synapse-storage
Version:
Набор инструментов для управления состоянием и апи-запросами
110 lines (105 loc) • 6.63 kB
JavaScript
import { EFFECT_NAME, EFFECT_OPTIONS } from "./effects.module.js";
/**
* Маркер «продукта `this.effect`» на функции-рецепте. По его отсутствию dev-проверка
* находит поля-функции, которые забыли обернуть в `this.effect(...)` (иначе они не
* попадут в реестр и молча не запустятся).
* @internal
*/ const EFFECT_MARKER = Symbol('synapse.effect.recipe');
/** Имена членов базового класса — не считаются «забытыми эффектами». */ const RESERVED_NAMES = new Set([
'onDestroy'
]);
/**
* Публичный class-based слой эффектов. Эффекты объявляются как поля класса через
* `this.effect(fn)`. Сервисы и внешние сторы передаются через конструктор и
* захватываются в замыкание рецепта (`this.api`, `this.core$`).
*
* @example
* ```ts
* class PostsEffects extends Effects<PostsState, PostsDispatcher> {
* constructor(private readonly api: PostsEndpoints, private readonly core$: Observable<CoreState>) {
* super()
* }
*
* readonly loadPosts = this.effect((action$, state$, { dispatcher: d }) =>
* action$.pipe(ofType(d.loadPosts), validateMap({ apiCall: () => fromRequest(this.api.getPosts.request()) })))
*
* override onDestroy() { this.socket.disconnect() }
* }
* ```
*
* **Правило**: сервисы из конструктора (`this.api`) можно *захватывать в замыкание*
* рецепта, но не дереференсить прямо в инициализаторе поля — parameter properties
* присваиваются ПОСЛЕ инициализаторов полей derived-класса.
*/ class Effects {
/**
* Опт-аут для dev-проверки «забытых эффектов»: имена полей-функций, которые НЕ являются
* эффектами (конструкторно-инъектированные зависимости — геттеры/фабрики, напр.
* `resolveSocket: () => Socket`). Такие поля — не рецепты `this.effect(...)`, и без этого
* списка `getEffects()` ложно ворнил бы на них.
*
* @example
* ```ts
* class PresenceEffects extends Effects<PresenceState, PresenceDispatcher> {
* static override nonEffectFields = ['resolveSocket']
* constructor(private resolveSocket: () => PresenceSocketService) { super() }
* connection = this.effect(...)
* }
* ```
*/ static nonEffectFields = [];
/** Реестр module-совместимых эффектов в порядке объявления полей. */ #effects = [];
/**
* Регистрирует рецепт эффекта. Сам рецепт НЕ вызывается при конструировании — он
* вызывается лениво при `EffectsModule.start()` с реальными потоками. Возвращает тот
* же `fn`, так что поле остаётся вызываемым рецептом (удобно для юнит-тестов в изоляции).
*
* @param fn рецепт `(action$, state$, ctx) => Observable`
* @param options опции эффекта (например, `resubscribeOnError`)
*/ effect(fn, options) {
// Module-совместимая обёртка: широкий контекст EffectsModule → узкий EffectCtx рецепта.
const moduleEffect = (action$, state$, context)=>fn(action$, state$, {
dispatcher: context.dispatcher,
external: context.externalDispatchers
});
if (options) {
;
moduleEffect[EFFECT_OPTIONS] = options;
}
this.#effects.push(moduleEffect);
fn[EFFECT_MARKER] = true;
return fn;
}
/**
* Список module-совместимых эффектов в порядке объявления полей. Сборщик скармливает
* его в `effectsModule.addEffects(...)`.
*
* Попутно (вне production) предупреждает о полях-функциях, не обёрнутых в `this.effect`.
* @internal
*/ getEffects() {
// Имена полей-рецептов идут в порядке объявления — ровно как и #effects (каждый
// this.effect() пушит в #effects и помечает свой fn). Зипуем их, чтобы EffectsModule
// мог назвать упавший эффект по имени поля, а не по индексу.
// Опт-аут подкласса: конструкторно-инъектированные функции-зависимости, помеченные
// `static nonEffectFields`, не считаем «забытыми эффектами».
const ignoredNames = new Set([
...RESERVED_NAMES,
...this.constructor.nonEffectFields ?? []
]);
const recipeNames = [];
for (const [name, value] of Object.entries(this)){
if (typeof value === 'function' && value[EFFECT_MARKER]) {
recipeNames.push(name);
} else if (process.env.NODE_ENV !== 'production' && typeof value === 'function' && !ignoredNames.has(name)) {
// Fail-fast (dev): поле-функция не обёрнута в this.effect — иначе эффект молча не запустится.
// Если это конструкторный хелпер-зависимость — объяви его в `static nonEffectFields`.
throw new Error(`Effects: поле "${name}" — функция, но не обёрнута в this.effect(...). ` + 'Оборачивай эффекты в this.effect(...), либо, если это хелпер-зависимость, объяви его в ' + '`static nonEffectFields = [...]`. Иначе эффект не зарегистрируется и молча не запустится.');
}
}
this.#effects.forEach((moduleEffect, i)=>{
const name = recipeNames[i];
if (name) moduleEffect[EFFECT_NAME] = name;
});
return this.#effects;
}
}
export { Effects };
//# sourceMappingURL=effects.base.js.map