UNPKG

@traxjs/trax

Version:

Reactive state management

1,144 lines (1,071 loc) 44.6 kB
import { createEventStream } from "./eventstream"; import { wrapFunction } from "./functionwrapper"; import { LinkedList } from "./linkedlist"; import { TraxInternalProcessor, createTraxProcessor } from "./processor"; import { Store, StoreWrapper, Trax, TraxIdDef, TraxProcessor, TraxObjectType, TraxLogTraxProcessingCtxt, traxEvents, TraxComputeFn, TraxEvent, ProcessingContext, TraxProcessorId, TraxObject, TraxObjectComputeFn, TraxLazyComputeDescriptor } from "./types"; /** Symbol used to attach meta data to trax objects */ const metaData = new WeakMap<object, TraxMd>(); // note: the key is the actual object, not its proxy /** Symbol used to retrieve a proxy target */ const traxProxyTarget = Symbol("trax.proxy.target"); /** Symbol used to attach the Object.keys() size to objects used as dictionaries - cf. getObjectKeys() */ const dictSize = Symbol("trax.dict.size"); const RX_INVALID_ID = /(\/|\>|\.|\#)/g; const ROOT = "data"; /** Separator used to join array id definitions */ const ID_SEPARATOR1 = ":"; /** Separator used to create automatic ids while navigating JSON objects */ const ID_SEPARATOR2 = "*"; /** Separator used to replace "/" when creating ids from other ids */ const ID_SEPARATOR3 = "-"; /** Separator used to append a unique counter to dedupe a generated id that already exists */ const ID_SEPARATOR4 = "$"; /** Property prefix for reference properties */ const REF_PROP_PREFIX = "$"; /** Separator for processor ids */ const ID_PROCESSOR_SEPARATOR = "#"; /** Separator for sub-store ids */ const ID_SUB_STORE_SEPARATOR = ">"; /** Separator for data objects */ const ID_DATA_SEPARATOR = "/"; /** Pseudo property name to track dict size */ const DICT_SIZE_PROP = '☆trax.dictionary.size☆'; /** * Meta-data object attached to each trax object (object, array, dictionary, processor, store) */ interface TraxMd { /** The trax unique id */ id: string; /** The object type */ type: TraxObjectType; /** Store that the object belongs to. Empty string for root stores */ storeId: string; /** * Used by data objects (objects / array / dictionary) to track processors that have a dependency on their properties */ propListeners?: Set<TraxInternalProcessor>; /** * Tell if the object has listeners outside the contentProcessors */ hasExternalPropListener: boolean; /** * Tell when the lazy processors are being checked */ processingLazyCheck: boolean; /** * Map of computed properties and their associated processor * Allows to detect if a computed property is illegaly changed */ computedProps?: { [propName: string]: TraxProcessorId | undefined }; /** * Property used when the trax object is a collection and its content is set by a processor * through updateArray or updateDictionary * In this case computedProps should be undefined */ computedContent?: TraxProcessorId; /** * Content processors associated to this object (can be lazy or eager) * These processors are created through store.add() or store.init() and will be disposed * when the object is removed from the store */ contentProcessors?: TraxInternalProcessor[]; /** * Auto-wrap level / Ref props computation * Tell how sub-objects properties should be handled and if sub-objects should be automatcially wrapped or if * they should be considered as references * If level is 1, properties will be considered as references and won't be wrapped * If level is >1, wrapped properties will be passed a decremented level (e.g. x-1 if x is the current level) * Default = 0 (sub-object will be wrapped) */ awLevel?: number; /** * Number of properties set on an object - only used for objects used as dictionaries */ dictSize?: number; } /** * Register a new processor as listener * @param p the processor to register * @param md the meta data object to update (update will be ignored if not provided) */ export function registerMdPropListener(p: TraxInternalProcessor, md?: TraxMd) { if (md) { if (!md.propListeners) { md.propListeners = new Set(); } md.propListeners.add(p); if (md.contentProcessors !== undefined && !md.hasExternalPropListener) { if ((!p.target || tmd(p.target) !== md)) { md.hasExternalPropListener = true; } } } } /** * Unregister a processor from the meta-data listener * @param p the processor to register * @param md the meta data object to update (update will be ignored if not provided) */ export function unregisterMdPropListener(p: TraxInternalProcessor, md?: TraxMd) { if (md && md.propListeners) { md.propListeners.delete(p); if (md.propListeners.size === 0) { md.propListeners = undefined; md.hasExternalPropListener = false; } else if (md.contentProcessors) { let hasExternalPropListener = false; for (const ln of md.propListeners) { if (!hasExternalPropListener && (!ln.target || tmd(ln.target) !== md)) { hasExternalPropListener = true; } } md.hasExternalPropListener = hasExternalPropListener; } else { md.hasExternalPropListener = false; } } } /** * Get the trax meta data associated to an object * @param o * @returns */ export function tmd(o: any): TraxMd | undefined { return o ? metaData.get((o as any)[traxProxyTarget] || o) : undefined; } /** * Create and attach meta data to a given object */ function attachMetaData(o: Object, id: string, type: TraxObjectType, storeId: string): TraxMd { const md: TraxMd = { id, type, storeId, propListeners: undefined, computedProps: undefined, computedContent: undefined, awLevel: undefined, dictSize: undefined, contentProcessors: undefined, hasExternalPropListener: false, processingLazyCheck: false }; metaData.set((o as any)[traxProxyTarget] || o, md); return md; } /** * Detach meta data (called on dispose) */ function detachMetaData(o: Object) { metaData.delete((o as any)[traxProxyTarget] || o); } /** * Create a trax environment. This function must only be used in test environments: * applications must use the global trax object instead * @see trax */ export function createTraxEnv(): Trax { /** Private key to authorize reserved events in the log stream */ const privateEventKey = {}; /** counter used to de-dupe auto-generated ids */ let dupeCount = 0; /** Global map containing all stores */ const stores = new Map<string, Store<any>>(); /** Global map containing weakrefs to all data */ const dataRefs = new Map<string, WeakRef<any>>(); /** Global processor map */ const processors = new Map<string, TraxInternalProcessor>(); /** Global map containing trax object ids per store */ const storeItems = new Map<string, Set<string>>(); const isArray = Array.isArray; /** Count of processors that have been created - used to set processor priorities */ let processorPriorityCounter = 0; /** Total number of active processors */ let processorCount = 0; /** Call stack of the processors in compute mode */ const processorStack = new LinkedList<TraxInternalProcessor>(); /** Reconciliation linked list: list of processors that need to be reconciled */ let reconciliationList = new LinkedList<TraxInternalProcessor>(); /** Count the number of reconciliation executions */ let reconciliationCount = 0; /** Log stream */ const jsonReplacer = (d: any) => { return JSON.stringify(d, (key: string, value: any) => { if (typeof value === "object") { const md = tmd(value); return md ? `[TRAX ${md.id}]` : value; } else if (typeof value === "function") { return "[Function]"; } return value; }); } const log = createEventStream(privateEventKey, jsonReplacer, () => { connectToDevTools(); trx.processChanges(); }); /** Tell if devtools have been detected and trax was registered on client proxy */ let devToolsDetected = false; const trx = { log, createStore<R, T extends Object>( idPrefix: TraxIdDef, initFunctionOrRoot: Object | ((store: Store<T>) => R) ): R extends void ? Store<T> : R & StoreWrapper { return createStore(idPrefix, "", initFunctionOrRoot); }, get pendingChanges() { return reconciliationList.size > 0; }, processChanges(): void { // reconciliation if (reconciliationList.size > 0) { reconciliationCount++; const recLog = startProcessingContext({ type: "!PCS", name: "!Reconciliation", index: reconciliationCount, processorCount }); let p = reconciliationList.shift(); while (p) { if (p.reconciliationId === reconciliationCount) { error(`(${p.id}) Circular reference: Processors cannot run twice during reconciliation`); } else { p.compute(false, "Reconciliation", reconciliationCount); } p = reconciliationList.shift(); } recLog.end(); } }, async reconciliation(): Promise<void> { let lastEvent = log.lastEvent(); if (!lastEvent || lastEvent.type === traxEvents.CycleComplete) { // some async promises may be pending await Promise.resolve(1); } // wait for the end of the current cycle if a cycle already started // (reconcilation will be automatically triggered at the end of the cycle) if (reconciliationList.size || (lastEvent && lastEvent.type !== traxEvents.CycleComplete)) { await log.awaitEvent(traxEvents.CycleComplete); } }, isTraxObject(obj: any): boolean { return tmd(obj) !== undefined; }, getTraxId(obj: any): string { return tmd(obj)?.id || ""; }, getTraxObjectType(obj: any): TraxObjectType { return tmd(obj)?.type || TraxObjectType.NotATraxObject; }, getProcessor(id: TraxProcessorId): TraxProcessor | void { return processors.get(id); }, getStore<T>(id: string): Store<T> | void { return stores.get(id); }, getData<T>(id: string): T | void { return getDataObject(id) || undefined; }, getActiveProcessor(): TraxProcessor | void { return processorStack.peek(); }, updateArray(array: any[], newContent: any[]) { if (!isArray(array) || !isArray(newContent)) { error(`updateArray: Invalid argument (array expected)`); return; } const id = checkComputedContent(array); const ctxt = startProcessingContext({ type: traxEvents.ProcessingStart, name: "!ArrayUpdate", objectId: id }); const len1 = array.length; const len2 = newContent.length; for (let i = 0; len2 > i; i++) { array[i] = newContent[i]; } if (len2 < len1) { for (let i = len2; len1 > i; i++) { // explicitely set items to undefined to notify potential listeners array[i] = undefined; } array.splice(len2, len1 - len2); } ctxt.end(); }, updateDictionary<T>(dict: { [k: string]: T }, newContent: { [k: string]: T }): void { if (dict === null || typeof dict !== "object") { error(`updateDictionary: Invalid argument (object expected)`); return; } const id = checkComputedContent(dict); const ctxt = startProcessingContext({ type: traxEvents.ProcessingStart, name: "!DictionaryUpdate", objectId: id }); const oldContentKeys = trx.getObjectKeys(dict); const newContentKeys = trx.getObjectKeys(newContent); // delete values that are not in newContent for (const k of oldContentKeys) { if (!newContentKeys.includes(k)) { delete dict[k] } } // create or update values in newContent for (const k of newContentKeys) { dict[k] = newContent[k]; } ctxt.end(); }, getObjectKeys(o: TraxObject): string[] { const md = tmd(o); if (!md) { return Object.keys(o); } else { // force read of dictSize to create a dependency on this property const sz: number = (o as any)[dictSize]; if (sz) return Object.keys(o); return []; } } } function checkComputedContent(o: Object) { const md = tmd(o); if (md) { sanitizeComputedMd(md); const pr = processorStack.peek() if (pr) { if (!md.computedContent) { md.computedContent = pr.id; } else { // illegal change unless the previous processor has been disposed if (pr.id !== md.computedContent) { error(`Computed content conflict: ${md.id} can only be changed by ${md.computedContent}`); } } } return md.id; } return ""; } const proxyHandler = { /** * Function called on each property get * @param target the original object that is proxied * @param prop the property name */ get(target: any, prop: string | symbol) { const md = tmd(target); if (prop === traxProxyTarget) { // pseudo-property used to retrieve the target behind a proxy return target; } else if (prop === "toJSON") { // JSON.stringify call on a proxy will get here return undefined; } else if (typeof prop === "string") { let v: any; let addLog = false; if (md) { // register dependency const pr = processorStack.peek(); if (pr) { pr.registerDependency(target, md.id, prop); } checkLazyProcessors(md); // don't add log for common internal built-in props addLog = ((prop !== "then" && prop !== "constructor") || v !== undefined); if (target[prop] !== undefined) { // don't set the value if undefined (otherwise it may create items in arrays) v = target[prop] = wrapPropObject(target[prop], prop, md); } addLog && logTraxEvent({ type: "!GET", objectId: md.id, propName: prop as string, propValue: v }); } else { v = target[prop]; } return v; } else if (prop === dictSize) { if (md) { let v = md.dictSize; if (v === undefined) { // first time md.dictSize = v = Object.keys(target).length; } const pr = processorStack.peek(); if (pr) { pr.registerDependency(target, md.id, DICT_SIZE_PROP); } checkLazyProcessors(md); logTraxEvent({ type: "!GET", objectId: md.id, propName: DICT_SIZE_PROP, propValue: v }); return v; } } return target[prop]; }, /** * Function called on each property set * @param target the original object that is proxied * @param prop the property name * @param value the value */ set(target: any, prop: string | number | symbol, value: any) { if (typeof prop !== "symbol") { const v = target[prop]; const md = tmd(target); if (md) { // Register computed props const pr = processorStack.peek(); sanitizeComputedMd(md); if (pr) { let prId = md.computedContent || undefined; // the current prop is computed: // - either it is an independent prop // - or it is part of a computed collection if (!md.computedContent) { // this is a computed property let computedProps = md.computedProps; if (!computedProps) { computedProps = md.computedProps = {}; } prId = computedProps[prop]; if (!prId) { computedProps[prop] = pr.id; } } if (prId) { // a processor is already defined for the current property if (prId !== pr.id) { // illegal change unless the previous processor has been disposed if (md.computedContent) { error(`Computed content conflict: ${md.id}.${prop} can only be set by ${prId}`); } else { error(`Computed property conflict: ${md.id}.${prop} can only be set by ${prId}`); } value = v; } } } else { // not in processor stack if (md.computedContent) { error(`Computed content conflict: ${md.id}.${prop} can only be set by ${md.computedContent}`); value = v; } } if (v !== value) { let lengthChange = false, dictSizeChange = false; if (isArray(target)) { // we need to notify length change as functions like Array.push won't explicitely do it const len = target.length; value = target[prop as any] = wrapPropObject(value, "" + prop, md); lengthChange = target.length !== len; } else { value = target[prop] = wrapPropObject(value, "" + prop, md); // Compute dictSize if object is used as a dictionary const dictSize1 = md.dictSize; if (v === undefined && dictSize1 !== undefined) { const dictSize2 = Object.keys(target).length; if (dictSize2 !== dictSize1) { md.dictSize = dictSize2; dictSizeChange = true; } } } logTraxEvent({ type: "!SET", objectId: md.id, propName: prop as string, fromValue: v, toValue: value }); if (typeof prop === "string") { notifyPropChange(md, prop); } if (lengthChange) { notifyPropChange(md, "length"); } if (dictSizeChange) { notifyPropChange(md, DICT_SIZE_PROP); } } } else { // object is disposed target[prop] = value; } } return true; }, /** * Proxy handler method called when a property is deleted through the delete operator * @param target * @param prop */ deleteProperty(target: any, prop: string | symbol): boolean { if (typeof prop === "string" && prop in target) { const md = tmd(target); if (md && md.dictSize) { // prop is in target so we can safely decrease dictSize md.dictSize--; notifyPropChange(md, DICT_SIZE_PROP); } delete target[prop]; return true; } return false; } }; /** * Auto-wrap a trax object property into a sub trax object */ function wrapPropObject(v: any, propName: string, targetMd: TraxMd) { if (v !== null && v !== undefined && typeof v === "object") { if (v[traxProxyTarget]) return v; // v is already a proxy // automatically wrap sub-objects let vmd = tmd(v); if (vmd) return v; // already wrapped // determine auto-wrap-level - if 1, direct properties must not be wrapped let awLevel = 0; // default = wrap if (propName[0] === REF_PROP_PREFIX) { // e.g. $myProp -> awLevel=1 / $$$myArray -> awLevel=3 let idx = 0, plen = propName.length; while (idx < plen) { if (propName[idx] === REF_PROP_PREFIX) { awLevel++; } else { break; } idx++; } } else if (targetMd.awLevel) { awLevel = targetMd.awLevel; } if (awLevel !== 1) { // let's autowrap if (!vmd) { // this value object is not wrapped yet v = getProxy(targetMd.id + ID_SEPARATOR2 + propName, v, targetMd.storeId, true); if (awLevel && awLevel > 1) { // propagate awLevel to child md let vmd = tmd(v); if (vmd && !vmd.awLevel) { vmd.awLevel = awLevel - 1; } } } } } return v; } function getProxy(id: string, obj: any, storeId: string, generateNewId = false) { const p = getDataObject(id); if (p) { // generateId is false when called from get() or getArray() // but true when called from getters (to generate proxies for sub-properties/sub-objects) if (obj && generateNewId) { // a new id must be generated because this is a new object // but an item with this id already exists // so we have to create a unique id thanks to the dupeCount counter const initId = id; while (true) { dupeCount++; id = initId + ID_SEPARATOR4 + dupeCount; if (!dataRefs.get(id)) { break; } } } else { return p; } } // create a new proxy let md: TraxMd; if (isArray(obj)) { // TODO md = attachMetaData(obj, id, TraxObjectType.Array, storeId); } else { md = attachMetaData(obj, id, TraxObjectType.Object, storeId); } logTraxEvent({ type: "!NEW", objectId: id, objectType: md.type }); const prx = new Proxy(obj, proxyHandler); storeDataObject(id, prx, storeId); return prx; } return trx; function registerProcessorForReconciliation(pr: TraxInternalProcessor) { const prio = pr.priority; const isRenderer = pr.isRenderer; reconciliationList.insert((prev?: TraxInternalProcessor, nd?: TraxInternalProcessor) => { if (!nd) { return pr; } else { if (isRenderer) { if (nd.isRenderer) { // both are renderers return prio <= nd.priority ? pr : undefined; } // else nd is not a renderer -> move next } else { if (nd.isRenderer) { return pr; // renderers go last } else { // both are not renderers return prio <= nd.priority ? pr : undefined; } } } }); } function buildIdSuffix(id: TraxIdDef, storeId?: string) { let suffix = ""; if (isArray(id)) { suffix = id.map(item => { if (typeof item === "object") { const md = tmd(item); if (!md) { error(`Invalid id param: not a trax object`); return getRandomId(); } else { const tid = md.id; if (storeId) { const slen = storeId.length + 1; if (tid.length > slen && tid.slice(0, slen) === storeId + "/") { // same store: return suffix return tid.slice(slen); } } // different store: replace "/" return tid.replace(/\//g, ID_SEPARATOR3); } } return "" + item; }).join(ID_SEPARATOR1); } else { suffix = id; } if (suffix.match(RX_INVALID_ID)) { const newSuffix = suffix.replace(RX_INVALID_ID, ""); error(`Invalid trax id: ${suffix} (changed into ${newSuffix})`); suffix = newSuffix; } return suffix; } function getRandomId(): string { return "" + Math.floor(Math.random() * 100000); } function buildId(id: TraxIdDef, storeId: string, isProcessor: boolean) { let suffix = buildIdSuffix(id, storeId); if (isProcessor) return storeId + ID_PROCESSOR_SEPARATOR + suffix; return storeId + ID_DATA_SEPARATOR + suffix; } /** * Check that processor meta data are still valid * (processor may have been disposed and ids still references in md) * @param md * @param propName */ function sanitizeComputedMd(md?: TraxMd, propName?: string) { if (md) { if (needReset(md.computedContent)) { md.computedContent = undefined; } const cps = md.computedProps; if (cps && propName && needReset(cps[propName])) { cps[propName] = undefined; } } function needReset(id?: string) { // return true if change required if (id && !processors.has(id)) { return true; } return false; } } function error(msg: string) { log.error('[TRAX] ' + msg); } function logTraxEvent(e: TraxEvent) { if (e.type === traxEvents.Error) { error("" + e.data); } else if (e.type === traxEvents.Info) { log.info("" + e.data); } else { log.event(e.type, e as any, privateEventKey); } } function startProcessingContext(event: TraxLogTraxProcessingCtxt): ProcessingContext { return log.startProcessingContext(event as any, privateEventKey); } function notifyPropChange(md: TraxMd, propName: string) { // set processor dirty if they depend on this property const processors = md.propListeners; if (processors) { for (const pr of processors) { if (pr.notifyChange(md.id, propName)) { // this processor turned dirty registerProcessorForReconciliation(pr); } } } } /** * Check that content processors associated to the meta data are run */ function checkLazyProcessors(md: TraxMd) { if (md.processingLazyCheck) return; // run max once per reconciliation if (md.contentProcessors) { md.processingLazyCheck = true; for (const p of md.contentProcessors) { p.isLazy && p.compute(false, "TargetRead"); } md.processingLazyCheck = false; } } /** * DevTools connection - called at each reconcialiation as devtools proxy may not be present * when trax starts */ function connectToDevTools() { if (!devToolsDetected && (globalThis as any)["__TRAX_DEVTOOLS__"]) { devToolsDetected = true; console.log("[Trax] DevTools detected"); (globalThis as any)["__TRAX_DEVTOOLS__"].connectTrax(trx); } } /** * Return objects, arrays or dictionaries */ function getDataObject(id: string): any { const ref = dataRefs.get(id); if (ref) { return ref.deref() || null; } return null; } function storeDataObject(id: string, data: Object, storeId: string) { dataRefs.set(id, new WeakRef(data)); addStoreItem(id, storeId); } function removeDataObject(id: string, o?: TraxObject, md?: TraxMd): boolean { if (o) { detachMetaData(o); } if (md) { if (md.contentProcessors) { // dispose associated processors for (const p of md.contentProcessors) { p.dispose(); } md.contentProcessors = undefined; } removeStoreItem(id, md.storeId); } logTraxEvent({ type: "!DEL", objectId: id }); return dataRefs.delete(id); } function addStoreItem(id: string, storeId: string) { let storeSet = storeItems.get(storeId); if (!storeSet) { storeSet = new Set<string>(); storeItems.set(storeId, storeSet); } storeSet.add(id); } function removeStoreItem(id: string, storeId: string) { let storeSet = storeItems.get(storeId); if (storeSet) { storeSet.delete(id); } } function createStore<R, T extends Object>( idPrefix: TraxIdDef, parentStoreId: string, initFunctionOrRoot: Object | ((store: Store<T>) => R), onDispose?: (id: string) => void ): R extends void ? Store<T> : R & StoreWrapper { const storeId = buildStoreId(idPrefix, parentStoreId, true); let data: any; let initPhase = true; let disposed = false; const storeInit = startProcessingContext({ type: "!PCS", name: "!StoreInit", storeId: storeId }); const initFunction = typeof initFunctionOrRoot === "function" ? initFunctionOrRoot : (store: Store<T>) => { store.init(initFunctionOrRoot as any); } const store: Store<T> = { get id() { return storeId; }, get data() { // root data should always be defined if initFunction is correctly implemented return data; }, get disposed(): boolean { return disposed; }, createStore<R, T extends Object>( id: TraxIdDef, initFunctionOrRoot: Object | ((store: Store<T>) => R) ): R extends void ? Store<T> : R & StoreWrapper { const st = createStore(id, storeId, initFunctionOrRoot, detachChildStore); addStoreItem(st.id, storeId); return st; }, init(r: T, lazyProcessors?: TraxLazyComputeDescriptor<T>) { if (initPhase) { data = getOrAdd(ROOT, r, true, lazyProcessors); } else { error(`(${storeId}) Store.init can only be called during the store init phase`); } return data; }, add<T extends Object | Object[]>(id: TraxIdDef, initValue: T, lazyProcessors?: TraxLazyComputeDescriptor<T>): T { return getOrAdd(id, initValue, false, lazyProcessors); }, get<T extends Object>(id: TraxIdDef): T | void { const sid = buildId(id, storeId, false); return getDataObject(sid) || undefined; }, remove<T extends Object>(o: T): boolean { const md = tmd(o); let id = ""; if (md) { id = md.id; if (md.type === TraxObjectType.Processor) { error(`(${id}) Processors cannot be disposed through store.remove()`); } else if (md.type === TraxObjectType.Store) { error(`(${id}) Stores cannot be disposed through store.remove()`); } else { const suffix = id.slice(storeId.length + 1); if (suffix === ROOT) { error(`(${id}) Root objects cannot be disposed through store.remove()`); } else { return removeDataObject(id, o, md); } } } return false; }, compute(id: TraxIdDef, compute: TraxComputeFn, autoCompute?: boolean, isRenderer?: boolean): TraxProcessor { const pname = buildIdSuffix(id, storeId); const pid = buildId(pname, storeId, true); return createProcessor(pid, pname, compute, null, autoCompute, isRenderer); }, getProcessor(id: TraxIdDef): TraxProcessor | undefined { const sid = buildId(id, storeId, true); return processors.get(sid) as any; }, getStore<T>(id: TraxIdDef): Store<T> | void { const subStoreId = buildStoreId(id, storeId, false); return stores.get(subStoreId); }, dispose(): boolean { return dispose(); }, async<F extends (...args: any[]) => Generator<Promise<any>, any, any>>( nameOrFn: string | F, fn?: F ): (...args: Parameters<F>) => Promise<any> { let name = "[ASYNC]"; let func: F; if (typeof nameOrFn === "string") { name = nameOrFn; func = fn!; } else { func = nameOrFn as F; } const f = wrapFunction( func, () => log.startProcessingContext({ name: storeId + "." + name + "()", storeId }), (ex) => { error(`(${storeId}.${name}) error: ${ex}`) } ); (f as any).updateAsyncName = (nm: string) => { name = nm; } return f as any; } }; // attach meta data attachMetaData(store, storeId, TraxObjectType.Store, ""); logTraxEvent({ type: "!NEW", objectId: storeId, objectType: TraxObjectType.Store }); // register store in parent stores.set(storeId, store); function dispose(): boolean { if (disposed) return false; stores.delete(storeId); disposed = true; // detach from parent store if (onDispose) { // onDispose is not provided for root stores onDispose(storeId); } const storeSet = storeItems.get(storeId); if (storeSet) { storeItems.delete(storeId); const len = storeId.length; let separator: string; let o: Store<any> | TraxInternalProcessor | undefined = undefined; for (const id of storeSet) { o = undefined; separator = id.charAt(len); if (separator === ID_SUB_STORE_SEPARATOR) { o = stores.get(id); o && o.dispose(); } else if (separator === ID_PROCESSOR_SEPARATOR) { o = processors.get(id); o && o.dispose(); if (processors.get(id)) { console.error("Unexpected processor dispose error"); processors.delete(id); } } else { // note: in this case we don't need to dispose processors associated // to this object (if any) as all store processsors will be disposed removeDataObject(id); } } storeSet.clear(); } logTraxEvent({ type: "!DEL", objectId: storeId }); return true; } function detachChildStore(id: string) { removeStoreItem(id, storeId); } function detachChildProcessor(id: string) { const ok = processors.delete(id); if (ok) { processorCount--; removeStoreItem(id, storeId); } } function wrapStoreAPIs(obj: Object) { const o = obj as any; for (const name of Object.keys(o)) { if (typeof o[name] === "function") { const fn = o[name]; if (typeof (fn as any).updateAsyncName === "function") { // this function was already wrapped through store.async() (fn as any).updateAsyncName(name); } else { o[name] = wrapFunction( fn, () => log.startProcessingContext({ name: storeId + "." + name + "()", storeId }), (ex) => { error(`(${storeId}.${name}) error: ${ex}`) } ); } } } } let r: R; try { r = initFunction(store); initPhase = false; if (r !== undefined) { if (r !== null && typeof r === "object") { wrapStoreAPIs(r); } else { error(`createStore init function must return a valid object (${storeId})`); r = {} as R; } } } catch (ex) { error(`createStore init error (${storeId}): ${ex}`); r = {} as R; } initPhase = false; checkRoot(); if (r === undefined) { // init function doesn't define any wrapper -> return the raw store object storeInit.end(); return store as any; } // wrap existing dispose if any const res = r as any; if (typeof res.dispose === 'function') { const originalDispose = res.dispose; res.dispose = () => { originalDispose.call(r); // already wrapped dispose(); } } else res.dispose = dispose; // add id property if (res.id) { error(`Store id will be overridden and must not be provided by init function (${storeId})`); } res.id = storeId; storeInit.end(); return res; function checkRoot() { if (data == undefined) { error(`(${storeId}) createStore init must define a root data object - see also: init()`); data = getOrAdd(ROOT, {}, true); } } function checkNotDisposed() { if (disposed) { error(`(${storeId}) Stores cannot be used after being disposed`); return false; } return true; } function buildStoreId(idPrefix: TraxIdDef, parentStoreId: string, makeUnique = true) { let storeId = buildIdSuffix(idPrefix); if (parentStoreId !== "") { storeId = parentStoreId + ID_SUB_STORE_SEPARATOR + storeId; } let suffix = ""; if (makeUnique) { let st = stores.get(storeId); let count = 0; while (st) { suffix = "" + (++count); st = st = stores.get(storeId + suffix); } } return storeId + suffix; } /** * Function behind store.add - support an extra argument to prevent ROOT id * @param id * @param o * @reeturns */ function getOrAdd<T extends Object>(id: TraxIdDef, o: T, acceptRootId: boolean, contentProcessors?: TraxLazyComputeDescriptor<T>): T { let idSuffix = buildIdSuffix(id, storeId); if (!acceptRootId) { if (idSuffix === ROOT) { error("Store.add: Invalid id 'data' (reserved)"); idSuffix = getRandomId(); } } if (checkNotDisposed()) { if (o === undefined || o === null || typeof o !== "object") { error(`(${storeId}) Store.add(${id}): Invalid init object parameter: [${typeof o}]`); o = {} as T; } const p = getProxy(buildId(idSuffix, storeId, false), o, storeId); if (contentProcessors !== undefined) { const md = tmd(p)!; const procs: TraxProcessor[] = []; for (const name of Object.getOwnPropertyNames(contentProcessors)) { const pid = `${storeId}${ID_PROCESSOR_SEPARATOR}${idSuffix}[${name}]`; procs.push(createProcessor(pid, name, contentProcessors[name], p, true, false)); } md.contentProcessors = procs as TraxInternalProcessor[]; } return p; } else { return o as any; } } function createProcessor<T>(pid: string, pname: string, compute: TraxComputeFn | TraxObjectComputeFn<T>, target: T | null, autoCompute?: boolean, isRenderer?: boolean): TraxProcessor { let pr = processors.get(pid); if (pr) { pr.updateComputeFn(compute); return pr; } processorPriorityCounter++; // used for priorities processorCount++; // used to track potential memory leaks pr = createTraxProcessor( pid, pname, processorPriorityCounter, compute, processorStack, getDataObject, logTraxEvent, startProcessingContext, target, detachChildProcessor, autoCompute, isRenderer ); if (!pr.disposed) { // run once processors can be already disposed attachMetaData(pr, pid, TraxObjectType.Processor, storeId); processors.set(pid, pr); addStoreItem(pid, storeId); } return pr; }; } }