UNPKG

dreamstate

Version:

Store management library based on react context and observers

266 lines (263 loc) 12.1 kB
import { DreamstateError } from '../error/DreamstateError.js'; import { throwOutOfScope } from '../error/throw.js'; import { SCOPE_SYMBOL } from '../internals.js'; import { getReactContext } from './getReactContext.js'; import { shouldObserversUpdate } from './shouldObserversUpdate.js'; import { processComputed } from '../storing/processComputed.js'; import { EDreamstateErrorCode } from '../../types/error.js'; import { isFunction } from '../../utils/typechecking.js'; /** * 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(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; }(); export { ContextManager };