@opengeni/sdk 0.46.0 → 0.52.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 (103) hide show
  1. package/README.md +94 -2
  2. package/dist/artifact-client.d.ts +20 -2
  3. package/dist/artifacts.d.ts +4 -0
  4. package/dist/artifacts.js +11 -0
  5. package/dist/artifacts.js.map +1 -0
  6. package/dist/chunk-FHI4DFIG.js +128 -0
  7. package/dist/chunk-FHI4DFIG.js.map +1 -0
  8. package/dist/chunk-FRJFNDIR.js +218 -0
  9. package/dist/chunk-FRJFNDIR.js.map +1 -0
  10. package/dist/{chunk-QKV5OCLJ.js → chunk-ILQZR5N6.js} +1380 -595
  11. package/dist/chunk-ILQZR5N6.js.map +1 -0
  12. package/dist/chunk-MZWTWOX6.js +661 -0
  13. package/dist/chunk-MZWTWOX6.js.map +1 -0
  14. package/dist/chunk-VWE2QIVO.js +1162 -0
  15. package/dist/chunk-VWE2QIVO.js.map +1 -0
  16. package/dist/chunk-VYCTL62C.js +230 -0
  17. package/dist/chunk-VYCTL62C.js.map +1 -0
  18. package/dist/client.d.ts +176 -20
  19. package/dist/codex-realtime-controller.d.ts +6 -6
  20. package/dist/codex-realtime-lifecycle.d.ts +1 -1
  21. package/dist/codex-realtime-v3.d.ts +3 -3
  22. package/dist/codex-realtime.d.ts +1 -1
  23. package/dist/company-profile.d.ts +100 -0
  24. package/dist/core.d.ts +5 -5
  25. package/dist/core.js +9 -4
  26. package/dist/desktop.d.ts +1 -1
  27. package/dist/editable-artifact-resources.d.ts +96 -0
  28. package/dist/editable-artifacts/browser-session.d.ts +35 -0
  29. package/dist/editable-artifacts/controller.d.ts +60 -0
  30. package/dist/editable-artifacts/errors.d.ts +23 -0
  31. package/dist/editable-artifacts/http-live-transport.d.ts +46 -0
  32. package/dist/editable-artifacts/index.d.ts +19 -0
  33. package/dist/editable-artifacts/pool.d.ts +15 -0
  34. package/dist/editable-artifacts/session.d.ts +96 -0
  35. package/dist/editable-artifacts/storage.d.ts +72 -0
  36. package/dist/editable-artifacts/types.d.ts +416 -0
  37. package/dist/editable-artifacts/worker/browser-client.d.ts +80 -0
  38. package/dist/editable-artifacts/worker/browser-entry.d.ts +6 -0
  39. package/dist/editable-artifacts/worker/kernel-adapter.d.ts +45 -0
  40. package/dist/editable-artifacts/worker/rpc-protocol.d.ts +92 -0
  41. package/dist/editable-artifacts/worker/runtime.d.ts +66 -0
  42. package/dist/editable-artifacts/worker/wire-codec.d.ts +52 -0
  43. package/dist/editable-artifacts-worker.d.ts +8 -0
  44. package/dist/editable-artifacts-worker.js +1708 -0
  45. package/dist/editable-artifacts-worker.js.map +1 -0
  46. package/dist/editable-artifacts.d.ts +6 -0
  47. package/dist/editable-artifacts.js +5954 -0
  48. package/dist/editable-artifacts.js.map +1 -0
  49. package/dist/gateway-realtime-transport.d.ts +1 -1
  50. package/dist/index.d.ts +41 -35
  51. package/dist/index.js +104 -68
  52. package/dist/index.js.map +1 -1
  53. package/dist/interaction.d.ts +885 -0
  54. package/dist/interaction.js +55 -0
  55. package/dist/interaction.js.map +1 -0
  56. package/dist/memory-slack-client.d.ts +13 -0
  57. package/dist/memory-slack-delivery.d.ts +99 -0
  58. package/dist/memory-slack.d.ts +4 -0
  59. package/dist/memory-slack.js +44 -0
  60. package/dist/memory-slack.js.map +1 -0
  61. package/dist/proxy.d.ts +3 -3
  62. package/dist/realtime.d.ts +12 -12
  63. package/dist/retained-artifacts.d.ts +12 -0
  64. package/dist/stream.d.ts +16 -1
  65. package/dist/terminal.d.ts +1 -1
  66. package/dist/transcription.d.ts +2 -2
  67. package/dist/types.d.ts +1203 -23
  68. package/dist/workspace-control-stream.d.ts +2 -2
  69. package/dist/workspace-state.d.ts +1 -1
  70. package/package.json +29 -3
  71. package/src/artifact-client.ts +129 -0
  72. package/src/artifacts.ts +17 -0
  73. package/src/client.ts +1801 -206
  74. package/src/company-profile.ts +92 -0
  75. package/src/editable-artifact-resources.ts +119 -0
  76. package/src/editable-artifacts/browser-session.ts +176 -0
  77. package/src/editable-artifacts/controller.ts +2632 -0
  78. package/src/editable-artifacts/errors.ts +52 -0
  79. package/src/editable-artifacts/http-live-transport.ts +1449 -0
  80. package/src/editable-artifacts/index.ts +175 -0
  81. package/src/editable-artifacts/pool.ts +81 -0
  82. package/src/editable-artifacts/session.ts +778 -0
  83. package/src/editable-artifacts/storage.ts +1718 -0
  84. package/src/editable-artifacts/types.ts +499 -0
  85. package/src/editable-artifacts/worker/browser-client.ts +1167 -0
  86. package/src/editable-artifacts/worker/browser-entry.ts +57 -0
  87. package/src/editable-artifacts/worker/kernel-adapter.ts +1039 -0
  88. package/src/editable-artifacts/worker/rpc-protocol.ts +622 -0
  89. package/src/editable-artifacts/worker/runtime.ts +1204 -0
  90. package/src/editable-artifacts/worker/wire-codec.ts +852 -0
  91. package/src/editable-artifacts-worker.ts +8 -0
  92. package/src/editable-artifacts.ts +6 -0
  93. package/src/index.ts +194 -0
  94. package/src/interaction.ts +1830 -0
  95. package/src/memory-slack-client.ts +71 -0
  96. package/src/memory-slack-delivery.ts +128 -0
  97. package/src/memory-slack.ts +19 -0
  98. package/src/retained-artifacts.ts +326 -0
  99. package/src/stream.ts +102 -4
  100. package/src/transcription.ts +2 -2
  101. package/src/types.ts +1430 -33
  102. package/src/workspace-control-stream.ts +15 -5
  103. package/dist/chunk-QKV5OCLJ.js.map +0 -1
@@ -0,0 +1,1449 @@
1
+ import {
2
+ decodeEditableArtifactLiveServerWireFrame,
3
+ encodeEditableArtifactLiveAppliedWireFrame,
4
+ encodeEditableArtifactLiveMutationWireFrame,
5
+ encodeEditableArtifactLiveOpenWireFrame,
6
+ type EditableArtifactLiveOpenWireFrame,
7
+ type EditableArtifactLiveCommittedTransaction as ContractCommittedTransaction,
8
+ type EditableArtifactLiveServerFrame,
9
+ } from "@opengeni/contracts/editable-artifact-live";
10
+ import {
11
+ EDITABLE_ARTIFACT_COMMAND_MAX_BYTES,
12
+ EDITABLE_ARTIFACT_INTENT_MAX_BYTES,
13
+ EDITABLE_ARTIFACT_PRODUCT_MAX_SNAPSHOT_BYTES,
14
+ hashEditableArtifactMutationIntentBytes,
15
+ } from "@opengeni/contracts/editable-artifacts";
16
+ import { MAX_COMMITTED_TRANSACTION_BYTES } from "@opengeni/contracts/editable-artifact-committed-transaction";
17
+ import { EditableArtifactSyncError, EditableArtifactTransportError } from "./errors";
18
+ import { OPENGENI_API_CONTRACT_HEADER, OPENGENI_API_CONTRACT_REVISION } from "../types";
19
+ import type {
20
+ EditableArtifactBootstrap,
21
+ EditableArtifactCausalFrontier,
22
+ EditableArtifactCommittedTransaction,
23
+ EditableArtifactLiveClose,
24
+ EditableArtifactLiveConnection,
25
+ EditableArtifactLiveLimits,
26
+ EditableArtifactLiveMessage,
27
+ EditableArtifactModality,
28
+ EditableArtifactPendingTransaction,
29
+ EditableArtifactReplayPage,
30
+ EditableArtifactSnapshot,
31
+ EditableArtifactSubmitReceipt,
32
+ EditableArtifactSyncTicket,
33
+ EditableArtifactSyncTransport,
34
+ } from "./types";
35
+
36
+ const LIVE_PATH = "/v1/editable-artifacts/live";
37
+ const LIVE_SUBPROTOCOL = "opengeni-artifact-v1";
38
+ const DEFAULT_MAX_SERVER_FRAME_BYTES = 8 * 1024 * 1024 + 64 * 1024;
39
+ const DEFAULT_MAX_SNAPSHOT_BYTES = EDITABLE_ARTIFACT_PRODUCT_MAX_SNAPSHOT_BYTES;
40
+ const DEFAULT_HANDSHAKE_TIMEOUT_MS = 20_000;
41
+ const DEFAULT_SUBMIT_TIMEOUT_MS = 30_000;
42
+ const MAX_TICKET_RESPONSE_BYTES = 16 * 1024;
43
+ const SHA256 = /^sha256:[0-9a-f]{64}$/u;
44
+ const STABLE_ID = /^(?!0{32}$)[0-9a-f]{32}$/u;
45
+ const REPLICA_ID = /^(?!0{16}$)[0-9a-f]{16}$/u;
46
+
47
+ export type EditableArtifactWebSocketMessageEvent = Readonly<{ data: unknown }>;
48
+ export type EditableArtifactWebSocketCloseEvent = Readonly<{
49
+ code?: number;
50
+ reason?: string;
51
+ }>;
52
+
53
+ export type EditableArtifactWebSocketLike = {
54
+ binaryType: string;
55
+ readonly protocol: string;
56
+ readonly readyState: number;
57
+ send(data: Uint8Array | ArrayBuffer): void;
58
+ close(code?: number, reason?: string): void;
59
+ addEventListener(type: "open", listener: () => void): void;
60
+ addEventListener(
61
+ type: "message",
62
+ listener: (event: EditableArtifactWebSocketMessageEvent) => void,
63
+ ): void;
64
+ addEventListener(type: "error", listener: () => void): void;
65
+ addEventListener(
66
+ type: "close",
67
+ listener: (event: EditableArtifactWebSocketCloseEvent) => void,
68
+ ): void;
69
+ removeEventListener(type: "open", listener: () => void): void;
70
+ removeEventListener(
71
+ type: "message",
72
+ listener: (event: EditableArtifactWebSocketMessageEvent) => void,
73
+ ): void;
74
+ removeEventListener(type: "error", listener: () => void): void;
75
+ removeEventListener(
76
+ type: "close",
77
+ listener: (event: EditableArtifactWebSocketCloseEvent) => void,
78
+ ): void;
79
+ };
80
+
81
+ export type CreateEditableArtifactHttpLiveTransportOptions = Readonly<{
82
+ baseUrl: string | URL;
83
+ workspaceId: string;
84
+ protocolVersion: number;
85
+ kernelVersion: string;
86
+ modelSchemaVersion: number;
87
+ apiKey?: string;
88
+ headers?: Readonly<Record<string, string>> | (() => Readonly<Record<string, string>>);
89
+ credentials?: RequestCredentials;
90
+ fetch?: typeof fetch;
91
+ webSocketUrl?: string | URL;
92
+ webSocketFactory?: (url: string, protocol: string) => EditableArtifactWebSocketLike;
93
+ allowInsecureDevelopmentTransport?: boolean;
94
+ maximumServerFrameBytes?: number;
95
+ maximumSnapshotBytes?: number;
96
+ handshakeTimeoutMs?: number;
97
+ submitTimeoutMs?: number;
98
+ }>;
99
+
100
+ /**
101
+ * Production HTTP + authenticated binary WebSocket transport. The one-use
102
+ * ticket is sent only inside the first OGALV frame, never in the socket URL.
103
+ */
104
+ export function createEditableArtifactHttpLiveTransport(
105
+ options: CreateEditableArtifactHttpLiveTransportOptions,
106
+ ): EditableArtifactSyncTransport {
107
+ return new HttpLiveTransport(options);
108
+ }
109
+
110
+ class HttpLiveTransport implements EditableArtifactSyncTransport {
111
+ private readonly baseUrl: URL;
112
+ private readonly socketUrl: URL;
113
+ private readonly workspaceId: string;
114
+ private readonly protocolVersion: number;
115
+ private readonly kernelVersion: string;
116
+ private readonly modelSchemaVersion: number;
117
+ private readonly apiKey: string | undefined;
118
+ private readonly headerSource: CreateEditableArtifactHttpLiveTransportOptions["headers"];
119
+ private readonly credentials: RequestCredentials;
120
+ private readonly fetchImpl: typeof fetch;
121
+ private readonly webSocketFactory: NonNullable<
122
+ CreateEditableArtifactHttpLiveTransportOptions["webSocketFactory"]
123
+ >;
124
+ private readonly maximumServerFrameBytes: number;
125
+ private readonly maximumSnapshotBytes: number;
126
+ private readonly handshakeTimeoutMs: number;
127
+ private readonly submitTimeoutMs: number;
128
+
129
+ constructor(options: CreateEditableArtifactHttpLiveTransportOptions) {
130
+ this.baseUrl = transportBaseUrl(options.baseUrl);
131
+ this.socketUrl = socketUrl(options.webSocketUrl, this.baseUrl);
132
+ const insecure = options.allowInsecureDevelopmentTransport ?? false;
133
+ requireTransportSecurity(this.baseUrl, this.socketUrl, insecure);
134
+ this.workspaceId = boundedString(options.workspaceId, "workspaceId", 256);
135
+ this.protocolVersion = positiveU16(options.protocolVersion, "protocolVersion");
136
+ this.kernelVersion = boundedString(options.kernelVersion, "kernelVersion", 512);
137
+ this.modelSchemaVersion = positiveU16(options.modelSchemaVersion, "modelSchemaVersion");
138
+ this.apiKey = options.apiKey;
139
+ if (this.apiKey !== undefined) boundedString(this.apiKey, "apiKey", 16 * 1024);
140
+ this.headerSource = options.headers;
141
+ this.credentials = options.credentials ?? "same-origin";
142
+ this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
143
+ this.webSocketFactory = options.webSocketFactory ?? defaultWebSocketFactory;
144
+ this.maximumServerFrameBytes = positiveBounded(
145
+ options.maximumServerFrameBytes ?? DEFAULT_MAX_SERVER_FRAME_BYTES,
146
+ 128 * 1024 * 1024,
147
+ "maximumServerFrameBytes",
148
+ );
149
+ this.maximumSnapshotBytes = positiveBounded(
150
+ options.maximumSnapshotBytes ?? DEFAULT_MAX_SNAPSHOT_BYTES,
151
+ EDITABLE_ARTIFACT_PRODUCT_MAX_SNAPSHOT_BYTES,
152
+ "maximumSnapshotBytes",
153
+ );
154
+ this.handshakeTimeoutMs = positiveBounded(
155
+ options.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS,
156
+ 5 * 60_000,
157
+ "handshakeTimeoutMs",
158
+ );
159
+ this.submitTimeoutMs = positiveBounded(
160
+ options.submitTimeoutMs ?? DEFAULT_SUBMIT_TIMEOUT_MS,
161
+ 5 * 60_000,
162
+ "submitTimeoutMs",
163
+ );
164
+ }
165
+
166
+ async mintTicket(input: {
167
+ artifactId: string;
168
+ replicaId: string;
169
+ signal: AbortSignal;
170
+ }): Promise<EditableArtifactSyncTicket> {
171
+ requireStableId(input.artifactId, "artifactId");
172
+ requireReplicaId(input.replicaId);
173
+ input.signal.throwIfAborted();
174
+ const url = childUrl(
175
+ this.baseUrl,
176
+ `/v1/workspaces/${encodeURIComponent(this.workspaceId)}/editable-artifacts/${encodeURIComponent(input.artifactId)}/live-ticket`,
177
+ );
178
+ let response: Response;
179
+ try {
180
+ response = await this.fetchImpl(url, {
181
+ method: "POST",
182
+ credentials: this.credentials,
183
+ headers: {
184
+ ...this.headers(),
185
+ accept: "application/json",
186
+ "content-type": "application/json",
187
+ },
188
+ body: JSON.stringify({
189
+ replicaId: input.replicaId,
190
+ protocolVersion: this.protocolVersion,
191
+ kernelVersion: this.kernelVersion,
192
+ modelSchemaVersion: this.modelSchemaVersion,
193
+ }),
194
+ signal: input.signal,
195
+ });
196
+ } catch (cause) {
197
+ input.signal.throwIfAborted();
198
+ throw new EditableArtifactTransportError("editable artifact ticket request failed", {
199
+ code: "ticket_transport_error",
200
+ cause,
201
+ });
202
+ }
203
+ if (!response.ok) {
204
+ await response.body?.cancel("ticket request rejected").catch(() => undefined);
205
+ throw new EditableArtifactTransportError(
206
+ `editable artifact ticket request failed with HTTP ${response.status}`,
207
+ {
208
+ code: response.status === 403 ? "permission_changed" : "ticket_rejected",
209
+ retryable: response.status === 408 || response.status === 429 || response.status >= 500,
210
+ },
211
+ );
212
+ }
213
+ const value = await readBoundedJson(response, MAX_TICKET_RESPONSE_BYTES);
214
+ return validateTicket(value, input.artifactId, input.replicaId, this.protocolVersion);
215
+ }
216
+
217
+ async openLive(input: {
218
+ ticket: EditableArtifactSyncTicket;
219
+ after: number;
220
+ stateHash: string | null;
221
+ resume:
222
+ | { modality: "spreadsheet"; causalFrontier: EditableArtifactCausalFrontier }
223
+ | { modality: "document" | "presentation"; nativeRevision: number | null };
224
+ requireSnapshot: boolean;
225
+ signal: AbortSignal;
226
+ onMessage: (message: EditableArtifactLiveMessage) => void;
227
+ }): Promise<EditableArtifactLiveConnection> {
228
+ if (input.resume.modality !== input.ticket.modality) {
229
+ throw new TypeError("live resume modality does not match its ticket");
230
+ }
231
+ if (
232
+ input.resume.modality !== "spreadsheet" &&
233
+ (input.stateHash === null) !== (input.resume.nativeRevision === null)
234
+ ) {
235
+ throw new TypeError(
236
+ "serialized live resume state hash and native revision must be present together",
237
+ );
238
+ }
239
+ const socket = this.webSocketFactory(this.socketUrl.href, LIVE_SUBPROTOCOL);
240
+ const commonResume = {
241
+ localCursor: input.stateHash === null ? null : safeSequence(input.after, "after"),
242
+ localStateHash: input.stateHash,
243
+ requireSnapshot: input.requireSnapshot || input.stateHash === null,
244
+ } as const;
245
+ const resume: EditableArtifactLiveOpenWireFrame["resume"] =
246
+ input.resume.modality === "spreadsheet"
247
+ ? {
248
+ ...commonResume,
249
+ localCausalFrontier: cloneFrontier(input.resume.causalFrontier),
250
+ }
251
+ : {
252
+ ...commonResume,
253
+ modality: input.resume.modality,
254
+ localNativeRevision: input.resume.nativeRevision,
255
+ };
256
+ const connection = new HttpLiveConnection({
257
+ socket,
258
+ ticket: input.ticket,
259
+ resume,
260
+ kernelVersion: this.kernelVersion,
261
+ modelSchemaVersion: this.modelSchemaVersion,
262
+ maximumServerFrameBytes: this.maximumServerFrameBytes,
263
+ maximumSnapshotBytes: this.maximumSnapshotBytes,
264
+ handshakeTimeoutMs: this.handshakeTimeoutMs,
265
+ submitTimeoutMs: this.submitTimeoutMs,
266
+ signal: input.signal,
267
+ onMessage: input.onMessage,
268
+ });
269
+ await connection.ready();
270
+ return connection;
271
+ }
272
+
273
+ private headers(): Readonly<Record<string, string>> {
274
+ const supplied =
275
+ typeof this.headerSource === "function" ? this.headerSource() : this.headerSource;
276
+ return {
277
+ ...(supplied ?? {}),
278
+ [OPENGENI_API_CONTRACT_HEADER]: OPENGENI_API_CONTRACT_REVISION,
279
+ ...(this.apiKey ? { authorization: `Bearer ${this.apiKey}` } : {}),
280
+ };
281
+ }
282
+ }
283
+
284
+ type ConnectionInput = Readonly<{
285
+ socket: EditableArtifactWebSocketLike;
286
+ ticket: EditableArtifactSyncTicket;
287
+ resume: EditableArtifactLiveOpenWireFrame["resume"];
288
+ kernelVersion: string;
289
+ modelSchemaVersion: number;
290
+ maximumServerFrameBytes: number;
291
+ maximumSnapshotBytes: number;
292
+ handshakeTimeoutMs: number;
293
+ submitTimeoutMs: number;
294
+ signal: AbortSignal;
295
+ onMessage: (message: EditableArtifactLiveMessage) => void;
296
+ }>;
297
+
298
+ type OmitEach<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
299
+
300
+ type SnapshotAssembly = {
301
+ metadata: OmitEach<
302
+ Extract<EditableArtifactLiveServerFrame, { type: "snapshot" }>,
303
+ "bytes" | "offset" | "final"
304
+ >;
305
+ bytes: Uint8Array;
306
+ nextOffset: number;
307
+ };
308
+
309
+ type PendingSubmission = {
310
+ pending: EditableArtifactPendingTransaction;
311
+ deferred: Deferred<EditableArtifactSubmitReceipt>;
312
+ accepted: Extract<EditableArtifactLiveServerFrame, { type: "mutationAccepted" }> | null;
313
+ timeout: ReturnType<typeof setTimeout>;
314
+ sent: boolean;
315
+ };
316
+
317
+ class HttpLiveConnection implements EditableArtifactLiveConnection {
318
+ private readonly socket: EditableArtifactWebSocketLike;
319
+ private readonly ticket: EditableArtifactSyncTicket;
320
+ private readonly resume: EditableArtifactLiveOpenWireFrame["resume"];
321
+ private readonly kernelVersion: string;
322
+ private readonly modelSchemaVersion: number;
323
+ private readonly maximumServerFrameBytes: number;
324
+ private readonly maximumSnapshotBytes: number;
325
+ private readonly submitTimeoutMs: number;
326
+ private readonly signal: AbortSignal;
327
+ private readonly onMessage: (message: EditableArtifactLiveMessage) => void;
328
+ private readonly readyDeferred = new Deferred<void>();
329
+ private readonly closedDeferred = new Deferred<EditableArtifactLiveClose>();
330
+ private readonly firstPostOpen = new Deferred<
331
+ "snapshot" | "transaction" | "barrier" | "resync"
332
+ >();
333
+ private readonly snapshotDeferred = new Deferred<EditableArtifactSnapshot>();
334
+ private readonly progressWaiters = new Set<Deferred<void>>();
335
+ private readonly replayByStart = new Map<number, EditableArtifactCommittedTransaction>();
336
+ private readonly transactionsById = new Map<string, EditableArtifactCommittedTransaction>();
337
+ private readonly submissions = new Map<string, PendingSubmission>();
338
+ private openFrame: Extract<EditableArtifactLiveServerFrame, { type: "open" }> | null = null;
339
+ private snapshotAssembly: SnapshotAssembly | null = null;
340
+ private snapshot: EditableArtifactSnapshot | null = null;
341
+ private terminal: Error | null = null;
342
+ private localClose = false;
343
+ private bootstrapRead = false;
344
+ private bootstrapDelivered = false;
345
+ private messageTail: Promise<void> = Promise.resolve();
346
+ private targetHead = 0;
347
+ private barrierSequence: number | null = null;
348
+ private handshakeTimer: ReturnType<typeof setTimeout>;
349
+
350
+ constructor(input: ConnectionInput) {
351
+ this.socket = input.socket;
352
+ this.ticket = input.ticket;
353
+ this.resume = input.resume;
354
+ this.kernelVersion = input.kernelVersion;
355
+ this.modelSchemaVersion = input.modelSchemaVersion;
356
+ this.maximumServerFrameBytes = input.maximumServerFrameBytes;
357
+ this.maximumSnapshotBytes = input.maximumSnapshotBytes;
358
+ this.submitTimeoutMs = input.submitTimeoutMs;
359
+ this.signal = input.signal;
360
+ this.onMessage = input.onMessage;
361
+ this.socket.binaryType = "arraybuffer";
362
+ this.socket.addEventListener("open", this.handleOpen);
363
+ this.socket.addEventListener("message", this.handleMessage);
364
+ this.socket.addEventListener("error", this.handleError);
365
+ this.socket.addEventListener("close", this.handleClose);
366
+ this.signal.addEventListener("abort", this.handleAbort, { once: true });
367
+ this.handshakeTimer = setTimeout(() => {
368
+ this.fail(new EditableArtifactTransportError("editable artifact live handshake timed out"));
369
+ }, input.handshakeTimeoutMs);
370
+ }
371
+
372
+ get streamEpoch(): string {
373
+ return this.requireOpen().streamEpoch;
374
+ }
375
+
376
+ get limits(): EditableArtifactLiveLimits {
377
+ const frame = this.requireOpen();
378
+ return Object.freeze({
379
+ maxClientFrameBytes: frame.maxClientFrameBytes,
380
+ maxCommandBytes: frame.maxCommandBytes,
381
+ maxIntentBytes: frame.maxIntentBytes,
382
+ maxCommittedTransactionBytes: frame.maxCommittedTransactionBytes,
383
+ maxSnapshotBytes: frame.maxSnapshotBytes,
384
+ maxInFlightTransactions: frame.maxInFlightTransactions,
385
+ maxInFlightBytes: frame.maxInFlightBytes,
386
+ });
387
+ }
388
+
389
+ readonly closed = this.closedDeferred.promise;
390
+
391
+ async ready(): Promise<void> {
392
+ await abortable(this.readyDeferred.promise, this.signal);
393
+ }
394
+
395
+ async readBootstrap(input: {
396
+ localCursor: number | null;
397
+ localStateHash: string | null;
398
+ resume:
399
+ | { modality: "spreadsheet"; localCausalFrontier: EditableArtifactCausalFrontier }
400
+ | { modality: "document" | "presentation"; localNativeRevision: number | null };
401
+ requireSnapshot: boolean;
402
+ signal: AbortSignal;
403
+ }): Promise<EditableArtifactBootstrap> {
404
+ if (this.bootstrapRead) throw new TypeError("live bootstrap may only be read once");
405
+ this.bootstrapRead = true;
406
+ const open = this.requireOpen();
407
+ if (input.resume.modality !== this.ticket.modality) {
408
+ throw new TypeError("bootstrap resume modality does not match its ticket");
409
+ }
410
+ if (
411
+ input.resume.modality !== "spreadsheet" &&
412
+ ((input.localCursor === null) !== (input.localStateHash === null) ||
413
+ (input.localStateHash === null) !== (input.resume.localNativeRevision === null))
414
+ ) {
415
+ throw new TypeError(
416
+ "serialized bootstrap cursor, state hash, and native revision must be present together",
417
+ );
418
+ }
419
+ const first = await abortable(this.firstPostOpen.promise, input.signal);
420
+ if (first === "resync") {
421
+ clearTimeout(this.handshakeTimer);
422
+ throw new EditableArtifactSyncError("resync_required", "server requires a fresh snapshot", {
423
+ retryable: true,
424
+ requiresSnapshot: true,
425
+ });
426
+ }
427
+ let snapshot: EditableArtifactSnapshot | null = null;
428
+ if (first === "snapshot")
429
+ snapshot = await abortable(this.snapshotDeferred.promise, input.signal);
430
+ if (input.requireSnapshot && snapshot === null) {
431
+ throw new EditableArtifactSyncError(
432
+ "invalid_bootstrap",
433
+ "server omitted the required snapshot",
434
+ {
435
+ requiresSnapshot: true,
436
+ },
437
+ );
438
+ }
439
+ const boundarySequence = snapshot?.sequence ?? input.localCursor;
440
+ const boundaryStateHash = snapshot?.stateHash ?? input.localStateHash;
441
+ if (boundarySequence === null || boundaryStateHash === null) {
442
+ throw new EditableArtifactSyncError(
443
+ "invalid_bootstrap",
444
+ "live stream established no state boundary",
445
+ {
446
+ requiresSnapshot: true,
447
+ },
448
+ );
449
+ }
450
+ if (boundarySequence > open.headSequence) {
451
+ throw new EditableArtifactSyncError(
452
+ "invalid_bootstrap",
453
+ "local state is ahead of the live head",
454
+ {
455
+ requiresSnapshot: true,
456
+ },
457
+ );
458
+ }
459
+ const commonBootstrap = {
460
+ artifactId: this.ticket.artifactId,
461
+ protocolVersion: this.ticket.protocolVersion,
462
+ headSequence: boundarySequence,
463
+ headStateHash: boundaryStateHash,
464
+ kernelVersion: snapshot?.kernelVersion ?? this.kernelVersion,
465
+ modelSchemaVersion: snapshot?.modelSchemaVersion ?? this.modelSchemaVersion,
466
+ writable: open.writable,
467
+ minimumReplaySequence: open.minimumReplaySequence,
468
+ snapshot,
469
+ resyncRequired: false,
470
+ } as const;
471
+ const bootstrap: EditableArtifactBootstrap =
472
+ this.ticket.modality === "spreadsheet"
473
+ ? {
474
+ ...commonBootstrap,
475
+ modality: "spreadsheet",
476
+ headCausalFrontier: cloneFrontier(
477
+ snapshot?.modality === "spreadsheet"
478
+ ? snapshot.causalFrontier
479
+ : input.resume.modality === "spreadsheet"
480
+ ? input.resume.localCausalFrontier
481
+ : [],
482
+ ),
483
+ }
484
+ : {
485
+ ...commonBootstrap,
486
+ modality: this.ticket.modality,
487
+ headNativeRevision: serializedBoundaryRevision(
488
+ snapshot,
489
+ input.resume,
490
+ this.ticket.modality,
491
+ ),
492
+ };
493
+ this.bootstrapDelivered = true;
494
+ clearTimeout(this.handshakeTimer);
495
+ return bootstrap;
496
+ }
497
+
498
+ async replay(input: {
499
+ after: number;
500
+ through: number;
501
+ limit: number;
502
+ signal: AbortSignal;
503
+ }): Promise<EditableArtifactReplayPage> {
504
+ const after = safeSequence(input.after, "replay after");
505
+ const through = safeSequence(input.through, "replay through");
506
+ const limit = positiveBounded(input.limit, 10_000, "replay limit");
507
+ if (after >= through) {
508
+ return {
509
+ artifactId: this.ticket.artifactId,
510
+ transactions: [],
511
+ headSequence: this.targetHead,
512
+ };
513
+ }
514
+ while (true) {
515
+ this.throwIfTerminal();
516
+ const transactions: EditableArtifactCommittedTransaction[] = [];
517
+ let expected = after + 1;
518
+ while (transactions.length < limit) {
519
+ const transaction = this.replayByStart.get(expected);
520
+ if (!transaction || transaction.endSequence > through) break;
521
+ transactions.push(transaction);
522
+ expected = transaction.endSequence + 1;
523
+ }
524
+ if (transactions.length > 0) {
525
+ return {
526
+ artifactId: this.ticket.artifactId,
527
+ transactions: transactions.map(cloneCommitted),
528
+ headSequence: this.targetHead,
529
+ };
530
+ }
531
+ if (this.barrierSequence !== null && this.barrierSequence >= through) {
532
+ throw new EditableArtifactSyncError(
533
+ "invalid_sequence",
534
+ `live stream omitted committed sequence ${after + 1}`,
535
+ { retryable: true, requiresSnapshot: true },
536
+ );
537
+ }
538
+ await this.waitForProgress(input.signal);
539
+ }
540
+ }
541
+
542
+ async submit(input: {
543
+ transaction: EditableArtifactPendingTransaction;
544
+ signal: AbortSignal;
545
+ }): Promise<EditableArtifactSubmitReceipt> {
546
+ this.throwIfTerminal();
547
+ const open = this.requireOpen();
548
+ const pending = input.transaction;
549
+ if (pending.artifactId !== this.ticket.artifactId) {
550
+ throw new TypeError("pending transaction belongs to another artifact");
551
+ }
552
+ if (pending.protocolVersion !== this.ticket.protocolVersion) {
553
+ throw new EditableArtifactTransportError("pending protocol does not match live stream", {
554
+ code: "unsupported",
555
+ retryable: false,
556
+ });
557
+ }
558
+ if (
559
+ pending.commandBytes.byteLength >
560
+ Math.min(open.maxCommandBytes, EDITABLE_ARTIFACT_COMMAND_MAX_BYTES)
561
+ ) {
562
+ throw new EditableArtifactTransportError("pending command exceeds negotiated live limit", {
563
+ code: "limit_exceeded",
564
+ retryable: false,
565
+ });
566
+ }
567
+ if (
568
+ pending.intentBytes.byteLength >
569
+ Math.min(open.maxIntentBytes, EDITABLE_ARTIFACT_INTENT_MAX_BYTES)
570
+ ) {
571
+ throw new EditableArtifactTransportError("pending intent exceeds negotiated live limit", {
572
+ code: "limit_exceeded",
573
+ retryable: false,
574
+ });
575
+ }
576
+ if (hashEditableArtifactMutationIntentBytes(pending.intentBytes) !== pending.requestHash) {
577
+ throw new TypeError("pending requestHash does not bind its exact OGATX bytes");
578
+ }
579
+ if (this.submissions.has(pending.requestHash)) {
580
+ throw new TypeError("pending request is already submitted on this stream");
581
+ }
582
+ const frame = encodeEditableArtifactLiveMutationWireFrame({
583
+ type: "mutation",
584
+ protocolVersion: pending.protocolVersion,
585
+ artifactId: pending.artifactId,
586
+ streamEpoch: open.streamEpoch,
587
+ requestHash: pending.requestHash,
588
+ intentBytes: pending.intentBytes,
589
+ });
590
+ if (frame.byteLength > open.maxClientFrameBytes) {
591
+ throw new EditableArtifactTransportError("pending frame exceeds negotiated live limit", {
592
+ code: "limit_exceeded",
593
+ retryable: false,
594
+ });
595
+ }
596
+ const deferred = new Deferred<EditableArtifactSubmitReceipt>();
597
+ const submission: PendingSubmission = {
598
+ pending,
599
+ deferred,
600
+ accepted: null,
601
+ sent: false,
602
+ timeout: setTimeout(() => {
603
+ if (!this.submissions.delete(pending.requestHash)) return;
604
+ deferred.reject(
605
+ new EditableArtifactTransportError("pending mutation response timed out", {
606
+ code: "submit_timeout",
607
+ outcomeUnknown: true,
608
+ }),
609
+ );
610
+ this.close("submit_timeout");
611
+ }, this.submitTimeoutMs),
612
+ };
613
+ this.submissions.set(pending.requestHash, submission);
614
+ try {
615
+ input.signal.throwIfAborted();
616
+ this.socket.send(frame);
617
+ submission.sent = true;
618
+ } catch (cause) {
619
+ clearTimeout(submission.timeout);
620
+ this.submissions.delete(pending.requestHash);
621
+ input.signal.throwIfAborted();
622
+ throw new EditableArtifactTransportError("pending mutation send failed", {
623
+ code: "transport_error",
624
+ outcomeUnknown: true,
625
+ cause,
626
+ });
627
+ }
628
+ return await abortable(deferred.promise, input.signal);
629
+ }
630
+
631
+ async acknowledge(input: {
632
+ sequence: number;
633
+ stateHash: string;
634
+ signal: AbortSignal;
635
+ }): Promise<void> {
636
+ this.throwIfTerminal();
637
+ input.signal.throwIfAborted();
638
+ const frame = encodeEditableArtifactLiveAppliedWireFrame({
639
+ type: "applied",
640
+ protocolVersion: this.ticket.protocolVersion,
641
+ artifactId: this.ticket.artifactId,
642
+ streamEpoch: this.streamEpoch,
643
+ sequence: safeSequence(input.sequence, "acknowledged sequence"),
644
+ stateHash: requireSha256(input.stateHash, "acknowledged stateHash"),
645
+ });
646
+ if (frame.byteLength > this.limits.maxClientFrameBytes) {
647
+ throw new TypeError("acknowledgement exceeds negotiated live limit");
648
+ }
649
+ try {
650
+ this.socket.send(frame);
651
+ } catch (cause) {
652
+ throw new EditableArtifactTransportError("live acknowledgement send failed", { cause });
653
+ }
654
+ for (const [start, transaction] of this.replayByStart) {
655
+ if (transaction.endSequence <= input.sequence) this.replayByStart.delete(start);
656
+ }
657
+ }
658
+
659
+ close(reason = "closed"): void {
660
+ if (this.terminal) return;
661
+ this.localClose = true;
662
+ try {
663
+ this.socket.close(1000, boundedCloseReason(reason));
664
+ } finally {
665
+ this.finish({ reason: "closed" });
666
+ }
667
+ }
668
+
669
+ private readonly handleOpen = (): void => {
670
+ if (this.signal.aborted) {
671
+ this.close("aborted");
672
+ return;
673
+ }
674
+ try {
675
+ if (this.socket.protocol !== LIVE_SUBPROTOCOL) {
676
+ throw new EditableArtifactTransportError(
677
+ "editable artifact WebSocket did not negotiate the required subprotocol",
678
+ { code: "protocol_mismatch" },
679
+ );
680
+ }
681
+ this.socket.send(
682
+ encodeEditableArtifactLiveOpenWireFrame({
683
+ type: "open",
684
+ protocolVersion: this.ticket.protocolVersion,
685
+ artifactId: this.ticket.artifactId,
686
+ token: this.ticket.token,
687
+ resume: this.resume,
688
+ }),
689
+ );
690
+ } catch (cause) {
691
+ this.fail(
692
+ cause instanceof EditableArtifactTransportError
693
+ ? cause
694
+ : new EditableArtifactTransportError("live open frame send failed", { cause }),
695
+ );
696
+ }
697
+ };
698
+
699
+ private readonly handleMessage = (event: EditableArtifactWebSocketMessageEvent): void => {
700
+ this.messageTail = this.messageTail
701
+ .then(async () => {
702
+ const bytes = await binaryMessage(event.data, this.maximumServerFrameBytes);
703
+ this.handleServerFrame(decodeEditableArtifactLiveServerWireFrame(bytes));
704
+ })
705
+ .catch((cause: unknown) => {
706
+ this.fail(
707
+ cause instanceof Error
708
+ ? cause
709
+ : new EditableArtifactTransportError("invalid live server frame", { cause }),
710
+ );
711
+ });
712
+ };
713
+
714
+ private readonly handleError = (): void => {
715
+ this.fail(new EditableArtifactTransportError("editable artifact WebSocket failed"));
716
+ };
717
+
718
+ private readonly handleClose = (event: EditableArtifactWebSocketCloseEvent): void => {
719
+ if (this.terminal) return;
720
+ const reason = event.reason ?? "transport_error";
721
+ if (RESYNC_CLOSE_REASONS.has(reason)) {
722
+ this.onMessage({
723
+ type: "resync_required",
724
+ artifactId: this.ticket.artifactId,
725
+ reason,
726
+ });
727
+ }
728
+ this.finish({ reason: this.localClose ? "closed" : closeReason(reason) });
729
+ };
730
+
731
+ private readonly handleAbort = (): void => this.close("aborted");
732
+
733
+ private handleServerFrame(frame: EditableArtifactLiveServerFrame): void {
734
+ if (this.terminal) return;
735
+ if (this.openFrame === null) {
736
+ if (frame.type !== "open") throw new TypeError("first live server frame must be open");
737
+ this.validateIdentity(frame);
738
+ this.validateNegotiatedLimits(frame);
739
+ this.openFrame = frame;
740
+ this.targetHead = frame.headSequence;
741
+ this.onMessage({
742
+ type: "head",
743
+ artifactId: this.ticket.artifactId,
744
+ headSequence: frame.headSequence,
745
+ });
746
+ this.readyDeferred.resolve();
747
+ return;
748
+ }
749
+ if (frame.type === "open") throw new TypeError("live server sent a second open frame");
750
+ this.validateIdentity(frame);
751
+ switch (frame.type) {
752
+ case "snapshot":
753
+ this.consumeSnapshot(frame);
754
+ return;
755
+ case "transaction": {
756
+ if (frame.transaction.artifactId !== this.ticket.artifactId) {
757
+ throw new TypeError("live transaction belongs to another artifact");
758
+ }
759
+ if (transactionModality(frame.transaction) !== this.ticket.modality) {
760
+ throw new TypeError("live transaction modality mismatch");
761
+ }
762
+ if (
763
+ (frame.transaction.modality === undefined ||
764
+ frame.transaction.modality === "spreadsheet") &&
765
+ frame.transaction.protocolVersion !== this.ticket.protocolVersion
766
+ )
767
+ throw new TypeError("live transaction protocol mismatch");
768
+ if (
769
+ frame.transaction.committedTransactionBytes.byteLength >
770
+ this.limits.maxCommittedTransactionBytes
771
+ ) {
772
+ throw new RangeError("live transaction exceeds negotiated committed byte limit");
773
+ }
774
+ const transaction = cloneCommitted(frame.transaction);
775
+ const existing = this.replayByStart.get(transaction.startSequence);
776
+ if (existing && existing.transactionId !== transaction.transactionId) {
777
+ throw new TypeError("live stream reused a committed sequence");
778
+ }
779
+ this.replayByStart.set(transaction.startSequence, transaction);
780
+ if (this.submissions.has(transaction.requestHash)) {
781
+ this.transactionsById.set(transaction.transactionId, transaction);
782
+ }
783
+ this.targetHead = Math.max(this.targetHead, transaction.endSequence);
784
+ this.firstPostOpen.resolve("transaction");
785
+ if (this.bootstrapDelivered) {
786
+ this.onMessage({
787
+ type: "transaction.committed",
788
+ transaction: cloneCommitted(transaction),
789
+ });
790
+ }
791
+ this.trySettleAccepted(transaction.transactionId);
792
+ this.notifyProgress();
793
+ return;
794
+ }
795
+ case "barrier":
796
+ this.barrierSequence = frame.sequence;
797
+ this.targetHead = Math.max(this.targetHead, frame.sequence);
798
+ this.firstPostOpen.resolve("barrier");
799
+ this.onMessage({
800
+ type: "head",
801
+ artifactId: this.ticket.artifactId,
802
+ headSequence: frame.sequence,
803
+ });
804
+ this.notifyProgress();
805
+ return;
806
+ case "watermark":
807
+ this.targetHead = Math.max(this.targetHead, frame.headSequence);
808
+ this.onMessage({
809
+ type: "head",
810
+ artifactId: this.ticket.artifactId,
811
+ headSequence: frame.headSequence,
812
+ });
813
+ this.notifyProgress();
814
+ return;
815
+ case "resyncRequired":
816
+ this.firstPostOpen.resolve("resync");
817
+ this.onMessage({
818
+ type: "resync_required",
819
+ artifactId: this.ticket.artifactId,
820
+ reason: frame.reason,
821
+ });
822
+ this.notifyProgress();
823
+ return;
824
+ case "authorizationChanged":
825
+ this.onMessage({
826
+ type: "authorization",
827
+ artifactId: this.ticket.artifactId,
828
+ writable: frame.writable,
829
+ });
830
+ return;
831
+ case "mutationAccepted":
832
+ this.acceptMutation(frame);
833
+ return;
834
+ case "mutationRejected":
835
+ this.rejectMutation(frame);
836
+ return;
837
+ case "applied":
838
+ return;
839
+ }
840
+ }
841
+
842
+ private consumeSnapshot(
843
+ frame: Extract<EditableArtifactLiveServerFrame, { type: "snapshot" }>,
844
+ ): void {
845
+ this.firstPostOpen.resolve("snapshot");
846
+ if (frame.totalBytes > Math.min(this.limits.maxSnapshotBytes, this.maximumSnapshotBytes)) {
847
+ throw new RangeError("live snapshot exceeds negotiated browser limit");
848
+ }
849
+ if (frame.protocolVersion !== this.ticket.protocolVersion) {
850
+ throw new TypeError("live snapshot protocol mismatch");
851
+ }
852
+ if (serverModality(frame.modality) !== this.ticket.modality) {
853
+ throw new TypeError("live snapshot modality mismatch");
854
+ }
855
+ if (this.snapshotAssembly === null) {
856
+ if (frame.offset !== 0) throw new TypeError("live snapshot does not start at byte zero");
857
+ const { bytes: _bytes, offset: _offset, final: _final, ...metadata } = frame;
858
+ this.snapshotAssembly = {
859
+ metadata,
860
+ bytes: new Uint8Array(frame.totalBytes),
861
+ nextOffset: 0,
862
+ };
863
+ }
864
+ const assembly = this.snapshotAssembly;
865
+ if (
866
+ frame.offset !== assembly.nextOffset ||
867
+ frame.totalBytes !== assembly.metadata.totalBytes ||
868
+ frame.sequence !== assembly.metadata.sequence ||
869
+ frame.stateHash !== assembly.metadata.stateHash ||
870
+ frame.digest !== assembly.metadata.digest ||
871
+ frame.kernelVersion !== assembly.metadata.kernelVersion ||
872
+ frame.modelSchemaVersion !== assembly.metadata.modelSchemaVersion ||
873
+ !snapshotChunkAuthorityEqual(frame, assembly.metadata)
874
+ ) {
875
+ throw new TypeError("live snapshot chunks do not share one identity");
876
+ }
877
+ assembly.bytes.set(frame.bytes, frame.offset);
878
+ assembly.nextOffset += frame.bytes.byteLength;
879
+ if (!frame.final) return;
880
+ if (assembly.nextOffset !== assembly.bytes.byteLength) {
881
+ throw new TypeError("live snapshot is incomplete");
882
+ }
883
+ const commonSnapshot = {
884
+ artifactId: this.ticket.artifactId,
885
+ sequence: frame.sequence,
886
+ stateHash: frame.stateHash,
887
+ digest: frame.digest,
888
+ kernelVersion: frame.kernelVersion,
889
+ modelSchemaVersion: frame.modelSchemaVersion,
890
+ bytes: assembly.bytes,
891
+ } as const;
892
+ const snapshot: EditableArtifactSnapshot =
893
+ "causalFrontier" in frame
894
+ ? Object.freeze({
895
+ ...commonSnapshot,
896
+ modality: "spreadsheet" as const,
897
+ causalFrontier: cloneFrontier(frame.causalFrontier),
898
+ protocolVersion: frame.protocolVersion,
899
+ })
900
+ : Object.freeze({
901
+ ...commonSnapshot,
902
+ modality: requireSerializedModality(frame.modality),
903
+ nativeRevision: frame.nativeRevision,
904
+ });
905
+ this.snapshot = snapshot;
906
+ this.snapshotDeferred.resolve(snapshot);
907
+ this.notifyProgress();
908
+ }
909
+
910
+ private acceptMutation(
911
+ frame: Extract<EditableArtifactLiveServerFrame, { type: "mutationAccepted" }>,
912
+ ): void {
913
+ const submission = this.submissions.get(frame.requestHash);
914
+ if (!submission) return;
915
+ if (frame.clientTransactionId !== submission.pending.clientTransactionId) {
916
+ throw new TypeError("mutation acceptance changed the client transaction identity");
917
+ }
918
+ submission.accepted = frame;
919
+ this.trySettleAccepted(frame.transactionId);
920
+ }
921
+
922
+ private trySettleAccepted(transactionId: string): void {
923
+ const transaction = this.transactionsById.get(transactionId);
924
+ if (!transaction) return;
925
+ const submission = this.submissions.get(transaction.requestHash);
926
+ if (!submission?.accepted || submission.accepted.transactionId !== transactionId) return;
927
+ const accepted = submission.accepted;
928
+ if (
929
+ accepted.startSequence !== transaction.startSequence ||
930
+ accepted.endSequence !== transaction.endSequence ||
931
+ accepted.stateHash !== transaction.stateHash ||
932
+ accepted.requestHash !== transaction.requestHash
933
+ ) {
934
+ throw new TypeError("mutation acceptance does not match its committed transaction");
935
+ }
936
+ clearTimeout(submission.timeout);
937
+ this.submissions.delete(transaction.requestHash);
938
+ this.transactionsById.delete(transaction.transactionId);
939
+ submission.deferred.resolve({
940
+ artifactId: this.ticket.artifactId,
941
+ clientTransactionId: accepted.clientTransactionId,
942
+ transactionId: transaction.transactionId,
943
+ requestHash: transaction.requestHash,
944
+ committed: cloneCommitted(transaction),
945
+ });
946
+ }
947
+
948
+ private rejectMutation(
949
+ frame: Extract<EditableArtifactLiveServerFrame, { type: "mutationRejected" }>,
950
+ ): void {
951
+ const submission = this.submissions.get(frame.requestHash);
952
+ if (!submission) return;
953
+ clearTimeout(submission.timeout);
954
+ this.submissions.delete(frame.requestHash);
955
+ submission.deferred.reject(
956
+ new EditableArtifactTransportError(`editable artifact mutation rejected: ${frame.code}`, {
957
+ code: frame.code,
958
+ retryable: frame.retryable,
959
+ outcomeUnknown: false,
960
+ }),
961
+ );
962
+ }
963
+
964
+ private validateIdentity(frame: EditableArtifactLiveServerFrame): void {
965
+ if (frame.artifactId !== this.ticket.artifactId) {
966
+ throw new TypeError("live server frame belongs to another artifact");
967
+ }
968
+ if (frame.protocolVersion !== this.ticket.protocolVersion) {
969
+ throw new TypeError("live server frame protocol mismatch");
970
+ }
971
+ if (frame.type === "open" && serverModality(frame.modality) !== this.ticket.modality) {
972
+ throw new TypeError("live open modality does not match its ticket");
973
+ }
974
+ if (this.openFrame && frame.streamEpoch !== this.openFrame.streamEpoch) {
975
+ throw new TypeError("live server frame belongs to another stream epoch");
976
+ }
977
+ }
978
+
979
+ private validateNegotiatedLimits(
980
+ frame: Extract<EditableArtifactLiveServerFrame, { type: "open" }>,
981
+ ): void {
982
+ if (
983
+ frame.maxCommandBytes > EDITABLE_ARTIFACT_COMMAND_MAX_BYTES ||
984
+ frame.maxIntentBytes > EDITABLE_ARTIFACT_INTENT_MAX_BYTES ||
985
+ frame.maxCommittedTransactionBytes > MAX_COMMITTED_TRANSACTION_BYTES
986
+ ) {
987
+ throw new TypeError("live server advertised incompatible protocol limits");
988
+ }
989
+ }
990
+
991
+ private requireOpen(): Extract<EditableArtifactLiveServerFrame, { type: "open" }> {
992
+ if (!this.openFrame) throw new TypeError("live stream has not completed its handshake");
993
+ return this.openFrame;
994
+ }
995
+
996
+ private throwIfTerminal(): void {
997
+ if (this.terminal) throw this.terminal;
998
+ this.signal.throwIfAborted();
999
+ }
1000
+
1001
+ private waitForProgress(signal: AbortSignal): Promise<void> {
1002
+ this.throwIfTerminal();
1003
+ const deferred = new Deferred<void>();
1004
+ this.progressWaiters.add(deferred);
1005
+ return abortable(deferred.promise, signal).finally(() => this.progressWaiters.delete(deferred));
1006
+ }
1007
+
1008
+ private notifyProgress(): void {
1009
+ for (const waiter of this.progressWaiters) waiter.resolve();
1010
+ this.progressWaiters.clear();
1011
+ }
1012
+
1013
+ private fail(error: Error): void {
1014
+ if (this.terminal) return;
1015
+ this.finish({ reason: "transport_error", error });
1016
+ try {
1017
+ this.socket.close(4400, "invalid_frame");
1018
+ } catch {
1019
+ // Terminal state above is authoritative even if the host socket throws.
1020
+ }
1021
+ }
1022
+
1023
+ private finish(input: { reason: EditableArtifactLiveClose["reason"]; error?: Error }): void {
1024
+ if (this.terminal) return;
1025
+ clearTimeout(this.handshakeTimer);
1026
+ this.terminal =
1027
+ input.error ?? new EditableArtifactTransportError("editable artifact live stream closed");
1028
+ this.socket.removeEventListener("open", this.handleOpen);
1029
+ this.socket.removeEventListener("message", this.handleMessage);
1030
+ this.socket.removeEventListener("error", this.handleError);
1031
+ this.socket.removeEventListener("close", this.handleClose);
1032
+ this.signal.removeEventListener("abort", this.handleAbort);
1033
+ this.readyDeferred.reject(this.terminal);
1034
+ this.firstPostOpen.reject(this.terminal);
1035
+ this.snapshotDeferred.reject(this.terminal);
1036
+ this.notifyProgress();
1037
+ for (const submission of this.submissions.values()) {
1038
+ clearTimeout(submission.timeout);
1039
+ submission.deferred.reject(
1040
+ new EditableArtifactTransportError("live stream closed before mutation outcome", {
1041
+ code: "transport_error",
1042
+ outcomeUnknown: submission.sent,
1043
+ cause: input.error,
1044
+ }),
1045
+ );
1046
+ }
1047
+ this.submissions.clear();
1048
+ this.closedDeferred.resolve({
1049
+ reason: input.reason,
1050
+ ...(input.error ? { error: input.error } : {}),
1051
+ });
1052
+ }
1053
+ }
1054
+
1055
+ class Deferred<T> {
1056
+ readonly promise: Promise<T>;
1057
+ private resolvePromise!: (value: T | PromiseLike<T>) => void;
1058
+ private rejectPromise!: (reason?: unknown) => void;
1059
+ private settled = false;
1060
+
1061
+ constructor() {
1062
+ this.promise = new Promise<T>((resolve, reject) => {
1063
+ this.resolvePromise = resolve;
1064
+ this.rejectPromise = reject;
1065
+ });
1066
+ // Several lifecycle barriers are intentionally rejectable before a caller
1067
+ // starts awaiting them; mark the rejection observed without changing it.
1068
+ void this.promise.catch(() => undefined);
1069
+ }
1070
+
1071
+ resolve(
1072
+ value: T extends void ? undefined : T = undefined as T extends void ? undefined : T,
1073
+ ): void {
1074
+ if (this.settled) return;
1075
+ this.settled = true;
1076
+ this.resolvePromise(value as T);
1077
+ }
1078
+
1079
+ reject(reason: unknown): void {
1080
+ if (this.settled) return;
1081
+ this.settled = true;
1082
+ this.rejectPromise(reason);
1083
+ }
1084
+ }
1085
+
1086
+ function cloneCommitted(
1087
+ transaction: EditableArtifactCommittedTransaction | ContractCommittedTransaction,
1088
+ ): EditableArtifactCommittedTransaction {
1089
+ if (transaction.modality === undefined || transaction.modality === "spreadsheet") {
1090
+ return {
1091
+ ...transaction,
1092
+ modality: "spreadsheet",
1093
+ causalFrontier: cloneFrontier(transaction.causalFrontier),
1094
+ committedTransactionBytes: transaction.committedTransactionBytes.slice(),
1095
+ };
1096
+ }
1097
+ if (!("priorNativeRevision" in transaction)) {
1098
+ throw new TypeError("serialized transaction lacks native revision metadata");
1099
+ }
1100
+ return {
1101
+ artifactId: transaction.artifactId,
1102
+ transactionId: transaction.transactionId,
1103
+ requestHash: transaction.requestHash,
1104
+ startSequence: transaction.startSequence,
1105
+ endSequence: transaction.endSequence,
1106
+ priorStateHash: transaction.priorStateHash,
1107
+ stateHash: transaction.stateHash,
1108
+ modality: requireSerializedModality(transaction.modality),
1109
+ priorNativeRevision: transaction.priorNativeRevision,
1110
+ nativeRevision: transaction.nativeRevision,
1111
+ commitProtocolVersion: transaction.commitProtocolVersion,
1112
+ committedTransactionBytes: transaction.committedTransactionBytes.slice(),
1113
+ };
1114
+ }
1115
+
1116
+ function transactionModality(
1117
+ transaction: EditableArtifactCommittedTransaction | ContractCommittedTransaction,
1118
+ ): EditableArtifactModality {
1119
+ return transaction.modality ?? "spreadsheet";
1120
+ }
1121
+
1122
+ function serverModality(value: EditableArtifactModality | undefined): EditableArtifactModality {
1123
+ return value ?? "spreadsheet";
1124
+ }
1125
+
1126
+ function serializedBoundaryRevision(
1127
+ snapshot: EditableArtifactSnapshot | null,
1128
+ resume:
1129
+ | { modality: "spreadsheet"; localCausalFrontier: EditableArtifactCausalFrontier }
1130
+ | { modality: "document" | "presentation"; localNativeRevision: number | null },
1131
+ modality: "document" | "presentation",
1132
+ ): number {
1133
+ if (snapshot !== null) {
1134
+ if (snapshot.modality !== modality) throw new TypeError("snapshot modality mismatch");
1135
+ return snapshot.nativeRevision;
1136
+ }
1137
+ if (resume.modality !== modality || resume.localNativeRevision === null) {
1138
+ throw new EditableArtifactSyncError(
1139
+ "invalid_bootstrap",
1140
+ "serialized live stream established no native revision boundary",
1141
+ { requiresSnapshot: true },
1142
+ );
1143
+ }
1144
+ return resume.localNativeRevision;
1145
+ }
1146
+
1147
+ function snapshotChunkAuthorityEqual(
1148
+ frame: Extract<EditableArtifactLiveServerFrame, { type: "snapshot" }>,
1149
+ metadata: SnapshotAssembly["metadata"],
1150
+ ): boolean {
1151
+ const modality = serverModality(frame.modality);
1152
+ if (modality !== serverModality(metadata.modality)) return false;
1153
+ if ("causalFrontier" in frame) {
1154
+ if (!("causalFrontier" in metadata)) return false;
1155
+ return frontiersEqual(frame.causalFrontier, metadata.causalFrontier);
1156
+ }
1157
+ if (!("nativeRevision" in metadata)) return false;
1158
+ return frame.nativeRevision === metadata.nativeRevision;
1159
+ }
1160
+
1161
+ function requireSerializedModality(
1162
+ value: EditableArtifactModality | undefined,
1163
+ ): "document" | "presentation" {
1164
+ if (value !== "document" && value !== "presentation") {
1165
+ throw new TypeError("serialized artifact modality is invalid");
1166
+ }
1167
+ return value;
1168
+ }
1169
+
1170
+ function cloneFrontier(frontier: EditableArtifactCausalFrontier): EditableArtifactCausalFrontier {
1171
+ if (!Array.isArray(frontier) || frontier.length > 1_024) {
1172
+ throw new TypeError("causal frontier is invalid");
1173
+ }
1174
+ let previous = "";
1175
+ return frontier.map((entry) => {
1176
+ requireReplicaId(entry.replicaId);
1177
+ if (!Number.isSafeInteger(entry.counter) || entry.counter <= 0 || entry.replicaId <= previous) {
1178
+ throw new TypeError("causal frontier must be positive, sorted, and duplicate-free");
1179
+ }
1180
+ previous = entry.replicaId;
1181
+ return { replicaId: entry.replicaId, counter: entry.counter };
1182
+ });
1183
+ }
1184
+
1185
+ function frontiersEqual(
1186
+ left: EditableArtifactCausalFrontier,
1187
+ right: EditableArtifactCausalFrontier,
1188
+ ): boolean {
1189
+ return (
1190
+ left.length === right.length &&
1191
+ left.every(
1192
+ (entry, index) =>
1193
+ entry.replicaId === right[index]?.replicaId && entry.counter === right[index]?.counter,
1194
+ )
1195
+ );
1196
+ }
1197
+
1198
+ async function binaryMessage(value: unknown, maximumBytes: number): Promise<Uint8Array> {
1199
+ if (value instanceof ArrayBuffer) {
1200
+ if (value.byteLength > maximumBytes)
1201
+ throw new RangeError("live server frame exceeds byte limit");
1202
+ return new Uint8Array(value);
1203
+ }
1204
+ if (ArrayBuffer.isView(value)) {
1205
+ if (value.byteLength > maximumBytes)
1206
+ throw new RangeError("live server frame exceeds byte limit");
1207
+ return new Uint8Array(value.buffer, value.byteOffset, value.byteLength).slice();
1208
+ }
1209
+ if (typeof Blob !== "undefined" && value instanceof Blob) {
1210
+ if (value.size > maximumBytes) throw new RangeError("live server frame exceeds byte limit");
1211
+ return new Uint8Array(await value.arrayBuffer());
1212
+ }
1213
+ throw new TypeError("editable artifact live transport accepts binary frames only");
1214
+ }
1215
+
1216
+ async function readBoundedJson(response: Response, maximumBytes: number): Promise<unknown> {
1217
+ const declared = response.headers.get("content-length");
1218
+ if (declared !== null) {
1219
+ const length = Number(declared);
1220
+ if (!Number.isSafeInteger(length) || length < 0 || length > maximumBytes) {
1221
+ await response.body?.cancel("ticket response exceeds byte limit").catch(() => undefined);
1222
+ throw new TypeError("ticket response content-length is invalid");
1223
+ }
1224
+ }
1225
+ const reader = response.body?.getReader();
1226
+ if (!reader) throw new TypeError("ticket response has no body");
1227
+ const chunks: Uint8Array[] = [];
1228
+ let total = 0;
1229
+ try {
1230
+ while (true) {
1231
+ const result = await reader.read();
1232
+ if (result.done) break;
1233
+ total += result.value.byteLength;
1234
+ if (total > maximumBytes) throw new RangeError("ticket response exceeds byte limit");
1235
+ chunks.push(result.value);
1236
+ }
1237
+ } catch (error) {
1238
+ await reader.cancel("ticket response rejected").catch(() => undefined);
1239
+ throw error;
1240
+ }
1241
+ const bytes = new Uint8Array(total);
1242
+ let offset = 0;
1243
+ for (const chunk of chunks) {
1244
+ bytes.set(chunk, offset);
1245
+ offset += chunk.byteLength;
1246
+ }
1247
+ let text: string;
1248
+ try {
1249
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1250
+ return JSON.parse(text) as unknown;
1251
+ } catch (cause) {
1252
+ throw new TypeError("ticket response is not valid UTF-8 JSON", { cause });
1253
+ }
1254
+ }
1255
+
1256
+ function validateTicket(
1257
+ value: unknown,
1258
+ artifactId: string,
1259
+ replicaId: string,
1260
+ protocolVersion: number,
1261
+ ): EditableArtifactSyncTicket {
1262
+ if (!isPlainRecord(value)) throw new TypeError("ticket response must be an object");
1263
+ const keys = Object.keys(value).sort();
1264
+ const expected = ["artifactId", "expiresAt", "modality", "protocolVersion", "replicaId", "token"];
1265
+ if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) {
1266
+ throw new TypeError("ticket response has unknown or missing fields");
1267
+ }
1268
+ if (
1269
+ value.artifactId !== artifactId ||
1270
+ value.replicaId !== replicaId ||
1271
+ value.protocolVersion !== protocolVersion
1272
+ ) {
1273
+ throw new TypeError("ticket response identity does not match its request");
1274
+ }
1275
+ const modality = requireModality(value.modality, "ticket modality");
1276
+ if (
1277
+ typeof value.token !== "string" ||
1278
+ value.token.length > 4_096 ||
1279
+ !/^[A-Za-z0-9._~-]+$/u.test(value.token)
1280
+ ) {
1281
+ throw new TypeError("ticket token is invalid");
1282
+ }
1283
+ if (typeof value.expiresAt !== "string" || !Number.isFinite(Date.parse(value.expiresAt))) {
1284
+ throw new TypeError("ticket expiry is invalid");
1285
+ }
1286
+ return {
1287
+ artifactId,
1288
+ modality,
1289
+ replicaId,
1290
+ token: value.token,
1291
+ expiresAt: value.expiresAt,
1292
+ protocolVersion,
1293
+ };
1294
+ }
1295
+
1296
+ function requireModality(value: unknown, label: string): EditableArtifactModality {
1297
+ if (value !== "document" && value !== "spreadsheet" && value !== "presentation") {
1298
+ throw new TypeError(`${label} is invalid`);
1299
+ }
1300
+ return value;
1301
+ }
1302
+
1303
+ function transportBaseUrl(value: string | URL): URL {
1304
+ const base = resolveUrl(value, "baseUrl");
1305
+ if (base.username || base.password || base.search || base.hash) {
1306
+ throw new TypeError("baseUrl must not contain credentials, query, or fragment");
1307
+ }
1308
+ if (base.protocol !== "http:" && base.protocol !== "https:") {
1309
+ throw new TypeError("baseUrl must use HTTP or HTTPS");
1310
+ }
1311
+ return base;
1312
+ }
1313
+
1314
+ function socketUrl(value: string | URL | undefined, baseUrl: URL): URL {
1315
+ const url =
1316
+ value === undefined ? childUrl(baseUrl, LIVE_PATH) : resolveUrl(value, "webSocketUrl");
1317
+ if (url.username || url.password || url.search || url.hash) {
1318
+ throw new TypeError("webSocketUrl must not contain credentials, query, or fragment");
1319
+ }
1320
+ url.protocol =
1321
+ url.protocol === "https:" ? "wss:" : url.protocol === "http:" ? "ws:" : url.protocol;
1322
+ if (url.protocol !== "ws:" && url.protocol !== "wss:") {
1323
+ throw new TypeError("webSocketUrl must use WS or WSS");
1324
+ }
1325
+ return url;
1326
+ }
1327
+
1328
+ function childUrl(base: URL, path: string): URL {
1329
+ const result = new URL(base.href);
1330
+ result.pathname = `${result.pathname.replace(/\/+$/u, "")}${path}`;
1331
+ result.search = "";
1332
+ result.hash = "";
1333
+ return result;
1334
+ }
1335
+
1336
+ function resolveUrl(value: string | URL, label: string): URL {
1337
+ if (value instanceof URL) return new URL(value.href);
1338
+ try {
1339
+ return new URL(value, typeof location === "undefined" ? undefined : location.href);
1340
+ } catch (cause) {
1341
+ throw new TypeError(`${label} must be an absolute URL outside a browser`, { cause });
1342
+ }
1343
+ }
1344
+
1345
+ function requireTransportSecurity(base: URL, socket: URL, allowInsecure: boolean): void {
1346
+ if (allowInsecure) return;
1347
+ if (base.protocol !== "https:" || socket.protocol !== "wss:") {
1348
+ throw new TypeError("editable artifact production transport requires HTTPS and WSS");
1349
+ }
1350
+ }
1351
+
1352
+ function defaultWebSocketFactory(url: string, protocol: string): EditableArtifactWebSocketLike {
1353
+ if (typeof WebSocket === "undefined") {
1354
+ throw new TypeError("WebSocket is unavailable; provide webSocketFactory");
1355
+ }
1356
+ return new WebSocket(url, protocol) as unknown as EditableArtifactWebSocketLike;
1357
+ }
1358
+
1359
+ function abortable<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
1360
+ signal.throwIfAborted();
1361
+ return new Promise<T>((resolve, reject) => {
1362
+ const abort = (): void => reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
1363
+ signal.addEventListener("abort", abort, { once: true });
1364
+ void promise.then(
1365
+ (value) => {
1366
+ signal.removeEventListener("abort", abort);
1367
+ resolve(value);
1368
+ },
1369
+ (error: unknown) => {
1370
+ signal.removeEventListener("abort", abort);
1371
+ reject(error);
1372
+ },
1373
+ );
1374
+ });
1375
+ }
1376
+
1377
+ function requireStableId(value: unknown, label: string): asserts value is string {
1378
+ if (typeof value !== "string" || !STABLE_ID.test(value)) {
1379
+ throw new TypeError(`${label} must be 32 lowercase nonzero hex characters`);
1380
+ }
1381
+ }
1382
+
1383
+ function requireReplicaId(value: unknown): asserts value is string {
1384
+ if (typeof value !== "string" || !REPLICA_ID.test(value)) {
1385
+ throw new TypeError("replicaId must be 16 lowercase nonzero hex characters");
1386
+ }
1387
+ }
1388
+
1389
+ function requireSha256(value: unknown, label: string): string {
1390
+ if (typeof value !== "string" || !SHA256.test(value)) throw new TypeError(`${label} is invalid`);
1391
+ return value;
1392
+ }
1393
+
1394
+ function boundedString(value: unknown, label: string, maximum: number): string {
1395
+ if (
1396
+ typeof value !== "string" ||
1397
+ value.length === 0 ||
1398
+ value.length > maximum ||
1399
+ value.trim() !== value
1400
+ ) {
1401
+ throw new TypeError(`${label} is invalid`);
1402
+ }
1403
+ return value;
1404
+ }
1405
+
1406
+ function safeSequence(value: unknown, label: string): number {
1407
+ if (!Number.isSafeInteger(value) || (value as number) < 0)
1408
+ throw new TypeError(`${label} is invalid`);
1409
+ return value as number;
1410
+ }
1411
+
1412
+ function positiveU16(value: unknown, label: string): number {
1413
+ const number = positiveBounded(value, 0xffff, label);
1414
+ return number;
1415
+ }
1416
+
1417
+ function positiveBounded(value: unknown, maximum: number, label: string): number {
1418
+ if (!Number.isSafeInteger(value) || (value as number) <= 0 || (value as number) > maximum) {
1419
+ throw new TypeError(`${label} is invalid`);
1420
+ }
1421
+ return value as number;
1422
+ }
1423
+
1424
+ function boundedCloseReason(value: string): string {
1425
+ return value.length <= 100 && /^[\x20-\x7e]*$/u.test(value) ? value : "closed";
1426
+ }
1427
+
1428
+ function closeReason(value: string): EditableArtifactLiveClose["reason"] {
1429
+ if (value === "permission_changed") return "permission_changed";
1430
+ if (value === "ticket_expired" || value === "ticket_replayed") return "ticket_expired";
1431
+ return value === "closed" ? "closed" : "transport_error";
1432
+ }
1433
+
1434
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
1435
+ return (
1436
+ typeof value === "object" &&
1437
+ value !== null &&
1438
+ !Array.isArray(value) &&
1439
+ Object.getPrototypeOf(value) === Object.prototype
1440
+ );
1441
+ }
1442
+
1443
+ const RESYNC_CLOSE_REASONS = new Set([
1444
+ "retention_gap",
1445
+ "durable_gap",
1446
+ "invalid_ack",
1447
+ "slow_client",
1448
+ "oversized_frame",
1449
+ ]);