@graphorin/server 0.5.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.
- package/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/dist/app.d.ts +174 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +684 -0
- package/dist/app.js.map +1 -0
- package/dist/commentary/audit-bridge.d.ts +57 -0
- package/dist/commentary/audit-bridge.d.ts.map +1 -0
- package/dist/commentary/audit-bridge.js +92 -0
- package/dist/commentary/audit-bridge.js.map +1 -0
- package/dist/commentary/built-in-patterns.d.ts +24 -0
- package/dist/commentary/built-in-patterns.d.ts.map +1 -0
- package/dist/commentary/built-in-patterns.js +62 -0
- package/dist/commentary/built-in-patterns.js.map +1 -0
- package/dist/commentary/index.d.ts +5 -0
- package/dist/commentary/index.js +5 -0
- package/dist/commentary/sanitizer.d.ts +37 -0
- package/dist/commentary/sanitizer.d.ts.map +1 -0
- package/dist/commentary/sanitizer.js +182 -0
- package/dist/commentary/sanitizer.js.map +1 -0
- package/dist/commentary/types.d.ts +120 -0
- package/dist/commentary/types.d.ts.map +1 -0
- package/dist/config.d.ts +760 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +217 -0
- package/dist/config.js.map +1 -0
- package/dist/consolidator/daemon.d.ts +88 -0
- package/dist/consolidator/daemon.d.ts.map +1 -0
- package/dist/consolidator/daemon.js +54 -0
- package/dist/consolidator/daemon.js.map +1 -0
- package/dist/consolidator/index.d.ts +2 -0
- package/dist/consolidator/index.js +3 -0
- package/dist/errors/index.d.ts +104 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +131 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/health/checks.d.ts +120 -0
- package/dist/health/checks.d.ts.map +1 -0
- package/dist/health/checks.js +127 -0
- package/dist/health/checks.js.map +1 -0
- package/dist/health/index.d.ts +3 -0
- package/dist/health/index.js +4 -0
- package/dist/health/routes.d.ts +66 -0
- package/dist/health/routes.d.ts.map +1 -0
- package/dist/health/routes.js +96 -0
- package/dist/health/routes.js.map +1 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +86 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/context.d.ts +66 -0
- package/dist/internal/context.d.ts.map +1 -0
- package/dist/internal/ids.js +21 -0
- package/dist/internal/ids.js.map +1 -0
- package/dist/internal/json.js +46 -0
- package/dist/internal/json.js.map +1 -0
- package/dist/lifecycle/hooks.d.ts +57 -0
- package/dist/lifecycle/hooks.d.ts.map +1 -0
- package/dist/lifecycle/index.d.ts +2 -0
- package/dist/lifecycle/index.js +3 -0
- package/dist/lifecycle/pre-bind.d.ts +38 -0
- package/dist/lifecycle/pre-bind.d.ts.map +1 -0
- package/dist/lifecycle/pre-bind.js +62 -0
- package/dist/lifecycle/pre-bind.js.map +1 -0
- package/dist/metrics/catalog.d.ts +34 -0
- package/dist/metrics/catalog.d.ts.map +1 -0
- package/dist/metrics/catalog.js +61 -0
- package/dist/metrics/catalog.js.map +1 -0
- package/dist/metrics/index.d.ts +3 -0
- package/dist/metrics/index.js +4 -0
- package/dist/metrics/registry.d.ts +63 -0
- package/dist/metrics/registry.d.ts.map +1 -0
- package/dist/metrics/registry.js +222 -0
- package/dist/metrics/registry.js.map +1 -0
- package/dist/middleware/audit.d.ts +47 -0
- package/dist/middleware/audit.d.ts.map +1 -0
- package/dist/middleware/audit.js +71 -0
- package/dist/middleware/audit.js.map +1 -0
- package/dist/middleware/auth.d.ts +50 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +164 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/cors.d.ts +15 -0
- package/dist/middleware/cors.d.ts.map +1 -0
- package/dist/middleware/cors.js +45 -0
- package/dist/middleware/cors.js.map +1 -0
- package/dist/middleware/csrf.d.ts +15 -0
- package/dist/middleware/csrf.d.ts.map +1 -0
- package/dist/middleware/csrf.js +77 -0
- package/dist/middleware/csrf.js.map +1 -0
- package/dist/middleware/idempotency.d.ts +41 -0
- package/dist/middleware/idempotency.d.ts.map +1 -0
- package/dist/middleware/idempotency.js +198 -0
- package/dist/middleware/idempotency.js.map +1 -0
- package/dist/middleware/index.d.ts +9 -0
- package/dist/middleware/index.js +10 -0
- package/dist/middleware/rate-limit.d.ts +17 -0
- package/dist/middleware/rate-limit.d.ts.map +1 -0
- package/dist/middleware/rate-limit.js +47 -0
- package/dist/middleware/rate-limit.js.map +1 -0
- package/dist/middleware/request-state.d.ts +27 -0
- package/dist/middleware/request-state.d.ts.map +1 -0
- package/dist/middleware/request-state.js +42 -0
- package/dist/middleware/request-state.js.map +1 -0
- package/dist/middleware/scope.d.ts +24 -0
- package/dist/middleware/scope.d.ts.map +1 -0
- package/dist/middleware/scope.js +50 -0
- package/dist/middleware/scope.js.map +1 -0
- package/dist/registry/index.d.ts +135 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +96 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/replay/index.d.ts +2 -0
- package/dist/replay/index.js +3 -0
- package/dist/replay/routes.d.ts +46 -0
- package/dist/replay/routes.d.ts.map +1 -0
- package/dist/replay/routes.js +189 -0
- package/dist/replay/routes.js.map +1 -0
- package/dist/routes/agents.d.ts +41 -0
- package/dist/routes/agents.d.ts.map +1 -0
- package/dist/routes/agents.js +213 -0
- package/dist/routes/agents.js.map +1 -0
- package/dist/routes/audit.d.ts +57 -0
- package/dist/routes/audit.d.ts.map +1 -0
- package/dist/routes/audit.js +116 -0
- package/dist/routes/audit.js.map +1 -0
- package/dist/routes/auth.d.ts +26 -0
- package/dist/routes/auth.d.ts.map +1 -0
- package/dist/routes/auth.js +54 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/health.d.ts +22 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +33 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/index.d.ts +10 -0
- package/dist/routes/index.js +12 -0
- package/dist/routes/mcp.d.ts +32 -0
- package/dist/routes/mcp.d.ts.map +1 -0
- package/dist/routes/mcp.js +61 -0
- package/dist/routes/mcp.js.map +1 -0
- package/dist/routes/memory.d.ts +141 -0
- package/dist/routes/memory.d.ts.map +1 -0
- package/dist/routes/memory.js +102 -0
- package/dist/routes/memory.js.map +1 -0
- package/dist/routes/sessions.d.ts +54 -0
- package/dist/routes/sessions.d.ts.map +1 -0
- package/dist/routes/sessions.js +135 -0
- package/dist/routes/sessions.js.map +1 -0
- package/dist/routes/skills.d.ts +34 -0
- package/dist/routes/skills.d.ts.map +1 -0
- package/dist/routes/skills.js +54 -0
- package/dist/routes/skills.js.map +1 -0
- package/dist/routes/tokens.d.ts +25 -0
- package/dist/routes/tokens.d.ts.map +1 -0
- package/dist/routes/tokens.js +87 -0
- package/dist/routes/tokens.js.map +1 -0
- package/dist/routes/workflows.d.ts +27 -0
- package/dist/routes/workflows.d.ts.map +1 -0
- package/dist/routes/workflows.js +220 -0
- package/dist/routes/workflows.js.map +1 -0
- package/dist/runtime/run-state.d.ts +158 -0
- package/dist/runtime/run-state.d.ts.map +1 -0
- package/dist/runtime/run-state.js +182 -0
- package/dist/runtime/run-state.js.map +1 -0
- package/dist/sse/events.d.ts +44 -0
- package/dist/sse/events.d.ts.map +1 -0
- package/dist/sse/events.js +200 -0
- package/dist/sse/events.js.map +1 -0
- package/dist/sse/index.d.ts +2 -0
- package/dist/sse/index.js +3 -0
- package/dist/triggers/daemon.d.ts +74 -0
- package/dist/triggers/daemon.d.ts.map +1 -0
- package/dist/triggers/daemon.js +114 -0
- package/dist/triggers/daemon.js.map +1 -0
- package/dist/triggers/index.d.ts +3 -0
- package/dist/triggers/index.js +4 -0
- package/dist/triggers/routes.d.ts +21 -0
- package/dist/triggers/routes.d.ts.map +1 -0
- package/dist/triggers/routes.js +117 -0
- package/dist/triggers/routes.js.map +1 -0
- package/dist/ws/dispatcher.d.ts +169 -0
- package/dist/ws/dispatcher.d.ts.map +1 -0
- package/dist/ws/dispatcher.js +303 -0
- package/dist/ws/dispatcher.js.map +1 -0
- package/dist/ws/index.d.ts +6 -0
- package/dist/ws/index.js +7 -0
- package/dist/ws/replay-buffer.d.ts +47 -0
- package/dist/ws/replay-buffer.d.ts.map +1 -0
- package/dist/ws/replay-buffer.js +88 -0
- package/dist/ws/replay-buffer.js.map +1 -0
- package/dist/ws/subjects.d.ts +71 -0
- package/dist/ws/subjects.d.ts.map +1 -0
- package/dist/ws/subjects.js +112 -0
- package/dist/ws/subjects.js.map +1 -0
- package/dist/ws/ticket.d.ts +74 -0
- package/dist/ws/ticket.d.ts.map +1 -0
- package/dist/ws/ticket.js +112 -0
- package/dist/ws/ticket.js.map +1 -0
- package/dist/ws/upgrade.d.ts +63 -0
- package/dist/ws/upgrade.d.ts.map +1 -0
- package/dist/ws/upgrade.js +324 -0
- package/dist/ws/upgrade.js.map +1 -0
- package/package.json +156 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
//#region src/runtime/run-state.ts
|
|
2
|
+
function isTerminalStatus(status) {
|
|
3
|
+
return status === "completed" || status === "failed" || status === "aborted";
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Pluggable tracker. The default in-memory implementation is the only
|
|
7
|
+
* one shipped in Phase 14a; future phases plug in a SQLite-backed
|
|
8
|
+
* variant so durable resume survives process restarts.
|
|
9
|
+
*
|
|
10
|
+
* @stable
|
|
11
|
+
*/
|
|
12
|
+
var RunStateTracker = class {
|
|
13
|
+
#records = /* @__PURE__ */ new Map();
|
|
14
|
+
#now;
|
|
15
|
+
#onTerminal;
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.#now = options.now ?? Date.now;
|
|
18
|
+
this.#onTerminal = options.onTerminal;
|
|
19
|
+
}
|
|
20
|
+
#emitTerminal(record) {
|
|
21
|
+
if (this.#onTerminal === void 0) return;
|
|
22
|
+
if (!isTerminalStatus(record.status)) return;
|
|
23
|
+
const durationMs = record.startedAt !== void 0 && record.completedAt !== void 0 ? record.completedAt - record.startedAt : void 0;
|
|
24
|
+
this.#onTerminal({
|
|
25
|
+
status: record.status,
|
|
26
|
+
kind: record.kind,
|
|
27
|
+
...durationMs !== void 0 ? { durationMs } : {}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** Reserve a run id without taking ownership of an AbortSignal. */
|
|
31
|
+
declare(runId, descriptor) {
|
|
32
|
+
if (this.#records.has(runId)) return;
|
|
33
|
+
const controller = new AbortController();
|
|
34
|
+
this.#records.set(runId, {
|
|
35
|
+
runId,
|
|
36
|
+
kind: descriptor.kind,
|
|
37
|
+
descriptor,
|
|
38
|
+
status: "pending",
|
|
39
|
+
controller
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** Promote a previously-declared run to `running` (or declare it). */
|
|
43
|
+
start(runId, descriptor) {
|
|
44
|
+
const record = this.#records.get(runId) ?? {
|
|
45
|
+
runId,
|
|
46
|
+
kind: descriptor.kind,
|
|
47
|
+
descriptor,
|
|
48
|
+
status: "pending",
|
|
49
|
+
controller: new AbortController()
|
|
50
|
+
};
|
|
51
|
+
record.status = "running";
|
|
52
|
+
record.startedAt = this.#now();
|
|
53
|
+
this.#records.set(runId, record);
|
|
54
|
+
return Object.freeze({
|
|
55
|
+
runId,
|
|
56
|
+
signal: record.controller.signal,
|
|
57
|
+
cancel: (reason) => {
|
|
58
|
+
if (!record.controller.signal.aborted) record.controller.abort(reason);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** Mark a run as terminal. */
|
|
63
|
+
complete(runId, status, err) {
|
|
64
|
+
const record = this.#records.get(runId);
|
|
65
|
+
if (record === void 0) return;
|
|
66
|
+
const wasTerminal = isTerminalStatus(record.status);
|
|
67
|
+
record.status = status;
|
|
68
|
+
record.completedAt = this.#now();
|
|
69
|
+
if (err !== void 0) record.error = { message: err instanceof Error ? err.message : String(err) };
|
|
70
|
+
if (!wasTerminal) this.#emitTerminal(record);
|
|
71
|
+
}
|
|
72
|
+
/** Cancel a run via its `AbortController`. */
|
|
73
|
+
abort(runId, reason) {
|
|
74
|
+
const record = this.#records.get(runId);
|
|
75
|
+
if (record === void 0) return false;
|
|
76
|
+
if (!record.controller.signal.aborted) record.controller.abort(reason);
|
|
77
|
+
if (record.status === "pending" || record.status === "running") {
|
|
78
|
+
record.status = "aborted";
|
|
79
|
+
record.completedAt = this.#now();
|
|
80
|
+
this.#emitTerminal(record);
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
/** Read-only snapshot, safe to JSON.stringify. */
|
|
85
|
+
snapshot(runId) {
|
|
86
|
+
const record = this.#records.get(runId);
|
|
87
|
+
if (record === void 0) return void 0;
|
|
88
|
+
const base = {
|
|
89
|
+
runId: record.runId,
|
|
90
|
+
kind: record.kind,
|
|
91
|
+
status: record.status
|
|
92
|
+
};
|
|
93
|
+
if (record.startedAt !== void 0) base.startedAt = record.startedAt;
|
|
94
|
+
if (record.completedAt !== void 0) base.completedAt = record.completedAt;
|
|
95
|
+
if (record.error !== void 0) base.error = record.error;
|
|
96
|
+
if (record.descriptor.kind === "agent") base.agentId = record.descriptor.agentId;
|
|
97
|
+
else {
|
|
98
|
+
base.workflowId = record.descriptor.workflowId;
|
|
99
|
+
if (record.descriptor.threadId !== void 0) base.threadId = record.descriptor.threadId;
|
|
100
|
+
}
|
|
101
|
+
if (record.descriptor.sessionId !== void 0) base.sessionId = record.descriptor.sessionId;
|
|
102
|
+
if (record.descriptor.userId !== void 0) base.userId = record.descriptor.userId;
|
|
103
|
+
return Object.freeze(base);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Number of runs currently in `pending` or `running`. Useful for
|
|
107
|
+
* snapshots / metrics. Note that `pending` runs hold a reservation
|
|
108
|
+
* but have not yet started any work — see {@link runningCount} for
|
|
109
|
+
* the drain-blocking subset.
|
|
110
|
+
*/
|
|
111
|
+
inflightCount() {
|
|
112
|
+
let n = 0;
|
|
113
|
+
for (const record of this.#records.values()) if (record.status === "pending" || record.status === "running") n += 1;
|
|
114
|
+
return n;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Number of runs with active work in progress (`running`). The
|
|
118
|
+
* lifecycle drain blocks on this counter only — pending runs are a
|
|
119
|
+
* pure reservation (e.g. an awaited WS subscription) and can be
|
|
120
|
+
* aborted immediately when SIGTERM arrives.
|
|
121
|
+
*/
|
|
122
|
+
runningCount() {
|
|
123
|
+
let n = 0;
|
|
124
|
+
for (const record of this.#records.values()) if (record.status === "running") n += 1;
|
|
125
|
+
return n;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Drop every reserved-but-not-yet-started run. Called by the
|
|
129
|
+
* server lifecycle at the start of `stop()` so the drain only
|
|
130
|
+
* waits for actual work in flight.
|
|
131
|
+
*/
|
|
132
|
+
abortPending(reason) {
|
|
133
|
+
let aborted = 0;
|
|
134
|
+
for (const record of this.#records.values()) if (record.status === "pending") {
|
|
135
|
+
if (!record.controller.signal.aborted) record.controller.abort(reason);
|
|
136
|
+
record.status = "aborted";
|
|
137
|
+
record.completedAt = this.#now();
|
|
138
|
+
this.#emitTerminal(record);
|
|
139
|
+
aborted += 1;
|
|
140
|
+
}
|
|
141
|
+
return aborted;
|
|
142
|
+
}
|
|
143
|
+
/** Drop terminal records older than `olderThan`. */
|
|
144
|
+
prune(olderThan) {
|
|
145
|
+
let removed = 0;
|
|
146
|
+
for (const [runId, record] of this.#records) if (record.completedAt !== void 0 && record.completedAt <= olderThan && record.status !== "pending" && record.status !== "running") {
|
|
147
|
+
this.#records.delete(runId);
|
|
148
|
+
removed += 1;
|
|
149
|
+
}
|
|
150
|
+
return removed;
|
|
151
|
+
}
|
|
152
|
+
/** Cancel every in-flight run. Used during graceful shutdown. */
|
|
153
|
+
abortAll(reason) {
|
|
154
|
+
let aborted = 0;
|
|
155
|
+
for (const [runId] of this.#records) if (this.abort(runId, reason)) aborted += 1;
|
|
156
|
+
return aborted;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
/** Default cadence for the terminal-record prune sweep (IP-16). */
|
|
160
|
+
const DEFAULT_RUN_PRUNE_INTERVAL_MS = 6e4;
|
|
161
|
+
/** Default retention for terminal run records before they are dropped. */
|
|
162
|
+
const DEFAULT_RUN_RETENTION_MS = 5 * 6e4;
|
|
163
|
+
/**
|
|
164
|
+
* IP-16: schedule a periodic prune of terminal run records. Without this the
|
|
165
|
+
* tracker's `prune()` was never called, so every run / stream / workflow left
|
|
166
|
+
* a `RunRecord` (each holding an `AbortController`) in memory forever — an
|
|
167
|
+
* unbounded leak on a long-living server. Returns a stop function that clears
|
|
168
|
+
* the timer; the timer is `unref`-ed so it never keeps the process alive.
|
|
169
|
+
*/
|
|
170
|
+
function scheduleRunPruning(runs, now, opts = {}) {
|
|
171
|
+
const intervalMs = opts.intervalMs ?? DEFAULT_RUN_PRUNE_INTERVAL_MS;
|
|
172
|
+
const retentionMs = opts.retentionMs ?? DEFAULT_RUN_RETENTION_MS;
|
|
173
|
+
const timer = setInterval(() => {
|
|
174
|
+
runs.prune(now() - retentionMs);
|
|
175
|
+
}, intervalMs);
|
|
176
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
177
|
+
return () => clearInterval(timer);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
//#endregion
|
|
181
|
+
export { RunStateTracker, scheduleRunPruning };
|
|
182
|
+
//# sourceMappingURL=run-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-state.js","names":["#records","#now","#onTerminal","record: RunRecord","#emitTerminal","base: { -readonly [K in keyof RunStateSnapshot]?: RunStateSnapshot[K] }"],"sources":["../../src/runtime/run-state.ts"],"sourcesContent":["/**\n * In-memory bookkeeping for in-flight agent / workflow runs. Exposes\n * a tiny CRUD surface route handlers consume to honour the\n * `GET /runs/:runId/state` and `POST /runs/:runId/abort` endpoints\n * declared in the runtime architecture.\n *\n * The full durable resume / replay path lives in Phase 14b/c on top\n * of the WebSocket layer + the consolidator daemon. Phase 14a only\n * needs enough state to:\n * - mint a runId on every `POST /run` / `POST /stream`,\n * - track its lifecycle (`pending` → `running` → `completed` / `failed` / `aborted`),\n * - propagate `AbortController.signal` so handlers can cancel.\n *\n * @packageDocumentation\n */\n\n/**\n * Stable status discriminator for a run snapshot. Mirrors the values\n * exposed on the public REST surface.\n *\n * @stable\n */\nexport type RunStatus = 'pending' | 'running' | 'completed' | 'failed' | 'aborted';\n\n/**\n * Identifying tag for the underlying execution kind. Workflows run\n * on the durable engine in `@graphorin/workflow`; agents run on the\n * `@graphorin/agent` runtime.\n *\n * @stable\n */\nexport type RunKind = 'agent' | 'workflow';\n\n/**\n * Terminal status a run can settle into (never `pending` / `running`).\n *\n * @stable\n */\nexport type TerminalRunStatus = Extract<RunStatus, 'completed' | 'failed' | 'aborted'>;\n\n/**\n * IP-15: payload handed to the {@link RunStateTracker} `onTerminal` callback\n * the first time a run reaches a terminal state. The server turns this into\n * the `graphorin_agent_runs_total` counter + `graphorin_agent_run_duration_seconds`\n * summary. `durationMs` is omitted for a run that was aborted before it ever\n * started (no meaningful wall-clock).\n *\n * @stable\n */\nexport interface TerminalRunInfo {\n readonly status: TerminalRunStatus;\n readonly kind: RunKind;\n readonly durationMs?: number;\n}\n\nfunction isTerminalStatus(status: RunStatus): status is TerminalRunStatus {\n return status === 'completed' || status === 'failed' || status === 'aborted';\n}\n\n/**\n * Snapshot returned by {@link RunStateTracker.snapshot}.\n *\n * @stable\n */\nexport interface RunStateSnapshot {\n readonly runId: string;\n readonly kind: RunKind;\n readonly status: RunStatus;\n readonly startedAt?: number;\n readonly completedAt?: number;\n readonly error?: { readonly message: string };\n readonly agentId?: string;\n readonly workflowId?: string;\n readonly threadId?: string;\n readonly sessionId?: string;\n readonly userId?: string;\n}\n\n/**\n * Bookkeeping descriptor recorded at run start. Either an agent run\n * (with `agentId`) or a workflow run (with `workflowId` + optional\n * `threadId`).\n *\n * @stable\n */\nexport type RunDescriptor =\n | {\n readonly kind: 'agent';\n readonly agentId: string;\n readonly sessionId?: string;\n readonly userId?: string;\n }\n | {\n readonly kind: 'workflow';\n readonly workflowId: string;\n readonly threadId?: string;\n readonly sessionId?: string;\n readonly userId?: string;\n };\n\n/**\n * In-flight handle returned by {@link RunStateTracker.start}. Handlers\n * pass `signal` into the underlying `agent.run / workflow.execute`\n * invocation so cancellation propagates instantly.\n *\n * @stable\n */\nexport interface RunHandle {\n readonly runId: string;\n readonly signal: AbortSignal;\n cancel(reason?: unknown): void;\n}\n\ninterface RunRecord {\n readonly runId: string;\n readonly kind: RunKind;\n readonly descriptor: RunDescriptor;\n status: RunStatus;\n controller: AbortController;\n startedAt?: number;\n completedAt?: number;\n error?: { readonly message: string };\n}\n\n/**\n * Pluggable tracker. The default in-memory implementation is the only\n * one shipped in Phase 14a; future phases plug in a SQLite-backed\n * variant so durable resume survives process restarts.\n *\n * @stable\n */\nexport class RunStateTracker {\n readonly #records: Map<string, RunRecord> = new Map();\n readonly #now: () => number;\n readonly #onTerminal: ((info: TerminalRunInfo) => void) | undefined;\n\n constructor(\n options: {\n readonly now?: () => number;\n /**\n * IP-15: invoked exactly once per run, the first time it settles into a\n * terminal state. Used to drive the run-count + duration metrics. Never\n * throws into the tracker — wrap your handler if it might.\n */\n readonly onTerminal?: (info: TerminalRunInfo) => void;\n } = {},\n ) {\n this.#now = options.now ?? Date.now;\n this.#onTerminal = options.onTerminal;\n }\n\n #emitTerminal(record: RunRecord): void {\n if (this.#onTerminal === undefined) return;\n if (!isTerminalStatus(record.status)) return;\n const durationMs =\n record.startedAt !== undefined && record.completedAt !== undefined\n ? record.completedAt - record.startedAt\n : undefined;\n this.#onTerminal({\n status: record.status,\n kind: record.kind,\n ...(durationMs !== undefined ? { durationMs } : {}),\n });\n }\n\n /** Reserve a run id without taking ownership of an AbortSignal. */\n declare(runId: string, descriptor: RunDescriptor): void {\n if (this.#records.has(runId)) return;\n const controller = new AbortController();\n this.#records.set(runId, {\n runId,\n kind: descriptor.kind,\n descriptor,\n status: 'pending',\n controller,\n });\n }\n\n /** Promote a previously-declared run to `running` (or declare it). */\n start(runId: string, descriptor: RunDescriptor): RunHandle {\n const existing = this.#records.get(runId);\n const record: RunRecord = existing ?? {\n runId,\n kind: descriptor.kind,\n descriptor,\n status: 'pending',\n controller: new AbortController(),\n };\n record.status = 'running';\n record.startedAt = this.#now();\n this.#records.set(runId, record);\n return Object.freeze({\n runId,\n signal: record.controller.signal,\n cancel: (reason?: unknown) => {\n if (!record.controller.signal.aborted) {\n record.controller.abort(reason);\n }\n },\n });\n }\n\n /** Mark a run as terminal. */\n complete(\n runId: string,\n status: Extract<RunStatus, 'completed' | 'failed' | 'aborted'>,\n err?: unknown,\n ): void {\n const record = this.#records.get(runId);\n if (record === undefined) return;\n // IP-15: only the FIRST terminal transition counts toward the metrics —\n // a run aborted then re-completed must not double-increment.\n const wasTerminal = isTerminalStatus(record.status);\n record.status = status;\n record.completedAt = this.#now();\n if (err !== undefined) {\n record.error = { message: err instanceof Error ? err.message : String(err) };\n }\n if (!wasTerminal) this.#emitTerminal(record);\n }\n\n /** Cancel a run via its `AbortController`. */\n abort(runId: string, reason?: unknown): boolean {\n const record = this.#records.get(runId);\n if (record === undefined) return false;\n if (!record.controller.signal.aborted) {\n record.controller.abort(reason);\n }\n if (record.status === 'pending' || record.status === 'running') {\n record.status = 'aborted';\n record.completedAt = this.#now();\n this.#emitTerminal(record);\n }\n return true;\n }\n\n /** Read-only snapshot, safe to JSON.stringify. */\n snapshot(runId: string): RunStateSnapshot | undefined {\n const record = this.#records.get(runId);\n if (record === undefined) return undefined;\n const base: { -readonly [K in keyof RunStateSnapshot]?: RunStateSnapshot[K] } = {\n runId: record.runId,\n kind: record.kind,\n status: record.status,\n };\n if (record.startedAt !== undefined) base.startedAt = record.startedAt;\n if (record.completedAt !== undefined) base.completedAt = record.completedAt;\n if (record.error !== undefined) base.error = record.error;\n if (record.descriptor.kind === 'agent') {\n base.agentId = record.descriptor.agentId;\n } else {\n base.workflowId = record.descriptor.workflowId;\n if (record.descriptor.threadId !== undefined) base.threadId = record.descriptor.threadId;\n }\n if (record.descriptor.sessionId !== undefined) base.sessionId = record.descriptor.sessionId;\n if (record.descriptor.userId !== undefined) base.userId = record.descriptor.userId;\n return Object.freeze(base as RunStateSnapshot);\n }\n\n /**\n * Number of runs currently in `pending` or `running`. Useful for\n * snapshots / metrics. Note that `pending` runs hold a reservation\n * but have not yet started any work — see {@link runningCount} for\n * the drain-blocking subset.\n */\n inflightCount(): number {\n let n = 0;\n for (const record of this.#records.values()) {\n if (record.status === 'pending' || record.status === 'running') n += 1;\n }\n return n;\n }\n\n /**\n * Number of runs with active work in progress (`running`). The\n * lifecycle drain blocks on this counter only — pending runs are a\n * pure reservation (e.g. an awaited WS subscription) and can be\n * aborted immediately when SIGTERM arrives.\n */\n runningCount(): number {\n let n = 0;\n for (const record of this.#records.values()) {\n if (record.status === 'running') n += 1;\n }\n return n;\n }\n\n /**\n * Drop every reserved-but-not-yet-started run. Called by the\n * server lifecycle at the start of `stop()` so the drain only\n * waits for actual work in flight.\n */\n abortPending(reason?: unknown): number {\n let aborted = 0;\n for (const record of this.#records.values()) {\n if (record.status === 'pending') {\n if (!record.controller.signal.aborted) record.controller.abort(reason);\n record.status = 'aborted';\n record.completedAt = this.#now();\n this.#emitTerminal(record);\n aborted += 1;\n }\n }\n return aborted;\n }\n\n /** Drop terminal records older than `olderThan`. */\n prune(olderThan: number): number {\n let removed = 0;\n for (const [runId, record] of this.#records) {\n if (\n record.completedAt !== undefined &&\n record.completedAt <= olderThan &&\n record.status !== 'pending' &&\n record.status !== 'running'\n ) {\n this.#records.delete(runId);\n removed += 1;\n }\n }\n return removed;\n }\n\n /** Cancel every in-flight run. Used during graceful shutdown. */\n abortAll(reason?: unknown): number {\n let aborted = 0;\n for (const [runId] of this.#records) {\n if (this.abort(runId, reason)) aborted += 1;\n }\n return aborted;\n }\n}\n\n/** Default cadence for the terminal-record prune sweep (IP-16). */\nexport const DEFAULT_RUN_PRUNE_INTERVAL_MS = 60_000;\n/** Default retention for terminal run records before they are dropped. */\nexport const DEFAULT_RUN_RETENTION_MS = 5 * 60_000;\n\n/**\n * IP-16: schedule a periodic prune of terminal run records. Without this the\n * tracker's `prune()` was never called, so every run / stream / workflow left\n * a `RunRecord` (each holding an `AbortController`) in memory forever — an\n * unbounded leak on a long-living server. Returns a stop function that clears\n * the timer; the timer is `unref`-ed so it never keeps the process alive.\n */\nexport function scheduleRunPruning(\n runs: RunStateTracker,\n now: () => number,\n opts: { readonly intervalMs?: number; readonly retentionMs?: number } = {},\n): () => void {\n const intervalMs = opts.intervalMs ?? DEFAULT_RUN_PRUNE_INTERVAL_MS;\n const retentionMs = opts.retentionMs ?? DEFAULT_RUN_RETENTION_MS;\n const timer = setInterval(() => {\n runs.prune(now() - retentionMs);\n }, intervalMs);\n if (typeof (timer as { unref?: () => void }).unref === 'function') {\n (timer as { unref: () => void }).unref();\n }\n return () => clearInterval(timer);\n}\n"],"mappings":";AAuDA,SAAS,iBAAiB,QAAgD;AACxE,QAAO,WAAW,eAAe,WAAW,YAAY,WAAW;;;;;;;;;AA2ErE,IAAa,kBAAb,MAA6B;CAC3B,CAASA,0BAAmC,IAAI,KAAK;CACrD,CAASC;CACT,CAASC;CAET,YACE,UAQI,EAAE,EACN;AACA,QAAKD,MAAO,QAAQ,OAAO,KAAK;AAChC,QAAKC,aAAc,QAAQ;;CAG7B,cAAc,QAAyB;AACrC,MAAI,MAAKA,eAAgB,OAAW;AACpC,MAAI,CAAC,iBAAiB,OAAO,OAAO,CAAE;EACtC,MAAM,aACJ,OAAO,cAAc,UAAa,OAAO,gBAAgB,SACrD,OAAO,cAAc,OAAO,YAC5B;AACN,QAAKA,WAAY;GACf,QAAQ,OAAO;GACf,MAAM,OAAO;GACb,GAAI,eAAe,SAAY,EAAE,YAAY,GAAG,EAAE;GACnD,CAAC;;;CAIJ,QAAQ,OAAe,YAAiC;AACtD,MAAI,MAAKF,QAAS,IAAI,MAAM,CAAE;EAC9B,MAAM,aAAa,IAAI,iBAAiB;AACxC,QAAKA,QAAS,IAAI,OAAO;GACvB;GACA,MAAM,WAAW;GACjB;GACA,QAAQ;GACR;GACD,CAAC;;;CAIJ,MAAM,OAAe,YAAsC;EAEzD,MAAMG,SADW,MAAKH,QAAS,IAAI,MAAM,IACH;GACpC;GACA,MAAM,WAAW;GACjB;GACA,QAAQ;GACR,YAAY,IAAI,iBAAiB;GAClC;AACD,SAAO,SAAS;AAChB,SAAO,YAAY,MAAKC,KAAM;AAC9B,QAAKD,QAAS,IAAI,OAAO,OAAO;AAChC,SAAO,OAAO,OAAO;GACnB;GACA,QAAQ,OAAO,WAAW;GAC1B,SAAS,WAAqB;AAC5B,QAAI,CAAC,OAAO,WAAW,OAAO,QAC5B,QAAO,WAAW,MAAM,OAAO;;GAGpC,CAAC;;;CAIJ,SACE,OACA,QACA,KACM;EACN,MAAM,SAAS,MAAKA,QAAS,IAAI,MAAM;AACvC,MAAI,WAAW,OAAW;EAG1B,MAAM,cAAc,iBAAiB,OAAO,OAAO;AACnD,SAAO,SAAS;AAChB,SAAO,cAAc,MAAKC,KAAM;AAChC,MAAI,QAAQ,OACV,QAAO,QAAQ,EAAE,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,EAAE;AAE9E,MAAI,CAAC,YAAa,OAAKG,aAAc,OAAO;;;CAI9C,MAAM,OAAe,QAA2B;EAC9C,MAAM,SAAS,MAAKJ,QAAS,IAAI,MAAM;AACvC,MAAI,WAAW,OAAW,QAAO;AACjC,MAAI,CAAC,OAAO,WAAW,OAAO,QAC5B,QAAO,WAAW,MAAM,OAAO;AAEjC,MAAI,OAAO,WAAW,aAAa,OAAO,WAAW,WAAW;AAC9D,UAAO,SAAS;AAChB,UAAO,cAAc,MAAKC,KAAM;AAChC,SAAKG,aAAc,OAAO;;AAE5B,SAAO;;;CAIT,SAAS,OAA6C;EACpD,MAAM,SAAS,MAAKJ,QAAS,IAAI,MAAM;AACvC,MAAI,WAAW,OAAW,QAAO;EACjC,MAAMK,OAA0E;GAC9E,OAAO,OAAO;GACd,MAAM,OAAO;GACb,QAAQ,OAAO;GAChB;AACD,MAAI,OAAO,cAAc,OAAW,MAAK,YAAY,OAAO;AAC5D,MAAI,OAAO,gBAAgB,OAAW,MAAK,cAAc,OAAO;AAChE,MAAI,OAAO,UAAU,OAAW,MAAK,QAAQ,OAAO;AACpD,MAAI,OAAO,WAAW,SAAS,QAC7B,MAAK,UAAU,OAAO,WAAW;OAC5B;AACL,QAAK,aAAa,OAAO,WAAW;AACpC,OAAI,OAAO,WAAW,aAAa,OAAW,MAAK,WAAW,OAAO,WAAW;;AAElF,MAAI,OAAO,WAAW,cAAc,OAAW,MAAK,YAAY,OAAO,WAAW;AAClF,MAAI,OAAO,WAAW,WAAW,OAAW,MAAK,SAAS,OAAO,WAAW;AAC5E,SAAO,OAAO,OAAO,KAAyB;;;;;;;;CAShD,gBAAwB;EACtB,IAAI,IAAI;AACR,OAAK,MAAM,UAAU,MAAKL,QAAS,QAAQ,CACzC,KAAI,OAAO,WAAW,aAAa,OAAO,WAAW,UAAW,MAAK;AAEvE,SAAO;;;;;;;;CAST,eAAuB;EACrB,IAAI,IAAI;AACR,OAAK,MAAM,UAAU,MAAKA,QAAS,QAAQ,CACzC,KAAI,OAAO,WAAW,UAAW,MAAK;AAExC,SAAO;;;;;;;CAQT,aAAa,QAA0B;EACrC,IAAI,UAAU;AACd,OAAK,MAAM,UAAU,MAAKA,QAAS,QAAQ,CACzC,KAAI,OAAO,WAAW,WAAW;AAC/B,OAAI,CAAC,OAAO,WAAW,OAAO,QAAS,QAAO,WAAW,MAAM,OAAO;AACtE,UAAO,SAAS;AAChB,UAAO,cAAc,MAAKC,KAAM;AAChC,SAAKG,aAAc,OAAO;AAC1B,cAAW;;AAGf,SAAO;;;CAIT,MAAM,WAA2B;EAC/B,IAAI,UAAU;AACd,OAAK,MAAM,CAAC,OAAO,WAAW,MAAKJ,QACjC,KACE,OAAO,gBAAgB,UACvB,OAAO,eAAe,aACtB,OAAO,WAAW,aAClB,OAAO,WAAW,WAClB;AACA,SAAKA,QAAS,OAAO,MAAM;AAC3B,cAAW;;AAGf,SAAO;;;CAIT,SAAS,QAA0B;EACjC,IAAI,UAAU;AACd,OAAK,MAAM,CAAC,UAAU,MAAKA,QACzB,KAAI,KAAK,MAAM,OAAO,OAAO,CAAE,YAAW;AAE5C,SAAO;;;;AAKX,MAAa,gCAAgC;;AAE7C,MAAa,2BAA2B,IAAI;;;;;;;;AAS5C,SAAgB,mBACd,MACA,KACA,OAAwE,EAAE,EAC9D;CACZ,MAAM,aAAa,KAAK,cAAc;CACtC,MAAM,cAAc,KAAK,eAAe;CACxC,MAAM,QAAQ,kBAAkB;AAC9B,OAAK,MAAM,KAAK,GAAG,YAAY;IAC9B,WAAW;AACd,KAAI,OAAQ,MAAiC,UAAU,WACrD,CAAC,MAAgC,OAAO;AAE1C,cAAa,cAAc,MAAM"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { DeliveryCommentaryConfig } from "../commentary/types.js";
|
|
2
|
+
import "../commentary/index.js";
|
|
3
|
+
import { ServerVariables } from "../internal/context.js";
|
|
4
|
+
import { WsDispatcher } from "../ws/dispatcher.js";
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
|
|
7
|
+
//#region src/sse/events.d.ts
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Stable shape consumed by {@link createSseRoutes}.
|
|
11
|
+
*
|
|
12
|
+
* @stable
|
|
13
|
+
*/
|
|
14
|
+
interface SseRoutesDeps {
|
|
15
|
+
/**
|
|
16
|
+
* Cap on the per-connection SSE delivery queue (IP-9). A consumer
|
|
17
|
+
* that stops reading past this many buffered frames is closed
|
|
18
|
+
* instead of growing the queue without bound. Default 1000.
|
|
19
|
+
*/
|
|
20
|
+
readonly perConnectionQueueLimit?: number;
|
|
21
|
+
readonly dispatcher: WsDispatcher;
|
|
22
|
+
readonly commentary?: DeliveryCommentaryConfig;
|
|
23
|
+
/**
|
|
24
|
+
* How long the SSE responder waits between heartbeat comments to
|
|
25
|
+
* keep proxies / load balancers from closing the idle connection.
|
|
26
|
+
* Default `15_000` ms.
|
|
27
|
+
*/
|
|
28
|
+
readonly keepAliveMs?: number;
|
|
29
|
+
readonly now?: () => number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build the SSE event-stream router. Mounts
|
|
33
|
+
* `GET /sessions/:id/events` (the canonical path documented in the
|
|
34
|
+
* runtime spec); operators that want a different path should mount
|
|
35
|
+
* the router under a custom prefix.
|
|
36
|
+
*
|
|
37
|
+
* @stable
|
|
38
|
+
*/
|
|
39
|
+
declare function createSseRoutes(deps: SseRoutesDeps): Hono<{
|
|
40
|
+
Variables: ServerVariables;
|
|
41
|
+
}>;
|
|
42
|
+
//#endregion
|
|
43
|
+
export { SseRoutesDeps, createSseRoutes };
|
|
44
|
+
//# sourceMappingURL=events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.d.ts","names":[],"sources":["../../src/sse/events.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;UA0CiB,aAAA;;;;;;;uBAOM;wBACC;;;;;;;;;;;;;;;;;iBAkBR,eAAA,OAAsB,gBAAgB;aAAkB"}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { createDeliveryCommentarySanitizer } from "../commentary/sanitizer.js";
|
|
2
|
+
import "../commentary/index.js";
|
|
3
|
+
import { createScopeMiddleware } from "../middleware/scope.js";
|
|
4
|
+
import { ServerMessageSchema, isEventFrame, isLifecycleFrame } from "@graphorin/protocol";
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
import { stream } from "hono/streaming";
|
|
7
|
+
|
|
8
|
+
//#region src/sse/events.ts
|
|
9
|
+
/**
|
|
10
|
+
* Server-Sent Events fallback for non-WebSocket-friendly clients.
|
|
11
|
+
*
|
|
12
|
+
* The endpoint is unidirectional: the server pushes
|
|
13
|
+
* `ServerMessage` event frames; control-plane operations
|
|
14
|
+
* (subscribe / unsubscribe / abort / resume) go through REST.
|
|
15
|
+
*
|
|
16
|
+
* Unlike the WS surface, the SSE responder is not multiplexed —
|
|
17
|
+
* one HTTP request per session subject. Reconnection is handled by
|
|
18
|
+
* the standard EventSource `Last-Event-ID` mechanism: when the
|
|
19
|
+
* client supplies the header on resume, the responder walks the
|
|
20
|
+
* dispatcher's replay buffer from that cursor.
|
|
21
|
+
*
|
|
22
|
+
* @packageDocumentation
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Build the SSE event-stream router. Mounts
|
|
26
|
+
* `GET /sessions/:id/events` (the canonical path documented in the
|
|
27
|
+
* runtime spec); operators that want a different path should mount
|
|
28
|
+
* the router under a custom prefix.
|
|
29
|
+
*
|
|
30
|
+
* @stable
|
|
31
|
+
*/
|
|
32
|
+
function createSseRoutes(deps) {
|
|
33
|
+
const sanitizer = deps.dispatcher.sanitizer.applyToEvents.length > 0 ? deps.dispatcher.sanitizer : createDeliveryCommentarySanitizer(deps.commentary);
|
|
34
|
+
const keepAliveMs = deps.keepAliveMs ?? 15e3;
|
|
35
|
+
const app = new Hono();
|
|
36
|
+
app.get("/:id/events", createScopeMiddleware("sessions:read"), sseHandler(deps.dispatcher, sanitizer, keepAliveMs, deps.perConnectionQueueLimit ?? 1e3));
|
|
37
|
+
return app;
|
|
38
|
+
}
|
|
39
|
+
function sseHandler(dispatcher, sanitizer, keepAliveMs, perConnectionQueueLimit) {
|
|
40
|
+
return async (c) => {
|
|
41
|
+
const sessionId = c.req.param("id");
|
|
42
|
+
if (sessionId === void 0 || sessionId.length === 0) return c.json({
|
|
43
|
+
error: "session-id-required",
|
|
44
|
+
message: "Path parameter :id is required."
|
|
45
|
+
}, 400);
|
|
46
|
+
const subject = `session:${sessionId}/events`;
|
|
47
|
+
const sinceEventId = c.req.header("last-event-id") ?? c.req.header("Last-Event-ID");
|
|
48
|
+
c.header("Content-Type", "text/event-stream; charset=utf-8");
|
|
49
|
+
c.header("Cache-Control", "no-cache, no-transform");
|
|
50
|
+
c.header("Connection", "keep-alive");
|
|
51
|
+
c.header("X-Accel-Buffering", "no");
|
|
52
|
+
return stream(c, async (writer) => {
|
|
53
|
+
const replay = dispatcher.replayBuffer.replay(subject, sinceEventId);
|
|
54
|
+
let lastEventId;
|
|
55
|
+
for (const frame of replay.events) {
|
|
56
|
+
const sanitized = sanitizer.sanitize({
|
|
57
|
+
...frame,
|
|
58
|
+
subscriptionId: "sse"
|
|
59
|
+
}, "sse");
|
|
60
|
+
await writeFrame(writer, sanitized);
|
|
61
|
+
lastEventId = sanitized.eventId;
|
|
62
|
+
}
|
|
63
|
+
if (replay.droppedCount > 0) await writeReplayMarker(writer, replay.droppedCount, lastEventId);
|
|
64
|
+
const subscriberId = `sse-${Math.random().toString(36).slice(2, 10)}`;
|
|
65
|
+
const subscriptionId = `${subscriberId}-sub`;
|
|
66
|
+
const authState = c.get("state").auth;
|
|
67
|
+
const grantedScopes = authState.kind === "token" || authState.kind === "anonymous" ? authState.grantedScopes : [];
|
|
68
|
+
const tokenId = authState.kind === "token" ? authState.token.tokenId : authState.kind === "anonymous" ? "anonymous" : "sse-anon";
|
|
69
|
+
let queue = [];
|
|
70
|
+
let closedByBackpressure = false;
|
|
71
|
+
const queueLimit = perConnectionQueueLimit;
|
|
72
|
+
let resumeNotify;
|
|
73
|
+
const notify = () => {
|
|
74
|
+
if (resumeNotify !== void 0) {
|
|
75
|
+
const fn = resumeNotify;
|
|
76
|
+
resumeNotify = void 0;
|
|
77
|
+
fn();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const handle = {
|
|
81
|
+
id: subscriberId,
|
|
82
|
+
tokenId,
|
|
83
|
+
grantedScopes,
|
|
84
|
+
send: (frame) => {
|
|
85
|
+
if (queue.length >= queueLimit) {
|
|
86
|
+
closedByBackpressure = true;
|
|
87
|
+
queue = [];
|
|
88
|
+
notify();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
queue.push(frame);
|
|
92
|
+
notify();
|
|
93
|
+
},
|
|
94
|
+
close: () => {
|
|
95
|
+
queue = [];
|
|
96
|
+
notify();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const reg = dispatcher.registerSubscriber(handle);
|
|
100
|
+
const result = dispatcher.subscribe({
|
|
101
|
+
subscriberId,
|
|
102
|
+
subject,
|
|
103
|
+
subscriptionId,
|
|
104
|
+
...lastEventId !== void 0 ? { sinceEventId: lastEventId } : {}
|
|
105
|
+
});
|
|
106
|
+
if (!result.ok) {
|
|
107
|
+
reg.unregister();
|
|
108
|
+
await writer.write(encodeSse({
|
|
109
|
+
event: "error",
|
|
110
|
+
data: JSON.stringify({
|
|
111
|
+
error: "sse-subscribe-failed",
|
|
112
|
+
reason: result.reason
|
|
113
|
+
})
|
|
114
|
+
}));
|
|
115
|
+
await writer.close();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const heartbeat = setInterval(() => {
|
|
119
|
+
writer.write(": keep-alive\n\n").catch(() => void 0);
|
|
120
|
+
}, keepAliveMs);
|
|
121
|
+
let aborted = false;
|
|
122
|
+
const onAbort = () => {
|
|
123
|
+
aborted = true;
|
|
124
|
+
notify();
|
|
125
|
+
};
|
|
126
|
+
c.req.raw.signal.addEventListener("abort", onAbort, { once: true });
|
|
127
|
+
try {
|
|
128
|
+
while (!aborted) {
|
|
129
|
+
if (closedByBackpressure) {
|
|
130
|
+
await writer.write(encodeSse({
|
|
131
|
+
event: "lifecycle",
|
|
132
|
+
data: JSON.stringify({
|
|
133
|
+
kind: "lifecycle",
|
|
134
|
+
subscriptionId,
|
|
135
|
+
status: "failed",
|
|
136
|
+
reason: "flow.throttled"
|
|
137
|
+
})
|
|
138
|
+
}));
|
|
139
|
+
aborted = true;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (queue.length > 0) {
|
|
143
|
+
const next = queue.shift();
|
|
144
|
+
if (next === void 0) continue;
|
|
145
|
+
if (isEventFrame(next)) await writeFrame(writer, next);
|
|
146
|
+
else if (isLifecycleFrame(next)) {
|
|
147
|
+
await writer.write(encodeSse({
|
|
148
|
+
event: "lifecycle",
|
|
149
|
+
data: JSON.stringify(next)
|
|
150
|
+
}));
|
|
151
|
+
if (next.status === "completed" || next.status === "aborted" || next.status === "failed") aborted = true;
|
|
152
|
+
}
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
await new Promise((resolve) => {
|
|
156
|
+
resumeNotify = resolve;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
} finally {
|
|
160
|
+
clearInterval(heartbeat);
|
|
161
|
+
c.req.raw.signal.removeEventListener("abort", onAbort);
|
|
162
|
+
dispatcher.unsubscribe(subscriptionId);
|
|
163
|
+
reg.unregister();
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
async function writeFrame(writer, frame) {
|
|
169
|
+
if (!ServerMessageSchema.safeParse(frame).success) return;
|
|
170
|
+
await writer.write(encodeSse({
|
|
171
|
+
event: frame.type,
|
|
172
|
+
id: frame.eventId,
|
|
173
|
+
data: JSON.stringify(frame)
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
async function writeReplayMarker(writer, droppedCount, lastEventId) {
|
|
177
|
+
await writer.write(encodeSse({
|
|
178
|
+
...lastEventId !== void 0 ? { id: lastEventId } : {},
|
|
179
|
+
event: "replay-marker",
|
|
180
|
+
data: JSON.stringify({
|
|
181
|
+
v: "1",
|
|
182
|
+
kind: "replay-marker",
|
|
183
|
+
subscriptionId: "sse",
|
|
184
|
+
eventId: lastEventId ?? `evt-replay-${Date.now().toString(36)}`,
|
|
185
|
+
droppedCount
|
|
186
|
+
})
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
function encodeSse(frame) {
|
|
190
|
+
let out = "";
|
|
191
|
+
if (frame.event !== void 0) out += `event: ${frame.event}\n`;
|
|
192
|
+
if (frame.id !== void 0) out += `id: ${frame.id}\n`;
|
|
193
|
+
for (const line of frame.data.split("\n")) out += `data: ${line}\n`;
|
|
194
|
+
out += "\n";
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
//#endregion
|
|
199
|
+
export { createSseRoutes };
|
|
200
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.js","names":["sanitizer: DeliveryCommentarySanitizer","lastEventId: string | undefined","queue: ServerMessage[]","resumeNotify: (() => void) | undefined"],"sources":["../../src/sse/events.ts"],"sourcesContent":["/**\n * Server-Sent Events fallback for non-WebSocket-friendly clients.\n *\n * The endpoint is unidirectional: the server pushes\n * `ServerMessage` event frames; control-plane operations\n * (subscribe / unsubscribe / abort / resume) go through REST.\n *\n * Unlike the WS surface, the SSE responder is not multiplexed —\n * one HTTP request per session subject. Reconnection is handled by\n * the standard EventSource `Last-Event-ID` mechanism: when the\n * client supplies the header on resume, the responder walks the\n * dispatcher's replay buffer from that cursor.\n *\n * @packageDocumentation\n */\n\nimport {\n isEventFrame,\n isLifecycleFrame,\n type ServerEventFrame,\n type ServerMessage,\n ServerMessageSchema,\n} from '@graphorin/protocol';\nimport type { Context, MiddlewareHandler } from 'hono';\nimport { Hono } from 'hono';\nimport { stream } from 'hono/streaming';\nimport type { StreamingApi } from 'hono/utils/stream';\n\nimport {\n createDeliveryCommentarySanitizer,\n type DeliveryCommentaryConfig,\n type DeliveryCommentarySanitizer,\n} from '../commentary/index.js';\nimport type { ServerVariables } from '../internal/context.js';\nimport { createScopeMiddleware } from '../middleware/scope.js';\nimport type { WsDispatcher } from '../ws/dispatcher.js';\n\n/**\n * Stable shape consumed by {@link createSseRoutes}.\n *\n * @stable\n */\nexport interface SseRoutesDeps {\n /**\n * Cap on the per-connection SSE delivery queue (IP-9). A consumer\n * that stops reading past this many buffered frames is closed\n * instead of growing the queue without bound. Default 1000.\n */\n readonly perConnectionQueueLimit?: number;\n readonly dispatcher: WsDispatcher;\n readonly commentary?: DeliveryCommentaryConfig;\n /**\n * How long the SSE responder waits between heartbeat comments to\n * keep proxies / load balancers from closing the idle connection.\n * Default `15_000` ms.\n */\n readonly keepAliveMs?: number;\n readonly now?: () => number;\n}\n\n/**\n * Build the SSE event-stream router. Mounts\n * `GET /sessions/:id/events` (the canonical path documented in the\n * runtime spec); operators that want a different path should mount\n * the router under a custom prefix.\n *\n * @stable\n */\nexport function createSseRoutes(deps: SseRoutesDeps): Hono<{ Variables: ServerVariables }> {\n const sanitizer: DeliveryCommentarySanitizer =\n deps.dispatcher.sanitizer.applyToEvents.length > 0\n ? deps.dispatcher.sanitizer\n : createDeliveryCommentarySanitizer(deps.commentary);\n const keepAliveMs = deps.keepAliveMs ?? 15_000;\n\n const app = new Hono<{ Variables: ServerVariables }>();\n app.get(\n '/:id/events',\n createScopeMiddleware('sessions:read'),\n sseHandler(deps.dispatcher, sanitizer, keepAliveMs, deps.perConnectionQueueLimit ?? 1000),\n );\n return app;\n}\n\nfunction sseHandler(\n dispatcher: WsDispatcher,\n sanitizer: DeliveryCommentarySanitizer,\n keepAliveMs: number,\n perConnectionQueueLimit: number,\n): MiddlewareHandler<{ Variables: ServerVariables }> {\n return async (c: Context<{ Variables: ServerVariables }>) => {\n const sessionId = c.req.param('id');\n if (sessionId === undefined || sessionId.length === 0) {\n return c.json(\n { error: 'session-id-required', message: 'Path parameter :id is required.' },\n 400,\n );\n }\n const subject = `session:${sessionId}/events`;\n const sinceEventId = c.req.header('last-event-id') ?? c.req.header('Last-Event-ID');\n\n c.header('Content-Type', 'text/event-stream; charset=utf-8');\n c.header('Cache-Control', 'no-cache, no-transform');\n c.header('Connection', 'keep-alive');\n c.header('X-Accel-Buffering', 'no');\n\n return stream(c, async (writer) => {\n const replay = dispatcher.replayBuffer.replay(subject, sinceEventId);\n let lastEventId: string | undefined;\n for (const frame of replay.events) {\n const sanitized = sanitizer.sanitize({ ...frame, subscriptionId: 'sse' }, 'sse');\n await writeFrame(writer as StreamingApi, sanitized);\n lastEventId = sanitized.eventId;\n }\n if (replay.droppedCount > 0) {\n await writeReplayMarker(writer as StreamingApi, replay.droppedCount, lastEventId);\n }\n // Live tail — register a synthetic subscription so the\n // dispatcher fans new events into this connection. The\n // dispatcher already validates + sanitizes every frame; here we\n // just write the bytes onto the wire.\n const subscriberId = `sse-${Math.random().toString(36).slice(2, 10)}`;\n const subscriptionId = `${subscriberId}-sub`;\n const authState = c.get('state').auth;\n // IP-13: the anonymous (auth.kind='none') principal carries `admin:*` in\n // grantedScopes, so the dispatcher's per-subject scope check still passes.\n const grantedScopes =\n authState.kind === 'token' || authState.kind === 'anonymous' ? authState.grantedScopes : [];\n const tokenId =\n authState.kind === 'token'\n ? authState.token.tokenId\n : authState.kind === 'anonymous'\n ? 'anonymous'\n : 'sse-anon';\n let queue: ServerMessage[] = [];\n let closedByBackpressure = false;\n const queueLimit = perConnectionQueueLimit;\n let resumeNotify: (() => void) | undefined;\n const notify = (): void => {\n if (resumeNotify !== undefined) {\n const fn = resumeNotify;\n resumeNotify = undefined;\n fn();\n }\n };\n const handle = {\n id: subscriberId,\n tokenId,\n // The strict-mode subscribe path enforces scope; we trust the\n // outer middleware (`createScopeMiddleware('sessions:read')`)\n // to gate this handler so we forward an empty scope set —\n // the dispatcher's per-subject scope check still requires\n // the `agents:invoke:<sessionId>` grant.\n grantedScopes,\n send: (frame: ServerMessage) => {\n // IP-9: bound the queue — a stalled consumer is closed, not\n // buffered into the heap forever.\n if (queue.length >= queueLimit) {\n closedByBackpressure = true;\n queue = [];\n notify();\n return;\n }\n queue.push(frame);\n notify();\n },\n close: () => {\n queue = [];\n notify();\n },\n };\n const reg = dispatcher.registerSubscriber(handle);\n const result = dispatcher.subscribe({\n subscriberId,\n subject,\n subscriptionId,\n ...(lastEventId !== undefined ? { sinceEventId: lastEventId } : {}),\n });\n if (!result.ok) {\n reg.unregister();\n await writer.write(\n encodeSse({\n event: 'error',\n data: JSON.stringify({\n error: 'sse-subscribe-failed',\n reason: result.reason,\n }),\n }),\n );\n await writer.close();\n return;\n }\n const heartbeat = setInterval(() => {\n // Fire-and-forget heartbeat comment — clients ignore comment\n // lines (RFC 6455 § 9.1).\n writer.write(': keep-alive\\n\\n').catch(() => undefined);\n }, keepAliveMs);\n let aborted = false;\n const onAbort = (): void => {\n aborted = true;\n notify();\n };\n c.req.raw.signal.addEventListener('abort', onAbort, { once: true });\n try {\n while (!aborted) {\n if (closedByBackpressure) {\n // IP-9: the consumer stalled past the queue cap — close the\n // stream with a terminal lifecycle frame instead of\n // buffering unboundedly.\n await writer.write(\n encodeSse({\n event: 'lifecycle',\n data: JSON.stringify({\n kind: 'lifecycle',\n subscriptionId,\n status: 'failed',\n reason: 'flow.throttled',\n }),\n }),\n );\n aborted = true;\n continue;\n }\n if (queue.length > 0) {\n const next = queue.shift();\n if (next === undefined) continue;\n if (isEventFrame(next)) {\n await writeFrame(writer as StreamingApi, next);\n } else if (isLifecycleFrame(next)) {\n await writer.write(\n encodeSse({\n event: 'lifecycle',\n data: JSON.stringify(next),\n }),\n );\n if (\n next.status === 'completed' ||\n next.status === 'aborted' ||\n next.status === 'failed'\n ) {\n aborted = true;\n }\n }\n continue;\n }\n await new Promise<void>((resolve) => {\n resumeNotify = resolve;\n });\n }\n } finally {\n clearInterval(heartbeat);\n c.req.raw.signal.removeEventListener('abort', onAbort);\n dispatcher.unsubscribe(subscriptionId);\n reg.unregister();\n }\n });\n };\n}\n\nasync function writeFrame(writer: StreamingApi, frame: ServerEventFrame): Promise<void> {\n const validated = ServerMessageSchema.safeParse(frame);\n if (!validated.success) return;\n await writer.write(\n encodeSse({\n event: frame.type,\n id: frame.eventId,\n data: JSON.stringify(frame),\n }),\n );\n}\n\nasync function writeReplayMarker(\n writer: StreamingApi,\n droppedCount: number,\n lastEventId: string | undefined,\n): Promise<void> {\n await writer.write(\n encodeSse({\n ...(lastEventId !== undefined ? { id: lastEventId } : {}),\n event: 'replay-marker',\n data: JSON.stringify({\n v: '1',\n kind: 'replay-marker',\n subscriptionId: 'sse',\n eventId: lastEventId ?? `evt-replay-${Date.now().toString(36)}`,\n droppedCount,\n }),\n }),\n );\n}\n\nfunction encodeSse(frame: { event?: string; id?: string; data: string }): string {\n let out = '';\n if (frame.event !== undefined) out += `event: ${frame.event}\\n`;\n if (frame.id !== undefined) out += `id: ${frame.id}\\n`;\n for (const line of frame.data.split('\\n')) {\n out += `data: ${line}\\n`;\n }\n out += '\\n';\n return out;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { CatchupPolicy, Scheduler } from "@graphorin/triggers";
|
|
2
|
+
|
|
3
|
+
//#region src/triggers/daemon.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Snapshot exposed via {@link TriggersDaemon.status} + the
|
|
7
|
+
* `/v1/health` aggregator. Counts split by `disabled` so the health
|
|
8
|
+
* endpoint can flag a deployment that disabled every cron trigger.
|
|
9
|
+
*
|
|
10
|
+
* @stable
|
|
11
|
+
*/
|
|
12
|
+
interface TriggersDaemonStatus {
|
|
13
|
+
readonly running: boolean;
|
|
14
|
+
readonly active: number;
|
|
15
|
+
readonly disabled: number;
|
|
16
|
+
readonly deferred: number;
|
|
17
|
+
/** Last fire timestamp across the entire pool (ISO-8601). */
|
|
18
|
+
readonly lastFireAt?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Aggregate counters surfaced through the Prometheus
|
|
22
|
+
* `/v1/metrics` exposition. Updated incrementally as the scheduler
|
|
23
|
+
* publishes lifecycle events.
|
|
24
|
+
*
|
|
25
|
+
* @stable
|
|
26
|
+
*/
|
|
27
|
+
interface TriggersDaemonMetrics {
|
|
28
|
+
readonly fires: ReadonlyMap<string, {
|
|
29
|
+
readonly success: number;
|
|
30
|
+
readonly error: number;
|
|
31
|
+
}>;
|
|
32
|
+
readonly catchupApplied: number;
|
|
33
|
+
readonly libModeWarnings: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* @stable
|
|
37
|
+
*/
|
|
38
|
+
interface CreateTriggersDaemonOptions {
|
|
39
|
+
readonly scheduler: Scheduler;
|
|
40
|
+
/**
|
|
41
|
+
* Optional logger. Defaults to the standard error stream so the
|
|
42
|
+
* daemon never depends on the framework logger directly.
|
|
43
|
+
*/
|
|
44
|
+
readonly warn?: (message: string) => void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Stateful handle returned by {@link createTriggersDaemon}. The
|
|
48
|
+
* `start()` / `stop()` methods are wired into
|
|
49
|
+
* `LifecycleHooks.beforeStart` / `beforeShutdown` by `createServer`.
|
|
50
|
+
*
|
|
51
|
+
* @stable
|
|
52
|
+
*/
|
|
53
|
+
interface TriggersDaemon {
|
|
54
|
+
start(): Promise<void>;
|
|
55
|
+
stop(): Promise<void>;
|
|
56
|
+
status(): Promise<TriggersDaemonStatus>;
|
|
57
|
+
metrics(): TriggersDaemonMetrics;
|
|
58
|
+
readonly scheduler: Scheduler;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* @stable
|
|
62
|
+
*/
|
|
63
|
+
declare function createTriggersDaemon(options: CreateTriggersDaemonOptions): TriggersDaemon;
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the catch-up policy default for triggers that did not
|
|
66
|
+
* declare one explicitly. Returns `'none'` per DEC-150 (personal-
|
|
67
|
+
* assistant friendly).
|
|
68
|
+
*
|
|
69
|
+
* @stable
|
|
70
|
+
*/
|
|
71
|
+
declare function defaultCatchupPolicy(): CatchupPolicy;
|
|
72
|
+
//#endregion
|
|
73
|
+
export { CreateTriggersDaemonOptions, TriggersDaemon, TriggersDaemonMetrics, TriggersDaemonStatus, createTriggersDaemon, defaultCatchupPolicy };
|
|
74
|
+
//# sourceMappingURL=daemon.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon.d.ts","names":[],"sources":["../../src/triggers/daemon.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;UAyBiB,oBAAA;;;;;;;;;;;;;;;UAgBA,qBAAA;kBACC;;;;;;;;;;UAQD,2BAAA;sBACK;;;;;;;;;;;;;;UAeL,cAAA;WACN;UACD;YACE,QAAQ;aACP;sBACS;;;;;iBAMN,oBAAA,UAA8B,8BAA8B;;;;;;;;iBAgH5D,oBAAA,CAAA,GAAwB"}
|