@traxjs/trax
Version:
Reactive state management
1,144 lines (1,071 loc) • 44.6 kB
text/typescript
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;
};
}
}