@telorun/runner-core 0.8.2 → 0.9.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/README.md +33 -2
- package/dist/backend.d.ts +107 -23
- package/dist/backend.d.ts.map +1 -1
- package/dist/config.d.ts +27 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +12 -0
- package/dist/config.js.map +1 -1
- package/dist/contract.d.ts +199 -15
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +9 -0
- package/dist/contract.js.map +1 -1
- package/dist/debug/ports-resolved.d.ts +26 -0
- package/dist/debug/ports-resolved.d.ts.map +1 -0
- package/dist/debug/ports-resolved.js +42 -0
- package/dist/debug/ports-resolved.js.map +1 -0
- package/dist/debug/relay.d.ts.map +1 -1
- package/dist/debug/relay.js +34 -6
- package/dist/debug/relay.js.map +1 -1
- package/dist/debug/run-projection.d.ts +53 -0
- package/dist/debug/run-projection.d.ts.map +1 -0
- package/dist/debug/run-projection.js +115 -0
- package/dist/debug/run-projection.js.map +1 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/routes/apps.d.ts +1 -1
- package/dist/routes/apps.d.ts.map +1 -1
- package/dist/routes/apps.js +14 -4
- package/dist/routes/apps.js.map +1 -1
- package/dist/routes/io.d.ts.map +1 -1
- package/dist/routes/io.js +42 -9
- package/dist/routes/io.js.map +1 -1
- package/dist/routes/session-start.d.ts +12 -12
- package/dist/routes/session-start.d.ts.map +1 -1
- package/dist/routes/session-start.js +86 -13
- package/dist/routes/session-start.js.map +1 -1
- package/dist/routes/sessions.d.ts +11 -1
- package/dist/routes/sessions.d.ts.map +1 -1
- package/dist/routes/sessions.js +429 -4
- package/dist/routes/sessions.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +21 -1
- package/dist/server.js.map +1 -1
- package/dist/session/byte-ring-buffer.d.ts +6 -1
- package/dist/session/byte-ring-buffer.d.ts.map +1 -1
- package/dist/session/byte-ring-buffer.js +2 -2
- package/dist/session/byte-ring-buffer.js.map +1 -1
- package/dist/session/registry.d.ts +144 -13
- package/dist/session/registry.d.ts.map +1 -1
- package/dist/session/registry.js +166 -25
- package/dist/session/registry.js.map +1 -1
- package/dist/session/watch-supervisor.d.ts +42 -0
- package/dist/session/watch-supervisor.d.ts.map +1 -0
- package/dist/session/watch-supervisor.js +99 -0
- package/dist/session/watch-supervisor.js.map +1 -0
- package/dist/session/workspace-app.d.ts +4 -0
- package/dist/session/workspace-app.d.ts.map +1 -0
- package/dist/session/workspace-app.js +26 -0
- package/dist/session/workspace-app.js.map +1 -0
- package/dist/session/workspace-client.d.ts +33 -0
- package/dist/session/workspace-client.d.ts.map +1 -0
- package/dist/session/workspace-client.js +62 -0
- package/dist/session/workspace-client.js.map +1 -0
- package/dist/session/workspace-marker.d.ts +25 -0
- package/dist/session/workspace-marker.d.ts.map +1 -0
- package/dist/session/workspace-marker.js +35 -0
- package/dist/session/workspace-marker.js.map +1 -0
- package/dist/sse/channel.d.ts.map +1 -1
- package/dist/sse/channel.js +4 -0
- package/dist/sse/channel.js.map +1 -1
- package/package.json +4 -3
- package/src/backend.ts +111 -22
- package/src/config.ts +59 -0
- package/src/contract.ts +197 -15
- package/src/debug/ports-resolved.test.ts +66 -0
- package/src/debug/ports-resolved.ts +43 -0
- package/src/debug/relay.ts +32 -4
- package/src/debug/run-projection.test.ts +155 -0
- package/src/debug/run-projection.ts +122 -0
- package/src/index.ts +20 -1
- package/src/routes/apps.ts +14 -5
- package/src/routes/io.ts +58 -12
- package/src/routes/session-start.ts +106 -27
- package/src/routes/sessions.ts +498 -7
- package/src/server.ts +22 -1
- package/src/session/byte-ring-buffer.ts +8 -2
- package/src/session/registry.ts +288 -28
- package/src/session/ring-buffer.test.ts +4 -3
- package/src/session/watch-lifetime.test.ts +203 -0
- package/src/session/watch-supervisor.ts +112 -0
- package/src/session/workspace-app.ts +27 -0
- package/src/session/workspace-client.ts +80 -0
- package/src/session/workspace-marker.test.ts +35 -0
- package/src/session/workspace-marker.ts +39 -0
- package/src/sse/channel.ts +5 -0
- package/workspace-app/telo.yaml +228 -0
|
@@ -1,15 +1,38 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
2
|
+
import type { DebugFrame } from "@telorun/debug-wire";
|
|
3
|
+
import type { BackendSession, WorkloadLaunch } from "../backend.js";
|
|
4
|
+
import { type ByteStreamTag, type IoMode, type PortMapping, type RunEvent, type RunStatus, type RunTrigger, type SessionMode, type WorkspaceCheckpointFile } from "../contract.js";
|
|
4
5
|
import { ByteRingBuffer, type BufferedBytes } from "./byte-ring-buffer.js";
|
|
5
6
|
import { EventRingBuffer, type BufferedEvent } from "./ring-buffer.js";
|
|
7
|
+
/**
|
|
8
|
+
* One application's terminal and run bookkeeping. The byte channel is keyed
|
|
9
|
+
* `(session, app)` rather than labelled on a merged stream: each app container
|
|
10
|
+
* has its OWN terminal, and merging them would make `/io` unattachable to one.
|
|
11
|
+
*/
|
|
12
|
+
export interface AppChannel {
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly io: IoMode;
|
|
15
|
+
readonly byteBuffer: ByteRingBuffer;
|
|
16
|
+
readonly byteEmitter: EventEmitter;
|
|
17
|
+
/** Monotonic per app, starting at 1. Counted here from the kernel lifecycle
|
|
18
|
+
* events the debug stream already carries — the kernel is asked for nothing. */
|
|
19
|
+
generation: number;
|
|
20
|
+
/** Epoch ms the current generation started, for `durationMs` on completion.
|
|
21
|
+
* Null between generations. */
|
|
22
|
+
startedAt: number | null;
|
|
23
|
+
/** The port set this app currently declares. Re-read on every reload, so a
|
|
24
|
+
* `ports:` edit patches the Service and Ingress instead of silently binding
|
|
25
|
+
* a port nothing routes. */
|
|
26
|
+
ports: PortMapping[];
|
|
27
|
+
}
|
|
6
28
|
export interface SessionEntry {
|
|
7
29
|
readonly sessionId: string;
|
|
8
30
|
readonly createdAt: Date;
|
|
31
|
+
readonly mode: SessionMode;
|
|
9
32
|
readonly buffer: EventRingBuffer;
|
|
10
|
-
readonly byteBuffer: ByteRingBuffer;
|
|
11
33
|
readonly emitter: EventEmitter;
|
|
12
|
-
|
|
34
|
+
/** One channel per application container, insertion-ordered. */
|
|
35
|
+
readonly apps: Map<string, AppChannel>;
|
|
13
36
|
/** The live backend workload. Null until `start` resolves; the route writes
|
|
14
37
|
* stdin / resize / stop through it. A backend's `writeStdin` is a no-op once
|
|
15
38
|
* the workload has terminated, so callers need not null it on exit. */
|
|
@@ -18,16 +41,88 @@ export interface SessionEntry {
|
|
|
18
41
|
exitedAt: Date | null;
|
|
19
42
|
userStopped: boolean;
|
|
20
43
|
evictionTimer: NodeJS.Timeout | null;
|
|
44
|
+
/** Catalog name of the co-resident agent, when one was requested. */
|
|
45
|
+
agent?: string;
|
|
46
|
+
/** Last whole-tree workspace snapshot, pulled on the checkpoint timer and
|
|
47
|
+
* again on suspend. A CACHE, never the only copy: the editor holds the
|
|
48
|
+
* authoritative workspace, so a runner restart losing this costs an upload,
|
|
49
|
+
* not user work. */
|
|
50
|
+
checkpoint: WorkspaceCheckpoint | null;
|
|
51
|
+
/** How the NEXT generation of an app should be attributed. Structural rather
|
|
52
|
+
* than the projection type itself, so the registry stays free of the debug
|
|
53
|
+
* stream it knows nothing about. */
|
|
54
|
+
attribution: RunAttribution | null;
|
|
55
|
+
/** The description this session was launched from, retained so `resume` can
|
|
56
|
+
* build a fresh pod under the same id without a second copy of it. */
|
|
57
|
+
launch: WorkloadLaunch | null;
|
|
58
|
+
/** Live SSE/WS subscriber count. Zero for longer than the idle window is what
|
|
59
|
+
* suspends a watch session. */
|
|
60
|
+
subscribers: number;
|
|
61
|
+
/** Epoch ms the subscriber count last fell to zero; null while someone is
|
|
62
|
+
* attached. */
|
|
63
|
+
idleSince: number | null;
|
|
64
|
+
/** Epoch ms of recent reloads, for the per-session reload rate limit. A watch
|
|
65
|
+
* session reloads on every save, so an unbounded one is a way to keep a pod
|
|
66
|
+
* permanently rebuilding. */
|
|
67
|
+
reloads: number[];
|
|
68
|
+
/** Epoch ms the last workspace checkpoint was pulled. */
|
|
69
|
+
checkpointedAt: number | null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The run projection as the registry sees it — structural, so the registry stays
|
|
73
|
+
* free of the debug stream it knows nothing about. `expect` is how a route tells
|
|
74
|
+
* it about a generation it is ABOUT to cause: without that, a manual reload
|
|
75
|
+
* would be reported as a watch reload, and the runner would be guessing at the
|
|
76
|
+
* one fact it actually knows.
|
|
77
|
+
*/
|
|
78
|
+
export interface RunAttribution {
|
|
79
|
+
expect(app: string, trigger: RunTrigger): void;
|
|
80
|
+
expectAll(trigger: RunTrigger): void;
|
|
81
|
+
frame(app: string, frame: DebugFrame): void;
|
|
82
|
+
/** Close the open generation because its workload ended — a container that
|
|
83
|
+
* goes away emits no `Kernel.Stopped`, so the backend reports the ending. */
|
|
84
|
+
endGeneration(app: string, outcome: {
|
|
85
|
+
code?: number;
|
|
86
|
+
reason?: string;
|
|
87
|
+
}): void;
|
|
88
|
+
}
|
|
89
|
+
/** A whole-tree snapshot plus the contents needed to re-seed a fresh pod. Whole
|
|
90
|
+
* tree rather than a delta log from the write path: a manifest workspace is
|
|
91
|
+
* small, and one shape is easier to reason about than a replay that has to be
|
|
92
|
+
* correct. */
|
|
93
|
+
export interface WorkspaceCheckpoint {
|
|
94
|
+
takenAt: Date;
|
|
95
|
+
files: WorkspaceCheckpointFile[];
|
|
21
96
|
}
|
|
22
97
|
export interface RegistryDeps {
|
|
23
98
|
maxSessions: number;
|
|
24
99
|
exitTtlMs: number;
|
|
100
|
+
/** Per APP, not per session — each application container has its own terminal
|
|
101
|
+
* and its own replay buffer. */
|
|
25
102
|
replayBufferBytes: number;
|
|
103
|
+
/** How long a suspended session record is retained before eviction. Bounds
|
|
104
|
+
* accumulation, and is deliberately not the pod deadline: that bounds a POD,
|
|
105
|
+
* so on its own nothing would ever evict a suspended record. */
|
|
106
|
+
suspendedTtlMs?: number;
|
|
26
107
|
}
|
|
27
108
|
export declare class SessionLimitError extends Error {
|
|
28
109
|
}
|
|
29
110
|
export declare class SessionEvictedError extends Error {
|
|
30
111
|
}
|
|
112
|
+
export declare class UnknownAppError extends Error {
|
|
113
|
+
}
|
|
114
|
+
export interface RegisterArgs {
|
|
115
|
+
sessionId: string;
|
|
116
|
+
mode?: SessionMode;
|
|
117
|
+
agent?: string;
|
|
118
|
+
/** The applications this session runs. Defaults to one app named `app` with a
|
|
119
|
+
* terminal, so a single-app session behaves exactly as before. */
|
|
120
|
+
apps?: Array<{
|
|
121
|
+
name: string;
|
|
122
|
+
io?: IoMode;
|
|
123
|
+
ports?: PortMapping[];
|
|
124
|
+
}>;
|
|
125
|
+
}
|
|
31
126
|
export declare class SessionRegistry {
|
|
32
127
|
private readonly deps;
|
|
33
128
|
private readonly sessions;
|
|
@@ -40,18 +135,54 @@ export declare class SessionRegistry {
|
|
|
40
135
|
* Creates a fresh registry entry. Callers are responsible for guarding against
|
|
41
136
|
* duplicate insertion. Throws SessionLimitError if we're at capacity.
|
|
42
137
|
*/
|
|
43
|
-
register(args:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
138
|
+
register(args: RegisterArgs): SessionEntry;
|
|
139
|
+
/** Add an application channel to an existing entry. Used at register time and
|
|
140
|
+
* when `PUT /v1/sessions/:id/apps` changes the running set. */
|
|
141
|
+
addApp(entry: SessionEntry, app: {
|
|
142
|
+
name: string;
|
|
143
|
+
io?: IoMode;
|
|
144
|
+
ports?: PortMapping[];
|
|
145
|
+
}): AppChannel;
|
|
146
|
+
/** The single app of a single-app session — what `/io` and `reload` fall back
|
|
147
|
+
* to when no `app` is given. Undefined when the session runs several, where
|
|
148
|
+
* there is no defensible default among many terminals. */
|
|
149
|
+
soleApp(entry: SessionEntry): AppChannel | undefined;
|
|
150
|
+
pushBytes(sessionId: string, appName: string, bytes: Buffer, stream?: ByteStreamTag): BufferedBytes | undefined;
|
|
151
|
+
subscribeBytes(sessionId: string, appName: string, listener: (b: BufferedBytes) => void): () => void;
|
|
48
152
|
emit(sessionId: string, event: RunEvent): BufferedEvent | undefined;
|
|
153
|
+
/** Begin a generation for one app and emit its `run.started`. Returns the new
|
|
154
|
+
* generation number, or undefined when the app is unknown. */
|
|
155
|
+
startGeneration(sessionId: string, appName: string, trigger: "initial" | "watch" | "manual" | "resume"): number | undefined;
|
|
156
|
+
/** Close the current generation of one app. A completion leaves the SESSION
|
|
157
|
+
* status untouched — that is the whole point of the split. */
|
|
158
|
+
finishGeneration(sessionId: string, appName: string, outcome: {
|
|
159
|
+
phase: "completed";
|
|
160
|
+
code: number;
|
|
161
|
+
} | {
|
|
162
|
+
phase: "failed";
|
|
163
|
+
reason: string;
|
|
164
|
+
}): void;
|
|
49
165
|
subscribe(sessionId: string, listener: (e: BufferedEvent) => void): () => void;
|
|
50
|
-
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
|
|
166
|
+
/** Count a live client. Idleness — no SSE/WS subscriber for the configured
|
|
167
|
+
* window — is what suspends a watch session, and aggressive reaping is what
|
|
168
|
+
* makes per-visitor watch sessions affordable; they only work as a pair. */
|
|
169
|
+
addSubscriber(sessionId: string): () => void;
|
|
170
|
+
/**
|
|
171
|
+
* Free a slot at capacity by removing the oldest RECLAIMABLE session: a
|
|
172
|
+
* terminated one (history kept for re-attach, which yields to a new run) or a
|
|
173
|
+
* SUSPENDED one.
|
|
174
|
+
*
|
|
175
|
+
* A suspended session is reclaimable because losing it costs exactly what the
|
|
176
|
+
* design already says a runner restart costs — a checkpoint the editor
|
|
177
|
+
* re-seeds from its own copy. Excluding it would let a handful of visitors who
|
|
178
|
+
* each left after five minutes hold every slot for the whole suspended TTL,
|
|
179
|
+
* which with the shipped defaults is a day.
|
|
180
|
+
*
|
|
181
|
+
* Ordered by when the session stopped being live, so the least recently
|
|
182
|
+
* abandoned goes first. Returns false when every session is still live.
|
|
183
|
+
*/
|
|
54
184
|
private evictOldestTerminal;
|
|
185
|
+
private cancelEviction;
|
|
55
186
|
private scheduleEviction;
|
|
56
187
|
/**
|
|
57
188
|
* Remove an entry immediately (used by shutdown sweeps and startup cleanup).
|
|
@@ -1 +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;
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/session/registry.ts"],"names":[],"mappings":"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"}
|
package/dist/session/registry.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
-
import { isTerminal } from "../contract.js";
|
|
2
|
+
import { DEFAULT_APP_NAME, isTerminal, } from "../contract.js";
|
|
3
3
|
import { ByteRingBuffer } from "./byte-ring-buffer.js";
|
|
4
4
|
import { EventRingBuffer } from "./ring-buffer.js";
|
|
5
5
|
const EVENT_EMITTED = "event";
|
|
@@ -15,6 +15,8 @@ export class SessionLimitError extends Error {
|
|
|
15
15
|
}
|
|
16
16
|
export class SessionEvictedError extends Error {
|
|
17
17
|
}
|
|
18
|
+
export class UnknownAppError extends Error {
|
|
19
|
+
}
|
|
18
20
|
export class SessionRegistry {
|
|
19
21
|
deps;
|
|
20
22
|
sessions = new Map();
|
|
@@ -44,32 +46,64 @@ export class SessionRegistry {
|
|
|
44
46
|
const entry = {
|
|
45
47
|
sessionId: args.sessionId,
|
|
46
48
|
createdAt: new Date(),
|
|
49
|
+
mode: args.mode ?? "run",
|
|
47
50
|
buffer: new EventRingBuffer(this.deps.replayBufferBytes),
|
|
48
|
-
byteBuffer: new ByteRingBuffer(this.deps.replayBufferBytes),
|
|
49
51
|
emitter: new EventEmitter(),
|
|
50
|
-
|
|
52
|
+
apps: new Map(),
|
|
51
53
|
session: null,
|
|
52
54
|
status: { kind: "starting" },
|
|
53
55
|
exitedAt: null,
|
|
54
56
|
userStopped: false,
|
|
55
57
|
evictionTimer: null,
|
|
58
|
+
agent: args.agent,
|
|
59
|
+
checkpoint: null,
|
|
60
|
+
attribution: null,
|
|
61
|
+
launch: null,
|
|
62
|
+
subscribers: 0,
|
|
63
|
+
idleSince: Date.now(),
|
|
64
|
+
reloads: [],
|
|
65
|
+
checkpointedAt: null,
|
|
56
66
|
};
|
|
57
67
|
// Many transient SSE / WS subscribers per session is normal — bump the
|
|
58
68
|
// default 10-listener warning to a high cap so the alarm still fires
|
|
59
69
|
// for a real listener leak. 256 is well above the realistic concurrent-
|
|
60
70
|
// tab count and well below "obviously a bug".
|
|
61
71
|
entry.emitter.setMaxListeners(256);
|
|
62
|
-
|
|
72
|
+
for (const app of args.apps ?? [{ name: DEFAULT_APP_NAME }]) {
|
|
73
|
+
this.addApp(entry, app);
|
|
74
|
+
}
|
|
63
75
|
this.sessions.set(args.sessionId, entry);
|
|
64
76
|
return entry;
|
|
65
77
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
78
|
+
/** Add an application channel to an existing entry. Used at register time and
|
|
79
|
+
* when `PUT /v1/sessions/:id/apps` changes the running set. */
|
|
80
|
+
addApp(entry, app) {
|
|
81
|
+
const channel = {
|
|
82
|
+
name: app.name,
|
|
83
|
+
io: app.io ?? "tty",
|
|
84
|
+
byteBuffer: new ByteRingBuffer(this.deps.replayBufferBytes),
|
|
85
|
+
byteEmitter: new EventEmitter(),
|
|
86
|
+
generation: 0,
|
|
87
|
+
startedAt: null,
|
|
88
|
+
ports: app.ports ?? [],
|
|
89
|
+
};
|
|
90
|
+
channel.byteEmitter.setMaxListeners(256);
|
|
91
|
+
entry.apps.set(app.name, channel);
|
|
92
|
+
return channel;
|
|
93
|
+
}
|
|
94
|
+
/** The single app of a single-app session — what `/io` and `reload` fall back
|
|
95
|
+
* to when no `app` is given. Undefined when the session runs several, where
|
|
96
|
+
* there is no defensible default among many terminals. */
|
|
97
|
+
soleApp(entry) {
|
|
98
|
+
return entry.apps.size === 1 ? entry.apps.values().next().value : undefined;
|
|
99
|
+
}
|
|
100
|
+
pushBytes(sessionId, appName, bytes, stream = "tty") {
|
|
101
|
+
const channel = this.sessions.get(sessionId)?.apps.get(appName);
|
|
102
|
+
if (!channel)
|
|
69
103
|
return undefined;
|
|
70
104
|
if (bytes.byteLength <= MAX_PUSH_CHUNK) {
|
|
71
|
-
const buffered =
|
|
72
|
-
|
|
105
|
+
const buffered = channel.byteBuffer.push(bytes, stream);
|
|
106
|
+
channel.byteEmitter.emit(BYTES_EMITTED, buffered);
|
|
73
107
|
return buffered;
|
|
74
108
|
}
|
|
75
109
|
// Split the chunk into MAX_PUSH_CHUNK-sized slices, each getting its
|
|
@@ -79,17 +113,21 @@ export class SessionRegistry {
|
|
|
79
113
|
const slice = bytes.subarray(off, Math.min(off + MAX_PUSH_CHUNK, bytes.byteLength));
|
|
80
114
|
// subarray shares memory with the parent buffer; copy so the ring's
|
|
81
115
|
// entry doesn't pin the original allocation past eviction.
|
|
82
|
-
last =
|
|
83
|
-
|
|
116
|
+
last = channel.byteBuffer.push(Buffer.from(slice), stream);
|
|
117
|
+
channel.byteEmitter.emit(BYTES_EMITTED, last);
|
|
84
118
|
}
|
|
85
119
|
return last;
|
|
86
120
|
}
|
|
87
|
-
subscribeBytes(sessionId, listener) {
|
|
121
|
+
subscribeBytes(sessionId, appName, listener) {
|
|
88
122
|
const entry = this.sessions.get(sessionId);
|
|
89
123
|
if (!entry)
|
|
90
124
|
throw new SessionEvictedError(`session '${sessionId}' not in registry`);
|
|
91
|
-
entry.
|
|
92
|
-
|
|
125
|
+
const channel = entry.apps.get(appName);
|
|
126
|
+
if (!channel) {
|
|
127
|
+
throw new UnknownAppError(`session '${sessionId}' runs no app named '${appName}'`);
|
|
128
|
+
}
|
|
129
|
+
channel.byteEmitter.on(BYTES_EMITTED, listener);
|
|
130
|
+
return () => channel.byteEmitter.off(BYTES_EMITTED, listener);
|
|
93
131
|
}
|
|
94
132
|
emit(sessionId, event) {
|
|
95
133
|
const entry = this.sessions.get(sessionId);
|
|
@@ -100,12 +138,65 @@ export class SessionRegistry {
|
|
|
100
138
|
entry.status = event.status;
|
|
101
139
|
if (isTerminal(event.status)) {
|
|
102
140
|
entry.exitedAt = new Date();
|
|
103
|
-
this.scheduleEviction(entry);
|
|
141
|
+
this.scheduleEviction(entry, this.deps.exitTtlMs);
|
|
142
|
+
}
|
|
143
|
+
else if (event.status.kind === "suspended") {
|
|
144
|
+
// Not terminal — the record and its checkpoint outlive the pod — but it
|
|
145
|
+
// still needs its own ceiling, or suspended records accumulate forever.
|
|
146
|
+
this.scheduleEviction(entry, this.deps.suspendedTtlMs);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
this.cancelEviction(entry);
|
|
104
150
|
}
|
|
105
151
|
}
|
|
106
152
|
entry.emitter.emit(EVENT_EMITTED, buffered);
|
|
107
153
|
return buffered;
|
|
108
154
|
}
|
|
155
|
+
/** Begin a generation for one app and emit its `run.started`. Returns the new
|
|
156
|
+
* generation number, or undefined when the app is unknown. */
|
|
157
|
+
startGeneration(sessionId, appName, trigger) {
|
|
158
|
+
const channel = this.sessions.get(sessionId)?.apps.get(appName);
|
|
159
|
+
if (!channel)
|
|
160
|
+
return undefined;
|
|
161
|
+
channel.generation += 1;
|
|
162
|
+
channel.startedAt = Date.now();
|
|
163
|
+
this.emit(sessionId, {
|
|
164
|
+
type: "run",
|
|
165
|
+
app: appName,
|
|
166
|
+
generation: channel.generation,
|
|
167
|
+
phase: "started",
|
|
168
|
+
trigger,
|
|
169
|
+
});
|
|
170
|
+
return channel.generation;
|
|
171
|
+
}
|
|
172
|
+
/** Close the current generation of one app. A completion leaves the SESSION
|
|
173
|
+
* status untouched — that is the whole point of the split. */
|
|
174
|
+
finishGeneration(sessionId, appName, outcome) {
|
|
175
|
+
const channel = this.sessions.get(sessionId)?.apps.get(appName);
|
|
176
|
+
if (!channel || channel.generation === 0)
|
|
177
|
+
return;
|
|
178
|
+
const durationMs = channel.startedAt === null ? undefined : Date.now() - channel.startedAt;
|
|
179
|
+
channel.startedAt = null;
|
|
180
|
+
if (outcome.phase === "completed") {
|
|
181
|
+
this.emit(sessionId, {
|
|
182
|
+
type: "run",
|
|
183
|
+
app: appName,
|
|
184
|
+
generation: channel.generation,
|
|
185
|
+
phase: "completed",
|
|
186
|
+
code: outcome.code,
|
|
187
|
+
durationMs,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
this.emit(sessionId, {
|
|
192
|
+
type: "run",
|
|
193
|
+
app: appName,
|
|
194
|
+
generation: channel.generation,
|
|
195
|
+
phase: "failed",
|
|
196
|
+
reason: outcome.reason,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
109
200
|
subscribe(sessionId, listener) {
|
|
110
201
|
const entry = this.sessions.get(sessionId);
|
|
111
202
|
if (!entry)
|
|
@@ -113,28 +204,68 @@ export class SessionRegistry {
|
|
|
113
204
|
entry.emitter.on(EVENT_EMITTED, listener);
|
|
114
205
|
return () => entry.emitter.off(EVENT_EMITTED, listener);
|
|
115
206
|
}
|
|
116
|
-
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
|
|
207
|
+
/** Count a live client. Idleness — no SSE/WS subscriber for the configured
|
|
208
|
+
* window — is what suspends a watch session, and aggressive reaping is what
|
|
209
|
+
* makes per-visitor watch sessions affordable; they only work as a pair. */
|
|
210
|
+
addSubscriber(sessionId) {
|
|
211
|
+
const entry = this.sessions.get(sessionId);
|
|
212
|
+
if (!entry)
|
|
213
|
+
return () => { };
|
|
214
|
+
entry.subscribers += 1;
|
|
215
|
+
entry.idleSince = null;
|
|
216
|
+
let released = false;
|
|
217
|
+
return () => {
|
|
218
|
+
if (released)
|
|
219
|
+
return;
|
|
220
|
+
released = true;
|
|
221
|
+
entry.subscribers = Math.max(0, entry.subscribers - 1);
|
|
222
|
+
if (entry.subscribers === 0)
|
|
223
|
+
entry.idleSince = Date.now();
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Free a slot at capacity by removing the oldest RECLAIMABLE session: a
|
|
228
|
+
* terminated one (history kept for re-attach, which yields to a new run) or a
|
|
229
|
+
* SUSPENDED one.
|
|
230
|
+
*
|
|
231
|
+
* A suspended session is reclaimable because losing it costs exactly what the
|
|
232
|
+
* design already says a runner restart costs — a checkpoint the editor
|
|
233
|
+
* re-seeds from its own copy. Excluding it would let a handful of visitors who
|
|
234
|
+
* each left after five minutes hold every slot for the whole suspended TTL,
|
|
235
|
+
* which with the shipped defaults is a day.
|
|
236
|
+
*
|
|
237
|
+
* Ordered by when the session stopped being live, so the least recently
|
|
238
|
+
* abandoned goes first. Returns false when every session is still live.
|
|
239
|
+
*/
|
|
120
240
|
evictOldestTerminal() {
|
|
121
241
|
let oldest;
|
|
242
|
+
let oldestAt = Number.POSITIVE_INFINITY;
|
|
122
243
|
for (const entry of this.sessions.values()) {
|
|
123
|
-
|
|
244
|
+
const since = reclaimableSince(entry);
|
|
245
|
+
if (since === null || since >= oldestAt)
|
|
124
246
|
continue;
|
|
125
|
-
|
|
126
|
-
|
|
247
|
+
oldest = entry;
|
|
248
|
+
oldestAt = since;
|
|
127
249
|
}
|
|
128
250
|
if (!oldest)
|
|
129
251
|
return false;
|
|
130
252
|
return this.remove(oldest.sessionId);
|
|
131
253
|
}
|
|
132
|
-
|
|
133
|
-
if (entry.evictionTimer)
|
|
254
|
+
cancelEviction(entry) {
|
|
255
|
+
if (!entry.evictionTimer)
|
|
134
256
|
return;
|
|
257
|
+
clearTimeout(entry.evictionTimer);
|
|
258
|
+
entry.evictionTimer = null;
|
|
259
|
+
}
|
|
260
|
+
scheduleEviction(entry, ttlMs) {
|
|
261
|
+
if (ttlMs === undefined)
|
|
262
|
+
return;
|
|
263
|
+
// A resume re-arms from `running`, so an already-armed timer is replaced
|
|
264
|
+
// rather than kept — the surviving one would evict a live session.
|
|
265
|
+
this.cancelEviction(entry);
|
|
135
266
|
entry.evictionTimer = setTimeout(() => {
|
|
136
267
|
this.sessions.delete(entry.sessionId);
|
|
137
|
-
},
|
|
268
|
+
}, ttlMs);
|
|
138
269
|
// Allow process exit even if evictions are pending — they are pure state,
|
|
139
270
|
// not work.
|
|
140
271
|
entry.evictionTimer.unref?.();
|
|
@@ -153,4 +284,14 @@ export class SessionRegistry {
|
|
|
153
284
|
return true;
|
|
154
285
|
}
|
|
155
286
|
}
|
|
287
|
+
/** When a session stopped being live, or null while it still is. A terminated
|
|
288
|
+
* session is reclaimable from its exit; a suspended one from when it went idle
|
|
289
|
+
* (it has no pod and its workspace is a checkpoint the editor can re-seed). */
|
|
290
|
+
function reclaimableSince(entry) {
|
|
291
|
+
if (entry.exitedAt !== null)
|
|
292
|
+
return entry.exitedAt.getTime();
|
|
293
|
+
if (entry.status.kind === "suspended")
|
|
294
|
+
return entry.idleSince ?? 0;
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
156
297
|
//# sourceMappingURL=registry.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/session/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/session/registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { SessionRegistry } from "./registry.js";
|
|
2
|
+
export interface WatchSupervisorDeps {
|
|
3
|
+
registry: SessionRegistry;
|
|
4
|
+
/** No SSE/WS subscriber for this long → suspend. */
|
|
5
|
+
idleMs: number;
|
|
6
|
+
/** How often to pull a whole-tree workspace snapshot. */
|
|
7
|
+
checkpointMs: number;
|
|
8
|
+
log?: {
|
|
9
|
+
warn(obj: unknown, msg: string): void;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The two background jobs that make a watch session affordable, and they only
|
|
14
|
+
* work as a pair: a periodic workspace checkpoint, and an idle reap that deletes
|
|
15
|
+
* the pod while keeping the session record and that checkpoint.
|
|
16
|
+
*
|
|
17
|
+
* Aggressive reaping is what makes per-visitor watch sessions affordable, and
|
|
18
|
+
* the checkpoint is what makes aggressive reaping safe to do.
|
|
19
|
+
*
|
|
20
|
+
* The checkpoint is a CACHE, never the only copy. The runner is a single replica
|
|
21
|
+
* with an in-memory registry, so a redeploy or crash drops every suspended
|
|
22
|
+
* session — the editor holds the authoritative workspace, already diffs its own
|
|
23
|
+
* files against `GET /workspace`, and on a `404` at resume creates a new session
|
|
24
|
+
* and re-seeds from its own copy in one change set. What the checkpoint saves is
|
|
25
|
+
* that upload.
|
|
26
|
+
*/
|
|
27
|
+
export declare class WatchSupervisor {
|
|
28
|
+
private readonly deps;
|
|
29
|
+
private timer;
|
|
30
|
+
/** Sessions with a checkpoint or suspend in flight, so a slow snapshot cannot
|
|
31
|
+
* have a second one started on top of it every tick. */
|
|
32
|
+
private readonly busy;
|
|
33
|
+
constructor(deps: WatchSupervisorDeps);
|
|
34
|
+
start(): void;
|
|
35
|
+
stop(): void;
|
|
36
|
+
/** Exposed for tests, which drive the clock rather than waiting on it. */
|
|
37
|
+
tick(): Promise<void>;
|
|
38
|
+
private run;
|
|
39
|
+
private checkpoint;
|
|
40
|
+
private suspend;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=watch-supervisor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch-supervisor.d.ts","sourceRoot":"","sources":["../../src/session/watch-supervisor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAgB,eAAe,EAAE,MAAM,eAAe,CAAC;AAEnE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,eAAe,CAAC;IAC1B,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,YAAY,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE;QAAE,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;CACjD;AAOD;;;;;;;;;;;;;;GAcG;AACH,qBAAa,eAAe;IAMd,OAAO,CAAC,QAAQ,CAAC,IAAI;IALjC,OAAO,CAAC,KAAK,CAA+B;IAC5C;6DACyD;IACzD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAqB;gBAEb,IAAI,EAAE,mBAAmB;IAEtD,KAAK,IAAI,IAAI;IAMb,IAAI,IAAI,IAAI;IAMZ,0EAA0E;IACpE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAuBb,GAAG;YAiBH,UAAU;YAQV,OAAO;CAWtB"}
|