synapse-storage
Version:
Набор инструментов для управления состоянием и апи-запросами
123 lines (122 loc) • 6.83 kB
TypeScript
import type { Observable } from 'rxjs';
import type { IStorage } from '../../core';
import type { Action, DispatchFunction, EnhancedMiddleware, WatcherFunction } from './dispatcher.module';
import { type ApiRequestState } from './standalone';
/**
* Маркер метода финализации диспетчера.
*
* Имя экшена/вотчера берётся из имени поля класса, но прочитать имена полей можно
* только ПОСЛЕ полного конструирования инстанса (инициализаторы полей derived-класса
* выполняются после конструктора базового класса). Поэтому имена назначаются отдельным
* шагом-финализацией:
*
* 1. **Сборщик `createSynapse(factory)`** вызывает `dispatcher[FINALIZE]()` до старта
* эффектов (эффекты читают `actionType` при сборке пайплайна).
* 2. **Ленивая само-финализация** — страховка для standalone-использования и тестов:
* первый dispatch экшена или первое обращение к реестру `dispatch`/`watchers`
* финализирует инстанс, если это ещё не сделано.
*/
export declare const FINALIZE: unique symbol;
/**
* Вызываемая группа жизненного цикла API-запроса.
*
* Сам вызов группы — это `init` (намерение): сбрасывает статус в `idle` и пробрасывает
* payload намерения дальше эффектам. Жизненный цикл — через методы-поля.
*
* `ofType(d.loadPosts)` ловит ТОЛЬКО init; чтобы среагировать на успех —
* `ofType(d.loadPosts.success)`.
*/
export interface ApiActions<TInitPayload = void> extends DispatchFunction<TInitPayload, TInitPayload> {
loading: DispatchFunction<void, void>;
success: DispatchFunction<void, void>;
failure: DispatchFunction<string, void>;
reset: DispatchFunction<void, void>;
}
/**
* Keyed-вариант: статус хранится по ключу. `init`/`loading`/`success`/`reset` принимают
* `key`, `failure` — `{ key, error }`.
*/
export interface KeyedApiActions<TInitPayload extends {
key: string;
} = {
key: string;
}> extends DispatchFunction<TInitPayload, TInitPayload> {
loading: DispatchFunction<string, string>;
success: DispatchFunction<string, string>;
failure: DispatchFunction<{
key: string;
error: string;
}, {
key: string;
error: string;
}>;
reset: DispatchFunction<string, string>;
}
/** Опции конструктора базового диспетчера. */
export interface DispatcherBaseOptions<TState extends Record<string, any>> {
middlewares?: EnhancedMiddleware<TState>[];
}
/**
* Публичный class-based слой диспетчера. Экшены объявляются как поля класса через
* фабрики `this.action` / `this.signal` / `this.apiActions` / `this.keyedApiActions`
* / `this.watcher`. Имя экшена = имя поля.
*
* Внутреннее состояние базы — hard-private (`#`-поля/методы): их имена в отдельном
* namespace и НЕ конфликтуют с полями-экшенами подкласса. Запрещённые имена экшенов —
* только `protected`/публичная поверхность из `RESERVED_NAMES`.
*
* @example
* ```ts
* class PostsDispatcher extends Dispatcher<PostsState> {
* readonly loadPosts = this.apiActions<PostsFindAllParams>((s) => s.api.postsRequest)
* readonly mounted = this.signal<FeedLifecyclePayload>('Лента смонтирована')
* readonly applyPosts = this.action((store, page: PostsFeedResponseDto) =>
* store.update((s) => { s.list = page.data }))
* }
* ```
*/
export declare abstract class Dispatcher<TState extends Record<string, any>> {
#private;
protected readonly storage: IStorage<TState>;
/** Поток всех экшенов модуля (его потребляет EffectsModule). */
readonly action$: Observable<Action>;
constructor(storage: IStorage<TState>, options?: DispatcherBaseOptions<TState>);
/** Реестр экшенов по имени — для middleware/devtools. */
get dispatch(): Record<string, DispatchFunction<any, any>>;
get watchers(): Record<string, WatcherFunction<any>>;
/** Алиас потока экшенов для совместимости с EffectsModule (`dispatcher.actions`). */
get actions(): Observable<Action>;
/**
* Экшен: handler в «рецептной» сигнатуре `(storage, params) => result`.
* payload экшена = возвращаемое значение handler'а.
*/
protected action<TParams = void, TResult = void>(handler: (storage: IStorage<TState>, params: TParams) => TResult | Promise<TResult>, options?: {
type?: string;
meta?: Record<string, any>;
memoize?: (cur: TParams, prev: TParams, prevResult: TResult) => boolean;
}): DispatchFunction<TParams, TResult>;
/** Чистый сигнал: `(_store, p) => p`. `description` уходит в meta. */
protected signal<TPayload = void>(description?: string): DispatchFunction<TPayload, TPayload>;
/** Вызываемая группа жизненного цикла API-запроса. Сам вызов = init (намерение). */
protected apiActions<TInitPayload = void>(accessor: (state: TState) => ApiRequestState): ApiActions<TInitPayload>;
/** То же для статусов по ключу (`Record<string, ApiRequestState>`). */
protected keyedApiActions<TInitPayload extends {
key: string;
} = {
key: string;
}>(accessor: (state: TState) => Record<string, ApiRequestState>): KeyedApiActions<TInitPayload>;
protected watcher<R>(config: {
selector: (state: TState) => R;
shouldTrigger?: (prev: R | undefined, current: R) => boolean;
notifyAfterSubscribe?: boolean;
type?: string;
meta?: Record<string, any>;
}): WatcherFunction<R>;
use(...middlewares: EnhancedMiddleware<TState>[]): this;
destroy(): void;
/**
* Финализация: скан own enumerable полей, назначение имён (`_assignType(имя поля)`)
* и регистрация в реестрах `dispatch`/`watchers`. Идемпотентна.
*/
[FINALIZE](): void;
}