synapse-storage
Version:
Набор инструментов для управления состоянием и апи-запросами
79 lines (78 loc) • 5.02 kB
TypeScript
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>;
}