UNPKG

synapse-storage

Version:

Набор инструментов для управления состоянием и апи-запросами

325 lines (318 loc) 14 kB
import { Observable, Subject, share } from "rxjs"; import { handleCallbackError } from "../../_utils/error-handling.util.js"; /** * Внутренний движок диспетчера для интеграции хранилищ с реактивной системой. * * Прежнее имя — `Dispatcher`. Переименован в `DispatcherCore`, поскольку публичным * `Dispatcher` теперь является abstract class-based слой (`dispatcher.base.ts`), * который строится поверх этого движка. * * `TActionsFn` — фантомный слот для вывода форм экшенов (`ActionsResult`); сам движок * его не использует в рантайме. */ class DispatcherCore { // Поток действий actions$ = new Subject(); // Публичный Observable для действий actions = this.actions$.asObservable(); // Методы диспетчеризации действий с типизацией dispatch = {}; // Watcher'ы для реактивной подписки на изменения watchers = {}; // Ссылка на хранилище storage; // Только один массив для хранения инициализированных middleware middlewareFunctions = []; // Pre-built middleware chain (null if no middlewares) dispatchChain = null; // Registry of action executors by action type actionRegistry = new Map(); // Temporary storage for passing params through middleware chain without polluting action object actionParams = new WeakMap(); // API для инициализации middleware middlewareAPI; /** * Создает новый экземпляр Dispatcher */ constructor(options){ this.storage = options.storage; // Создаем API для middleware сразу this.middlewareAPI = { getState: ()=>this.storage.getState(), dispatch: (action)=>this.executeChain(action), storage: this.storage, actions$: this.actions, actions: this.dispatch, watchers: this.watchers, findActionByType: (type)=>this.findActionByType(type), findWatcherByType: (type)=>this.findWatcherByType(type) }; // Если есть middleware в options, добавляем их if (options.middlewares && options.middlewares.length > 0) { this.use(...options.middlewares); } } /** * Добавляет middleware в цепочку обработки */ use(...middlewares) { // Инициализируем каждый middleware и добавляем только инициализированную версию for(let i = 0; i < middlewares.length; i++){ try { // Инициализируем middleware с API const initializedMiddleware = middlewares[i](this.middlewareAPI); this.middlewareFunctions.push(initializedMiddleware); } catch (error) { handleCallbackError(`Dispatcher: error initializing middleware [${i}]`, error); } } this.rebuildChain(); return this; } /** * Base executor: runs the registered action handler or passes through */ baseExecute = async (action)=>{ // No params in WeakMap means this is a pass-through dispatch (via api.dispatch) if (!this.actionParams.has(action)) { return action.payload; } const entry = this.actionRegistry.get(action.type); if (!entry) { return action.payload; } const params = this.actionParams.get(action); return Promise.resolve(entry.action(params)); }; /** * Executes action through middleware chain and emits to actions$ (single emission point) */ async executeChain(action) { const executor = this.dispatchChain ?? this.baseExecute; const result = await executor(action); // Single emission point for all dispatches action.payload = result; this.actionParams.delete(action); this.actions$.next(action); return result; } /** * Rebuilds the pre-composed middleware chain. Called once on use(), not on every dispatch. */ rebuildChain() { if (this.middlewareFunctions.length === 0) { this.dispatchChain = null; return; } let chain = this.baseExecute; for(let i = this.middlewareFunctions.length - 1; i >= 0; i--){ chain = this.middlewareFunctions[i](chain); } this.dispatchChain = chain; } /** * Получает все действия с улучшенной типизацией */ getActions() { return this.dispatch; } /** * Находит действие по типу */ findActionByType(actionType) { return Object.values(this.dispatch).find((action)=>{ return action.actionType.split(`[${this.storage.name}]`)[1] === actionType; }); } /** * Находит наблюдатель по типу */ findWatcherByType(actionType) { return Object.values(this.watchers).find((watcher)=>{ return watcher.actionType.split(`[${this.storage.name}]`)[1] === actionType; }); } /** * Уничтожает dispatcher: отписывает watchers, завершает поток действий, очищает состояние */ destroy() { Object.values(this.watchers).forEach((w)=>w.unsubscribe()); this.actions$.complete(); this.dispatch = {}; this.watchers = {}; this.middlewareFunctions = []; this.dispatchChain = null; this.actionRegistry.clear(); } /** * Создает действие */ createAction(actionConfig, executionOptions) { const hasExplicitType = !!actionConfig.type; let actionType = hasExplicitType ? `[${this.storage.name}]${actionConfig.type}` : ''; // Register action executor in the registry (сразу, если тип известен) if (hasExplicitType) { this.actionRegistry.set(actionType, { action: actionConfig.action }); } // Для мемоизации храним последние аргументы и результат let lastParams; let lastResult; let hasCached = false; // Создаем функцию диспетчеризации const dispatchFn = async (params)=>{ if (!actionType) { throw new Error('Action type not assigned. Provide "type" in config or declare the action as a field of a Dispatcher subclass.'); } // Проверяем мемоизацию if (executionOptions?.memoize && hasCached) { if (executionOptions.memoize(params, lastParams, lastResult)) { return lastResult; } } // Создаем объект действия const actionObject = { type: actionType, meta: actionConfig.meta }; // Store params for base executor to retrieve via WeakMap this.actionParams.set(actionObject, params); // Execute through middleware chain (includes single emission to actions$) const result = await this.executeChain(actionObject); // Сохраняем аргументы и результат для мемоизации lastParams = params; lastResult = result; hasCached = true; return result; }; dispatchFn._type = 'dispatch'; // Добавляем тип действия как свойство функции (configurable для deferred assignment) Object.defineProperty(dispatchFn, 'actionType', { value: actionType, writable: false, enumerable: true, configurable: true }); // Добавляем метаданные, если они есть if (actionConfig.meta) { Object.defineProperty(dispatchFn, 'meta', { value: actionConfig.meta, writable: false, enumerable: true }); } // Deferred type assignment — используется при финализации Dispatcher для авто-генерации типа из имени поля if (!hasExplicitType) { ; dispatchFn._assignType = (name)=>{ actionType = `[${this.storage.name}]${name}`; this.actionRegistry.set(actionType, { action: actionConfig.action }); Object.defineProperty(dispatchFn, 'actionType', { value: actionType, writable: false, enumerable: true, configurable: true }); }; } return dispatchFn; } /** * Создает watcher для отслеживания изменений в хранилище */ createWatcher(config) { const hasExplicitType = !!config.type; let actionType = hasExplicitType ? `[${this.storage.name}]${config.type}` : ''; // Lazy-подписка: подписываемся на storage только при первом subscribe на Observable let storageUnsubscribe = null; const sharedObservable = new Observable((subscriber)=>{ // Инициализируем prevValue текущим значением при первой подписке let prevValue; let disposed = false; const initAndSubscribe = async ()=>{ try { const currentState = await Promise.resolve(this.storage.getState()); prevValue = config.selector(currentState); } catch { // Если не удалось получить начальное значение — продолжаем с undefined } if (disposed) return; // Если нужно уведомить о текущем значении при подписке if (config.notifyAfterSubscribe && prevValue !== undefined) { if (!config.shouldTrigger || config.shouldTrigger(undefined, prevValue)) { const initialAction = { type: actionType, payload: prevValue, meta: { ...config.meta, isInitial: true } }; this.actions$.next(initialAction); subscriber.next(initialAction); } } // Подписываемся на изменения storage storageUnsubscribe = this.storage.subscribe(config.selector, (value)=>{ if (!config.shouldTrigger || config.shouldTrigger(prevValue, value)) { const action = { type: actionType, payload: value, meta: config.meta }; this.actions$.next(action); subscriber.next(action); prevValue = value; } }); // Если отписались пока шла асинхронная инициализация — сразу очищаем if (disposed) { storageUnsubscribe(); storageUnsubscribe = null; } }; initAndSubscribe(); return ()=>{ disposed = true; if (storageUnsubscribe) { storageUnsubscribe(); storageUnsubscribe = null; } }; }).pipe(share()); // Создаем функцию watcher'а const watcherFn = ()=>sharedObservable; watcherFn._type = 'watchers'; Object.defineProperty(watcherFn, 'actionType', { value: actionType, writable: false, enumerable: true, configurable: true }); if (config.meta) { Object.defineProperty(watcherFn, 'meta', { value: config.meta, writable: false, enumerable: true }); } Object.defineProperty(watcherFn, 'unsubscribe', { value: ()=>{ if (storageUnsubscribe) { storageUnsubscribe(); storageUnsubscribe = null; } }, writable: false, enumerable: true }); // Deferred type assignment — используется при финализации Dispatcher для авто-генерации типа из имени поля if (!hasExplicitType) { ; watcherFn._assignType = (name)=>{ actionType = `[${this.storage.name}]${name}`; Object.defineProperty(watcherFn, 'actionType', { value: actionType, writable: false, enumerable: true, configurable: true }); }; } //@ts-ignore return watcherFn; } } export { DispatcherCore }; //# sourceMappingURL=dispatcher.module.js.map