dreamstate
Version:
Store management library based on react context and observers
448 lines (432 loc) • 20.4 kB
JavaScript
;
var createProvider = require('./lib.js');
var react = require('react');
require('tslib');
require('shallow-equal');
/**
* Factory function that creates a bound method descriptor for a given method.
* This ensures the method is bound to the instance, preserving the correct `this` context when invoked.
*
* @template T The type of the method being bound.
* @param {TypedPropertyDescriptor<T>} from The original typed property descriptor of the method to bind.
* @param {PropertyKey} property The property key of the method being modified.
* @returns {PropertyDescriptor} A new property descriptor with the method bound to the instance.
*/
function createBoundDescriptor(from, property) {
// Todo: Wait for autobind merge with fix of shared callbacks issue and other.
var definingProperty = false;
return {
configurable: true,
get: function () {
if (definingProperty
/*
this === target.prototype || - will it fire? Check parent prototypes?
Object.prototype.hasOwnProperty.call(this, property) ||
typeof from.value !== "function"
*/) {
return from.value;
}
// Expect only functions to be called, throw errors on other cases.
var bound = from.value.bind(this);
definingProperty = true;
Object.defineProperty(this, property, {
configurable: true,
writable: false,
value: bound
});
definingProperty = false;
return bound;
},
set: function () {
throw new createProvider.DreamstateError(createProvider.EDreamstateErrorCode.RESTRICTED_OPERATION, "Direct runtime modification of bound method is not allowed.");
}
};
}
/**
* Decorator factory that modifies the method descriptor to bind the method to the prototype instance.
* This ensures that the method retains the correct `this` context when invoked.
*
* All credits: 'https://www.npmjs.com/package/autobind-decorator'.
* Modified to support proposal syntax.
*
* @returns {MethodDecorator} A method decorator that binds the method to the instance prototype.
*/
function Bind() {
return function (targetOrDescriptor, propertyKey, descriptor) {
// Different behaviour for legacy and proposal decorators.
if (propertyKey && descriptor) {
return createBoundDescriptor(descriptor, propertyKey);
} else {
targetOrDescriptor.descriptor = createBoundDescriptor(targetOrDescriptor.descriptor, targetOrDescriptor.key);
}
};
}
// Todo: Wait for proper proposal decorators.
// Todo: Tests.
function createMethodDecorator(resolver) {
return function (prototypeOrDescriptor, method) {
if (prototypeOrDescriptor && method) {
resolver(method, prototypeOrDescriptor.constructor);
return prototypeOrDescriptor;
} else {
prototypeOrDescriptor.finisher = function (targetClass) {
resolver(prototypeOrDescriptor.key, targetClass);
};
return prototypeOrDescriptor;
}
};
}
/**
* Class method decorator factory that marks the decorated method as a handler for specified query types.
*
* This decorator ensures that the decorated method will be invoked when a query of the specified type(s)
* is triggered within the current scope. It supports handling a single query type or an array of query types.
* The supported query types include `string`, `number`, and `symbol`.
*
* @param {TQueryType | Array<TQueryType>} queryType - The query type or an array of query types
* that the decorated method will handle.
* @returns {MethodDecorator} A method decorator that attaches the query handler functionality to the method.
*/
function OnQuery(queryType) {
if (!createProvider.isCorrectQueryType(queryType)) {
throw new createProvider.DreamstateError(createProvider.EDreamstateErrorCode.INCORRECT_QUERY_TYPE, typeof queryType);
}
/*
* Support old and new decorators with polyfill.
*/
return createMethodDecorator(function (method, ManagerClass) {
if (!(ManagerClass.prototype instanceof createProvider.ContextManager)) {
throw new createProvider.DreamstateError(createProvider.EDreamstateErrorCode.TARGET_CONTEXT_MANAGER_EXPECTED, "Only ContextManager extending classes methods can be decorated as query handlers.");
}
if (createProvider.QUERY_METADATA_REGISTRY.has(ManagerClass)) {
createProvider.QUERY_METADATA_REGISTRY.get(ManagerClass).push([method, queryType]);
} else {
createProvider.QUERY_METADATA_REGISTRY.set(ManagerClass, [[method, queryType]]);
}
});
}
/**
* Class method decorator factory that marks the decorated method as a handler for specified signal types.
*
* This decorator ensures that the decorated method is invoked when a signal of the specified type(s)
* is emitted within the current scope. It supports handling a single signal type or an array of signal types.
*
* Supported signal types include: `string`, `number`, and `symbol`.
*
* @param {(TSignalType | Array<TSignalType>)} signalType - The signal type or an array of signal types
* that the decorated method will handle.
* @returns {MethodDecorator} A method decorator that attaches the handler functionality to the method.
*/
function OnSignal(signalType) {
/*
* If Array:
* - Check not empty
* - Validate all elements
* If single type:
* - Check the only value
*/
if (Array.isArray(signalType) ? signalType.length === 0 || signalType.some(function (it) {
return !createProvider.isCorrectSignalType(it);
}) : !createProvider.isCorrectSignalType(signalType)) {
throw new createProvider.DreamstateError(createProvider.EDreamstateErrorCode.INCORRECT_PARAMETER, "Unexpected signal type provided, expected symbol, string, number or array of it. Got: ".concat(typeof signalType, "."));
}
/*
* Support old and new decorators with polyfill.
*/
return createMethodDecorator(function (method, ManagerClass) {
if (!(ManagerClass.prototype instanceof createProvider.ContextManager)) {
throw new createProvider.DreamstateError(createProvider.EDreamstateErrorCode.INCORRECT_PARAMETER, "Only ContextManager extending classes methods can be decorated as handlers.");
}
if (createProvider.SIGNAL_METADATA_REGISTRY.has(ManagerClass)) {
createProvider.SIGNAL_METADATA_REGISTRY.get(ManagerClass).push([method, signalType]);
} else {
createProvider.SIGNAL_METADATA_REGISTRY.set(ManagerClass, [[method, signalType]]);
}
});
}
/**
* Lazy initializer of current scope context provider props object that preserves object references.
* Composes props object that will be same for each tree re-render.
*
* @returns {ProviderProps<IScopeContext>} The initialized props object for the scope provider.
*/
function scopeStateInitializer() {
return {
value: createProvider.createScope()
};
}
/**
* Provides an isolated scope for signaling and context managers.
*
* The `ScopeProvider` component wraps its children within a dedicated scope, ensuring that signals
* and context managers operate independently from other parts of the React tree. This isolation
* helps prevent interference between different parts of the application and maintains the integrity
* of context data and signal handling.
*
* @param {IScopeProviderProps} props - The properties for the scope provider, including the children
* to be rendered within the isolated scope.
* @returns {ReactElement} A React element representing the scope provider.
*/
function ScopeProvider(props) {
var scopeState = react.useState(scopeStateInitializer);
return react.createElement(createProvider.ScopeContext.Provider, scopeState[0], props.children);
}
/*
* Easier devtools usage for dev environment.
*/
{
ScopeProvider.displayName = "Dreamstate.ScopeProvider";
}
/**
* Creates an actions store, which is an object containing readonly method links representing actions.
* The intention is to provide a container that is visually and programmatically distinguishable as
* a storage of actions.
*
* Every call to 'setContext' will perform a comparison of the current 'context' before updating,
* excluding the actions object, as it is expected to be immutable and consistent.
*
* @template T The type of actions object.
* @param {T} actions - An object containing a set of mutation operations (actions).
* @returns {Readonly<T>} An instance of an ActionsStore class containing the supplied actions.
*/
function createActions(actions) {
if (createProvider.isObject(actions)) {
return new createProvider.ActionsStore(actions);
} else {
throw new createProvider.DreamstateError(createProvider.EDreamstateErrorCode.INCORRECT_PARAMETER, "Actions store should be initialized with an object, got '".concat(typeof actions, "' instead."));
}
}
/**
* Creates a computed value that will be re-calculated after each context update.
* The computed value is recalculated only when its dependencies, as determined by the memo function,
* are updated.
*
* @template T The type of the computed value.
* @template C The type of the context the computed value depends on.
* @param {Function} selector A generic selector function that returns computed values on update.
* @param {Function} memo An optional memo checker function that returns an array of dependencies,
* indicating whether the computed value should be updated.
* @returns {TComputed<T, C>} A computed value object that will be updated based on context changes.
*/
function createComputed(selector, memo) {
if (createProvider.isFunction(selector) && (createProvider.isUndefined(memo) || createProvider.isFunction(memo))) {
// Cast computed to T & TComputed since it works like state object later.
return new createProvider.ComputedValue(selector, memo);
} else {
throw new createProvider.DreamstateError(createProvider.EDreamstateErrorCode.INCORRECT_PARAMETER, "Computed value should be initialized with functional selector and optional memo function.");
}
}
/**
* A utility class for extending nested stores with loadable state and enabling shallow checking.
*
* This class is used by the `ContextManager` to manage asynchronous data, track loading states,
* and detect changes efficiently. It helps in handling data fetching scenarios where state
* needs to reflect loading, success, or error conditions.
*/
var LoadableStore = /** @class */function () {
function LoadableStore(value, isLoading, error) {
this.value = value;
this.isLoading = isLoading;
this.error = error;
}
/**
* Optionally sets a value for the loading state.
* If provided, it updates the loading state with the given value.
*
* @template T - The type of the loading state value.
* @template E - The type of error state, if any.
* @param {T | undefined} value - The value to set for the loading state. If not provided, the loading
* state will remain unchanged.
* @returns {ILoadable<T, E>} The updated loadable instance with the new loading state.
*/
LoadableStore.prototype.asLoading = function (value) {
return new LoadableStore(arguments.length > 0 ? value : this.value, true, null);
};
/**
* Optionally sets a value for the failed state.
* If provided, it updates the state to reflect a failure and optionally includes the provided value.
*
* @template T - The type of the loading state value.
* @template E - The type of the error state.
* @param {E} error - The error object representing the failure state.
* @param {T | undefined} value - Optional value to associate with the failed state. If not provided,
* the failure state will only reflect the error.
* @returns {ILoadable<T, E>} The updated loadable instance with the failed state.
*/
LoadableStore.prototype.asFailed = function (error, value) {
return new LoadableStore(arguments.length > 1 ? value : this.value, false, error);
};
/**
* Optionally sets a value for the ready state.
* If provided, it updates the state to reflect readiness and optionally includes the provided value.
*
* @template T - The type of the loading state value.
* @template E - The type of the error state.
* @param {T | undefined} value - Optional value to associate with the ready state. If not provided,
* the ready state will reflect only the readiness.
* @returns {ILoadable<T, E>} The updated loadable instance with the ready state.
*/
LoadableStore.prototype.asReady = function (value) {
return new LoadableStore(arguments.length > 0 ? value : this.value, false, null);
};
/**
* Optionally sets the loading state and error for an update.
* This method allows updating the loadable state with a new value, and optionally sets
* the loading state and any associated error.
*
* @template T - The type of the loading state value.
* @template E - The type of the error state.
* @param {T} value - The new value to associate with the updated state.
* @param {boolean} [isLoading] - Optional flag to set the loading state. Defaults to `false` if not provided.
* @param {E | null} [error] - Optional error to associate with the update. Defaults to `null` if not provided.
* @returns {ILoadable<T, E>} The updated loadable instance with the new value, loading state, and error.
*/
LoadableStore.prototype.asUpdated = function (value, isLoading, error) {
return new LoadableStore(value, arguments.length > 1 ? isLoading : this.isLoading, arguments.length > 2 ? error : this.error);
};
return LoadableStore;
}();
/**
* Creates a loadable value, which is useful when the context value has error/loading states.
* The loadable value can represent different states: loading, ready, or error.
*
* @template T The type of the value being loaded.
* @template E The type of the error (defaults to `Error`).
* @param {T | null} [value] The initial value or `null` if not yet loaded.
* @param {boolean} [isLoading] A flag indicating whether the value is in a loading state.
* @param {E | null} [error] The error if the value failed to load, or `null` if no error.
* @returns {ILoadable<T, E>} A loadable value utility representing the current state of the value.
*/
function createLoadable(value, isLoading, error) {
if (value === void 0) {
value = null;
}
if (isLoading === void 0) {
isLoading = false;
}
if (error === void 0) {
error = null;
}
return new LoadableStore(value, isLoading, error);
}
/**
* Creates a nested sub-state for deeper shallow checking, useful when the context contains nested objects
* that need to be checked separately during updates.
*
* As an example:
* - `{ first: 'first', second: { one: 1, two: 2 } }` - `first` and `second` will be checked,
* while `one` and `two` will be ignored.
* - `{ first: 'first', second: createNested({ one: 1, two: 2 }) }` - `first`, `one`, and `two`
* will be checked during updates.
*
* @template T The type of the nested object.
* @param {T} initialValue The initial value of the nested store object.
* @returns {TNested<T>} An instance of a nested store containing the initial state, marked for deeper shallow checking.
*/
function createNested(initialValue) {
if (createProvider.isObject(initialValue)) {
return Object.assign(new createProvider.NestedStore(), initialValue);
} else {
throw new createProvider.DreamstateError(createProvider.EDreamstateErrorCode.INCORRECT_PARAMETER, "Nested stores should be initialized with an object, got '".concat(typeof initialValue, "' instead."));
}
}
/**
* A custom hook that subscribes to context updates with memoization.
*
* This hook functions similarly to the standard `useContext` hook but adds memoization based on a dependency selector.
* It is particularly useful when a context manager contains a large or frequently changing state,
* yet a component only requires updates for specific parts of that state.
*
* @template T - The type of the context state object.
* @template D - The type of the context manager constructor that provides the context state.
* @param {D} ManagerClass - The class constructor for the context manager which supplies the context state.
* @param {(context: T) => unknown[]} dependenciesSelector - A selector of dependencies from the context state.
* The hook will re-render the component only when these selected dependencies change.
* @returns {T} The current context state, memoized based on the provided dependencies.
*/
function useContextWithMemo(ManagerClass, dependenciesSelector) {
var scope = react.useContext(createProvider.ScopeContext);
var state = react.useState(function () {
return scope.INTERNAL.REGISTRY.CONTEXT_STATES_REGISTRY.get(ManagerClass);
});
/*
* Fire state change only if any of dependencies is updated.
*/
react.useEffect(function () {
var initialState = state[0];
var setState = state[1];
var subscriptionState = scope.INTERNAL.REGISTRY.CONTEXT_STATES_REGISTRY.get(ManagerClass) || null;
// Flag `null` if HMR/StrictMode reset happen, usually just means HMR manager replacing or react 18 strict mode.
var observed = subscriptionState ? dependenciesSelector(subscriptionState) : null;
/*
* Expected to be skipped first time, when state is picked with selector from registry.
* Expected to be fired every time ManagerClass is changed - when HMR is called (state is same, effect triggered).
*/
if (initialState !== subscriptionState) {
setState(subscriptionState);
}
return scope.INTERNAL.subscribeToManager(ManagerClass, function (nextContext) {
if (!observed) {
observed = dependenciesSelector(nextContext);
return setState(nextContext);
}
var nextObserved = dependenciesSelector(nextContext);
for (var it_1 = 0; it_1 < nextObserved.length; it_1++) {
if (observed[it_1] !== nextObserved[it_1]) {
observed = nextObserved;
return setState(nextContext);
}
}
});
}, [ManagerClass, scope.INTERNAL]);
return state[0];
}
/**
* Custom hook that wraps `useContext` to provide scoped context data with optional update
* optimization via a dependency selector. It returns the context from the specified manager
* class and limits re-renders to changes in selected dependencies.
*
* @template T - The type of the context state.
* @template D - The type of the context manager constructor.
* @param {D} ManagerClass - The manager class whose instance context is returned.
* @param {(context: D["prototype"]["context"]) => TAnyValue[]} dependenciesSelector - An optional function
* that receives the current context and returns an array of dependencies. The component re-renders
* only if values in this array change. Without it, the component updates on every context change.
* @returns {D["prototype"]["context"]} The context data provided by the manager within the current
* dreamstate scope.
*/
function useManager(ManagerClass, dependenciesSelector) {
/*
* Use pub-sub + checking approach only if dependency selector was provided.
* If component should update on every change there is no point of doing anything additional to default context.
*/
return dependenciesSelector ? useContextWithMemo(ManagerClass, dependenciesSelector) : react.useContext(ManagerClass.REACT_CONTEXT);
}
/**
* Custom hook that retrieves the current scope context.
* This hook provides access to the current scope in the React tree. It returns a bundle of
* functions and data that allow for processing data, signals and queries within that scope.
*
* @returns {IScopeContext} The current scope context in the React tree.
*/
function useScope() {
return react.useContext(createProvider.ScopeContext);
}
exports.ContextManager = createProvider.ContextManager;
exports.DreamstateError = createProvider.DreamstateError;
Object.defineProperty(exports, "DreamstateErrorCode", {
enumerable: true,
get: function () { return createProvider.EDreamstateErrorCode; }
});
exports.createProvider = createProvider.createProvider;
exports.createScope = createProvider.createScope;
exports.Bind = Bind;
exports.OnQuery = OnQuery;
exports.OnSignal = OnSignal;
exports.ScopeProvider = ScopeProvider;
exports.createActions = createActions;
exports.createComputed = createComputed;
exports.createLoadable = createLoadable;
exports.createNested = createNested;
exports.useManager = useManager;
exports.useScope = useScope;