/** * vapor-chamber — Extra plugins * * cache, circuitBreaker, rateLimit, metrics * * These are optional, tree-shaken, and use only the public Plugin/AsyncPlugin types. */ import type { Command, Plugin, AsyncPlugin } from './command-bus'; export type CacheOptions = { /** Cache TTL in milliseconds. Default: 30_000 (30s). */ ttl?: number; /** Max entries in the cache. Default: 100. LRU eviction. */ maxSize?: number; /** Which actions to cache. Glob patterns supported. Default: all. */ actions?: string[]; /** Custom cache key. Default: commandKey(action, target). */ key?: (cmd: Command) => string; }; /** * cache — memoize handler results by (action, target) key. * Use with `query()` for read-only commands. The plugin intercepts the * dispatch/query pipeline and returns the cached result if fresh. * * @example * bus.use(cache({ ttl: 60_000, actions: ['getUser*'] })); * bus.query('getUser', { id: 42 }); // handler runs * bus.query('getUser', { id: 42 }); // cache hit, handler skipped */ export declare function cache(options?: CacheOptions): Plugin & { /** Manually invalidate a cache entry. */ invalidate(action: string, target?: any): void; /** Clear the entire cache. */ clear(): void; /** Current cache size. */ size(): number; }; export type CircuitBreakerOptions = { /** Number of consecutive failures before the circuit opens. Default: 5. */ threshold?: number; /** Time in ms the circuit stays open before trying half-open. Default: 30_000. */ resetTimeout?: number; /** Which actions to protect. Glob patterns. Default: all. */ actions?: string[]; /** Called when circuit opens. */ onOpen?: (action: string, failCount: number) => void; /** Called when circuit resets (half-open succeeds). */ onClose?: (action: string) => void; }; type CircuitState = 'closed' | 'open' | 'half-open'; /** * circuitBreaker — trips after N consecutive failures, rejects fast while open. * * @example * bus.use(circuitBreaker({ threshold: 3, resetTimeout: 10_000, actions: ['api*'] })); */ export declare function circuitBreaker(options?: CircuitBreakerOptions): Plugin & { /** Get the current circuit state for an action. */ getState(action: string): CircuitState; /** Manually reset a circuit. */ reset(action: string): void; }; export type RateLimitOptions = { /** Max dispatches per window. Default: 10. */ max?: number; /** Window size in milliseconds. Default: 1_000 (1 second). */ window?: number; /** Which actions to rate limit. Glob patterns. Default: all. */ actions?: string[]; }; /** * rateLimit — per-action sliding window rate limiter. * Unlike throttle (which delays execution), rateLimit rejects immediately * when the limit is exceeded. * * @example * bus.use(rateLimit({ max: 5, window: 1000, actions: ['api*'] })); */ export declare function rateLimit(options?: RateLimitOptions): Plugin; export type MetricsEntry = { action: string; ok: boolean; durationMs: number; timestamp: number; }; export type MetricsOptions = { /** Max entries to keep. Default: 1000. Oldest evicted first. */ maxEntries?: number; /** Which actions to track. Default: all. */ actions?: string[]; /** Called after each dispatch with the metrics entry. */ onEntry?: (entry: MetricsEntry) => void; }; /** * metrics — lightweight telemetry plugin. * Tracks dispatch count, success rate, and avg duration per action. * * @example * const m = metrics({ maxEntries: 500 }); * bus.use(m); * console.log(m.summary()); // { cartAdd: { count: 42, avgMs: 1.2, errorRate: 0.02 } } * console.log(m.entries()); // raw entries */ export declare function metrics(options?: MetricsOptions): Plugin & { /** Get all raw metric entries. */ entries(): MetricsEntry[]; /** Get a summary per action: count, avgMs, errorRate. */ summary(): Record; /** Clear all entries. */ clear(): void; }; export type SerializeOptions = { /** * Derive the serialization key from a command. Commands resolving to the SAME * key run strictly one-at-a-time (FIFO); different keys run concurrently. * Return `null`/`undefined` to skip serialization for that command. * Default: `cmd.action` (each action serialized against itself). */ key?: (cmd: Command) => string | number | null | undefined; /** Restrict to specific actions (glob patterns). Default: all. */ actions?: string[]; /** * Where the serialization lane lives. * - `'instance'` (default): a per-bus in-memory FIFO queue. Same-key commands * serialize within THIS bus instance only. * - `'cross-tab'`: use the Web Locks API (`navigator.locks`) so same-key * commands serialize across every tab / window of the same origin — true * browser-arbitrated mutual exclusion, no custom transport. Automatically * falls back to the `'instance'` queue when `navigator.locks` is unavailable * (SSR, older browsers, or workers without the API). */ scope?: 'instance' | 'cross-tab'; /** Web Locks name prefix used in `'cross-tab'` mode. Default: `'vapor-chamber:serialize'`. */ lockPrefix?: string; }; /** * serialize — guarantee that async commands sharing a key never overlap. * * **Async bus only.** Sync handlers run to completion synchronously and cannot * interleave, so serialization is meaningless there (and would turn a sync * dispatch into a Promise). Register on `createAsyncCommandBus`. * * Prevents read-modify-write races on a shared resource: two `accountWithdraw` * for the same account, rapid `cartCheckout` clicks, or any handler where a * second dispatch must observe the first one's committed effect. This is * distinct from in-flight request dedup (which collapses *identical* requests) — * serialize queues *distinct* same-key commands so they apply in order. * * Failure-safe: a rejected/failed command does NOT stall its lane — the next * same-key command proceeds regardless of the previous outcome. Per-key entries * are reclaimed once a lane drains, so the map never grows unbounded. * * `scope: 'cross-tab'` extends serialization across tabs via the Web Locks API: * `navigator.locks.request` queues same-name requests FIFO across all same-origin * contexts and releases the lock when the handler settles (even on throw), so the * failure-safety guarantee holds across tabs too. Degrades to per-instance when * the API is absent. * * @example * const bus = createAsyncCommandBus(); * bus.use(serialize({ key: (cmd) => cmd.target.accountId, actions: ['account*'] })); * // withdrawals for the same account now run strictly in order; * // different accounts still run concurrently * * @example * // serialize across every tab of the same origin: * bus.use(serialize({ scope: 'cross-tab', key: (cmd) => cmd.target.accountId })); */ export declare function serialize(options?: SerializeOptions): AsyncPlugin; export type IdempotentOptions = { /** * Derive a stable idempotency key from a command. Commands with the same key * are the same logical operation. Return `null`/`undefined` to skip a command. * Default: `commandKey(action, target)` (action + stable JSON of target). */ key?: (cmd: Command) => string | null | undefined; /** * How long (ms) a *completed* key is remembered for dedup — the window in * which a repeat (double-click, retry, reconnect replay) is collapsed to the * first result instead of hitting the handler/backend again. Default: 60_000. */ ttl?: number; /** Restrict to specific actions (glob patterns). Default: all. */ actions?: string[]; /** * Also stamp the key onto `cmd.meta.idempotencyKey` so transports forward it * (the HTTP bridge sends it as an `Idempotency-Key` header). Default: true. */ stampMeta?: boolean; /** * Max completed keys remembered at once — oldest is evicted first, so memory * stays bounded on long-lived buses with many distinct targets. Default: 500. */ maxKeys?: number; }; /** * idempotent — make duplicate dispatches a no-op against the handler/backend. * * The client-side half of exactly-once delivery: it collapses repeats of the * same logical command — double-clicked Checkout, an auto-retry, a reconnect * that replays a queued action — so the handler (and the backend it calls) runs * **once**. Concurrent duplicates share the first in-flight promise; sequential * duplicates within `ttl` get the cached result. Failures are NOT cached, so a * genuine retry after an error still runs. * * Pairs with `serialize` (orders same-key commands locally) and with the HTTP * bridge, which forwards the stamped `cmd.meta.idempotencyKey` as an * `Idempotency-Key` header so the backend can reject the duplicate write too — * the wire half of exactly-once. Register `idempotent` at a HIGHER priority than * the transport so the key is stamped before the request is built. * * @example * const bus = createAsyncCommandBus(); * bus.use(idempotent({ actions: ['order*'] }), { priority: 100 }); // outermost * bus.use(createHttpBridge({ endpoint: '/commands', csrf: true })); * // two rapid orderCreate dispatches → one handler run, one backend write */ export declare function idempotent(options?: IdempotentOptions): AsyncPlugin; export type SupersedeOptions = { /** * Derive the supersede key from a command. Commands resolving to the SAME * key auto-cancel their predecessor; different keys race independently. * Return `null`/`undefined` to skip superseding for that command. * Default: `commandKey(action, target)` — same default `idempotent` uses, * which already includes the action name, so distinct actions never * collide and are never silently dropped by this plugin. */ key?: (cmd: Command) => string | null | undefined; /** Restrict to specific actions (glob patterns). Default: all. */ actions?: string[]; }; /** * supersede — auto-cancel the previous in-flight dispatch for the same key. * * Built for rapid-fire reads that can overwrite each other in flight — a * search box re-querying on every keystroke, a filter changing before the * previous fetch lands. Without it, a slow first response can arrive AFTER a * faster second one and clobber it with stale data. With it, the first * dispatch's AbortSignal fires the instant a second dispatch for the same key * starts — `createHttpBridge` / `createBatchingHttpBridge` already forward * `cmd.signal` to `fetch()`, so the stale request is genuinely cancelled, not * merely ignored once it resolves. * * **Async bus only** — mutates `cmd.signal` before the rest of the pipeline * runs, which is only meaningful for cancelable async dispatches. * * @example * const bus = createAsyncCommandBus(); * bus.use(supersede({ actions: ['productSearch'] })); * bus.use(createHttpBridge({ endpoint: '/api/vc' })); * // three quick productSearch dispatches → only the last one's response is * // ever awaited; the first two are aborted mid-flight, not just discarded */ export declare function supersede(options?: SupersedeOptions): AsyncPlugin; export {}; //# sourceMappingURL=plugins-extra.d.ts.map