dreamstate
Version:
Store management library based on react context and observers
1,186 lines (1,162 loc) • 70.8 kB
JavaScript
'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