@rotorsoft/act-http 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/sse/index.ts","../../src/sse/apply-patch.ts","../../src/sse/broadcast.ts","../../src/sse/state-cache.ts","../../src/sse/presence.ts"],"sourcesContent":["/**\n * @packageDocumentation\n * @module act-http/sse\n *\n * Incremental state broadcast over SSE for act event-sourced apps.\n *\n * Server-side broadcast with domain patch forwarding, an LRU state cache,\n * presence tracking, and a client-side patch applicator with version\n * validation and resync detection.\n *\n * ## Architecture\n *\n * ```\n * app.do() → snapshots (each carries its domain patch)\n * │\n * ▼\n * deriveState(snap) ← app-specific (overlay presence, deadlines, etc.)\n * state._v = snap.event.version\n * │\n * ▼\n * broadcast.publish(streamId, state, patches)\n * │\n * ├── version-key each patch: { [baseV+1]: patch1, [baseV+2]: patch2 }\n * └── push to all SSE subscribers\n * │\n * ▼\n * Client: applyPatchMessage(msg, cached)\n * │\n * ├── contiguous → deep-merge patches in version order\n * ├── stale → skip (client already ahead)\n * └── behind → resync (client missed versions)\n * ```\n *\n * ## Version Contract\n *\n * `_v` is always the event store stream version (`snap.event.version`).\n * No separate version counters. The event store is the single source of truth.\n */\n\nexport { patch } from \"@rotorsoft/act-patch\";\nexport type { ApplyResult } from \"./apply-patch.js\";\nexport { applyPatchMessage } from \"./apply-patch.js\";\nexport { BroadcastChannel } from \"./broadcast.js\";\nexport { PresenceTracker } from \"./presence.js\";\nexport { StateCache } from \"./state-cache.js\";\nexport type { BroadcastState, PatchMessage, Subscriber } from \"./types.js\";\n","import { patch as deepMerge } from \"@rotorsoft/act-patch\";\nimport type { BroadcastState, PatchMessage } from \"./types.js\";\n\n/**\n * Result of applying a patch message to cached client state.\n */\nexport type ApplyResult<S extends BroadcastState = BroadcastState> =\n | { ok: true; state: S }\n | { ok: false; reason: \"stale\" | \"behind\" };\n\n/**\n * Apply a version-keyed patch message to the client's cached state.\n *\n * ## Version logic\n *\n * - All patches older than cached → \"stale\" (client already ahead)\n * - Gap between cached version and first patch → \"behind\" (client missed versions, must resync)\n * - Contiguous from cached version → apply in order\n *\n * ## Usage (React Query)\n *\n * ```typescript\n * onData: (msg) => {\n * const cached = utils.getState.getData({ streamId });\n * const result = applyPatchMessage(msg, cached);\n * if (result.ok) {\n * utils.getState.setData({ streamId }, result.state);\n * } else if (result.reason === \"behind\") {\n * utils.getState.invalidate({ streamId }); // trigger full refetch\n * }\n * // \"stale\" → no-op, client already has newer state\n * }\n * ```\n */\nexport function applyPatchMessage<S extends BroadcastState>(\n msg: PatchMessage<S>,\n cached: S | null | undefined\n): ApplyResult<S> {\n const cachedV = cached?._v ?? 0;\n const versions = Object.keys(msg)\n .map(Number)\n .sort((a, b) => a - b);\n\n if (!versions.length) return { ok: false, reason: \"stale\" };\n\n const minV = versions[0];\n const maxV = versions[versions.length - 1];\n\n if (maxV <= cachedV) return { ok: false, reason: \"stale\" };\n if (!cached || minV > cachedV + 1) return { ok: false, reason: \"behind\" };\n\n let state = cached;\n for (const v of versions) {\n if (v <= cachedV) continue;\n state = { ...deepMerge(state, msg[v]), _v: v } as S;\n }\n return { ok: true, state };\n}\n","import { patch as applyPatch } from \"@rotorsoft/act-patch\";\nimport { StateCache } from \"./state-cache.js\";\nimport type { BroadcastState, PatchMessage, Subscriber } from \"./types.js\";\n\n/**\n * Server-side broadcast channel for incremental state sync over SSE.\n *\n * Manages per-stream subscriber sets and an LRU state cache. When state\n * changes, forwards domain patches (from event handlers) to all subscribers\n * as version-keyed messages.\n *\n * ## Usage\n *\n * ```typescript\n * const broadcast = new BroadcastChannel<MyState>();\n *\n * // After every app.do():\n * const snaps = await app.do(...);\n * const patches = snaps.map(s => s.patch).filter(Boolean);\n * const state = deriveState(snaps.at(-1));\n * broadcast.publish(streamId, state, patches);\n *\n * // In SSE subscription:\n * const cleanup = broadcast.subscribe(streamId, (msg) => {\n * pending = msg;\n * resolve?.();\n * });\n *\n * // Initial state for reconnects:\n * const cached = broadcast.getState(streamId);\n * ```\n *\n * ## Version Contract\n *\n * The `_v` field on state MUST be set from `snap.event.version` (the event\n * store's monotonic stream version) BEFORE calling `publish()`. This is the\n * single source of truth for ordering — no separate version counters.\n */\nexport class BroadcastChannel<S extends BroadcastState = BroadcastState> {\n private channels = new Map<string, Set<Subscriber<S>>>();\n private stateCache: StateCache<S>;\n\n constructor(options?: { cacheSize?: number }) {\n this.stateCache = new StateCache<S>(options?.cacheSize ?? 50);\n }\n\n /**\n * Publish domain patches from a commit.\n * patches[i] corresponds to version baseV + i + 1.\n *\n * @param streamId - The event store stream ID\n * @param state - Full state with `_v` set from `snap.event.version`\n * @param patches - Array of domain patches, one per emitted event\n */\n publish(\n streamId: string,\n state: S,\n patches: Partial<S>[] = []\n ): PatchMessage<S> {\n this.stateCache.set(streamId, state);\n\n const baseV = state._v - patches.length;\n const msg: PatchMessage<S> = {};\n patches.forEach((p, i) => {\n msg[baseV + i + 1] = p;\n });\n\n const subs = this.channels.get(streamId);\n if (subs?.size) {\n for (const cb of subs) cb(msg);\n }\n return msg;\n }\n\n /**\n * Publish a state update that doesn't change the event version\n * (e.g. presence overlay, computed field refresh).\n * Uses the same version as the cached state, single entry.\n */\n publishOverlay(\n streamId: string,\n overlayPatch: Partial<S>\n ): PatchMessage<S> | undefined {\n const prev = this.stateCache.get(streamId);\n if (!prev) return undefined;\n\n const state = applyPatch(prev, overlayPatch) as S;\n this.stateCache.set(streamId, state);\n\n const msg: PatchMessage<S> = { [state._v]: overlayPatch };\n const subs = this.channels.get(streamId);\n if (subs?.size) {\n for (const cb of subs) cb(msg);\n }\n return msg;\n }\n\n /**\n * Subscribe to broadcast messages for a stream.\n * Returns a cleanup function that removes the subscription.\n */\n subscribe(streamId: string, cb: Subscriber<S>): () => void {\n if (!this.channels.has(streamId)) this.channels.set(streamId, new Set());\n this.channels.get(streamId)!.add(cb);\n return () => {\n this.channels.get(streamId)?.delete(cb);\n if (this.channels.get(streamId)?.size === 0) {\n this.channels.delete(streamId);\n }\n };\n }\n\n /** Get the number of subscribers for a stream. */\n getSubscriberCount(streamId: string): number {\n return this.channels.get(streamId)?.size ?? 0;\n }\n\n /** Get the cached state for a stream (for reconnects / initial SSE yield). */\n getState(streamId: string): S | undefined {\n return this.stateCache.get(streamId);\n }\n\n /** Direct access to the state cache (for app-specific reads like presence). */\n get cache(): StateCache<S> {\n return this.stateCache;\n }\n}\n","import type { BroadcastState } from \"./types.js\";\n\n/**\n * Generic LRU cache for aggregate state objects.\n *\n * Keyed by stream ID. Each entry stores the full state (with `_v` from the\n * event store). Used as the \"previous state\" baseline for computing patches,\n * and as the fast path for reconnects.\n *\n * The cache is shared between the broadcast hot path and read queries.\n * Projections should maintain their own cache to avoid double-apply bugs.\n */\nexport class StateCache<S extends BroadcastState = BroadcastState> {\n private cache = new Map<string, S>();\n private maxSize: number;\n\n constructor(maxSize = 50) {\n this.maxSize = maxSize;\n }\n\n /** Get a cached state, promoting it to MRU position. */\n get(key: string): S | undefined {\n const s = this.cache.get(key);\n if (s) {\n this.cache.delete(key);\n this.cache.set(key, s);\n }\n return s;\n }\n\n /** Set a cached state, evicting the LRU entry if at capacity. */\n set(key: string, state: S): void {\n this.cache.delete(key);\n this.cache.set(key, state);\n if (this.cache.size > this.maxSize) {\n this.cache.delete(this.cache.keys().next().value!);\n }\n }\n\n /** Remove a cached entry. */\n delete(key: string): void {\n this.cache.delete(key);\n }\n\n /** Check if a key exists in the cache. */\n has(key: string): boolean {\n return this.cache.has(key);\n }\n\n /** Current number of cached entries. */\n get size(): number {\n return this.cache.size;\n }\n\n /** Direct access to the underlying map (for iteration). */\n entries(): IterableIterator<[string, S]> {\n return this.cache.entries();\n }\n}\n","/**\n * Generic presence tracker — ref-counted online status per stream per identity.\n *\n * Supports multi-tab: each subscribe increments the ref count, each\n * unsubscribe decrements it. An identity is considered online when\n * ref count > 0.\n *\n * ## Usage\n *\n * ```typescript\n * const presence = new PresenceTracker();\n *\n * // On SSE connect:\n * presence.add(gameId, playerId);\n *\n * // On SSE disconnect:\n * presence.remove(gameId, playerId);\n *\n * // Query:\n * presence.getOnline(gameId); // Set<string>\n * ```\n */\nexport class PresenceTracker {\n private streams = new Map<string, Map<string, number>>();\n\n /** Increment ref count for an identity on a stream. */\n add(streamId: string, identityId: string): void {\n if (!this.streams.has(streamId)) this.streams.set(streamId, new Map());\n const counts = this.streams.get(streamId)!;\n counts.set(identityId, (counts.get(identityId) ?? 0) + 1);\n }\n\n /** Decrement ref count. Removes the identity when count reaches 0. */\n remove(streamId: string, identityId: string): void {\n const counts = this.streams.get(streamId);\n if (!counts) return;\n const n = (counts.get(identityId) ?? 1) - 1;\n if (n <= 0) counts.delete(identityId);\n else counts.set(identityId, n);\n if (counts.size === 0) this.streams.delete(streamId);\n }\n\n /** Get the set of online identity IDs for a stream. */\n getOnline(streamId: string): Set<string> {\n const counts = this.streams.get(streamId);\n return counts ? new Set(counts.keys()) : new Set();\n }\n\n /** Check if a specific identity is online for a stream. */\n isOnline(streamId: string, identityId: string): boolean {\n return (this.streams.get(streamId)?.get(identityId) ?? 0) > 0;\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/webhook/index.ts
21
+ var webhook_exports = {};
22
+ __export(webhook_exports, {
23
+ WebhookError: () => WebhookError,
24
+ webhook: () => webhook
25
+ });
26
+ module.exports = __toCommonJS(webhook_exports);
27
+
28
+ // src/webhook/types.ts
29
+ var WebhookError = class extends Error {
30
+ status;
31
+ retryable;
32
+ url;
33
+ responseBody;
34
+ constructor(message, init) {
35
+ super(message);
36
+ this.name = "WebhookError";
37
+ this.status = init.status;
38
+ this.retryable = init.retryable;
39
+ this.url = init.url;
40
+ this.responseBody = init.responseBody;
41
+ }
42
+ };
43
+
44
+ // src/webhook/index.ts
45
+ function resolve(resolver, event, fallback) {
46
+ if (resolver === void 0) return fallback;
47
+ return typeof resolver === "function" ? resolver(event) : resolver;
48
+ }
49
+ function hasHeader(headers, name) {
50
+ const lower = name.toLowerCase();
51
+ for (const k of Object.keys(headers)) {
52
+ if (k.toLowerCase() === lower) return true;
53
+ }
54
+ return false;
55
+ }
56
+ function webhook(config) {
57
+ const timeoutMs = config.timeoutMs ?? 5e3;
58
+ const method = config.method ?? "POST";
59
+ const fetchImpl = config.fetch ?? globalThis.fetch;
60
+ return async function webhookDeliver(event) {
61
+ const url = resolve(config.url, event, "");
62
+ const customHeaders = resolve(
63
+ config.headers,
64
+ event,
65
+ {}
66
+ );
67
+ const headers = { ...customHeaders };
68
+ if (!hasHeader(headers, "content-type")) {
69
+ headers["Content-Type"] = "application/json";
70
+ }
71
+ if (!hasHeader(headers, "idempotency-key")) {
72
+ const key = config.idempotencyKey ? config.idempotencyKey(event) : String(event.id);
73
+ if (key !== null) headers["Idempotency-Key"] = key;
74
+ }
75
+ const rawBody = resolve(config.body, event, event);
76
+ const body = typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody);
77
+ const controller = new AbortController();
78
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
79
+ let response;
80
+ try {
81
+ response = await fetchImpl(url, {
82
+ method,
83
+ headers,
84
+ body,
85
+ signal: controller.signal
86
+ });
87
+ } catch (err) {
88
+ const aborted = controller.signal.aborted;
89
+ throw new WebhookError(
90
+ aborted ? `webhook ${method} ${url} timed out after ${timeoutMs}ms` : `webhook ${method} ${url} failed: ${err.message}`,
91
+ { status: 0, retryable: true, url }
92
+ );
93
+ } finally {
94
+ clearTimeout(timer);
95
+ }
96
+ if (response.ok) return;
97
+ let responseBody;
98
+ try {
99
+ responseBody = await response.text();
100
+ } catch {
101
+ }
102
+ const retryable = response.status >= 500;
103
+ throw new WebhookError(
104
+ `webhook ${method} ${url} responded ${response.status}`,
105
+ { status: response.status, retryable, url, responseBody }
106
+ );
107
+ };
108
+ }
109
+ // Annotate the CommonJS export names for ESM import in node:
110
+ 0 && (module.exports = {
111
+ WebhookError,
112
+ webhook
113
+ });
114
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/webhook/index.ts","../../src/webhook/types.ts"],"sourcesContent":["/**\n * @packageDocumentation\n * @module act-http/webhook\n *\n * Reaction-handler sugar for POSTing committed events to external URLs.\n *\n * Wraps `fetch` with timeouts, automatic `Idempotency-Key` derivation, and\n * status-classified errors. Designed to be composed with the reaction\n * options shipped in ACT-601 (`maxRetries`, `blockOnError`, `backoff`):\n *\n * ```ts\n * import { webhook } from \"@rotorsoft/act-http/webhook\";\n *\n * .on(\"OrderConfirmed\")\n * .do(\n * webhook({\n * url: \"https://api.example.com/webhooks/orders\",\n * headers: (e) => ({ Authorization: \"Bearer ...\" }),\n * body: (e) => ({ orderId: e.stream, total: e.data.total }),\n * timeoutMs: 5_000,\n * }),\n * { maxRetries: 5, backoff: { strategy: \"exponential\", baseMs: 200, maxMs: 30_000 } }\n * )\n * .to(resolver);\n * ```\n */\n\nimport type { Committed, ReactionHandler, Schemas } from \"@rotorsoft/act\";\nimport { type WebhookConfig, WebhookError } from \"./types.js\";\n\nexport type { WebhookConfig, WebhookResolver } from \"./types.js\";\nexport { WebhookError } from \"./types.js\";\n\nfunction resolve<TEvents extends Schemas, T>(\n resolver: T | ((e: Committed<TEvents, keyof TEvents>) => T) | undefined,\n event: Committed<TEvents, keyof TEvents>,\n fallback: T\n): T {\n if (resolver === undefined) return fallback;\n return typeof resolver === \"function\"\n ? (resolver as (e: Committed<TEvents, keyof TEvents>) => T)(event)\n : resolver;\n}\n\n/** Case-insensitive lookup; returns true if a header is already set. */\nfunction hasHeader(headers: Record<string, string>, name: string): boolean {\n const lower = name.toLowerCase();\n for (const k of Object.keys(headers)) {\n if (k.toLowerCase() === lower) return true;\n }\n return false;\n}\n\n/**\n * Build a reaction handler that POSTs each event to an external URL.\n *\n * Behavior:\n *\n * - Network errors and timeouts throw {@link WebhookError} with\n * `status: 0`, `retryable: true`.\n * - 5xx responses throw with `retryable: true`.\n * - 4xx responses throw with `retryable: false`.\n * - 2xx and 3xx return successfully.\n *\n * Drain retry behavior follows the reaction's `maxRetries` / `backoff`\n * options. To skip retries entirely for client errors, set\n * `maxRetries: 0` on the reaction — both 4xx and 5xx will block on the\n * first failed attempt.\n */\nexport function webhook<TEvents extends Schemas = Schemas>(\n config: WebhookConfig<TEvents>\n): ReactionHandler<TEvents, keyof TEvents> {\n const timeoutMs = config.timeoutMs ?? 5_000;\n const method = config.method ?? \"POST\";\n const fetchImpl = config.fetch ?? globalThis.fetch;\n\n // Named function: slice/act builders require non-anonymous reaction\n // handlers so lifecycle telemetry can attribute work.\n return async function webhookDeliver(event) {\n const url = resolve(config.url, event, \"\");\n\n const customHeaders = resolve(\n config.headers,\n event,\n {} as Record<string, string>\n );\n const headers: Record<string, string> = { ...customHeaders };\n\n if (!hasHeader(headers, \"content-type\")) {\n headers[\"Content-Type\"] = \"application/json\";\n }\n if (!hasHeader(headers, \"idempotency-key\")) {\n const key = config.idempotencyKey\n ? config.idempotencyKey(event)\n : String(event.id);\n if (key !== null) headers[\"Idempotency-Key\"] = key;\n }\n\n const rawBody = resolve(config.body, event, event as unknown);\n const body =\n typeof rawBody === \"string\" ? rawBody : JSON.stringify(rawBody);\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n let response: Response;\n try {\n response = await fetchImpl(url, {\n method,\n headers,\n body,\n signal: controller.signal,\n });\n } catch (err) {\n const aborted = controller.signal.aborted;\n throw new WebhookError(\n aborted\n ? `webhook ${method} ${url} timed out after ${timeoutMs}ms`\n : `webhook ${method} ${url} failed: ${(err as Error).message}`,\n { status: 0, retryable: true, url }\n );\n } finally {\n clearTimeout(timer);\n }\n\n if (response.ok) return;\n\n let responseBody: string | undefined;\n try {\n responseBody = await response.text();\n } catch {\n // best-effort body capture; ignore read errors\n }\n\n const retryable = response.status >= 500;\n throw new WebhookError(\n `webhook ${method} ${url} responded ${response.status}`,\n { status: response.status, retryable, url, responseBody }\n );\n };\n}\n","import type { Committed, Schemas } from \"@rotorsoft/act\";\n\n/**\n * Function or static value resolver. Used so callers can pass either a\n * constant or a per-event function for headers / body / url.\n *\n * The static side `T` is constrained to non-function types so that a\n * passed `(event) => ...` is unambiguously typed as the function variant.\n */\nexport type WebhookResolver<TEvents extends Schemas, T> =\n | T\n | ((event: Committed<TEvents, keyof TEvents>) => T);\n\n/**\n * Plain-data body shape the helper accepts as a static value. Functions\n * are deliberately excluded so the union with the resolver function is\n * unambiguous at the call site (TypeScript can discriminate by shape).\n */\nexport type WebhookBody =\n | string\n | { readonly [k: string]: unknown }\n | readonly unknown[];\n\n/**\n * Configuration for {@link webhook}.\n *\n * @template TEvents - Event schemas; resolvers receive the typed committed event.\n */\nexport type WebhookConfig<TEvents extends Schemas = Schemas> = {\n /** Target URL — static string or per-event function. */\n readonly url: WebhookResolver<TEvents, string>;\n /** HTTP method. Defaults to `\"POST\"`. */\n readonly method?: \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n /**\n * Headers to send. Resolver may return a record per event. The\n * `Content-Type: application/json` and `Idempotency-Key` headers are\n * applied automatically; both can be overridden by returning a header\n * with the same name (case-insensitive).\n */\n readonly headers?: WebhookResolver<TEvents, Record<string, string>>;\n /**\n * Request body. Static plain data (object, array, string) or a\n * per-event function returning the same. Strings are sent as-is;\n * anything else is JSON-serialized. Defaults to the committed event\n * itself.\n */\n readonly body?:\n | WebhookBody\n | ((event: Committed<TEvents, keyof TEvents>) => WebhookBody);\n /**\n * Per-request timeout in milliseconds. Defaults to 5000.\n * The handler throws after the timeout via `AbortController`.\n */\n readonly timeoutMs?: number;\n /**\n * Override for the auto-generated `Idempotency-Key`. By default, the\n * helper sends `event.id` (the immutable, monotonic event identifier).\n * Return a string to override; return `null` to skip the header entirely.\n */\n readonly idempotencyKey?: (\n event: Committed<TEvents, keyof TEvents>\n ) => string | null;\n /**\n * Injection point for tests. Defaults to global `fetch`.\n */\n readonly fetch?: typeof fetch;\n};\n\n/**\n * Error thrown by the webhook handler on network failure, timeout, or\n * non-2xx response. The `status` field is `0` for network / timeout\n * errors and the HTTP status code otherwise.\n *\n * `retryable` reflects the helper's classification: network errors,\n * timeouts, and 5xx are flagged retryable; 4xx is not. The current drain\n * pipeline does not distinguish — both are caught and counted against\n * `maxRetries`. Callers who want different retry semantics per category\n * can introspect the error in a wrapping handler or tune `maxRetries` /\n * `backoff` on the reaction options.\n */\nexport class WebhookError extends Error {\n readonly status: number;\n readonly retryable: boolean;\n readonly url: string;\n readonly responseBody?: string;\n\n constructor(\n message: string,\n init: {\n status: number;\n retryable: boolean;\n url: string;\n responseBody?: string;\n }\n ) {\n super(message);\n this.name = \"WebhookError\";\n this.status = init.status;\n this.retryable = init.retryable;\n this.url = init.url;\n this.responseBody = init.responseBody;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACgFO,IAAM,eAAN,cAA2B,MAAM;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAET,YACE,SACA,MAMA;AACA,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS,KAAK;AACnB,SAAK,YAAY,KAAK;AACtB,SAAK,MAAM,KAAK;AAChB,SAAK,eAAe,KAAK;AAAA,EAC3B;AACF;;;ADrEA,SAAS,QACP,UACA,OACA,UACG;AACH,MAAI,aAAa,OAAW,QAAO;AACnC,SAAO,OAAO,aAAa,aACtB,SAAyD,KAAK,IAC/D;AACN;AAGA,SAAS,UAAU,SAAiC,MAAuB;AACzE,QAAM,QAAQ,KAAK,YAAY;AAC/B,aAAW,KAAK,OAAO,KAAK,OAAO,GAAG;AACpC,QAAI,EAAE,YAAY,MAAM,MAAO,QAAO;AAAA,EACxC;AACA,SAAO;AACT;AAkBO,SAAS,QACd,QACyC;AACzC,QAAM,YAAY,OAAO,aAAa;AACtC,QAAM,SAAS,OAAO,UAAU;AAChC,QAAM,YAAY,OAAO,SAAS,WAAW;AAI7C,SAAO,eAAe,eAAe,OAAO;AAC1C,UAAM,MAAM,QAAQ,OAAO,KAAK,OAAO,EAAE;AAEzC,UAAM,gBAAgB;AAAA,MACpB,OAAO;AAAA,MACP;AAAA,MACA,CAAC;AAAA,IACH;AACA,UAAM,UAAkC,EAAE,GAAG,cAAc;AAE3D,QAAI,CAAC,UAAU,SAAS,cAAc,GAAG;AACvC,cAAQ,cAAc,IAAI;AAAA,IAC5B;AACA,QAAI,CAAC,UAAU,SAAS,iBAAiB,GAAG;AAC1C,YAAM,MAAM,OAAO,iBACf,OAAO,eAAe,KAAK,IAC3B,OAAO,MAAM,EAAE;AACnB,UAAI,QAAQ,KAAM,SAAQ,iBAAiB,IAAI;AAAA,IACjD;AAEA,UAAM,UAAU,QAAQ,OAAO,MAAM,OAAO,KAAgB;AAC5D,UAAM,OACJ,OAAO,YAAY,WAAW,UAAU,KAAK,UAAU,OAAO;AAEhE,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,SAAS;AAE5D,QAAI;AACJ,QAAI;AACF,iBAAW,MAAM,UAAU,KAAK;AAAA,QAC9B;AAAA,QACA;AAAA,QACA;AAAA,QACA,QAAQ,WAAW;AAAA,MACrB,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,YAAM,UAAU,WAAW,OAAO;AAClC,YAAM,IAAI;AAAA,QACR,UACI,WAAW,MAAM,IAAI,GAAG,oBAAoB,SAAS,OACrD,WAAW,MAAM,IAAI,GAAG,YAAa,IAAc,OAAO;AAAA,QAC9D,EAAE,QAAQ,GAAG,WAAW,MAAM,IAAI;AAAA,MACpC;AAAA,IACF,UAAE;AACA,mBAAa,KAAK;AAAA,IACpB;AAEA,QAAI,SAAS,GAAI;AAEjB,QAAI;AACJ,QAAI;AACF,qBAAe,MAAM,SAAS,KAAK;AAAA,IACrC,QAAQ;AAAA,IAER;AAEA,UAAM,YAAY,SAAS,UAAU;AACrC,UAAM,IAAI;AAAA,MACR,WAAW,MAAM,IAAI,GAAG,cAAc,SAAS,MAAM;AAAA,MACrD,EAAE,QAAQ,SAAS,QAAQ,WAAW,KAAK,aAAa;AAAA,IAC1D;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,86 @@
1
+ // src/webhook/types.ts
2
+ var WebhookError = class extends Error {
3
+ status;
4
+ retryable;
5
+ url;
6
+ responseBody;
7
+ constructor(message, init) {
8
+ super(message);
9
+ this.name = "WebhookError";
10
+ this.status = init.status;
11
+ this.retryable = init.retryable;
12
+ this.url = init.url;
13
+ this.responseBody = init.responseBody;
14
+ }
15
+ };
16
+
17
+ // src/webhook/index.ts
18
+ function resolve(resolver, event, fallback) {
19
+ if (resolver === void 0) return fallback;
20
+ return typeof resolver === "function" ? resolver(event) : resolver;
21
+ }
22
+ function hasHeader(headers, name) {
23
+ const lower = name.toLowerCase();
24
+ for (const k of Object.keys(headers)) {
25
+ if (k.toLowerCase() === lower) return true;
26
+ }
27
+ return false;
28
+ }
29
+ function webhook(config) {
30
+ const timeoutMs = config.timeoutMs ?? 5e3;
31
+ const method = config.method ?? "POST";
32
+ const fetchImpl = config.fetch ?? globalThis.fetch;
33
+ return async function webhookDeliver(event) {
34
+ const url = resolve(config.url, event, "");
35
+ const customHeaders = resolve(
36
+ config.headers,
37
+ event,
38
+ {}
39
+ );
40
+ const headers = { ...customHeaders };
41
+ if (!hasHeader(headers, "content-type")) {
42
+ headers["Content-Type"] = "application/json";
43
+ }
44
+ if (!hasHeader(headers, "idempotency-key")) {
45
+ const key = config.idempotencyKey ? config.idempotencyKey(event) : String(event.id);
46
+ if (key !== null) headers["Idempotency-Key"] = key;
47
+ }
48
+ const rawBody = resolve(config.body, event, event);
49
+ const body = typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody);
50
+ const controller = new AbortController();
51
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
52
+ let response;
53
+ try {
54
+ response = await fetchImpl(url, {
55
+ method,
56
+ headers,
57
+ body,
58
+ signal: controller.signal
59
+ });
60
+ } catch (err) {
61
+ const aborted = controller.signal.aborted;
62
+ throw new WebhookError(
63
+ aborted ? `webhook ${method} ${url} timed out after ${timeoutMs}ms` : `webhook ${method} ${url} failed: ${err.message}`,
64
+ { status: 0, retryable: true, url }
65
+ );
66
+ } finally {
67
+ clearTimeout(timer);
68
+ }
69
+ if (response.ok) return;
70
+ let responseBody;
71
+ try {
72
+ responseBody = await response.text();
73
+ } catch {
74
+ }
75
+ const retryable = response.status >= 500;
76
+ throw new WebhookError(
77
+ `webhook ${method} ${url} responded ${response.status}`,
78
+ { status: response.status, retryable, url, responseBody }
79
+ );
80
+ };
81
+ }
82
+ export {
83
+ WebhookError,
84
+ webhook
85
+ };
86
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/webhook/types.ts","../../src/webhook/index.ts"],"sourcesContent":["import type { Committed, Schemas } from \"@rotorsoft/act\";\n\n/**\n * Function or static value resolver. Used so callers can pass either a\n * constant or a per-event function for headers / body / url.\n *\n * The static side `T` is constrained to non-function types so that a\n * passed `(event) => ...` is unambiguously typed as the function variant.\n */\nexport type WebhookResolver<TEvents extends Schemas, T> =\n | T\n | ((event: Committed<TEvents, keyof TEvents>) => T);\n\n/**\n * Plain-data body shape the helper accepts as a static value. Functions\n * are deliberately excluded so the union with the resolver function is\n * unambiguous at the call site (TypeScript can discriminate by shape).\n */\nexport type WebhookBody =\n | string\n | { readonly [k: string]: unknown }\n | readonly unknown[];\n\n/**\n * Configuration for {@link webhook}.\n *\n * @template TEvents - Event schemas; resolvers receive the typed committed event.\n */\nexport type WebhookConfig<TEvents extends Schemas = Schemas> = {\n /** Target URL — static string or per-event function. */\n readonly url: WebhookResolver<TEvents, string>;\n /** HTTP method. Defaults to `\"POST\"`. */\n readonly method?: \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n /**\n * Headers to send. Resolver may return a record per event. The\n * `Content-Type: application/json` and `Idempotency-Key` headers are\n * applied automatically; both can be overridden by returning a header\n * with the same name (case-insensitive).\n */\n readonly headers?: WebhookResolver<TEvents, Record<string, string>>;\n /**\n * Request body. Static plain data (object, array, string) or a\n * per-event function returning the same. Strings are sent as-is;\n * anything else is JSON-serialized. Defaults to the committed event\n * itself.\n */\n readonly body?:\n | WebhookBody\n | ((event: Committed<TEvents, keyof TEvents>) => WebhookBody);\n /**\n * Per-request timeout in milliseconds. Defaults to 5000.\n * The handler throws after the timeout via `AbortController`.\n */\n readonly timeoutMs?: number;\n /**\n * Override for the auto-generated `Idempotency-Key`. By default, the\n * helper sends `event.id` (the immutable, monotonic event identifier).\n * Return a string to override; return `null` to skip the header entirely.\n */\n readonly idempotencyKey?: (\n event: Committed<TEvents, keyof TEvents>\n ) => string | null;\n /**\n * Injection point for tests. Defaults to global `fetch`.\n */\n readonly fetch?: typeof fetch;\n};\n\n/**\n * Error thrown by the webhook handler on network failure, timeout, or\n * non-2xx response. The `status` field is `0` for network / timeout\n * errors and the HTTP status code otherwise.\n *\n * `retryable` reflects the helper's classification: network errors,\n * timeouts, and 5xx are flagged retryable; 4xx is not. The current drain\n * pipeline does not distinguish — both are caught and counted against\n * `maxRetries`. Callers who want different retry semantics per category\n * can introspect the error in a wrapping handler or tune `maxRetries` /\n * `backoff` on the reaction options.\n */\nexport class WebhookError extends Error {\n readonly status: number;\n readonly retryable: boolean;\n readonly url: string;\n readonly responseBody?: string;\n\n constructor(\n message: string,\n init: {\n status: number;\n retryable: boolean;\n url: string;\n responseBody?: string;\n }\n ) {\n super(message);\n this.name = \"WebhookError\";\n this.status = init.status;\n this.retryable = init.retryable;\n this.url = init.url;\n this.responseBody = init.responseBody;\n }\n}\n","/**\n * @packageDocumentation\n * @module act-http/webhook\n *\n * Reaction-handler sugar for POSTing committed events to external URLs.\n *\n * Wraps `fetch` with timeouts, automatic `Idempotency-Key` derivation, and\n * status-classified errors. Designed to be composed with the reaction\n * options shipped in ACT-601 (`maxRetries`, `blockOnError`, `backoff`):\n *\n * ```ts\n * import { webhook } from \"@rotorsoft/act-http/webhook\";\n *\n * .on(\"OrderConfirmed\")\n * .do(\n * webhook({\n * url: \"https://api.example.com/webhooks/orders\",\n * headers: (e) => ({ Authorization: \"Bearer ...\" }),\n * body: (e) => ({ orderId: e.stream, total: e.data.total }),\n * timeoutMs: 5_000,\n * }),\n * { maxRetries: 5, backoff: { strategy: \"exponential\", baseMs: 200, maxMs: 30_000 } }\n * )\n * .to(resolver);\n * ```\n */\n\nimport type { Committed, ReactionHandler, Schemas } from \"@rotorsoft/act\";\nimport { type WebhookConfig, WebhookError } from \"./types.js\";\n\nexport type { WebhookConfig, WebhookResolver } from \"./types.js\";\nexport { WebhookError } from \"./types.js\";\n\nfunction resolve<TEvents extends Schemas, T>(\n resolver: T | ((e: Committed<TEvents, keyof TEvents>) => T) | undefined,\n event: Committed<TEvents, keyof TEvents>,\n fallback: T\n): T {\n if (resolver === undefined) return fallback;\n return typeof resolver === \"function\"\n ? (resolver as (e: Committed<TEvents, keyof TEvents>) => T)(event)\n : resolver;\n}\n\n/** Case-insensitive lookup; returns true if a header is already set. */\nfunction hasHeader(headers: Record<string, string>, name: string): boolean {\n const lower = name.toLowerCase();\n for (const k of Object.keys(headers)) {\n if (k.toLowerCase() === lower) return true;\n }\n return false;\n}\n\n/**\n * Build a reaction handler that POSTs each event to an external URL.\n *\n * Behavior:\n *\n * - Network errors and timeouts throw {@link WebhookError} with\n * `status: 0`, `retryable: true`.\n * - 5xx responses throw with `retryable: true`.\n * - 4xx responses throw with `retryable: false`.\n * - 2xx and 3xx return successfully.\n *\n * Drain retry behavior follows the reaction's `maxRetries` / `backoff`\n * options. To skip retries entirely for client errors, set\n * `maxRetries: 0` on the reaction — both 4xx and 5xx will block on the\n * first failed attempt.\n */\nexport function webhook<TEvents extends Schemas = Schemas>(\n config: WebhookConfig<TEvents>\n): ReactionHandler<TEvents, keyof TEvents> {\n const timeoutMs = config.timeoutMs ?? 5_000;\n const method = config.method ?? \"POST\";\n const fetchImpl = config.fetch ?? globalThis.fetch;\n\n // Named function: slice/act builders require non-anonymous reaction\n // handlers so lifecycle telemetry can attribute work.\n return async function webhookDeliver(event) {\n const url = resolve(config.url, event, \"\");\n\n const customHeaders = resolve(\n config.headers,\n event,\n {} as Record<string, string>\n );\n const headers: Record<string, string> = { ...customHeaders };\n\n if (!hasHeader(headers, \"content-type\")) {\n headers[\"Content-Type\"] = \"application/json\";\n }\n if (!hasHeader(headers, \"idempotency-key\")) {\n const key = config.idempotencyKey\n ? config.idempotencyKey(event)\n : String(event.id);\n if (key !== null) headers[\"Idempotency-Key\"] = key;\n }\n\n const rawBody = resolve(config.body, event, event as unknown);\n const body =\n typeof rawBody === \"string\" ? rawBody : JSON.stringify(rawBody);\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n let response: Response;\n try {\n response = await fetchImpl(url, {\n method,\n headers,\n body,\n signal: controller.signal,\n });\n } catch (err) {\n const aborted = controller.signal.aborted;\n throw new WebhookError(\n aborted\n ? `webhook ${method} ${url} timed out after ${timeoutMs}ms`\n : `webhook ${method} ${url} failed: ${(err as Error).message}`,\n { status: 0, retryable: true, url }\n );\n } finally {\n clearTimeout(timer);\n }\n\n if (response.ok) return;\n\n let responseBody: string | undefined;\n try {\n responseBody = await response.text();\n } catch {\n // best-effort body capture; ignore read errors\n }\n\n const retryable = response.status >= 500;\n throw new WebhookError(\n `webhook ${method} ${url} responded ${response.status}`,\n { status: response.status, retryable, url, responseBody }\n );\n };\n}\n"],"mappings":";AAgFO,IAAM,eAAN,cAA2B,MAAM;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAET,YACE,SACA,MAMA;AACA,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS,KAAK;AACnB,SAAK,YAAY,KAAK;AACtB,SAAK,MAAM,KAAK;AAChB,SAAK,eAAe,KAAK;AAAA,EAC3B;AACF;;;ACrEA,SAAS,QACP,UACA,OACA,UACG;AACH,MAAI,aAAa,OAAW,QAAO;AACnC,SAAO,OAAO,aAAa,aACtB,SAAyD,KAAK,IAC/D;AACN;AAGA,SAAS,UAAU,SAAiC,MAAuB;AACzE,QAAM,QAAQ,KAAK,YAAY;AAC/B,aAAW,KAAK,OAAO,KAAK,OAAO,GAAG;AACpC,QAAI,EAAE,YAAY,MAAM,MAAO,QAAO;AAAA,EACxC;AACA,SAAO;AACT;AAkBO,SAAS,QACd,QACyC;AACzC,QAAM,YAAY,OAAO,aAAa;AACtC,QAAM,SAAS,OAAO,UAAU;AAChC,QAAM,YAAY,OAAO,SAAS,WAAW;AAI7C,SAAO,eAAe,eAAe,OAAO;AAC1C,UAAM,MAAM,QAAQ,OAAO,KAAK,OAAO,EAAE;AAEzC,UAAM,gBAAgB;AAAA,MACpB,OAAO;AAAA,MACP;AAAA,MACA,CAAC;AAAA,IACH;AACA,UAAM,UAAkC,EAAE,GAAG,cAAc;AAE3D,QAAI,CAAC,UAAU,SAAS,cAAc,GAAG;AACvC,cAAQ,cAAc,IAAI;AAAA,IAC5B;AACA,QAAI,CAAC,UAAU,SAAS,iBAAiB,GAAG;AAC1C,YAAM,MAAM,OAAO,iBACf,OAAO,eAAe,KAAK,IAC3B,OAAO,MAAM,EAAE;AACnB,UAAI,QAAQ,KAAM,SAAQ,iBAAiB,IAAI;AAAA,IACjD;AAEA,UAAM,UAAU,QAAQ,OAAO,MAAM,OAAO,KAAgB;AAC5D,UAAM,OACJ,OAAO,YAAY,WAAW,UAAU,KAAK,UAAU,OAAO;AAEhE,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,SAAS;AAE5D,QAAI;AACJ,QAAI;AACF,iBAAW,MAAM,UAAU,KAAK;AAAA,QAC9B;AAAA,QACA;AAAA,QACA;AAAA,QACA,QAAQ,WAAW;AAAA,MACrB,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,YAAM,UAAU,WAAW,OAAO;AAClC,YAAM,IAAI;AAAA,QACR,UACI,WAAW,MAAM,IAAI,GAAG,oBAAoB,SAAS,OACrD,WAAW,MAAM,IAAI,GAAG,YAAa,IAAc,OAAO;AAAA,QAC9D,EAAE,QAAQ,GAAG,WAAW,MAAM,IAAI;AAAA,MACpC;AAAA,IACF,UAAE;AACA,mBAAa,KAAK;AAAA,IACpB;AAEA,QAAI,SAAS,GAAI;AAEjB,QAAI;AACJ,QAAI;AACF,qBAAe,MAAM,SAAS,KAAK;AAAA,IACrC,QAAQ;AAAA,IAER;AAEA,UAAM,YAAY,SAAS,UAAU;AACrC,UAAM,IAAI;AAAA,MACR,WAAW,MAAM,IAAI,GAAG,cAAc,SAAS,MAAM;AAAA,MACrD,EAAE,QAAQ,SAAS,QAAQ,WAAW,KAAK,aAAa;AAAA,IAC1D;AAAA,EACF;AACF;","names":[]}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@rotorsoft/act-http",
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "description": "HTTP integrations for act apps — webhooks and SSE",
6
+ "keywords": [
7
+ "typescript",
8
+ "http",
9
+ "webhook",
10
+ "sse",
11
+ "server-sent-events",
12
+ "real-time"
13
+ ],
14
+ "author": "rotorsoft",
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/rotorsoft/act-root.git",
19
+ "directory": "libs/act-http"
20
+ },
21
+ "homepage": "https://github.com/rotorsoft/act-root/tree/master/libs/act-http#readme",
22
+ "bugs": {
23
+ "url": "https://github.com/rotorsoft/act-root/issues"
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "exports": {
29
+ "./webhook": {
30
+ "types": "./dist/@types/webhook/index.d.ts",
31
+ "import": "./dist/webhook/index.js",
32
+ "require": "./dist/webhook/index.cjs"
33
+ },
34
+ "./sse": {
35
+ "types": "./dist/@types/sse/index.d.ts",
36
+ "import": "./dist/sse/index.js",
37
+ "require": "./dist/sse/index.cjs"
38
+ }
39
+ },
40
+ "sideEffects": false,
41
+ "engines": {
42
+ "node": ">=22.18.0"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "peerDependencies": {
48
+ "@rotorsoft/act": "^0.42.0"
49
+ },
50
+ "dependencies": {
51
+ "@rotorsoft/act-patch": "^1.2.2"
52
+ },
53
+ "scripts": {
54
+ "clean": "rm -rf dist",
55
+ "types": "tsc --build tsconfig.build.json --emitDeclarationOnly",
56
+ "build": "pnpm clean && tsup && pnpm types"
57
+ }
58
+ }