@telorun/runner-core 0.5.2
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 +17 -0
- package/README.md +38 -0
- package/dist/backend.d.ts +71 -0
- package/dist/backend.d.ts.map +1 -0
- package/dist/backend.js +2 -0
- package/dist/backend.js.map +1 -0
- package/dist/base-image-catalog.d.ts +110 -0
- package/dist/base-image-catalog.d.ts.map +1 -0
- package/dist/base-image-catalog.js +245 -0
- package/dist/base-image-catalog.js.map +1 -0
- package/dist/capabilities-schema.d.ts +33 -0
- package/dist/capabilities-schema.d.ts.map +1 -0
- package/dist/capabilities-schema.js +44 -0
- package/dist/capabilities-schema.js.map +1 -0
- package/dist/config.d.ts +37 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +93 -0
- package/dist/config.js.map +1 -0
- package/dist/contract.d.ts +170 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +24 -0
- package/dist/contract.js.map +1 -0
- package/dist/debug/relay.d.ts +25 -0
- package/dist/debug/relay.d.ts.map +1 -0
- package/dist/debug/relay.js +89 -0
- package/dist/debug/relay.js.map +1 -0
- package/dist/dependency-key.d.ts +38 -0
- package/dist/dependency-key.d.ts.map +1 -0
- package/dist/dependency-key.js +68 -0
- package/dist/dependency-key.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/routes/capabilities.d.ts +12 -0
- package/dist/routes/capabilities.d.ts.map +1 -0
- package/dist/routes/capabilities.js +15 -0
- package/dist/routes/capabilities.js.map +1 -0
- package/dist/routes/health.d.ts +5 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +8 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/io.d.ts +8 -0
- package/dist/routes/io.d.ts.map +1 -0
- package/dist/routes/io.js +239 -0
- package/dist/routes/io.js.map +1 -0
- package/dist/routes/probe.d.ts +7 -0
- package/dist/routes/probe.d.ts.map +1 -0
- package/dist/routes/probe.js +22 -0
- package/dist/routes/probe.js.map +1 -0
- package/dist/routes/sessions.d.ts +22 -0
- package/dist/routes/sessions.d.ts.map +1 -0
- package/dist/routes/sessions.js +223 -0
- package/dist/routes/sessions.js.map +1 -0
- package/dist/server.d.ts +34 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +67 -0
- package/dist/server.js.map +1 -0
- package/dist/session/bundle-path.d.ts +12 -0
- package/dist/session/bundle-path.d.ts.map +1 -0
- package/dist/session/bundle-path.js +27 -0
- package/dist/session/bundle-path.js.map +1 -0
- package/dist/session/byte-ring-buffer.d.ts +29 -0
- package/dist/session/byte-ring-buffer.d.ts.map +1 -0
- package/dist/session/byte-ring-buffer.js +54 -0
- package/dist/session/byte-ring-buffer.js.map +1 -0
- package/dist/session/registry.d.ts +62 -0
- package/dist/session/registry.d.ts.map +1 -0
- package/dist/session/registry.js +156 -0
- package/dist/session/registry.js.map +1 -0
- package/dist/session/ring-buffer.d.ts +37 -0
- package/dist/session/ring-buffer.d.ts.map +1 -0
- package/dist/session/ring-buffer.js +64 -0
- package/dist/session/ring-buffer.js.map +1 -0
- package/dist/session/session-id.d.ts +3 -0
- package/dist/session/session-id.d.ts.map +1 -0
- package/dist/session/session-id.js +19 -0
- package/dist/session/session-id.js.map +1 -0
- package/dist/sse/channel.d.ts +11 -0
- package/dist/sse/channel.d.ts.map +1 -0
- package/dist/sse/channel.js +129 -0
- package/dist/sse/channel.js.map +1 -0
- package/package.json +48 -0
- package/src/backend.ts +88 -0
- package/src/base-image-catalog.test.ts +209 -0
- package/src/base-image-catalog.ts +320 -0
- package/src/capabilities-schema.test.ts +54 -0
- package/src/capabilities-schema.ts +71 -0
- package/src/config.ts +122 -0
- package/src/contract.ts +170 -0
- package/src/debug/relay.ts +104 -0
- package/src/dependency-key.test.ts +64 -0
- package/src/dependency-key.ts +105 -0
- package/src/index.ts +33 -0
- package/src/routes/capabilities.ts +20 -0
- package/src/routes/health.ts +9 -0
- package/src/routes/io.ts +265 -0
- package/src/routes/probe.ts +35 -0
- package/src/routes/sessions.ts +270 -0
- package/src/server.ts +108 -0
- package/src/session/bundle-path.ts +27 -0
- package/src/session/byte-ring-buffer.ts +62 -0
- package/src/session/registry.test.ts +34 -0
- package/src/session/registry.ts +185 -0
- package/src/session/ring-buffer.test.ts +54 -0
- package/src/session/ring-buffer.ts +75 -0
- package/src/session/session-id.test.ts +17 -0
- package/src/session/session-id.ts +20 -0
- package/src/sse/channel.ts +154 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import Fastify from "fastify";
|
|
2
|
+
import cors from "@fastify/cors";
|
|
3
|
+
import websocket from "@fastify/websocket";
|
|
4
|
+
import { capabilitiesRoute } from "./routes/capabilities.js";
|
|
5
|
+
import { healthRoute } from "./routes/health.js";
|
|
6
|
+
import { ioRoute } from "./routes/io.js";
|
|
7
|
+
import { probeRoute } from "./routes/probe.js";
|
|
8
|
+
import { sessionsRoute } from "./routes/sessions.js";
|
|
9
|
+
import { SessionRegistry } from "./session/registry.js";
|
|
10
|
+
export async function buildServer(deps) {
|
|
11
|
+
const app = Fastify({
|
|
12
|
+
logger: { level: deps.config.logLevel },
|
|
13
|
+
});
|
|
14
|
+
// CORS: SSE and fetch from the editor's browser origin are cross-origin by
|
|
15
|
+
// default. A runner with no auth is driven by whoever can reach the port, so
|
|
16
|
+
// default to `*` and let operators narrow via RUNNER_CORS_ORIGINS.
|
|
17
|
+
await app.register(cors, {
|
|
18
|
+
origin: deps.config.corsOrigins,
|
|
19
|
+
methods: ["GET", "POST", "DELETE", "OPTIONS"],
|
|
20
|
+
});
|
|
21
|
+
await app.register(websocket);
|
|
22
|
+
const registry = deps.registry ??
|
|
23
|
+
new SessionRegistry({
|
|
24
|
+
maxSessions: deps.config.maxSessions,
|
|
25
|
+
exitTtlMs: deps.config.exitTtlMs,
|
|
26
|
+
replayBufferBytes: deps.config.replayBufferBytes,
|
|
27
|
+
});
|
|
28
|
+
// Terms are stable across the process — resolve the capabilities once for them
|
|
29
|
+
// even when `capabilities` is a getter (the route still re-resolves per request).
|
|
30
|
+
const capabilitiesValue = typeof deps.capabilities === "function" ? deps.capabilities() : deps.capabilities;
|
|
31
|
+
await app.register(healthRoute(deps.version));
|
|
32
|
+
await app.register(capabilitiesRoute(deps.capabilities));
|
|
33
|
+
await app.register(probeRoute({ backend: deps.backend }));
|
|
34
|
+
await app.register(sessionsRoute({
|
|
35
|
+
backend: deps.backend,
|
|
36
|
+
registry,
|
|
37
|
+
corsOrigins: deps.config.corsOrigins,
|
|
38
|
+
defaultRegistryUrl: deps.defaultRegistryUrl,
|
|
39
|
+
validateConfig: deps.validateConfig,
|
|
40
|
+
// The capabilities document is the single source of the runner's terms;
|
|
41
|
+
// the session route enforces what /v1/capabilities advertises.
|
|
42
|
+
terms: capabilitiesValue.terms,
|
|
43
|
+
}));
|
|
44
|
+
await app.register(ioRoute({ registry, corsOrigins: deps.config.corsOrigins }));
|
|
45
|
+
return { app, registry };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Stop every live session. Used by graceful shutdown — marks each entry
|
|
49
|
+
* userStopped and stops its backend workload so nothing leaks past process
|
|
50
|
+
* exit. Backend-neutral: it only touches the abstract `BackendSession`.
|
|
51
|
+
*/
|
|
52
|
+
export async function stopAllSessions(registry, log) {
|
|
53
|
+
const live = registry.list().filter((e) => e.session !== null && e.exitedAt === null);
|
|
54
|
+
if (live.length === 0)
|
|
55
|
+
return;
|
|
56
|
+
log.info({ count: live.length }, "stopping live sessions before shutdown");
|
|
57
|
+
await Promise.all(live.map(async (entry) => {
|
|
58
|
+
entry.userStopped = true;
|
|
59
|
+
try {
|
|
60
|
+
await entry.session?.stop();
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
log.warn({ err, sessionId: entry.sessionId }, "failed to stop session during shutdown");
|
|
64
|
+
}
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,OAAyD,MAAM,SAAS,CAAC;AAChF,OAAO,IAAI,MAAM,eAAe,CAAC;AACjC,OAAO,SAAS,MAAM,oBAAoB,CAAC;AAK3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAyBxD,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,IAAgB;IAChD,MAAM,GAAG,GAAG,OAAO,CAAC;QAClB,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE;KACxC,CAAC,CAAC;IAEH,2EAA2E;IAC3E,6EAA6E;IAC7E,mEAAmE;IACnE,MAAM,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE;QACvB,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW;QAC/B,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,CAAC;KAC9C,CAAC,CAAC;IAEH,MAAM,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IAE9B,MAAM,QAAQ,GACZ,IAAI,CAAC,QAAQ;QACb,IAAI,eAAe,CAAC;YAClB,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW;YACpC,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS;YAChC,iBAAiB,EAAE,IAAI,CAAC,MAAM,CAAC,iBAAiB;SACjD,CAAC,CAAC;IAEL,+EAA+E;IAC/E,kFAAkF;IAClF,MAAM,iBAAiB,GACrB,OAAO,IAAI,CAAC,YAAY,KAAK,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC;IAEpF,MAAM,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;IAC9C,MAAM,GAAG,CAAC,QAAQ,CAAC,iBAAiB,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC;IACzD,MAAM,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IAC1D,MAAM,GAAG,CAAC,QAAQ,CAChB,aAAa,CAAC;QACZ,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,QAAQ;QACR,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW;QACpC,kBAAkB,EAAE,IAAI,CAAC,kBAAkB;QAC3C,cAAc,EAAE,IAAI,CAAC,cAAc;QACnC,wEAAwE;QACxE,+DAA+D;QAC/D,KAAK,EAAE,iBAAiB,CAAC,KAAK;KAC/B,CAAC,CACH,CAAC;IACF,MAAM,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAEhF,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC;AAC3B,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,QAAyB,EACzB,GAA6C;IAE7C,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,IAAI,IAAI,CAAC,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC;IACtF,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAC9B,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,wCAAwC,CAAC,CAAC;IAC3E,MAAM,OAAO,CAAC,GAAG,CACf,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;QACvB,KAAK,CAAC,WAAW,GAAG,IAAI,CAAC;QACzB,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC;QAC9B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,EAAE,wCAAwC,CAAC,CAAC;QAC1F,CAAC;IACH,CAAC,CAAC,CACH,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare class BundlePathError extends Error {
|
|
2
|
+
}
|
|
3
|
+
/**
|
|
4
|
+
* Bundle file paths arrive POSIX-style and untrusted. Guard against traversal
|
|
5
|
+
* explicitly rather than trusting `path.resolve` — an `entryRelativePath` or
|
|
6
|
+
* `files[].relativePath` of `../foo` would escape the session's own directory.
|
|
7
|
+
* Backend-neutral: every backend normalizes paths the same way before placing
|
|
8
|
+
* files, regardless of how it ultimately delivers the bundle.
|
|
9
|
+
*/
|
|
10
|
+
export declare function normalizeBundlePath(p: string): string;
|
|
11
|
+
export declare function validateSessionId(id: string): void;
|
|
12
|
+
//# sourceMappingURL=bundle-path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundle-path.d.ts","sourceRoot":"","sources":["../../src/session/bundle-path.ts"],"names":[],"mappings":"AAEA,qBAAa,eAAgB,SAAQ,KAAK;CAAG;AAE7C;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CASrD;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAIlD"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
export class BundlePathError extends Error {
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Bundle file paths arrive POSIX-style and untrusted. Guard against traversal
|
|
6
|
+
* explicitly rather than trusting `path.resolve` — an `entryRelativePath` or
|
|
7
|
+
* `files[].relativePath` of `../foo` would escape the session's own directory.
|
|
8
|
+
* Backend-neutral: every backend normalizes paths the same way before placing
|
|
9
|
+
* files, regardless of how it ultimately delivers the bundle.
|
|
10
|
+
*/
|
|
11
|
+
export function normalizeBundlePath(p) {
|
|
12
|
+
const normalized = posix.normalize(p).replace(/^\/+/, "");
|
|
13
|
+
if (normalized === "" || normalized === "." || normalized.startsWith("../") || normalized === "..") {
|
|
14
|
+
throw new BundlePathError(`invalid bundle relativePath '${p}'`);
|
|
15
|
+
}
|
|
16
|
+
for (const part of normalized.split("/")) {
|
|
17
|
+
if (part === "..")
|
|
18
|
+
throw new BundlePathError(`invalid bundle relativePath '${p}'`);
|
|
19
|
+
}
|
|
20
|
+
return normalized;
|
|
21
|
+
}
|
|
22
|
+
export function validateSessionId(id) {
|
|
23
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
|
|
24
|
+
throw new BundlePathError(`invalid sessionId '${id}' (must match /^[a-zA-Z0-9_-]+$/)`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=bundle-path.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundle-path.js","sourceRoot":"","sources":["../../src/session/bundle-path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAElC,MAAM,OAAO,eAAgB,SAAQ,KAAK;CAAG;AAE7C;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,CAAS;IAC3C,MAAM,UAAU,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC1D,IAAI,UAAU,KAAK,EAAE,IAAI,UAAU,KAAK,GAAG,IAAI,UAAU,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;QACnG,MAAM,IAAI,eAAe,CAAC,gCAAgC,CAAC,GAAG,CAAC,CAAC;IAClE,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACzC,IAAI,IAAI,KAAK,IAAI;YAAE,MAAM,IAAI,eAAe,CAAC,gCAAgC,CAAC,GAAG,CAAC,CAAC;IACrF,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,EAAU;IAC1C,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,eAAe,CAAC,sBAAsB,EAAE,mCAAmC,CAAC,CAAC;IACzF,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface BufferedBytes {
|
|
2
|
+
seq: number;
|
|
3
|
+
bytes: Buffer;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Byte-capped FIFO ring buffer for raw PTY output replay. Mirrors
|
|
7
|
+
* EventRingBuffer's eviction-with-gap semantics but stores Buffer slices keyed
|
|
8
|
+
* by a monotonic seq number — never reused, never reset.
|
|
9
|
+
*
|
|
10
|
+
* A client's `?lastSeq=<n>` asks for chunks with seq > n; if the oldest
|
|
11
|
+
* remaining seq is still > n+1, the gap must be signaled separately.
|
|
12
|
+
*/
|
|
13
|
+
export declare class ByteRingBuffer {
|
|
14
|
+
private readonly maxBytes;
|
|
15
|
+
private readonly entries;
|
|
16
|
+
private totalBytes;
|
|
17
|
+
private nextSeq;
|
|
18
|
+
constructor(maxBytes: number);
|
|
19
|
+
push(bytes: Buffer): BufferedBytes;
|
|
20
|
+
private evict;
|
|
21
|
+
replay(afterSeq: number): {
|
|
22
|
+
entries: BufferedBytes[];
|
|
23
|
+
hasGap: boolean;
|
|
24
|
+
};
|
|
25
|
+
get size(): number;
|
|
26
|
+
get bytes(): number;
|
|
27
|
+
get latestSeq(): number;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=byte-ring-buffer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"byte-ring-buffer.d.ts","sourceRoot":"","sources":["../../src/session/byte-ring-buffer.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;GAOG;AACH,qBAAa,cAAc;IAKb,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAJrC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuB;IAC/C,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,OAAO,CAAK;gBAES,QAAQ,EAAE,MAAM;IAM7C,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa;IASlC,OAAO,CAAC,KAAK;IAUb,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,aAAa,EAAE,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE;IAOvE,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,IAAI,SAAS,IAAI,MAAM,CAEtB;CACF"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Byte-capped FIFO ring buffer for raw PTY output replay. Mirrors
|
|
3
|
+
* EventRingBuffer's eviction-with-gap semantics but stores Buffer slices keyed
|
|
4
|
+
* by a monotonic seq number — never reused, never reset.
|
|
5
|
+
*
|
|
6
|
+
* A client's `?lastSeq=<n>` asks for chunks with seq > n; if the oldest
|
|
7
|
+
* remaining seq is still > n+1, the gap must be signaled separately.
|
|
8
|
+
*/
|
|
9
|
+
export class ByteRingBuffer {
|
|
10
|
+
maxBytes;
|
|
11
|
+
entries = [];
|
|
12
|
+
totalBytes = 0;
|
|
13
|
+
nextSeq = 1;
|
|
14
|
+
constructor(maxBytes) {
|
|
15
|
+
this.maxBytes = maxBytes;
|
|
16
|
+
if (!Number.isInteger(maxBytes) || maxBytes <= 0) {
|
|
17
|
+
throw new Error(`ByteRingBuffer maxBytes must be a positive integer, got ${maxBytes}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
push(bytes) {
|
|
21
|
+
const seq = this.nextSeq++;
|
|
22
|
+
const entry = { seq, bytes };
|
|
23
|
+
this.entries.push(entry);
|
|
24
|
+
this.totalBytes += bytes.byteLength;
|
|
25
|
+
this.evict();
|
|
26
|
+
return entry;
|
|
27
|
+
}
|
|
28
|
+
evict() {
|
|
29
|
+
// Always retain at least the most-recent entry, even if it alone exceeds
|
|
30
|
+
// maxBytes — same invariant as EventRingBuffer.
|
|
31
|
+
while (this.totalBytes > this.maxBytes && this.entries.length > 1) {
|
|
32
|
+
const dropped = this.entries.shift();
|
|
33
|
+
if (!dropped)
|
|
34
|
+
break;
|
|
35
|
+
this.totalBytes -= dropped.bytes.byteLength;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
replay(afterSeq) {
|
|
39
|
+
const entries = this.entries.filter((e) => e.seq > afterSeq);
|
|
40
|
+
const oldestSeq = this.entries[0]?.seq ?? this.nextSeq;
|
|
41
|
+
const hasGap = afterSeq + 1 < oldestSeq;
|
|
42
|
+
return { entries, hasGap };
|
|
43
|
+
}
|
|
44
|
+
get size() {
|
|
45
|
+
return this.entries.length;
|
|
46
|
+
}
|
|
47
|
+
get bytes() {
|
|
48
|
+
return this.totalBytes;
|
|
49
|
+
}
|
|
50
|
+
get latestSeq() {
|
|
51
|
+
return this.nextSeq - 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=byte-ring-buffer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"byte-ring-buffer.js","sourceRoot":"","sources":["../../src/session/byte-ring-buffer.ts"],"names":[],"mappings":"AAKA;;;;;;;GAOG;AACH,MAAM,OAAO,cAAc;IAKI;IAJZ,OAAO,GAAoB,EAAE,CAAC;IACvC,UAAU,GAAG,CAAC,CAAC;IACf,OAAO,GAAG,CAAC,CAAC;IAEpB,YAA6B,QAAgB;QAAhB,aAAQ,GAAR,QAAQ,CAAQ;QAC3C,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,QAAQ,IAAI,CAAC,EAAE,CAAC;YACjD,MAAM,IAAI,KAAK,CAAC,2DAA2D,QAAQ,EAAE,CAAC,CAAC;QACzF,CAAC;IACH,CAAC;IAED,IAAI,CAAC,KAAa;QAChB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAkB,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;QAC5C,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzB,IAAI,CAAC,UAAU,IAAI,KAAK,CAAC,UAAU,CAAC;QACpC,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,OAAO,KAAK,CAAC;IACf,CAAC;IAEO,KAAK;QACX,yEAAyE;QACzE,gDAAgD;QAChD,OAAO,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAClE,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YACrC,IAAI,CAAC,OAAO;gBAAE,MAAM;YACpB,IAAI,CAAC,UAAU,IAAI,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC;QAC9C,CAAC;IACH,CAAC;IAED,MAAM,CAAC,QAAgB;QACrB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,QAAQ,CAAC,CAAC;QAC7D,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC;QACvD,MAAM,MAAM,GAAG,QAAQ,GAAG,CAAC,GAAG,SAAS,CAAC;QACxC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAC7B,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;IAC7B,CAAC;IAED,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IAED,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC;IAC1B,CAAC;CACF"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import type { BackendSession } from "../backend.js";
|
|
3
|
+
import { type RunEvent, type RunStatus } from "../contract.js";
|
|
4
|
+
import { ByteRingBuffer, type BufferedBytes } from "./byte-ring-buffer.js";
|
|
5
|
+
import { EventRingBuffer, type BufferedEvent } from "./ring-buffer.js";
|
|
6
|
+
export interface SessionEntry {
|
|
7
|
+
readonly sessionId: string;
|
|
8
|
+
readonly createdAt: Date;
|
|
9
|
+
readonly buffer: EventRingBuffer;
|
|
10
|
+
readonly byteBuffer: ByteRingBuffer;
|
|
11
|
+
readonly emitter: EventEmitter;
|
|
12
|
+
readonly byteEmitter: EventEmitter;
|
|
13
|
+
/** The live backend workload. Null until `start` resolves; the route writes
|
|
14
|
+
* stdin / resize / stop through it. A backend's `writeStdin` is a no-op once
|
|
15
|
+
* the workload has terminated, so callers need not null it on exit. */
|
|
16
|
+
session: BackendSession | null;
|
|
17
|
+
status: RunStatus;
|
|
18
|
+
exitedAt: Date | null;
|
|
19
|
+
userStopped: boolean;
|
|
20
|
+
evictionTimer: NodeJS.Timeout | null;
|
|
21
|
+
}
|
|
22
|
+
export interface RegistryDeps {
|
|
23
|
+
maxSessions: number;
|
|
24
|
+
exitTtlMs: number;
|
|
25
|
+
replayBufferBytes: number;
|
|
26
|
+
}
|
|
27
|
+
export declare class SessionLimitError extends Error {
|
|
28
|
+
}
|
|
29
|
+
export declare class SessionEvictedError extends Error {
|
|
30
|
+
}
|
|
31
|
+
export declare class SessionRegistry {
|
|
32
|
+
private readonly deps;
|
|
33
|
+
private readonly sessions;
|
|
34
|
+
constructor(deps: RegistryDeps);
|
|
35
|
+
size(): number;
|
|
36
|
+
has(sessionId: string): boolean;
|
|
37
|
+
get(sessionId: string): SessionEntry | undefined;
|
|
38
|
+
list(): SessionEntry[];
|
|
39
|
+
/**
|
|
40
|
+
* Creates a fresh registry entry. Callers are responsible for guarding against
|
|
41
|
+
* duplicate insertion. Throws SessionLimitError if we're at capacity.
|
|
42
|
+
*/
|
|
43
|
+
register(args: {
|
|
44
|
+
sessionId: string;
|
|
45
|
+
}): SessionEntry;
|
|
46
|
+
pushBytes(sessionId: string, bytes: Buffer): BufferedBytes | undefined;
|
|
47
|
+
subscribeBytes(sessionId: string, listener: (b: BufferedBytes) => void): () => void;
|
|
48
|
+
emit(sessionId: string, event: RunEvent): BufferedEvent | undefined;
|
|
49
|
+
subscribe(sessionId: string, listener: (e: BufferedEvent) => void): () => void;
|
|
50
|
+
/** Free a slot at capacity by removing the oldest already-terminated session
|
|
51
|
+
* (by exit time). Retained exited sessions are history kept for re-attach, so
|
|
52
|
+
* they yield to a new run rather than blocking it; live sessions are never
|
|
53
|
+
* evicted. Returns false when every session is still live. */
|
|
54
|
+
private evictOldestTerminal;
|
|
55
|
+
private scheduleEviction;
|
|
56
|
+
/**
|
|
57
|
+
* Remove an entry immediately (used by shutdown sweeps and startup cleanup).
|
|
58
|
+
* Returns true if it was present.
|
|
59
|
+
*/
|
|
60
|
+
remove(sessionId: string): boolean;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/session/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAc,KAAK,QAAQ,EAAE,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAAE,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEvE,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,cAAc,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,YAAY,CAAC;IAEnC;;4EAEwE;IACxE,OAAO,EAAE,cAAc,GAAG,IAAI,CAAC;IAC/B,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC;IACtB,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAaD,qBAAa,iBAAkB,SAAQ,KAAK;CAAG;AAC/C,qBAAa,mBAAoB,SAAQ,KAAK;CAAG;AAEjD,qBAAa,eAAe;IAGd,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmC;gBAE/B,IAAI,EAAE,YAAY;IAE/C,IAAI,IAAI,MAAM;IAId,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAI/B,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;IAIhD,IAAI,IAAI,YAAY,EAAE;IAItB;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,YAAY;IA6BnD,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAqBtE,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,IAAI,GAAG,MAAM,IAAI;IAOnF,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,aAAa,GAAG,SAAS;IAenE,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,IAAI,GAAG,MAAM,IAAI;IAO9E;;;mEAG+D;IAC/D,OAAO,CAAC,mBAAmB;IAU3B,OAAO,CAAC,gBAAgB;IAUxB;;;OAGG;IACH,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;CAOnC"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { isTerminal } from "../contract.js";
|
|
3
|
+
import { ByteRingBuffer } from "./byte-ring-buffer.js";
|
|
4
|
+
import { EventRingBuffer } from "./ring-buffer.js";
|
|
5
|
+
const EVENT_EMITTED = "event";
|
|
6
|
+
const BYTES_EMITTED = "chunk";
|
|
7
|
+
/** Cap on a single buffered byte chunk. Without this, one huge workload
|
|
8
|
+
* burst (`cat largefile`) could be admitted as one entry; the ring
|
|
9
|
+
* buffer's "retain at least one" invariant would then keep that one
|
|
10
|
+
* oversized entry resident regardless of `replayBufferBytes`. Splitting
|
|
11
|
+
* on push means the cap actually bounds memory and the eviction loop
|
|
12
|
+
* has fine-grained units to drop. */
|
|
13
|
+
const MAX_PUSH_CHUNK = 64 * 1024;
|
|
14
|
+
export class SessionLimitError extends Error {
|
|
15
|
+
}
|
|
16
|
+
export class SessionEvictedError extends Error {
|
|
17
|
+
}
|
|
18
|
+
export class SessionRegistry {
|
|
19
|
+
deps;
|
|
20
|
+
sessions = new Map();
|
|
21
|
+
constructor(deps) {
|
|
22
|
+
this.deps = deps;
|
|
23
|
+
}
|
|
24
|
+
size() {
|
|
25
|
+
return this.sessions.size;
|
|
26
|
+
}
|
|
27
|
+
has(sessionId) {
|
|
28
|
+
return this.sessions.has(sessionId);
|
|
29
|
+
}
|
|
30
|
+
get(sessionId) {
|
|
31
|
+
return this.sessions.get(sessionId);
|
|
32
|
+
}
|
|
33
|
+
list() {
|
|
34
|
+
return [...this.sessions.values()];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Creates a fresh registry entry. Callers are responsible for guarding against
|
|
38
|
+
* duplicate insertion. Throws SessionLimitError if we're at capacity.
|
|
39
|
+
*/
|
|
40
|
+
register(args) {
|
|
41
|
+
if (this.sessions.size >= this.deps.maxSessions && !this.evictOldestTerminal()) {
|
|
42
|
+
throw new SessionLimitError(`runner is at its configured max of ${this.deps.maxSessions} concurrent sessions`);
|
|
43
|
+
}
|
|
44
|
+
const entry = {
|
|
45
|
+
sessionId: args.sessionId,
|
|
46
|
+
createdAt: new Date(),
|
|
47
|
+
buffer: new EventRingBuffer(this.deps.replayBufferBytes),
|
|
48
|
+
byteBuffer: new ByteRingBuffer(this.deps.replayBufferBytes),
|
|
49
|
+
emitter: new EventEmitter(),
|
|
50
|
+
byteEmitter: new EventEmitter(),
|
|
51
|
+
session: null,
|
|
52
|
+
status: { kind: "starting" },
|
|
53
|
+
exitedAt: null,
|
|
54
|
+
userStopped: false,
|
|
55
|
+
evictionTimer: null,
|
|
56
|
+
};
|
|
57
|
+
// Many transient SSE / WS subscribers per session is normal — bump the
|
|
58
|
+
// default 10-listener warning to a high cap so the alarm still fires
|
|
59
|
+
// for a real listener leak. 256 is well above the realistic concurrent-
|
|
60
|
+
// tab count and well below "obviously a bug".
|
|
61
|
+
entry.emitter.setMaxListeners(256);
|
|
62
|
+
entry.byteEmitter.setMaxListeners(256);
|
|
63
|
+
this.sessions.set(args.sessionId, entry);
|
|
64
|
+
return entry;
|
|
65
|
+
}
|
|
66
|
+
pushBytes(sessionId, bytes) {
|
|
67
|
+
const entry = this.sessions.get(sessionId);
|
|
68
|
+
if (!entry)
|
|
69
|
+
return undefined;
|
|
70
|
+
if (bytes.byteLength <= MAX_PUSH_CHUNK) {
|
|
71
|
+
const buffered = entry.byteBuffer.push(bytes);
|
|
72
|
+
entry.byteEmitter.emit(BYTES_EMITTED, buffered);
|
|
73
|
+
return buffered;
|
|
74
|
+
}
|
|
75
|
+
// Split the chunk into MAX_PUSH_CHUNK-sized slices, each getting its
|
|
76
|
+
// own seq. Returns the last buffered piece for the caller's bookkeeping.
|
|
77
|
+
let last;
|
|
78
|
+
for (let off = 0; off < bytes.byteLength; off += MAX_PUSH_CHUNK) {
|
|
79
|
+
const slice = bytes.subarray(off, Math.min(off + MAX_PUSH_CHUNK, bytes.byteLength));
|
|
80
|
+
// subarray shares memory with the parent buffer; copy so the ring's
|
|
81
|
+
// entry doesn't pin the original allocation past eviction.
|
|
82
|
+
last = entry.byteBuffer.push(Buffer.from(slice));
|
|
83
|
+
entry.byteEmitter.emit(BYTES_EMITTED, last);
|
|
84
|
+
}
|
|
85
|
+
return last;
|
|
86
|
+
}
|
|
87
|
+
subscribeBytes(sessionId, listener) {
|
|
88
|
+
const entry = this.sessions.get(sessionId);
|
|
89
|
+
if (!entry)
|
|
90
|
+
throw new SessionEvictedError(`session '${sessionId}' not in registry`);
|
|
91
|
+
entry.byteEmitter.on(BYTES_EMITTED, listener);
|
|
92
|
+
return () => entry.byteEmitter.off(BYTES_EMITTED, listener);
|
|
93
|
+
}
|
|
94
|
+
emit(sessionId, event) {
|
|
95
|
+
const entry = this.sessions.get(sessionId);
|
|
96
|
+
if (!entry)
|
|
97
|
+
return undefined;
|
|
98
|
+
const buffered = entry.buffer.push(event);
|
|
99
|
+
if (event.type === "status") {
|
|
100
|
+
entry.status = event.status;
|
|
101
|
+
if (isTerminal(event.status)) {
|
|
102
|
+
entry.exitedAt = new Date();
|
|
103
|
+
this.scheduleEviction(entry);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
entry.emitter.emit(EVENT_EMITTED, buffered);
|
|
107
|
+
return buffered;
|
|
108
|
+
}
|
|
109
|
+
subscribe(sessionId, listener) {
|
|
110
|
+
const entry = this.sessions.get(sessionId);
|
|
111
|
+
if (!entry)
|
|
112
|
+
throw new SessionEvictedError(`session '${sessionId}' not in registry`);
|
|
113
|
+
entry.emitter.on(EVENT_EMITTED, listener);
|
|
114
|
+
return () => entry.emitter.off(EVENT_EMITTED, listener);
|
|
115
|
+
}
|
|
116
|
+
/** Free a slot at capacity by removing the oldest already-terminated session
|
|
117
|
+
* (by exit time). Retained exited sessions are history kept for re-attach, so
|
|
118
|
+
* they yield to a new run rather than blocking it; live sessions are never
|
|
119
|
+
* evicted. Returns false when every session is still live. */
|
|
120
|
+
evictOldestTerminal() {
|
|
121
|
+
let oldest;
|
|
122
|
+
for (const entry of this.sessions.values()) {
|
|
123
|
+
if (entry.exitedAt === null)
|
|
124
|
+
continue;
|
|
125
|
+
if (!oldest || entry.exitedAt < oldest.exitedAt)
|
|
126
|
+
oldest = entry;
|
|
127
|
+
}
|
|
128
|
+
if (!oldest)
|
|
129
|
+
return false;
|
|
130
|
+
return this.remove(oldest.sessionId);
|
|
131
|
+
}
|
|
132
|
+
scheduleEviction(entry) {
|
|
133
|
+
if (entry.evictionTimer)
|
|
134
|
+
return;
|
|
135
|
+
entry.evictionTimer = setTimeout(() => {
|
|
136
|
+
this.sessions.delete(entry.sessionId);
|
|
137
|
+
}, this.deps.exitTtlMs);
|
|
138
|
+
// Allow process exit even if evictions are pending — they are pure state,
|
|
139
|
+
// not work.
|
|
140
|
+
entry.evictionTimer.unref?.();
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Remove an entry immediately (used by shutdown sweeps and startup cleanup).
|
|
144
|
+
* Returns true if it was present.
|
|
145
|
+
*/
|
|
146
|
+
remove(sessionId) {
|
|
147
|
+
const entry = this.sessions.get(sessionId);
|
|
148
|
+
if (!entry)
|
|
149
|
+
return false;
|
|
150
|
+
if (entry.evictionTimer)
|
|
151
|
+
clearTimeout(entry.evictionTimer);
|
|
152
|
+
this.sessions.delete(sessionId);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/session/registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { RunEvent } from "../contract.js";
|
|
2
|
+
export interface BufferedEvent {
|
|
3
|
+
id: number;
|
|
4
|
+
event: RunEvent;
|
|
5
|
+
bytes: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Byte-capped FIFO ring buffer for SSE replay. Each entry tracks its
|
|
9
|
+
* serialized byte size; when a new entry would push total bytes past the cap,
|
|
10
|
+
* oldest entries are evicted until it fits. Entries are never split.
|
|
11
|
+
*
|
|
12
|
+
* Ids are assigned monotonically starting at 1 — never reused, never reset.
|
|
13
|
+
* A client's `Last-Event-ID: <n>` asks for events with id > n; if the oldest
|
|
14
|
+
* remaining id is still > n+1, the gap must be signaled separately.
|
|
15
|
+
*/
|
|
16
|
+
export declare class EventRingBuffer {
|
|
17
|
+
private readonly maxBytes;
|
|
18
|
+
private readonly entries;
|
|
19
|
+
private totalBytes;
|
|
20
|
+
private nextId;
|
|
21
|
+
constructor(maxBytes: number);
|
|
22
|
+
push(event: RunEvent): BufferedEvent;
|
|
23
|
+
private evict;
|
|
24
|
+
/**
|
|
25
|
+
* Returns entries with id > afterId, in insertion order.
|
|
26
|
+
* If `hasGap` is true, the caller should emit a `gap` marker before replay
|
|
27
|
+
* because events between afterId+1 and the first returned id have been evicted.
|
|
28
|
+
*/
|
|
29
|
+
replay(afterId: number): {
|
|
30
|
+
entries: BufferedEvent[];
|
|
31
|
+
hasGap: boolean;
|
|
32
|
+
};
|
|
33
|
+
get size(): number;
|
|
34
|
+
get bytes(): number;
|
|
35
|
+
get latestId(): number;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=ring-buffer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ring-buffer.d.ts","sourceRoot":"","sources":["../../src/session/ring-buffer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,QAAQ,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;GAQG;AACH,qBAAa,eAAe;IAKd,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAJrC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuB;IAC/C,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,MAAM,CAAK;gBAEU,QAAQ,EAAE,MAAM;IAM7C,IAAI,CAAC,KAAK,EAAE,QAAQ,GAAG,aAAa;IAUpC,OAAO,CAAC,KAAK;IAab;;;;OAIG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,aAAa,EAAE,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE;IAOtE,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,IAAI,QAAQ,IAAI,MAAM,CAErB;CACF"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Byte-capped FIFO ring buffer for SSE replay. Each entry tracks its
|
|
3
|
+
* serialized byte size; when a new entry would push total bytes past the cap,
|
|
4
|
+
* oldest entries are evicted until it fits. Entries are never split.
|
|
5
|
+
*
|
|
6
|
+
* Ids are assigned monotonically starting at 1 — never reused, never reset.
|
|
7
|
+
* A client's `Last-Event-ID: <n>` asks for events with id > n; if the oldest
|
|
8
|
+
* remaining id is still > n+1, the gap must be signaled separately.
|
|
9
|
+
*/
|
|
10
|
+
export class EventRingBuffer {
|
|
11
|
+
maxBytes;
|
|
12
|
+
entries = [];
|
|
13
|
+
totalBytes = 0;
|
|
14
|
+
nextId = 1;
|
|
15
|
+
constructor(maxBytes) {
|
|
16
|
+
this.maxBytes = maxBytes;
|
|
17
|
+
if (!Number.isInteger(maxBytes) || maxBytes <= 0) {
|
|
18
|
+
throw new Error(`EventRingBuffer maxBytes must be a positive integer, got ${maxBytes}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
push(event) {
|
|
22
|
+
const id = this.nextId++;
|
|
23
|
+
const bytes = Buffer.byteLength(JSON.stringify(event), "utf8");
|
|
24
|
+
const entry = { id, event, bytes };
|
|
25
|
+
this.entries.push(entry);
|
|
26
|
+
this.totalBytes += bytes;
|
|
27
|
+
this.evict();
|
|
28
|
+
return entry;
|
|
29
|
+
}
|
|
30
|
+
evict() {
|
|
31
|
+
// Invariant: always retain at least the most-recently-pushed entry, even
|
|
32
|
+
// if it alone exceeds maxBytes. A single oversized event (a huge log
|
|
33
|
+
// chunk) is still more useful to a reconnecting client than an empty
|
|
34
|
+
// buffer, and the alternative (drop-and-gap) would surface as a spurious
|
|
35
|
+
// "earlier output truncated" banner immediately after a big write.
|
|
36
|
+
while (this.totalBytes > this.maxBytes && this.entries.length > 1) {
|
|
37
|
+
const dropped = this.entries.shift();
|
|
38
|
+
if (!dropped)
|
|
39
|
+
break;
|
|
40
|
+
this.totalBytes -= dropped.bytes;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Returns entries with id > afterId, in insertion order.
|
|
45
|
+
* If `hasGap` is true, the caller should emit a `gap` marker before replay
|
|
46
|
+
* because events between afterId+1 and the first returned id have been evicted.
|
|
47
|
+
*/
|
|
48
|
+
replay(afterId) {
|
|
49
|
+
const entries = this.entries.filter((e) => e.id > afterId);
|
|
50
|
+
const oldestId = this.entries[0]?.id ?? this.nextId;
|
|
51
|
+
const hasGap = afterId + 1 < oldestId;
|
|
52
|
+
return { entries, hasGap };
|
|
53
|
+
}
|
|
54
|
+
get size() {
|
|
55
|
+
return this.entries.length;
|
|
56
|
+
}
|
|
57
|
+
get bytes() {
|
|
58
|
+
return this.totalBytes;
|
|
59
|
+
}
|
|
60
|
+
get latestId() {
|
|
61
|
+
return this.nextId - 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=ring-buffer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ring-buffer.js","sourceRoot":"","sources":["../../src/session/ring-buffer.ts"],"names":[],"mappings":"AAQA;;;;;;;;GAQG;AACH,MAAM,OAAO,eAAe;IAKG;IAJZ,OAAO,GAAoB,EAAE,CAAC;IACvC,UAAU,GAAG,CAAC,CAAC;IACf,MAAM,GAAG,CAAC,CAAC;IAEnB,YAA6B,QAAgB;QAAhB,aAAQ,GAAR,QAAQ,CAAQ;QAC3C,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,QAAQ,IAAI,CAAC,EAAE,CAAC;YACjD,MAAM,IAAI,KAAK,CAAC,4DAA4D,QAAQ,EAAE,CAAC,CAAC;QAC1F,CAAC;IACH,CAAC;IAED,IAAI,CAAC,KAAe;QAClB,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC;QAC/D,MAAM,KAAK,GAAkB,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;QAClD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzB,IAAI,CAAC,UAAU,IAAI,KAAK,CAAC;QACzB,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,OAAO,KAAK,CAAC;IACf,CAAC;IAEO,KAAK;QACX,yEAAyE;QACzE,qEAAqE;QACrE,qEAAqE;QACrE,yEAAyE;QACzE,mEAAmE;QACnE,OAAO,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAClE,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YACrC,IAAI,CAAC,OAAO;gBAAE,MAAM;YACpB,IAAI,CAAC,UAAU,IAAI,OAAO,CAAC,KAAK,CAAC;QACnC,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,OAAe;QACpB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC;QAC3D,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,IAAI,IAAI,CAAC,MAAM,CAAC;QACpD,MAAM,MAAM,GAAG,OAAO,GAAG,CAAC,GAAG,QAAQ,CAAC;QACtC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAC7B,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;IAC7B,CAAC;IAED,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IAED,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;IACzB,CAAC;CACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-id.d.ts","sourceRoot":"","sources":["../../src/session/session-id.ts"],"names":[],"mappings":"AAYA,8DAA8D;AAC9D,wBAAgB,iBAAiB,IAAI,MAAM,CAM1C"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
// Lowercase RFC 4648 base32 alphabet. Every character is valid in a DNS label
|
|
3
|
+
// and an RFC 1123 Kubernetes resource name, so the id drops straight into both
|
|
4
|
+
// `<id>.<domain>` session hostnames and `telo-run-<id>` container/pod names.
|
|
5
|
+
const ALPHABET = "abcdefghijklmnopqrstuvwxyz234567";
|
|
6
|
+
// 12 chars × 5 bits ≈ 60 bits of entropy. Collisions are negligible across the
|
|
7
|
+
// concurrently-active session set (there is no dedup check, so the id must be
|
|
8
|
+
// unique on its own) while staying far shorter than the previous UUID.
|
|
9
|
+
const SESSION_ID_LENGTH = 12;
|
|
10
|
+
/** Generates a short, DNS- and Kubernetes-safe session id. */
|
|
11
|
+
export function generateSessionId() {
|
|
12
|
+
const bytes = randomBytes(SESSION_ID_LENGTH);
|
|
13
|
+
let id = "";
|
|
14
|
+
// 256 is a multiple of 32, so `byte % 32` indexes the alphabet without bias.
|
|
15
|
+
for (let i = 0; i < SESSION_ID_LENGTH; i++)
|
|
16
|
+
id += ALPHABET[bytes[i] % 32];
|
|
17
|
+
return id;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=session-id.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-id.js","sourceRoot":"","sources":["../../src/session/session-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,8EAA8E;AAC9E,+EAA+E;AAC/E,6EAA6E;AAC7E,MAAM,QAAQ,GAAG,kCAAkC,CAAC;AAEpD,+EAA+E;AAC/E,8EAA8E;AAC9E,uEAAuE;AACvE,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAE7B,8DAA8D;AAC9D,MAAM,UAAU,iBAAiB;IAC/B,MAAM,KAAK,GAAG,WAAW,CAAC,iBAAiB,CAAC,CAAC;IAC7C,IAAI,EAAE,GAAG,EAAE,CAAC;IACZ,6EAA6E;IAC7E,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,iBAAiB,EAAE,CAAC,EAAE;QAAE,EAAE,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;IAC1E,OAAO,EAAE,CAAC;AACZ,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
2
|
+
import { type SessionRegistry } from "../session/registry.js";
|
|
3
|
+
export interface SseStreamArgs {
|
|
4
|
+
registry: SessionRegistry;
|
|
5
|
+
req: FastifyRequest;
|
|
6
|
+
reply: FastifyReply;
|
|
7
|
+
sessionId: string;
|
|
8
|
+
corsOrigins: string[] | "*";
|
|
9
|
+
}
|
|
10
|
+
export declare function streamSessionEvents(args: SseStreamArgs): Promise<void>;
|
|
11
|
+
//# sourceMappingURL=channel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../src/sse/channel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAG5D,OAAO,EAAuB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAKnF,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,eAAe,CAAC;IAC1B,GAAG,EAAE,cAAc,CAAC;IACpB,KAAK,EAAE,YAAY,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC;CAC7B;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAuF5E"}
|