Middleware
Подключаемый наблюдатель, прицепленный к каждому триггеру в рантайме. Семь хуков срабатывают в предсказуемых точках вокруг диспатча — три из них (onFire, onSkip, onError) часто складываются в полный аудит-трейл; остальные (onBeforeMatch, onActionStart, onActionEnd, onCascade) — для трассировки, метрик и devtools.
Все хуки необязательные. Исключение, выброшенное в хуке, ловится и логируется рантаймом, но не прерывает запуск — middleware не должен быть load-bearing.
import type { Middleware } from '@triggery/core';
Определение
Заголовок раздела «Определение»type Middleware = {
readonly name: string;
onFire?(ctx: FireContext): void | { cancel: true; reason: string };
onBeforeMatch?(ctx: MatchContext): void;
onSkip?(ctx: SkipContext): void;
onActionStart?(ctx: ActionContext): void;
onActionEnd?(ctx: ActionContext & { durationMs: number; result?: unknown }): void;
onError?(ctx: ActionContext & { error: unknown }): void;
onCascade?(ctx: CascadeContext): void;
};name — string
Заголовок раздела «name — string»Стабильный id middleware. Обязателен. Полезен в сообщениях об ошибках и в панелях devtools.
onFire(ctx) -> void | { cancel: true; reason: string }
Заголовок раздела «onFire(ctx) -> void | { cancel: true; reason: string }»Вызывается один раз на каждый fire(eventName, payload) верхнего уровня (или каскадный) перед тем, как сматчится любой триггер. Возврат { cancel: true, reason } прерывает весь диспатч — триггеры не запускаются, и запись в стиле 'overflow' не пишется. Это единственный хук с коротким замыканием.
Поле FireContext | Описание |
|---|---|
eventName | Диспатчируемое событие. |
payload | Полезная нагрузка, непрозрачная для middleware. |
cascadeDepth | 0 для срабатываний верхнего уровня; n внутри каскада. |
parentRunId? | Установлен, если fire происходит внутри работающего обработчика. |
parentTriggerId? | То же — id upstream-триггера. |
parentContext? | Непрозрачная ссылка-связный список для детекта циклов. |
onBeforeMatch(ctx) -> void
Заголовок раздела «onBeforeMatch(ctx) -> void»Вызывается на каждую пару (event, trigger) сразу после того, как диспатч достал триггер из индекса событий — до любой concurrency-gate или проверки required. Чисто наблюдательный; полезен для «какие триггеры были рассмотрены для этого события» без инструментирования onSkip и onActionStart по отдельности.
onSkip(ctx) -> void
Заголовок раздела «onSkip(ctx) -> void»Вызывается, когда триггер сматчился, но был пропущен. reason — один из 'disabled', 'required-missing', 'concurrency-take-first', 'aborted', 'cycle', 'overflow' и т. д. Используй для отслеживания «почему мой триггер не запустился».
onActionStart(ctx) -> void
Заголовок раздела «onActionStart(ctx) -> void»Вызывается прямо перед тем, как обработчик вызовет действие. actionName и payload — аргументы. Используй для трассировки по действиям.
onActionEnd(ctx & { durationMs; result? }) -> void
Заголовок раздела «onActionEnd(ctx & { durationMs; result? }) -> void»Вызывается, когда действие вернёт значение (или его промис разрешится). durationMs — wall-clock-время от onActionStart до onActionEnd. Рантайм замеряет это только когда есть хотя бы один слушатель — без миддлвэров с таймингом затрат нет.
onError(ctx & { error }) -> void
Заголовок раздела «onError(ctx & { error }) -> void»Вызывается, когда действие выбрасывает исключение или его промис реджектится. Паря с onActionStart, обрамляет полную попытку. Ошибки также записываются в снепшот инспектора со status: 'errored'.
onCascade(ctx) -> void
Заголовок раздела «onCascade(ctx) -> void»Вызывается, когда каскадное событие подавлено — kind: 'overflow' (cascadeDepth превысил maxCascadeDepth) или kind: 'cycle' (триггер появляется в своей же цепочке предков).
Примеры
Заголовок раздела «Примеры»Трассировка каждого fire
Заголовок раздела «Трассировка каждого fire»import { createRuntime function createRuntime(options?: RuntimeOptions): Runtime , type Middleware type Middleware = {
readonly name: string;
onFire?(ctx: FireContext): void | {
cancel: true;
reason: string;
};
onBeforeMatch?(ctx: MatchContext): void;
onSkip?(ctx: SkipContext): void;
onActionStart?(ctx: ActionContext): void;
onActionEnd?(ctx: ActionContext & {
durationMs: number;
result?: unknown;
}): void;
onError?(ctx: ActionContext & {
error: unknown;
}): void;
onCascade?(ctx: CascadeContext): void;
}
} from '@triggery/core';
const tracing const tracing: Middleware : Middleware type Middleware = {
readonly name: string;
onFire?(ctx: FireContext): void | {
cancel: true;
reason: string;
};
onBeforeMatch?(ctx: MatchContext): void;
onSkip?(ctx: SkipContext): void;
onActionStart?(ctx: ActionContext): void;
onActionEnd?(ctx: ActionContext & {
durationMs: number;
result?: unknown;
}): void;
onError?(ctx: ActionContext & {
error: unknown;
}): void;
onCascade?(ctx: CascadeContext): void;
}
= {
name name: string : 'tracing',
onFire onFire?(ctx: FireContext): void | {
cancel: true;
reason: string;
}
({ eventName eventName: string , cascadeDepth cascadeDepth: number }) {
console var console: Console .log Console.log(...data: any[]): voidThe **`console.log()`** static method outputs a message to the console.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static) (`[fire] ${eventName eventName: string }${cascadeDepth cascadeDepth: number ? ` (cascade ${cascadeDepth cascadeDepth: number })` : ''}`);
},
onActionEnd onActionEnd?(ctx: ActionContext & {
durationMs: number;
result?: unknown;
}): void
({ triggerId triggerId: string , actionName actionName: string , durationMs durationMs: number }) {
console var console: Console .log Console.log(...data: any[]): voidThe **`console.log()`** static method outputs a message to the console.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static) (` ${triggerId triggerId: string }.${actionName actionName: string } ${durationMs durationMs: number .toFixed Number.toFixed(fractionDigits?: number): stringReturns a string representing a number in fixed-point notation. (2)}ms`);
},
};
const runtime const runtime: Runtime = createRuntime function createRuntime(options?: RuntimeOptions): Runtime ({ middleware middleware?: readonly Middleware[] | undefinedGlobal middleware applied to every trigger in this runtime. : [tracing const tracing: Middleware ] });Аудит ошибок в бэкенд
Заголовок раздела «Аудит ошибок в бэкенд»import { createRuntime function createRuntime(options?: RuntimeOptions): Runtime , type Middleware type Middleware = {
readonly name: string;
onFire?(ctx: FireContext): void | {
cancel: true;
reason: string;
};
onBeforeMatch?(ctx: MatchContext): void;
onSkip?(ctx: SkipContext): void;
onActionStart?(ctx: ActionContext): void;
onActionEnd?(ctx: ActionContext & {
durationMs: number;
result?: unknown;
}): void;
onError?(ctx: ActionContext & {
error: unknown;
}): void;
onCascade?(ctx: CascadeContext): void;
}
} from '@triggery/core';
const audit const audit: Middleware : Middleware type Middleware = {
readonly name: string;
onFire?(ctx: FireContext): void | {
cancel: true;
reason: string;
};
onBeforeMatch?(ctx: MatchContext): void;
onSkip?(ctx: SkipContext): void;
onActionStart?(ctx: ActionContext): void;
onActionEnd?(ctx: ActionContext & {
durationMs: number;
result?: unknown;
}): void;
onError?(ctx: ActionContext & {
error: unknown;
}): void;
onCascade?(ctx: CascadeContext): void;
}
= {
name name: string : 'audit',
onError onError?(ctx: ActionContext & {
error: unknown;
}): void
({ triggerId triggerId: string , actionName actionName: string , error error: unknown }) {
void fetch function fetch(input: string | URL | Request, init?: RequestInit): Promise<Response> (+1 overload)[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch) ('/api/log', {
method RequestInit.method?: string | undefinedA string to set request's method. : 'POST',
body RequestInit.body?: BodyInit | null | undefinedA BodyInit object or null to set request's body. : JSON var JSON: JSONAn intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format. .stringify JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)Converts a JavaScript value to a JavaScript Object Notation (JSON) string. ({
triggerId triggerId: string ,
actionName actionName: string ,
message message: string : error error: unknown instanceof Error var Error: ErrorConstructor ? error error: Error .message Error.message: string : String var String: StringConstructor
(value?: any) => string
Allows manipulation and formatting of text strings and determination and location of substrings within strings. (error error: unknown ),
stack stack: string | undefined : error error: unknown instanceof Error var Error: ErrorConstructor ? error error: Error .stack Error.stack?: string | undefined : undefined var undefined ,
}),
});
},
};
const runtime const runtime: Runtime = createRuntime function createRuntime(options?: RuntimeOptions): Runtime ({ middleware middleware?: readonly Middleware[] | undefinedGlobal middleware applied to every trigger in this runtime. : [audit const audit: Middleware ] });Вето через возврат из onFire
Заголовок раздела «Вето через возврат из onFire»import { createRuntime function createRuntime(options?: RuntimeOptions): Runtime , type Middleware type Middleware = {
readonly name: string;
onFire?(ctx: FireContext): void | {
cancel: true;
reason: string;
};
onBeforeMatch?(ctx: MatchContext): void;
onSkip?(ctx: SkipContext): void;
onActionStart?(ctx: ActionContext): void;
onActionEnd?(ctx: ActionContext & {
durationMs: number;
result?: unknown;
}): void;
onError?(ctx: ActionContext & {
error: unknown;
}): void;
onCascade?(ctx: CascadeContext): void;
}
} from '@triggery/core';
const featureFlag const featureFlag: Middleware : Middleware type Middleware = {
readonly name: string;
onFire?(ctx: FireContext): void | {
cancel: true;
reason: string;
};
onBeforeMatch?(ctx: MatchContext): void;
onSkip?(ctx: SkipContext): void;
onActionStart?(ctx: ActionContext): void;
onActionEnd?(ctx: ActionContext & {
durationMs: number;
result?: unknown;
}): void;
onError?(ctx: ActionContext & {
error: unknown;
}): void;
onCascade?(ctx: CascadeContext): void;
}
= {
name name: string : 'feature-flag',
onFire onFire?(ctx: FireContext): void | {
cancel: true;
reason: string;
}
({ eventName eventName: string }) {
if (eventName eventName: string === 'beta:experiment' && globalThis module globalThis .localStorage var localStorage: Storage[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage) ?.getItem Storage.getItem(key: string): string | nullThe **`getItem()`** method of the Storage interface, when passed a key name, will return that key's value, or `null` if the key does not exist, in the given `Storage` object.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/getItem) ('beta') !== 'on') {
return { cancel cancel: true : true, reason reason: string : 'beta-disabled' };
}
},
};
const runtime const runtime: Runtime = createRuntime function createRuntime(options?: RuntimeOptions): Runtime ({ middleware middleware?: readonly Middleware[] | undefinedGlobal middleware applied to every trigger in this runtime. : [featureFlag const featureFlag: Middleware ] });Учёт причин пропуска
Заголовок раздела «Учёт причин пропуска»import { createRuntime function createRuntime(options?: RuntimeOptions): Runtime , type Middleware type Middleware = {
readonly name: string;
onFire?(ctx: FireContext): void | {
cancel: true;
reason: string;
};
onBeforeMatch?(ctx: MatchContext): void;
onSkip?(ctx: SkipContext): void;
onActionStart?(ctx: ActionContext): void;
onActionEnd?(ctx: ActionContext & {
durationMs: number;
result?: unknown;
}): void;
onError?(ctx: ActionContext & {
error: unknown;
}): void;
onCascade?(ctx: CascadeContext): void;
}
} from '@triggery/core';
const skipCounter const skipCounter: Map<string, number> = new Map var Map: MapConstructor
new <string, number>(iterable?: Iterable<readonly [string, number]> | null | undefined) => Map<string, number> (+3 overloads)
<string, number>();
const counter const counter: Middleware : Middleware type Middleware = {
readonly name: string;
onFire?(ctx: FireContext): void | {
cancel: true;
reason: string;
};
onBeforeMatch?(ctx: MatchContext): void;
onSkip?(ctx: SkipContext): void;
onActionStart?(ctx: ActionContext): void;
onActionEnd?(ctx: ActionContext & {
durationMs: number;
result?: unknown;
}): void;
onError?(ctx: ActionContext & {
error: unknown;
}): void;
onCascade?(ctx: CascadeContext): void;
}
= {
name name: string : 'skip-counter',
onSkip onSkip?(ctx: SkipContext): void ({ triggerId triggerId: string , reason reason: string }) {
const key const key: string = `${triggerId triggerId: string }:${reason reason: string }`;
skipCounter const skipCounter: Map<string, number> .set Map<string, number>.set(key: string, value: number): Map<string, number>Adds a new element with a specified key and value to the Map. If an element with the same key already exists, the element will be updated. (key const key: string , (skipCounter const skipCounter: Map<string, number> .get Map<string, number>.get(key: string): number | undefinedReturns a specified element from the Map object. If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the Map. (key const key: string ) ?? 0) + 1);
},
};
const runtime const runtime: Runtime = createRuntime function createRuntime(options?: RuntimeOptions): Runtime ({ middleware middleware?: readonly Middleware[] | undefinedGlobal middleware applied to every trigger in this runtime. : [counter const counter: Middleware ] });Детект циклов
Заголовок раздела «Детект циклов»import { createRuntime function createRuntime(options?: RuntimeOptions): Runtime , type Middleware type Middleware = {
readonly name: string;
onFire?(ctx: FireContext): void | {
cancel: true;
reason: string;
};
onBeforeMatch?(ctx: MatchContext): void;
onSkip?(ctx: SkipContext): void;
onActionStart?(ctx: ActionContext): void;
onActionEnd?(ctx: ActionContext & {
durationMs: number;
result?: unknown;
}): void;
onError?(ctx: ActionContext & {
error: unknown;
}): void;
onCascade?(ctx: CascadeContext): void;
}
} from '@triggery/core';
const guard const guard: Middleware : Middleware type Middleware = {
readonly name: string;
onFire?(ctx: FireContext): void | {
cancel: true;
reason: string;
};
onBeforeMatch?(ctx: MatchContext): void;
onSkip?(ctx: SkipContext): void;
onActionStart?(ctx: ActionContext): void;
onActionEnd?(ctx: ActionContext & {
durationMs: number;
result?: unknown;
}): void;
onError?(ctx: ActionContext & {
error: unknown;
}): void;
onCascade?(ctx: CascadeContext): void;
}
= {
name name: string : 'cycle-guard',
onCascade onCascade?(ctx: CascadeContext): void ({ kind kind: "overflow" | "cycle" , parentTriggerId parentTriggerId: string , newEventName newEventName: string }) {
if (kind kind: "overflow" | "cycle" === 'cycle') {
console var console: Console .warn Console.warn(...data: any[]): voidThe **`console.warn()`** static method outputs a warning message to the console at the 'warning' log level.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/warn_static) (`Cycle: ${parentTriggerId parentTriggerId: string } → ${newEventName newEventName: string } would loop back`);
}
},
};
const runtime const runtime: Runtime = createRuntime function createRuntime(options?: RuntimeOptions): Runtime ({ middleware middleware?: readonly Middleware[] | undefinedGlobal middleware applied to every trigger in this runtime. : [guard const guard: Middleware ] });