synapse-storage
Version:
Набор инструментов для управления состоянием и апи-запросами
325 lines (318 loc) • 14 kB
JavaScript
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