@opengeni/core 0.21.10 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access/index.d.ts +1 -1
- package/dist/application/new-session-drafts.d.ts +1 -1
- package/dist/application/session-commands.d.ts +2 -2
- package/dist/billing/limits.d.ts +1 -1
- package/dist/canonical-human-identities.d.ts +12 -0
- package/dist/canonical-human-identities.js +23 -0
- package/dist/canonical-human-identities.js.map +1 -0
- package/dist/chunk-6WKPWE5S.js +2380 -0
- package/dist/chunk-6WKPWE5S.js.map +1 -0
- package/dist/chunk-IBOEYG6N.js +34 -0
- package/dist/chunk-IBOEYG6N.js.map +1 -0
- package/dist/chunk-JJU6I5XD.js +2713 -0
- package/dist/chunk-JJU6I5XD.js.map +1 -0
- package/dist/chunk-NYPMQ7NO.js +2270 -0
- package/dist/chunk-NYPMQ7NO.js.map +1 -0
- package/dist/dependencies.d.ts +30 -6
- package/dist/domain/capabilities.d.ts +28 -3
- package/dist/domain/company-profile-durable-learning-adapter.d.ts +39 -0
- package/dist/domain/conversation-integrations.d.ts +225 -0
- package/dist/domain/durable-learning-slack-publication.d.ts +72 -0
- package/dist/domain/editable-artifacts/agent-application.d.ts +192 -0
- package/dist/domain/editable-artifacts/durable-export-postgres.d.ts +4 -0
- package/dist/domain/editable-artifacts/durable-export.d.ts +212 -0
- package/dist/domain/editable-artifacts/errors.d.ts +47 -0
- package/dist/domain/editable-artifacts/hash.d.ts +11 -0
- package/dist/domain/editable-artifacts/in-memory.d.ts +99 -0
- package/dist/domain/editable-artifacts/index.d.ts +14 -0
- package/dist/domain/editable-artifacts/ports.d.ts +341 -0
- package/dist/domain/editable-artifacts/postgres-adapters.d.ts +13 -0
- package/dist/domain/editable-artifacts/service.d.ts +105 -0
- package/dist/domain/editable-artifacts/snapshot-compaction.d.ts +44 -0
- package/dist/domain/editable-artifacts/snapshot-verifier-genesis.d.ts +50 -0
- package/dist/domain/editable-artifacts/snapshot-verifier-materialization.d.ts +21 -0
- package/dist/domain/editable-artifacts/snapshot-verifier.d.ts +118 -0
- package/dist/domain/editable-artifacts/types.d.ts +410 -0
- package/dist/domain/fiken.d.ts +35 -0
- package/dist/domain/memory-slack-delivery.d.ts +19 -0
- package/dist/domain/packs.d.ts +29 -1
- package/dist/domain/personal-connection-delegations.d.ts +9 -1
- package/dist/domain/scheduled-tasks.d.ts +2 -2
- package/dist/domain/session-tool-policy.d.ts +2 -2
- package/dist/domain/sessions.d.ts +24 -8
- package/dist/domain/skill-imports.d.ts +32 -0
- package/dist/domain/slack-publication-secret-safety.d.ts +8 -0
- package/dist/domain/timeline-annotations.d.ts +5 -0
- package/dist/domain/video-generation-capabilities.d.ts +10 -0
- package/dist/domain/video-generation.d.ts +47 -0
- package/dist/editable-artifact-live/application.d.ts +69 -0
- package/dist/editable-artifact-live/errors.d.ts +1 -0
- package/dist/editable-artifact-live/index.d.ts +8 -0
- package/dist/editable-artifact-live/outbox-dispatcher.d.ts +162 -0
- package/dist/editable-artifact-live/ports.d.ts +100 -0
- package/dist/editable-artifact-live/server.d.ts +71 -0
- package/dist/editable-artifact-live/ticket.d.ts +40 -0
- package/dist/editable-artifact-live/types.d.ts +213 -0
- package/dist/editable-artifact-live/wire.d.ts +34 -0
- package/dist/editable-artifact-live.d.ts +2 -0
- package/dist/editable-artifact-live.js +58 -0
- package/dist/editable-artifact-live.js.map +1 -0
- package/dist/editable-artifacts.d.ts +2 -0
- package/dist/editable-artifacts.js +179 -0
- package/dist/editable-artifacts.js.map +1 -0
- package/dist/index.d.ts +37 -26
- package/dist/index.js +4145 -959
- package/dist/index.js.map +1 -1
- package/dist/managed-session.d.ts +6 -2
- package/dist/rigs/index.d.ts +1 -1
- package/dist/rigs/provider-images.d.ts +31 -0
- package/dist/sandbox/fleet.d.ts +8 -5
- package/dist/sandbox/routing.d.ts +1 -0
- package/dist/session-authorization.d.ts +4 -2
- package/dist/transcription.d.ts +5 -0
- package/dist/workflow-wake-contract.d.ts +7 -0
- package/package.json +22 -10
- package/src/access/index.ts +14 -2
- package/src/application/new-session-drafts.ts +2 -0
- package/src/application/session-commands.ts +188 -107
- package/src/canonical-human-identities.ts +27 -0
- package/src/dependencies.ts +34 -2
- package/src/domain/capabilities.ts +729 -408
- package/src/domain/company-profile-durable-learning-adapter.ts +221 -0
- package/src/domain/conversation-integrations.ts +1238 -0
- package/src/domain/durable-learning-slack-publication.ts +202 -0
- package/src/domain/editable-artifacts/agent-application.ts +687 -0
- package/src/domain/editable-artifacts/durable-export-postgres.ts +139 -0
- package/src/domain/editable-artifacts/durable-export.ts +734 -0
- package/src/domain/editable-artifacts/errors.ts +115 -0
- package/src/domain/editable-artifacts/hash.ts +131 -0
- package/src/domain/editable-artifacts/in-memory.ts +1662 -0
- package/src/domain/editable-artifacts/index.ts +14 -0
- package/src/domain/editable-artifacts/ports.ts +462 -0
- package/src/domain/editable-artifacts/postgres-adapters.ts +49 -0
- package/src/domain/editable-artifacts/service.ts +2402 -0
- package/src/domain/editable-artifacts/snapshot-compaction.ts +246 -0
- package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +235 -0
- package/src/domain/editable-artifacts/snapshot-verifier-materialization.ts +74 -0
- package/src/domain/editable-artifacts/snapshot-verifier.ts +687 -0
- package/src/domain/editable-artifacts/types.ts +835 -0
- package/src/domain/fiken.ts +88 -0
- package/src/domain/insights.ts +163 -50
- package/src/domain/memory-slack-delivery.ts +204 -0
- package/src/domain/memory-slack-publication.ts +5 -2
- package/src/domain/packs.ts +266 -5
- package/src/domain/personal-connection-delegations.ts +97 -6
- package/src/domain/scheduled-tasks.ts +171 -29
- package/src/domain/session-tool-policy.ts +8 -2
- package/src/domain/sessions.ts +395 -148
- package/src/domain/skill-imports.ts +350 -0
- package/src/domain/slack-publication-secret-safety.ts +42 -0
- package/src/domain/timeline-annotations.ts +204 -0
- package/src/domain/video-generation-capabilities.ts +104 -0
- package/src/domain/video-generation.ts +249 -0
- package/src/editable-artifact-live/application.ts +182 -0
- package/src/editable-artifact-live/errors.ts +4 -0
- package/src/editable-artifact-live/index.ts +8 -0
- package/src/editable-artifact-live/outbox-dispatcher.ts +916 -0
- package/src/editable-artifact-live/ports.ts +139 -0
- package/src/editable-artifact-live/server.ts +1707 -0
- package/src/editable-artifact-live/ticket.ts +217 -0
- package/src/editable-artifact-live/types.ts +286 -0
- package/src/editable-artifact-live/wire.ts +95 -0
- package/src/editable-artifact-live.ts +2 -0
- package/src/editable-artifacts.ts +2 -0
- package/src/index.ts +15 -0
- package/src/managed-session.ts +19 -2
- package/src/rigs/index.ts +26 -19
- package/src/rigs/provider-images.ts +110 -0
- package/src/sandbox/fleet.ts +72 -66
- package/src/sandbox/routing.ts +6 -3
- package/src/session-authorization.ts +187 -52
- package/src/transcription.ts +5 -0
- package/src/workflow-wake-contract.ts +8 -0
|
@@ -0,0 +1,2270 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EDITABLE_ARTIFACT_MAX_COMMITTED_TRANSACTION_BYTES,
|
|
3
|
+
assertIsoTimestamp,
|
|
4
|
+
assertNonnegativeSafeInteger,
|
|
5
|
+
assertPositiveSafeInteger,
|
|
6
|
+
editableArtifactActorKey,
|
|
7
|
+
editableArtifactCausalFrontier,
|
|
8
|
+
editableArtifactClientTransactionId,
|
|
9
|
+
editableArtifactId,
|
|
10
|
+
editableArtifactOutboxId,
|
|
11
|
+
editableArtifactRequestHash,
|
|
12
|
+
editableArtifactScope,
|
|
13
|
+
editableArtifactSnapshotId,
|
|
14
|
+
editableArtifactStateHash,
|
|
15
|
+
editableArtifactTransactionId,
|
|
16
|
+
validateEditableArtifactActor
|
|
17
|
+
} from "./chunk-JJU6I5XD.js";
|
|
18
|
+
|
|
19
|
+
// src/editable-artifact-live/application.ts
|
|
20
|
+
import { EDITABLE_ARTIFACT_KERNEL_VERSION_MAX_BYTES } from "@opengeni/contracts/editable-artifacts";
|
|
21
|
+
var EditableArtifactCompatibilityError = class extends Error {
|
|
22
|
+
code = "unsupported_protocol";
|
|
23
|
+
constructor(message = "Editable artifact runtime is incompatible") {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "EditableArtifactCompatibilityError";
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var EditableArtifactApplication = class {
|
|
29
|
+
constructor(dependencies) {
|
|
30
|
+
this.dependencies = dependencies;
|
|
31
|
+
}
|
|
32
|
+
async createArtifact(input) {
|
|
33
|
+
const scope = editableArtifactScope(input.scope);
|
|
34
|
+
validateEditableArtifactActor(input.actor);
|
|
35
|
+
const result = await this.dependencies.domain.createArtifact({
|
|
36
|
+
scope,
|
|
37
|
+
actor: Object.freeze({ ...input.actor }),
|
|
38
|
+
request: {
|
|
39
|
+
idempotencyKey: editableArtifactClientTransactionId(input.idempotencyKey),
|
|
40
|
+
modality: input.modality,
|
|
41
|
+
title: input.title
|
|
42
|
+
},
|
|
43
|
+
...input.signal ? { signal: input.signal } : {}
|
|
44
|
+
});
|
|
45
|
+
return result.artifact;
|
|
46
|
+
}
|
|
47
|
+
async importArtifact(input) {
|
|
48
|
+
const scope = editableArtifactScope(input.scope);
|
|
49
|
+
validateEditableArtifactActor(input.actor);
|
|
50
|
+
const result = await this.dependencies.domain.importArtifact({
|
|
51
|
+
scope,
|
|
52
|
+
actor: Object.freeze({ ...input.actor }),
|
|
53
|
+
request: {
|
|
54
|
+
idempotencyKey: editableArtifactClientTransactionId(input.idempotencyKey),
|
|
55
|
+
modality: input.modality,
|
|
56
|
+
title: input.title,
|
|
57
|
+
originalImport: input.originalImport,
|
|
58
|
+
snapshot: input.snapshot
|
|
59
|
+
},
|
|
60
|
+
...input.signal ? { signal: input.signal } : {}
|
|
61
|
+
});
|
|
62
|
+
return result.artifact;
|
|
63
|
+
}
|
|
64
|
+
async readArtifact(input) {
|
|
65
|
+
const scope = editableArtifactScope(input.scope);
|
|
66
|
+
const artifactId = editableArtifactId(input.artifactId);
|
|
67
|
+
validateEditableArtifactActor(input.actor);
|
|
68
|
+
return await this.dependencies.domain.getArtifact({
|
|
69
|
+
scope,
|
|
70
|
+
artifactId,
|
|
71
|
+
actor: Object.freeze({ ...input.actor })
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
async mintLiveTicket(input) {
|
|
75
|
+
const artifact = await this.readArtifact(input);
|
|
76
|
+
await this.dependencies.compatibility.assertCompatible({
|
|
77
|
+
artifact,
|
|
78
|
+
protocolVersion: positiveInteger(input.protocolVersion, "protocolVersion"),
|
|
79
|
+
kernelVersion: boundedVersion(input.kernelVersion, "kernelVersion"),
|
|
80
|
+
modelSchemaVersion: positiveInteger(input.modelSchemaVersion, "modelSchemaVersion")
|
|
81
|
+
});
|
|
82
|
+
return await this.dependencies.live.mintTicket({
|
|
83
|
+
scope: artifact.scope,
|
|
84
|
+
artifactId: artifact.id,
|
|
85
|
+
modality: artifact.modality,
|
|
86
|
+
actor: input.actor,
|
|
87
|
+
allowEdit: input.allowEdit
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
openLive(input) {
|
|
91
|
+
return this.dependencies.live.openLive(input);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
function positiveInteger(value, label) {
|
|
95
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
96
|
+
throw new TypeError(`${label} must be a positive safe integer`);
|
|
97
|
+
}
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
function boundedVersion(value, label) {
|
|
101
|
+
const bytes = new TextEncoder().encode(value).byteLength;
|
|
102
|
+
if (bytes < 1 || bytes > EDITABLE_ARTIFACT_KERNEL_VERSION_MAX_BYTES || value.trim() !== value) {
|
|
103
|
+
throw new TypeError(`${label} is malformed`);
|
|
104
|
+
}
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/editable-artifact-live/errors.ts
|
|
109
|
+
import {
|
|
110
|
+
EditableArtifactLiveError
|
|
111
|
+
} from "@opengeni/contracts/editable-artifact-live";
|
|
112
|
+
|
|
113
|
+
// src/editable-artifact-live/outbox-dispatcher.ts
|
|
114
|
+
var textEncoder = new TextEncoder();
|
|
115
|
+
var EDITABLE_ARTIFACT_HINT_MAX_BYTES = 16 * 1024;
|
|
116
|
+
var EDITABLE_ARTIFACT_HINT_SUBJECT_PREFIX = "editable_artifacts.v1";
|
|
117
|
+
var DEFAULTS = Object.freeze({
|
|
118
|
+
batchSize: 32,
|
|
119
|
+
concurrency: 8,
|
|
120
|
+
leaseDurationMs: 3e4,
|
|
121
|
+
leaseRenewIntervalMs: 1e4,
|
|
122
|
+
pollIntervalMs: 500,
|
|
123
|
+
publishTimeoutMs: 5e3,
|
|
124
|
+
retryBaseMs: 500,
|
|
125
|
+
retryMaxMs: 6e4,
|
|
126
|
+
retryJitterRatio: 0.2,
|
|
127
|
+
claimBackoffBaseMs: 500,
|
|
128
|
+
claimBackoffMaxMs: 3e4,
|
|
129
|
+
maxHintBytes: EDITABLE_ARTIFACT_HINT_MAX_BYTES
|
|
130
|
+
});
|
|
131
|
+
var EditableArtifactLiveOutboxDispatcher = class {
|
|
132
|
+
constructor(dependencies, options) {
|
|
133
|
+
this.dependencies = dependencies;
|
|
134
|
+
this.options = normalizeOptions(options);
|
|
135
|
+
}
|
|
136
|
+
options;
|
|
137
|
+
activeDispatch = null;
|
|
138
|
+
/**
|
|
139
|
+
* Long-running resilient loop. Abort stops new claims; an already-claimed
|
|
140
|
+
* bounded batch drains before this promise resolves.
|
|
141
|
+
*/
|
|
142
|
+
async run(signal) {
|
|
143
|
+
let consecutiveClaimFailures = 0;
|
|
144
|
+
while (!signal.aborted) {
|
|
145
|
+
const summary = await this.dispatchOnce();
|
|
146
|
+
if (summary.claimFailed) consecutiveClaimFailures += 1;
|
|
147
|
+
else consecutiveClaimFailures = 0;
|
|
148
|
+
if (signal.aborted) break;
|
|
149
|
+
const delay = summary.claimFailed ? exponentialDelay(
|
|
150
|
+
consecutiveClaimFailures,
|
|
151
|
+
this.options.claimBackoffBaseMs,
|
|
152
|
+
this.options.claimBackoffMaxMs,
|
|
153
|
+
this.options.retryJitterRatio,
|
|
154
|
+
this.dependencies.random
|
|
155
|
+
) : summary.claimed === 0 ? this.options.pollIntervalMs : 0;
|
|
156
|
+
if (delay > 0 && !await sleepUntilNextPoll(this.dependencies.scheduler, delay, signal)) {
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
await this.drain();
|
|
161
|
+
}
|
|
162
|
+
/** Concurrent callers share one bounded claim/dispatch pass. */
|
|
163
|
+
dispatchOnce() {
|
|
164
|
+
if (this.activeDispatch) return this.activeDispatch;
|
|
165
|
+
const dispatch = this.dispatchBatch();
|
|
166
|
+
this.activeDispatch = dispatch;
|
|
167
|
+
void dispatch.then(
|
|
168
|
+
() => {
|
|
169
|
+
if (this.activeDispatch === dispatch) this.activeDispatch = null;
|
|
170
|
+
},
|
|
171
|
+
() => {
|
|
172
|
+
if (this.activeDispatch === dispatch) this.activeDispatch = null;
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
return dispatch;
|
|
176
|
+
}
|
|
177
|
+
async drain() {
|
|
178
|
+
await this.activeDispatch;
|
|
179
|
+
}
|
|
180
|
+
async dispatchBatch() {
|
|
181
|
+
const summary = mutableSummary();
|
|
182
|
+
let records;
|
|
183
|
+
try {
|
|
184
|
+
records = await this.dependencies.store.claimLiveOutbox({
|
|
185
|
+
owner: this.options.owner,
|
|
186
|
+
leaseDurationMs: this.options.leaseDurationMs,
|
|
187
|
+
limit: this.options.batchSize
|
|
188
|
+
});
|
|
189
|
+
if (!Array.isArray(records) || records.length > this.options.batchSize) {
|
|
190
|
+
throw new TypeError("Outbox store returned an invalid claim batch");
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
summary.claimFailed = true;
|
|
194
|
+
this.metric("claim_failed");
|
|
195
|
+
this.warn("Editable artifact outbox claim failed", "claim_failed", error);
|
|
196
|
+
return freezeSummary(summary);
|
|
197
|
+
}
|
|
198
|
+
summary.claimed = records.length;
|
|
199
|
+
if (records.length > 0) this.metric("claimed", records.length);
|
|
200
|
+
let cursor = 0;
|
|
201
|
+
const worker = async () => {
|
|
202
|
+
for (; ; ) {
|
|
203
|
+
const index = cursor;
|
|
204
|
+
cursor += 1;
|
|
205
|
+
const record = records[index];
|
|
206
|
+
if (!record) return;
|
|
207
|
+
await this.dispatchRecord(record, summary);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
await Promise.all(
|
|
211
|
+
Array.from(
|
|
212
|
+
{ length: Math.min(records.length, this.options.concurrency) },
|
|
213
|
+
async () => await worker()
|
|
214
|
+
)
|
|
215
|
+
);
|
|
216
|
+
return freezeSummary(summary);
|
|
217
|
+
}
|
|
218
|
+
async dispatchRecord(record, summary) {
|
|
219
|
+
let outboxId;
|
|
220
|
+
try {
|
|
221
|
+
outboxId = validateClaimedRecord(record, this.options.owner);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
await this.deadLetter(record, "invalid_hint", summary, error);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
let encoded;
|
|
227
|
+
try {
|
|
228
|
+
encoded = encodeEditableArtifactBrokerHint(record, this.options.maxHintBytes);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
await this.deadLetter(
|
|
231
|
+
record,
|
|
232
|
+
error instanceof EditableArtifactHintEncodingError && error.code === "oversized_hint" ? "oversized_hint" : "invalid_hint",
|
|
233
|
+
summary,
|
|
234
|
+
error
|
|
235
|
+
);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const publishAbort = new AbortController();
|
|
239
|
+
const renewalAbort = new AbortController();
|
|
240
|
+
let leaseLost = false;
|
|
241
|
+
const renewal = this.renewWhilePublishing(record, renewalAbort.signal, publishAbort, () => {
|
|
242
|
+
leaseLost = true;
|
|
243
|
+
}).catch((error) => {
|
|
244
|
+
leaseLost = true;
|
|
245
|
+
publishAbort.abort();
|
|
246
|
+
this.warn("Editable artifact outbox renewal loop failed", "lease_lost", error);
|
|
247
|
+
});
|
|
248
|
+
let publishError = null;
|
|
249
|
+
let timedOut = false;
|
|
250
|
+
const startedAt = this.dependencies.clock.now().getTime();
|
|
251
|
+
try {
|
|
252
|
+
await withTimeout(
|
|
253
|
+
this.dependencies.broker.publish({
|
|
254
|
+
subject: encoded.subject,
|
|
255
|
+
payload: encoded.payload.slice(),
|
|
256
|
+
signal: publishAbort.signal
|
|
257
|
+
}),
|
|
258
|
+
this.options.publishTimeoutMs,
|
|
259
|
+
this.dependencies.scheduler,
|
|
260
|
+
publishAbort,
|
|
261
|
+
() => {
|
|
262
|
+
timedOut = true;
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
publishError = error;
|
|
267
|
+
} finally {
|
|
268
|
+
renewalAbort.abort();
|
|
269
|
+
await renewal;
|
|
270
|
+
this.observePublish(timedOut ? "timed_out" : publishError ? "failed" : "acked", startedAt);
|
|
271
|
+
}
|
|
272
|
+
if (leaseLost) {
|
|
273
|
+
summary.leaseLost += 1;
|
|
274
|
+
this.metric("lease_lost");
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (publishError) {
|
|
278
|
+
const errorCode = timedOut ? "publish_timeout" : brokerErrorCode(publishError);
|
|
279
|
+
await this.retry(record, errorCode, summary, publishError);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
await this.dependencies.store.renewLiveOutbox({
|
|
284
|
+
outboxId,
|
|
285
|
+
owner: this.options.owner,
|
|
286
|
+
attemptCount: record.attemptCount,
|
|
287
|
+
leaseDurationMs: this.options.leaseDurationMs
|
|
288
|
+
});
|
|
289
|
+
} catch (error) {
|
|
290
|
+
summary.leaseLost += 1;
|
|
291
|
+
this.metric("lease_lost");
|
|
292
|
+
this.warn("Editable artifact outbox lease was lost after broker ACK", "lease_lost", error);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
await this.dependencies.store.markLiveOutboxPublished({
|
|
297
|
+
outboxId,
|
|
298
|
+
owner: this.options.owner,
|
|
299
|
+
attemptCount: record.attemptCount
|
|
300
|
+
});
|
|
301
|
+
summary.published += 1;
|
|
302
|
+
this.metric("published");
|
|
303
|
+
} catch (error) {
|
|
304
|
+
summary.markFailed += 1;
|
|
305
|
+
this.metric("mark_failed");
|
|
306
|
+
this.warn("Editable artifact outbox publish mark failed", "mark_failed", error);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async renewWhilePublishing(record, signal, publishAbort, onLeaseLost) {
|
|
310
|
+
for (; ; ) {
|
|
311
|
+
if (!await sleepUntilNextPoll(
|
|
312
|
+
this.dependencies.scheduler,
|
|
313
|
+
this.options.leaseRenewIntervalMs,
|
|
314
|
+
signal
|
|
315
|
+
)) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
await this.dependencies.store.renewLiveOutbox({
|
|
320
|
+
outboxId: editableArtifactOutboxId(record.outboxId),
|
|
321
|
+
owner: this.options.owner,
|
|
322
|
+
attemptCount: record.attemptCount,
|
|
323
|
+
leaseDurationMs: this.options.leaseDurationMs
|
|
324
|
+
});
|
|
325
|
+
} catch (error) {
|
|
326
|
+
onLeaseLost();
|
|
327
|
+
publishAbort.abort();
|
|
328
|
+
this.warn("Editable artifact outbox lease renewal failed", "lease_lost", error);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
async retry(record, errorCode, summary, error) {
|
|
334
|
+
const retryDelayMs = exponentialDelay(
|
|
335
|
+
record.attemptCount,
|
|
336
|
+
this.options.retryBaseMs,
|
|
337
|
+
this.options.retryMaxMs,
|
|
338
|
+
this.options.retryJitterRatio,
|
|
339
|
+
this.dependencies.random
|
|
340
|
+
);
|
|
341
|
+
try {
|
|
342
|
+
await this.dependencies.store.retryLiveOutbox({
|
|
343
|
+
outboxId: editableArtifactOutboxId(record.outboxId),
|
|
344
|
+
owner: this.options.owner,
|
|
345
|
+
attemptCount: record.attemptCount,
|
|
346
|
+
retryDelayMs,
|
|
347
|
+
errorCode
|
|
348
|
+
});
|
|
349
|
+
summary.retried += 1;
|
|
350
|
+
this.metric("retried");
|
|
351
|
+
} catch (retryError) {
|
|
352
|
+
this.metric("retry_store_failed");
|
|
353
|
+
this.warn(
|
|
354
|
+
"Editable artifact outbox retry persistence failed",
|
|
355
|
+
"retry_store_failed",
|
|
356
|
+
retryError
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
this.warn("Editable artifact hint publish failed", errorCode, error);
|
|
360
|
+
}
|
|
361
|
+
async deadLetter(record, errorCode, summary, error) {
|
|
362
|
+
try {
|
|
363
|
+
await this.dependencies.store.deadLetterLiveOutbox({
|
|
364
|
+
outboxId: editableArtifactOutboxId(record.outboxId),
|
|
365
|
+
owner: this.options.owner,
|
|
366
|
+
attemptCount: record.attemptCount,
|
|
367
|
+
errorCode
|
|
368
|
+
});
|
|
369
|
+
summary.deadLettered += 1;
|
|
370
|
+
this.metric("dead_lettered");
|
|
371
|
+
} catch (deadLetterError) {
|
|
372
|
+
this.metric("dead_letter_store_failed");
|
|
373
|
+
this.warn(
|
|
374
|
+
"Editable artifact outbox dead-letter persistence failed",
|
|
375
|
+
"dead_letter_store_failed",
|
|
376
|
+
deadLetterError
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
this.warn("Editable artifact outbox contains an undeliverable hint", errorCode, error);
|
|
380
|
+
}
|
|
381
|
+
metric(outcome, count = 1) {
|
|
382
|
+
try {
|
|
383
|
+
this.dependencies.metrics?.increment(outcome, count);
|
|
384
|
+
} catch {
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
observePublish(outcome, startedAt) {
|
|
388
|
+
let elapsed = 0;
|
|
389
|
+
const now = this.dependencies.clock.now().getTime();
|
|
390
|
+
if (Number.isFinite(now) && Number.isFinite(startedAt)) elapsed = Math.max(0, now - startedAt);
|
|
391
|
+
try {
|
|
392
|
+
this.dependencies.metrics?.observePublishSeconds(outcome, elapsed / 1e3);
|
|
393
|
+
} catch {
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
warn(message, code, error) {
|
|
397
|
+
try {
|
|
398
|
+
this.dependencies.logger?.warn(message, {
|
|
399
|
+
code,
|
|
400
|
+
errorClass: error instanceof Error ? error.name : "UnknownError"
|
|
401
|
+
});
|
|
402
|
+
} catch {
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
var EditableArtifactHintEncodingError = class extends Error {
|
|
407
|
+
constructor(code, message) {
|
|
408
|
+
super(message);
|
|
409
|
+
this.code = code;
|
|
410
|
+
this.name = "EditableArtifactHintEncodingError";
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
function encodeEditableArtifactBrokerHint(record, maxBytes = EDITABLE_ARTIFACT_HINT_MAX_BYTES) {
|
|
414
|
+
assertPositiveSafeInteger(maxBytes, "maximum broker hint bytes");
|
|
415
|
+
let hint;
|
|
416
|
+
try {
|
|
417
|
+
const event = record.event;
|
|
418
|
+
const scope = editableArtifactScope(event.scope);
|
|
419
|
+
const artifactId = editableArtifactId(event.artifactId);
|
|
420
|
+
const stateHash = editableArtifactStateHash(event.stateHash);
|
|
421
|
+
if (event.schemaVersion !== 1) throw new TypeError("Unsupported artifact hint schema");
|
|
422
|
+
if (event.kind === "transaction_committed") {
|
|
423
|
+
assertPositiveSafeInteger(event.sequenceEnd, "hint head sequence");
|
|
424
|
+
const common = {
|
|
425
|
+
kind: "head_advanced",
|
|
426
|
+
schemaVersion: 1,
|
|
427
|
+
scope,
|
|
428
|
+
artifactId,
|
|
429
|
+
modality: event.modality,
|
|
430
|
+
headSequence: event.sequenceEnd,
|
|
431
|
+
stateHash
|
|
432
|
+
};
|
|
433
|
+
if (event.modality === "spreadsheet") {
|
|
434
|
+
assertPositiveSafeInteger(
|
|
435
|
+
event.operationProtocolVersion,
|
|
436
|
+
"hint operation protocol version"
|
|
437
|
+
);
|
|
438
|
+
hint = Object.freeze({
|
|
439
|
+
...common,
|
|
440
|
+
modality: "spreadsheet",
|
|
441
|
+
operationProtocolVersion: event.operationProtocolVersion
|
|
442
|
+
});
|
|
443
|
+
} else {
|
|
444
|
+
assertPositiveSafeInteger(event.commitProtocolVersion, "hint commit protocol version");
|
|
445
|
+
hint = Object.freeze({
|
|
446
|
+
...common,
|
|
447
|
+
modality: event.modality,
|
|
448
|
+
commitProtocolVersion: event.commitProtocolVersion
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
} else if (event.kind === "snapshot_published") {
|
|
452
|
+
assertNonnegativeSafeInteger(event.coveredHeadSequence, "snapshot hint head sequence");
|
|
453
|
+
const common = {
|
|
454
|
+
kind: "snapshot_available",
|
|
455
|
+
schemaVersion: 1,
|
|
456
|
+
scope,
|
|
457
|
+
artifactId,
|
|
458
|
+
modality: event.modality,
|
|
459
|
+
headSequence: event.coveredHeadSequence,
|
|
460
|
+
stateHash,
|
|
461
|
+
snapshotId: editableArtifactSnapshotId(event.snapshotId)
|
|
462
|
+
};
|
|
463
|
+
if (event.modality === "spreadsheet") {
|
|
464
|
+
assertPositiveSafeInteger(
|
|
465
|
+
event.operationProtocolVersion,
|
|
466
|
+
"hint operation protocol version"
|
|
467
|
+
);
|
|
468
|
+
hint = Object.freeze({
|
|
469
|
+
...common,
|
|
470
|
+
modality: "spreadsheet",
|
|
471
|
+
operationProtocolVersion: event.operationProtocolVersion
|
|
472
|
+
});
|
|
473
|
+
} else {
|
|
474
|
+
hint = Object.freeze({ ...common, modality: event.modality });
|
|
475
|
+
}
|
|
476
|
+
} else {
|
|
477
|
+
throw new TypeError("Unknown editable artifact outbox event");
|
|
478
|
+
}
|
|
479
|
+
const payload = textEncoder.encode(JSON.stringify(hint));
|
|
480
|
+
if (payload.byteLength > maxBytes) {
|
|
481
|
+
throw new EditableArtifactHintEncodingError(
|
|
482
|
+
"oversized_hint",
|
|
483
|
+
"Editable artifact broker hint exceeds its byte limit"
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
return Object.freeze({
|
|
487
|
+
subject: editableArtifactLiveHintSubject(scope, artifactId),
|
|
488
|
+
payload,
|
|
489
|
+
hint
|
|
490
|
+
});
|
|
491
|
+
} catch (error) {
|
|
492
|
+
if (error instanceof EditableArtifactHintEncodingError) throw error;
|
|
493
|
+
throw new EditableArtifactHintEncodingError(
|
|
494
|
+
"invalid_hint",
|
|
495
|
+
"Editable artifact outbox event cannot be projected to a broker hint"
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function editableArtifactLiveHintSubject(scope, artifactIdInput) {
|
|
500
|
+
const normalized = editableArtifactScope(scope);
|
|
501
|
+
const artifactId = editableArtifactId(artifactIdInput);
|
|
502
|
+
const subject = [
|
|
503
|
+
EDITABLE_ARTIFACT_HINT_SUBJECT_PREFIX,
|
|
504
|
+
base64Url(textEncoder.encode(normalized.accountId)),
|
|
505
|
+
base64Url(textEncoder.encode(normalized.workspaceId)),
|
|
506
|
+
artifactId
|
|
507
|
+
].join(".");
|
|
508
|
+
if (textEncoder.encode(subject).byteLength > 1024) {
|
|
509
|
+
throw new EditableArtifactHintEncodingError(
|
|
510
|
+
"invalid_hint",
|
|
511
|
+
"Editable artifact hint subject exceeds its byte limit"
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
return subject;
|
|
515
|
+
}
|
|
516
|
+
function decodeEditableArtifactBrokerHint(payload, expected, maxBytes = EDITABLE_ARTIFACT_HINT_MAX_BYTES) {
|
|
517
|
+
if (!(payload instanceof Uint8Array) || payload.byteLength < 2 || payload.byteLength > maxBytes) {
|
|
518
|
+
throw new EditableArtifactHintEncodingError(
|
|
519
|
+
"oversized_hint",
|
|
520
|
+
"Editable artifact broker hint exceeds its byte limit"
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
try {
|
|
524
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(payload);
|
|
525
|
+
const value = JSON.parse(text);
|
|
526
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
527
|
+
throw new TypeError("hint must be an object");
|
|
528
|
+
}
|
|
529
|
+
const record = value;
|
|
530
|
+
if (JSON.stringify(record) !== text) throw new TypeError("hint must be canonical JSON");
|
|
531
|
+
const kind = record.kind;
|
|
532
|
+
const modality = record.modality;
|
|
533
|
+
if (modality !== "spreadsheet" && modality !== "document" && modality !== "presentation") {
|
|
534
|
+
throw new TypeError("hint modality invalid");
|
|
535
|
+
}
|
|
536
|
+
const keys = Object.keys(record);
|
|
537
|
+
const expectedKeys = kind === "head_advanced" ? modality === "spreadsheet" ? [
|
|
538
|
+
"kind",
|
|
539
|
+
"schemaVersion",
|
|
540
|
+
"scope",
|
|
541
|
+
"artifactId",
|
|
542
|
+
"modality",
|
|
543
|
+
"headSequence",
|
|
544
|
+
"stateHash",
|
|
545
|
+
"operationProtocolVersion"
|
|
546
|
+
] : [
|
|
547
|
+
"kind",
|
|
548
|
+
"schemaVersion",
|
|
549
|
+
"scope",
|
|
550
|
+
"artifactId",
|
|
551
|
+
"modality",
|
|
552
|
+
"headSequence",
|
|
553
|
+
"stateHash",
|
|
554
|
+
"commitProtocolVersion"
|
|
555
|
+
] : modality === "spreadsheet" ? [
|
|
556
|
+
"kind",
|
|
557
|
+
"schemaVersion",
|
|
558
|
+
"scope",
|
|
559
|
+
"artifactId",
|
|
560
|
+
"modality",
|
|
561
|
+
"headSequence",
|
|
562
|
+
"stateHash",
|
|
563
|
+
"snapshotId",
|
|
564
|
+
"operationProtocolVersion"
|
|
565
|
+
] : [
|
|
566
|
+
"kind",
|
|
567
|
+
"schemaVersion",
|
|
568
|
+
"scope",
|
|
569
|
+
"artifactId",
|
|
570
|
+
"modality",
|
|
571
|
+
"headSequence",
|
|
572
|
+
"stateHash",
|
|
573
|
+
"snapshotId"
|
|
574
|
+
];
|
|
575
|
+
if (keys.length !== expectedKeys.length || keys.some((key, index) => key !== expectedKeys[index])) {
|
|
576
|
+
throw new TypeError("hint fields invalid");
|
|
577
|
+
}
|
|
578
|
+
if (record.schemaVersion !== 1) throw new TypeError("hint schema invalid");
|
|
579
|
+
const scope = editableArtifactScope(record.scope);
|
|
580
|
+
const artifactId = editableArtifactId(record.artifactId);
|
|
581
|
+
if (scope.accountId !== expected.scope.accountId || scope.workspaceId !== expected.scope.workspaceId || artifactId !== editableArtifactId(expected.artifactId)) {
|
|
582
|
+
throw new TypeError("hint authority mismatch");
|
|
583
|
+
}
|
|
584
|
+
assertNonnegativeSafeInteger(record.headSequence, "hint head sequence");
|
|
585
|
+
if (modality === "spreadsheet") {
|
|
586
|
+
assertPositiveSafeInteger(
|
|
587
|
+
record.operationProtocolVersion,
|
|
588
|
+
"hint operation protocol version"
|
|
589
|
+
);
|
|
590
|
+
} else if (kind === "head_advanced") {
|
|
591
|
+
assertPositiveSafeInteger(
|
|
592
|
+
record.commitProtocolVersion,
|
|
593
|
+
"hint commit protocol version"
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
editableArtifactStateHash(record.stateHash);
|
|
597
|
+
if (kind === "head_advanced") return Object.freeze(record);
|
|
598
|
+
if (kind === "snapshot_available") {
|
|
599
|
+
editableArtifactSnapshotId(record.snapshotId);
|
|
600
|
+
return Object.freeze(record);
|
|
601
|
+
}
|
|
602
|
+
throw new TypeError("hint kind invalid");
|
|
603
|
+
} catch (error) {
|
|
604
|
+
if (error instanceof EditableArtifactHintEncodingError) throw error;
|
|
605
|
+
throw new EditableArtifactHintEncodingError(
|
|
606
|
+
"invalid_hint",
|
|
607
|
+
"Editable artifact broker hint is invalid"
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
var MathEditableArtifactOutboxRandom = class {
|
|
612
|
+
unit() {
|
|
613
|
+
return Math.random();
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
function validateClaimedRecord(record, owner) {
|
|
617
|
+
const outboxId = editableArtifactOutboxId(record.outboxId);
|
|
618
|
+
if (record.state !== "publishing") throw new TypeError("Claimed outbox row is not publishing");
|
|
619
|
+
if (record.leaseOwner !== owner) throw new TypeError("Claimed outbox row has another owner");
|
|
620
|
+
assertPositiveSafeInteger(record.attemptCount, "outbox attempt count");
|
|
621
|
+
if (record.leaseExpiresAt === null) throw new TypeError("Claimed outbox row has no lease expiry");
|
|
622
|
+
return outboxId;
|
|
623
|
+
}
|
|
624
|
+
function normalizeOptions(options) {
|
|
625
|
+
const owner = boundedOwner(options.owner);
|
|
626
|
+
const normalized = {
|
|
627
|
+
owner,
|
|
628
|
+
batchSize: options.batchSize ?? DEFAULTS.batchSize,
|
|
629
|
+
concurrency: options.concurrency ?? DEFAULTS.concurrency,
|
|
630
|
+
leaseDurationMs: options.leaseDurationMs ?? DEFAULTS.leaseDurationMs,
|
|
631
|
+
leaseRenewIntervalMs: options.leaseRenewIntervalMs ?? DEFAULTS.leaseRenewIntervalMs,
|
|
632
|
+
pollIntervalMs: options.pollIntervalMs ?? DEFAULTS.pollIntervalMs,
|
|
633
|
+
publishTimeoutMs: options.publishTimeoutMs ?? DEFAULTS.publishTimeoutMs,
|
|
634
|
+
retryBaseMs: options.retryBaseMs ?? DEFAULTS.retryBaseMs,
|
|
635
|
+
retryMaxMs: options.retryMaxMs ?? DEFAULTS.retryMaxMs,
|
|
636
|
+
retryJitterRatio: options.retryJitterRatio ?? DEFAULTS.retryJitterRatio,
|
|
637
|
+
claimBackoffBaseMs: options.claimBackoffBaseMs ?? DEFAULTS.claimBackoffBaseMs,
|
|
638
|
+
claimBackoffMaxMs: options.claimBackoffMaxMs ?? DEFAULTS.claimBackoffMaxMs,
|
|
639
|
+
maxHintBytes: options.maxHintBytes ?? DEFAULTS.maxHintBytes
|
|
640
|
+
};
|
|
641
|
+
for (const [label, value] of Object.entries(normalized)) {
|
|
642
|
+
if (label === "owner" || label === "retryJitterRatio") continue;
|
|
643
|
+
if (typeof value !== "number") throw new TypeError(`${label} must be numeric`);
|
|
644
|
+
assertPositiveSafeInteger(value, label);
|
|
645
|
+
}
|
|
646
|
+
if (normalized.batchSize > 256) throw new TypeError("outbox batch size exceeds 256");
|
|
647
|
+
if (normalized.concurrency > normalized.batchSize)
|
|
648
|
+
throw new TypeError("outbox concurrency exceeds batch size");
|
|
649
|
+
if (normalized.leaseDurationMs < 5e3 || normalized.leaseDurationMs > 3e5)
|
|
650
|
+
throw new TypeError("outbox lease duration must be 5-300 seconds");
|
|
651
|
+
if (normalized.leaseRenewIntervalMs * 2 >= normalized.leaseDurationMs)
|
|
652
|
+
throw new TypeError("outbox lease renewal interval must be less than half the lease");
|
|
653
|
+
if (normalized.publishTimeoutMs >= normalized.leaseDurationMs)
|
|
654
|
+
throw new TypeError("outbox publish timeout must be shorter than the lease");
|
|
655
|
+
if (normalized.retryBaseMs > normalized.retryMaxMs)
|
|
656
|
+
throw new TypeError("outbox retry base exceeds retry maximum");
|
|
657
|
+
if (normalized.claimBackoffBaseMs > normalized.claimBackoffMaxMs)
|
|
658
|
+
throw new TypeError("outbox claim backoff base exceeds its maximum");
|
|
659
|
+
if (!Number.isFinite(normalized.retryJitterRatio) || normalized.retryJitterRatio < 0 || normalized.retryJitterRatio > 1) {
|
|
660
|
+
throw new TypeError("outbox retry jitter ratio must be between zero and one");
|
|
661
|
+
}
|
|
662
|
+
if (normalized.maxHintBytes > EDITABLE_ARTIFACT_HINT_MAX_BYTES)
|
|
663
|
+
throw new TypeError("outbox hint byte limit exceeds the protocol maximum");
|
|
664
|
+
return Object.freeze(normalized);
|
|
665
|
+
}
|
|
666
|
+
function boundedOwner(value) {
|
|
667
|
+
const bytes = textEncoder.encode(value).byteLength;
|
|
668
|
+
if (bytes < 1 || bytes > 200 || value.trim() !== value || !/^[A-Za-z0-9._:-]+$/u.test(value)) {
|
|
669
|
+
throw new TypeError("outbox owner is malformed");
|
|
670
|
+
}
|
|
671
|
+
return value;
|
|
672
|
+
}
|
|
673
|
+
function exponentialDelay(attempt, base, maximum, jitterRatio, random) {
|
|
674
|
+
const exponent = Math.min(30, Math.max(0, attempt - 1));
|
|
675
|
+
const withoutJitter = Math.min(maximum, base * 2 ** exponent);
|
|
676
|
+
const sample = random.unit();
|
|
677
|
+
const unit = Number.isFinite(sample) && sample >= 0 && sample < 1 ? sample : 0.5;
|
|
678
|
+
const jitter = Math.floor(withoutJitter * jitterRatio * unit);
|
|
679
|
+
return Math.min(maximum, withoutJitter + jitter);
|
|
680
|
+
}
|
|
681
|
+
async function withTimeout(operation, timeoutMs, scheduler, operationAbort, onTimeout) {
|
|
682
|
+
const timeoutAbort = new AbortController();
|
|
683
|
+
try {
|
|
684
|
+
await Promise.race([
|
|
685
|
+
operation,
|
|
686
|
+
scheduler.sleep(timeoutMs, timeoutAbort.signal).then(() => {
|
|
687
|
+
onTimeout();
|
|
688
|
+
operationAbort.abort();
|
|
689
|
+
throw new Error("Editable artifact broker publish timed out");
|
|
690
|
+
})
|
|
691
|
+
]);
|
|
692
|
+
} finally {
|
|
693
|
+
timeoutAbort.abort();
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
async function sleepUntilNextPoll(scheduler, milliseconds, signal) {
|
|
697
|
+
try {
|
|
698
|
+
await scheduler.sleep(milliseconds, signal);
|
|
699
|
+
return true;
|
|
700
|
+
} catch (error) {
|
|
701
|
+
if (signal.aborted) return false;
|
|
702
|
+
throw error;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
function brokerErrorCode(error) {
|
|
706
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "broker_backpressure") {
|
|
707
|
+
return "broker_backpressure";
|
|
708
|
+
}
|
|
709
|
+
return "broker_unavailable";
|
|
710
|
+
}
|
|
711
|
+
function mutableSummary() {
|
|
712
|
+
return {
|
|
713
|
+
claimed: 0,
|
|
714
|
+
published: 0,
|
|
715
|
+
retried: 0,
|
|
716
|
+
deadLettered: 0,
|
|
717
|
+
leaseLost: 0,
|
|
718
|
+
markFailed: 0,
|
|
719
|
+
claimFailed: false
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
function freezeSummary(summary) {
|
|
723
|
+
return Object.freeze({ ...summary });
|
|
724
|
+
}
|
|
725
|
+
function base64Url(bytes) {
|
|
726
|
+
let binary = "";
|
|
727
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
728
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// src/editable-artifact-live/server.ts
|
|
732
|
+
import {
|
|
733
|
+
EDITABLE_ARTIFACT_COMMAND_MAX_BYTES,
|
|
734
|
+
EDITABLE_ARTIFACT_INTENT_MAX_BYTES,
|
|
735
|
+
EDITABLE_ARTIFACT_KERNEL_VERSION_MAX_BYTES as EDITABLE_ARTIFACT_KERNEL_VERSION_MAX_BYTES2,
|
|
736
|
+
EDITABLE_ARTIFACT_PRODUCT_MAX_SNAPSHOT_BYTES
|
|
737
|
+
} from "@opengeni/contracts/editable-artifacts";
|
|
738
|
+
import { decodeCommittedTransactionSummary } from "@opengeni/contracts/editable-artifact-committed-transaction";
|
|
739
|
+
import { decodeEditableArtifactSerializedCommit } from "@opengeni/contracts/editable-artifact-serialized-commit";
|
|
740
|
+
|
|
741
|
+
// src/editable-artifact-live/types.ts
|
|
742
|
+
var EDITABLE_ARTIFACT_LIVE_PROTOCOL_VERSION = 1;
|
|
743
|
+
|
|
744
|
+
// src/editable-artifact-live/ticket.ts
|
|
745
|
+
var DEFAULT_TTL_MS = 3e4;
|
|
746
|
+
var MAX_TTL_MS = 6e4;
|
|
747
|
+
var MAX_TOKEN_BYTES = 4096;
|
|
748
|
+
var EditableArtifactLiveTicketAuthority = class {
|
|
749
|
+
constructor(dependencies) {
|
|
750
|
+
this.dependencies = dependencies;
|
|
751
|
+
this.ttlMs = dependencies.ttlMs ?? DEFAULT_TTL_MS;
|
|
752
|
+
if (!Number.isSafeInteger(this.ttlMs) || this.ttlMs < 1e3 || this.ttlMs > MAX_TTL_MS) {
|
|
753
|
+
throw new TypeError(`ticket ttl must be 1000-${MAX_TTL_MS} milliseconds`);
|
|
754
|
+
}
|
|
755
|
+
this.protocolVersion = dependencies.protocolVersion ?? EDITABLE_ARTIFACT_LIVE_PROTOCOL_VERSION;
|
|
756
|
+
if (!Number.isSafeInteger(this.protocolVersion) || this.protocolVersion < 1) {
|
|
757
|
+
throw new TypeError("protocol version must be a positive safe integer");
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
ttlMs;
|
|
761
|
+
protocolVersion;
|
|
762
|
+
async mint(input) {
|
|
763
|
+
const scope = editableArtifactScope(input.scope);
|
|
764
|
+
const artifactId = editableArtifactId(input.artifactId);
|
|
765
|
+
const modality = assertModality(input.modality);
|
|
766
|
+
validateEditableArtifactActor(input.actor);
|
|
767
|
+
if (typeof input.allowEdit !== "boolean") {
|
|
768
|
+
throw new TypeError("ticket allowEdit must be boolean");
|
|
769
|
+
}
|
|
770
|
+
const actor = Object.freeze({ ...input.actor });
|
|
771
|
+
const decision = await this.dependencies.authorization.authorize({
|
|
772
|
+
scope,
|
|
773
|
+
artifactId,
|
|
774
|
+
actor,
|
|
775
|
+
permission: "read"
|
|
776
|
+
});
|
|
777
|
+
if (!decision.allowed) {
|
|
778
|
+
throw new EditableArtifactLiveError(
|
|
779
|
+
"permission_changed",
|
|
780
|
+
"Editable artifact read permission denied"
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
const token = this.dependencies.tokens.randomOpaqueToken();
|
|
784
|
+
assertToken(token);
|
|
785
|
+
const tokenDigest = await this.dependencies.tokens.digestOpaqueToken(token);
|
|
786
|
+
assertDigest(tokenDigest);
|
|
787
|
+
const issuedAtDate = this.dependencies.clock.now();
|
|
788
|
+
if (!Number.isFinite(issuedAtDate.getTime()))
|
|
789
|
+
throw new TypeError("clock returned an invalid date");
|
|
790
|
+
const issuedAt = issuedAtDate.toISOString();
|
|
791
|
+
const expiresAt = new Date(issuedAtDate.getTime() + this.ttlMs).toISOString();
|
|
792
|
+
const record = Object.freeze({
|
|
793
|
+
tokenDigest,
|
|
794
|
+
scope,
|
|
795
|
+
artifactId,
|
|
796
|
+
modality,
|
|
797
|
+
actor,
|
|
798
|
+
allowEdit: input.allowEdit,
|
|
799
|
+
protocolVersion: this.protocolVersion,
|
|
800
|
+
issuedAt,
|
|
801
|
+
expiresAt
|
|
802
|
+
});
|
|
803
|
+
await this.dependencies.tickets.put(record);
|
|
804
|
+
return Object.freeze({
|
|
805
|
+
artifactId,
|
|
806
|
+
modality,
|
|
807
|
+
replicaId: actor.replicaId,
|
|
808
|
+
token,
|
|
809
|
+
expiresAt,
|
|
810
|
+
protocolVersion: this.protocolVersion
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
async consume(input) {
|
|
814
|
+
assertToken(input.token);
|
|
815
|
+
const artifactId = editableArtifactId(input.artifactId);
|
|
816
|
+
const digest = await this.dependencies.tokens.digestOpaqueToken(input.token);
|
|
817
|
+
assertDigest(digest);
|
|
818
|
+
const record = await this.dependencies.tickets.consume(digest);
|
|
819
|
+
if (!record) {
|
|
820
|
+
throw new EditableArtifactLiveError(
|
|
821
|
+
"ticket_replayed",
|
|
822
|
+
"Live ticket is invalid or was already consumed"
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
validateTicketRecord(record);
|
|
826
|
+
if (Date.parse(record.expiresAt) <= this.dependencies.clock.now().getTime()) {
|
|
827
|
+
throw new EditableArtifactLiveError("ticket_expired", "Live ticket has expired", {
|
|
828
|
+
retryable: true
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
if (record.artifactId !== artifactId) {
|
|
832
|
+
throw new EditableArtifactLiveError("invalid_ticket", "Live ticket scope does not match");
|
|
833
|
+
}
|
|
834
|
+
if (record.protocolVersion !== input.protocolVersion || input.protocolVersion !== this.protocolVersion) {
|
|
835
|
+
throw new EditableArtifactLiveError(
|
|
836
|
+
"protocol_mismatch",
|
|
837
|
+
"Editable artifact live protocol is unsupported"
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
return Object.freeze({
|
|
841
|
+
...record,
|
|
842
|
+
scope: editableArtifactScope(record.scope),
|
|
843
|
+
actor: Object.freeze({ ...record.actor })
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
var InMemoryEditableArtifactLiveTicketStore = class {
|
|
848
|
+
records = /* @__PURE__ */ new Map();
|
|
849
|
+
async put(record) {
|
|
850
|
+
if (this.records.has(record.tokenDigest)) throw new Error("Live ticket digest collision");
|
|
851
|
+
this.records.set(record.tokenDigest, record);
|
|
852
|
+
}
|
|
853
|
+
async consume(tokenDigest) {
|
|
854
|
+
const record = this.records.get(tokenDigest) ?? null;
|
|
855
|
+
if (record) this.records.delete(tokenDigest);
|
|
856
|
+
return record;
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
var WebCryptoEditableArtifactLiveTokens = class {
|
|
860
|
+
randomOpaqueToken() {
|
|
861
|
+
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
862
|
+
let binary = "";
|
|
863
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
864
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
865
|
+
}
|
|
866
|
+
async digestOpaqueToken(token) {
|
|
867
|
+
const digest = new Uint8Array(
|
|
868
|
+
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token))
|
|
869
|
+
);
|
|
870
|
+
return `sha256:${[...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
function validateTicketRecord(record) {
|
|
874
|
+
assertDigest(record.tokenDigest);
|
|
875
|
+
editableArtifactScope(record.scope);
|
|
876
|
+
editableArtifactId(record.artifactId);
|
|
877
|
+
assertModality(record.modality);
|
|
878
|
+
validateEditableArtifactActor(record.actor);
|
|
879
|
+
if (typeof record.allowEdit !== "boolean") {
|
|
880
|
+
throw new TypeError("ticket allowEdit is invalid");
|
|
881
|
+
}
|
|
882
|
+
assertIsoTimestamp(record.issuedAt, "ticket issuedAt");
|
|
883
|
+
assertIsoTimestamp(record.expiresAt, "ticket expiresAt");
|
|
884
|
+
}
|
|
885
|
+
function assertModality(value) {
|
|
886
|
+
if (value !== "document" && value !== "spreadsheet" && value !== "presentation") {
|
|
887
|
+
throw new TypeError("ticket modality is invalid");
|
|
888
|
+
}
|
|
889
|
+
return value;
|
|
890
|
+
}
|
|
891
|
+
function assertToken(token) {
|
|
892
|
+
const bytes = new TextEncoder().encode(token).byteLength;
|
|
893
|
+
if (bytes < 32 || bytes > MAX_TOKEN_BYTES || !/^[A-Za-z0-9._~-]+$/u.test(token)) {
|
|
894
|
+
throw new EditableArtifactLiveError("invalid_ticket", "Live ticket is malformed");
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
function assertDigest(value) {
|
|
898
|
+
if (!/^sha256:[0-9a-f]{64}$/u.test(value)) throw new TypeError("ticket digest is malformed");
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// src/editable-artifact-live/server.ts
|
|
902
|
+
var textEncoder2 = new TextEncoder();
|
|
903
|
+
var strictDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
904
|
+
var DEFAULT_MAX_CLIENT_FRAME_BYTES = 5 * 1024 * 1024 + 64 * 1024;
|
|
905
|
+
var DEFAULT_MAX_OUTBOUND_FRAME_BYTES = 8 * 1024 * 1024 + 64 * 1024;
|
|
906
|
+
var DEFAULT_MAX_SNAPSHOT_BYTES = EDITABLE_ARTIFACT_PRODUCT_MAX_SNAPSHOT_BYTES;
|
|
907
|
+
var DEFAULT_SNAPSHOT_CHUNK_BYTES = 256 * 1024;
|
|
908
|
+
var DEFAULT_MAX_IN_FLIGHT_TRANSACTIONS = 256;
|
|
909
|
+
var DEFAULT_MAX_IN_FLIGHT_BYTES = 32 * 1024 * 1024;
|
|
910
|
+
var DEFAULT_REPLAY_PAGE_TRANSACTIONS = 256;
|
|
911
|
+
var DEFAULT_REPLAY_PAGE_BYTES = 32 * 1024 * 1024;
|
|
912
|
+
var DEFAULT_MAX_SOCKET_BUFFERED_BYTES = 16 * 1024 * 1024;
|
|
913
|
+
var DEFAULT_MAX_INBOUND_QUEUE_FRAMES = 64;
|
|
914
|
+
var DEFAULT_MAX_INBOUND_QUEUE_BYTES = 12 * 1024 * 1024;
|
|
915
|
+
var DEFAULT_RECONCILE_INTERVAL_MS = 5e3;
|
|
916
|
+
var DEFAULT_REAUTHORIZE_INTERVAL_MS = 15e3;
|
|
917
|
+
var DEFAULT_ACK_TIMEOUT_MS = 3e4;
|
|
918
|
+
var DEFAULT_SEND_TIMEOUT_MS = 1e4;
|
|
919
|
+
var MAX_IDENTIFIER_BYTES = 512;
|
|
920
|
+
var MAX_CAUSAL_ACTORS = 1024;
|
|
921
|
+
var EditableArtifactLiveServer = class {
|
|
922
|
+
constructor(dependencies, options = {}) {
|
|
923
|
+
this.dependencies = dependencies;
|
|
924
|
+
this.options = normalizeOptions2(options);
|
|
925
|
+
this.ticketAuthority = new EditableArtifactLiveTicketAuthority({
|
|
926
|
+
authorization: dependencies.authorization,
|
|
927
|
+
tickets: dependencies.tickets,
|
|
928
|
+
tokens: dependencies.tokens,
|
|
929
|
+
clock: dependencies.clock,
|
|
930
|
+
protocolVersion: this.options.protocolVersion,
|
|
931
|
+
...this.options.ticketTtlMs === void 0 ? {} : { ttlMs: this.options.ticketTtlMs }
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
options;
|
|
935
|
+
ticketAuthority;
|
|
936
|
+
mintTicket(input) {
|
|
937
|
+
return this.ticketAuthority.mint(input);
|
|
938
|
+
}
|
|
939
|
+
async openLive(input) {
|
|
940
|
+
const artifactId = editableArtifactId(input.artifactId);
|
|
941
|
+
const ticket = await this.ticketAuthority.consume({
|
|
942
|
+
token: input.token,
|
|
943
|
+
artifactId,
|
|
944
|
+
protocolVersion: input.protocolVersion
|
|
945
|
+
});
|
|
946
|
+
validateResume(input.resume, ticket.modality);
|
|
947
|
+
const streamEpoch = `live_${this.dependencies.tokens.randomOpaqueToken()}`;
|
|
948
|
+
boundedString(streamEpoch, "stream epoch", MAX_IDENTIFIER_BYTES);
|
|
949
|
+
const session = new EditableArtifactLiveSessionImpl({
|
|
950
|
+
dependencies: this.dependencies,
|
|
951
|
+
options: this.options,
|
|
952
|
+
ticket,
|
|
953
|
+
streamEpoch,
|
|
954
|
+
resume: input.resume,
|
|
955
|
+
sink: input.sink,
|
|
956
|
+
...input.signal === void 0 ? {} : { parentSignal: input.signal }
|
|
957
|
+
});
|
|
958
|
+
await session.start();
|
|
959
|
+
return session;
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
var EditableArtifactLiveSessionImpl = class {
|
|
963
|
+
artifactId;
|
|
964
|
+
modality;
|
|
965
|
+
streamEpoch;
|
|
966
|
+
closed;
|
|
967
|
+
dependencies;
|
|
968
|
+
options;
|
|
969
|
+
ticket;
|
|
970
|
+
resume;
|
|
971
|
+
sink;
|
|
972
|
+
abort = new AbortController();
|
|
973
|
+
boundaries = /* @__PURE__ */ new Map();
|
|
974
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
975
|
+
resolveClosed;
|
|
976
|
+
workTail = Promise.resolve();
|
|
977
|
+
releaseHints = null;
|
|
978
|
+
releaseInvalidations = null;
|
|
979
|
+
detachParent = null;
|
|
980
|
+
maintenance = null;
|
|
981
|
+
closedValue = null;
|
|
982
|
+
bootstrapping = true;
|
|
983
|
+
reconcileRequested = false;
|
|
984
|
+
reconcileScheduled = false;
|
|
985
|
+
reauthorizationRequested = false;
|
|
986
|
+
reauthorizationScheduled = false;
|
|
987
|
+
sentSequence = 0;
|
|
988
|
+
sentStateHash = null;
|
|
989
|
+
ackedSequence = 0;
|
|
990
|
+
ackedStateHash = null;
|
|
991
|
+
targetHead = 0;
|
|
992
|
+
inFlightBytes = 0;
|
|
993
|
+
oldestInFlightAt = 0;
|
|
994
|
+
writable = false;
|
|
995
|
+
authorizationRevision = 0;
|
|
996
|
+
queuedInboundFrames = 0;
|
|
997
|
+
queuedInboundBytes = 0;
|
|
998
|
+
constructor(input) {
|
|
999
|
+
this.dependencies = input.dependencies;
|
|
1000
|
+
this.options = input.options;
|
|
1001
|
+
this.ticket = input.ticket;
|
|
1002
|
+
this.artifactId = input.ticket.artifactId;
|
|
1003
|
+
this.modality = input.ticket.modality;
|
|
1004
|
+
this.streamEpoch = input.streamEpoch;
|
|
1005
|
+
this.resume = input.resume;
|
|
1006
|
+
this.sink = input.sink;
|
|
1007
|
+
this.closed = new Promise((resolve) => {
|
|
1008
|
+
this.resolveClosed = resolve;
|
|
1009
|
+
});
|
|
1010
|
+
if (input.parentSignal) {
|
|
1011
|
+
const close = () => void this.close("closed");
|
|
1012
|
+
if (input.parentSignal.aborted) close();
|
|
1013
|
+
else {
|
|
1014
|
+
input.parentSignal.addEventListener("abort", close, { once: true });
|
|
1015
|
+
this.detachParent = () => input.parentSignal.removeEventListener("abort", close);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
async start() {
|
|
1020
|
+
try {
|
|
1021
|
+
this.assertOpen();
|
|
1022
|
+
this.authorizationRevision = await this.requireRead();
|
|
1023
|
+
this.assertOpen();
|
|
1024
|
+
const releaseHints = await this.dependencies.hints.subscribe({
|
|
1025
|
+
scope: this.ticket.scope,
|
|
1026
|
+
artifactId: this.artifactId,
|
|
1027
|
+
onHint: () => this.requestReconcile(),
|
|
1028
|
+
onReconnect: () => this.requestReconcile()
|
|
1029
|
+
});
|
|
1030
|
+
if (this.closedValue) {
|
|
1031
|
+
safeRelease(releaseHints);
|
|
1032
|
+
this.assertOpen();
|
|
1033
|
+
}
|
|
1034
|
+
this.releaseHints = releaseHints;
|
|
1035
|
+
const releaseInvalidations = await this.dependencies.invalidations.subscribe({
|
|
1036
|
+
scope: this.ticket.scope,
|
|
1037
|
+
artifactId: this.artifactId,
|
|
1038
|
+
actor: this.ticket.actor,
|
|
1039
|
+
onInvalidated: () => this.requestReauthorization()
|
|
1040
|
+
});
|
|
1041
|
+
if (this.closedValue) {
|
|
1042
|
+
safeRelease(releaseInvalidations);
|
|
1043
|
+
this.assertOpen();
|
|
1044
|
+
}
|
|
1045
|
+
this.releaseInvalidations = releaseInvalidations;
|
|
1046
|
+
this.authorizationRevision = Math.max(this.authorizationRevision, await this.requireRead());
|
|
1047
|
+
this.assertOpen();
|
|
1048
|
+
let editDecision = await this.checkPermission("edit");
|
|
1049
|
+
if (editDecision.revision > this.authorizationRevision) {
|
|
1050
|
+
this.authorizationRevision = editDecision.revision;
|
|
1051
|
+
await this.requireRead();
|
|
1052
|
+
} else if (editDecision.revision < this.authorizationRevision) {
|
|
1053
|
+
editDecision = await this.checkPermission("edit");
|
|
1054
|
+
}
|
|
1055
|
+
this.writable = editDecision.revision === this.authorizationRevision && editDecision.allowed;
|
|
1056
|
+
this.assertOpen();
|
|
1057
|
+
const bootstrap = await this.dependencies.read.readBootstrap({
|
|
1058
|
+
scope: this.ticket.scope,
|
|
1059
|
+
artifactId: this.artifactId,
|
|
1060
|
+
resume: this.resume,
|
|
1061
|
+
protocolVersion: this.options.protocolVersion
|
|
1062
|
+
});
|
|
1063
|
+
this.assertOpen();
|
|
1064
|
+
validateBootstrap(bootstrap, this.artifactId, this.modality, this.resume, this.options);
|
|
1065
|
+
this.targetHead = bootstrap.headSequence;
|
|
1066
|
+
await this.send({
|
|
1067
|
+
type: "open",
|
|
1068
|
+
protocolVersion: this.options.protocolVersion,
|
|
1069
|
+
artifactId: this.artifactId,
|
|
1070
|
+
streamEpoch: this.streamEpoch,
|
|
1071
|
+
modality: this.modality,
|
|
1072
|
+
writable: this.writable,
|
|
1073
|
+
headSequence: bootstrap.headSequence,
|
|
1074
|
+
minimumReplaySequence: bootstrap.minimumReplaySequence,
|
|
1075
|
+
maxClientFrameBytes: this.options.maxClientFrameBytes,
|
|
1076
|
+
maxCommandBytes: Math.min(
|
|
1077
|
+
EDITABLE_ARTIFACT_COMMAND_MAX_BYTES,
|
|
1078
|
+
Math.max(0, this.options.maxClientFrameBytes - 64 * 1024)
|
|
1079
|
+
),
|
|
1080
|
+
maxIntentBytes: Math.min(
|
|
1081
|
+
EDITABLE_ARTIFACT_INTENT_MAX_BYTES,
|
|
1082
|
+
Math.max(0, this.options.maxClientFrameBytes - 64 * 1024)
|
|
1083
|
+
),
|
|
1084
|
+
maxCommittedTransactionBytes: Math.min(
|
|
1085
|
+
EDITABLE_ARTIFACT_MAX_COMMITTED_TRANSACTION_BYTES,
|
|
1086
|
+
Math.max(0, this.options.maxOutboundFrameBytes - 64 * 1024)
|
|
1087
|
+
),
|
|
1088
|
+
maxSnapshotBytes: this.options.maxSnapshotBytes,
|
|
1089
|
+
maxInFlightTransactions: this.options.maxInFlightTransactions,
|
|
1090
|
+
maxInFlightBytes: this.options.maxInFlightBytes
|
|
1091
|
+
});
|
|
1092
|
+
await this.applyBootstrap(bootstrap);
|
|
1093
|
+
await this.reconcileFromDurableHead();
|
|
1094
|
+
await this.sendBarrier();
|
|
1095
|
+
this.bootstrapping = false;
|
|
1096
|
+
if (this.reconcileRequested) this.requestReconcile();
|
|
1097
|
+
this.maintenance = this.runMaintenance().catch((error) => {
|
|
1098
|
+
if (!this.abort.signal.aborted) void this.fail(error);
|
|
1099
|
+
});
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
await this.fail(error);
|
|
1102
|
+
throw error;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
receive(bytes) {
|
|
1106
|
+
if (this.closedValue)
|
|
1107
|
+
return Promise.reject(new EditableArtifactLiveError("closed", "Live session is closed"));
|
|
1108
|
+
if (!(bytes instanceof Uint8Array)) {
|
|
1109
|
+
return Promise.reject(new TypeError("Live frame must be Uint8Array"));
|
|
1110
|
+
}
|
|
1111
|
+
if (bytes.byteLength > this.options.maxClientFrameBytes) {
|
|
1112
|
+
const error = new EditableArtifactLiveError(
|
|
1113
|
+
"oversized_frame",
|
|
1114
|
+
"Client live frame exceeds the byte limit"
|
|
1115
|
+
);
|
|
1116
|
+
void this.fail(error);
|
|
1117
|
+
return Promise.reject(error);
|
|
1118
|
+
}
|
|
1119
|
+
let frame;
|
|
1120
|
+
try {
|
|
1121
|
+
frame = decodeClientFrame(
|
|
1122
|
+
bytes,
|
|
1123
|
+
this.artifactId,
|
|
1124
|
+
this.streamEpoch,
|
|
1125
|
+
this.options.protocolVersion
|
|
1126
|
+
);
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
const failure = error instanceof EditableArtifactLiveError ? error : new EditableArtifactLiveError("invalid_frame", "Client live frame is malformed", {
|
|
1129
|
+
cause: error
|
|
1130
|
+
});
|
|
1131
|
+
void this.fail(failure);
|
|
1132
|
+
return Promise.reject(failure);
|
|
1133
|
+
}
|
|
1134
|
+
return this.enqueueInbound(bytes.byteLength, async () => {
|
|
1135
|
+
await this.applyAcknowledgement(frame);
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
acknowledge(frame) {
|
|
1139
|
+
if (this.closedValue) {
|
|
1140
|
+
return Promise.reject(new EditableArtifactLiveError("closed", "Live session is closed"));
|
|
1141
|
+
}
|
|
1142
|
+
let normalized;
|
|
1143
|
+
try {
|
|
1144
|
+
normalized = validateAppliedFrame(
|
|
1145
|
+
frame,
|
|
1146
|
+
this.artifactId,
|
|
1147
|
+
this.streamEpoch,
|
|
1148
|
+
this.options.protocolVersion
|
|
1149
|
+
);
|
|
1150
|
+
} catch (error) {
|
|
1151
|
+
const failure = error instanceof EditableArtifactLiveError ? error : new EditableArtifactLiveError("invalid_frame", "Client live ACK is malformed", {
|
|
1152
|
+
cause: error
|
|
1153
|
+
});
|
|
1154
|
+
void this.fail(failure);
|
|
1155
|
+
return Promise.reject(failure);
|
|
1156
|
+
}
|
|
1157
|
+
return this.enqueueInbound(256, async () => {
|
|
1158
|
+
await this.applyAcknowledgement(normalized);
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
submitIntent(input) {
|
|
1162
|
+
if (this.closedValue) {
|
|
1163
|
+
return Promise.reject(new EditableArtifactLiveError("closed", "Live session is closed"));
|
|
1164
|
+
}
|
|
1165
|
+
if (input.protocolVersion !== this.options.protocolVersion) {
|
|
1166
|
+
const error = new EditableArtifactLiveError(
|
|
1167
|
+
"protocol_mismatch",
|
|
1168
|
+
"Mutation protocol mismatch"
|
|
1169
|
+
);
|
|
1170
|
+
void this.fail(error);
|
|
1171
|
+
return Promise.reject(error);
|
|
1172
|
+
}
|
|
1173
|
+
let inputArtifactId;
|
|
1174
|
+
try {
|
|
1175
|
+
inputArtifactId = editableArtifactId(input.artifactId);
|
|
1176
|
+
} catch (cause) {
|
|
1177
|
+
const error = new EditableArtifactLiveError(
|
|
1178
|
+
"invalid_frame",
|
|
1179
|
+
"Mutation artifact id is malformed",
|
|
1180
|
+
{ cause }
|
|
1181
|
+
);
|
|
1182
|
+
void this.fail(error);
|
|
1183
|
+
return Promise.reject(error);
|
|
1184
|
+
}
|
|
1185
|
+
if (inputArtifactId !== this.artifactId) {
|
|
1186
|
+
const error = new EditableArtifactLiveError("invalid_frame", "Mutation artifact mismatch");
|
|
1187
|
+
void this.fail(error);
|
|
1188
|
+
return Promise.reject(error);
|
|
1189
|
+
}
|
|
1190
|
+
if (input.streamEpoch !== this.streamEpoch) {
|
|
1191
|
+
const error = new EditableArtifactLiveError(
|
|
1192
|
+
"stale_epoch",
|
|
1193
|
+
"Mutation belongs to a stale stream epoch"
|
|
1194
|
+
);
|
|
1195
|
+
void this.fail(error);
|
|
1196
|
+
return Promise.reject(error);
|
|
1197
|
+
}
|
|
1198
|
+
try {
|
|
1199
|
+
editableArtifactRequestHash(input.requestHash);
|
|
1200
|
+
} catch (cause) {
|
|
1201
|
+
const error = new EditableArtifactLiveError(
|
|
1202
|
+
"invalid_frame",
|
|
1203
|
+
"Mutation request hash is malformed",
|
|
1204
|
+
{ cause }
|
|
1205
|
+
);
|
|
1206
|
+
return Promise.reject(error);
|
|
1207
|
+
}
|
|
1208
|
+
if (!(input.intentBytes instanceof Uint8Array) || input.intentBytes.byteLength === 0 || input.intentBytes.byteLength > this.options.maxClientFrameBytes) {
|
|
1209
|
+
const error = new EditableArtifactLiveError(
|
|
1210
|
+
"oversized_frame",
|
|
1211
|
+
"Mutation intent exceeds the byte limit"
|
|
1212
|
+
);
|
|
1213
|
+
void this.fail(error);
|
|
1214
|
+
return Promise.reject(error);
|
|
1215
|
+
}
|
|
1216
|
+
return this.enqueueInbound(
|
|
1217
|
+
input.intentBytes.byteLength,
|
|
1218
|
+
() => this.applyMutation(input),
|
|
1219
|
+
false
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
reconcileNow() {
|
|
1223
|
+
return this.enqueue(() => this.reconcileFromDurableHead());
|
|
1224
|
+
}
|
|
1225
|
+
reauthorizeNow() {
|
|
1226
|
+
return this.enqueue(() => this.reauthorize());
|
|
1227
|
+
}
|
|
1228
|
+
async close(reason = "closed") {
|
|
1229
|
+
if (this.closedValue) return;
|
|
1230
|
+
const close = closeValue(reason);
|
|
1231
|
+
this.closedValue = close;
|
|
1232
|
+
this.abort.abort();
|
|
1233
|
+
safeRelease(this.releaseHints);
|
|
1234
|
+
this.releaseHints = null;
|
|
1235
|
+
safeRelease(this.releaseInvalidations);
|
|
1236
|
+
this.releaseInvalidations = null;
|
|
1237
|
+
safeRelease(this.detachParent);
|
|
1238
|
+
this.detachParent = null;
|
|
1239
|
+
try {
|
|
1240
|
+
this.sink.close(close);
|
|
1241
|
+
} catch {
|
|
1242
|
+
}
|
|
1243
|
+
this.resolveClosed(close);
|
|
1244
|
+
}
|
|
1245
|
+
assertOpen() {
|
|
1246
|
+
if (this.closedValue) {
|
|
1247
|
+
throw new EditableArtifactLiveError("closed", "Live session is closed");
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
enqueue(task) {
|
|
1251
|
+
const result = this.workTail.then(async () => {
|
|
1252
|
+
if (this.closedValue) {
|
|
1253
|
+
throw new EditableArtifactLiveError("closed", "Live session is closed");
|
|
1254
|
+
}
|
|
1255
|
+
return await task();
|
|
1256
|
+
});
|
|
1257
|
+
this.workTail = result.then(
|
|
1258
|
+
() => void 0,
|
|
1259
|
+
(error) => this.fail(error)
|
|
1260
|
+
);
|
|
1261
|
+
return result;
|
|
1262
|
+
}
|
|
1263
|
+
enqueueInbound(bytes, task, fatal = true) {
|
|
1264
|
+
if (this.queuedInboundFrames + 1 > this.options.maxInboundQueueFrames || this.queuedInboundBytes + bytes > this.options.maxInboundQueueBytes) {
|
|
1265
|
+
const error = new EditableArtifactLiveError(
|
|
1266
|
+
"slow_client",
|
|
1267
|
+
"Live inbound work queue is full",
|
|
1268
|
+
{ requiresSnapshot: true }
|
|
1269
|
+
);
|
|
1270
|
+
void this.fail(error);
|
|
1271
|
+
return Promise.reject(error);
|
|
1272
|
+
}
|
|
1273
|
+
this.queuedInboundFrames += 1;
|
|
1274
|
+
this.queuedInboundBytes += bytes;
|
|
1275
|
+
const run = async () => {
|
|
1276
|
+
try {
|
|
1277
|
+
return await task();
|
|
1278
|
+
} finally {
|
|
1279
|
+
this.queuedInboundFrames -= 1;
|
|
1280
|
+
this.queuedInboundBytes -= bytes;
|
|
1281
|
+
}
|
|
1282
|
+
};
|
|
1283
|
+
if (fatal) return this.enqueue(run);
|
|
1284
|
+
const result = this.workTail.then(async () => {
|
|
1285
|
+
if (this.closedValue) {
|
|
1286
|
+
throw new EditableArtifactLiveError("closed", "Live session is closed");
|
|
1287
|
+
}
|
|
1288
|
+
return await run();
|
|
1289
|
+
});
|
|
1290
|
+
this.workTail = result.then(
|
|
1291
|
+
() => void 0,
|
|
1292
|
+
() => void 0
|
|
1293
|
+
);
|
|
1294
|
+
return result;
|
|
1295
|
+
}
|
|
1296
|
+
requestReconcile() {
|
|
1297
|
+
if (this.closedValue) return;
|
|
1298
|
+
this.reconcileRequested = true;
|
|
1299
|
+
if (this.bootstrapping || this.reconcileScheduled) return;
|
|
1300
|
+
this.reconcileScheduled = true;
|
|
1301
|
+
void this.enqueue(async () => {
|
|
1302
|
+
try {
|
|
1303
|
+
while (this.reconcileRequested && !this.closedValue) {
|
|
1304
|
+
this.reconcileRequested = false;
|
|
1305
|
+
await this.reconcileFromDurableHead();
|
|
1306
|
+
}
|
|
1307
|
+
} finally {
|
|
1308
|
+
this.reconcileScheduled = false;
|
|
1309
|
+
if (this.reconcileRequested && !this.closedValue) this.requestReconcile();
|
|
1310
|
+
}
|
|
1311
|
+
}).catch(() => void 0);
|
|
1312
|
+
}
|
|
1313
|
+
requestReauthorization() {
|
|
1314
|
+
if (this.closedValue) return;
|
|
1315
|
+
this.reauthorizationRequested = true;
|
|
1316
|
+
if (this.reauthorizationScheduled) return;
|
|
1317
|
+
this.reauthorizationScheduled = true;
|
|
1318
|
+
void this.enqueue(async () => {
|
|
1319
|
+
try {
|
|
1320
|
+
while (this.reauthorizationRequested && !this.closedValue) {
|
|
1321
|
+
this.reauthorizationRequested = false;
|
|
1322
|
+
await this.reauthorize();
|
|
1323
|
+
}
|
|
1324
|
+
} finally {
|
|
1325
|
+
this.reauthorizationScheduled = false;
|
|
1326
|
+
if (this.reauthorizationRequested && !this.closedValue) {
|
|
1327
|
+
this.requestReauthorization();
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}).catch(() => void 0);
|
|
1331
|
+
}
|
|
1332
|
+
async applyBootstrap(bootstrap) {
|
|
1333
|
+
if (bootstrap.resumeAccepted) {
|
|
1334
|
+
this.sentSequence = bootstrap.resumeSequence;
|
|
1335
|
+
this.ackedSequence = bootstrap.resumeSequence;
|
|
1336
|
+
this.sentStateHash = bootstrap.resumeStateHash;
|
|
1337
|
+
this.ackedStateHash = bootstrap.resumeStateHash;
|
|
1338
|
+
this.boundaries.set(bootstrap.resumeSequence, bootstrap.resumeStateHash);
|
|
1339
|
+
} else if (bootstrap.snapshot) {
|
|
1340
|
+
await this.sendSnapshot(bootstrap.snapshot);
|
|
1341
|
+
this.sentSequence = bootstrap.snapshot.sequence;
|
|
1342
|
+
this.sentStateHash = bootstrap.snapshot.stateHash;
|
|
1343
|
+
this.boundaries.set(bootstrap.snapshot.sequence, bootstrap.snapshot.stateHash);
|
|
1344
|
+
if (bootstrap.snapshot.sequence === 0) {
|
|
1345
|
+
this.ackedStateHash = bootstrap.snapshot.stateHash;
|
|
1346
|
+
}
|
|
1347
|
+
} else {
|
|
1348
|
+
await this.requireResync("retention_gap", bootstrap.headSequence);
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
if (this.sentSequence + 1 < bootstrap.minimumReplaySequence) {
|
|
1352
|
+
await this.requireResync("retention_gap", bootstrap.headSequence);
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
await this.pumpThrough(bootstrap.headSequence);
|
|
1356
|
+
if (!this.closedValue && this.sentSequence === bootstrap.headSequence && this.sentStateHash !== bootstrap.stateHash) {
|
|
1357
|
+
await this.requireResync("durable_gap", bootstrap.headSequence);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
async sendSnapshot(snapshot) {
|
|
1361
|
+
if (snapshot.bytes.byteLength > this.options.maxSnapshotBytes) {
|
|
1362
|
+
await this.requireResync("oversized_frame", snapshot.sequence);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
const snapshotBytes = snapshot.bytes;
|
|
1366
|
+
if (snapshot.modality !== this.modality) {
|
|
1367
|
+
throw new EditableArtifactLiveError(
|
|
1368
|
+
"durable_gap",
|
|
1369
|
+
"Snapshot modality differs from its durable ticket",
|
|
1370
|
+
{ requiresSnapshot: true }
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
const causalFrontier = snapshot.modality === "spreadsheet" ? validateFrontier(snapshot.causalFrontier, "snapshot causal frontier") : null;
|
|
1374
|
+
const totalBytes = snapshotBytes.byteLength;
|
|
1375
|
+
if (totalBytes === 0)
|
|
1376
|
+
throw new EditableArtifactLiveError("durable_gap", "Snapshot is empty", {
|
|
1377
|
+
requiresSnapshot: true
|
|
1378
|
+
});
|
|
1379
|
+
for (let offset = 0; offset < totalBytes; offset += this.options.snapshotChunkBytes) {
|
|
1380
|
+
const bytes = snapshotBytes.slice(
|
|
1381
|
+
offset,
|
|
1382
|
+
Math.min(offset + this.options.snapshotChunkBytes, totalBytes)
|
|
1383
|
+
);
|
|
1384
|
+
const common = {
|
|
1385
|
+
type: "snapshot",
|
|
1386
|
+
protocolVersion: this.options.protocolVersion,
|
|
1387
|
+
artifactId: this.artifactId,
|
|
1388
|
+
streamEpoch: this.streamEpoch,
|
|
1389
|
+
sequence: snapshot.sequence,
|
|
1390
|
+
stateHash: snapshot.stateHash,
|
|
1391
|
+
digest: snapshot.digest,
|
|
1392
|
+
kernelVersion: snapshot.kernelVersion,
|
|
1393
|
+
modelSchemaVersion: snapshot.modelSchemaVersion,
|
|
1394
|
+
offset,
|
|
1395
|
+
totalBytes,
|
|
1396
|
+
final: offset + bytes.byteLength === totalBytes,
|
|
1397
|
+
bytes
|
|
1398
|
+
};
|
|
1399
|
+
await this.send(
|
|
1400
|
+
snapshot.modality === "spreadsheet" ? { ...common, modality: snapshot.modality, causalFrontier } : {
|
|
1401
|
+
...common,
|
|
1402
|
+
modality: snapshot.modality,
|
|
1403
|
+
nativeRevision: snapshot.nativeRevision
|
|
1404
|
+
}
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
async reconcileFromDurableHead() {
|
|
1409
|
+
if (this.closedValue) return;
|
|
1410
|
+
const head = await this.dependencies.read.readHead(this.ticket.scope, this.artifactId);
|
|
1411
|
+
validateHead(head, this.modality);
|
|
1412
|
+
if (head.headSequence < this.sentSequence) {
|
|
1413
|
+
await this.requireResync("durable_gap", head.headSequence);
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
this.targetHead = head.headSequence;
|
|
1417
|
+
if (this.sentSequence + 1 < head.minimumReplaySequence) {
|
|
1418
|
+
await this.requireResync("retention_gap", head.headSequence);
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
await this.pumpThrough(head.headSequence);
|
|
1422
|
+
if (!this.closedValue && this.sentSequence === head.headSequence && this.sentStateHash !== head.stateHash) {
|
|
1423
|
+
await this.requireResync("durable_gap", head.headSequence);
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
if (!this.closedValue) {
|
|
1427
|
+
await this.send({
|
|
1428
|
+
type: "watermark",
|
|
1429
|
+
protocolVersion: this.options.protocolVersion,
|
|
1430
|
+
artifactId: this.artifactId,
|
|
1431
|
+
streamEpoch: this.streamEpoch,
|
|
1432
|
+
headSequence: head.headSequence
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
async pumpThrough(through) {
|
|
1437
|
+
assertNonnegativeSafeInteger(through, "target head sequence");
|
|
1438
|
+
while (!this.closedValue && this.sentSequence < through) {
|
|
1439
|
+
if (this.inFlight.size >= this.options.maxInFlightTransactions) return;
|
|
1440
|
+
const page = await this.dependencies.read.readTransactions({
|
|
1441
|
+
scope: this.ticket.scope,
|
|
1442
|
+
artifactId: this.artifactId,
|
|
1443
|
+
after: this.sentSequence,
|
|
1444
|
+
through,
|
|
1445
|
+
maxCount: this.options.replayPageTransactions,
|
|
1446
|
+
maxBytes: this.options.replayPageBytes
|
|
1447
|
+
});
|
|
1448
|
+
validatePage(
|
|
1449
|
+
page,
|
|
1450
|
+
this.sentSequence,
|
|
1451
|
+
through,
|
|
1452
|
+
this.options.replayPageTransactions,
|
|
1453
|
+
this.options.replayPageBytes
|
|
1454
|
+
);
|
|
1455
|
+
if (this.sentSequence + 1 < page.minimumReplaySequence) {
|
|
1456
|
+
await this.requireResync("retention_gap", page.headSequence);
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
if (page.transactions.length === 0) {
|
|
1460
|
+
await this.requireResync("durable_gap", page.headSequence);
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
let progressed = false;
|
|
1464
|
+
for (const transaction of page.transactions) {
|
|
1465
|
+
validateTransaction(
|
|
1466
|
+
transaction,
|
|
1467
|
+
this.artifactId,
|
|
1468
|
+
this.modality,
|
|
1469
|
+
Math.min(
|
|
1470
|
+
this.options.replayPageBytes,
|
|
1471
|
+
Math.max(0, this.options.maxOutboundFrameBytes - 64 * 1024)
|
|
1472
|
+
)
|
|
1473
|
+
);
|
|
1474
|
+
if (transaction.endSequence > through) {
|
|
1475
|
+
await this.requireResync("durable_gap", page.headSequence);
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
if (transaction.startSequence !== this.sentSequence + 1) {
|
|
1479
|
+
await this.requireResync("durable_gap", page.headSequence);
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
if (this.sentStateHash && transaction.priorStateHash !== this.sentStateHash) {
|
|
1483
|
+
await this.requireResync("durable_gap", page.headSequence);
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
const ownedTransaction = ownTransaction(transaction);
|
|
1487
|
+
const frameBytes = estimateFrameBytes({
|
|
1488
|
+
type: "transaction",
|
|
1489
|
+
protocolVersion: this.options.protocolVersion,
|
|
1490
|
+
artifactId: this.artifactId,
|
|
1491
|
+
streamEpoch: this.streamEpoch,
|
|
1492
|
+
transaction: ownedTransaction
|
|
1493
|
+
});
|
|
1494
|
+
if (frameBytes > this.options.maxOutboundFrameBytes || frameBytes > this.options.maxInFlightBytes) {
|
|
1495
|
+
await this.requireResync("oversized_frame", page.headSequence);
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
if (this.inFlight.size >= this.options.maxInFlightTransactions || this.inFlightBytes + frameBytes > this.options.maxInFlightBytes) {
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
await this.send({
|
|
1502
|
+
type: "transaction",
|
|
1503
|
+
protocolVersion: this.options.protocolVersion,
|
|
1504
|
+
artifactId: this.artifactId,
|
|
1505
|
+
streamEpoch: this.streamEpoch,
|
|
1506
|
+
transaction: ownedTransaction
|
|
1507
|
+
});
|
|
1508
|
+
this.sentSequence = ownedTransaction.endSequence;
|
|
1509
|
+
this.sentStateHash = ownedTransaction.stateHash;
|
|
1510
|
+
this.boundaries.set(ownedTransaction.endSequence, ownedTransaction.stateHash);
|
|
1511
|
+
const sentAt = this.dependencies.clock.now().getTime();
|
|
1512
|
+
this.inFlight.set(ownedTransaction.endSequence, {
|
|
1513
|
+
bytes: frameBytes,
|
|
1514
|
+
stateHash: ownedTransaction.stateHash,
|
|
1515
|
+
sentAt
|
|
1516
|
+
});
|
|
1517
|
+
this.inFlightBytes += frameBytes;
|
|
1518
|
+
if (this.oldestInFlightAt === 0) this.oldestInFlightAt = sentAt;
|
|
1519
|
+
progressed = true;
|
|
1520
|
+
if (this.sentSequence >= through) break;
|
|
1521
|
+
}
|
|
1522
|
+
if (!progressed) return;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
async applyAcknowledgement(frame) {
|
|
1526
|
+
if (frame.sequence < this.ackedSequence || frame.sequence > this.sentSequence) {
|
|
1527
|
+
await this.requireResync("invalid_ack", this.targetHead);
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
const expected = frame.sequence === this.ackedSequence ? this.ackedStateHash : this.boundaries.get(frame.sequence);
|
|
1531
|
+
if (!expected || expected !== frame.stateHash) {
|
|
1532
|
+
await this.requireResync("invalid_ack", this.targetHead);
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
if (frame.sequence > this.ackedSequence) {
|
|
1536
|
+
await this.dependencies.read.acknowledgeReplica({
|
|
1537
|
+
scope: this.ticket.scope,
|
|
1538
|
+
artifactId: this.artifactId,
|
|
1539
|
+
replicaId: this.ticket.actor.replicaId,
|
|
1540
|
+
actorKey: editableArtifactActorKey(this.ticket.actor),
|
|
1541
|
+
streamEpoch: this.streamEpoch,
|
|
1542
|
+
sequence: frame.sequence,
|
|
1543
|
+
stateHash: frame.stateHash
|
|
1544
|
+
});
|
|
1545
|
+
this.ackedSequence = frame.sequence;
|
|
1546
|
+
this.ackedStateHash = frame.stateHash;
|
|
1547
|
+
for (const [sequence, inFlight] of this.inFlight) {
|
|
1548
|
+
if (sequence <= frame.sequence) {
|
|
1549
|
+
this.inFlight.delete(sequence);
|
|
1550
|
+
this.inFlightBytes -= inFlight.bytes;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
for (const sequence of this.boundaries.keys()) {
|
|
1554
|
+
if (sequence < frame.sequence) this.boundaries.delete(sequence);
|
|
1555
|
+
}
|
|
1556
|
+
this.oldestInFlightAt = this.inFlight.size === 0 ? 0 : Math.min(...[...this.inFlight.values()].map((value) => value.sentAt));
|
|
1557
|
+
}
|
|
1558
|
+
await this.send({
|
|
1559
|
+
type: "applied",
|
|
1560
|
+
protocolVersion: this.options.protocolVersion,
|
|
1561
|
+
artifactId: this.artifactId,
|
|
1562
|
+
streamEpoch: this.streamEpoch,
|
|
1563
|
+
sequence: frame.sequence,
|
|
1564
|
+
stateHash: frame.stateHash
|
|
1565
|
+
});
|
|
1566
|
+
await this.reconcileFromDurableHead();
|
|
1567
|
+
}
|
|
1568
|
+
async applyMutation(frame) {
|
|
1569
|
+
await this.reauthorize();
|
|
1570
|
+
if (this.closedValue) {
|
|
1571
|
+
throw new EditableArtifactLiveError(
|
|
1572
|
+
"permission_changed",
|
|
1573
|
+
"Editable artifact read permission changed"
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
if (!this.writable) {
|
|
1577
|
+
throw new EditableArtifactLiveError(
|
|
1578
|
+
"permission_changed",
|
|
1579
|
+
"Editable artifact edit permission denied"
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
let result;
|
|
1583
|
+
try {
|
|
1584
|
+
result = await this.dependencies.domain.applyTransaction({
|
|
1585
|
+
scope: this.ticket.scope,
|
|
1586
|
+
artifactId: this.artifactId,
|
|
1587
|
+
actor: this.ticket.actor,
|
|
1588
|
+
request: {
|
|
1589
|
+
intentBytes: frame.intentBytes.slice(),
|
|
1590
|
+
requestHash: frame.requestHash
|
|
1591
|
+
}
|
|
1592
|
+
});
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
await this.reauthorize();
|
|
1595
|
+
throw error;
|
|
1596
|
+
}
|
|
1597
|
+
const transaction = await this.dependencies.read.readCommittedTransaction({
|
|
1598
|
+
scope: this.ticket.scope,
|
|
1599
|
+
artifactId: this.artifactId,
|
|
1600
|
+
transactionId: result.receipt.serverTransactionId
|
|
1601
|
+
});
|
|
1602
|
+
if (!transaction || result.receipt.modality !== this.modality || transaction.modality !== this.modality || transaction.startSequence !== result.receipt.sequenceStart || transaction.endSequence !== result.receipt.sequenceEnd || transaction.transactionId !== result.receipt.serverTransactionId || result.receipt.requestHash !== frame.requestHash || transaction.requestHash !== result.receipt.requestHash || transaction.stateHash !== result.receipt.stateHash) {
|
|
1603
|
+
await this.requireResync("durable_gap", this.targetHead);
|
|
1604
|
+
throw new EditableArtifactLiveError(
|
|
1605
|
+
"durable_gap",
|
|
1606
|
+
"Committed mutation is unavailable from durable replay",
|
|
1607
|
+
{ requiresSnapshot: true }
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
validateTransaction(
|
|
1611
|
+
transaction,
|
|
1612
|
+
this.artifactId,
|
|
1613
|
+
this.modality,
|
|
1614
|
+
Math.min(
|
|
1615
|
+
this.options.replayPageBytes,
|
|
1616
|
+
Math.max(0, this.options.maxOutboundFrameBytes - 64 * 1024)
|
|
1617
|
+
)
|
|
1618
|
+
);
|
|
1619
|
+
const ownedTransaction = ownTransaction(transaction);
|
|
1620
|
+
await this.reconcileFromDurableHead();
|
|
1621
|
+
return Object.freeze({
|
|
1622
|
+
clientTransactionId: result.receipt.clientTransactionId,
|
|
1623
|
+
requestHash: result.receipt.requestHash,
|
|
1624
|
+
transaction: ownedTransaction
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
async reauthorize() {
|
|
1628
|
+
let readDecision = await this.checkPermission("read");
|
|
1629
|
+
let editDecision = await this.checkPermission("edit");
|
|
1630
|
+
const targetRevision = Math.max(
|
|
1631
|
+
this.authorizationRevision,
|
|
1632
|
+
readDecision.revision,
|
|
1633
|
+
editDecision.revision
|
|
1634
|
+
);
|
|
1635
|
+
if (readDecision.revision < targetRevision) {
|
|
1636
|
+
readDecision = await this.checkPermission("read");
|
|
1637
|
+
}
|
|
1638
|
+
if (editDecision.revision < targetRevision) {
|
|
1639
|
+
editDecision = await this.checkPermission("edit");
|
|
1640
|
+
}
|
|
1641
|
+
const coherentRevision = Math.max(targetRevision, readDecision.revision, editDecision.revision);
|
|
1642
|
+
if (readDecision.revision !== coherentRevision) {
|
|
1643
|
+
await this.close("permission_changed");
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
if (!readDecision.allowed) {
|
|
1647
|
+
await this.close("permission_changed");
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
const nextWritable = editDecision.revision === coherentRevision && editDecision.allowed;
|
|
1651
|
+
const revisionChanged = coherentRevision !== this.authorizationRevision;
|
|
1652
|
+
this.authorizationRevision = coherentRevision;
|
|
1653
|
+
if (nextWritable !== this.writable || revisionChanged) {
|
|
1654
|
+
this.writable = nextWritable;
|
|
1655
|
+
await this.sendAuthorization();
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
async sendAuthorization() {
|
|
1659
|
+
await this.send({
|
|
1660
|
+
type: "authorizationChanged",
|
|
1661
|
+
protocolVersion: this.options.protocolVersion,
|
|
1662
|
+
artifactId: this.artifactId,
|
|
1663
|
+
streamEpoch: this.streamEpoch,
|
|
1664
|
+
writable: this.writable
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
async requireRead() {
|
|
1668
|
+
const decision = await this.checkPermission("read");
|
|
1669
|
+
if (!decision.allowed || decision.revision < this.authorizationRevision) {
|
|
1670
|
+
throw new EditableArtifactLiveError(
|
|
1671
|
+
"permission_changed",
|
|
1672
|
+
"Editable artifact read permission denied"
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
return decision.revision;
|
|
1676
|
+
}
|
|
1677
|
+
async checkPermission(permission) {
|
|
1678
|
+
const decision = await this.dependencies.authorization.authorize({
|
|
1679
|
+
scope: this.ticket.scope,
|
|
1680
|
+
artifactId: this.artifactId,
|
|
1681
|
+
actor: this.ticket.actor,
|
|
1682
|
+
permission
|
|
1683
|
+
});
|
|
1684
|
+
if (permission === "edit" && !this.ticket.allowEdit) {
|
|
1685
|
+
return Object.freeze({ ...decision, allowed: false });
|
|
1686
|
+
}
|
|
1687
|
+
return decision;
|
|
1688
|
+
}
|
|
1689
|
+
async sendBarrier() {
|
|
1690
|
+
if (!this.sentStateHash)
|
|
1691
|
+
throw new EditableArtifactLiveError("durable_gap", "Bootstrap has no state boundary");
|
|
1692
|
+
await this.send({
|
|
1693
|
+
type: "barrier",
|
|
1694
|
+
protocolVersion: this.options.protocolVersion,
|
|
1695
|
+
artifactId: this.artifactId,
|
|
1696
|
+
streamEpoch: this.streamEpoch,
|
|
1697
|
+
sequence: this.sentSequence,
|
|
1698
|
+
stateHash: this.sentStateHash
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
async send(frame) {
|
|
1702
|
+
if (this.closedValue) return;
|
|
1703
|
+
const bytes = estimateFrameBytes(frame);
|
|
1704
|
+
if (bytes > this.options.maxOutboundFrameBytes) {
|
|
1705
|
+
throw new EditableArtifactLiveError(
|
|
1706
|
+
"oversized_frame",
|
|
1707
|
+
"Server live frame exceeds the byte limit",
|
|
1708
|
+
{ requiresSnapshot: true }
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1711
|
+
const buffered = this.sink.bufferedBytes();
|
|
1712
|
+
if (!Number.isSafeInteger(buffered) || buffered < 0 || buffered + bytes > this.options.maxSocketBufferedBytes) {
|
|
1713
|
+
throw new EditableArtifactLiveError("slow_client", "Live socket send buffer is full", {
|
|
1714
|
+
retryable: true,
|
|
1715
|
+
requiresSnapshot: true
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
const timeout = new AbortController();
|
|
1719
|
+
try {
|
|
1720
|
+
await Promise.race([
|
|
1721
|
+
this.sink.send(frame),
|
|
1722
|
+
this.dependencies.scheduler.sleep(this.options.sendTimeoutMs, timeout.signal).then(() => {
|
|
1723
|
+
throw new EditableArtifactLiveError("slow_client", "Live socket write timed out", {
|
|
1724
|
+
retryable: true,
|
|
1725
|
+
requiresSnapshot: true
|
|
1726
|
+
});
|
|
1727
|
+
})
|
|
1728
|
+
]);
|
|
1729
|
+
} finally {
|
|
1730
|
+
timeout.abort();
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
async requireResync(reason, headSequence) {
|
|
1734
|
+
if (this.closedValue) return;
|
|
1735
|
+
try {
|
|
1736
|
+
await this.send({
|
|
1737
|
+
type: "resyncRequired",
|
|
1738
|
+
protocolVersion: this.options.protocolVersion,
|
|
1739
|
+
artifactId: this.artifactId,
|
|
1740
|
+
streamEpoch: this.streamEpoch,
|
|
1741
|
+
reason,
|
|
1742
|
+
headSequence
|
|
1743
|
+
});
|
|
1744
|
+
} catch {
|
|
1745
|
+
}
|
|
1746
|
+
await this.close(reason);
|
|
1747
|
+
}
|
|
1748
|
+
async runMaintenance() {
|
|
1749
|
+
let nextReconcile = this.dependencies.clock.now().getTime() + this.options.reconcileIntervalMs;
|
|
1750
|
+
let nextAuthorization = this.dependencies.clock.now().getTime() + this.options.reauthorizeIntervalMs;
|
|
1751
|
+
const tick = Math.max(
|
|
1752
|
+
10,
|
|
1753
|
+
Math.min(
|
|
1754
|
+
this.options.reconcileIntervalMs,
|
|
1755
|
+
this.options.reauthorizeIntervalMs,
|
|
1756
|
+
this.options.ackTimeoutMs
|
|
1757
|
+
)
|
|
1758
|
+
);
|
|
1759
|
+
while (!this.abort.signal.aborted) {
|
|
1760
|
+
await this.dependencies.scheduler.sleep(tick, this.abort.signal);
|
|
1761
|
+
if (this.abort.signal.aborted || this.closedValue) return;
|
|
1762
|
+
const now = this.dependencies.clock.now().getTime();
|
|
1763
|
+
if (this.oldestInFlightAt !== 0 && now - this.oldestInFlightAt >= this.options.ackTimeoutMs) {
|
|
1764
|
+
await this.enqueue(() => this.requireResync("slow_client", this.targetHead));
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
if (now >= nextAuthorization) {
|
|
1768
|
+
await this.reauthorizeNow();
|
|
1769
|
+
nextAuthorization = now + this.options.reauthorizeIntervalMs;
|
|
1770
|
+
}
|
|
1771
|
+
if (this.closedValue) return;
|
|
1772
|
+
if (now >= nextReconcile) {
|
|
1773
|
+
await this.reconcileNow();
|
|
1774
|
+
nextReconcile = now + this.options.reconcileIntervalMs;
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
async fail(error) {
|
|
1779
|
+
if (this.closedValue) return;
|
|
1780
|
+
if (error instanceof EditableArtifactLiveError) {
|
|
1781
|
+
if (error.requiresSnapshot && ["durable_gap", "retention_gap", "invalid_ack", "slow_client", "oversized_frame"].includes(
|
|
1782
|
+
error.code
|
|
1783
|
+
)) {
|
|
1784
|
+
await this.requireResync(
|
|
1785
|
+
error.code,
|
|
1786
|
+
this.targetHead
|
|
1787
|
+
);
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
await this.close(error.code === "invalid_ticket" ? "transport_error" : error.code);
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
await this.close("transport_error");
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
function decodeClientFrame(bytes, artifactId, streamEpoch, protocolVersion) {
|
|
1797
|
+
let value;
|
|
1798
|
+
try {
|
|
1799
|
+
value = JSON.parse(strictDecoder.decode(bytes));
|
|
1800
|
+
} catch (cause) {
|
|
1801
|
+
throw new EditableArtifactLiveError(
|
|
1802
|
+
"invalid_frame",
|
|
1803
|
+
"Client live frame is not valid UTF-8 JSON",
|
|
1804
|
+
{ cause }
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1807
|
+
const frame = plainRecord(value, "client frame");
|
|
1808
|
+
const type = requiredString(frame, "type", 32);
|
|
1809
|
+
const actualProtocol = requiredSafeInteger(frame, "protocolVersion");
|
|
1810
|
+
const actualArtifact = editableArtifactId(requiredString(frame, "artifactId", 64));
|
|
1811
|
+
const actualEpoch = requiredString(frame, "streamEpoch", MAX_IDENTIFIER_BYTES);
|
|
1812
|
+
if (actualProtocol !== protocolVersion)
|
|
1813
|
+
throw new EditableArtifactLiveError("protocol_mismatch", "Client frame protocol mismatch");
|
|
1814
|
+
if (actualArtifact !== artifactId)
|
|
1815
|
+
throw new EditableArtifactLiveError("invalid_frame", "Client frame artifact mismatch");
|
|
1816
|
+
if (actualEpoch !== streamEpoch)
|
|
1817
|
+
throw new EditableArtifactLiveError(
|
|
1818
|
+
"stale_epoch",
|
|
1819
|
+
"Client frame belongs to a stale stream epoch"
|
|
1820
|
+
);
|
|
1821
|
+
if (type === "applied") {
|
|
1822
|
+
rejectUnknown(frame, [
|
|
1823
|
+
"type",
|
|
1824
|
+
"protocolVersion",
|
|
1825
|
+
"artifactId",
|
|
1826
|
+
"streamEpoch",
|
|
1827
|
+
"sequence",
|
|
1828
|
+
"stateHash"
|
|
1829
|
+
]);
|
|
1830
|
+
return validateAppliedFrame(
|
|
1831
|
+
Object.freeze({
|
|
1832
|
+
type,
|
|
1833
|
+
protocolVersion,
|
|
1834
|
+
artifactId,
|
|
1835
|
+
streamEpoch,
|
|
1836
|
+
sequence: requiredSafeInteger(frame, "sequence"),
|
|
1837
|
+
stateHash: editableArtifactStateHash(requiredString(frame, "stateHash", 80))
|
|
1838
|
+
}),
|
|
1839
|
+
artifactId,
|
|
1840
|
+
streamEpoch,
|
|
1841
|
+
protocolVersion
|
|
1842
|
+
);
|
|
1843
|
+
}
|
|
1844
|
+
throw new EditableArtifactLiveError("invalid_frame", `Unknown client live frame: ${type}`);
|
|
1845
|
+
}
|
|
1846
|
+
function validateAppliedFrame(frame, artifactId, streamEpoch, protocolVersion) {
|
|
1847
|
+
if (frame.type !== "applied") {
|
|
1848
|
+
throw new EditableArtifactLiveError("invalid_frame", "Client frame is not an apply ACK");
|
|
1849
|
+
}
|
|
1850
|
+
if (frame.protocolVersion !== protocolVersion) {
|
|
1851
|
+
throw new EditableArtifactLiveError("protocol_mismatch", "Client frame protocol mismatch");
|
|
1852
|
+
}
|
|
1853
|
+
if (editableArtifactId(frame.artifactId) !== artifactId) {
|
|
1854
|
+
throw new EditableArtifactLiveError("invalid_frame", "Client frame artifact mismatch");
|
|
1855
|
+
}
|
|
1856
|
+
boundedString(frame.streamEpoch, "stream epoch", MAX_IDENTIFIER_BYTES);
|
|
1857
|
+
if (frame.streamEpoch !== streamEpoch) {
|
|
1858
|
+
throw new EditableArtifactLiveError(
|
|
1859
|
+
"stale_epoch",
|
|
1860
|
+
"Client frame belongs to a stale stream epoch"
|
|
1861
|
+
);
|
|
1862
|
+
}
|
|
1863
|
+
assertNonnegativeSafeInteger(frame.sequence, "applied sequence");
|
|
1864
|
+
return Object.freeze({
|
|
1865
|
+
type: "applied",
|
|
1866
|
+
protocolVersion,
|
|
1867
|
+
artifactId,
|
|
1868
|
+
streamEpoch,
|
|
1869
|
+
sequence: frame.sequence,
|
|
1870
|
+
stateHash: editableArtifactStateHash(frame.stateHash)
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
function validateResume(resume, durableModality) {
|
|
1874
|
+
const resumeModality = resume.modality ?? "spreadsheet";
|
|
1875
|
+
if (resumeModality !== durableModality) {
|
|
1876
|
+
throw new EditableArtifactLiveError(
|
|
1877
|
+
"invalid_frame",
|
|
1878
|
+
"Resume modality differs from the durable artifact"
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1881
|
+
if (resume.localCursor !== null) assertNonnegativeSafeInteger(resume.localCursor, "local cursor");
|
|
1882
|
+
if (resume.localStateHash !== null) editableArtifactStateHash(resume.localStateHash);
|
|
1883
|
+
if (resumeModality === "spreadsheet") {
|
|
1884
|
+
if (!("localCausalFrontier" in resume)) {
|
|
1885
|
+
throw new TypeError("spreadsheet resume requires a causal frontier");
|
|
1886
|
+
}
|
|
1887
|
+
validateFrontier(resume.localCausalFrontier, "local causal frontier");
|
|
1888
|
+
} else if ("localNativeRevision" in resume && resume.localNativeRevision !== null) {
|
|
1889
|
+
assertNonnegativeSafeInteger(resume.localNativeRevision, "local native revision");
|
|
1890
|
+
} else if (!("localNativeRevision" in resume)) {
|
|
1891
|
+
throw new TypeError("serialized resume requires a native revision");
|
|
1892
|
+
}
|
|
1893
|
+
if (typeof resume.requireSnapshot !== "boolean")
|
|
1894
|
+
throw new TypeError("requireSnapshot must be boolean");
|
|
1895
|
+
if (resume.localCursor === null !== (resume.localStateHash === null)) {
|
|
1896
|
+
throw new TypeError("resume cursor and state hash must both be null or present");
|
|
1897
|
+
}
|
|
1898
|
+
if (resumeModality !== "spreadsheet" && "localNativeRevision" in resume && resume.localCursor === null !== (resume.localNativeRevision === null)) {
|
|
1899
|
+
throw new TypeError(
|
|
1900
|
+
"serialized resume cursor and native revision must both be null or present"
|
|
1901
|
+
);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
function validateBootstrap(bootstrap, artifactId, durableModality, resume, options) {
|
|
1905
|
+
validateHead(bootstrap, durableModality);
|
|
1906
|
+
assertNonnegativeSafeInteger(bootstrap.resumeSequence, "bootstrap resume sequence");
|
|
1907
|
+
editableArtifactStateHash(bootstrap.resumeStateHash);
|
|
1908
|
+
if (typeof bootstrap.resumeAccepted !== "boolean")
|
|
1909
|
+
throw new TypeError("resumeAccepted must be boolean");
|
|
1910
|
+
if (bootstrap.resumeSequence > bootstrap.headSequence)
|
|
1911
|
+
throw new TypeError("bootstrap resume is beyond head");
|
|
1912
|
+
if (bootstrap.resumeAccepted && (resume.requireSnapshot || resume.localCursor === null || resume.localStateHash === null || bootstrap.resumeSequence !== resume.localCursor || bootstrap.resumeStateHash !== resume.localStateHash)) {
|
|
1913
|
+
throw new TypeError("bootstrap accepted a different or unverified resume boundary");
|
|
1914
|
+
}
|
|
1915
|
+
if (bootstrap.snapshot) {
|
|
1916
|
+
const snapshot = bootstrap.snapshot;
|
|
1917
|
+
if (snapshot.modality !== durableModality) throw new TypeError("snapshot modality mismatch");
|
|
1918
|
+
if (snapshot.artifactId !== artifactId) throw new TypeError("snapshot artifact mismatch");
|
|
1919
|
+
assertNonnegativeSafeInteger(snapshot.sequence, "snapshot sequence");
|
|
1920
|
+
if (snapshot.sequence > bootstrap.headSequence) throw new TypeError("snapshot is beyond head");
|
|
1921
|
+
editableArtifactStateHash(snapshot.stateHash);
|
|
1922
|
+
if (snapshot.modality === "spreadsheet") {
|
|
1923
|
+
validateFrontier(snapshot.causalFrontier, "snapshot causal frontier");
|
|
1924
|
+
assertPositiveSafeInteger(
|
|
1925
|
+
snapshot.operationProtocolVersion,
|
|
1926
|
+
"snapshot operation protocol version"
|
|
1927
|
+
);
|
|
1928
|
+
} else {
|
|
1929
|
+
assertNonnegativeSafeInteger(snapshot.nativeRevision, "snapshot native revision");
|
|
1930
|
+
}
|
|
1931
|
+
if (!/^sha256:[0-9a-f]{64}$/u.test(snapshot.digest))
|
|
1932
|
+
throw new TypeError("snapshot digest is malformed");
|
|
1933
|
+
assertPositiveSafeInteger(snapshot.modelSchemaVersion, "snapshot model schema version");
|
|
1934
|
+
boundedString(
|
|
1935
|
+
snapshot.kernelVersion,
|
|
1936
|
+
"snapshot kernel version",
|
|
1937
|
+
EDITABLE_ARTIFACT_KERNEL_VERSION_MAX_BYTES2
|
|
1938
|
+
);
|
|
1939
|
+
if (!(snapshot.bytes instanceof Uint8Array) || snapshot.bytes.byteLength > options.maxSnapshotBytes) {
|
|
1940
|
+
throw new EditableArtifactLiveError("oversized_frame", "Snapshot bytes exceed live limit", {
|
|
1941
|
+
requiresSnapshot: true
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
function validateHead(head, durableModality) {
|
|
1947
|
+
if (head.modality !== durableModality) throw new TypeError("durable live head modality mismatch");
|
|
1948
|
+
assertNonnegativeSafeInteger(head.headSequence, "head sequence");
|
|
1949
|
+
assertNonnegativeSafeInteger(head.minimumReplaySequence, "minimum replay sequence");
|
|
1950
|
+
if (head.minimumReplaySequence > head.headSequence + 1)
|
|
1951
|
+
throw new TypeError("minimum replay sequence is beyond head");
|
|
1952
|
+
editableArtifactStateHash(head.stateHash);
|
|
1953
|
+
if (head.modality === "spreadsheet") {
|
|
1954
|
+
validateFrontier(head.causalFrontier, "head causal frontier");
|
|
1955
|
+
} else {
|
|
1956
|
+
assertNonnegativeSafeInteger(head.nativeRevision, "head native revision");
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
function validatePage(page, after, through, maxCount, maxBytes) {
|
|
1960
|
+
if (!Array.isArray(page.transactions)) throw new TypeError("transaction page is malformed");
|
|
1961
|
+
assertNonnegativeSafeInteger(page.headSequence, "page head sequence");
|
|
1962
|
+
assertNonnegativeSafeInteger(page.minimumReplaySequence, "page minimum replay sequence");
|
|
1963
|
+
if (page.headSequence < through || after > through)
|
|
1964
|
+
throw new TypeError("transaction page does not cover requested target");
|
|
1965
|
+
if (page.transactions.length > maxCount) {
|
|
1966
|
+
throw new EditableArtifactLiveError(
|
|
1967
|
+
"oversized_frame",
|
|
1968
|
+
"Durable transaction page exceeds the count limit",
|
|
1969
|
+
{ requiresSnapshot: true }
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
let bytes = 0;
|
|
1973
|
+
for (const transaction of page.transactions) {
|
|
1974
|
+
bytes += transaction.committedTransactionBytes.byteLength;
|
|
1975
|
+
if (!Number.isSafeInteger(bytes) || bytes > maxBytes) {
|
|
1976
|
+
throw new EditableArtifactLiveError(
|
|
1977
|
+
"oversized_frame",
|
|
1978
|
+
"Durable transaction page exceeds the byte limit",
|
|
1979
|
+
{ requiresSnapshot: true }
|
|
1980
|
+
);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
function validateTransaction(transaction, artifactId, durableModality, maxBytes) {
|
|
1985
|
+
if (transaction.artifactId !== artifactId) throw new TypeError("transaction artifact mismatch");
|
|
1986
|
+
if (transaction.modality !== durableModality)
|
|
1987
|
+
throw new TypeError("transaction modality differs from the durable artifact");
|
|
1988
|
+
editableArtifactTransactionId(transaction.transactionId);
|
|
1989
|
+
assertPositiveSafeInteger(transaction.startSequence, "transaction start sequence");
|
|
1990
|
+
assertPositiveSafeInteger(transaction.endSequence, "transaction end sequence");
|
|
1991
|
+
if (transaction.endSequence < transaction.startSequence)
|
|
1992
|
+
throw new TypeError("transaction sequence interval is inverted");
|
|
1993
|
+
editableArtifactRequestHash(transaction.requestHash);
|
|
1994
|
+
editableArtifactStateHash(transaction.priorStateHash);
|
|
1995
|
+
editableArtifactStateHash(transaction.stateHash);
|
|
1996
|
+
if (transaction.modality === "spreadsheet") {
|
|
1997
|
+
assertPositiveSafeInteger(
|
|
1998
|
+
transaction.operationProtocolVersion,
|
|
1999
|
+
"transaction operation protocol version"
|
|
2000
|
+
);
|
|
2001
|
+
validateFrontier(transaction.causalFrontier, "transaction causal frontier");
|
|
2002
|
+
} else {
|
|
2003
|
+
assertPositiveSafeInteger(
|
|
2004
|
+
transaction.commitProtocolVersion,
|
|
2005
|
+
"transaction commit protocol version"
|
|
2006
|
+
);
|
|
2007
|
+
assertNonnegativeSafeInteger(
|
|
2008
|
+
transaction.priorNativeRevision,
|
|
2009
|
+
"transaction prior native revision"
|
|
2010
|
+
);
|
|
2011
|
+
assertNonnegativeSafeInteger(transaction.nativeRevision, "transaction native revision");
|
|
2012
|
+
}
|
|
2013
|
+
if (!(transaction.committedTransactionBytes instanceof Uint8Array) || transaction.committedTransactionBytes.byteLength === 0 || transaction.committedTransactionBytes.byteLength > maxBytes) {
|
|
2014
|
+
throw new EditableArtifactLiveError(
|
|
2015
|
+
"oversized_frame",
|
|
2016
|
+
"Canonical transaction bytes exceed live limit",
|
|
2017
|
+
{ requiresSnapshot: true }
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
try {
|
|
2021
|
+
if (transaction.modality === "spreadsheet") {
|
|
2022
|
+
const summary = decodeCommittedTransactionSummary(transaction.committedTransactionBytes);
|
|
2023
|
+
if (summary.transactionId !== transaction.transactionId || summary.priorStateHash !== transaction.priorStateHash || summary.stateHash !== transaction.stateHash || summary.operationProtocolVersion !== transaction.operationProtocolVersion || !sameFrontier(summary.resultingCausalFrontier, transaction.causalFrontier)) {
|
|
2024
|
+
throw new TypeError("spreadsheet transaction projection mismatch");
|
|
2025
|
+
}
|
|
2026
|
+
} else {
|
|
2027
|
+
const summary = decodeEditableArtifactSerializedCommit(
|
|
2028
|
+
transaction.committedTransactionBytes,
|
|
2029
|
+
transaction.modality
|
|
2030
|
+
);
|
|
2031
|
+
if (summary.transactionId !== transaction.transactionId || summary.requestHash !== transaction.requestHash || summary.parentHeadSequence !== transaction.startSequence - 1 || summary.resultHeadSequence !== transaction.endSequence || summary.priorStateHash !== transaction.priorStateHash || summary.stateHash !== transaction.stateHash || summary.priorNativeRevision !== transaction.priorNativeRevision || summary.nativeReceipt.revision !== transaction.nativeRevision || summary.commitProtocolVersion !== transaction.commitProtocolVersion) {
|
|
2032
|
+
throw new TypeError("serialized transaction projection mismatch");
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
} catch (cause) {
|
|
2036
|
+
throw new EditableArtifactLiveError(
|
|
2037
|
+
"durable_gap",
|
|
2038
|
+
"Canonical transaction bytes disagree with durable live metadata",
|
|
2039
|
+
{ requiresSnapshot: true, cause }
|
|
2040
|
+
);
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
function estimateFrameBytes(frame) {
|
|
2044
|
+
if (frame.type === "snapshot") {
|
|
2045
|
+
const { bytes, ...metadata } = frame;
|
|
2046
|
+
return textEncoder2.encode(JSON.stringify(metadata)).byteLength + bytes.byteLength + 128;
|
|
2047
|
+
}
|
|
2048
|
+
if (frame.type === "transaction") {
|
|
2049
|
+
const { committedTransactionBytes, ...transaction } = frame.transaction;
|
|
2050
|
+
return textEncoder2.encode(JSON.stringify({ ...frame, transaction })).byteLength + committedTransactionBytes.byteLength + 128;
|
|
2051
|
+
}
|
|
2052
|
+
return textEncoder2.encode(JSON.stringify(frame)).byteLength;
|
|
2053
|
+
}
|
|
2054
|
+
function validateFrontier(frontier, label) {
|
|
2055
|
+
if (!Array.isArray(frontier) || frontier.length > MAX_CAUSAL_ACTORS) {
|
|
2056
|
+
throw new EditableArtifactLiveError(
|
|
2057
|
+
"oversized_frame",
|
|
2058
|
+
`${label} exceeds ${MAX_CAUSAL_ACTORS} actors`,
|
|
2059
|
+
{ requiresSnapshot: true }
|
|
2060
|
+
);
|
|
2061
|
+
}
|
|
2062
|
+
const normalized = editableArtifactCausalFrontier(frontier);
|
|
2063
|
+
if (normalized.length > MAX_CAUSAL_ACTORS) {
|
|
2064
|
+
throw new EditableArtifactLiveError(
|
|
2065
|
+
"oversized_frame",
|
|
2066
|
+
`${label} exceeds ${MAX_CAUSAL_ACTORS} actors`,
|
|
2067
|
+
{ requiresSnapshot: true }
|
|
2068
|
+
);
|
|
2069
|
+
}
|
|
2070
|
+
return normalized;
|
|
2071
|
+
}
|
|
2072
|
+
function sameFrontier(left, right) {
|
|
2073
|
+
return left.length === right.length && left.every(
|
|
2074
|
+
(entry, index) => entry.replicaId === right[index]?.replicaId && entry.counter === right[index]?.counter
|
|
2075
|
+
);
|
|
2076
|
+
}
|
|
2077
|
+
function ownTransaction(transaction) {
|
|
2078
|
+
return transaction.modality === "spreadsheet" ? Object.freeze({
|
|
2079
|
+
...transaction,
|
|
2080
|
+
causalFrontier: validateFrontier(transaction.causalFrontier, "transaction causal frontier"),
|
|
2081
|
+
committedTransactionBytes: transaction.committedTransactionBytes.slice()
|
|
2082
|
+
}) : Object.freeze({
|
|
2083
|
+
...transaction,
|
|
2084
|
+
committedTransactionBytes: transaction.committedTransactionBytes.slice()
|
|
2085
|
+
});
|
|
2086
|
+
}
|
|
2087
|
+
function plainRecord(value, label) {
|
|
2088
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2089
|
+
throw new EditableArtifactLiveError("invalid_frame", `${label} must be an object`);
|
|
2090
|
+
}
|
|
2091
|
+
const prototype = Object.getPrototypeOf(value);
|
|
2092
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
2093
|
+
throw new EditableArtifactLiveError("invalid_frame", `${label} must be a plain object`);
|
|
2094
|
+
}
|
|
2095
|
+
return value;
|
|
2096
|
+
}
|
|
2097
|
+
function requiredString(record, key, maxBytes) {
|
|
2098
|
+
const value = record[key];
|
|
2099
|
+
if (typeof value !== "string")
|
|
2100
|
+
throw new EditableArtifactLiveError("invalid_frame", `${key} must be a string`);
|
|
2101
|
+
boundedString(value, key, maxBytes);
|
|
2102
|
+
return value;
|
|
2103
|
+
}
|
|
2104
|
+
function requiredSafeInteger(record, key) {
|
|
2105
|
+
const value = record[key];
|
|
2106
|
+
if (!Number.isSafeInteger(value))
|
|
2107
|
+
throw new EditableArtifactLiveError("invalid_frame", `${key} must be a safe integer`);
|
|
2108
|
+
return value;
|
|
2109
|
+
}
|
|
2110
|
+
function rejectUnknown(record, allowed) {
|
|
2111
|
+
const allowedSet = new Set(allowed);
|
|
2112
|
+
const unknown = Object.keys(record).find((key) => !allowedSet.has(key));
|
|
2113
|
+
if (unknown)
|
|
2114
|
+
throw new EditableArtifactLiveError(
|
|
2115
|
+
"invalid_frame",
|
|
2116
|
+
`Unknown client frame property: ${unknown}`
|
|
2117
|
+
);
|
|
2118
|
+
}
|
|
2119
|
+
function boundedString(value, label, maxBytes) {
|
|
2120
|
+
const bytes = textEncoder2.encode(value).byteLength;
|
|
2121
|
+
if (bytes < 1 || bytes > maxBytes || value.trim() !== value)
|
|
2122
|
+
throw new TypeError(`${label} is malformed`);
|
|
2123
|
+
}
|
|
2124
|
+
function normalizeOptions2(options) {
|
|
2125
|
+
const normalized = {
|
|
2126
|
+
protocolVersion: options.protocolVersion ?? EDITABLE_ARTIFACT_LIVE_PROTOCOL_VERSION,
|
|
2127
|
+
maxClientFrameBytes: options.maxClientFrameBytes ?? DEFAULT_MAX_CLIENT_FRAME_BYTES,
|
|
2128
|
+
maxOutboundFrameBytes: options.maxOutboundFrameBytes ?? DEFAULT_MAX_OUTBOUND_FRAME_BYTES,
|
|
2129
|
+
maxSnapshotBytes: options.maxSnapshotBytes ?? DEFAULT_MAX_SNAPSHOT_BYTES,
|
|
2130
|
+
snapshotChunkBytes: options.snapshotChunkBytes ?? DEFAULT_SNAPSHOT_CHUNK_BYTES,
|
|
2131
|
+
maxInFlightTransactions: options.maxInFlightTransactions ?? DEFAULT_MAX_IN_FLIGHT_TRANSACTIONS,
|
|
2132
|
+
maxInFlightBytes: options.maxInFlightBytes ?? DEFAULT_MAX_IN_FLIGHT_BYTES,
|
|
2133
|
+
replayPageTransactions: options.replayPageTransactions ?? DEFAULT_REPLAY_PAGE_TRANSACTIONS,
|
|
2134
|
+
replayPageBytes: options.replayPageBytes ?? DEFAULT_REPLAY_PAGE_BYTES,
|
|
2135
|
+
maxSocketBufferedBytes: options.maxSocketBufferedBytes ?? DEFAULT_MAX_SOCKET_BUFFERED_BYTES,
|
|
2136
|
+
maxInboundQueueFrames: options.maxInboundQueueFrames ?? DEFAULT_MAX_INBOUND_QUEUE_FRAMES,
|
|
2137
|
+
maxInboundQueueBytes: options.maxInboundQueueBytes ?? DEFAULT_MAX_INBOUND_QUEUE_BYTES,
|
|
2138
|
+
reconcileIntervalMs: options.reconcileIntervalMs ?? DEFAULT_RECONCILE_INTERVAL_MS,
|
|
2139
|
+
reauthorizeIntervalMs: options.reauthorizeIntervalMs ?? DEFAULT_REAUTHORIZE_INTERVAL_MS,
|
|
2140
|
+
ackTimeoutMs: options.ackTimeoutMs ?? DEFAULT_ACK_TIMEOUT_MS,
|
|
2141
|
+
sendTimeoutMs: options.sendTimeoutMs ?? DEFAULT_SEND_TIMEOUT_MS,
|
|
2142
|
+
...options.ticketTtlMs === void 0 ? {} : { ticketTtlMs: options.ticketTtlMs }
|
|
2143
|
+
};
|
|
2144
|
+
for (const [label, value] of Object.entries(normalized)) {
|
|
2145
|
+
if (label === "ticketTtlMs") continue;
|
|
2146
|
+
assertPositiveSafeInteger(value, label);
|
|
2147
|
+
}
|
|
2148
|
+
if (normalized.maxSnapshotBytes > EDITABLE_ARTIFACT_PRODUCT_MAX_SNAPSHOT_BYTES) {
|
|
2149
|
+
throw new TypeError("maxSnapshotBytes exceeds the browser-compatible product limit");
|
|
2150
|
+
}
|
|
2151
|
+
if (normalized.snapshotChunkBytes + 1024 > normalized.maxOutboundFrameBytes) {
|
|
2152
|
+
throw new TypeError("snapshot chunk exceeds maximum outbound frame");
|
|
2153
|
+
}
|
|
2154
|
+
if (normalized.replayPageBytes < normalized.maxOutboundFrameBytes) {
|
|
2155
|
+
throw new TypeError("replay page bytes must cover one maximum outbound frame");
|
|
2156
|
+
}
|
|
2157
|
+
return Object.freeze(normalized);
|
|
2158
|
+
}
|
|
2159
|
+
function closeValue(reason) {
|
|
2160
|
+
return Object.freeze({
|
|
2161
|
+
reason,
|
|
2162
|
+
retryable: ["ticket_expired", "permission_changed", "slow_client", "transport_error"].includes(
|
|
2163
|
+
reason
|
|
2164
|
+
),
|
|
2165
|
+
requiresSnapshot: [
|
|
2166
|
+
"invalid_ack",
|
|
2167
|
+
"retention_gap",
|
|
2168
|
+
"durable_gap",
|
|
2169
|
+
"slow_client",
|
|
2170
|
+
"oversized_frame"
|
|
2171
|
+
].includes(reason)
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
function safeRelease(release) {
|
|
2175
|
+
try {
|
|
2176
|
+
release?.();
|
|
2177
|
+
} catch {
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
var SystemEditableArtifactLiveClock = class {
|
|
2181
|
+
now() {
|
|
2182
|
+
return /* @__PURE__ */ new Date();
|
|
2183
|
+
}
|
|
2184
|
+
};
|
|
2185
|
+
var SystemEditableArtifactLiveScheduler = class {
|
|
2186
|
+
sleep(milliseconds, signal) {
|
|
2187
|
+
return new Promise((resolve, reject) => {
|
|
2188
|
+
if (signal.aborted) {
|
|
2189
|
+
reject(signal.reason ?? new Error("aborted"));
|
|
2190
|
+
return;
|
|
2191
|
+
}
|
|
2192
|
+
const timer = setTimeout(resolve, milliseconds);
|
|
2193
|
+
signal.addEventListener(
|
|
2194
|
+
"abort",
|
|
2195
|
+
() => {
|
|
2196
|
+
clearTimeout(timer);
|
|
2197
|
+
reject(signal.reason ?? new Error("aborted"));
|
|
2198
|
+
},
|
|
2199
|
+
{ once: true }
|
|
2200
|
+
);
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
};
|
|
2204
|
+
|
|
2205
|
+
// src/editable-artifact-live/wire.ts
|
|
2206
|
+
import {
|
|
2207
|
+
EDITABLE_ARTIFACT_LIVE_WIRE_VERSION,
|
|
2208
|
+
decodeEditableArtifactLiveClientWireFrame as decodeContractClientFrame,
|
|
2209
|
+
decodeEditableArtifactLiveServerWireFrame,
|
|
2210
|
+
encodeEditableArtifactLiveAppliedWireFrame as encodeContractAppliedFrame,
|
|
2211
|
+
encodeEditableArtifactLiveMutationWireFrame as encodeContractMutationFrame,
|
|
2212
|
+
encodeEditableArtifactLiveOpenWireFrame as encodeContractOpenFrame,
|
|
2213
|
+
encodeEditableArtifactLiveServerWireFrame as encodeContractServerFrame,
|
|
2214
|
+
inspectEditableArtifactLiveWireEnvelope
|
|
2215
|
+
} from "@opengeni/contracts/editable-artifact-live";
|
|
2216
|
+
function decodeEditableArtifactLiveClientWireFrame(bytes) {
|
|
2217
|
+
return decodeContractClientFrame(bytes);
|
|
2218
|
+
}
|
|
2219
|
+
function encodeEditableArtifactLiveOpenWireFrame(frame) {
|
|
2220
|
+
return encodeContractOpenFrame(frame);
|
|
2221
|
+
}
|
|
2222
|
+
function encodeEditableArtifactLiveAppliedWireFrame(frame) {
|
|
2223
|
+
return encodeContractAppliedFrame(frame);
|
|
2224
|
+
}
|
|
2225
|
+
function encodeEditableArtifactLiveMutationWireFrame(frame) {
|
|
2226
|
+
return encodeContractMutationFrame(frame);
|
|
2227
|
+
}
|
|
2228
|
+
function encodeEditableArtifactLiveServerWireFrame(frame) {
|
|
2229
|
+
if (frame.type === "transaction" && frame.transaction.modality === "spreadsheet") {
|
|
2230
|
+
const { operationProtocolVersion, modality: _modality, ...transaction } = frame.transaction;
|
|
2231
|
+
return encodeContractServerFrame({
|
|
2232
|
+
...frame,
|
|
2233
|
+
transaction: {
|
|
2234
|
+
...transaction,
|
|
2235
|
+
protocolVersion: operationProtocolVersion
|
|
2236
|
+
}
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
return encodeContractServerFrame(frame);
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
export {
|
|
2243
|
+
EditableArtifactCompatibilityError,
|
|
2244
|
+
EditableArtifactApplication,
|
|
2245
|
+
EditableArtifactLiveError,
|
|
2246
|
+
EDITABLE_ARTIFACT_HINT_MAX_BYTES,
|
|
2247
|
+
EDITABLE_ARTIFACT_HINT_SUBJECT_PREFIX,
|
|
2248
|
+
EditableArtifactLiveOutboxDispatcher,
|
|
2249
|
+
EditableArtifactHintEncodingError,
|
|
2250
|
+
encodeEditableArtifactBrokerHint,
|
|
2251
|
+
editableArtifactLiveHintSubject,
|
|
2252
|
+
decodeEditableArtifactBrokerHint,
|
|
2253
|
+
MathEditableArtifactOutboxRandom,
|
|
2254
|
+
EDITABLE_ARTIFACT_LIVE_PROTOCOL_VERSION,
|
|
2255
|
+
EditableArtifactLiveTicketAuthority,
|
|
2256
|
+
InMemoryEditableArtifactLiveTicketStore,
|
|
2257
|
+
WebCryptoEditableArtifactLiveTokens,
|
|
2258
|
+
EditableArtifactLiveServer,
|
|
2259
|
+
SystemEditableArtifactLiveClock,
|
|
2260
|
+
SystemEditableArtifactLiveScheduler,
|
|
2261
|
+
EDITABLE_ARTIFACT_LIVE_WIRE_VERSION,
|
|
2262
|
+
decodeEditableArtifactLiveServerWireFrame,
|
|
2263
|
+
inspectEditableArtifactLiveWireEnvelope,
|
|
2264
|
+
decodeEditableArtifactLiveClientWireFrame,
|
|
2265
|
+
encodeEditableArtifactLiveOpenWireFrame,
|
|
2266
|
+
encodeEditableArtifactLiveAppliedWireFrame,
|
|
2267
|
+
encodeEditableArtifactLiveMutationWireFrame,
|
|
2268
|
+
encodeEditableArtifactLiveServerWireFrame
|
|
2269
|
+
};
|
|
2270
|
+
//# sourceMappingURL=chunk-NYPMQ7NO.js.map
|