@jini-ai/http-kit 0.2.1
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/LICENSE +201 -0
- package/README.md +107 -0
- package/dist/active-context.d.ts +99 -0
- package/dist/active-context.d.ts.map +1 -0
- package/dist/active-context.js +77 -0
- package/dist/active-context.js.map +1 -0
- package/dist/adapter.d.ts +36 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +92 -0
- package/dist/adapter.js.map +1 -0
- package/dist/agents.d.ts +55 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +23 -0
- package/dist/agents.js.map +1 -0
- package/dist/api-security-middleware.d.ts +146 -0
- package/dist/api-security-middleware.d.ts.map +1 -0
- package/dist/api-security-middleware.js +276 -0
- package/dist/api-security-middleware.js.map +1 -0
- package/dist/attachments.d.ts +317 -0
- package/dist/attachments.d.ts.map +1 -0
- package/dist/attachments.js +664 -0
- package/dist/attachments.js.map +1 -0
- package/dist/cancel-owned-runs.d.ts +44 -0
- package/dist/cancel-owned-runs.d.ts.map +1 -0
- package/dist/cancel-owned-runs.js +35 -0
- package/dist/cancel-owned-runs.js.map +1 -0
- package/dist/compat.d.ts +22 -0
- package/dist/compat.d.ts.map +1 -0
- package/dist/compat.js +17 -0
- package/dist/compat.js.map +1 -0
- package/dist/component-catalog.d.ts +74 -0
- package/dist/component-catalog.d.ts.map +1 -0
- package/dist/component-catalog.js +74 -0
- package/dist/component-catalog.js.map +1 -0
- package/dist/connectors.d.ts +165 -0
- package/dist/connectors.d.ts.map +1 -0
- package/dist/connectors.js +474 -0
- package/dist/connectors.js.map +1 -0
- package/dist/daemon-status.d.ts +72 -0
- package/dist/daemon-status.d.ts.map +1 -0
- package/dist/daemon-status.js +60 -0
- package/dist/daemon-status.js.map +1 -0
- package/dist/db-ops.d.ts +116 -0
- package/dist/db-ops.d.ts.map +1 -0
- package/dist/db-ops.js +191 -0
- package/dist/db-ops.js.map +1 -0
- package/dist/delegated-tools.d.ts +46 -0
- package/dist/delegated-tools.d.ts.map +1 -0
- package/dist/delegated-tools.js +155 -0
- package/dist/delegated-tools.js.map +1 -0
- package/dist/express/run-stream.d.ts +15 -0
- package/dist/express/run-stream.d.ts.map +1 -0
- package/dist/express/run-stream.js +11 -0
- package/dist/express/run-stream.js.map +1 -0
- package/dist/express-index.d.ts +32 -0
- package/dist/express-index.d.ts.map +1 -0
- package/dist/express-index.js +14 -0
- package/dist/express-index.js.map +1 -0
- package/dist/fastify/adapter.d.ts +31 -0
- package/dist/fastify/adapter.d.ts.map +1 -0
- package/dist/fastify/adapter.js +64 -0
- package/dist/fastify/adapter.js.map +1 -0
- package/dist/fastify/agents.d.ts +13 -0
- package/dist/fastify/agents.d.ts.map +1 -0
- package/dist/fastify/agents.js +7 -0
- package/dist/fastify/agents.js.map +1 -0
- package/dist/fastify/api-security-middleware.d.ts +64 -0
- package/dist/fastify/api-security-middleware.d.ts.map +1 -0
- package/dist/fastify/api-security-middleware.js +139 -0
- package/dist/fastify/api-security-middleware.js.map +1 -0
- package/dist/fastify/compat.d.ts +22 -0
- package/dist/fastify/compat.d.ts.map +1 -0
- package/dist/fastify/compat.js +16 -0
- package/dist/fastify/compat.js.map +1 -0
- package/dist/fastify/daemon-status.d.ts +22 -0
- package/dist/fastify/daemon-status.d.ts.map +1 -0
- package/dist/fastify/daemon-status.js +9 -0
- package/dist/fastify/daemon-status.js.map +1 -0
- package/dist/fastify/host-tools.d.ts +13 -0
- package/dist/fastify/host-tools.d.ts.map +1 -0
- package/dist/fastify/host-tools.js +8 -0
- package/dist/fastify/host-tools.js.map +1 -0
- package/dist/fastify/index.d.ts +36 -0
- package/dist/fastify/index.d.ts.map +1 -0
- package/dist/fastify/index.js +18 -0
- package/dist/fastify/index.js.map +1 -0
- package/dist/fastify/local-daemon-request.d.ts +43 -0
- package/dist/fastify/local-daemon-request.d.ts.map +1 -0
- package/dist/fastify/local-daemon-request.js +155 -0
- package/dist/fastify/local-daemon-request.js.map +1 -0
- package/dist/fastify/origin.d.ts +21 -0
- package/dist/fastify/origin.d.ts.map +1 -0
- package/dist/fastify/origin.js +14 -0
- package/dist/fastify/origin.js.map +1 -0
- package/dist/fastify/request.d.ts +20 -0
- package/dist/fastify/request.d.ts.map +1 -0
- package/dist/fastify/request.js +25 -0
- package/dist/fastify/request.js.map +1 -0
- package/dist/fastify/response.d.ts +20 -0
- package/dist/fastify/response.d.ts.map +1 -0
- package/dist/fastify/response.js +41 -0
- package/dist/fastify/response.js.map +1 -0
- package/dist/fastify/route-registration-guard.d.ts +70 -0
- package/dist/fastify/route-registration-guard.d.ts.map +1 -0
- package/dist/fastify/route-registration-guard.js +69 -0
- package/dist/fastify/route-registration-guard.js.map +1 -0
- package/dist/fastify/run-stream.d.ts +18 -0
- package/dist/fastify/run-stream.d.ts.map +1 -0
- package/dist/fastify/run-stream.js +10 -0
- package/dist/fastify/run-stream.js.map +1 -0
- package/dist/fastify/runs.d.ts +17 -0
- package/dist/fastify/runs.d.ts.map +1 -0
- package/dist/fastify/runs.js +33 -0
- package/dist/fastify/runs.js.map +1 -0
- package/dist/frontend-control.d.ts +131 -0
- package/dist/frontend-control.d.ts.map +1 -0
- package/dist/frontend-control.js +77 -0
- package/dist/frontend-control.js.map +1 -0
- package/dist/frontend-sessions.d.ts +100 -0
- package/dist/frontend-sessions.d.ts.map +1 -0
- package/dist/frontend-sessions.js +180 -0
- package/dist/frontend-sessions.js.map +1 -0
- package/dist/health.d.ts +91 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +53 -0
- package/dist/health.js.map +1 -0
- package/dist/host-tools.d.ts +174 -0
- package/dist/host-tools.d.ts.map +1 -0
- package/dist/host-tools.js +338 -0
- package/dist/host-tools.js.map +1 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/local-daemon-request.d.ts +44 -0
- package/dist/local-daemon-request.d.ts.map +1 -0
- package/dist/local-daemon-request.js +145 -0
- package/dist/local-daemon-request.js.map +1 -0
- package/dist/media.d.ts +131 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +352 -0
- package/dist/media.js.map +1 -0
- package/dist/memory.d.ts +230 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +331 -0
- package/dist/memory.js.map +1 -0
- package/dist/model-proxy.d.ts +45 -0
- package/dist/model-proxy.d.ts.map +1 -0
- package/dist/model-proxy.js +619 -0
- package/dist/model-proxy.js.map +1 -0
- package/dist/origin-validation.d.ts +64 -0
- package/dist/origin-validation.d.ts.map +1 -0
- package/dist/origin-validation.js +226 -0
- package/dist/origin-validation.js.map +1 -0
- package/dist/origin.d.ts +32 -0
- package/dist/origin.d.ts.map +1 -0
- package/dist/origin.js +14 -0
- package/dist/origin.js.map +1 -0
- package/dist/pack-http.d.ts +30 -0
- package/dist/pack-http.d.ts.map +1 -0
- package/dist/pack-http.js +12 -0
- package/dist/pack-http.js.map +1 -0
- package/dist/raw-sse.d.ts +44 -0
- package/dist/raw-sse.d.ts.map +1 -0
- package/dist/raw-sse.js +93 -0
- package/dist/raw-sse.js.map +1 -0
- package/dist/remote-run-events.d.ts +66 -0
- package/dist/remote-run-events.d.ts.map +1 -0
- package/dist/remote-run-events.js +230 -0
- package/dist/remote-run-events.js.map +1 -0
- package/dist/request.d.ts +19 -0
- package/dist/request.d.ts.map +1 -0
- package/dist/request.js +25 -0
- package/dist/request.js.map +1 -0
- package/dist/research.d.ts +42 -0
- package/dist/research.d.ts.map +1 -0
- package/dist/research.js +316 -0
- package/dist/research.js.map +1 -0
- package/dist/response.d.ts +14 -0
- package/dist/response.d.ts.map +1 -0
- package/dist/response.js +45 -0
- package/dist/response.js.map +1 -0
- package/dist/route-manifest.d.ts +31 -0
- package/dist/route-manifest.d.ts.map +1 -0
- package/dist/route-manifest.js +103 -0
- package/dist/route-manifest.js.map +1 -0
- package/dist/route-registration-guard.d.ts +65 -0
- package/dist/route-registration-guard.d.ts.map +1 -0
- package/dist/route-registration-guard.js +71 -0
- package/dist/route-registration-guard.js.map +1 -0
- package/dist/routines.d.ts +114 -0
- package/dist/routines.d.ts.map +1 -0
- package/dist/routines.js +320 -0
- package/dist/routines.js.map +1 -0
- package/dist/run-stream.d.ts +60 -0
- package/dist/run-stream.d.ts.map +1 -0
- package/dist/run-stream.js +108 -0
- package/dist/run-stream.js.map +1 -0
- package/dist/runs.d.ts +79 -0
- package/dist/runs.d.ts.map +1 -0
- package/dist/runs.js +232 -0
- package/dist/runs.js.map +1 -0
- package/dist/sse.d.ts +139 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +157 -0
- package/dist/sse.js.map +1 -0
- package/dist/terminals.d.ts +125 -0
- package/dist/terminals.d.ts.map +1 -0
- package/dist/terminals.js +349 -0
- package/dist/terminals.js.map +1 -0
- package/dist/tool-catalog.d.ts +68 -0
- package/dist/tool-catalog.d.ts.map +1 -0
- package/dist/tool-catalog.js +68 -0
- package/dist/tool-catalog.js.map +1 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/workspace-root.d.ts +69 -0
- package/dist/workspace-root.d.ts.map +1 -0
- package/dist/workspace-root.js +63 -0
- package/dist/workspace-root.js.map +1 -0
- package/dist/xai.d.ts +86 -0
- package/dist/xai.d.ts.map +1 -0
- package/dist/xai.js +668 -0
- package/dist/xai.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createSseResponse } from './raw-sse.js';
|
|
2
|
+
/** Backward-compatible route path used by the AG-UI composition. The handler itself is encoder-agnostic. */
|
|
3
|
+
export const RUN_STREAM_ROUTE_PATH = '/api/runs/:runId/agui-stream';
|
|
4
|
+
function reportRunStreamInternalError(deps, source, runId, error) {
|
|
5
|
+
const context = { source, runId, error };
|
|
6
|
+
try {
|
|
7
|
+
if (deps.onInternalError) {
|
|
8
|
+
deps.onInternalError(context);
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
// eslint-disable-next-line no-console
|
|
12
|
+
console.error(`[@jini-ai/http-kit] internal error (run-stream:${source}, runId=${runId})`, error);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
catch (sinkError) {
|
|
16
|
+
// A diagnostic sink must never turn an already-contained stream failure into an unhandled
|
|
17
|
+
// rejection of its own.
|
|
18
|
+
// eslint-disable-next-line no-console
|
|
19
|
+
console.error(`[@jini-ai/http-kit] internal error sink failed (run-stream:${source}, runId=${runId})`, sinkError);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Handles one encoded SSE run-stream request end to end. Opens the SSE connection immediately (an
|
|
24
|
+
* SSE endpoint commits to `text/event-stream` headers before it can know whether `runId` is
|
|
25
|
+
* valid), then subscribes to the run: a replay-then-live-subscribe `unknown-run`/`replay-gap`/
|
|
26
|
+
* `invalid-cursor` result is reported as one `{ error }` SSE data event before closing (there is
|
|
27
|
+
* no JSON-status-code channel left once SSE headers are already committed). On the happy path,
|
|
28
|
+
* every event `RunLifecycle` delivers is encoded and forwarded; the connection closes itself once
|
|
29
|
+
* the run's own terminal `'end'` event has been forwarded (a driver's contract with
|
|
30
|
+
* `RunLifecycle` guarantees no further events follow a run's `'end')`, so nothing is lost by
|
|
31
|
+
* closing right after it). If the client disconnects first, `createSseResponse`'s own
|
|
32
|
+
* `req.on('close', ...)` fires first, which (via `onClose` here) unsubscribes from the run so a
|
|
33
|
+
* disconnected client never leaves a dangling `RunLifecycle` subscriber.
|
|
34
|
+
*
|
|
35
|
+
* @param req - The raw request `createSseResponse` opens the stream against.
|
|
36
|
+
* @param res - The raw response `createSseResponse` opens the stream against.
|
|
37
|
+
* @param runId - The run to stream, already resolved by the caller's transport-specific glue.
|
|
38
|
+
* @param deps.lifecycle - The `RunLifecycle` to subscribe to.
|
|
39
|
+
* @param deps.encoder - The protocol adapter chosen by the composition root.
|
|
40
|
+
* @complexity O(1) plus the supplied encoder's per-event cost and `RunLifecycle.stream`'s replay cost.
|
|
41
|
+
* @overallScore 100/100
|
|
42
|
+
*/
|
|
43
|
+
export async function handleRunStreamRequest(req, res, runId, deps) {
|
|
44
|
+
let unsubscribe;
|
|
45
|
+
const connection = createSseResponse(req, res, {
|
|
46
|
+
onClose: () => unsubscribe?.(),
|
|
47
|
+
});
|
|
48
|
+
const encoder = deps.encoder;
|
|
49
|
+
let result;
|
|
50
|
+
try {
|
|
51
|
+
result = await deps.lifecycle.stream(runId, (event) => {
|
|
52
|
+
if (connection.closed)
|
|
53
|
+
return;
|
|
54
|
+
try {
|
|
55
|
+
const encoded = encoder.encode(event, { runId });
|
|
56
|
+
if (encoded != null)
|
|
57
|
+
connection.send(encoded);
|
|
58
|
+
if (event.kind === 'end')
|
|
59
|
+
connection.close();
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
reportRunStreamInternalError(deps, 'encoder', runId, error);
|
|
63
|
+
connection.close();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
reportRunStreamInternalError(deps, 'lifecycle', runId, error);
|
|
69
|
+
connection.close();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (result.kind !== 'ok') {
|
|
73
|
+
connection.send({ error: result.kind });
|
|
74
|
+
connection.close();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// Assigned even when the run was already terminal (in which case the driver-facing 'end' event
|
|
78
|
+
// was already replayed synchronously above, already closing the connection, and `unsubscribe`
|
|
79
|
+
// here is `RunLifecycle`'s own no-op stub for that case) — keeping this assignment unconditional
|
|
80
|
+
// avoids a branch whose two arms would otherwise do the same thing.
|
|
81
|
+
unsubscribe = result.unsubscribe;
|
|
82
|
+
// The raw request can close while RunLifecycle is still awaiting durable replay. In that
|
|
83
|
+
// window `onClose` runs before `unsubscribe` exists, so clean up immediately once it arrives.
|
|
84
|
+
if (connection.closed)
|
|
85
|
+
unsubscribe();
|
|
86
|
+
}
|
|
87
|
+
/** Mounts the encoder-driven SSE run-stream route on `app`. */
|
|
88
|
+
export function registerRunStreamRoute(app, deps) {
|
|
89
|
+
app.get(RUN_STREAM_ROUTE_PATH, async (req, res) => {
|
|
90
|
+
// `:runId` is a required path segment of RUN_STREAM_ROUTE_PATH — this handler is only ever
|
|
91
|
+
// reached via a URL that already matched it, so the param is always present at runtime even
|
|
92
|
+
// though @types/express types every param as possibly `undefined` in general.
|
|
93
|
+
const runId = req.params.runId;
|
|
94
|
+
try {
|
|
95
|
+
await handleRunStreamRequest(req, res, runId, deps);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
reportRunStreamInternalError(deps, 'route', runId, error);
|
|
99
|
+
if (!res.headersSent) {
|
|
100
|
+
res.status(500).json({ error: { code: 'INTERNAL_ERROR', message: 'an internal error occurred' } });
|
|
101
|
+
}
|
|
102
|
+
else if (!res.writableEnded) {
|
|
103
|
+
res.end();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=run-stream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-stream.js","sourceRoot":"","sources":["../src/run-stream.ts"],"names":[],"mappings":"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"}
|
package/dist/runs.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Express } from 'express';
|
|
2
|
+
import { type RunStatus } from '@jini-ai/protocol';
|
|
3
|
+
import type { RunLifecycle } from '@jini-ai/daemon';
|
|
4
|
+
import { type AdapterContext } from './adapter.js';
|
|
5
|
+
import type { ServerResponse } from 'node:http';
|
|
6
|
+
/**
|
|
7
|
+
* Diagnostic detail for an internal-error response the public API deliberately does not
|
|
8
|
+
* disclose (SEC-005): spawn/storage/adapter failures can embed executable paths, working
|
|
9
|
+
* directories, hostnames, or third-party provider text. The correlation id is the only thing
|
|
10
|
+
* that crosses the boundary; the real `error` goes only to this host-owned sink so an operator
|
|
11
|
+
* can still find and act on it.
|
|
12
|
+
*/
|
|
13
|
+
export interface RunInternalErrorContext {
|
|
14
|
+
readonly source: 'run-start' | 'run-stream';
|
|
15
|
+
readonly runId: string;
|
|
16
|
+
readonly correlationId: string;
|
|
17
|
+
readonly error: unknown;
|
|
18
|
+
}
|
|
19
|
+
export interface RunCreateRequest {
|
|
20
|
+
readonly contextRef: string;
|
|
21
|
+
readonly agentId?: string;
|
|
22
|
+
readonly idempotencyKey?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface RunStartContext {
|
|
25
|
+
readonly request: RunCreateRequest;
|
|
26
|
+
readonly run: RunStatus;
|
|
27
|
+
/** The lifecycle this driver must use for emitted events, cancellation observation, and terminal completion. */
|
|
28
|
+
readonly lifecycle: RunLifecycle;
|
|
29
|
+
}
|
|
30
|
+
/** Host-owned execution hook. It attaches a driver only after a durable run has been created. */
|
|
31
|
+
export type RunStartHandler = (context: RunStartContext) => Promise<void> | void;
|
|
32
|
+
export interface RunHttpDeps {
|
|
33
|
+
readonly lifecycle: RunLifecycle;
|
|
34
|
+
readonly onStarted?: RunStartHandler;
|
|
35
|
+
/** Host-owned sink for the real exception behind a generic `INTERNAL_ERROR` response (SEC-005). Defaults to `console.error`. */
|
|
36
|
+
readonly onInternalError?: (context: RunInternalErrorContext) => void;
|
|
37
|
+
}
|
|
38
|
+
export interface RunStartResponse {
|
|
39
|
+
readonly run: RunStatus;
|
|
40
|
+
readonly started: boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface RunStatusResponse {
|
|
43
|
+
readonly run: RunStatus;
|
|
44
|
+
}
|
|
45
|
+
export interface RunCancelResponse {
|
|
46
|
+
readonly run: RunStatus;
|
|
47
|
+
}
|
|
48
|
+
export interface RunListResponse {
|
|
49
|
+
readonly runs: readonly RunStatus[];
|
|
50
|
+
}
|
|
51
|
+
export declare const runStartRoute: import("./types.js").JsonRouteSpec<RunCreateRequest, RunStartResponse, RunHttpDeps>;
|
|
52
|
+
/** `GET /api/runs` — lists runs, optionally scoped to a `contextRef` query parameter. */
|
|
53
|
+
export declare const runListRoute: import("./types.js").JsonRouteSpec<{
|
|
54
|
+
contextRef?: string;
|
|
55
|
+
}, RunListResponse, RunHttpDeps>;
|
|
56
|
+
export declare const runStatusRoute: import("./types.js").JsonRouteSpec<string, RunStatusResponse, RunHttpDeps>;
|
|
57
|
+
export declare const runCancelRoute: import("./types.js").JsonRouteSpec<{
|
|
58
|
+
runId: string;
|
|
59
|
+
reason?: string;
|
|
60
|
+
}, RunCancelResponse, RunHttpDeps>;
|
|
61
|
+
/**
|
|
62
|
+
* The core of `GET /api/runs/:runId/events` — canonical events as SSE, with Last-Event-ID
|
|
63
|
+
* reconnect support. Takes `runId`/`afterCursor` already resolved by the caller. `res` is typed
|
|
64
|
+
* against the raw `node:http` `ServerResponse` rather than Express's own `Response` type — Express's
|
|
65
|
+
* `Response` extends it directly, so this is a no-op widening, the same pattern `run-stream.ts`'s
|
|
66
|
+
* AG-UI handler already established.
|
|
67
|
+
*/
|
|
68
|
+
export declare function handleRunEventStreamRequest(res: ServerResponse, runId: string, afterCursor: string | null, deps: RunHttpDeps): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Path {@link registerRunEventStream} mounts. Exported as a constant so a caller that needs to know
|
|
71
|
+
* this route exists — a reverse proxy's forward list, `route-manifest.ts` — references it rather than
|
|
72
|
+
* restating the literal and later drifting from it.
|
|
73
|
+
*/
|
|
74
|
+
export declare const RUN_EVENTS_ROUTE_PATH = "/api/runs/:runId/events";
|
|
75
|
+
/** Express mounting glue for {@link handleRunEventStreamRequest} — resolves `runId`/`afterCursor` from an Express `Request` and hands the request straight through. */
|
|
76
|
+
export declare function registerRunEventStream(app: Express, deps: RunHttpDeps): void;
|
|
77
|
+
/** Mounts create/status/cancel JSON endpoints and the SSE event stream as one run transport. */
|
|
78
|
+
export declare function registerRunRoutes(app: Express, deps: RunHttpDeps, adapter: AdapterContext): void;
|
|
79
|
+
//# sourceMappingURL=runs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runs.d.ts","sourceRoot":"","sources":["../src/runs.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AAC1D,OAAO,EAAyC,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC1F,OAAO,KAAK,EAAE,YAAY,EAA8B,MAAM,iBAAiB,CAAC;AAChF,OAAO,EAAmC,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAIpF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAGhD;;;;;;GAMG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,YAAY,CAAC;IAC5C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAQD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAC;IACnC,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAC;IACxB,gHAAgH;IAChH,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAC;CAClC;AAED,iGAAiG;AACjG,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAEjF,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAC;IACjC,QAAQ,CAAC,SAAS,CAAC,EAAE,eAAe,CAAC;IACrC,gIAAgI;IAChI,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,IAAI,CAAC;CACvE;AAqBD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAC;CACzB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAC;CACzB;AA8BD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,SAAS,SAAS,EAAE,CAAC;CACrC;AA4BD,eAAO,MAAM,aAAa,qFAuBxB,CAAC;AAEH,yFAAyF;AACzF,eAAO,MAAM,YAAY;iBAAkC,MAAM;gCAK/D,CAAC;AAEH,eAAO,MAAM,cAAc,4EAQzB,CAAC;AAEH,eAAO,MAAM,cAAc;WAA4B,MAAM;aAAW,MAAM;kCAU5E,CAAC;AAoBH;;;;;;GAMG;AACH,wBAAsB,2BAA2B,CAC/C,GAAG,EAAE,cAAc,EACnB,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAAG,IAAI,EAC1B,IAAI,EAAE,WAAW,GAChB,OAAO,CAAC,IAAI,CAAC,CAiDf;AAED;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,4BAA4B,CAAC;AAE/D,uKAAuK;AACvK,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAS5E;AAED,gGAAgG;AAChG,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAMhG"}
|
package/dist/runs.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic HTTP/SSE projection of the kernel RunLifecycle. The routes own no
|
|
3
|
+
* agent, tool, or product vocabulary: a host optionally supplies `onStarted`
|
|
4
|
+
* to attach its chosen driver after the lifecycle has durably recorded start.
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { createApiError } from '@jini-ai/protocol';
|
|
8
|
+
import { defineJsonRoute, mountJsonRoute } from './adapter.js';
|
|
9
|
+
import { validationError } from './request.js';
|
|
10
|
+
import { sendApiError } from './response.js';
|
|
11
|
+
import { createSseChannel, requestedAfterCursor, sendRawApiError } from './sse.js';
|
|
12
|
+
import { err, ok } from './types.js';
|
|
13
|
+
/** Default sink when a host does not supply `onInternalError`: still observable, never silent. */
|
|
14
|
+
function defaultInternalErrorSink(context) {
|
|
15
|
+
// eslint-disable-next-line no-console
|
|
16
|
+
console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, context.error);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Logs the real failure server-side and returns the generic, correlation-id-bearing public error
|
|
20
|
+
* (SEC-005: never the raw exception). `runId` is mandatory — both real call sites (`run-start`,
|
|
21
|
+
* after a durably-created run's id is already known; `run-stream`, against an already-parsed path
|
|
22
|
+
* parameter) always have one in hand, so an optional field with an unreachable "no runId" branch
|
|
23
|
+
* was speculative flexibility no caller ever exercised.
|
|
24
|
+
*/
|
|
25
|
+
function reportInternalError(deps, source, error, runId) {
|
|
26
|
+
const correlationId = randomUUID();
|
|
27
|
+
const sink = deps.onInternalError ?? defaultInternalErrorSink;
|
|
28
|
+
sink({ source, runId, correlationId, error });
|
|
29
|
+
return createApiError('INTERNAL_ERROR', 'an internal error occurred', { requestId: correlationId });
|
|
30
|
+
}
|
|
31
|
+
function isRecord(value) {
|
|
32
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
function optionalString(body, key) {
|
|
35
|
+
const value = body[key];
|
|
36
|
+
if (value === undefined)
|
|
37
|
+
return undefined;
|
|
38
|
+
return typeof value === 'string' && value.trim().length > 0 ? value : null;
|
|
39
|
+
}
|
|
40
|
+
function parseRunCreate(input) {
|
|
41
|
+
if (!isRecord(input.body))
|
|
42
|
+
return err(validationError('body must be a JSON object'));
|
|
43
|
+
const contextRef = optionalString(input.body, 'contextRef');
|
|
44
|
+
if (contextRef === undefined || contextRef === null) {
|
|
45
|
+
return err(validationError('contextRef must be a non-empty string', [{ path: 'contextRef', message: 'required non-empty string' }]));
|
|
46
|
+
}
|
|
47
|
+
const agentId = optionalString(input.body, 'agentId');
|
|
48
|
+
const idempotencyKey = optionalString(input.body, 'idempotencyKey');
|
|
49
|
+
if (agentId === null || idempotencyKey === null) {
|
|
50
|
+
return err(validationError('optional string fields must be non-empty when provided'));
|
|
51
|
+
}
|
|
52
|
+
return ok({
|
|
53
|
+
contextRef,
|
|
54
|
+
...(agentId === undefined ? {} : { agentId }),
|
|
55
|
+
...(idempotencyKey === undefined ? {} : { idempotencyKey }),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function parseRunList(input) {
|
|
59
|
+
const value = input.query.contextRef;
|
|
60
|
+
if (value === undefined)
|
|
61
|
+
return ok({});
|
|
62
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
63
|
+
return err(validationError('contextRef must be a non-empty string when provided', [{ path: 'contextRef', message: 'non-empty string when provided' }]));
|
|
64
|
+
}
|
|
65
|
+
return ok({ contextRef: value });
|
|
66
|
+
}
|
|
67
|
+
function parseRunId(input) {
|
|
68
|
+
const runId = input.params.runId;
|
|
69
|
+
return typeof runId === 'string' && runId.length > 0
|
|
70
|
+
? ok(runId)
|
|
71
|
+
: err(validationError('runId must be a non-empty path parameter'));
|
|
72
|
+
}
|
|
73
|
+
function parseRunCancel(input) {
|
|
74
|
+
const parsedRunId = parseRunId(input);
|
|
75
|
+
if (!parsedRunId.ok)
|
|
76
|
+
return parsedRunId;
|
|
77
|
+
if (input.body === undefined || input.body === null)
|
|
78
|
+
return ok({ runId: parsedRunId.value });
|
|
79
|
+
if (!isRecord(input.body))
|
|
80
|
+
return err(validationError('body must be a JSON object when provided'));
|
|
81
|
+
const reason = optionalString(input.body, 'reason');
|
|
82
|
+
if (reason === null)
|
|
83
|
+
return err(validationError('reason must be a non-empty string when provided'));
|
|
84
|
+
return ok({ runId: parsedRunId.value, ...(reason === undefined ? {} : { reason }) });
|
|
85
|
+
}
|
|
86
|
+
export const runStartRoute = defineJsonRoute({
|
|
87
|
+
method: 'post',
|
|
88
|
+
path: '/api/runs',
|
|
89
|
+
requireSameOrigin: true,
|
|
90
|
+
parse: parseRunCreate,
|
|
91
|
+
handle: async (input, deps) => {
|
|
92
|
+
const startInput = input;
|
|
93
|
+
const started = await deps.lifecycle.start(startInput);
|
|
94
|
+
if (started.started && deps.onStarted) {
|
|
95
|
+
try {
|
|
96
|
+
await deps.onStarted({ request: input, run: started.run, lifecycle: deps.lifecycle });
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
await deps.lifecycle.finish({ runId: started.run.id, status: 'failed', code: null, signal: null, resumable: false });
|
|
100
|
+
return err(reportInternalError(deps, 'run-start', error, started.run.id));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// A host-owned driver may finish immediately (for example, a single-step
|
|
104
|
+
// run). Return the lifecycle's current view rather than the snapshot
|
|
105
|
+
// captured before invoking that driver.
|
|
106
|
+
const run = (await deps.lifecycle.get(started.run.id)) ?? started.run;
|
|
107
|
+
return ok({ run, started: started.started });
|
|
108
|
+
},
|
|
109
|
+
successStatus: 201,
|
|
110
|
+
});
|
|
111
|
+
/** `GET /api/runs` — lists runs, optionally scoped to a `contextRef` query parameter. */
|
|
112
|
+
export const runListRoute = defineJsonRoute({
|
|
113
|
+
method: 'get',
|
|
114
|
+
path: '/api/runs',
|
|
115
|
+
parse: parseRunList,
|
|
116
|
+
handle: async (input, deps) => ok({ runs: await deps.lifecycle.list(input.contextRef) }),
|
|
117
|
+
});
|
|
118
|
+
export const runStatusRoute = defineJsonRoute({
|
|
119
|
+
method: 'get',
|
|
120
|
+
path: '/api/runs/:runId',
|
|
121
|
+
parse: parseRunId,
|
|
122
|
+
handle: async (runId, deps) => {
|
|
123
|
+
const run = await deps.lifecycle.get(runId);
|
|
124
|
+
return run === undefined ? err(createApiError('NOT_FOUND', `run "${runId}" was not found`)) : ok({ run });
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
export const runCancelRoute = defineJsonRoute({
|
|
128
|
+
method: 'post',
|
|
129
|
+
path: '/api/runs/:runId/cancel',
|
|
130
|
+
requireSameOrigin: true,
|
|
131
|
+
parse: parseRunCancel,
|
|
132
|
+
handle: async (input, deps) => {
|
|
133
|
+
const run = await deps.lifecycle.get(input.runId);
|
|
134
|
+
if (run === undefined)
|
|
135
|
+
return err(createApiError('NOT_FOUND', `run "${input.runId}" was not found`));
|
|
136
|
+
return ok({ run: await deps.lifecycle.cancel(input) });
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
function sendStreamFailure(res, kind) {
|
|
140
|
+
if (kind.kind === 'unknown-run') {
|
|
141
|
+
sendRawApiError(res, 404, createApiError('NOT_FOUND', 'run was not found'));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (kind.kind === 'invalid-cursor') {
|
|
145
|
+
sendRawApiError(res, 400, createApiError('BAD_REQUEST', `invalid replay cursor "${kind.requestedCursor}"`));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
sendRawApiError(res, 409, createApiError('CONFLICT', `replay gap after cursor "${kind.requestedCursor}"`, {
|
|
149
|
+
details: { oldestAvailableCursor: kind.oldestAvailableCursor },
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The core of `GET /api/runs/:runId/events` — canonical events as SSE, with Last-Event-ID
|
|
154
|
+
* reconnect support. Takes `runId`/`afterCursor` already resolved by the caller. `res` is typed
|
|
155
|
+
* against the raw `node:http` `ServerResponse` rather than Express's own `Response` type — Express's
|
|
156
|
+
* `Response` extends it directly, so this is a no-op widening, the same pattern `run-stream.ts`'s
|
|
157
|
+
* AG-UI handler already established.
|
|
158
|
+
*/
|
|
159
|
+
export async function handleRunEventStreamRequest(res, runId, afterCursor, deps) {
|
|
160
|
+
if (runId.length === 0) {
|
|
161
|
+
sendRawApiError(res, 400, createApiError('BAD_REQUEST', 'runId must be a non-empty path parameter'));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// `createSseChannel` (`sse.ts`) owns the bounded queue, backpressure, and client-disconnect
|
|
165
|
+
// handling that used to be inlined here — see that module's doc for the generalization.
|
|
166
|
+
const channel = createSseChannel(res, { isEndEvent: (event) => event.kind === 'end' });
|
|
167
|
+
let unsubscribeFn = null;
|
|
168
|
+
channel.onClose(() => {
|
|
169
|
+
const stop = unsubscribeFn;
|
|
170
|
+
unsubscribeFn = null;
|
|
171
|
+
stop?.();
|
|
172
|
+
});
|
|
173
|
+
try {
|
|
174
|
+
const subscribed = await deps.lifecycle.stream(runId, channel.enqueue, { afterCursor });
|
|
175
|
+
if (subscribed.kind !== 'ok') {
|
|
176
|
+
// Nothing was ever subscribed, so `abandon()` has nothing to unsubscribe — it only marks
|
|
177
|
+
// the channel closed. `res` itself is untouched, leaving `sendStreamFailure` free to send
|
|
178
|
+
// a normal JSON error response instead of an SSE stream.
|
|
179
|
+
channel.abandon();
|
|
180
|
+
sendStreamFailure(res, subscribed);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (channel.isClosed()) {
|
|
184
|
+
// The client already disconnected (or the bounded queue already gave up) while
|
|
185
|
+
// `stream()` was resolving — unsubscribe immediately instead of leaking it.
|
|
186
|
+
subscribed.unsubscribe();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
unsubscribeFn = subscribed.unsubscribe;
|
|
190
|
+
channel.open();
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
if (!res.headersSent) {
|
|
194
|
+
// `channel.open()` never ran (or never got past `flushHeaders`) — abandon without
|
|
195
|
+
// touching `res`, so the JSON error response below is the only thing written.
|
|
196
|
+
// Using `channel.end()` here instead would end the response before this write, turning
|
|
197
|
+
// it into a write-after-end failure.
|
|
198
|
+
channel.abandon();
|
|
199
|
+
sendRawApiError(res, 500, reportInternalError(deps, 'run-stream', error, runId));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
// Headers were already sent (the stream had started, or `open()` partially ran before
|
|
203
|
+
// throwing) — end the stream itself rather than attempting a second, incompatible response.
|
|
204
|
+
channel.end();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Path {@link registerRunEventStream} mounts. Exported as a constant so a caller that needs to know
|
|
209
|
+
* this route exists — a reverse proxy's forward list, `route-manifest.ts` — references it rather than
|
|
210
|
+
* restating the literal and later drifting from it.
|
|
211
|
+
*/
|
|
212
|
+
export const RUN_EVENTS_ROUTE_PATH = '/api/runs/:runId/events';
|
|
213
|
+
/** Express mounting glue for {@link handleRunEventStreamRequest} — resolves `runId`/`afterCursor` from an Express `Request` and hands the request straight through. */
|
|
214
|
+
export function registerRunEventStream(app, deps) {
|
|
215
|
+
app.get(RUN_EVENTS_ROUTE_PATH, async (req, res) => {
|
|
216
|
+
const runId = req.params.runId;
|
|
217
|
+
if (typeof runId !== 'string' || runId.length === 0) {
|
|
218
|
+
sendApiError(res, 400, createApiError('BAD_REQUEST', 'runId must be a non-empty path parameter'));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
await handleRunEventStreamRequest(res, runId, requestedAfterCursor(req), deps);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
/** Mounts create/status/cancel JSON endpoints and the SSE event stream as one run transport. */
|
|
225
|
+
export function registerRunRoutes(app, deps, adapter) {
|
|
226
|
+
mountJsonRoute(app, runStartRoute, deps, adapter);
|
|
227
|
+
mountJsonRoute(app, runListRoute, deps, adapter);
|
|
228
|
+
mountJsonRoute(app, runStatusRoute, deps, adapter);
|
|
229
|
+
mountJsonRoute(app, runCancelRoute, deps, adapter);
|
|
230
|
+
registerRunEventStream(app, deps);
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=runs.js.map
|
package/dist/runs.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runs.js","sourceRoot":"","sources":["../src/runs.ts"],"names":[],"mappings":"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"}
|
package/dist/sse.d.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic Server-Sent-Events transport primitive.
|
|
3
|
+
*
|
|
4
|
+
* Before this file, the only SSE machinery in this package was inlined
|
|
5
|
+
* directly inside `runs.ts`'s `registerRunEventStream` (a bounded-queue,
|
|
6
|
+
* backpressure-aware `res.write`/`'drain'` state machine with Last-Event-ID
|
|
7
|
+
* replay support) — see `source-map.md`'s 2026-07-19 "generic lifecycle SSE
|
|
8
|
+
* projection" note and the many "requires SSE (deferred)" verdicts against
|
|
9
|
+
* `chat.ts`/`memory.ts`/`terminal.ts` in the routes-classification table.
|
|
10
|
+
* `createSseChannel` below is that same state machine, generalized over any
|
|
11
|
+
* event type (not just `RunProtocolEvent`) so a route only has to describe
|
|
12
|
+
* *what* to stream, not *how* to stream it. `runs.ts` is refactored to call
|
|
13
|
+
* this primitive rather than keeping its own copy — the same discipline this
|
|
14
|
+
* package already applies to `origin-validation.ts`/`compat.ts` (shared
|
|
15
|
+
* mechanism, one implementation, one set of tests for the mechanism itself).
|
|
16
|
+
*
|
|
17
|
+
* OD's own precedent for a shared SSE primitive is `ctx.http.createSseResponse`
|
|
18
|
+
* (an Express-request-scoped helper both `apps/daemon/src/routes/runs.ts` and
|
|
19
|
+
* `apps/daemon/src/routes/terminal.ts` receive via dependency injection and
|
|
20
|
+
* call themselves) — this file is the Jini equivalent of that role, not an
|
|
21
|
+
* invented-from-scratch design.
|
|
22
|
+
*/
|
|
23
|
+
import type { ServerResponse } from 'node:http';
|
|
24
|
+
import { type ApiError } from '@jini-ai/protocol';
|
|
25
|
+
/** The minimal shape any event needs to flow through an `SseChannel`: an id for `Last-Event-ID` reconnect bookkeeping and a `kind` used as the wire `event:` field. */
|
|
26
|
+
export interface SseEvent {
|
|
27
|
+
readonly opaqueCursor: string;
|
|
28
|
+
readonly kind: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Cap on events queued for one client — covers both a pre-header replay
|
|
32
|
+
* burst and any backlog built up while `res.write` reports backpressure
|
|
33
|
+
* (`write() === false`, awaiting `'drain'`). A stalled or malicious client
|
|
34
|
+
* would otherwise let an unbounded producer grow this array without limit
|
|
35
|
+
* (SEC-006, carried forward from `runs.ts`'s original constant). Once
|
|
36
|
+
* exceeded, the connection is dropped rather than accepting unbounded
|
|
37
|
+
* memory growth.
|
|
38
|
+
*/
|
|
39
|
+
export declare const DEFAULT_MAX_QUEUED_SSE_EVENTS = 1000;
|
|
40
|
+
export interface CreateSseChannelOptions<E extends SseEvent> {
|
|
41
|
+
/** Queued-event ceiling before the connection is dropped. Defaults to {@link DEFAULT_MAX_QUEUED_SSE_EVENTS}. */
|
|
42
|
+
readonly maxQueuedEvents?: number;
|
|
43
|
+
/** When true for a given event, the channel ends the stream immediately after writing it (matches `runs.ts`'s `'end'`-kind auto-close). Omit for a channel that only ends on client disconnect/overflow/explicit `end()`. */
|
|
44
|
+
readonly isEndEvent?: (event: E) => boolean;
|
|
45
|
+
/** Overrides the wire format. Defaults to {@link defaultFormatEvent}. */
|
|
46
|
+
readonly formatEvent?: (event: E) => string;
|
|
47
|
+
/** Invoked once, synchronously, if `res.write` throws mid-flush — lets a caller log the real error without it crossing back into whatever produced the event (mirrors `runs.ts`'s `RunHttpDeps.onInternalError` seam, but this primitive itself stays logging-free: silent by default, matching a transport channel's "never throw back through the producer" contract). */
|
|
48
|
+
readonly onWriteError?: (error: unknown) => void;
|
|
49
|
+
}
|
|
50
|
+
export interface SseChannel<E extends SseEvent> {
|
|
51
|
+
/**
|
|
52
|
+
* Queues `event` for delivery. A no-op once the channel is closed. Before
|
|
53
|
+
* {@link SseChannel.open} is called, events accumulate in the same bounded
|
|
54
|
+
* queue used for later backpressure, so a caller may safely `enqueue`
|
|
55
|
+
* replay history before opening the stream — the client's very first
|
|
56
|
+
* flush then already contains that backlog.
|
|
57
|
+
*/
|
|
58
|
+
readonly enqueue: (event: E) => void;
|
|
59
|
+
/**
|
|
60
|
+
* Writes SSE headers (`200`, `text/event-stream`, `Cache-Control: no-cache,
|
|
61
|
+
* no-transform`, `Connection: keep-alive`) and starts draining the queue.
|
|
62
|
+
* Idempotent — a no-op if already open or already closed.
|
|
63
|
+
*/
|
|
64
|
+
readonly open: () => void;
|
|
65
|
+
/** True once the channel has closed for any reason (explicit `end()`, client disconnect, queue overflow, or a fatal write). */
|
|
66
|
+
readonly isClosed: () => boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Idempotent: marks the channel closed, unsubscribes nothing itself (a
|
|
69
|
+
* caller's {@link SseChannel.onClose} callback does that), and ends the
|
|
70
|
+
* underlying HTTP response if it has not already ended. Calling `res.end()`
|
|
71
|
+
* a second time (e.g. after the client already disconnected) is safe and
|
|
72
|
+
* documented as a no-op by Node's `http.ServerResponse` — this method does
|
|
73
|
+
* not need to distinguish "we are ending it" from "the client already
|
|
74
|
+
* ended it."
|
|
75
|
+
*/
|
|
76
|
+
readonly end: () => void;
|
|
77
|
+
/**
|
|
78
|
+
* Idempotent: marks the channel closed and runs every registered
|
|
79
|
+
* {@link SseChannel.onClose} callback — but, unlike {@link SseChannel.end},
|
|
80
|
+
* never touches `res` itself. For a caller that hits a failure *before*
|
|
81
|
+
* ever calling {@link SseChannel.open} and wants to send a different,
|
|
82
|
+
* non-SSE response instead (e.g. a JSON error body) rather than an empty
|
|
83
|
+
* ended stream. Calling {@link SseChannel.end} in that situation would
|
|
84
|
+
* end the response first and make the caller's own subsequent
|
|
85
|
+
* `res.status(...).json(...)` a write-after-end failure; `abandon` avoids
|
|
86
|
+
* that by leaving `res` completely untouched.
|
|
87
|
+
*/
|
|
88
|
+
readonly abandon: () => void;
|
|
89
|
+
/**
|
|
90
|
+
* Registers a callback invoked exactly once when the channel closes, from
|
|
91
|
+
* whichever cause fires first (explicit `end()`, a client disconnect
|
|
92
|
+
* observed via the response's own `'close'` event, or an internal
|
|
93
|
+
* overflow/write failure). Safe to call after the channel is already
|
|
94
|
+
* closed — the callback then runs immediately, synchronously.
|
|
95
|
+
*/
|
|
96
|
+
readonly onClose: (callback: () => void) => void;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Builds a bounded, backpressure-aware SSE channel over an Express `Response`.
|
|
100
|
+
* The channel does not know where events come from — a route wires its own
|
|
101
|
+
* event source (a `RunLifecycle.stream` subscription, an `EventEmitter`
|
|
102
|
+
* fan-in, a PTY's `onData`, …) to {@link SseChannel.enqueue} and lets the
|
|
103
|
+
* channel own delivery, ordering, and backpressure.
|
|
104
|
+
*
|
|
105
|
+
* The response's own `'close'` event (the client disconnecting) is observed
|
|
106
|
+
* from construction time, before {@link SseChannel.open} is ever called —
|
|
107
|
+
* this lets a caller `await` some asynchronous subscribe step (as `runs.ts`
|
|
108
|
+
* does against `RunLifecycle.stream`) and still detect "the client already
|
|
109
|
+
* disappeared while we were waiting" via {@link SseChannel.isClosed} once
|
|
110
|
+
* that await resolves, without the disconnect handler itself trying to
|
|
111
|
+
* re-end an already-gone response (see {@link SseChannel.end}'s doc for why
|
|
112
|
+
* that distinction doesn't actually need special-casing).
|
|
113
|
+
*
|
|
114
|
+
* @param res - The response to stream over, typed against the raw `node:http` `ServerResponse`
|
|
115
|
+
* rather than Express's own `Response` — Express's `Response` extends `ServerResponse` directly,
|
|
116
|
+
* so every existing Express caller still satisfies this signature unchanged. Never read from — only
|
|
117
|
+
* `write`/`statusCode`/`setHeader`/`flushHeaders`/`end`/`on('close'|'drain')` are used, all of
|
|
118
|
+
* which exist identically on the raw type (the one genuinely Express-specific call this function
|
|
119
|
+
* used to make, `res.status(200)`, is `res.statusCode = 200` here instead — the same assignment
|
|
120
|
+
* Express's own `.status()` performs internally).
|
|
121
|
+
* @param options - See {@link CreateSseChannelOptions}.
|
|
122
|
+
* @complexity `enqueue`/`open`/`end` are O(1) amortized; a full queue drain is O(events written).
|
|
123
|
+
* @overallScore 100/100
|
|
124
|
+
*/
|
|
125
|
+
export declare function createSseChannel<E extends SseEvent>(res: ServerResponse, options?: CreateSseChannelOptions<E>): SseChannel<E>;
|
|
126
|
+
/** Reads a reconnect cursor from the standard `Last-Event-ID` header, falling back to an `afterCursor` query parameter. Shared by any SSE route that supports reconnect replay (the header takes precedence since it is what browsers set automatically on `EventSource` reconnect). */
|
|
127
|
+
export declare function requestedAfterCursor(req: {
|
|
128
|
+
get(name: string): string | undefined;
|
|
129
|
+
query: Record<string, unknown>;
|
|
130
|
+
}): string | null;
|
|
131
|
+
/**
|
|
132
|
+
* Writes an `ApiError`, wrapped in the standard `{ error }` envelope, directly onto a raw
|
|
133
|
+
* `ServerResponse` — for the narrow window before an `SseChannel` has opened (so Express's own
|
|
134
|
+
* `res.json()` response wrapper isn't safe to use yet). Produces the identical
|
|
135
|
+
* `createApiErrorResponse(error)` envelope shape `response.ts`'s `sendApiError`/`sendJson` do —
|
|
136
|
+
* this just writes that same shape without going through Express's response helper.
|
|
137
|
+
*/
|
|
138
|
+
export declare function sendRawApiError(res: ServerResponse, status: number, error: ApiError): void;
|
|
139
|
+
//# sourceMappingURL=sse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EAA0B,KAAK,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAE1E,uKAAuK;AACvK,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B,OAAO,CAAC;AAOlD,MAAM,WAAW,uBAAuB,CAAC,CAAC,SAAS,QAAQ;IACzD,gHAAgH;IAChH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,6NAA6N;IAC7N,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC;IAC5C,yEAAyE;IACzE,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,CAAC;IAC5C,4WAA4W;IAC5W,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAClD;AAED,MAAM,WAAW,UAAU,CAAC,CAAC,SAAS,QAAQ;IAC5C;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IACrC;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,IAAI,CAAC;IAC1B,+HAA+H;IAC/H,QAAQ,CAAC,QAAQ,EAAE,MAAM,OAAO,CAAC;IACjC;;;;;;;;OAQG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,IAAI,CAAC;IACzB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;IAC7B;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;CAClD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,QAAQ,EACjD,GAAG,EAAE,cAAc,EACnB,OAAO,GAAE,uBAAuB,CAAC,CAAC,CAAM,GACvC,UAAU,CAAC,CAAC,CAAC,CA8Ff;AAED,wRAAwR;AACxR,wBAAgB,oBAAoB,CAAC,GAAG,EAAE;IACxC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACtC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC,GAAG,MAAM,GAAG,IAAI,CAKhB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,IAAI,CAI1F"}
|