UNPKG

synapse-storage

Version:

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

79 lines (78 loc) 5.02 kB
import type { Observable } from 'rxjs'; import type { Action } from '../dispatcher'; import type { Dispatcher } from '../dispatcher/dispatcher.base'; import { type Effect, type EffectOptions } from './effects.module'; /** * Контекст эффекта в class-стиле — узкий надтип контекста EffectsModule. Рецепт читает * только эти два поля; остальное (services, externalStates) приходит через конструктор * класса и захватывается замыканием. */ export interface EffectCtx<TDispatcher, TExternalDispatchers = Record<string, never>> { /** Инстанс нашего class-диспетчера: `ofType(d.loadPosts)` + `d.applyPosts(...)`. */ dispatcher: TDispatcher; /** Внешние диспетчеры (их экшены уже влиты в общий `action$`). */ external: TExternalDispatchers; } /** Рецепт эффекта: отложенная функция, вызываемая один раз при `EffectsModule.start()`. */ export type EffectRecipe<TState, TDispatcher, TExternalDispatchers> = (action$: Observable<Action>, state$: Observable<TState>, ctx: EffectCtx<TDispatcher, TExternalDispatchers>) => Observable<unknown>; /** * Публичный 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-класса. */ export declare abstract class Effects<TState extends Record<string, any>, TDispatcher, TExternalDispatchers extends Record<string, Dispatcher<any>> = Record<string, never>> { #private; /** * Опт-аут для 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: string[]; /** * Регистрирует рецепт эффекта. Сам рецепт НЕ вызывается при конструировании — он * вызывается лениво при `EffectsModule.start()` с реальными потоками. Возвращает тот * же `fn`, так что поле остаётся вызываемым рецептом (удобно для юнит-тестов в изоляции). * * @param fn рецепт `(action$, state$, ctx) => Observable` * @param options опции эффекта (например, `resubscribeOnError`) */ protected effect(fn: EffectRecipe<TState, TDispatcher, TExternalDispatchers>, options?: EffectOptions): EffectRecipe<TState, TDispatcher, TExternalDispatchers>; /** * Список module-совместимых эффектов в порядке объявления полей. Сборщик скармливает * его в `effectsModule.addEffects(...)`. * * Попутно (вне production) предупреждает о полях-функциях, не обёрнутых в `this.effect`. * @internal */ getEffects(): Effect[]; /** Опциональный teardown (закрыть сокет и т.п.) — вызывается сборщиком при `synapse.destroy()`. */ onDestroy?(): void | Promise<void>; }