Middleware
A pluggable observer attached to every trigger in a runtime. The seven hooks fire at predictable points around dispatch — three of them (onFire, onSkip, onError) often pair to form a complete audit trail; the others (onBeforeMatch, onActionStart, onActionEnd, onCascade) are for tracing, metrics, and devtools.
All hooks are optional. Throwing inside a hook is caught and logged by the runtime but does not abort the run — middleware must not be load-bearing.
Import
Section titled “Import”import type { Middleware } from '@triggery/core';
Definition
Section titled “Definition”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
Section titled “name — string”Stable id for the middleware. Required. Useful in error messages and devtools panels.
onFire(ctx) -> void | { cancel: true; reason: string }
Section titled “onFire(ctx) -> void | { cancel: true; reason: string }”Called once per top-level (or cascade) fire(eventName, payload) before any trigger is matched. Return { cancel: true, reason } to abort the entire dispatch — no triggers run and an 'overflow'-style record is not emitted. This is the only short-circuit hook.
FireContext field | Description |
|---|---|
eventName | The event being dispatched. |
payload | The payload, opaque to middleware. |
cascadeDepth | 0 for top-level fires; n inside a cascade. |
parentRunId? | Set when the fire happens inside a running handler. |
parentTriggerId? | Same — id of the upstream trigger. |
parentContext? | Opaque linked-list reference for cycle detection. |
onBeforeMatch(ctx) -> void
Section titled “onBeforeMatch(ctx) -> void”Called once per (event, trigger) pair right after dispatch picks the trigger out of the event index — before any concurrency gate or required check. Purely observational; useful for “which triggers were considered for this event” without instrumenting onSkip and onActionStart separately.
onSkip(ctx) -> void
Section titled “onSkip(ctx) -> void”Called when a trigger is matched but skipped. reason is one of 'disabled', 'required-missing', 'concurrency-take-first', 'aborted', 'cycle', 'overflow', etc. Use this to track “why didn’t my trigger run”.
onActionStart(ctx) -> void
Section titled “onActionStart(ctx) -> void”Called immediately before an action is invoked by a handler. actionName and payload are the arguments. Use for per-action tracing.
onActionEnd(ctx & { durationMs; result? }) -> void
Section titled “onActionEnd(ctx & { durationMs; result? }) -> void”Called when the action returns (or its returned promise resolves). durationMs is the wall-clock time from onActionStart to onActionEnd. The runtime only measures this when at least one middleware listens — there’s no cost when no middleware needs timing.
onError(ctx & { error }) -> void
Section titled “onError(ctx & { error }) -> void”Called when an action throws or its promise rejects. Pair with onActionStart to bracket a full attempt. Errors are also recorded on the inspector snapshot with status: 'errored'.
onCascade(ctx) -> void
Section titled “onCascade(ctx) -> void”Called when a cascade event is suppressed — kind: 'overflow' (cascadeDepth exceeded maxCascadeDepth) or kind: 'cycle' (a trigger appears in its own ancestor chain).
Examples
Section titled “Examples”Tracing every fire
Section titled “Tracing every 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 ] });Auditing errors to a backend
Section titled “Auditing errors to a backend”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 ] });Veto by onFire return
Section titled “Veto by onFire return”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 ] });Track skip reasons
Section titled “Track skip reasons”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 ] });Detect cycles
Section titled “Detect cycles”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 ] });