UNPKG

dreamstate

Version:

Store management library based on react context and observers

1,186 lines (1,162 loc) 70.8 kB
'use strict'; var tslib = require('tslib'); var react = require('react'); var shallowEqual = require('shallow-equal'); /** * Error codes describing various issues related to Dreamstate usage. * These error codes are used to categorize different types of errors. * * @enum {string} */ exports.EDreamstateErrorCode = void 0; (function (EDreamstateErrorCode) { EDreamstateErrorCode["UNEXPECTED_ERROR"] = "UNEXPECTED_ERROR"; EDreamstateErrorCode["RESTRICTED_OPERATION"] = "RESTRICTED_OPERATION"; EDreamstateErrorCode["INSTANCE_DISPOSED_LIFECYCLE"] = "INSTANCE_DISPOSED_LIFECYCLE"; EDreamstateErrorCode["INSTANCE_DISPOSED_SCOPE"] = "INSTANCE_DISPOSED_SCOPE"; EDreamstateErrorCode["OUT_OF_SCOPE"] = "OUT_OF_SCOPE"; EDreamstateErrorCode["INCORRECT_PARAMETER"] = "INCORRECT_PARAMETER"; EDreamstateErrorCode["INCORRECT_SIGNAL_LISTENER"] = "INCORRECT_SIGNAL_LISTENER"; EDreamstateErrorCode["INCORRECT_SIGNAL_TYPE"] = "INCORRECT_SIGNAL_TYPE"; EDreamstateErrorCode["INCORRECT_QUERY_PROVIDER"] = "INCORRECT_QUERY_PROVIDER"; EDreamstateErrorCode["INCORRECT_QUERY_TYPE"] = "INCORRECT_QUERY_TYPE"; EDreamstateErrorCode["TARGET_CONTEXT_MANAGER_EXPECTED"] = "TARGET_CONTEXT_MANAGER_EXPECTED"; })(exports.EDreamstateErrorCode || (exports.EDreamstateErrorCode = {})); /** * Maps a Dreamstate error code to a human-readable error message. * * This function takes an error code and an optional detail string, returning a formatted * error message that provides more context about the error. It's useful for handling and * displaying Dreamstate-specific errors in a user-friendly way. * * @param {EDreamstateErrorCode} code - The error code representing the specific Dreamstate error. * @param {string} [detail] - An optional string providing additional details to be included * in the error message. * @returns {string} A formatted error message based on the provided error code and details. */ function mapDreamstateErrorMessage(code, detail) { switch (code) { case exports.EDreamstateErrorCode.INSTANCE_DISPOSED_LIFECYCLE: return "Disposed manager instances are not supposed to access lifecycle."; case exports.EDreamstateErrorCode.INSTANCE_DISPOSED_SCOPE: return "Disposed manager instances are not supposed to access scope."; case exports.EDreamstateErrorCode.OUT_OF_SCOPE: return "Instance is out of scope, make sure it is created or mocked correctly."; case exports.EDreamstateErrorCode.INCORRECT_PARAMETER: return "Incorrect parameter supplied.".concat(detail ? " ".concat(detail) : ""); case exports.EDreamstateErrorCode.INCORRECT_SIGNAL_TYPE: return "Unexpected signal type provided, expected symbol, string or number.".concat(detail ? " Got: '".concat(detail, "' instead.") : ""); case exports.EDreamstateErrorCode.INCORRECT_SIGNAL_LISTENER: return "Signal listener must be function, '".concat(detail, "' provided."); case exports.EDreamstateErrorCode.INCORRECT_QUERY_TYPE: return "Unexpected query type provided, expected symbol, string or number. Got: '".concat(detail, "' instead."); case exports.EDreamstateErrorCode.INCORRECT_QUERY_PROVIDER: return "Query provider must be factory function, '".concat(detail, "' provided."); case exports.EDreamstateErrorCode.TARGET_CONTEXT_MANAGER_EXPECTED: return "Cannot perform action, class extending ContextManager is expected.".concat(detail ? " ".concat(detail) : ""); case exports.EDreamstateErrorCode.RESTRICTED_OPERATION: return "Operation is restricted.".concat(detail ? " ".concat(detail) : ""); case exports.EDreamstateErrorCode.UNEXPECTED_ERROR: default: return "Unexpected dreamstate error.".concat(detail ? " ".concat(detail) : ""); } } /** * A custom error class that contains generic error information for Dreamstate-related issues. * * This class extends the native `Error` class and is used to represent errors specific * to the Dreamstate library, providing more structured error handling. */ var DreamstateError = /** @class */function (_super) { tslib.__extends(DreamstateError, _super); function DreamstateError(code, detail) { if (code === void 0) { code = exports.EDreamstateErrorCode.UNEXPECTED_ERROR; } var _this = _super.call(this) || this; /** * Name or error class to help differentiate error class in minified environments. */ _this.name = "DreamstateError"; _this.code = code; _this.message = mapDreamstateErrorMessage(code, detail); return _this; } return DreamstateError; }(Error); /** * Utility function that throws error on every call. * Intended to be placeholder when react scope is being disposed. */ function throwAfterDisposal() { throw new DreamstateError(exports.EDreamstateErrorCode.INSTANCE_DISPOSED_SCOPE); } /** * Utility function that throws error on every out of scope call. */ function throwOutOfScope() { throw new DreamstateError(exports.EDreamstateErrorCode.OUT_OF_SCOPE); } /** * Meta symbols for private internals in context managers. */ var SIGNAL_METADATA_SYMBOL = Symbol("SIGNAL_METADATA"); var QUERY_METADATA_SYMBOL = Symbol("QUERY_METADATA"); var SIGNALING_HANDLER_SYMBOL = Symbol("SIGNALING_HANDLER"); var SCOPE_SYMBOL = Symbol("SCOPE"); /** * A weak map registry that stores React context instances bound to specific manager classes. * * This registry ensures that the library does not retain unnecessary references to manager classes, * preventing memory leaks and unintended side effects. * * This is particularly useful in scenarios such as: * - Hot Module Replacement * - Module unloading * - Scope disposal */ var CONTEXT_REACT_CONTEXTS_REGISTRY = new WeakMap(); var SIGNAL_METADATA_REGISTRY = new WeakMap(); var QUERY_METADATA_REGISTRY = new WeakMap(); /** * Retrieves the React context reference associated with the provided `ManagerClass`. * The context is lazily created only after the first access attempt. If no manager is provided in the scope, * a default context value can be applied. * * This function allows for the dynamic creation and retrieval of a context specific to a given manager, * ensuring that the context is properly initialized and available for use. * * @template S - The type of the context state. * @template M - The type of the context manager constructor. * @param {M} ManagerClass - The context manager constructor reference used to identify the context. * @returns {Context<S>} - A React context instance with a pre-defined default value. */ function getReactContext(ManagerClass) { var existing = CONTEXT_REACT_CONTEXTS_REGISTRY.get(ManagerClass); if (existing) { return existing; } else { var reactContext = react.createContext(ManagerClass.getDefaultContext()); /* * Later providers and consumers in tree will be displayed as * 'Dreamstate.Class.Provider' or 'Dreamstate.Class.Consumer'. */ { reactContext.displayName = "Dreamstate." + ManagerClass.name; } CONTEXT_REACT_CONTEXTS_REGISTRY.set(ManagerClass, reactContext); return reactContext; } } /** * A utility class for storing and managing nested action values. * * This class is primarily used by the `ContextManager` to track and avoid comparing actions during context updates. * It helps in detecting differences in state changes efficiently. */ var ActionsStore = /** @class */function () { function ActionsStore(actions) { Object.assign(this, actions); } return ActionsStore; }(); /** * A utility class for managing nested computed values. * * This class is used by the `ContextManager` to track and compare computed values during context updates. * It helps optimize state management by recalculating values only when dependencies change. */ var ComputedValue = /** @class */function () { function ComputedValue(selector, memo) { this.__selector__ = selector; this.__memo__ = memo; } ComputedValue.prototype.process = function (context) { // Process memoized computed in a special way, updated default computed every time. if (this.__memo__) { var diff_1 = this.__memo__(context); // Warn if someone provided wrong selector. { if (!Array.isArray(diff_1)) { console.warn("Expecting diff function from createComputed to return diff-array of dependencies."); } } // If diff initialized and we can check memo values. if (!this.__diff__ || this.__diff__.some(function (it, idx) { return it !== diff_1[idx]; })) { this.__diff__ = diff_1; Object.assign(this, this.__selector__(context)); } } else { Object.assign(this, this.__selector__(context)); } }; return ComputedValue; }(); /** * A utility class for extending context manager state with structured state management and shallow comparison. * * This class is used by the `ContextManager` to efficiently track and compare nested state updates. * It helps in optimizing reactivity by ensuring that updates trigger only when relevant changes occur. */ var NestedStore = /** @class */function () { function NestedStore() {} /** * Merges the provided partial state with the existing state and returns a shallow copy. * * @template T - The type of the state. * @param {Partial<T>} state The next state partial to merge with the existing state and commit update. * @returns {TNested<T>} A merged shallow copy based on the partial state parameter. */ NestedStore.prototype.asMerged = function (state) { if (state === void 0) { state = {}; } return Object.assign(new NestedStore(), this, state); }; return NestedStore; }(); /** * Utility function to check whether the supplied parameter is a string. * * @param {unknown} target The parameter to check. * @returns {boolean} True if the parameter is a string, otherwise false. */ function isString(target) { return typeof target === "string"; } /** * Utility function to check whether the supplied parameter is a number primitive. * * @param {unknown} target The parameter to check. * @returns {boolean} True if the parameter is a number, otherwise false. */ function isNumber(target) { return typeof target === "number"; } /** * Utility function to check whether the supplied parameter is an object. * * @param {unknown} target The parameter to check. * @returns {boolean} True if the parameter is an object, otherwise false. */ function isObject(target) { return typeof target === "object" && target !== null; } /** * Utility function to check whether the supplied parameter is a symbol. * * @param {unknown} target The parameter to check. * @returns {boolean} True if the parameter is a symbol, otherwise false. */ function isSymbol(target) { return typeof target === "symbol"; } /** * Utility function to check whether the supplied parameter is a function. * * @param {unknown} target The parameter to check. * @returns {boolean} True if the parameter is a function, otherwise false. */ function isFunction(target) { return typeof target === "function"; } /** * Utility function to check whether the supplied parameter is undefined. * * @param {unknown} target The parameter to check. * @returns {boolean} True if the parameter is undefined, otherwise false. */ function isUndefined(target) { return typeof target === "undefined"; } /** * Check whether the provided type is correct for query usage. * * @param {unknown} type The provided type to check. * @returns {boolean} True if the provided type is valid for query usage, otherwise false. */ function isCorrectQueryType(type) { return isString(type) || isNumber(type) || isSymbol(type); } /** * Check whether the provided type is correct for signal usage. * * @param {unknown} type The provided type to check. * @returns {boolean} True if the provided type is valid for signal usage, otherwise false. */ function isCorrectSignalType(type) { return isString(type) || isNumber(type) || isSymbol(type); } /** * Compares two context manager states to determine if there are any changes that would require * observers to update. Performs a shallow comparison while respecting specific meta-fields like * `NestedStore`, `ComputedValue`, `ActionsStore`, etc. to ensure that nested objects and specialized * stores are taken into account during the comparison. * * The comparison helps decide whether the observers need to react to the changes and re-render accordingly. * * @template T - The type of the context state. * @param {T} previousContext - The previous context to be compared against. * @param {T} nextContext - The new context to check for differences. * @returns {boolean} - `true` if observers should update (i.e., if there is a difference between * the contexts); `false` otherwise. */ function shouldObserversUpdate(previousContext, nextContext) { if (!isObject(nextContext)) { throw new DreamstateError(exports.EDreamstateErrorCode.INCORRECT_PARAMETER, "Context should be non-nullable object, supplied '".concat(typeof nextContext, "' type instead.")); } if (!previousContext) { return true; } return Object.keys(nextContext).some(function (key) { /* * Ignore computed values. * Ignore action values. * Since nested computed stores are not representing data itself, we should not verify anything there. */ if (nextContext[key] instanceof ComputedValue || nextContext[key] instanceof ActionsStore) { return false; } /* * Shallow check for mutable objects created by library. * Optimization for sub-states to prevent pollution of context and improve performance. * We cannot guess about each object because it is (1) not obvious, (2) can be unwanted and (3) will not work for * some objects like native MediaStream/MediaStreamTrack. */ return nextContext[key] instanceof NestedStore ? !shallowEqual.shallowEqualObjects(nextContext[key], previousContext[key]) : nextContext[key] !== previousContext[key]; }); } /** * Processes computed values within the given context. * * This function mutates the provided context object, updating its computed values while maintaining * the original object reference. It ensures that computed properties are properly initialized * and reactive based on the current state. * * @template T - The type of the context object. * @param {T} context - The context object containing computed values to be processed. * @returns {T} The same context object with updated computed values. */ function processComputed(context) { for (var key in context) { var it_1 = context[key]; if (it_1 instanceof ComputedValue) { it_1.process(context); } } return context; } /** * Abstract context manager class. * This class wraps data and logic, separating them from the React tree. It allows you to create * global or local storages with lifecycles, which can be cleaned up and ejected when no longer needed. * * This class serves as a foundation for managing scoped data and logic in a React application using * the Dreamstate library. * * To provide specific `ContextManager` classes in the React tree, use the `createProvider` method. * To consume specific `ContextManager` data, use the `useManager` method. * For more details on shallow checks of context updates, see the `createNested`, `createActions`, * and other related methods. * * Every instance of this class is automatically managed and created by Dreamstate scope if needed. * - Instances can emit signals and query data within the scope where they were created. * - Instances can register methods as scope signal listeners or query data providers. * - Each instance is responsible for a specific data part (similar to reducers in Redux). * * Examples of `ContextManager` subclasses: AuthManager, GraphicsManager, ChatManager, LocalMediaManager, etc. * * **Important Notes**: * - Async methods called after the manager class is unregistered will trigger warnings during development, * but they will not affect the actual scope after ejection. * * @template T - The type of the context state managed by this class. * @template S - The type of additional data or metadata that can be attached to the manager. */ var ContextManager = /** @class */function () { /** * Generic context manager constructor. * The initial state can be used as an initialization value or SSR-provided data. * Treating the initial state as an optional value allows for more generic and reusable code, * as the manager can be provided in different places with different initial states. * * @template S - The type of initial state object. * @param {S} initialState - Optional initial state received from the Dreamstate Provider component properties. */ function ContextManager(initialState) { /** * Flag indicating whether the current manager is still active or has been disposed. * Once a manager is disposed, it cannot be reused or continue functioning. * Scope-related methods (signals, queries) will be inaccessible, and using them will throw exceptions. */ this.IS_DISPOSED = false; /** * Manager instance context. * This field will be synchronized with React providers when the 'setContext' method is called. * It should hold an object value. * * While manual mutations of nested value fields are allowed, they are not recommended. * After calling 'setContext', the context will be shallowly compared with the existing context * before it is synced with the React tree. * Meta fields created by Dreamstate utilities (such as createActions, createNested, etc.) may * have a different comparison mechanism instead of the standard shallow check. * * For more information about the shallow check process, refer to 'createNested', 'createActions', * and similar methods. */ this.context = {}; /* * Make sure values marked as computed ('createComputed') are calculated before provision. */ processComputed(this.context); } /** * React context default value getter. * This method provides placeholder values to context consumers when the corresponding manager is not provided. * * @returns {TAnyObject | null} * - Returns the default value for context consumers when the manager is not provided. * - Defaults to `null` if no specific getter is defined. */ ContextManager.getDefaultContext = function () { return null; }; Object.defineProperty(ContextManager, "REACT_CONTEXT", { /** * React context getter. * This method allows access to the related React.Context, which can be useful for manual rendering * or testing scenarios. * * The context is lazily initialized, even for static resolving, before any other elements of the * ContextManager are used. * * @returns {Context<TAnyValue>} The React context associated with this ContextManager. */ get: function () { if (this === ContextManager) { throw new DreamstateError(exports.EDreamstateErrorCode.RESTRICTED_OPERATION, "Direct references to ContextManager statics forbidden."); } return getReactContext(this); }, enumerable: false, configurable: true }); /** * Lifecycle method called when the first provider is injected into the React tree. * This follows a similar philosophy to 'componentWillMount' in class-based components. * * This method is useful for initializing data and setting up subscriptions. */ ContextManager.prototype.onProvisionStarted = function () {}; /** * Lifecycle method called when the last provider is removed from the React tree. * This follows a similar philosophy to 'componentWillUnmount' in class-based components. * * This method is useful for disposing of data when the context is being ejected * or when Hot Module Replacement (HMR) occurs. */ ContextManager.prototype.onProvisionEnded = function () {}; /** * Get the current manager scope. * This method allows access to the current execution scope and provides methods * for retrieving manager instances within it. * * @returns {IScopeContext} The current manager scope. */ ContextManager.prototype.getScope = function () { if (this[SCOPE_SYMBOL]) { return this[SCOPE_SYMBOL]; } else { throwOutOfScope(); } }; /** * Forces an update and re-render of subscribed components. * This is useful when you need to ensure that the components remain in sync with the current context. * * Side effect: After a successful update, all subscribed components will be re-rendered * according to their subscription. * * Note: This will only force an update of the provider; components using `useManager` selectors * will not be forced to render. * * Note: A new shallow copy of `this.context` is created after each call. * * Note: If the manager is out of scope, the method will simply replace `this.context`. */ ContextManager.prototype.forceUpdate = function () { /* * Always do shallow copy to point new ref object in current context. */ this.context = processComputed(Object.assign({}, this.context)); if (this[SCOPE_SYMBOL]) { this[SCOPE_SYMBOL].INTERNAL.notifyObservers(this); } }; /** * Updates the current context from a partially supplied state or a functional selector. * The update is applied to the React provider tree only if the `shouldObserversUpdate` check passes * and if any changes have occurred in the store. * This follows the same philosophy as `setState` in React class components. * * Side effect: After a successful update, all subscribed components will be updated accordingly * to their subscription. * * Note: A partial context object or a callback that returns a partial context is required. * * Note: This will only update the provider; components using `useManager` selectors will not be * forced to render. * * Note: If the manager is out of scope, it will simply rewrite the `this.context` object without * any side effects. * * @param {object | Function} next - A part of the context to be updated or a context transformer function. * If a function is provided, it will be executed immediately with the `currentContext` as its parameter. */ ContextManager.prototype.setContext = function (next) { var nextContext = Object.assign({}, this.context, /* * Handle context transformer functions. */ isFunction(next) ? next(this.context) : next); /* * Always update context, even if it was created out of scope. * In case of existing scope just send additional notification. */ this.context = processComputed(nextContext); /* * Compare current context with saved for observing one. */ if (this[SCOPE_SYMBOL] && shouldObserversUpdate(this[SCOPE_SYMBOL].INTERNAL.REGISTRY.CONTEXT_STATES_REGISTRY.get(this.constructor), nextContext)) { this[SCOPE_SYMBOL].INTERNAL.notifyObservers(this); } }; /** * Emits a signal to other managers and subscribers within the current scope. * Valid signal types include `string`, `number`, and `symbol`. * * @template D - The type of the data associated with the signal. * @param {IBaseSignal<D>} baseSignal - The base signal object containing a signal type and * optional data. * @param {*} baseSignal.data - Optional data associated with the signal. * @returns {ISignalEvent<D>} The signal event object that encapsulates the emitted signal. * * @throws {Error} Throws an error if the manager is out of scope. */ ContextManager.prototype.emitSignal = function (baseSignal) { if (this[SCOPE_SYMBOL]) { return this[SCOPE_SYMBOL].emitSignal(baseSignal, this.constructor); } else { throwOutOfScope(); } }; /** * Sends a context query to retrieve data from query handler methods. * This asynchronous method is particularly useful for async providers, although * synchronous providers are handled as well. * * If a valid query handler is found in the current scope, it returns a promise that resolves * with a query response object; otherwise, it resolves with `null`. * * @template D - The type of the query data. * @template T - The type of the query. * @template Q - The query request type, extending IOptionalQueryRequest<D, T>. * @param {Q} queryRequest - The query request object containing the query type and optional data. * @param {TQueryType} queryRequest.type - The type of the query. * @param {*} [queryRequest.data] - Optional data used as parameters for data retrieval. * @returns {Promise<TQueryResponse<TAnyValue> | null>} A promise that resolves with the query response if a valid * handler is found, or `null` if no handler exists in the current scope. */ ContextManager.prototype.queryDataAsync = function (queryRequest) { if (this[SCOPE_SYMBOL]) { return this[SCOPE_SYMBOL].queryDataAsync(queryRequest); } else { throwOutOfScope(); } }; /** * Sends a context query to retrieve data from query handler methods synchronously. * This method is ideal for synchronous operations; asynchronous handlers will return a promise * in the data field. * * If a valid query handler is found in the current scope, the method returns a query response object. * Otherwise, it returns `null`. * * @template D - The type of the query data. * @template T - The type of the query. * @template Q - The type of the query request, extending IOptionalQueryRequest<D, T>. * @param {Q} queryRequest - The query request object containing: * - `type`: The type of the query. * - `data` (optional): Additional data or parameters for data retrieval. * @returns {TQueryResponse<TAnyValue> | null} The query response object if a valid handler is found, * or `null` if no handler exists in the current scope. */ ContextManager.prototype.queryDataSync = function (queryRequest) { if (this[SCOPE_SYMBOL]) { return this[SCOPE_SYMBOL].queryDataSync(queryRequest); } else { throwOutOfScope(); } }; return ContextManager; }(); /** * A utility function that does nothing and ignores all further calls. */ function noop() { /* Nothing to do here. */ } /** * Promisifies a query handler by wrapping it in a `Promise`. * * This function takes a query handler, and if it's asynchronous, it adds `.then` and `.catch` handlers to it. * If the query handler is synchronous, it directly returns the result or rejects the promise in case of an error. * It is useful for handling queries that may either return a promise or a direct result, ensuring consistent * promise-based handling. * * @template R - The type of the response expected from the query handler. * @template D - The type of data associated with the query request (optional). * @template T - The type of the query (defaults to `TQueryType`). * @param {TQueryListener<T, D, R>} callback - The query handler function that is called when the query is executed. * It can either be synchronous or asynchronous. * @param {IOptionalQueryRequest<D, T>} query - The query request containing necessary data for the query. * @param {TAnyContextManagerConstructor | null} answerer - The context manager class reference * that is handling the query. * @returns {Promise<TQueryResponse<R>>} A promise that resolves with the query response, either from the * synchronous result or the asynchronous operation. */ function promisifyQuery(callback, query, answerer) { return new Promise(function (resolve, reject) { try { var timestamp_1 = Date.now(); var result = callback(query); /* * Not all query responders are sync or async. * Here we expect it to be either sync or async and handle it in an async way. */ if (result instanceof Promise) { return result.then(function (data) { resolve({ answerer: answerer || callback, type: query.type, data: data, timestamp: timestamp_1 }); }).catch(reject); } else { return resolve({ answerer: answerer || callback, type: query.type, data: result, timestamp: timestamp_1 }); } } catch (error) { reject(error); } }); } /** * Finds the correct asynchronous listener or an array of listeners and returns the promise response or null. * * This function searches for a matching async listener based on the provided query type. If a listener is found, * it invokes the corresponding method and returns the result as a promise. If no matching listener is found, * the function returns `null`. It is useful for handling queries that require asynchronous processing and * responding with a promise. * * @template R - The type of the response expected from the query. * @template D - The type of the data associated with the query request. * @template T - The type of the query. * @template Q - The type of the query request (extends `IOptionalQueryRequest<D, T>`). * @param {Q} query - The query request containing the necessary data for the query. * @param {IRegistry} registry - An object containing registries for `CONTEXT_INSTANCES_REGISTRY` * and `QUERY_PROVIDERS_REGISTRY`, which store context instances and query providers for the respective queries. * @returns {Promise<TQueryResponse<R> | null>} A promise that resolves with the query response if a matching listener * is found, or `null` if no listener matches the query type. */ function queryDataAsync(query, _a) { var e_1, _b, e_2, _c; var CONTEXT_INSTANCES_REGISTRY = _a.CONTEXT_INSTANCES_REGISTRY, QUERY_PROVIDERS_REGISTRY = _a.QUERY_PROVIDERS_REGISTRY; if (!query || !query.type) { throw new DreamstateError(exports.EDreamstateErrorCode.INCORRECT_PARAMETER, "Query must be an object with declared type or array of objects with type."); } try { /* * Managers classes are in priority over custom handlers. * Registered in order of creation. */ for (var _d = tslib.__values(CONTEXT_INSTANCES_REGISTRY.values()), _e = _d.next(); !_e.done; _e = _d.next()) { var service = _e.value; try { for (var _f = (e_2 = void 0, tslib.__values(service[QUERY_METADATA_SYMBOL])), _g = _f.next(); !_g.done; _g = _f.next()) { var entry = _g.value; if (query.type === entry[1]) { var method = entry[0]; return promisifyQuery(service[method].bind(service), query, service.constructor); } } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (_g && !_g.done && (_c = _f.return)) _c.call(_f); } finally { if (e_2) throw e_2.error; } } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (_e && !_e.done && (_b = _d.return)) _b.call(_d); } finally { if (e_1) throw e_1.error; } } /* * From class providers fallback to manually listed query provider factories. */ if (QUERY_PROVIDERS_REGISTRY.has(query.type)) { var handlerFunction = QUERY_PROVIDERS_REGISTRY.get(query.type)[0]; return promisifyQuery(handlerFunction, query, null); } /* * Resolve null if nothing was found to handle request. */ return Promise.resolve(null); } /** * Executes a query and returns the result synchronously. * * This function processes the query using the provided query handler and returns the result directly. * It is designed for cases where the query handler is synchronous and can immediately return a response. * If the query handler is asynchronous, this function will not wait for the result and return raw promise response. * * @template R - The type of the response expected from the query handler. * @template D - The type of the data associated with the query request (optional). * @template T - The type of the query (defaults to `TQueryType`). * @param {TQueryListener<T, D>} callback - The query handler function that is called with the query and * should return a result synchronously. * @param {IOptionalQueryRequest<D, T>} query - The query request containing necessary data for the query. * @param {TAnyContextManagerConstructor | null} answerer - The context manager class reference * responsible for handling the query. * @returns {TQueryResponse<R>} The result of the query execution, returned synchronously. */ function executeQuerySync(callback, query, answerer) { return { answerer: answerer || callback, type: query.type, data: callback(query), timestamp: Date.now() }; } /** * Finds the correct listener and returns the response synchronously, or `null` if no matching listener is found. * * This function searches for a matching listener based on the provided query type and invokes the corresponding * method. It is designed for handling synchronous queries. The function will return the result directly. * * @template R - The type of the response expected from the query. * @template D - The type of the data associated with the query request. * @template T - The type of the query. * @template Q - The type of the query request (extends `IOptionalQueryRequest<D, T>`). * @param {Q} query - The query request containing the necessary data for the query. * @param {IRegistry} registry - The registry object to execute query in. * @returns {TQueryResponse<R> | null} The result of the query if a matching listener is found, * or `null` if no matching listener exists. */ function queryDataSync(query, _a) { var e_1, _b, e_2, _c; var CONTEXT_INSTANCES_REGISTRY = _a.CONTEXT_INSTANCES_REGISTRY, QUERY_PROVIDERS_REGISTRY = _a.QUERY_PROVIDERS_REGISTRY; if (!query || !query.type) { throw new DreamstateError(exports.EDreamstateErrorCode.INCORRECT_PARAMETER, "Query must be an object with declared type or array of objects with type."); } try { /* * Managers classes are in priority over custom handlers. * Registered in order of creation. */ for (var _d = tslib.__values(CONTEXT_INSTANCES_REGISTRY.values()), _e = _d.next(); !_e.done; _e = _d.next()) { var service = _e.value; try { for (var _f = (e_2 = void 0, tslib.__values(service[QUERY_METADATA_SYMBOL])), _g = _f.next(); !_g.done; _g = _f.next()) { var entry = _g.value; if (query.type === entry[1]) { var method = entry[0]; return executeQuerySync(service[method].bind(service), query, service.constructor); } } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (_g && !_g.done && (_c = _f.return)) _c.call(_f); } finally { if (e_2) throw e_2.error; } } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (_e && !_e.done && (_b = _d.return)) _b.call(_d); } finally { if (e_1) throw e_1.error; } } /* * From class providers fallback to manually listed query provider factories. */ if (QUERY_PROVIDERS_REGISTRY.has(query.type)) { var handlerFunction = QUERY_PROVIDERS_REGISTRY.get(query.type)[0]; return executeQuerySync(handlerFunction, query, null); } return null; } /** * Unsubscribes the specified listener from handling queries of a given type. * * This function removes the provided listener for the given query type from the registry, * ensuring it no longer handles future data queries for that type. * * @template T - The type of the query. * @param {TQueryType} queryType - The type of query that the listener should be unsubscribed from. * @param {TQueryListener<T, TAnyValue>} listener - The callback listener to be removed from query handling. * @param {IRegistry} registry - The current scope registry containing the query providers. * @returns {void} This function does not return a value; it performs the unsubscribe action. */ function unRegisterQueryProvider(queryType, listener, _a) { var QUERY_PROVIDERS_REGISTRY = _a.QUERY_PROVIDERS_REGISTRY; if (!isFunction(listener)) { throw new DreamstateError(exports.EDreamstateErrorCode.INCORRECT_QUERY_PROVIDER, typeof listener); } else if (!isCorrectQueryType(queryType)) { throw new DreamstateError(exports.EDreamstateErrorCode.INCORRECT_QUERY_TYPE, typeof queryType); } if (QUERY_PROVIDERS_REGISTRY.has(queryType)) { var nextProviders = QUERY_PROVIDERS_REGISTRY.get(queryType).filter(function (it) { return it !== listener; }); if (nextProviders.length) { QUERY_PROVIDERS_REGISTRY.set(queryType, nextProviders); } else { QUERY_PROVIDERS_REGISTRY.delete(queryType); } } } /** * Registers a callback as a query provider to handle data queries. * * This function registers a query provider callback for a given query type in the current * scope's registry. The listener will handle incoming data queries and return the requested data. * * @template T - The type of the query. * @param {TQueryType} queryType - The type of query for which data provisioning is provided. * @param {TQueryListener<T, TAnyValue>} listener - The callback that listens to queries and returns * the requested data. * @param {IRegistry} registry - The current scope registry where the query provider is registered. * @returns {TCallable} A function that, when called, unsubscribes the registered query provider. */ function registerQueryProvider(queryType, listener, registry) { if (typeof listener !== "function") { throw new DreamstateError(exports.EDreamstateErrorCode.INCORRECT_QUERY_PROVIDER, typeof listener); } else if (!isCorrectQueryType(queryType)) { throw new DreamstateError(exports.EDreamstateErrorCode.INCORRECT_QUERY_TYPE, typeof queryType); } /* * Handle query providers as array so for one type many queries can be provided, but only first one will be called. */ if (registry.QUERY_PROVIDERS_REGISTRY.has(queryType)) { var currentProviders = registry.QUERY_PROVIDERS_REGISTRY.get(queryType); // Do not overwrite same listeners. if (!currentProviders.includes(listener)) { currentProviders.unshift(listener); } } else { // Just add new entry. registry.QUERY_PROVIDERS_REGISTRY.set(queryType, [listener]); } /* * Return un-subscriber callback. */ return function () { unRegisterQueryProvider(queryType, listener, registry); }; } /** * Creates a new registry to hold internal stores for the current scope. * * This function returns an `IRegistry` object containing several collections (e.g., `Map`, `Set`) * that are used to store mutable data such as query providers, signal listeners, context instances, * context states, services references, observers, and subscribers within the current scope. * The registry facilitates the management and access of these stores during the application's lifecycle. * * @returns {IRegistry} The newly created registry containing various internal stores used for scope management. */ function createRegistry() { return { QUERY_PROVIDERS_REGISTRY: new Map(), SIGNAL_LISTENERS_REGISTRY: new Set(), CONTEXT_INSTANCES_REGISTRY: new Map(), CONTEXT_STATES_REGISTRY: new Map(), CONTEXT_SERVICES_REFERENCES: new Map(), CONTEXT_OBSERVERS_REGISTRY: new Map(), CONTEXT_SUBSCRIBERS_REGISTRY: new Map() }; } /** * Processes a metadata signal and dispatches it to the corresponding metadata listeners * of the bound manager. * * This function is intended to be used as a method bound to a ContextManager instance. It * filters incoming signal events and calls metadata listeners that are associated with the * manager instance. * * @template D - The type of the data contained in the signal event. * @param {ISignalEvent<D>} signal - The signal event containing the signal type and optional data. * @returns {void} */ function onMetadataSignalListenerCalled(signal) { var e_1, _a; try { /* * For each metadata entry do a check/call for signal handler. */ for (var _b = tslib.__values(this[SIGNAL_METADATA_SYMBOL]), _c = _b.next(); !_c.done; _c = _b.next()) { var _d = tslib.__read(_c.value, 2), method = _d[0], subscribed = _d[1]; if (Array.isArray(subscribed) ? subscribed.includes(signal.type) : signal.type === subscribed) { this[method](signal); } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_1) throw e_1.error; } } } /** * Cancels the signal event, preventing further propagation to subsequent listeners. * * This method is used within signal events to stop their execution, ensuring that * no additional subscribers receive the event after cancellation. * * @template D - The type of the signal event data, defaults to `undefined`. * @returns {ISignalEvent<D>} The canceled signal event instance. */ function cancelSignal() { this.canceled = true; return this; } /** * Emits a signal and notifies all subscribed listeners. * * This function constructs a signal event from the provided base data and dispatches it * to all registered listeners within the current scope. If any listener cancels the event, * propagation to subsequent handlers is stopped. * * @template D - The type of the signal event data, defaults to `undefined`. * @param {IBaseSignal<D>} base - The base signal data used to create the event. * @param {TAnyContextManagerConstructor | null} [emitter] - The optional emitter of the signal, * typically a context manager class. * @param {IRegistry} REGISTRY - The registry containing all signal event listeners. * @returns {ISignalEvent<D>} The dispatched signal event instance. */ function emitSignal(base, emitter, REGISTRY) { if (emitter === void 0) { emitter = null; } if (!base || !isCorrectSignalType(base.type)) { throw new DreamstateError(exports.EDreamstateErrorCode.INCORRECT_SIGNAL_TYPE); } var signalEvent = { type: base.type, data: base.data, emitter: emitter, timestamp: Date.now(), cancel: cancelSignal }; // todo: Use for-in and break loop on cancel. REGISTRY.SIGNAL_LISTENERS_REGISTRY.forEach(function (it) { try { if (!signalEvent.canceled) { it(signalEvent); } } catch (error) { console.error("[DS]", "Failed to proceed emitted signal (".concat(String(base.type), "):"), error); } }); return signalEvent; } /** * Collects metadata from the prototype chain of a given context manager class. * * This function resolves issues related to class inheritance, particularly when dealing with query * and signal handlers. It traverses the prototype chain of the provided context manager class, * gathering metadata and ensuring all inherited behaviors are properly registered. * * @template T - The type of the context manager constructor. * @template D - The type of the metadata stored in the registry. * @param {T} target - The base context manager class from which metadata should be collected. * @param {WeakMap<T, D>} registry - A weak map registry that holds metadata for context manager classes. * @returns {D} The collected metadata for the given context manager. */ function collectProtoChainMetadata(target, registry) { if (target.prototype instanceof ContextManager) { var metadata = []; var current = target; while (current !== ContextManager) { metadata.push(registry.get(current)); current = Object.getPrototypeOf(current); } /** * todo: Remove duplicates from an array? * todo: If overriding signal handling methods and adding @OnSignal, it may cause issues with double call of method. */ return metadata.reduce(function (pr, it) { if (it) { return pr.concat(it); } else { return pr; } }, []); } else { throw new DreamstateError(exports.EDreamstateErrorCode.INCORRECT_PARAMETER, "Failed to collect metadata of class that is not extending ContextManager."); } } /** * Initializes the core scope context for managing stores, signals, and queries in the VDOM tree. * This function sets up the scope that is responsible for handling state and interactions within * the context of React's virtual DOM. * * @param {IRegistry} [registry] - Optional registry object to initialize. * @returns {IScopeContext} Mutable scope with a set of methods and registry stores for the React VDOM tree. */ function createScope(registry) { if (registry === void 0) { registry = createRegistry(); } var SIGNAL_LISTENERS_REGISTRY = registry.SIGNAL_LISTENERS_REGISTRY, CONTEXT_STATES_REGISTRY = registry.CONTEXT_STATES_REGISTRY, CONTEXT_OBSERVERS_REGISTRY = registry.CONTEXT_OBSERVERS_REGISTRY; registry.QUERY_PROVIDERS_REGISTRY; var CONTEXT_SERVICES_REFERENCES = registry.CONTEXT_SERVICES_REFERENCES, CONTEXT_INSTANCES_REGISTRY = registry.CONTEXT_INSTANCES_REGISTRY, CONTEXT_SUBSCRIBERS_REGISTRY = registry.CONTEXT_SUBSCRIBERS_REGISTRY; var scope = { INTERNAL: { REGISTRY: registry, registerManager: function (ManagerClass, initialState, initialContext) { // Only if registry is empty -> create new instance, remember its context and save it to registry. if (!CONTEXT_INSTANCES_REGISTRY.has(ManagerClass)) { var instance_1 = new ManagerClass(initialState); /* * Inject initial context fields if provided for overriding on manager construction. */ if (initialContext) { Object.assign(instance_1.context, initialContext); } // todo: Add checkContext method call for dev bundle with warnings for initial state nesting. processComputed(instance_1.context); instance_1[SCOPE_SYMBOL] = scope; instance_1[SIGNAL_METADATA_SYMBOL] = collectProtoChainMetadata(ManagerClass, SIGNAL_METADATA_REGISTRY); instance_1[QUERY_METADATA_SYMBOL] = collectProtoChainMetadata(ManagerClass, QUERY_METADATA_REGISTRY); instance_1[SIGNALING_HANDLER_SYMBOL] = onMetadataSignalListenerCalled.bind(instance_1); CONTEXT_STATES_REGISTRY.set(ManagerClass, instance_1.context); CONTEXT_SERVICES_REFERENCES.set(ManagerClass, 0); CONTEXT_OBSERVERS_REGISTRY.set(ManagerClass, new Set()); SIGNAL_LISTENERS_REGISTRY.add(instance_1[SIGNALING_HANDLER_SYMBOL]); CONTEXT_INSTANCES_REGISTRY.set(ManagerClass, instance_1); /* * Notify subscribers if they exist. * Create new entry if it is needed. */ if (CONTEXT_SUBSCRIBERS_REGISTRY.has(ManagerClass)) { CONTEXT_SUBSCRIBERS_REGISTRY.get(ManagerClass).forEach(function (it) { it(instance_1.context); }); } else { CONTEXT_SUBSCRIBERS_REGISTRY.set(ManagerClass, new Set()); } return true; } else { return false; } }, unRegisterManager: function (ManagerClass) { if (CONTEXT_INSTANCES_REGISTRY.has(ManagerClass)) { var instance = CONTEXT_INSTANCES_REGISTRY.get(ManagerClass); /* * Unset handlers and stop affecting scope after unregister. * For external calls throw an exception (queries). */ instance[SCOPE_SYMBOL] = null; instance["setContext"] = noop; instance["forceUpdate"] = noop; /* * Most likely code will fail with null pointer in case of warning. * Or it will start work in an unexpected way with 'null' check. */ instance["emitSignal"] = throwAfterDisposal; instance["queryDataSync"] = throwAfterDisposal; instance["queryDataAsync"] = throwAfterDisposal; /* * Mark instance as disposed to enable internal logic related to lifecycle and async actions/timers. */ instance.IS_DISPOSED = true; SIGNAL_LISTENERS_REGISTRY.delete(instance[SIGNALING_HANDLER_SYMBOL]); CONTEXT_INSTANCES_REGISTRY.delete(ManagerClass); CONTEXT_STATES_REGISTRY.delete(ManagerClass); return true; } else { CONTEXT_INSTANCES_REGISTRY.delete(ManagerClass); CONTEXT_STATES_REGISTRY.delete(ManagerClass); return false; } /* * Observers and subscribers should not be affected by un-registering. */ }, addServiceObserver: function (ManagerClass, observer, referencesCount) { if (referencesCount === void 0) { referencesCount = CONTEXT_SERVICES_REFERENCES.get(ManagerClass) + 1; } CONTEXT_OBSERVERS_REGISTRY.get(ManagerClass).add(observer); CONTEXT_SERVICES_REFERENCES.set(ManagerClass, referencesCount); if (referencesCount === 1) { CONTEXT_INSTANCES_REGISTRY.get(ManagerClass)["onProvisionStarted"](); } }, removeServiceObserver: function (ManagerClass, observer, referencesCount) { if (referencesCount === void 0) { referencesCount = CONTEXT_SERVICES_REFERENCES.get(ManagerClass) - 1; } CONTEXT_OBSERVERS_REGISTRY.get(ManagerClass).delete(observer); CONTEXT_SERVICES_REFERENCES.set(ManagerClass, referencesCount); if (referencesCount === 0) { CONTEXT_INSTANCES_REGISTRY.get(ManagerClass)["onProvisionEnded"](); this.unRegisterManager(ManagerClass); } }, notifyObservers: function (manager) { var nextContext = ma