@vibecook/ghosttea-react 0.10.1 → 0.11.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.
Files changed (40) hide show
  1. package/README.md +67 -0
  2. package/dist/TerminalSurface.d.ts +5 -0
  3. package/dist/TerminalSurface.d.ts.map +1 -1
  4. package/dist/TerminalSurface.js +19 -4
  5. package/dist/TerminalSurface.js.map +1 -1
  6. package/dist/index.d.ts +4 -3
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/performance.d.ts +31 -0
  11. package/dist/performance.d.ts.map +1 -1
  12. package/dist/performance.js.map +1 -1
  13. package/dist/routed-activation.d.ts +110 -0
  14. package/dist/routed-activation.d.ts.map +1 -0
  15. package/dist/routed-activation.js +287 -0
  16. package/dist/routed-activation.js.map +1 -0
  17. package/dist/routed-control.d.ts +79 -0
  18. package/dist/routed-control.d.ts.map +1 -0
  19. package/dist/routed-control.js +397 -0
  20. package/dist/routed-control.js.map +1 -0
  21. package/dist/routed-frames.d.ts +69 -0
  22. package/dist/routed-frames.d.ts.map +1 -0
  23. package/dist/routed-frames.js +660 -0
  24. package/dist/routed-frames.js.map +1 -0
  25. package/dist/runtime.d.ts +96 -4
  26. package/dist/runtime.d.ts.map +1 -1
  27. package/dist/runtime.js +1227 -100
  28. package/dist/runtime.js.map +1 -1
  29. package/dist/terminal-render.worker.js +1089 -45
  30. package/dist/terminal-render.worker.js.map +3 -3
  31. package/dist/worker-messages.d.ts +18 -1
  32. package/dist/worker-messages.d.ts.map +1 -1
  33. package/dist/workspace/Workspace.d.ts +12 -1
  34. package/dist/workspace/Workspace.d.ts.map +1 -1
  35. package/dist/workspace/Workspace.js +77 -14
  36. package/dist/workspace/Workspace.js.map +1 -1
  37. package/dist/workspace/index.d.ts +1 -1
  38. package/dist/workspace/index.d.ts.map +1 -1
  39. package/dist/workspace/index.js.map +1 -1
  40. package/package.json +4 -4
@@ -0,0 +1,660 @@
1
+ import { DEFAULT_ROUTED_RECEIVER_CAPACITIES, ROUTED_CLOSE_CODES, ROUTED_DOOR_LIMITS, ROUTED_LEG_OUTBOUND, ROUTED_PROTOCOL_VERSION, compareRoutedSceneContent, decodeRoutedMessage, decodeRoutedPresentationEnvelope, encodeRoutedMessage, routedCrc32c, } from "@vibecook/ghosttea-protocol";
2
+ const socketOpen = 1;
3
+ const socketClosing = 2;
4
+ function exactStamp(left, right) {
5
+ return left !== undefined && compareRoutedSceneContent(left, right) === 0;
6
+ }
7
+ function copyArrayBuffer(bytes) {
8
+ const copy = bytes.slice();
9
+ return copy.buffer;
10
+ }
11
+ function capacitiesFitWithin(accepted, advertised) {
12
+ return (accepted.connectionCreditBytes <= advertised.connectionCreditBytes &&
13
+ accepted.perActivationCreditBytes <= advertised.perActivationCreditBytes &&
14
+ accepted.stagingBytesPerSession <= advertised.stagingBytesPerSession &&
15
+ accepted.stagingBytesTotal <= advertised.stagingBytesTotal &&
16
+ accepted.maxConcurrentActivations <= advertised.maxConcurrentActivations &&
17
+ accepted.maxConcurrentSeeds <= advertised.maxConcurrentSeeds);
18
+ }
19
+ /** Worker-owned T1 frames connection pool. It never posts presentation bytes to main. */
20
+ export class RoutedFramesTransport {
21
+ #socketFactory;
22
+ #applyFrame;
23
+ #emit;
24
+ #creditReturned;
25
+ #connections = new Map();
26
+ #activations = new Map();
27
+ #stagingBytes = 0;
28
+ #disposed = false;
29
+ constructor(options) {
30
+ this.#socketFactory = options.socketFactory ?? ((url) => new WebSocket(url));
31
+ this.#applyFrame = options.applyFrame;
32
+ this.#emit = options.emit;
33
+ this.#creditReturned = options.creditReturned;
34
+ }
35
+ attach(request) {
36
+ if (this.#disposed)
37
+ return;
38
+ const claims = request.attachGrant.claims;
39
+ if (claims.sessionId.length === 0 ||
40
+ request.activationId.length === 0 ||
41
+ !/^(0|[1-9][0-9]*)$/.test(request.sessionHandle) ||
42
+ claims.audienceCellBootId !== request.cellBootId ||
43
+ request.transportGrant.claims.audienceCellBootId !== request.cellBootId) {
44
+ throw new Error("Routed frames attach grant does not match the requested cell");
45
+ }
46
+ const previous = this.#activations.get(request.activationId);
47
+ if (previous?.appliedContent !== undefined &&
48
+ request.resume !== undefined &&
49
+ !exactStamp(previous.appliedContent, request.resume.from)) {
50
+ throw new Error("Routed frames resume does not name the worker's applied scene");
51
+ }
52
+ const appliedContent = request.resume?.from ?? previous?.appliedContent;
53
+ const lastActivationSequence = previous?.lastActivationSequence ?? -1;
54
+ const workerStatusSequence = previous?.workerStatusSequence ?? 0;
55
+ if (previous)
56
+ this.detach(request.activationId);
57
+ const activation = {
58
+ request,
59
+ sessionId: claims.sessionId,
60
+ leaseEpoch: claims.leaseEpoch ?? 0,
61
+ ...(appliedContent === undefined ? {} : { appliedContent }),
62
+ lastActivationSequence,
63
+ workerStatusSequence,
64
+ };
65
+ this.#activations.set(request.activationId, activation);
66
+ this.#emit({ type: "frames-state", activationId: request.activationId, state: "attaching" });
67
+ const connection = this.#connection(request);
68
+ connection.activationIds.add(request.activationId);
69
+ if (connection.accepted)
70
+ this.#sendAttach(connection, activation);
71
+ }
72
+ detach(activationId) {
73
+ const activation = this.#activations.get(activationId);
74
+ if (!activation)
75
+ return;
76
+ this.#releaseTransfer(activation);
77
+ if (activation.sceneRefreshTimer !== undefined)
78
+ clearTimeout(activation.sceneRefreshTimer);
79
+ if (activation.presentationRefreshTimer !== undefined)
80
+ clearTimeout(activation.presentationRefreshTimer);
81
+ this.#activations.delete(activationId);
82
+ const connection = this.#connections.get(activation.request.cellBootId);
83
+ if (connection) {
84
+ connection.activationIds.delete(activationId);
85
+ this.#scheduleAccountDrain(connection, activationId);
86
+ }
87
+ }
88
+ dispose() {
89
+ if (this.#disposed)
90
+ return;
91
+ this.#disposed = true;
92
+ for (const activationId of [...this.#activations.keys()])
93
+ this.detach(activationId);
94
+ for (const connection of this.#connections.values()) {
95
+ if (connection.heartbeatTimer !== undefined)
96
+ clearTimeout(connection.heartbeatTimer);
97
+ if (connection.heartbeatDeadlineTimer !== undefined)
98
+ clearTimeout(connection.heartbeatDeadlineTimer);
99
+ if (connection.creditTimer !== undefined)
100
+ clearTimeout(connection.creditTimer);
101
+ for (const timer of connection.accountDrainTimers.values())
102
+ clearTimeout(timer);
103
+ if (connection.socket.readyState < socketClosing)
104
+ connection.socket.close(1000, "runtime-destroyed");
105
+ }
106
+ this.#connections.clear();
107
+ }
108
+ #connection(request) {
109
+ const existing = this.#connections.get(request.cellBootId);
110
+ if (existing &&
111
+ existing.framesUrl === request.framesUrl &&
112
+ existing.transportGrant.claims.connectionSetId === request.transportGrant.claims.connectionSetId &&
113
+ existing.socket.readyState < socketClosing) {
114
+ return existing;
115
+ }
116
+ if (existing)
117
+ this.#closeConnection(existing, 1000, "connection-replaced", true);
118
+ const socket = this.#socketFactory(request.framesUrl);
119
+ socket.binaryType = "arraybuffer";
120
+ const connection = {
121
+ cellBootId: request.cellBootId,
122
+ framesUrl: request.framesUrl,
123
+ transportGrant: request.transportGrant,
124
+ receiverCapacities: request.receiverCapacities ?? DEFAULT_ROUTED_RECEIVER_CAPACITIES,
125
+ capabilities: request.capabilities ?? ["resume"],
126
+ socket,
127
+ activationIds: new Set(),
128
+ heartbeatSequence: 0,
129
+ heartbeatAckSequence: 0,
130
+ creditSequence: 0,
131
+ connectionBytesReturned: 0,
132
+ connectionBytesReported: 0,
133
+ accountBytesReturned: new Map(),
134
+ accountDrainTimers: new Map(),
135
+ };
136
+ this.#connections.set(request.cellBootId, connection);
137
+ socket.addEventListener("open", () => this.#hello(connection));
138
+ socket.addEventListener("message", (event) => void this.#message(connection, event));
139
+ socket.addEventListener("close", (event) => this.#closed(connection, event.code, event.reason));
140
+ socket.addEventListener("error", () => {
141
+ // The close event carries the protocol-classifying code. Browsers expose
142
+ // no useful error detail here and logging a grant-bearing request is forbidden.
143
+ });
144
+ return connection;
145
+ }
146
+ #hello(connection) {
147
+ this.#send(connection, encodeRoutedMessage("ConnectionHello", {
148
+ protocolMajor: ROUTED_PROTOCOL_VERSION.major,
149
+ protocolMinor: ROUTED_PROTOCOL_VERSION.minor,
150
+ channel: "frames",
151
+ transportGrant: connection.transportGrant,
152
+ receiverCapacities: connection.receiverCapacities,
153
+ capabilities: connection.capabilities,
154
+ }));
155
+ }
156
+ async #message(connection, event) {
157
+ if (this.#connections.get(connection.cellBootId) !== connection)
158
+ return;
159
+ if (typeof event.data === "string") {
160
+ this.#text(connection, event.data);
161
+ return;
162
+ }
163
+ let bytes;
164
+ if (event.data instanceof ArrayBuffer)
165
+ bytes = new Uint8Array(event.data);
166
+ else if (ArrayBuffer.isView(event.data)) {
167
+ bytes = new Uint8Array(event.data.buffer, event.data.byteOffset, event.data.byteLength);
168
+ }
169
+ else if (event.data instanceof Blob) {
170
+ bytes = new Uint8Array(await event.data.arrayBuffer());
171
+ }
172
+ else {
173
+ this.#protocolFailure(connection, undefined, 0, "unsupported binary message");
174
+ return;
175
+ }
176
+ this.#binary(connection, bytes);
177
+ }
178
+ #text(connection, text) {
179
+ const maxBytes = connection.accepted?.protocolLimits.maxControlMessageBytes ?? 262_144;
180
+ if (new TextEncoder().encode(text).byteLength > maxBytes) {
181
+ this.#protocolFailure(connection, undefined, 0, "frames control message too large");
182
+ return;
183
+ }
184
+ const decoded = decodeRoutedMessage(text, ROUTED_LEG_OUTBOUND.frames);
185
+ if (!decoded.ok) {
186
+ this.#protocolFailure(connection, undefined, 0, `invalid frames message: ${decoded.error}`);
187
+ return;
188
+ }
189
+ const message = decoded.message;
190
+ if (message.type === "ConnectionAccepted") {
191
+ if (connection.accepted ||
192
+ message.channel !== "frames" ||
193
+ message.connectionSetId !== connection.transportGrant.claims.connectionSetId ||
194
+ message.selectedProtocolVersion.major !== ROUTED_PROTOCOL_VERSION.major ||
195
+ message.selectedProtocolVersion.minor > ROUTED_PROTOCOL_VERSION.minor ||
196
+ message.capabilities.some((capability) => !connection.capabilities.includes(capability)) ||
197
+ message.creditEpoch === undefined ||
198
+ message.initialWindows === undefined ||
199
+ !capacitiesFitWithin(message.initialWindows, connection.receiverCapacities)) {
200
+ this.#protocolFailure(connection, undefined, 0, "invalid frames acceptance");
201
+ return;
202
+ }
203
+ connection.accepted = message;
204
+ this.#armHeartbeatDeadline(connection);
205
+ this.#scheduleHeartbeat(connection);
206
+ for (const activationId of connection.activationIds) {
207
+ const activation = this.#activations.get(activationId);
208
+ if (activation)
209
+ this.#sendAttach(connection, activation);
210
+ }
211
+ return;
212
+ }
213
+ if (message.type === "ConnectionRefused") {
214
+ connection.refusal = message;
215
+ this.#closeConnection(connection, 1000, message.code, true);
216
+ return;
217
+ }
218
+ if (!connection.accepted) {
219
+ this.#protocolFailure(connection, undefined, 0, "message before ConnectionAccepted");
220
+ return;
221
+ }
222
+ if (message.type === "LegHeartbeatAck") {
223
+ if (message.sequence > connection.heartbeatSequence) {
224
+ this.#protocolFailure(connection, undefined, 0, "heartbeat ack is ahead");
225
+ return;
226
+ }
227
+ if (message.sequence > connection.heartbeatAckSequence) {
228
+ connection.heartbeatAckSequence = message.sequence;
229
+ this.#armHeartbeatDeadline(connection);
230
+ }
231
+ return;
232
+ }
233
+ if (message.type === "AttachRefused") {
234
+ if (message.activationId !== undefined && !connection.activationIds.has(message.activationId)) {
235
+ if (!this.#activations.has(message.activationId))
236
+ return;
237
+ this.#protocolFailure(connection, undefined, 0, "attach refusal binding mismatch");
238
+ return;
239
+ }
240
+ this.#emit({
241
+ type: "attach-refused",
242
+ ...(message.activationId === undefined ? {} : { activationId: message.activationId }),
243
+ code: message.code,
244
+ retryable: message.retryable,
245
+ });
246
+ return;
247
+ }
248
+ if (message.type === "FramesLegAttached") {
249
+ const activation = this.#activations.get(message.activationId);
250
+ if (!connection.activationIds.has(message.activationId) && !activation)
251
+ return;
252
+ const duplicateIdentity = [...connection.activationIds].some((activationId) => {
253
+ if (activationId === message.activationId)
254
+ return false;
255
+ const identity = this.#activations.get(activationId)?.identity;
256
+ return (identity?.sessionHandle === message.trfIdentity.sessionHandle &&
257
+ identity.viewHandle === message.trfIdentity.viewHandle);
258
+ });
259
+ if (!activation ||
260
+ !connection.activationIds.has(message.activationId) ||
261
+ message.sessionId !== activation.sessionId ||
262
+ message.trfIdentity.sessionHandle !== activation.request.sessionHandle ||
263
+ duplicateIdentity ||
264
+ (message.outcome.kind === "resume-accepted" &&
265
+ (!connection.accepted.capabilities.includes("resume") ||
266
+ activation.request.resume === undefined ||
267
+ message.outcome.from === undefined ||
268
+ !exactStamp(activation.request.resume.from, message.outcome.from)))) {
269
+ this.#protocolFailure(connection, message.activationId, 0, "frames attach identity mismatch");
270
+ return;
271
+ }
272
+ activation.identity = message.trfIdentity;
273
+ activation.resumeToken = message.resumeToken;
274
+ this.#emit({ type: "frames-attached", attached: message });
275
+ this.#emit({
276
+ type: "frames-state",
277
+ activationId: message.activationId,
278
+ state: message.outcome.kind === "resume-accepted" ? "resuming" : "seeding",
279
+ resumeToken: message.resumeToken,
280
+ });
281
+ this.#reportPresentation(connection, activation, message.outcome.kind === "resume-accepted" ? "recovering" : "seeding");
282
+ return;
283
+ }
284
+ this.#protocolFailure(connection, undefined, 0, `unexpected frames message ${message.type}`);
285
+ }
286
+ #sendAttach(connection, activation) {
287
+ const resume = connection.accepted?.capabilities.includes("resume") ? activation.request.resume : undefined;
288
+ const body = {
289
+ activationId: activation.request.activationId,
290
+ attachGrant: activation.request.attachGrant,
291
+ ...(resume === undefined ? {} : { resume }),
292
+ };
293
+ this.#send(connection, encodeRoutedMessage("AttachFramesLeg", body));
294
+ }
295
+ #binary(connection, bytes) {
296
+ if (!connection.accepted) {
297
+ this.#protocolFailure(connection, undefined, bytes.byteLength, "binary message before acceptance");
298
+ return;
299
+ }
300
+ const decoded = decodeRoutedPresentationEnvelope(bytes);
301
+ if (!decoded.ok) {
302
+ this.#protocolFailure(connection, undefined, bytes.byteLength, `bad envelope: ${decoded.error}`);
303
+ return;
304
+ }
305
+ const { header, payload } = decoded.envelope;
306
+ if (header.creditEpoch !== connection.accepted.creditEpoch) {
307
+ this.#protocolFailure(connection, header.activationId, decoded.chargedBytes, "credit epoch mismatch");
308
+ return;
309
+ }
310
+ if (header.profiling !== undefined && !connection.accepted.capabilities.includes("profiling-envelope")) {
311
+ this.#protocolFailure(connection, header.activationId, decoded.chargedBytes, "unnegotiated profiling envelope");
312
+ return;
313
+ }
314
+ if (header.kind === "calibration") {
315
+ if (payload.byteLength !== 0) {
316
+ this.#protocolFailure(connection, undefined, decoded.chargedBytes, "calibration payload is not empty");
317
+ return;
318
+ }
319
+ const sequence = header.calibration.sequence;
320
+ if (sequence > connection.heartbeatSequence) {
321
+ this.#protocolFailure(connection, undefined, decoded.chargedBytes, "calibration echo is ahead");
322
+ return;
323
+ }
324
+ if (sequence > connection.heartbeatAckSequence) {
325
+ connection.heartbeatAckSequence = sequence;
326
+ this.#armHeartbeatDeadline(connection);
327
+ }
328
+ this.#returnCredit(connection, undefined, decoded.chargedBytes);
329
+ return;
330
+ }
331
+ const activation = this.#activations.get(header.activationId);
332
+ if (!activation || !connection.activationIds.has(header.activationId) || !activation.identity) {
333
+ this.#returnCredit(connection, header.activationId, decoded.chargedBytes);
334
+ if (activation)
335
+ this.#protocolFailure(connection, header.activationId, 0, "presentation activation binding mismatch");
336
+ return;
337
+ }
338
+ if (header.sessionId !== activation.sessionId ||
339
+ header.leaseEpoch !== activation.leaseEpoch ||
340
+ header.activationSequence <= activation.lastActivationSequence) {
341
+ this.#protocolFailure(connection, header.activationId, decoded.chargedBytes, "presentation identity or sequence mismatch");
342
+ return;
343
+ }
344
+ activation.lastActivationSequence = header.activationSequence;
345
+ if ((header.kind === "transfer-begin" || header.kind === "transfer-end") && payload.byteLength !== 0) {
346
+ this.#protocolFailure(connection, header.activationId, decoded.chargedBytes, "transfer metadata carries payload");
347
+ return;
348
+ }
349
+ switch (header.kind) {
350
+ case "trf1-frame":
351
+ this.#incremental(connection, activation, header, payload, decoded.chargedBytes);
352
+ break;
353
+ case "transfer-begin":
354
+ this.#beginTransfer(connection, activation, header, decoded.chargedBytes);
355
+ break;
356
+ case "transfer-chunk":
357
+ this.#transferChunk(connection, activation, header, payload, decoded.chargedBytes);
358
+ break;
359
+ case "transfer-end":
360
+ this.#endTransfer(connection, activation, header, decoded.chargedBytes);
361
+ break;
362
+ }
363
+ }
364
+ #incremental(connection, activation, header, payload, chargedBytes) {
365
+ if (payload.byteLength > connection.accepted.protocolLimits.maxPresentationChunkBytes ||
366
+ !header.resultContent ||
367
+ header.baseContent === undefined ||
368
+ (header.baseContent !== null && !exactStamp(activation.appliedContent, header.baseContent))) {
369
+ this.#protocolFailure(connection, header.activationId, chargedBytes, "incremental base mismatch");
370
+ return;
371
+ }
372
+ try {
373
+ this.#emit({ type: "frames-state", activationId: header.activationId, state: "applying" });
374
+ this.#applyFrame(copyArrayBuffer(payload), activation.identity);
375
+ activation.appliedContent = header.resultContent;
376
+ this.#returnCredit(connection, header.activationId, chargedBytes);
377
+ this.#sceneApplied(connection, activation);
378
+ }
379
+ catch (error) {
380
+ this.#protocolFailure(connection, header.activationId, chargedBytes, String(error));
381
+ }
382
+ }
383
+ #beginTransfer(connection, activation, header, chargedBytes) {
384
+ const transfer = header.transfer;
385
+ const limits = connection.accepted.protocolLimits;
386
+ const windows = connection.accepted.initialWindows;
387
+ if (activation.transfer ||
388
+ !transfer?.kind ||
389
+ transfer.totalBytes === undefined ||
390
+ transfer.chunkCount === undefined ||
391
+ !transfer.targetLayout ||
392
+ !transfer.checksum ||
393
+ !header.resultContent ||
394
+ transfer.totalBytes > windows.stagingBytesPerSession ||
395
+ this.#stagingBytes + transfer.totalBytes > windows.stagingBytesTotal ||
396
+ (transfer.kind === "catchup" && transfer.totalBytes > limits.maxCatchupBytes) ||
397
+ (transfer.kind === "catchup" &&
398
+ (header.baseContent == null || !exactStamp(activation.appliedContent, header.baseContent)))) {
399
+ this.#protocolFailure(connection, header.activationId, chargedBytes, "transfer budget or header invalid");
400
+ return;
401
+ }
402
+ const activeSeeds = [...this.#activations.values()].filter((candidate) => candidate.transfer?.kind === "seed").length;
403
+ if (transfer.kind === "seed" && activeSeeds >= windows.maxConcurrentSeeds) {
404
+ this.#protocolFailure(connection, header.activationId, chargedBytes, "seed concurrency exceeded");
405
+ return;
406
+ }
407
+ activation.transfer = {
408
+ transferId: transfer.transferId,
409
+ kind: transfer.kind,
410
+ bytes: new Uint8Array(transfer.totalBytes),
411
+ chunkCount: transfer.chunkCount,
412
+ chunks: new Set(),
413
+ ranges: [],
414
+ checksum: transfer.checksum.value,
415
+ targetLayout: transfer.targetLayout,
416
+ baseContent: header.baseContent ?? null,
417
+ resultContent: header.resultContent,
418
+ };
419
+ this.#stagingBytes += transfer.totalBytes;
420
+ this.#returnCredit(connection, header.activationId, chargedBytes);
421
+ this.#emit({ type: "frames-state", activationId: header.activationId, state: "seeding" });
422
+ }
423
+ #transferChunk(connection, activation, header, payload, chargedBytes) {
424
+ const staging = activation.transfer;
425
+ const transfer = header.transfer;
426
+ const chunkIndex = transfer?.chunkIndex;
427
+ const byteOffset = transfer?.byteOffset;
428
+ if (!staging ||
429
+ transfer?.transferId !== staging.transferId ||
430
+ chunkIndex === undefined ||
431
+ byteOffset === undefined ||
432
+ chunkIndex >= staging.chunkCount ||
433
+ staging.chunks.has(chunkIndex) ||
434
+ payload.byteLength > connection.accepted.protocolLimits.maxPresentationChunkBytes ||
435
+ byteOffset + payload.byteLength > staging.bytes.byteLength) {
436
+ this.#protocolFailure(connection, header.activationId, chargedBytes, "transfer chunk invalid");
437
+ return;
438
+ }
439
+ const end = byteOffset + payload.byteLength;
440
+ if (staging.ranges.some((range) => byteOffset < range.end && end > range.start)) {
441
+ this.#protocolFailure(connection, header.activationId, chargedBytes, "transfer chunk overlap");
442
+ return;
443
+ }
444
+ staging.bytes.set(payload, byteOffset);
445
+ staging.chunks.add(chunkIndex);
446
+ staging.ranges.push({ start: byteOffset, end });
447
+ // Transfer bytes become reclaimable as soon as the staging copy completes.
448
+ this.#returnCredit(connection, header.activationId, chargedBytes);
449
+ }
450
+ #endTransfer(connection, activation, header, chargedBytes) {
451
+ const staging = activation.transfer;
452
+ if (!staging || header.transfer?.transferId !== staging.transferId) {
453
+ this.#protocolFailure(connection, header.activationId, chargedBytes, "transfer end without begin");
454
+ return;
455
+ }
456
+ this.#returnCredit(connection, header.activationId, chargedBytes);
457
+ const ranges = [...staging.ranges].sort((left, right) => left.start - right.start);
458
+ let cursor = 0;
459
+ for (const range of ranges) {
460
+ if (range.start !== cursor) {
461
+ this.#protocolFailure(connection, header.activationId, 0, "transfer has a range gap");
462
+ return;
463
+ }
464
+ cursor = range.end;
465
+ }
466
+ if (staging.chunks.size !== staging.chunkCount ||
467
+ cursor !== staging.bytes.byteLength ||
468
+ routedCrc32c(staging.bytes) !== staging.checksum ||
469
+ (staging.kind === "catchup" &&
470
+ (staging.baseContent === null || !exactStamp(activation.appliedContent, staging.baseContent)))) {
471
+ this.#protocolFailure(connection, header.activationId, 0, "transfer validation failed");
472
+ return;
473
+ }
474
+ try {
475
+ this.#emit({ type: "frames-state", activationId: header.activationId, state: "applying" });
476
+ this.#applyFrame(staging.bytes.buffer, activation.identity, staging.targetLayout);
477
+ activation.appliedContent = staging.resultContent;
478
+ this.#releaseTransfer(activation);
479
+ this.#sceneApplied(connection, activation);
480
+ }
481
+ catch (error) {
482
+ this.#protocolFailure(connection, header.activationId, 0, String(error));
483
+ }
484
+ }
485
+ #releaseTransfer(activation) {
486
+ if (!activation.transfer)
487
+ return;
488
+ this.#stagingBytes = Math.max(0, this.#stagingBytes - activation.transfer.bytes.byteLength);
489
+ delete activation.transfer;
490
+ }
491
+ #sceneApplied(connection, activation) {
492
+ const appliedContent = activation.appliedContent;
493
+ if (!appliedContent)
494
+ return;
495
+ this.#send(connection, encodeRoutedMessage("SceneApplied", {
496
+ sessionId: activation.sessionId,
497
+ activationId: activation.request.activationId,
498
+ leaseEpoch: activation.leaseEpoch,
499
+ appliedContent,
500
+ }));
501
+ this.#emit({
502
+ type: "frames-state",
503
+ activationId: activation.request.activationId,
504
+ state: "active",
505
+ ...(activation.resumeToken === undefined ? {} : { resumeToken: activation.resumeToken }),
506
+ appliedContent,
507
+ });
508
+ this.#reportPresentation(connection, activation, "active");
509
+ if (activation.sceneRefreshTimer !== undefined)
510
+ clearTimeout(activation.sceneRefreshTimer);
511
+ activation.sceneRefreshTimer = setTimeout(() => this.#sceneApplied(connection, activation), connection.accepted.protocolLimits.sceneAppliedRefreshMs);
512
+ }
513
+ #reportPresentation(connection, activation, state) {
514
+ activation.workerStatusSequence += 1;
515
+ const status = {
516
+ activationId: activation.request.activationId,
517
+ workerStatusSequence: activation.workerStatusSequence,
518
+ state,
519
+ ...(activation.appliedContent === undefined ? {} : { sceneContent: activation.appliedContent }),
520
+ leaseTtlMs: Math.max(connection.accepted?.protocolLimits.presentationStatusRefreshMs ?? 2_000, connection.accepted?.heartbeatTtlMs ?? ROUTED_DOOR_LIMITS.heartbeatTtlMs),
521
+ };
522
+ this.#emit({ type: "presentation-status", status });
523
+ if (activation.presentationRefreshTimer !== undefined)
524
+ clearTimeout(activation.presentationRefreshTimer);
525
+ const refreshMs = connection.accepted?.protocolLimits.presentationStatusRefreshMs ?? 2_000;
526
+ activation.presentationRefreshTimer = setTimeout(() => this.#reportPresentation(connection, activation, state), refreshMs);
527
+ }
528
+ #returnCredit(connection, activationId, bytes) {
529
+ if (bytes <= 0)
530
+ return;
531
+ connection.connectionBytesReturned += bytes;
532
+ if (activationId) {
533
+ connection.accountBytesReturned.set(activationId, (connection.accountBytesReturned.get(activationId) ?? 0) + bytes);
534
+ if (!connection.activationIds.has(activationId))
535
+ this.#scheduleAccountDrain(connection, activationId);
536
+ }
537
+ const limits = connection.accepted?.protocolLimits;
538
+ if (connection.creditTimer !== undefined)
539
+ return;
540
+ connection.creditTimer = setTimeout(() => this.#flushCredit(connection), limits?.maxCreditReturnDelayMs ?? 16);
541
+ }
542
+ #flushCredit(connection) {
543
+ if (connection.creditTimer !== undefined)
544
+ clearTimeout(connection.creditTimer);
545
+ delete connection.creditTimer;
546
+ if (!connection.accepted || connection.connectionBytesReturned === 0)
547
+ return;
548
+ connection.creditSequence += 1;
549
+ const newlyReturned = connection.connectionBytesReturned - connection.connectionBytesReported;
550
+ connection.connectionBytesReported = connection.connectionBytesReturned;
551
+ this.#send(connection, encodeRoutedMessage("TransportCredit", {
552
+ creditEpoch: connection.accepted.creditEpoch,
553
+ creditSequence: connection.creditSequence,
554
+ connectionBytesReturned: connection.connectionBytesReturned,
555
+ accounts: [...connection.accountBytesReturned].map(([activationId, bytesReturned]) => ({
556
+ activationId,
557
+ bytesReturned,
558
+ })),
559
+ }));
560
+ this.#creditReturned?.(newlyReturned);
561
+ }
562
+ #scheduleAccountDrain(connection, activationId) {
563
+ const previous = connection.accountDrainTimers.get(activationId);
564
+ if (previous !== undefined)
565
+ clearTimeout(previous);
566
+ const ttl = connection.accepted?.protocolLimits.creditAccountDrainTtlMs ?? 5_000;
567
+ connection.accountDrainTimers.set(activationId, setTimeout(() => {
568
+ connection.accountDrainTimers.delete(activationId);
569
+ if (!connection.activationIds.has(activationId))
570
+ connection.accountBytesReturned.delete(activationId);
571
+ }, ttl));
572
+ }
573
+ #scheduleHeartbeat(connection) {
574
+ if (connection.heartbeatTimer !== undefined)
575
+ clearTimeout(connection.heartbeatTimer);
576
+ connection.heartbeatTimer = setTimeout(() => {
577
+ if (!connection.accepted || connection.socket.readyState !== socketOpen)
578
+ return;
579
+ connection.heartbeatSequence += 1;
580
+ this.#send(connection, encodeRoutedMessage("CalibrationPing", {
581
+ sequence: connection.heartbeatSequence,
582
+ t0: Date.now(),
583
+ }));
584
+ this.#scheduleHeartbeat(connection);
585
+ }, Math.max(100, Math.min(ROUTED_DOOR_LIMITS.heartbeatIntervalMs, Math.floor((connection.accepted?.heartbeatTtlMs ?? ROUTED_DOOR_LIMITS.heartbeatTtlMs) / 3))));
586
+ }
587
+ #armHeartbeatDeadline(connection) {
588
+ if (connection.heartbeatDeadlineTimer !== undefined)
589
+ clearTimeout(connection.heartbeatDeadlineTimer);
590
+ const ttl = connection.accepted?.heartbeatTtlMs ?? ROUTED_DOOR_LIMITS.heartbeatTtlMs;
591
+ connection.heartbeatDeadlineTimer = setTimeout(() => {
592
+ if (this.#connections.get(connection.cellBootId) !== connection)
593
+ return;
594
+ this.#closeConnection(connection, ROUTED_CLOSE_CODES.LEG_TIMEOUT, "heartbeat timeout", true);
595
+ }, ttl);
596
+ }
597
+ #protocolFailure(connection, activationId, chargedBytes, reason) {
598
+ this.#returnCredit(connection, activationId, chargedBytes);
599
+ this.#flushCredit(connection);
600
+ if (activationId)
601
+ this.#emit({ type: "frames-state", activationId, state: "failed", reason: "PROTOCOL" });
602
+ this.#closeConnection(connection, ROUTED_CLOSE_CODES.PROTOCOL, reason, true);
603
+ }
604
+ #send(connection, text) {
605
+ if (connection.socket.readyState === socketOpen)
606
+ connection.socket.send(text);
607
+ }
608
+ #closeConnection(connection, code, reason, closeSocket) {
609
+ if (connection.heartbeatTimer !== undefined)
610
+ clearTimeout(connection.heartbeatTimer);
611
+ if (connection.heartbeatDeadlineTimer !== undefined)
612
+ clearTimeout(connection.heartbeatDeadlineTimer);
613
+ if (connection.creditTimer !== undefined)
614
+ clearTimeout(connection.creditTimer);
615
+ for (const timer of connection.accountDrainTimers.values())
616
+ clearTimeout(timer);
617
+ if (closeSocket && connection.socket.readyState < socketClosing)
618
+ connection.socket.close(code, reason.slice(0, 123));
619
+ this.#closed(connection, code, reason);
620
+ }
621
+ #closed(connection, code, reason) {
622
+ if (this.#connections.get(connection.cellBootId) !== connection)
623
+ return;
624
+ this.#connections.delete(connection.cellBootId);
625
+ if (connection.heartbeatTimer !== undefined)
626
+ clearTimeout(connection.heartbeatTimer);
627
+ if (connection.heartbeatDeadlineTimer !== undefined)
628
+ clearTimeout(connection.heartbeatDeadlineTimer);
629
+ if (connection.creditTimer !== undefined)
630
+ clearTimeout(connection.creditTimer);
631
+ for (const timer of connection.accountDrainTimers.values())
632
+ clearTimeout(timer);
633
+ connection.accountDrainTimers.clear();
634
+ const activationIds = [...connection.activationIds];
635
+ for (const activationId of activationIds) {
636
+ const activation = this.#activations.get(activationId);
637
+ if (!activation)
638
+ continue;
639
+ this.#releaseTransfer(activation);
640
+ if (activation.sceneRefreshTimer !== undefined)
641
+ clearTimeout(activation.sceneRefreshTimer);
642
+ if (activation.presentationRefreshTimer !== undefined)
643
+ clearTimeout(activation.presentationRefreshTimer);
644
+ }
645
+ this.#emit({
646
+ type: "transport-closed",
647
+ cellBootId: connection.cellBootId,
648
+ code,
649
+ reason,
650
+ activationIds,
651
+ preAuth: connection.refusal === undefined && connection.accepted === undefined && code === 1008,
652
+ ...(connection.refusal === undefined ? {} : { refusal: connection.refusal }),
653
+ });
654
+ }
655
+ }
656
+ /** Kept local to avoid a second source of protocol defaults in the worker. */
657
+ export function routedCreditDelay(limits) {
658
+ return limits?.maxCreditReturnDelayMs ?? 16;
659
+ }
660
+ //# sourceMappingURL=routed-frames.js.map