UNPKG

@traxjs/trax

Version:

Reactive state management

641 lines (597 loc) 22.7 kB
/** * Object Id - can be either a string or an array that will be joined to produce a unique id * e.g. ["foo",42] -> will generate id "foo:42" if used on the root store or "contextid/foo:42" * if used on a sub-store * Note: the array can also contain another trax object in the first position - in this case the object id will be used as prefix */ export type TraxIdDef = string | (string | number | boolean | TraxObject)[]; export type TraxObject = Object; export type StoreWrapper = { readonly id: string; dispose: () => void; } /** * Trax object types */ export enum TraxObjectType { NotATraxObject = "", Object = "O", Array = "A", Store = "S", Processor = "P" } export interface Trax { /** * Create a root store * @param id the store id * @param initFunction the function that will be called to initialize the store. This function must * define the store "root" object otherwise an error will be generated */ createStore<T extends Object, R>( id: TraxIdDef, initFunction: (store: Store<T>) => R ): R extends void ? Store<T> : R & StoreWrapper; /** * Create a root store * @param id the store id * @param data the root object to initialize the store */ createStore<T extends Object>(id: TraxIdDef, data: T): Store<T>; /** * The trax event logs */ log: EventStream; /** * Tell if an object is a trax object */ isTraxObject(obj: any): boolean; /** * Get the unique id associated to a trax object * Return an empty string if the object is not a trax object * @param obj */ getTraxId(obj: any): string; /** * Get the trax type associated to an object * @param obj */ getTraxObjectType(obj: any): TraxObjectType; /** * Get a processor from its id * @param id */ getProcessor(id: string): TraxProcessor | void; /** * Retrieve a store from its id. Note: this method returns the internal trax store object, * not the store API that may be returned by createStore * @param id */ getStore<T>(id: string): Store<T> | void; /** * Get a trax data object (object / array or dictionary). Note: only objects that have already been * accessed can be returned (otherwise their id is not yet defined) * @param id */ getData<T>(id: string): T | void; /** * Return the processor that is being computing (if getActiveProcessor() is called in a compute call stack). * Return undefined otherwise. * @seealso componentId in trax-react */ getActiveProcessor(): TraxProcessor | void; /** * Tell if some changes are pending (i.e. dirty processors) * Return false if there are no dirty processors - which means that all computed values * can be safely read with no risks of invalid value */ readonly pendingChanges: boolean; /** * Process the pending changes - i.e. run the dirty processors dependency chain * This function will be automatically asynchronously called at the end of each trax cycle * but it can be also explictly called if a synchronous behaviour is required */ processChanges(): void; /** * Get a promise that will be fulfilled when trax reconciliation is complete * (i.e. at the end of the current cycle) * If there is no cycle on-going, the promise will be immediately fulfilled */ reconciliation(): Promise<void>; /** * Helper function to update the content of an array without changing its reference * Must be used in processors generating computed array collections * Note: this method will also flag the array as computed and will ensure errors are raised * if changes are made outside this processor * @param array the array to update * @param newContent the new array content */ updateArray(array: any[], newContent: any[]): void; /** * Helper function to update the content of a dictionary without changing its reference * Must be used in processors generating computed dictionary collections * Note: this method will also flag the dictionary as computed and will ensure errors are raised * if changes are made outside this processor * @param dict the dictionary to update * @param newContent the new dictionary content */ updateDictionary<T>(dict: { [k: string]: T }, newContent: { [k: string]: T }): void; /** * Wrapper around Object.keys() that should be used in processors * that read objects as dictionaries. This will allow processors to get dirty when * properties are added or removed * @param o */ getObjectKeys(o: TraxObject): string[]; } /** * Trax Store * Gather trax objects in a same namespace (i.e. objects, arrays, dictionaries, processors and stores). * Allow to : * - create multiple instances of the same store type with no risks of id collisions * - manage local ids instead of global ids * - simplify troubleshooting * - easily dispose group of objects to make them ready for garbage collection */ export interface Store<T> { /** * The store id */ readonly id: string; /** * Store root data object * All objects, arrays and dictionaries that are not reachable through this object will be * automatically garbage-collected */ readonly data: T, /** * Initialize the root data object - must be only called in the store init function * @param contentProcessors optional compute functions associated to the root object. The processor associated to these functions will follow the object life cycle. * @param data */ init(data: T, contentProcessors?: TraxLazyComputeDescriptor<T>): T; /** * Tell if the store is disposed and should be ignored */ readonly disposed: boolean; /** * Create a sub-store * @param id the store id * @param initFunction the function that will be called to initialize the store. This function must * define the store "root" data object otherwise an error will be generated */ createStore<T extends Object, R>( id: TraxIdDef, initFunction: (store: Store<T>) => R ): R extends void ? Store<T> : R & StoreWrapper; /** * Create a sub-store * @param id the store id * @param data the root data object to initialize the store */ createStore<T extends Object>(id: TraxIdDef, data: T): Store<T>; /** * Retrieve a sub-store * @param id */ getStore<T>(id: TraxIdDef): Store<T> | void; /** * Get or create a data object associated to the given id * @param id the object id - must be unique with the store scope * @param initValue the object init value (empty object if nothing is provided) * @param contentProcessors optional compute functions associated to this object. The processor associated to these functions will follow the object life cycle. */ add<T extends Object | Object[]>(id: TraxIdDef, initValue: T, contentProcessors?: TraxLazyComputeDescriptor<T>): T; /** * Retrieve a data object/array/dictionary that has been previously created * (Doesn't work for processors or stores) * Note: if this object is not indirectly referenced by the root data object, it may habe been garbage collected * @returns the tracked object or undefined if not found */ get<T extends Object>(id: TraxIdDef): T | void; /** * Delete a data object from the store * @param idOrObject * @returns true if an object was successfully deleted */ remove<T extends Object>(dataObject: T): boolean; /** * Create or retrieve an **eager** compute processor (eager processors are always called even if the data they compute are not read). These processors may be **synchronous** or **asynchronous** (cf. $TraxComputeFn) * If a processor with the same id is found, it will be returned instead of creating a new one * but its compute function will be updated in order to benefit from new closure values that may not exist * in the previous function. * @param id the processor id - must be unique with the store scope * @param compute the compute function * @param autoCompute if true (default) the processor will be automatically called after getting dirty. * (i.e. at the end of a cycle when trax.processChanges() is called) * If false, the process function will need to be explicitely called (useful for React renderers for instance) */ compute(id: TraxIdDef, compute: TraxComputeFn, autoCompute?: boolean, isRenderer?: boolean): TraxProcessor; /** * Retrieve a processor created on this store * @param id */ getProcessor(id: TraxIdDef): TraxProcessor | void; /** * Dispose the current store and all its sub-stores and processor * so that they can be garbage collected */ dispose(): boolean; /** * Create an async function from a generator function * in order to have its logs properly tracked in the trax logger * This is meant to be used in store wrapper objects to expose action functions * @param fn */ async<F extends (...args: any[]) => Generator<Promise<any>, any, any>>(fn: F): (...args: Parameters<F>) => Promise<any>; /** * Create an async function from a generator function * in order to have its logs properly tracked in the trax logger * This can be used to define an async block that will be called asychronously (e.g. store async initialization) * @param name the name of the function as it should appear in the logs * @param fn */ async<F extends (...args: any[]) => Generator<Promise<any>, any, any>>(name: string, fn: F): (...args: Parameters<F>) => Promise<any>; } /** * Context passed to compute functions. * Allows to stop a processor after a certain amount of counts */ export interface TraxComputeContext { readonly processorId: string; readonly processorName: string; readonly computeCount: number; maxComputeCount: number; } /** * Trax compute function * Define the processing instructions associated to a processor * Asynchronous compute functions must return a Generator, whereas synchronous * compute functions shall not return anything */ export type TraxComputeFn = (cc: TraxComputeContext) => (void | Generator<Promise<any>, void, any>); /** * Trax object compute function * Create a processor associated to an object. The processor will be automatically disposed when the object is disposed. * Asynchronous compute functions must return a Generator, whereas synchronous * compute functions shall not return anything */ export type TraxObjectComputeFn<T> = (o: T, cc: TraxComputeContext) => (void | Generator<Promise<any>, void, any>); /** * Trax object compute descriptor * Allows to add extra arguments to a compute function */ export interface TraxObjectComputeDescriptor<T> { /** Processor name (default = argument index in the store.add() call */ processorName?: string; /** Compute function */ compute: TraxObjectComputeFn<T>; } /** * Ordered map of lazy compute processors associated to a trax object */ export interface TraxLazyComputeDescriptor<T> { [computeName: string]: TraxObjectComputeFn<T>; } /** * Processor id */ export type TraxProcessorId = string; /** * Trax processor * This object track the dependencies of its compute function and will automatically * re-call the compute function in case of dependency changes */ export interface TraxProcessor { readonly id: TraxProcessorId; /** * Tell if the processor should automatically re-run the compute function * when it gets dirty or not (in which case the processor creator should use * the onDirty callback and eventually call compute() explicitely) */ readonly autoCompute: boolean; /** * Processor priority - tell how/when this processor should be called * compared to other processors (in practice priority = creation order) */ readonly priority: number; /** * Compute count - tell how many times the processor compute function was called */ readonly computeCount: number; /** * Tell if the processor is dirty (following a dependency update) and must be reprocessed. */ readonly dirty: boolean; /** * Tell if the processor was labeled as a renderer (debug info) */ readonly isRenderer: boolean; /** * Tell if the processor is lazy (name must start with "~" and must be associated to a target object) */ readonly isLazy: boolean; /** * Tell if the processor is disposed and should be ignored */ readonly disposed: boolean; /** Get the processor current dependencies */ readonly dependencies: string[]; /** * Callback to call when the processor value gets dirty * This callback is called synchronously, right after the processor gets dirty * Only one callback can be defined */ onDirty: (() => void) | null; /** * Execute the compute function if the processor is dirty * @param forceExecution if true compute will be exececuted event if processor is not dirty */ compute(forceExecution?: boolean): void; /** * Dispose the current processor to stop further compute and * have it garbage collected */ dispose(): boolean; } /** * Trax event types * Internal code start with "!" to avoid collisions with external events * (not an enum to avoid potential minifier issues) */ export const traxEvents = Object.freeze({ /** When info data are logged */ "Info": "!LOG", /** When a warning is logged */ "Warning": "!WRN", /** When an error is logged */ "Error": "!ERR", /** When a cycle is created */ "CycleStart": "!CS", /** When a cycle ends */ "CycleComplete": "!CC", /** When a trax entity is created (e.g. object / processor / store) */ "New": "!NEW", /** When a trax entity is disposed (e.g. object / processor / store) */ "Dispose": "!DEL", /** When an object property is set (changed) */ "Set": "!SET", /** When an object property is read */ "Get": "!GET", /** When a processor is set dirty */ "ProcessorDirty": "!DRT", /** When a lazy processor is skipped */ "ProcessorSkipped": "!SKP", /** When a processing context starts */ "ProcessingStart": "!PCS", /** When an async processing context pauses */ "ProcessingPause": "!PCP", /** When an async processing context resumes */ "ProcessingResume": "!PCR", /** When a processing context ends */ "ProcessingEnd": "!PCE" }); export const traxEventTypes = new Set<string>(); Object.getOwnPropertyNames(traxEvents).forEach((name) => { traxEventTypes.add((traxEvents as any)[name]); }); export type TraxEvent = TraxLogMsg | TraxLogObjectLifeCycle | TraxLogPropGet | TraxLogPropSet | TraxLogProcDirty | TraxLogProcSkipped; /** Reason that triggered a call to a processor's compute function */ export type TraxComputeTrigger = "Init" | "Reconciliation" | "DirectCall" | "TargetRead"; export type TraxLogEvent = TraxLogMsg | TraxLogCycle | TraxLogObjectLifeCycle | TraxLogPropSet | TraxLogPropGet | TraxLogProcDirty | TraxLogTraxProcessingCtxt | TraxLogProcessingCtxtEvent; export type TraxLogObjectLifeCycle = TraxLogObjectCreate | TraxLogObjectDispose; export interface TraxLogObjectCreate { type: "!NEW"; objectId: string; objectType?: TraxObjectType } export interface TraxLogObjectDispose { type: "!DEL"; objectId: string; } export interface TraxLogPropGet { type: "!GET"; objectId: string; propName: string; propValue: any; } export interface TraxLogPropSet { type: "!SET"; objectId: string; propName: string; fromValue: any; toValue: any; } export interface TraxLogProcDirty { type: "!DRT"; processorId: string; /** Object holding the value that triggered the dirty event */ objectId: string; propName: string; } export interface TraxLogProcSkipped { type: "!SKP"; processorId: string; } export interface TraxLogMsg { type: "!LOG" | "!WRN" | "!ERR"; data?: JSONValue; } export interface TraxLogCycle { type: "!CS" | "!CC"; elapsedTime: number; } export interface TraxLogProcessingCtxtEvent { type: "!PCS" | "!PCP" | "!PCR" | "!PCE"; data?: JSONValue; } export type TraxLogTraxProcessingCtxt = TraxLogProcessStoreInit | TraxLogProcessCompute | TraxLogReconciliation | TraxLogCollectionUpdate; export interface TraxLogProcessStoreInit { type: "!PCS" | "!PCE"; name: "!StoreInit"; storeId: string; } export interface TraxLogProcessCompute { type: "!PCS" | "!PCP" | "!PCR" | "!PCE"; name: "!Compute"; processorId: string; processorPriority: number; trigger: TraxComputeTrigger; isRenderer: boolean; computeCount: number; } export interface TraxLogCollectionUpdate { type: "!PCS" | "!PCE"; name: "!ArrayUpdate" | "!DictionaryUpdate"; objectId: string; } export interface TraxLogReconciliation { type: "!PCS" | "!PCE"; name: "!Reconciliation"; /** Counter incremented everytime a reconciliation runs */ index: number; /** Number of active processors when a reconciliation starts (allows to track memory leaks) */ processorCount: number; } /** JSON type */ export type JSONValue = string | number | boolean | null | { [key: string]: JSONValue } | Array<JSONValue>; /** * Data type that can be used in logs * Must be a valid parameter for JSON.stringify() */ export type LogData = JSONValue; /** * Log Event */ export interface StreamEvent { /** * Unique id composed of 2 numbers: cycle id and event count * e.g. 42:12 where 42 is the cycle index and 12 the event count within cycle #42 */ id: string; /** Event type - allows to determine how to interprete data */ type: string; /** Event data - JSON stringified */ data?: string; /** Id of another event that the current event relates to */ parentId?: string; } /** * Log entry in the log stream */ export interface StreamListEvent extends StreamEvent { next?: StreamListEvent; }; /** * Start a processing context that allows to virtually group events together * The processing context can be either synchronous or asynchronous * If synchronous, the end() method is expected to be called before the end of the current cycle * If asynchronous, the pause() or the end() methods are expected to be called before the end of the current cycle * If pause() is called, resume() may be called into another cycle (until end() is eventually called - but this * is not mandatory as the async processing could be stopped) */ export interface ProcessingContext { id: string; /** Raise a pause event in the event stream */ pause(): void; /** Raise a resume event in the event stream */ resume(): void; /** Raise an end event in the event stream */ end(): void; } export type ProcessingContextData = { name: string, id?: string } & { [key: string]: JSONValue }; export type SubscriptionId = Object; /** * Tell how logs should be logged on the console. * Useful in jest/vitest environments where dev tools are not available * Possible values: * - "": no output * - "Main": most significant events (writes + explicit logs + dirty changes + re-processing) * - "AllButGet": log all events except Cycle Start/End and Property Getters * - "All": log all events except Cycle Start/End */ export type ConsoleOutput = "" | "Main" | "AllButGet" | "All"; export interface EventStream { /** * Log an event * @param type unique event type - e.g. "namespace.name", cannot start with "!" (reserved for trax events) * @param data event data - must support JSON.stringify * @param src optional event source - used for internal events only */ event(type: string, data?: LogData, src?: any): void; /** * Log info data in the trax logs * @param data */ info(...data: LogData[]): void; /** * Log warning data in the trax logs * @param data */ warn(...data: LogData[]): void; /** * Log error data in the trax logs * @param data */ error(...data: LogData[]): void; /** * Create a processing context and raise a start event in the event stream * Processing contexts are used to virtually regroup events that occur in a given context * Processing contexts can be stacked * @param data data associated with the processing context. Must contain a name (e.g. process name) * and may contain an id (useful for awaitEvent()) */ startProcessingContext(data: ProcessingContextData, src?: any): ProcessingContext; /** * Number of items in the stream */ size: number; /** * Stream max size * Use -1 to specify no limits * Otherwise minimum size will be 2 * (Default: 1000) */ maxSize: number; /** * Tell if logs should be logged on the console. */ consoleOutput: ConsoleOutput; /** * Scan all current entries in the log stream * (oldest to newest) * @param eventProcessor the function called for each event - can return false to stop the scan */ scan(eventProcessor: (itm: StreamEvent) => void | boolean): void; /** * Return the last event added to the stream */ lastEvent(): StreamEvent | undefined; /** * Await a certain event. Typical usage: * await log.await(traxEvents.CycleComplete); * @param eventType * @param targetData [optional] value or fields that should be matched against the event data (depends on the event type) */ awaitEvent(eventType: string | "*", targetData?: string | number | boolean | Record<string, string | number | boolean | RegExp>): Promise<StreamEvent>; /** * Register an event consumer that will be synchronously called when a given event occurs * @param eventType an event type or "*" to listen to all events * @param callback * @returns a subscribtion id that will be used to unsubscribe */ subscribe(eventType: string | "*", callback: (e: StreamEvent) => void): SubscriptionId; /** * Unregister an event consumer * @param subscriptionId * @returns true if the consumer was found and succesfully unregistered */ unsubscribe(subscriptionId: SubscriptionId): boolean; }