UNPKG

synapse-storage

Version:

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

110 lines (105 loc) 6.63 kB
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