@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,2402 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EditableArtifactCausalChainError,
|
|
3
|
+
EditableArtifactCausalFutureError,
|
|
4
|
+
EditableArtifactForbiddenError,
|
|
5
|
+
EditableArtifactIdempotencyConflictError,
|
|
6
|
+
EditableArtifactInvalidRequestError,
|
|
7
|
+
EditableArtifactKernelContractError,
|
|
8
|
+
EditableArtifactNotEditableError,
|
|
9
|
+
EditableArtifactNotFoundError,
|
|
10
|
+
EditableArtifactRetryableConflictError,
|
|
11
|
+
EditableArtifactSnapshotConflictError,
|
|
12
|
+
EditableArtifactStaleBaseError,
|
|
13
|
+
EditableArtifactUndoTargetError,
|
|
14
|
+
} from "./errors";
|
|
15
|
+
import type {
|
|
16
|
+
AuthoritativeEditableArtifactKernelPort,
|
|
17
|
+
EditableArtifactAuthorizationPort,
|
|
18
|
+
EditableArtifactClockPort,
|
|
19
|
+
EditableArtifactCompactionPort,
|
|
20
|
+
EditableArtifactGenesisPort,
|
|
21
|
+
EditableArtifactKernelState,
|
|
22
|
+
EditableArtifactMutationIntentCodecPort,
|
|
23
|
+
EditableArtifactSnapshotVerifierPort,
|
|
24
|
+
EditableArtifactStableIdFactoryPort,
|
|
25
|
+
EditableArtifactStorePort,
|
|
26
|
+
} from "./ports";
|
|
27
|
+
import { hashEditableArtifactCreateRequest, hashEditableArtifactImportRequest } from "./hash";
|
|
28
|
+
import {
|
|
29
|
+
EDITABLE_ARTIFACT_COMMAND_MAX_BYTES,
|
|
30
|
+
EDITABLE_ARTIFACT_INTENT_MAX_BYTES,
|
|
31
|
+
EDITABLE_ARTIFACT_INTENT_MAX_UNDO_TARGETS,
|
|
32
|
+
EDITABLE_ARTIFACT_INTENT_VERSION,
|
|
33
|
+
EDITABLE_ARTIFACT_KERNEL_VERSION_MAX_BYTES,
|
|
34
|
+
} from "@opengeni/contracts/editable-artifacts";
|
|
35
|
+
import {
|
|
36
|
+
COMMITTED_TRANSACTION_PROTOCOL_VERSION,
|
|
37
|
+
MAX_COMMITTED_TRANSACTION_BYTES,
|
|
38
|
+
MAX_COMMITTED_TRANSACTION_OPERATIONS,
|
|
39
|
+
decodeCommittedTransactionSummary,
|
|
40
|
+
} from "@opengeni/contracts/editable-artifact-committed-transaction";
|
|
41
|
+
import {
|
|
42
|
+
EDITABLE_ARTIFACT_SERIALIZED_COMMIT_VERSION,
|
|
43
|
+
decodeEditableArtifactSerializedCommit,
|
|
44
|
+
encodeEditableArtifactSerializedCommit,
|
|
45
|
+
} from "@opengeni/contracts/editable-artifact-serialized-commit";
|
|
46
|
+
import { editableArtifactCodecFor } from "@opengeni/contracts/editable-artifact-codec-registry";
|
|
47
|
+
import { EditableArtifactSnapshotVerificationError } from "./snapshot-verifier";
|
|
48
|
+
import {
|
|
49
|
+
assertBoundedArtifactTitle,
|
|
50
|
+
assertBoundedKernelVersion,
|
|
51
|
+
assertBoundedOpaqueReference,
|
|
52
|
+
assertIsoTimestamp,
|
|
53
|
+
assertNonnegativeSafeInteger,
|
|
54
|
+
assertPositiveSafeInteger,
|
|
55
|
+
causalCounter,
|
|
56
|
+
causalFrontierDominates,
|
|
57
|
+
causalFrontiersEqual,
|
|
58
|
+
editableArtifactActorKey,
|
|
59
|
+
editableArtifactCausalFrontier,
|
|
60
|
+
editableArtifactClientTransactionId,
|
|
61
|
+
compareCodeUnits,
|
|
62
|
+
editableArtifactContentHash,
|
|
63
|
+
editableArtifactId,
|
|
64
|
+
editableArtifactOperationId,
|
|
65
|
+
editableArtifactOutboxId,
|
|
66
|
+
editableArtifactReceiptId,
|
|
67
|
+
editableArtifactReplicaId,
|
|
68
|
+
editableArtifactRequestHash,
|
|
69
|
+
editableArtifactSnapshotId,
|
|
70
|
+
editableArtifactScope,
|
|
71
|
+
editableArtifactStateHash,
|
|
72
|
+
editableArtifactTransactionId,
|
|
73
|
+
EDITABLE_ARTIFACT_ORIGINAL_IMPORT_MAX_BYTES,
|
|
74
|
+
mergeCausalFrontiers,
|
|
75
|
+
type ApplyEditableArtifactTransactionRequest,
|
|
76
|
+
type ApplyEditableArtifactTransactionResult,
|
|
77
|
+
type CreateEditableArtifactRequest,
|
|
78
|
+
type CreateEditableArtifactResult,
|
|
79
|
+
type EditableArtifactOriginalImport,
|
|
80
|
+
type ImportEditableArtifactRequest,
|
|
81
|
+
type EditableArtifact,
|
|
82
|
+
type EditableArtifactActor,
|
|
83
|
+
type EditableArtifactCausalFrontier,
|
|
84
|
+
type EditableArtifactClientTransactionId,
|
|
85
|
+
type EditableArtifactKernelOperation,
|
|
86
|
+
type EditableArtifactLiveOutboxRecord,
|
|
87
|
+
type EditableArtifactMutationIntent,
|
|
88
|
+
type EditableArtifactOperationRecord,
|
|
89
|
+
type EditableArtifactPermission,
|
|
90
|
+
type EditableArtifactReceipt,
|
|
91
|
+
type EditableArtifactScope,
|
|
92
|
+
type EditableArtifactSnapshotMetadata,
|
|
93
|
+
type EditableArtifactSerialized,
|
|
94
|
+
type EditableArtifactSerializedCommittedTransactionRecord,
|
|
95
|
+
type EditableArtifactSerializedReceipt,
|
|
96
|
+
type EditableArtifactSpreadsheet,
|
|
97
|
+
type EditableArtifactSpreadsheetCommittedTransactionRecord,
|
|
98
|
+
type EditableArtifactSpreadsheetReceipt,
|
|
99
|
+
type PublishEditableArtifactSnapshotRequest,
|
|
100
|
+
type ValidatedEditableArtifactMutationIntent,
|
|
101
|
+
validateEditableArtifactActor,
|
|
102
|
+
} from "./types";
|
|
103
|
+
|
|
104
|
+
export const EDITABLE_ARTIFACT_MAX_OPERATIONS_PER_TRANSACTION =
|
|
105
|
+
MAX_COMMITTED_TRANSACTION_OPERATIONS;
|
|
106
|
+
export const EDITABLE_ARTIFACT_MAX_UNDO_TARGETS_PER_TRANSACTION = Math.min(
|
|
107
|
+
EDITABLE_ARTIFACT_INTENT_MAX_UNDO_TARGETS,
|
|
108
|
+
MAX_COMMITTED_TRANSACTION_OPERATIONS,
|
|
109
|
+
);
|
|
110
|
+
export const EDITABLE_ARTIFACT_MAX_COMMAND_BYTES_PER_TRANSACTION =
|
|
111
|
+
EDITABLE_ARTIFACT_COMMAND_MAX_BYTES;
|
|
112
|
+
export const EDITABLE_ARTIFACT_MAX_INTENT_BYTES_PER_TRANSACTION =
|
|
113
|
+
EDITABLE_ARTIFACT_INTENT_MAX_BYTES;
|
|
114
|
+
export const EDITABLE_ARTIFACT_INTENT_PROTOCOL_VERSION = 1;
|
|
115
|
+
export const EDITABLE_ARTIFACT_MAX_OPERATION_BYTES_PER_TRANSACTION =
|
|
116
|
+
MAX_COMMITTED_TRANSACTION_BYTES;
|
|
117
|
+
/** Canonical OGACO envelope bound. Kept as an alias for existing consumers. */
|
|
118
|
+
export const EDITABLE_ARTIFACT_MAX_COMMITTED_TRANSACTION_BYTES =
|
|
119
|
+
EDITABLE_ARTIFACT_MAX_OPERATION_BYTES_PER_TRANSACTION;
|
|
120
|
+
export const EDITABLE_ARTIFACT_OPERATION_PROTOCOL_VERSION = COMMITTED_TRANSACTION_PROTOCOL_VERSION;
|
|
121
|
+
export const EDITABLE_ARTIFACT_MAX_COMMIT_ATTEMPTS = 4;
|
|
122
|
+
/** Compact well before hard replay ceilings so one maximum transaction cannot cross them. */
|
|
123
|
+
export const EDITABLE_ARTIFACT_COMPACTION_TRANSACTION_THRESHOLD = 1_024;
|
|
124
|
+
export const EDITABLE_ARTIFACT_COMPACTION_BYTE_THRESHOLD = 16 * 1024 * 1024;
|
|
125
|
+
|
|
126
|
+
export type EditableArtifactServiceDependencies = Readonly<{
|
|
127
|
+
authorization: EditableArtifactAuthorizationPort;
|
|
128
|
+
kernel: AuthoritativeEditableArtifactKernelPort;
|
|
129
|
+
store: EditableArtifactStorePort;
|
|
130
|
+
intentCodec: EditableArtifactMutationIntentCodecPort;
|
|
131
|
+
clock: EditableArtifactClockPort;
|
|
132
|
+
ids: EditableArtifactStableIdFactoryPort;
|
|
133
|
+
snapshotVerifier: EditableArtifactSnapshotVerifierPort;
|
|
134
|
+
genesis: EditableArtifactGenesisPort;
|
|
135
|
+
compaction?: EditableArtifactCompactionPort;
|
|
136
|
+
}>;
|
|
137
|
+
|
|
138
|
+
export class EditableArtifactService {
|
|
139
|
+
private readonly inFlightCreations = new Map<
|
|
140
|
+
string,
|
|
141
|
+
Readonly<{
|
|
142
|
+
requestHash: EditableArtifactReceipt["requestHash"];
|
|
143
|
+
promise: Promise<CreateEditableArtifactResult>;
|
|
144
|
+
}>
|
|
145
|
+
>();
|
|
146
|
+
private readonly inFlightTransactions = new Map<
|
|
147
|
+
string,
|
|
148
|
+
Readonly<{
|
|
149
|
+
requestHash: EditableArtifactReceipt["requestHash"];
|
|
150
|
+
promise: Promise<ApplyEditableArtifactTransactionResult>;
|
|
151
|
+
}>
|
|
152
|
+
>();
|
|
153
|
+
private readonly inFlightImports = new Map<
|
|
154
|
+
string,
|
|
155
|
+
Readonly<{
|
|
156
|
+
requestHash: EditableArtifactReceipt["requestHash"];
|
|
157
|
+
promise: Promise<CreateEditableArtifactResult>;
|
|
158
|
+
}>
|
|
159
|
+
>();
|
|
160
|
+
private readonly inFlightCompactions = new Map<
|
|
161
|
+
string,
|
|
162
|
+
Promise<EditableArtifactSnapshotMetadata>
|
|
163
|
+
>();
|
|
164
|
+
|
|
165
|
+
constructor(private readonly dependencies: EditableArtifactServiceDependencies) {}
|
|
166
|
+
|
|
167
|
+
async createArtifact(input: {
|
|
168
|
+
scope: EditableArtifactScope;
|
|
169
|
+
actor: EditableArtifactActor;
|
|
170
|
+
request: CreateEditableArtifactRequest;
|
|
171
|
+
signal?: AbortSignal;
|
|
172
|
+
}): Promise<CreateEditableArtifactResult> {
|
|
173
|
+
const scope = editableArtifactScope(input.scope);
|
|
174
|
+
validateEditableArtifactActor(input.actor);
|
|
175
|
+
const actor: EditableArtifactActor = Object.freeze({ ...input.actor });
|
|
176
|
+
const request = normalizeCreateRequest(input.request);
|
|
177
|
+
const requestHash = hashEditableArtifactCreateRequest(request);
|
|
178
|
+
const authorityKey = editableArtifactActorKey(actor);
|
|
179
|
+
const inFlightKey = creationInFlightKey(scope, "create", authorityKey, request.idempotencyKey);
|
|
180
|
+
for (;;) {
|
|
181
|
+
const inFlight = this.inFlightCreations.get(inFlightKey);
|
|
182
|
+
if (inFlight) {
|
|
183
|
+
if (inFlight.requestHash === requestHash) {
|
|
184
|
+
const result = await inFlight.promise;
|
|
185
|
+
return copyCreateResult(result, true);
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
await inFlight.promise;
|
|
189
|
+
} catch {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
throw new EditableArtifactIdempotencyConflictError();
|
|
193
|
+
}
|
|
194
|
+
const promise = this.createArtifactOptimistically({
|
|
195
|
+
scope,
|
|
196
|
+
actor,
|
|
197
|
+
authorityKey,
|
|
198
|
+
request,
|
|
199
|
+
requestHash,
|
|
200
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
201
|
+
});
|
|
202
|
+
this.inFlightCreations.set(inFlightKey, { requestHash, promise });
|
|
203
|
+
try {
|
|
204
|
+
return await promise;
|
|
205
|
+
} finally {
|
|
206
|
+
if (this.inFlightCreations.get(inFlightKey)?.promise === promise) {
|
|
207
|
+
this.inFlightCreations.delete(inFlightKey);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private async createArtifactOptimistically(input: {
|
|
214
|
+
scope: EditableArtifactScope;
|
|
215
|
+
actor: EditableArtifactActor;
|
|
216
|
+
authorityKey: string;
|
|
217
|
+
request: CreateEditableArtifactRequest;
|
|
218
|
+
requestHash: EditableArtifactReceipt["requestHash"];
|
|
219
|
+
signal?: AbortSignal;
|
|
220
|
+
}): Promise<CreateEditableArtifactResult> {
|
|
221
|
+
const { scope, actor, authorityKey, request, requestHash } = input;
|
|
222
|
+
const artifactId = editableArtifactId(this.dependencies.ids.next("artifact"));
|
|
223
|
+
let authorizationRevision = await this.requirePermission({
|
|
224
|
+
scope,
|
|
225
|
+
artifactId,
|
|
226
|
+
actor,
|
|
227
|
+
permission: "create",
|
|
228
|
+
});
|
|
229
|
+
const existing = await this.dependencies.store.findArtifactCreation(
|
|
230
|
+
scope,
|
|
231
|
+
"create",
|
|
232
|
+
authorityKey,
|
|
233
|
+
request.idempotencyKey,
|
|
234
|
+
);
|
|
235
|
+
if (existing) {
|
|
236
|
+
if (existing.creationReceipt.requestHash !== requestHash) {
|
|
237
|
+
throw new EditableArtifactIdempotencyConflictError();
|
|
238
|
+
}
|
|
239
|
+
return copyCreateResult(existing, true);
|
|
240
|
+
}
|
|
241
|
+
const snapshotId = editableArtifactSnapshotId(this.dependencies.ids.next("snapshot"));
|
|
242
|
+
const candidate = validateSnapshotRequest(
|
|
243
|
+
await this.dependencies.genesis.prepare({
|
|
244
|
+
scope,
|
|
245
|
+
artifactId,
|
|
246
|
+
snapshotId,
|
|
247
|
+
modality: request.modality,
|
|
248
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
249
|
+
}),
|
|
250
|
+
);
|
|
251
|
+
if (
|
|
252
|
+
candidate.snapshotId !== snapshotId ||
|
|
253
|
+
candidate.modality !== request.modality ||
|
|
254
|
+
candidate.coveredHeadSequence !== 0 ||
|
|
255
|
+
(candidate.modality === "spreadsheet" && candidate.coveredCausalFrontier.length !== 0)
|
|
256
|
+
) {
|
|
257
|
+
throw new EditableArtifactKernelContractError(
|
|
258
|
+
"Genesis snapshot must use its assigned identity and empty modality boundary",
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
await this.dependencies.snapshotVerifier.verify({
|
|
262
|
+
scope,
|
|
263
|
+
artifactId,
|
|
264
|
+
actor,
|
|
265
|
+
snapshot: candidate,
|
|
266
|
+
});
|
|
267
|
+
const publishedAt = canonicalNow(this.dependencies.clock);
|
|
268
|
+
if (Date.parse(candidate.verifiedAt) > Date.parse(publishedAt)) {
|
|
269
|
+
throw new EditableArtifactSnapshotConflictError(
|
|
270
|
+
"Snapshot verification time cannot be after publication",
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
const genesisSnapshot: EditableArtifactSnapshotMetadata = Object.freeze({
|
|
274
|
+
...candidate,
|
|
275
|
+
scope,
|
|
276
|
+
artifactId,
|
|
277
|
+
publishedAt,
|
|
278
|
+
});
|
|
279
|
+
const receiptId = editableArtifactReceiptId(this.dependencies.ids.next("receipt"));
|
|
280
|
+
const outbox = snapshotOutboxRecord(
|
|
281
|
+
editableArtifactOutboxId(this.dependencies.ids.next("outbox")),
|
|
282
|
+
genesisSnapshot,
|
|
283
|
+
);
|
|
284
|
+
for (let attempt = 1; attempt <= EDITABLE_ARTIFACT_MAX_COMMIT_ATTEMPTS; attempt += 1) {
|
|
285
|
+
const outcome = await this.dependencies.store.createArtifact({
|
|
286
|
+
scope,
|
|
287
|
+
artifactId,
|
|
288
|
+
authorizationActor: actor,
|
|
289
|
+
receiptId,
|
|
290
|
+
authorityKey,
|
|
291
|
+
idempotencyKey: request.idempotencyKey,
|
|
292
|
+
requestHash,
|
|
293
|
+
operationKind: "create",
|
|
294
|
+
expectedScopeAuthorizationRevision: authorizationRevision,
|
|
295
|
+
initialArtifactAuthorizationRevision: authorizationRevision,
|
|
296
|
+
modality: request.modality,
|
|
297
|
+
title: request.title,
|
|
298
|
+
createdBySubjectId: actor.subjectId,
|
|
299
|
+
genesisSnapshot,
|
|
300
|
+
outbox,
|
|
301
|
+
});
|
|
302
|
+
if (outcome.kind === "result") return outcome.value;
|
|
303
|
+
authorizationRevision = await this.requirePermission({
|
|
304
|
+
scope,
|
|
305
|
+
artifactId,
|
|
306
|
+
actor,
|
|
307
|
+
permission: "create",
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
throw new EditableArtifactRetryableConflictError();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Publishes one verified Office import as the sequence-zero durable state.
|
|
315
|
+
* The caller may name immutable source/snapshot objects, but neither becomes
|
|
316
|
+
* authority until the native snapshot verifier and atomic store commit pass.
|
|
317
|
+
*/
|
|
318
|
+
async importArtifact(input: {
|
|
319
|
+
scope: EditableArtifactScope;
|
|
320
|
+
actor: EditableArtifactActor;
|
|
321
|
+
request: ImportEditableArtifactRequest;
|
|
322
|
+
signal?: AbortSignal;
|
|
323
|
+
}): Promise<CreateEditableArtifactResult> {
|
|
324
|
+
const scope = editableArtifactScope(input.scope);
|
|
325
|
+
validateEditableArtifactActor(input.actor);
|
|
326
|
+
const actor: EditableArtifactActor = Object.freeze({ ...input.actor });
|
|
327
|
+
const request = normalizeImportRequest(input.request);
|
|
328
|
+
const requestHash = hashEditableArtifactImportRequest(request);
|
|
329
|
+
const authorityKey = editableArtifactActorKey(actor);
|
|
330
|
+
const inFlightKey = creationInFlightKey(scope, "import", authorityKey, request.idempotencyKey);
|
|
331
|
+
for (;;) {
|
|
332
|
+
const inFlight = this.inFlightImports.get(inFlightKey);
|
|
333
|
+
if (inFlight) {
|
|
334
|
+
if (inFlight.requestHash === requestHash) {
|
|
335
|
+
return copyCreateResult(await inFlight.promise, true);
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
await inFlight.promise;
|
|
339
|
+
} catch {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
throw new EditableArtifactIdempotencyConflictError();
|
|
343
|
+
}
|
|
344
|
+
const promise = this.importArtifactOptimistically({
|
|
345
|
+
scope,
|
|
346
|
+
actor,
|
|
347
|
+
authorityKey,
|
|
348
|
+
request,
|
|
349
|
+
requestHash,
|
|
350
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
351
|
+
});
|
|
352
|
+
this.inFlightImports.set(inFlightKey, { requestHash, promise });
|
|
353
|
+
try {
|
|
354
|
+
return await promise;
|
|
355
|
+
} finally {
|
|
356
|
+
if (this.inFlightImports.get(inFlightKey)?.promise === promise) {
|
|
357
|
+
this.inFlightImports.delete(inFlightKey);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private async importArtifactOptimistically(input: {
|
|
364
|
+
scope: EditableArtifactScope;
|
|
365
|
+
actor: EditableArtifactActor;
|
|
366
|
+
authorityKey: string;
|
|
367
|
+
request: ImportEditableArtifactRequest;
|
|
368
|
+
requestHash: EditableArtifactReceipt["requestHash"];
|
|
369
|
+
signal?: AbortSignal;
|
|
370
|
+
}): Promise<CreateEditableArtifactResult> {
|
|
371
|
+
const { scope, actor, authorityKey, request, requestHash } = input;
|
|
372
|
+
const artifactId = editableArtifactId(this.dependencies.ids.next("artifact"));
|
|
373
|
+
let authorizationRevision = await this.requirePermission({
|
|
374
|
+
scope,
|
|
375
|
+
artifactId,
|
|
376
|
+
actor,
|
|
377
|
+
permission: "import",
|
|
378
|
+
});
|
|
379
|
+
const existing = await this.dependencies.store.findArtifactCreation(
|
|
380
|
+
scope,
|
|
381
|
+
"import",
|
|
382
|
+
authorityKey,
|
|
383
|
+
request.idempotencyKey,
|
|
384
|
+
);
|
|
385
|
+
if (existing) {
|
|
386
|
+
if (existing.creationReceipt.requestHash !== requestHash) {
|
|
387
|
+
throw new EditableArtifactIdempotencyConflictError();
|
|
388
|
+
}
|
|
389
|
+
return copyCreateResult(existing, true);
|
|
390
|
+
}
|
|
391
|
+
const snapshotId = editableArtifactSnapshotId(this.dependencies.ids.next("snapshot"));
|
|
392
|
+
const verifiedAt = canonicalNow(this.dependencies.clock);
|
|
393
|
+
const candidate = validateSnapshotRequest({
|
|
394
|
+
...request.snapshot,
|
|
395
|
+
snapshotId,
|
|
396
|
+
verifiedAt,
|
|
397
|
+
} as PublishEditableArtifactSnapshotRequest);
|
|
398
|
+
if (candidate.modality !== request.modality || candidate.coveredHeadSequence !== 0) {
|
|
399
|
+
throw new EditableArtifactKernelContractError(
|
|
400
|
+
"Imported snapshot must use its assigned modality and sequence-zero boundary",
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
await this.dependencies.snapshotVerifier.verify({
|
|
404
|
+
scope,
|
|
405
|
+
artifactId,
|
|
406
|
+
actor,
|
|
407
|
+
snapshot: candidate,
|
|
408
|
+
});
|
|
409
|
+
const publishedAt = canonicalNow(this.dependencies.clock);
|
|
410
|
+
if (Date.parse(candidate.verifiedAt) > Date.parse(publishedAt)) {
|
|
411
|
+
throw new EditableArtifactSnapshotConflictError(
|
|
412
|
+
"Snapshot verification time cannot be after publication",
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
const genesisSnapshot: EditableArtifactSnapshotMetadata = Object.freeze({
|
|
416
|
+
...candidate,
|
|
417
|
+
scope,
|
|
418
|
+
artifactId,
|
|
419
|
+
publishedAt,
|
|
420
|
+
});
|
|
421
|
+
const receiptId = editableArtifactReceiptId(this.dependencies.ids.next("receipt"));
|
|
422
|
+
const originalImport = Object.freeze({
|
|
423
|
+
...request.originalImport,
|
|
424
|
+
blobRefId: this.dependencies.ids.next("blob"),
|
|
425
|
+
});
|
|
426
|
+
const outbox = snapshotOutboxRecord(
|
|
427
|
+
editableArtifactOutboxId(this.dependencies.ids.next("outbox")),
|
|
428
|
+
genesisSnapshot,
|
|
429
|
+
);
|
|
430
|
+
for (let attempt = 1; attempt <= EDITABLE_ARTIFACT_MAX_COMMIT_ATTEMPTS; attempt += 1) {
|
|
431
|
+
const outcome = await this.dependencies.store.createArtifact({
|
|
432
|
+
scope,
|
|
433
|
+
artifactId,
|
|
434
|
+
authorizationActor: actor,
|
|
435
|
+
receiptId,
|
|
436
|
+
authorityKey,
|
|
437
|
+
idempotencyKey: request.idempotencyKey,
|
|
438
|
+
requestHash,
|
|
439
|
+
operationKind: "import",
|
|
440
|
+
expectedScopeAuthorizationRevision: authorizationRevision,
|
|
441
|
+
initialArtifactAuthorizationRevision: authorizationRevision,
|
|
442
|
+
modality: request.modality,
|
|
443
|
+
title: request.title,
|
|
444
|
+
createdBySubjectId: actor.subjectId,
|
|
445
|
+
genesisSnapshot,
|
|
446
|
+
originalImport,
|
|
447
|
+
outbox,
|
|
448
|
+
});
|
|
449
|
+
if (outcome.kind === "result") return outcome.value;
|
|
450
|
+
authorizationRevision = await this.requirePermission({
|
|
451
|
+
scope,
|
|
452
|
+
artifactId,
|
|
453
|
+
actor,
|
|
454
|
+
permission: "import",
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
throw new EditableArtifactRetryableConflictError();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async getArtifact(input: {
|
|
461
|
+
scope: EditableArtifactScope;
|
|
462
|
+
artifactId: EditableArtifact["id"];
|
|
463
|
+
actor: EditableArtifactActor;
|
|
464
|
+
}): Promise<EditableArtifact> {
|
|
465
|
+
const context = normalizeBoundaryContext(input);
|
|
466
|
+
let authorizationRevision = await this.requirePermission({
|
|
467
|
+
...context,
|
|
468
|
+
permission: "read",
|
|
469
|
+
});
|
|
470
|
+
for (let attempt = 1; attempt <= EDITABLE_ARTIFACT_MAX_COMMIT_ATTEMPTS; attempt += 1) {
|
|
471
|
+
const outcome = await this.dependencies.store.readArtifactAtAuthorizationRevision(
|
|
472
|
+
context.scope,
|
|
473
|
+
context.artifactId,
|
|
474
|
+
authorizationRevision,
|
|
475
|
+
);
|
|
476
|
+
if (outcome.kind === "result") {
|
|
477
|
+
if (!outcome.artifact) throw new EditableArtifactNotFoundError();
|
|
478
|
+
return outcome.artifact;
|
|
479
|
+
}
|
|
480
|
+
authorizationRevision = await this.requirePermission({
|
|
481
|
+
...context,
|
|
482
|
+
permission: "read",
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
throw new EditableArtifactRetryableConflictError();
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Resolve one actor-scoped durable mutation identity after an idempotency
|
|
490
|
+
* collision. Normal edits do not pay this read; agent transports use it to
|
|
491
|
+
* recover a committed receipt after a lost response without process state.
|
|
492
|
+
*/
|
|
493
|
+
async findTransactionReceipt(input: {
|
|
494
|
+
scope: EditableArtifactScope;
|
|
495
|
+
artifactId: EditableArtifact["id"];
|
|
496
|
+
actor: EditableArtifactActor;
|
|
497
|
+
clientTransactionId: EditableArtifactClientTransactionId;
|
|
498
|
+
}): Promise<EditableArtifactReceipt | null> {
|
|
499
|
+
const context = normalizeBoundaryContext(input);
|
|
500
|
+
await this.requirePermission({ ...context, permission: "edit" });
|
|
501
|
+
const result = await this.dependencies.store.readTransactionBasis(
|
|
502
|
+
context.scope,
|
|
503
|
+
context.artifactId,
|
|
504
|
+
{
|
|
505
|
+
actorKey: editableArtifactActorKey(context.actor),
|
|
506
|
+
clientTransactionId: editableArtifactClientTransactionId(input.clientTransactionId),
|
|
507
|
+
previousLocalTransactionId: null,
|
|
508
|
+
selectiveUndoOperationIds: Object.freeze([]),
|
|
509
|
+
},
|
|
510
|
+
);
|
|
511
|
+
return result.kind === "existing" ? copyReceipt(result.receipt) : null;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Returns one detached, internally consistent reconstruction basis for an
|
|
516
|
+
* authorized current-head read. Inspection and compaction deliberately share
|
|
517
|
+
* this store primitive so neither can observe a metadata/snapshot/tail mix.
|
|
518
|
+
*/
|
|
519
|
+
async readCurrentKernelState(input: {
|
|
520
|
+
scope: EditableArtifactScope;
|
|
521
|
+
artifactId: EditableArtifact["id"];
|
|
522
|
+
actor: EditableArtifactActor;
|
|
523
|
+
}): Promise<EditableArtifactKernelState> {
|
|
524
|
+
const context = normalizeBoundaryContext(input);
|
|
525
|
+
let authorizationRevision = await this.requirePermission({
|
|
526
|
+
...context,
|
|
527
|
+
permission: "read",
|
|
528
|
+
});
|
|
529
|
+
for (let attempt = 1; attempt <= EDITABLE_ARTIFACT_MAX_COMMIT_ATTEMPTS; attempt += 1) {
|
|
530
|
+
const outcome = await this.dependencies.store.readCurrentKernelState(
|
|
531
|
+
context.scope,
|
|
532
|
+
context.artifactId,
|
|
533
|
+
authorizationRevision,
|
|
534
|
+
);
|
|
535
|
+
if (outcome.kind === "basis") return outcome.state;
|
|
536
|
+
authorizationRevision = await this.requirePermission({
|
|
537
|
+
...context,
|
|
538
|
+
permission: "read",
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
throw new EditableArtifactRetryableConflictError();
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
async applyTransaction(input: {
|
|
545
|
+
scope: EditableArtifactScope;
|
|
546
|
+
artifactId: EditableArtifact["id"];
|
|
547
|
+
actor: EditableArtifactActor;
|
|
548
|
+
request: ApplyEditableArtifactTransactionRequest;
|
|
549
|
+
}): Promise<ApplyEditableArtifactTransactionResult> {
|
|
550
|
+
const context = normalizeBoundaryContext(input);
|
|
551
|
+
const request = normalizeTransactionEnvelope(input.request);
|
|
552
|
+
const authorizationRevision = await this.requirePermission({
|
|
553
|
+
...context,
|
|
554
|
+
permission: "edit",
|
|
555
|
+
});
|
|
556
|
+
const intent = normalizeMutationIntent(
|
|
557
|
+
await this.dependencies.intentCodec.decodeAndVerify({
|
|
558
|
+
intentBytes: request.intentBytes.slice(),
|
|
559
|
+
requestHash: request.requestHash,
|
|
560
|
+
}),
|
|
561
|
+
);
|
|
562
|
+
if (intent.artifactId !== context.artifactId) {
|
|
563
|
+
throw new EditableArtifactInvalidRequestError("Mutation intent is bound to another artifact");
|
|
564
|
+
}
|
|
565
|
+
if (intent.replicaId !== context.actor.replicaId) {
|
|
566
|
+
throw new EditableArtifactInvalidRequestError(
|
|
567
|
+
"Mutation intent is bound to another actor replica",
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
const intentBytes = request.intentBytes.slice();
|
|
571
|
+
const actorKey = editableArtifactActorKey(context.actor);
|
|
572
|
+
const inFlightKey = transactionInFlightKey(
|
|
573
|
+
context.scope,
|
|
574
|
+
context.artifactId,
|
|
575
|
+
actorKey,
|
|
576
|
+
intent.clientTransactionId,
|
|
577
|
+
);
|
|
578
|
+
for (;;) {
|
|
579
|
+
const inFlight = this.inFlightTransactions.get(inFlightKey);
|
|
580
|
+
if (inFlight) {
|
|
581
|
+
if (inFlight.requestHash === request.requestHash) {
|
|
582
|
+
const result = await inFlight.promise;
|
|
583
|
+
return Object.freeze({
|
|
584
|
+
receipt: copyReceipt(result.receipt),
|
|
585
|
+
replayed: true,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
// Serialize a conflicting reuse behind the current candidate. If the
|
|
589
|
+
// first commits, the normal durable idempotency check rejects this hash;
|
|
590
|
+
// if it fails, the second remains free to become authoritative. Looping
|
|
591
|
+
// lets simultaneous conflicts coalesce behind their next exact hash.
|
|
592
|
+
try {
|
|
593
|
+
await inFlight.promise;
|
|
594
|
+
} catch {
|
|
595
|
+
// Deliberately continue; the conflicting request has its own outcome.
|
|
596
|
+
}
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const promise = this.applyTransactionOptimistically({
|
|
601
|
+
context,
|
|
602
|
+
actorKey,
|
|
603
|
+
request,
|
|
604
|
+
intent,
|
|
605
|
+
intentBytes,
|
|
606
|
+
authorizationRevision,
|
|
607
|
+
});
|
|
608
|
+
this.inFlightTransactions.set(inFlightKey, {
|
|
609
|
+
requestHash: request.requestHash,
|
|
610
|
+
promise,
|
|
611
|
+
});
|
|
612
|
+
try {
|
|
613
|
+
return await promise;
|
|
614
|
+
} finally {
|
|
615
|
+
if (this.inFlightTransactions.get(inFlightKey)?.promise === promise) {
|
|
616
|
+
this.inFlightTransactions.delete(inFlightKey);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
private async applyTransactionOptimistically(input: {
|
|
623
|
+
context: Readonly<{
|
|
624
|
+
scope: EditableArtifactScope;
|
|
625
|
+
artifactId: EditableArtifact["id"];
|
|
626
|
+
actor: EditableArtifactActor;
|
|
627
|
+
}>;
|
|
628
|
+
actorKey: string;
|
|
629
|
+
request: ApplyEditableArtifactTransactionRequest;
|
|
630
|
+
intent: ValidatedEditableArtifactMutationIntent;
|
|
631
|
+
intentBytes: Uint8Array;
|
|
632
|
+
authorizationRevision: number;
|
|
633
|
+
}): Promise<ApplyEditableArtifactTransactionResult> {
|
|
634
|
+
const { context, actorKey, request, intent, intentBytes } = input;
|
|
635
|
+
let authorizationRevision = input.authorizationRevision;
|
|
636
|
+
for (let attempt = 1; attempt <= EDITABLE_ARTIFACT_MAX_COMMIT_ATTEMPTS; attempt += 1) {
|
|
637
|
+
const basisRead = await this.dependencies.store.readTransactionBasis(
|
|
638
|
+
context.scope,
|
|
639
|
+
context.artifactId,
|
|
640
|
+
{
|
|
641
|
+
actorKey,
|
|
642
|
+
clientTransactionId: intent.clientTransactionId,
|
|
643
|
+
previousLocalTransactionId: intent.previousLocalTransactionId,
|
|
644
|
+
selectiveUndoOperationIds: intent.selectiveUndoOperationIds,
|
|
645
|
+
},
|
|
646
|
+
);
|
|
647
|
+
if (basisRead.kind === "existing") {
|
|
648
|
+
if (basisRead.receipt.requestHash !== request.requestHash) {
|
|
649
|
+
throw new EditableArtifactIdempotencyConflictError();
|
|
650
|
+
}
|
|
651
|
+
return Object.freeze({
|
|
652
|
+
receipt: copyReceipt(basisRead.receipt),
|
|
653
|
+
replayed: true,
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
const basis = basisRead;
|
|
657
|
+
|
|
658
|
+
const artifact = basis.artifact;
|
|
659
|
+
if (artifact.authorizationRevision !== authorizationRevision) {
|
|
660
|
+
authorizationRevision = await this.requirePermission({
|
|
661
|
+
...context,
|
|
662
|
+
permission: "edit",
|
|
663
|
+
});
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
if (
|
|
667
|
+
request.expectedHead &&
|
|
668
|
+
(artifact.headSequence !== request.expectedHead.sequence ||
|
|
669
|
+
artifact.stateHash !== request.expectedHead.stateHash)
|
|
670
|
+
) {
|
|
671
|
+
throw new EditableArtifactStaleBaseError();
|
|
672
|
+
}
|
|
673
|
+
if (artifact.lifecycle !== "active") {
|
|
674
|
+
throw new EditableArtifactNotEditableError(artifact.lifecycle);
|
|
675
|
+
}
|
|
676
|
+
if (this.dependencies.compaction && shouldCompactKernelState(basis.kernelState)) {
|
|
677
|
+
await this.compactCurrentHeadWithPermission(context, "edit");
|
|
678
|
+
// Snapshot publication changes replay coverage but not the head. Read a
|
|
679
|
+
// fresh detached basis so the kernel never replays the old long tail.
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
validateIntentCommands(artifact, intent);
|
|
683
|
+
|
|
684
|
+
if (artifact.modality !== "spreadsheet") {
|
|
685
|
+
if (intent.observedHeadSequence !== artifact.headSequence) {
|
|
686
|
+
throw new EditableArtifactStaleBaseError();
|
|
687
|
+
}
|
|
688
|
+
if (intent.causalBase.length !== 0 || intent.selectiveUndoOperationIds.length !== 0) {
|
|
689
|
+
throw new EditableArtifactInvalidRequestError(
|
|
690
|
+
"Serialized artifacts do not accept CRDT causality or selective undo",
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
validateSerializedPredecessor(
|
|
694
|
+
basis.predecessor,
|
|
695
|
+
context.actor.replicaId,
|
|
696
|
+
intent,
|
|
697
|
+
artifact.modality,
|
|
698
|
+
);
|
|
699
|
+
if (
|
|
700
|
+
basis.undoTargets.length !== 0 ||
|
|
701
|
+
basis.kernelState.modality !== artifact.modality ||
|
|
702
|
+
basis.kernelState.artifact.modality !== artifact.modality
|
|
703
|
+
) {
|
|
704
|
+
throw new Error("Editable artifact store returned a cross-modality serialized basis");
|
|
705
|
+
}
|
|
706
|
+
const priorNativeRevision = serializedBasisNativeRevision(basis.kernelState, artifact);
|
|
707
|
+
const kernelResult = await this.dependencies.kernel.applyTransaction({
|
|
708
|
+
modality: artifact.modality,
|
|
709
|
+
state: basis.kernelState,
|
|
710
|
+
actor: context.actor,
|
|
711
|
+
intent,
|
|
712
|
+
requestHash: request.requestHash,
|
|
713
|
+
intentBytes: intentBytes.slice(),
|
|
714
|
+
});
|
|
715
|
+
const serverTransactionId = editableArtifactTransactionId(
|
|
716
|
+
this.dependencies.ids.next("transaction"),
|
|
717
|
+
);
|
|
718
|
+
let normalizedKernel: ReturnType<typeof validateSerializedKernelResult>;
|
|
719
|
+
try {
|
|
720
|
+
normalizedKernel = validateSerializedKernelResult({
|
|
721
|
+
artifact,
|
|
722
|
+
intent,
|
|
723
|
+
intentBytes,
|
|
724
|
+
requestHash: request.requestHash,
|
|
725
|
+
serverTransactionId,
|
|
726
|
+
priorNativeRevision,
|
|
727
|
+
result: kernelResult,
|
|
728
|
+
});
|
|
729
|
+
} catch (error) {
|
|
730
|
+
if (error instanceof EditableArtifactKernelContractError) throw error;
|
|
731
|
+
throw new EditableArtifactKernelContractError(
|
|
732
|
+
`Authoritative kernel returned malformed serialized data: ${
|
|
733
|
+
error instanceof Error ? error.message : "unknown contract error"
|
|
734
|
+
}`,
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
const committedAt = canonicalNow(this.dependencies.clock);
|
|
738
|
+
const sequence = artifact.headSequence + 1;
|
|
739
|
+
assertPositiveSafeInteger(sequence, "serialized resulting head sequence");
|
|
740
|
+
const committedTransaction: EditableArtifactSerializedCommittedTransactionRecord =
|
|
741
|
+
Object.freeze({
|
|
742
|
+
scope: artifact.scope,
|
|
743
|
+
artifactId: artifact.id,
|
|
744
|
+
modality: artifact.modality,
|
|
745
|
+
serverTransactionId,
|
|
746
|
+
requestHash: request.requestHash,
|
|
747
|
+
sequenceStart: sequence,
|
|
748
|
+
sequenceEnd: sequence,
|
|
749
|
+
priorStateHash: artifact.stateHash,
|
|
750
|
+
stateHash: normalizedKernel.stateHash,
|
|
751
|
+
commitProtocolVersion: EDITABLE_ARTIFACT_SERIALIZED_COMMIT_VERSION,
|
|
752
|
+
priorNativeRevision,
|
|
753
|
+
nativeRevision: normalizedKernel.nativeRevision,
|
|
754
|
+
commandCount: normalizedKernel.commandCount,
|
|
755
|
+
nativeReceiptBytes: normalizedKernel.nativeReceiptBytes.slice(),
|
|
756
|
+
modelSchemaVersion: normalizedKernel.modelSchemaVersion,
|
|
757
|
+
kernelVersion: normalizedKernel.kernelVersion,
|
|
758
|
+
committedTransactionBytes: normalizedKernel.committedTransactionBytes.slice(),
|
|
759
|
+
committedAt,
|
|
760
|
+
});
|
|
761
|
+
const receipt: EditableArtifactSerializedReceipt = Object.freeze({
|
|
762
|
+
receiptId: editableArtifactReceiptId(this.dependencies.ids.next("receipt")),
|
|
763
|
+
scope: artifact.scope,
|
|
764
|
+
artifactId: artifact.id,
|
|
765
|
+
modality: artifact.modality,
|
|
766
|
+
serverTransactionId,
|
|
767
|
+
clientTransactionId: intent.clientTransactionId,
|
|
768
|
+
replicaId: context.actor.replicaId,
|
|
769
|
+
replicaCounter: intent.replicaCounter,
|
|
770
|
+
previousLocalTransactionId: intent.previousLocalTransactionId,
|
|
771
|
+
requestHash: request.requestHash,
|
|
772
|
+
intentBytes: intentBytes.slice(),
|
|
773
|
+
actorKey,
|
|
774
|
+
sequenceStart: sequence,
|
|
775
|
+
sequenceEnd: sequence,
|
|
776
|
+
priorStateHash: artifact.stateHash,
|
|
777
|
+
stateHash: normalizedKernel.stateHash,
|
|
778
|
+
intentEnvelopeVersion: intent.envelopeVersion,
|
|
779
|
+
intentProtocolVersion: intent.protocolVersion,
|
|
780
|
+
commandProtocolVersion: intent.commandProtocolVersion,
|
|
781
|
+
kernelVersion: normalizedKernel.kernelVersion,
|
|
782
|
+
modelSchemaVersion: normalizedKernel.modelSchemaVersion,
|
|
783
|
+
commitProtocolVersion: EDITABLE_ARTIFACT_SERIALIZED_COMMIT_VERSION,
|
|
784
|
+
priorNativeRevision,
|
|
785
|
+
nativeRevision: normalizedKernel.nativeRevision,
|
|
786
|
+
commandCount: normalizedKernel.commandCount,
|
|
787
|
+
committedAt,
|
|
788
|
+
});
|
|
789
|
+
const outbox = transactionOutboxRecord(
|
|
790
|
+
editableArtifactOutboxId(this.dependencies.ids.next("outbox")),
|
|
791
|
+
receipt,
|
|
792
|
+
);
|
|
793
|
+
const commit = await this.dependencies.store.tryCommitAppliedTransaction({
|
|
794
|
+
scope: context.scope,
|
|
795
|
+
artifactId: context.artifactId,
|
|
796
|
+
expectedLifecycle: "active",
|
|
797
|
+
expectedAuthorizationRevision: authorizationRevision,
|
|
798
|
+
authorizationActor: context.actor,
|
|
799
|
+
expectedHeadSequence: artifact.headSequence,
|
|
800
|
+
actorKey,
|
|
801
|
+
clientTransactionId: intent.clientTransactionId,
|
|
802
|
+
requestHash: request.requestHash,
|
|
803
|
+
expectedPredecessor: basis.predecessor
|
|
804
|
+
? {
|
|
805
|
+
receiptId: basis.predecessor.receiptId,
|
|
806
|
+
serverTransactionId: basis.predecessor.serverTransactionId,
|
|
807
|
+
actorKey: basis.predecessor.actorKey,
|
|
808
|
+
clientTransactionId: basis.predecessor.clientTransactionId,
|
|
809
|
+
replicaId: basis.predecessor.replicaId,
|
|
810
|
+
replicaCounter: basis.predecessor.replicaCounter,
|
|
811
|
+
}
|
|
812
|
+
: null,
|
|
813
|
+
expectedUnclaimedUndoTargets: Object.freeze([]),
|
|
814
|
+
serverTransactionId,
|
|
815
|
+
receipt,
|
|
816
|
+
committedTransaction,
|
|
817
|
+
operations: Object.freeze([]),
|
|
818
|
+
outbox,
|
|
819
|
+
});
|
|
820
|
+
if (commit.kind === "committed") {
|
|
821
|
+
return Object.freeze({
|
|
822
|
+
receipt: copyReceipt(commit.receipt),
|
|
823
|
+
replayed: false,
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
if (commit.kind === "replayed") {
|
|
827
|
+
if (commit.receipt.requestHash !== request.requestHash) {
|
|
828
|
+
throw new EditableArtifactIdempotencyConflictError();
|
|
829
|
+
}
|
|
830
|
+
return Object.freeze({
|
|
831
|
+
receipt: copyReceipt(commit.receipt),
|
|
832
|
+
replayed: true,
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
// Re-read before any new kernel call. A changed serialized head fails
|
|
836
|
+
// exact-base validation above instead of being merged or rebased.
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
if (
|
|
841
|
+
basis.kernelState.modality !== "spreadsheet" ||
|
|
842
|
+
basis.kernelState.artifact.modality !== "spreadsheet"
|
|
843
|
+
) {
|
|
844
|
+
throw new Error("Editable artifact store returned a cross-modality spreadsheet basis");
|
|
845
|
+
}
|
|
846
|
+
if (intent.observedHeadSequence > artifact.headSequence) {
|
|
847
|
+
throw new EditableArtifactCausalFutureError();
|
|
848
|
+
}
|
|
849
|
+
if (!causalFrontierDominates(artifact.causalFrontier, intent.causalBase)) {
|
|
850
|
+
throw new EditableArtifactCausalFutureError();
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
let resolvedCausalBase = intent.causalBase;
|
|
854
|
+
if (intent.previousLocalTransactionId === null) {
|
|
855
|
+
if (intent.replicaCounter !== 1) {
|
|
856
|
+
throw new EditableArtifactCausalChainError(
|
|
857
|
+
"Only a replica's first transaction may omit its local predecessor",
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
} else {
|
|
861
|
+
const predecessor = basis.predecessor;
|
|
862
|
+
if (!predecessor) {
|
|
863
|
+
throw new EditableArtifactCausalChainError(
|
|
864
|
+
"Previous local transaction is not committed yet",
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
if (
|
|
868
|
+
predecessor.modality !== "spreadsheet" ||
|
|
869
|
+
predecessor.replicaId !== context.actor.replicaId ||
|
|
870
|
+
predecessor.replicaCounter + 1 !== intent.replicaCounter
|
|
871
|
+
) {
|
|
872
|
+
throw new EditableArtifactCausalChainError(
|
|
873
|
+
"Previous local transaction does not match this replica counter chain",
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
resolvedCausalBase = mergeCausalFrontiers(
|
|
877
|
+
intent.causalBase,
|
|
878
|
+
predecessor.resolvedCausalBase,
|
|
879
|
+
editableArtifactCausalFrontier([
|
|
880
|
+
{
|
|
881
|
+
replicaId: predecessor.replicaId,
|
|
882
|
+
counter: predecessor.replicaCounter,
|
|
883
|
+
},
|
|
884
|
+
]),
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
if (
|
|
888
|
+
intent.replicaCounter !==
|
|
889
|
+
causalCounter(artifact.causalFrontier, context.actor.replicaId) + 1
|
|
890
|
+
) {
|
|
891
|
+
throw new EditableArtifactCausalChainError(
|
|
892
|
+
"Replica counter does not immediately advance the authoritative frontier",
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
if (!causalFrontierDominates(artifact.causalFrontier, resolvedCausalBase)) {
|
|
896
|
+
throw new EditableArtifactCausalFutureError();
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
if (basis.undoTargets.length !== intent.selectiveUndoOperationIds.length) {
|
|
900
|
+
throw new Error("Editable artifact store returned a malformed transaction undo basis");
|
|
901
|
+
}
|
|
902
|
+
const resolvedUndoTargets: EditableArtifactOperationRecord[] = [];
|
|
903
|
+
for (const [index, operationId] of intent.selectiveUndoOperationIds.entries()) {
|
|
904
|
+
const undoBasis = basis.undoTargets[index];
|
|
905
|
+
if (!undoBasis || undoBasis.operationId !== operationId) {
|
|
906
|
+
throw new Error("Editable artifact store returned a malformed transaction undo basis");
|
|
907
|
+
}
|
|
908
|
+
const operation = undoBasis.operation;
|
|
909
|
+
if (!operation) {
|
|
910
|
+
throw new EditableArtifactUndoTargetError(`Unknown undo target: ${operationId}`);
|
|
911
|
+
}
|
|
912
|
+
if (operation.actorKey !== actorKey) {
|
|
913
|
+
throw new EditableArtifactUndoTargetError(
|
|
914
|
+
`Selective undo may target only operations authored by the same authority: ${operationId}`,
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
if (undoBasis.claimedBy) {
|
|
918
|
+
throw new EditableArtifactUndoTargetError(
|
|
919
|
+
`Operation was already selectively undone: ${operationId}`,
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
if (causalCounter(resolvedCausalBase, operation.dot.replicaId) < operation.dot.counter) {
|
|
923
|
+
throw new EditableArtifactUndoTargetError(
|
|
924
|
+
`Selective undo target is not present in the transaction's causal base: ${operationId}`,
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
resolvedUndoTargets.push(operation);
|
|
928
|
+
}
|
|
929
|
+
Object.freeze(resolvedUndoTargets);
|
|
930
|
+
|
|
931
|
+
// This is intentionally outside the store's read and write transactions.
|
|
932
|
+
const kernelResult = await this.dependencies.kernel.applyTransaction({
|
|
933
|
+
modality: "spreadsheet",
|
|
934
|
+
state: basis.kernelState,
|
|
935
|
+
actor: context.actor,
|
|
936
|
+
intent,
|
|
937
|
+
requestHash: request.requestHash,
|
|
938
|
+
intentBytes: intentBytes.slice(),
|
|
939
|
+
resolvedCausalBase,
|
|
940
|
+
resolvedUndoTargets,
|
|
941
|
+
});
|
|
942
|
+
let normalizedKernel: ReturnType<typeof validateKernelResult>;
|
|
943
|
+
try {
|
|
944
|
+
normalizedKernel = validateKernelResult(
|
|
945
|
+
artifact,
|
|
946
|
+
context.actor,
|
|
947
|
+
intent,
|
|
948
|
+
resolvedCausalBase,
|
|
949
|
+
kernelResult,
|
|
950
|
+
);
|
|
951
|
+
} catch (error) {
|
|
952
|
+
if (error instanceof EditableArtifactKernelContractError) throw error;
|
|
953
|
+
throw new EditableArtifactKernelContractError(
|
|
954
|
+
`Authoritative kernel returned malformed data: ${
|
|
955
|
+
error instanceof Error ? error.message : "unknown contract error"
|
|
956
|
+
}`,
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
const committedAt = canonicalNow(this.dependencies.clock);
|
|
961
|
+
const serverTransactionId = normalizedKernel.serverTransactionId;
|
|
962
|
+
const sequenceStart = artifact.headSequence + 1;
|
|
963
|
+
const sequenceEnd = artifact.headSequence + normalizedKernel.operationIds.length;
|
|
964
|
+
assertNonnegativeSafeInteger(sequenceEnd, "resulting head sequence");
|
|
965
|
+
const operations: EditableArtifactOperationRecord[] = normalizedKernel.operationIds.map(
|
|
966
|
+
(operationId, index) =>
|
|
967
|
+
Object.freeze({
|
|
968
|
+
scope: artifact.scope,
|
|
969
|
+
artifactId: artifact.id,
|
|
970
|
+
serverTransactionId,
|
|
971
|
+
sequence: sequenceStart + index,
|
|
972
|
+
actorKey,
|
|
973
|
+
createdAt: committedAt,
|
|
974
|
+
operationId,
|
|
975
|
+
dot: normalizedKernel.dot,
|
|
976
|
+
}),
|
|
977
|
+
);
|
|
978
|
+
const committedTransaction: EditableArtifactSpreadsheetCommittedTransactionRecord =
|
|
979
|
+
Object.freeze({
|
|
980
|
+
scope: artifact.scope,
|
|
981
|
+
artifactId: artifact.id,
|
|
982
|
+
modality: "spreadsheet",
|
|
983
|
+
serverTransactionId,
|
|
984
|
+
requestHash: request.requestHash,
|
|
985
|
+
sequenceStart,
|
|
986
|
+
sequenceEnd,
|
|
987
|
+
priorStateHash: normalizedKernel.priorStateHash,
|
|
988
|
+
stateHash: normalizedKernel.stateHash,
|
|
989
|
+
dot: normalizedKernel.dot,
|
|
990
|
+
resolvedCausalBase,
|
|
991
|
+
resultingCausalFrontier: normalizedKernel.resultingCausalFrontier,
|
|
992
|
+
operationIds: normalizedKernel.operationIds,
|
|
993
|
+
operationProtocolVersion: normalizedKernel.operationProtocolVersion,
|
|
994
|
+
modelSchemaVersion: normalizedKernel.modelSchemaVersion,
|
|
995
|
+
kernelVersion: normalizedKernel.kernelVersion,
|
|
996
|
+
committedTransactionBytes: normalizedKernel.committedTransactionBytes.slice(),
|
|
997
|
+
committedAt,
|
|
998
|
+
});
|
|
999
|
+
const receipt = Object.freeze({
|
|
1000
|
+
receiptId: editableArtifactReceiptId(this.dependencies.ids.next("receipt")),
|
|
1001
|
+
scope: artifact.scope,
|
|
1002
|
+
artifactId: artifact.id,
|
|
1003
|
+
modality: "spreadsheet",
|
|
1004
|
+
serverTransactionId,
|
|
1005
|
+
clientTransactionId: intent.clientTransactionId,
|
|
1006
|
+
replicaId: context.actor.replicaId,
|
|
1007
|
+
replicaCounter: intent.replicaCounter,
|
|
1008
|
+
previousLocalTransactionId: intent.previousLocalTransactionId,
|
|
1009
|
+
requestHash: request.requestHash,
|
|
1010
|
+
intentBytes: intentBytes.slice(),
|
|
1011
|
+
actorKey,
|
|
1012
|
+
sequenceStart,
|
|
1013
|
+
sequenceEnd,
|
|
1014
|
+
priorStateHash: normalizedKernel.priorStateHash,
|
|
1015
|
+
causalBase: intent.causalBase,
|
|
1016
|
+
resolvedCausalBase,
|
|
1017
|
+
resultingCausalFrontier: normalizedKernel.resultingCausalFrontier,
|
|
1018
|
+
stateHash: normalizedKernel.stateHash,
|
|
1019
|
+
operationCount: operations.length,
|
|
1020
|
+
selectiveUndoOperationIds: intent.selectiveUndoOperationIds,
|
|
1021
|
+
intentEnvelopeVersion: intent.envelopeVersion,
|
|
1022
|
+
intentProtocolVersion: intent.protocolVersion,
|
|
1023
|
+
commandProtocolVersion: intent.commandProtocolVersion,
|
|
1024
|
+
kernelVersion: normalizedKernel.kernelVersion,
|
|
1025
|
+
modelSchemaVersion: normalizedKernel.modelSchemaVersion,
|
|
1026
|
+
operationProtocolVersion: normalizedKernel.operationProtocolVersion,
|
|
1027
|
+
committedAt,
|
|
1028
|
+
} as const satisfies EditableArtifactSpreadsheetReceipt);
|
|
1029
|
+
const outbox = transactionOutboxRecord(
|
|
1030
|
+
editableArtifactOutboxId(this.dependencies.ids.next("outbox")),
|
|
1031
|
+
receipt,
|
|
1032
|
+
);
|
|
1033
|
+
const commit = await this.dependencies.store.tryCommitAppliedTransaction({
|
|
1034
|
+
scope: context.scope,
|
|
1035
|
+
artifactId: context.artifactId,
|
|
1036
|
+
expectedLifecycle: "active",
|
|
1037
|
+
expectedAuthorizationRevision: authorizationRevision,
|
|
1038
|
+
authorizationActor: context.actor,
|
|
1039
|
+
expectedHeadSequence: artifact.headSequence,
|
|
1040
|
+
actorKey,
|
|
1041
|
+
clientTransactionId: intent.clientTransactionId,
|
|
1042
|
+
requestHash: request.requestHash,
|
|
1043
|
+
expectedPredecessor: basis.predecessor
|
|
1044
|
+
? {
|
|
1045
|
+
receiptId: basis.predecessor.receiptId,
|
|
1046
|
+
serverTransactionId: basis.predecessor.serverTransactionId,
|
|
1047
|
+
actorKey: basis.predecessor.actorKey,
|
|
1048
|
+
clientTransactionId: basis.predecessor.clientTransactionId,
|
|
1049
|
+
replicaId: basis.predecessor.replicaId,
|
|
1050
|
+
replicaCounter: basis.predecessor.replicaCounter,
|
|
1051
|
+
}
|
|
1052
|
+
: null,
|
|
1053
|
+
expectedUnclaimedUndoTargets: intent.selectiveUndoOperationIds,
|
|
1054
|
+
serverTransactionId,
|
|
1055
|
+
receipt,
|
|
1056
|
+
committedTransaction,
|
|
1057
|
+
operations,
|
|
1058
|
+
outbox,
|
|
1059
|
+
});
|
|
1060
|
+
if (commit.kind === "committed") {
|
|
1061
|
+
return Object.freeze({
|
|
1062
|
+
receipt: copyReceipt(commit.receipt),
|
|
1063
|
+
replayed: false,
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
if (commit.kind === "replayed") {
|
|
1067
|
+
if (commit.receipt.requestHash !== request.requestHash) {
|
|
1068
|
+
throw new EditableArtifactIdempotencyConflictError();
|
|
1069
|
+
}
|
|
1070
|
+
return Object.freeze({
|
|
1071
|
+
receipt: copyReceipt(commit.receipt),
|
|
1072
|
+
replayed: true,
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
// A newer head invalidates every derived value above. Re-read and rerun
|
|
1076
|
+
// from scratch; never patch or partially reuse speculative kernel output.
|
|
1077
|
+
}
|
|
1078
|
+
throw new EditableArtifactRetryableConflictError();
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
async compactCurrentHead(input: {
|
|
1082
|
+
scope: EditableArtifactScope;
|
|
1083
|
+
artifactId: EditableArtifact["id"];
|
|
1084
|
+
actor: EditableArtifactActor;
|
|
1085
|
+
signal?: AbortSignal;
|
|
1086
|
+
}): Promise<EditableArtifactSnapshotMetadata> {
|
|
1087
|
+
return await this.compactCurrentHeadWithPermission(input, "read");
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
private async compactCurrentHeadWithPermission(
|
|
1091
|
+
input: {
|
|
1092
|
+
scope: EditableArtifactScope;
|
|
1093
|
+
artifactId: EditableArtifact["id"];
|
|
1094
|
+
actor: EditableArtifactActor;
|
|
1095
|
+
signal?: AbortSignal;
|
|
1096
|
+
},
|
|
1097
|
+
authorizationPermission: "read" | "edit",
|
|
1098
|
+
): Promise<EditableArtifactSnapshotMetadata> {
|
|
1099
|
+
if (!this.dependencies.compaction) {
|
|
1100
|
+
throw new Error("Editable artifact snapshot compaction is not configured");
|
|
1101
|
+
}
|
|
1102
|
+
const context = normalizeBoundaryContext(input);
|
|
1103
|
+
// Coalescing is only a compute optimization. Every caller must cross its
|
|
1104
|
+
// own authorization boundary before it may observe another caller's result.
|
|
1105
|
+
const authorizationRevision = await this.requirePermission({
|
|
1106
|
+
...context,
|
|
1107
|
+
permission: authorizationPermission,
|
|
1108
|
+
});
|
|
1109
|
+
const key = compactionInFlightKey(
|
|
1110
|
+
context.scope,
|
|
1111
|
+
context.artifactId,
|
|
1112
|
+
context.actor,
|
|
1113
|
+
authorizationPermission,
|
|
1114
|
+
);
|
|
1115
|
+
const existing = this.inFlightCompactions.get(key);
|
|
1116
|
+
if (existing) return await existing;
|
|
1117
|
+
const promise = this.compactCurrentHeadOnce({
|
|
1118
|
+
...context,
|
|
1119
|
+
authorizationRevision,
|
|
1120
|
+
authorizationPermission,
|
|
1121
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
1122
|
+
});
|
|
1123
|
+
this.inFlightCompactions.set(key, promise);
|
|
1124
|
+
try {
|
|
1125
|
+
return await promise;
|
|
1126
|
+
} finally {
|
|
1127
|
+
if (this.inFlightCompactions.get(key) === promise) this.inFlightCompactions.delete(key);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
private async compactCurrentHeadOnce(input: {
|
|
1132
|
+
scope: EditableArtifactScope;
|
|
1133
|
+
artifactId: EditableArtifact["id"];
|
|
1134
|
+
actor: EditableArtifactActor;
|
|
1135
|
+
authorizationRevision: number;
|
|
1136
|
+
authorizationPermission: "read" | "edit";
|
|
1137
|
+
signal?: AbortSignal;
|
|
1138
|
+
}): Promise<EditableArtifactSnapshotMetadata> {
|
|
1139
|
+
const compaction = this.dependencies.compaction;
|
|
1140
|
+
if (!compaction) throw new Error("Editable artifact snapshot compaction is not configured");
|
|
1141
|
+
let authorizationRevision = input.authorizationRevision;
|
|
1142
|
+
for (let attempt = 1; attempt <= EDITABLE_ARTIFACT_MAX_COMMIT_ATTEMPTS; attempt += 1) {
|
|
1143
|
+
if (input.signal?.aborted) {
|
|
1144
|
+
throw new EditableArtifactSnapshotVerificationError("cancelled");
|
|
1145
|
+
}
|
|
1146
|
+
const basis = await this.dependencies.store.readCurrentKernelState(
|
|
1147
|
+
input.scope,
|
|
1148
|
+
input.artifactId,
|
|
1149
|
+
authorizationRevision,
|
|
1150
|
+
);
|
|
1151
|
+
if (basis.kind === "authorization_stale") {
|
|
1152
|
+
authorizationRevision = await this.requirePermission({
|
|
1153
|
+
...input,
|
|
1154
|
+
permission: input.authorizationPermission,
|
|
1155
|
+
});
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
const current = basis.state.snapshot;
|
|
1159
|
+
if (current && current.coveredHeadSequence === basis.state.artifact.headSequence) {
|
|
1160
|
+
return current;
|
|
1161
|
+
}
|
|
1162
|
+
const candidate = await compaction.prepare({
|
|
1163
|
+
scope: input.scope,
|
|
1164
|
+
artifactId: input.artifactId,
|
|
1165
|
+
snapshotId: editableArtifactSnapshotId(this.dependencies.ids.next("snapshot")),
|
|
1166
|
+
state: basis.state,
|
|
1167
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
1168
|
+
});
|
|
1169
|
+
await this.dependencies.snapshotVerifier.verify({ ...input, snapshot: candidate });
|
|
1170
|
+
try {
|
|
1171
|
+
const published = await this.commitVerifiedSnapshotCandidate({
|
|
1172
|
+
context: input,
|
|
1173
|
+
candidate,
|
|
1174
|
+
authorizationRevision,
|
|
1175
|
+
authorizationPermission: input.authorizationPermission,
|
|
1176
|
+
requireCurrentHead: true,
|
|
1177
|
+
});
|
|
1178
|
+
return published.snapshot;
|
|
1179
|
+
} catch (error) {
|
|
1180
|
+
// The durable head advanced while this exact candidate was outside the
|
|
1181
|
+
// lock. Reread authority; deterministic candidate failures are not retried.
|
|
1182
|
+
if (error instanceof EditableArtifactCompactionRaceError) continue;
|
|
1183
|
+
throw error;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
throw new EditableArtifactRetryableConflictError();
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
async publishVerifiedSnapshot(input: {
|
|
1190
|
+
scope: EditableArtifactScope;
|
|
1191
|
+
artifactId: EditableArtifact["id"];
|
|
1192
|
+
actor: EditableArtifactActor;
|
|
1193
|
+
snapshot: PublishEditableArtifactSnapshotRequest;
|
|
1194
|
+
}): Promise<{
|
|
1195
|
+
snapshot: EditableArtifactSnapshotMetadata;
|
|
1196
|
+
replayed: boolean;
|
|
1197
|
+
}> {
|
|
1198
|
+
const context = normalizeBoundaryContext(input);
|
|
1199
|
+
const candidate = validateSnapshotRequest(input.snapshot);
|
|
1200
|
+
const authorizationRevision = await this.requirePermission({
|
|
1201
|
+
...context,
|
|
1202
|
+
permission: "manage",
|
|
1203
|
+
});
|
|
1204
|
+
await this.dependencies.snapshotVerifier.verify({
|
|
1205
|
+
...context,
|
|
1206
|
+
snapshot: candidate,
|
|
1207
|
+
});
|
|
1208
|
+
return await this.commitVerifiedSnapshotCandidate({
|
|
1209
|
+
context,
|
|
1210
|
+
candidate,
|
|
1211
|
+
authorizationRevision,
|
|
1212
|
+
authorizationPermission: "manage",
|
|
1213
|
+
requireCurrentHead: false,
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
private async commitVerifiedSnapshotCandidate(input: {
|
|
1218
|
+
context: Readonly<{
|
|
1219
|
+
scope: EditableArtifactScope;
|
|
1220
|
+
artifactId: EditableArtifact["id"];
|
|
1221
|
+
actor: EditableArtifactActor;
|
|
1222
|
+
}>;
|
|
1223
|
+
candidate: PublishEditableArtifactSnapshotRequest;
|
|
1224
|
+
authorizationRevision: number;
|
|
1225
|
+
authorizationPermission: "manage" | "read" | "edit";
|
|
1226
|
+
requireCurrentHead: boolean;
|
|
1227
|
+
}): Promise<{
|
|
1228
|
+
snapshot: EditableArtifactSnapshotMetadata;
|
|
1229
|
+
replayed: boolean;
|
|
1230
|
+
}> {
|
|
1231
|
+
const { context, candidate, authorizationPermission } = input;
|
|
1232
|
+
let authorizationRevision = input.authorizationRevision;
|
|
1233
|
+
for (let attempt = 1; attempt <= EDITABLE_ARTIFACT_MAX_COMMIT_ATTEMPTS; attempt += 1) {
|
|
1234
|
+
const outcome = await this.dependencies.store.withSnapshotPublicationLock(
|
|
1235
|
+
context.scope,
|
|
1236
|
+
context.artifactId,
|
|
1237
|
+
async (unitOfWork) => {
|
|
1238
|
+
const artifact = unitOfWork.artifact();
|
|
1239
|
+
if (artifact.modality !== candidate.modality) {
|
|
1240
|
+
throw new EditableArtifactSnapshotConflictError(
|
|
1241
|
+
"Snapshot modality does not match the durable artifact",
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
if (input.requireCurrentHead && candidate.coveredHeadSequence !== artifact.headSequence) {
|
|
1245
|
+
throw new EditableArtifactCompactionRaceError(
|
|
1246
|
+
"Compaction candidate no longer covers the current durable head",
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
if (artifact.authorizationRevision !== authorizationRevision) {
|
|
1250
|
+
return Object.freeze({ kind: "authorization_stale" as const });
|
|
1251
|
+
}
|
|
1252
|
+
const existing = await unitOfWork.findSnapshot(candidate.snapshotId);
|
|
1253
|
+
if (existing) {
|
|
1254
|
+
if (!sameSnapshotRequest(existing, candidate)) {
|
|
1255
|
+
throw new EditableArtifactSnapshotConflictError(
|
|
1256
|
+
"Snapshot id was already published with different immutable metadata",
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
return Object.freeze({
|
|
1260
|
+
kind: "result" as const,
|
|
1261
|
+
value: Object.freeze({ snapshot: existing, replayed: true }),
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
const checkpoint = await unitOfWork.checkpoint(candidate.coveredHeadSequence);
|
|
1265
|
+
if (
|
|
1266
|
+
!checkpoint ||
|
|
1267
|
+
checkpoint.modality !== candidate.modality ||
|
|
1268
|
+
checkpoint.stateHash !== candidate.stateHash ||
|
|
1269
|
+
(candidate.modality === "spreadsheet" &&
|
|
1270
|
+
(checkpoint.modality !== "spreadsheet" ||
|
|
1271
|
+
!causalFrontiersEqual(
|
|
1272
|
+
checkpoint.causalFrontier,
|
|
1273
|
+
candidate.coveredCausalFrontier,
|
|
1274
|
+
))) ||
|
|
1275
|
+
(candidate.modality !== "spreadsheet" &&
|
|
1276
|
+
(checkpoint.modality === "spreadsheet" ||
|
|
1277
|
+
checkpoint.nativeRevision !== candidate.nativeRevision))
|
|
1278
|
+
) {
|
|
1279
|
+
throw new EditableArtifactSnapshotConflictError(
|
|
1280
|
+
"Snapshot coverage does not match an authoritative transaction boundary",
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
if (artifact.currentSnapshotId) {
|
|
1284
|
+
const current = await unitOfWork.findSnapshot(artifact.currentSnapshotId);
|
|
1285
|
+
if (!current) {
|
|
1286
|
+
throw new EditableArtifactSnapshotConflictError(
|
|
1287
|
+
"Current snapshot metadata is missing",
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
if (candidate.coveredHeadSequence <= current.coveredHeadSequence) {
|
|
1291
|
+
if (
|
|
1292
|
+
input.requireCurrentHead &&
|
|
1293
|
+
current.coveredHeadSequence === artifact.headSequence
|
|
1294
|
+
) {
|
|
1295
|
+
return Object.freeze({
|
|
1296
|
+
kind: "result" as const,
|
|
1297
|
+
value: Object.freeze({ snapshot: current, replayed: true }),
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
throw new EditableArtifactSnapshotConflictError(
|
|
1301
|
+
"Snapshot publication must advance coverage",
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
const publishedAt = canonicalNow(this.dependencies.clock);
|
|
1306
|
+
const snapshot: EditableArtifactSnapshotMetadata = Object.freeze({
|
|
1307
|
+
...candidate,
|
|
1308
|
+
scope: artifact.scope,
|
|
1309
|
+
artifactId: artifact.id,
|
|
1310
|
+
publishedAt,
|
|
1311
|
+
});
|
|
1312
|
+
const outbox = snapshotOutboxRecord(
|
|
1313
|
+
editableArtifactOutboxId(this.dependencies.ids.next("outbox")),
|
|
1314
|
+
snapshot,
|
|
1315
|
+
);
|
|
1316
|
+
const publication = await unitOfWork.commitSnapshot({
|
|
1317
|
+
expectedCurrentSnapshotId: artifact.currentSnapshotId,
|
|
1318
|
+
expectedAuthorizationRevision: authorizationRevision,
|
|
1319
|
+
authorizationActor: context.actor,
|
|
1320
|
+
authorizationPermission,
|
|
1321
|
+
snapshot,
|
|
1322
|
+
outbox,
|
|
1323
|
+
});
|
|
1324
|
+
if (publication.kind === "authorization_stale") {
|
|
1325
|
+
return Object.freeze({ kind: "authorization_stale" as const });
|
|
1326
|
+
}
|
|
1327
|
+
return Object.freeze({
|
|
1328
|
+
kind: "result" as const,
|
|
1329
|
+
value: Object.freeze({ snapshot, replayed: false }),
|
|
1330
|
+
});
|
|
1331
|
+
},
|
|
1332
|
+
);
|
|
1333
|
+
if (outcome.kind === "result") return outcome.value;
|
|
1334
|
+
authorizationRevision = await this.requirePermission({
|
|
1335
|
+
...context,
|
|
1336
|
+
permission: authorizationPermission,
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
throw new EditableArtifactRetryableConflictError();
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
private async requirePermission(input: {
|
|
1343
|
+
scope: EditableArtifactScope;
|
|
1344
|
+
artifactId: EditableArtifact["id"];
|
|
1345
|
+
actor: EditableArtifactActor;
|
|
1346
|
+
permission: EditableArtifactPermission;
|
|
1347
|
+
}): Promise<number> {
|
|
1348
|
+
const decision = await this.dependencies.authorization.authorize({
|
|
1349
|
+
scope: input.scope,
|
|
1350
|
+
artifactId: input.artifactId,
|
|
1351
|
+
actor: input.actor,
|
|
1352
|
+
permission: input.permission,
|
|
1353
|
+
});
|
|
1354
|
+
if (typeof decision.allowed !== "boolean") {
|
|
1355
|
+
throw new TypeError("Artifact authorization decision is malformed");
|
|
1356
|
+
}
|
|
1357
|
+
assertPositiveSafeInteger(decision.revision, "artifact authorization revision");
|
|
1358
|
+
if (!decision.allowed) {
|
|
1359
|
+
throw new EditableArtifactForbiddenError(input.permission);
|
|
1360
|
+
}
|
|
1361
|
+
return decision.revision;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
function creationInFlightKey(
|
|
1366
|
+
scope: EditableArtifactScope,
|
|
1367
|
+
operationKind: "create" | "import",
|
|
1368
|
+
actorKey: string,
|
|
1369
|
+
idempotencyKey: string,
|
|
1370
|
+
): string {
|
|
1371
|
+
return JSON.stringify([
|
|
1372
|
+
scope.accountId,
|
|
1373
|
+
scope.workspaceId,
|
|
1374
|
+
operationKind,
|
|
1375
|
+
actorKey,
|
|
1376
|
+
idempotencyKey,
|
|
1377
|
+
]);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
function transactionInFlightKey(
|
|
1381
|
+
scope: EditableArtifactScope,
|
|
1382
|
+
artifactId: EditableArtifact["id"],
|
|
1383
|
+
actorKey: string,
|
|
1384
|
+
clientTransactionId: EditableArtifactReceipt["clientTransactionId"],
|
|
1385
|
+
): string {
|
|
1386
|
+
return JSON.stringify([
|
|
1387
|
+
scope.accountId,
|
|
1388
|
+
scope.workspaceId,
|
|
1389
|
+
artifactId,
|
|
1390
|
+
actorKey,
|
|
1391
|
+
clientTransactionId,
|
|
1392
|
+
]);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function compactionInFlightKey(
|
|
1396
|
+
scope: EditableArtifactScope,
|
|
1397
|
+
artifactId: EditableArtifact["id"],
|
|
1398
|
+
actor: EditableArtifactActor,
|
|
1399
|
+
permission: "read" | "edit",
|
|
1400
|
+
): string {
|
|
1401
|
+
return JSON.stringify([
|
|
1402
|
+
scope.accountId,
|
|
1403
|
+
scope.workspaceId,
|
|
1404
|
+
artifactId,
|
|
1405
|
+
editableArtifactActorKey(actor),
|
|
1406
|
+
permission,
|
|
1407
|
+
]);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
function shouldCompactKernelState(state: EditableArtifactKernelState): boolean {
|
|
1411
|
+
return (
|
|
1412
|
+
state.tailTransactionCount >= EDITABLE_ARTIFACT_COMPACTION_TRANSACTION_THRESHOLD ||
|
|
1413
|
+
state.tailByteSize >= EDITABLE_ARTIFACT_COMPACTION_BYTE_THRESHOLD
|
|
1414
|
+
);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
class EditableArtifactCompactionRaceError extends EditableArtifactSnapshotConflictError {}
|
|
1418
|
+
|
|
1419
|
+
function normalizeCreateRequest(
|
|
1420
|
+
input: CreateEditableArtifactRequest,
|
|
1421
|
+
): CreateEditableArtifactRequest {
|
|
1422
|
+
const record = plainDataRecord(input, "artifact create request");
|
|
1423
|
+
rejectUnknownKeys(record, ["idempotencyKey", "modality", "title"], "artifact create request");
|
|
1424
|
+
const rawIdempotencyKey = dataProperty(record, "idempotencyKey", true);
|
|
1425
|
+
const modality = dataProperty(record, "modality", true);
|
|
1426
|
+
const title = dataProperty(record, "title", true);
|
|
1427
|
+
if (
|
|
1428
|
+
typeof rawIdempotencyKey !== "string" ||
|
|
1429
|
+
typeof title !== "string" ||
|
|
1430
|
+
(modality !== "spreadsheet" && modality !== "presentation" && modality !== "document")
|
|
1431
|
+
) {
|
|
1432
|
+
throw new TypeError("Artifact create request fields are malformed");
|
|
1433
|
+
}
|
|
1434
|
+
assertBoundedArtifactTitle(title);
|
|
1435
|
+
return Object.freeze({
|
|
1436
|
+
idempotencyKey: editableArtifactClientTransactionId(rawIdempotencyKey),
|
|
1437
|
+
modality,
|
|
1438
|
+
title,
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
function normalizeImportRequest(
|
|
1443
|
+
input: ImportEditableArtifactRequest,
|
|
1444
|
+
): ImportEditableArtifactRequest {
|
|
1445
|
+
const record = plainDataRecord(input, "artifact import request");
|
|
1446
|
+
rejectUnknownKeys(
|
|
1447
|
+
record,
|
|
1448
|
+
["idempotencyKey", "modality", "title", "originalImport", "snapshot"],
|
|
1449
|
+
"artifact import request",
|
|
1450
|
+
);
|
|
1451
|
+
const rawIdempotencyKey = dataProperty(record, "idempotencyKey", true);
|
|
1452
|
+
const modality = dataProperty(record, "modality", true);
|
|
1453
|
+
const title = dataProperty(record, "title", true);
|
|
1454
|
+
const rawOriginalImport = dataProperty(record, "originalImport", true);
|
|
1455
|
+
const rawSnapshot = dataProperty(record, "snapshot", true);
|
|
1456
|
+
if (
|
|
1457
|
+
typeof rawIdempotencyKey !== "string" ||
|
|
1458
|
+
typeof title !== "string" ||
|
|
1459
|
+
(modality !== "spreadsheet" && modality !== "presentation" && modality !== "document")
|
|
1460
|
+
) {
|
|
1461
|
+
throw new TypeError("Artifact import request fields are malformed");
|
|
1462
|
+
}
|
|
1463
|
+
assertBoundedArtifactTitle(title);
|
|
1464
|
+
const originalImport = normalizeOriginalImport(rawOriginalImport, modality);
|
|
1465
|
+
const snapshotRecord = plainDataRecord(rawSnapshot, "artifact import snapshot");
|
|
1466
|
+
if (
|
|
1467
|
+
Reflect.ownKeys(snapshotRecord).includes("snapshotId") ||
|
|
1468
|
+
Reflect.ownKeys(snapshotRecord).includes("verifiedAt")
|
|
1469
|
+
) {
|
|
1470
|
+
throw new TypeError("Artifact import snapshot identity and timestamps are server-assigned");
|
|
1471
|
+
}
|
|
1472
|
+
const candidate = Object.fromEntries(
|
|
1473
|
+
Reflect.ownKeys(snapshotRecord).map((key) => {
|
|
1474
|
+
if (typeof key !== "string") throw new TypeError("Artifact import snapshot has symbols");
|
|
1475
|
+
return [key, dataProperty(snapshotRecord, key, true)];
|
|
1476
|
+
}),
|
|
1477
|
+
);
|
|
1478
|
+
const normalized = validateSnapshotRequest({
|
|
1479
|
+
...candidate,
|
|
1480
|
+
snapshotId: "00000000000000000000000000000001",
|
|
1481
|
+
verifiedAt: "1970-01-01T00:00:00.000Z",
|
|
1482
|
+
} as PublishEditableArtifactSnapshotRequest);
|
|
1483
|
+
if (normalized.modality !== modality || normalized.coveredHeadSequence !== 0) {
|
|
1484
|
+
throw new TypeError("Artifact import snapshot boundary is malformed");
|
|
1485
|
+
}
|
|
1486
|
+
const {
|
|
1487
|
+
snapshotId: _serverAssignedSnapshotId,
|
|
1488
|
+
verifiedAt: _serverAssignedVerifiedAt,
|
|
1489
|
+
...snapshot
|
|
1490
|
+
} = normalized;
|
|
1491
|
+
return Object.freeze({
|
|
1492
|
+
idempotencyKey: editableArtifactClientTransactionId(rawIdempotencyKey),
|
|
1493
|
+
modality,
|
|
1494
|
+
title,
|
|
1495
|
+
originalImport,
|
|
1496
|
+
snapshot: Object.freeze(snapshot) as ImportEditableArtifactRequest["snapshot"],
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function normalizeOriginalImport(
|
|
1501
|
+
input: unknown,
|
|
1502
|
+
modality: ImportEditableArtifactRequest["modality"],
|
|
1503
|
+
): EditableArtifactOriginalImport {
|
|
1504
|
+
const record = plainDataRecord(input, "artifact original import");
|
|
1505
|
+
rejectUnknownKeys(
|
|
1506
|
+
record,
|
|
1507
|
+
["fileId", "blobReference", "byteSize", "contentHash", "mimeType"],
|
|
1508
|
+
"artifact original import",
|
|
1509
|
+
);
|
|
1510
|
+
const fileId = dataProperty(record, "fileId", true);
|
|
1511
|
+
const blobReference = dataProperty(record, "blobReference", true);
|
|
1512
|
+
const byteSize = dataProperty(record, "byteSize", true);
|
|
1513
|
+
const contentHash = dataProperty(record, "contentHash", true);
|
|
1514
|
+
const mimeType = dataProperty(record, "mimeType", true);
|
|
1515
|
+
const expectedMimeType =
|
|
1516
|
+
modality === "spreadsheet"
|
|
1517
|
+
? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
1518
|
+
: modality === "document"
|
|
1519
|
+
? "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
1520
|
+
: "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
1521
|
+
if (
|
|
1522
|
+
typeof fileId !== "string" ||
|
|
1523
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(fileId) ||
|
|
1524
|
+
typeof blobReference !== "string" ||
|
|
1525
|
+
typeof byteSize !== "number" ||
|
|
1526
|
+
typeof contentHash !== "string" ||
|
|
1527
|
+
mimeType !== expectedMimeType
|
|
1528
|
+
) {
|
|
1529
|
+
throw new TypeError("Artifact original import fields are malformed");
|
|
1530
|
+
}
|
|
1531
|
+
assertBoundedOpaqueReference(blobReference, "original import blob reference");
|
|
1532
|
+
assertPositiveSafeInteger(byteSize, "original import byte size");
|
|
1533
|
+
if (byteSize > EDITABLE_ARTIFACT_ORIGINAL_IMPORT_MAX_BYTES) {
|
|
1534
|
+
throw new TypeError("Artifact original import exceeds its byte limit");
|
|
1535
|
+
}
|
|
1536
|
+
return Object.freeze({
|
|
1537
|
+
fileId,
|
|
1538
|
+
blobReference,
|
|
1539
|
+
byteSize,
|
|
1540
|
+
contentHash: editableArtifactContentHash(contentHash),
|
|
1541
|
+
mimeType: expectedMimeType,
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
function copyReceipt(receipt: EditableArtifactReceipt): EditableArtifactReceipt {
|
|
1546
|
+
if (receipt.modality !== "spreadsheet") {
|
|
1547
|
+
return Object.freeze({
|
|
1548
|
+
...receipt,
|
|
1549
|
+
scope: Object.freeze({ ...receipt.scope }),
|
|
1550
|
+
intentBytes: receipt.intentBytes.slice(),
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
return Object.freeze({
|
|
1554
|
+
...receipt,
|
|
1555
|
+
scope: Object.freeze({ ...receipt.scope }),
|
|
1556
|
+
intentBytes: receipt.intentBytes.slice(),
|
|
1557
|
+
causalBase: editableArtifactCausalFrontier(receipt.causalBase),
|
|
1558
|
+
resolvedCausalBase: editableArtifactCausalFrontier(receipt.resolvedCausalBase),
|
|
1559
|
+
resultingCausalFrontier: editableArtifactCausalFrontier(receipt.resultingCausalFrontier),
|
|
1560
|
+
selectiveUndoOperationIds: Object.freeze([...receipt.selectiveUndoOperationIds]),
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
function copyCreateResult(
|
|
1565
|
+
result: CreateEditableArtifactResult,
|
|
1566
|
+
replayed = result.replayed,
|
|
1567
|
+
): CreateEditableArtifactResult {
|
|
1568
|
+
const artifact =
|
|
1569
|
+
result.artifact.modality === "spreadsheet"
|
|
1570
|
+
? Object.freeze({
|
|
1571
|
+
...result.artifact,
|
|
1572
|
+
scope: Object.freeze({ ...result.artifact.scope }),
|
|
1573
|
+
causalFrontier: editableArtifactCausalFrontier(result.artifact.causalFrontier),
|
|
1574
|
+
})
|
|
1575
|
+
: Object.freeze({
|
|
1576
|
+
...result.artifact,
|
|
1577
|
+
scope: Object.freeze({ ...result.artifact.scope }),
|
|
1578
|
+
});
|
|
1579
|
+
const genesisSnapshot =
|
|
1580
|
+
result.genesisSnapshot.modality === "spreadsheet"
|
|
1581
|
+
? Object.freeze({
|
|
1582
|
+
...result.genesisSnapshot,
|
|
1583
|
+
scope: Object.freeze({ ...result.genesisSnapshot.scope }),
|
|
1584
|
+
coveredCausalFrontier: editableArtifactCausalFrontier(
|
|
1585
|
+
result.genesisSnapshot.coveredCausalFrontier,
|
|
1586
|
+
),
|
|
1587
|
+
})
|
|
1588
|
+
: Object.freeze({
|
|
1589
|
+
...result.genesisSnapshot,
|
|
1590
|
+
scope: Object.freeze({ ...result.genesisSnapshot.scope }),
|
|
1591
|
+
});
|
|
1592
|
+
return Object.freeze({
|
|
1593
|
+
artifact,
|
|
1594
|
+
genesisSnapshot,
|
|
1595
|
+
creationReceipt: Object.freeze({
|
|
1596
|
+
...result.creationReceipt,
|
|
1597
|
+
scope: Object.freeze({ ...result.creationReceipt.scope }),
|
|
1598
|
+
}),
|
|
1599
|
+
replayed,
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
function normalizeBoundaryContext(input: {
|
|
1604
|
+
scope: EditableArtifactScope;
|
|
1605
|
+
artifactId: EditableArtifact["id"];
|
|
1606
|
+
actor: EditableArtifactActor;
|
|
1607
|
+
}): Readonly<{
|
|
1608
|
+
scope: EditableArtifactScope;
|
|
1609
|
+
artifactId: EditableArtifact["id"];
|
|
1610
|
+
actor: EditableArtifactActor;
|
|
1611
|
+
}> {
|
|
1612
|
+
const scope = editableArtifactScope(input.scope);
|
|
1613
|
+
const artifactId = editableArtifactId(input.artifactId);
|
|
1614
|
+
validateEditableArtifactActor(input.actor);
|
|
1615
|
+
const actor: EditableArtifactActor = Object.freeze({ ...input.actor });
|
|
1616
|
+
return Object.freeze({ scope, artifactId, actor });
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
function normalizeTransactionEnvelope(
|
|
1620
|
+
request: ApplyEditableArtifactTransactionRequest,
|
|
1621
|
+
): ApplyEditableArtifactTransactionRequest {
|
|
1622
|
+
const record = plainDataRecord(request, "mutation envelope");
|
|
1623
|
+
rejectUnknownKeys(record, ["intentBytes", "requestHash", "expectedHead"], "mutation envelope");
|
|
1624
|
+
const rawIntentBytes = dataProperty(record, "intentBytes", true);
|
|
1625
|
+
const rawRequestHash = dataProperty(record, "requestHash", true);
|
|
1626
|
+
const rawExpectedHead = dataProperty(record, "expectedHead", false);
|
|
1627
|
+
if (!(rawIntentBytes instanceof Uint8Array)) {
|
|
1628
|
+
throw new TypeError("Mutation intent bytes must be a Uint8Array");
|
|
1629
|
+
}
|
|
1630
|
+
if (
|
|
1631
|
+
rawIntentBytes.byteLength < 1 ||
|
|
1632
|
+
rawIntentBytes.byteLength > EDITABLE_ARTIFACT_MAX_INTENT_BYTES_PER_TRANSACTION
|
|
1633
|
+
) {
|
|
1634
|
+
throw new TypeError("Mutation intent bytes exceed the bounded envelope size");
|
|
1635
|
+
}
|
|
1636
|
+
if (typeof rawRequestHash !== "string") {
|
|
1637
|
+
throw new TypeError("Mutation request hash must be a string");
|
|
1638
|
+
}
|
|
1639
|
+
let expectedHead: ApplyEditableArtifactTransactionRequest["expectedHead"];
|
|
1640
|
+
if (rawExpectedHead !== undefined) {
|
|
1641
|
+
const expected = plainDataRecord(rawExpectedHead, "mutation expected head");
|
|
1642
|
+
rejectUnknownKeys(expected, ["sequence", "stateHash"], "mutation expected head");
|
|
1643
|
+
const sequence = dataProperty(expected, "sequence", true);
|
|
1644
|
+
const stateHash = dataProperty(expected, "stateHash", true);
|
|
1645
|
+
if (typeof sequence !== "number" || typeof stateHash !== "string") {
|
|
1646
|
+
throw new TypeError("Mutation expected head fields are malformed");
|
|
1647
|
+
}
|
|
1648
|
+
assertNonnegativeSafeInteger(sequence, "mutation expected head sequence");
|
|
1649
|
+
expectedHead = Object.freeze({
|
|
1650
|
+
sequence,
|
|
1651
|
+
stateHash: editableArtifactStateHash(stateHash),
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
return Object.freeze({
|
|
1655
|
+
intentBytes: rawIntentBytes.slice(),
|
|
1656
|
+
requestHash: editableArtifactRequestHash(rawRequestHash),
|
|
1657
|
+
...(expectedHead ? { expectedHead } : {}),
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
function normalizeMutationIntent(
|
|
1662
|
+
input: EditableArtifactMutationIntent,
|
|
1663
|
+
): ValidatedEditableArtifactMutationIntent {
|
|
1664
|
+
const record = plainDataRecord(input, "decoded mutation intent");
|
|
1665
|
+
rejectUnknownKeys(
|
|
1666
|
+
record,
|
|
1667
|
+
[
|
|
1668
|
+
"envelopeVersion",
|
|
1669
|
+
"protocolVersion",
|
|
1670
|
+
"modelSchemaVersion",
|
|
1671
|
+
"commandProtocolVersion",
|
|
1672
|
+
"artifactId",
|
|
1673
|
+
"clientTransactionId",
|
|
1674
|
+
"replicaId",
|
|
1675
|
+
"replicaCounter",
|
|
1676
|
+
"previousLocalTransactionId",
|
|
1677
|
+
"observedHeadSequence",
|
|
1678
|
+
"causalBase",
|
|
1679
|
+
"selectiveUndoOperationIds",
|
|
1680
|
+
"commandBytes",
|
|
1681
|
+
],
|
|
1682
|
+
"decoded mutation intent",
|
|
1683
|
+
);
|
|
1684
|
+
const envelopeVersion = dataProperty(record, "envelopeVersion", true);
|
|
1685
|
+
const protocolVersion = dataProperty(record, "protocolVersion", true);
|
|
1686
|
+
const modelSchemaVersion = dataProperty(record, "modelSchemaVersion", true);
|
|
1687
|
+
const commandProtocolVersion = dataProperty(record, "commandProtocolVersion", true);
|
|
1688
|
+
const rawArtifactId = dataProperty(record, "artifactId", true);
|
|
1689
|
+
const rawClientTransactionId = dataProperty(record, "clientTransactionId", true);
|
|
1690
|
+
const rawReplicaId = dataProperty(record, "replicaId", true);
|
|
1691
|
+
const replicaCounter = dataProperty(record, "replicaCounter", true);
|
|
1692
|
+
const rawPreviousLocalTransactionId = dataProperty(record, "previousLocalTransactionId", true);
|
|
1693
|
+
const observedHeadSequence = dataProperty(record, "observedHeadSequence", true);
|
|
1694
|
+
const rawCausalBase = dataProperty(record, "causalBase", true);
|
|
1695
|
+
const rawUndoTargets = dataProperty(record, "selectiveUndoOperationIds", true);
|
|
1696
|
+
const rawCommandBytes = dataProperty(record, "commandBytes", true);
|
|
1697
|
+
if (
|
|
1698
|
+
typeof envelopeVersion !== "number" ||
|
|
1699
|
+
typeof protocolVersion !== "number" ||
|
|
1700
|
+
typeof modelSchemaVersion !== "number" ||
|
|
1701
|
+
typeof commandProtocolVersion !== "number" ||
|
|
1702
|
+
typeof rawArtifactId !== "string" ||
|
|
1703
|
+
typeof rawClientTransactionId !== "string" ||
|
|
1704
|
+
typeof rawReplicaId !== "string" ||
|
|
1705
|
+
typeof replicaCounter !== "number" ||
|
|
1706
|
+
(rawPreviousLocalTransactionId !== null && typeof rawPreviousLocalTransactionId !== "string") ||
|
|
1707
|
+
typeof observedHeadSequence !== "number" ||
|
|
1708
|
+
!Array.isArray(rawCausalBase) ||
|
|
1709
|
+
!Array.isArray(rawUndoTargets) ||
|
|
1710
|
+
!(rawCommandBytes instanceof Uint8Array)
|
|
1711
|
+
) {
|
|
1712
|
+
throw new TypeError("Decoded mutation intent fields are malformed");
|
|
1713
|
+
}
|
|
1714
|
+
if (envelopeVersion !== EDITABLE_ARTIFACT_INTENT_VERSION) {
|
|
1715
|
+
throw new TypeError("Mutation intent envelope version is unsupported");
|
|
1716
|
+
}
|
|
1717
|
+
assertPositiveSafeInteger(protocolVersion, "intent protocol version");
|
|
1718
|
+
if (protocolVersion !== EDITABLE_ARTIFACT_INTENT_PROTOCOL_VERSION) {
|
|
1719
|
+
throw new TypeError("Mutation intent protocol version is unsupported");
|
|
1720
|
+
}
|
|
1721
|
+
assertPositiveSafeInteger(modelSchemaVersion, "intent model schema version");
|
|
1722
|
+
assertPositiveSafeInteger(commandProtocolVersion, "intent command protocol version");
|
|
1723
|
+
const artifactId = editableArtifactId(rawArtifactId);
|
|
1724
|
+
const clientTransactionId = editableArtifactClientTransactionId(rawClientTransactionId);
|
|
1725
|
+
const replicaId = editableArtifactReplicaId(rawReplicaId);
|
|
1726
|
+
assertPositiveSafeInteger(replicaCounter, "replica counter");
|
|
1727
|
+
const previousLocalTransactionId =
|
|
1728
|
+
rawPreviousLocalTransactionId === null
|
|
1729
|
+
? null
|
|
1730
|
+
: editableArtifactClientTransactionId(rawPreviousLocalTransactionId);
|
|
1731
|
+
if (previousLocalTransactionId === clientTransactionId) {
|
|
1732
|
+
throw new TypeError("A transaction cannot name itself as its local predecessor");
|
|
1733
|
+
}
|
|
1734
|
+
assertNonnegativeSafeInteger(observedHeadSequence, "observed head sequence");
|
|
1735
|
+
const causalBase = editableArtifactCausalFrontier(
|
|
1736
|
+
rawCausalBase as EditableArtifactCausalFrontier,
|
|
1737
|
+
);
|
|
1738
|
+
if (rawUndoTargets.length > EDITABLE_ARTIFACT_MAX_UNDO_TARGETS_PER_TRANSACTION) {
|
|
1739
|
+
throw new TypeError("Editable artifact transaction has too many undo targets");
|
|
1740
|
+
}
|
|
1741
|
+
const selectiveUndoOperationIds = rawUndoTargets.map((target) => {
|
|
1742
|
+
if (typeof target !== "string") {
|
|
1743
|
+
throw new TypeError("Selective undo target must be a string");
|
|
1744
|
+
}
|
|
1745
|
+
return editableArtifactOperationId(target);
|
|
1746
|
+
});
|
|
1747
|
+
if (new Set(selectiveUndoOperationIds).size !== selectiveUndoOperationIds.length) {
|
|
1748
|
+
throw new TypeError("Selective undo targets must be unique");
|
|
1749
|
+
}
|
|
1750
|
+
const sortedUndoTargets = [...selectiveUndoOperationIds].sort(compareCodeUnits);
|
|
1751
|
+
if (sortedUndoTargets.some((target, index) => target !== selectiveUndoOperationIds[index])) {
|
|
1752
|
+
throw new TypeError("Selective undo targets must be canonically sorted");
|
|
1753
|
+
}
|
|
1754
|
+
if (
|
|
1755
|
+
rawCommandBytes.byteLength < 1 ||
|
|
1756
|
+
rawCommandBytes.byteLength > EDITABLE_ARTIFACT_MAX_COMMAND_BYTES_PER_TRANSACTION
|
|
1757
|
+
) {
|
|
1758
|
+
throw new TypeError("Mutation command bytes exceed the transaction limit");
|
|
1759
|
+
}
|
|
1760
|
+
return Object.freeze({
|
|
1761
|
+
envelopeVersion,
|
|
1762
|
+
protocolVersion,
|
|
1763
|
+
modelSchemaVersion,
|
|
1764
|
+
commandProtocolVersion,
|
|
1765
|
+
artifactId,
|
|
1766
|
+
clientTransactionId,
|
|
1767
|
+
replicaId,
|
|
1768
|
+
replicaCounter,
|
|
1769
|
+
previousLocalTransactionId,
|
|
1770
|
+
observedHeadSequence,
|
|
1771
|
+
causalBase,
|
|
1772
|
+
selectiveUndoOperationIds: Object.freeze(selectiveUndoOperationIds),
|
|
1773
|
+
commandBytes: rawCommandBytes.slice(),
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
function plainDataRecord(value: unknown, label: string): Record<string, unknown> {
|
|
1778
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1779
|
+
throw new TypeError(`${label} must be a plain object`);
|
|
1780
|
+
}
|
|
1781
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1782
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
1783
|
+
throw new TypeError(`${label} must be a plain object`);
|
|
1784
|
+
}
|
|
1785
|
+
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
1786
|
+
throw new TypeError(`${label} must not contain symbol properties`);
|
|
1787
|
+
}
|
|
1788
|
+
return value as Record<string, unknown>;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
function rejectUnknownKeys(
|
|
1792
|
+
record: Record<string, unknown>,
|
|
1793
|
+
allowed: readonly string[],
|
|
1794
|
+
label: string,
|
|
1795
|
+
): void {
|
|
1796
|
+
const allowedSet = new Set(allowed);
|
|
1797
|
+
const unknown = Object.keys(record).find((key) => !allowedSet.has(key));
|
|
1798
|
+
if (unknown) throw new TypeError(`${label} contains unknown property: ${unknown}`);
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
function dataProperty(record: Record<string, unknown>, key: string, required: boolean): unknown {
|
|
1802
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, key);
|
|
1803
|
+
if (!descriptor) {
|
|
1804
|
+
if (required) throw new TypeError(`Missing required property: ${key}`);
|
|
1805
|
+
return undefined;
|
|
1806
|
+
}
|
|
1807
|
+
if (!("value" in descriptor)) throw new TypeError(`Accessor property is not allowed: ${key}`);
|
|
1808
|
+
if (descriptor.value === undefined) {
|
|
1809
|
+
throw new TypeError(`Undefined property is not allowed: ${key}`);
|
|
1810
|
+
}
|
|
1811
|
+
return descriptor.value;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
function validateIntentCommands(
|
|
1815
|
+
artifact: EditableArtifact,
|
|
1816
|
+
intent: ValidatedEditableArtifactMutationIntent,
|
|
1817
|
+
): void {
|
|
1818
|
+
try {
|
|
1819
|
+
const descriptor = editableArtifactCodecFor({
|
|
1820
|
+
durableModality: artifact.modality,
|
|
1821
|
+
modelSchemaVersion: intent.modelSchemaVersion,
|
|
1822
|
+
commandProtocolVersion: intent.commandProtocolVersion,
|
|
1823
|
+
});
|
|
1824
|
+
descriptor.command.assertCanonical(intent.commandBytes);
|
|
1825
|
+
const decoded = descriptor.command.decode(intent.commandBytes);
|
|
1826
|
+
if (artifact.modality !== "spreadsheet" && commandCount(decoded) < 1) {
|
|
1827
|
+
throw new TypeError("serialized command batch must not be empty");
|
|
1828
|
+
}
|
|
1829
|
+
} catch (error) {
|
|
1830
|
+
throw new EditableArtifactInvalidRequestError(
|
|
1831
|
+
`Mutation commands are incompatible with the durable ${artifact.modality} artifact: ${
|
|
1832
|
+
error instanceof Error ? error.message : "invalid command envelope"
|
|
1833
|
+
}`,
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
function commandCount(value: unknown): number {
|
|
1839
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1840
|
+
throw new TypeError("command decoder returned an invalid batch");
|
|
1841
|
+
}
|
|
1842
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, "commands");
|
|
1843
|
+
if (!descriptor || !("value" in descriptor) || !Array.isArray(descriptor.value)) {
|
|
1844
|
+
throw new TypeError("command decoder returned an invalid batch");
|
|
1845
|
+
}
|
|
1846
|
+
return descriptor.value.length;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
function validateSerializedPredecessor(
|
|
1850
|
+
predecessor: EditableArtifactReceipt | null,
|
|
1851
|
+
replicaId: EditableArtifactReceipt["replicaId"],
|
|
1852
|
+
intent: ValidatedEditableArtifactMutationIntent,
|
|
1853
|
+
modality: "document" | "presentation",
|
|
1854
|
+
): void {
|
|
1855
|
+
if (intent.previousLocalTransactionId === null) {
|
|
1856
|
+
if (intent.replicaCounter !== 1) {
|
|
1857
|
+
throw new EditableArtifactCausalChainError(
|
|
1858
|
+
"Only a replica's first transaction may omit its local predecessor",
|
|
1859
|
+
);
|
|
1860
|
+
}
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
if (!predecessor) {
|
|
1864
|
+
throw new EditableArtifactCausalChainError("Previous local transaction is not committed yet");
|
|
1865
|
+
}
|
|
1866
|
+
if (
|
|
1867
|
+
predecessor.modality !== modality ||
|
|
1868
|
+
predecessor.replicaId !== replicaId ||
|
|
1869
|
+
predecessor.replicaCounter + 1 !== intent.replicaCounter
|
|
1870
|
+
) {
|
|
1871
|
+
throw new EditableArtifactCausalChainError(
|
|
1872
|
+
"Previous local transaction does not match this serialized replica chain",
|
|
1873
|
+
);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
function serializedBasisNativeRevision(
|
|
1878
|
+
state: Extract<
|
|
1879
|
+
import("./ports").EditableArtifactKernelState,
|
|
1880
|
+
{ modality: "document" | "presentation" }
|
|
1881
|
+
>,
|
|
1882
|
+
artifact: EditableArtifactSerialized,
|
|
1883
|
+
): number {
|
|
1884
|
+
if (
|
|
1885
|
+
state.modality !== artifact.modality ||
|
|
1886
|
+
state.artifact.id !== artifact.id ||
|
|
1887
|
+
state.artifact.stateHash !== artifact.stateHash ||
|
|
1888
|
+
state.artifact.headSequence !== artifact.headSequence
|
|
1889
|
+
) {
|
|
1890
|
+
throw new Error("Serialized kernel basis disagrees with its artifact head");
|
|
1891
|
+
}
|
|
1892
|
+
let headSequence = 0;
|
|
1893
|
+
assertNonnegativeSafeInteger(state.baseNativeRevision, "serialized kernel base native revision");
|
|
1894
|
+
let nativeRevision = state.baseNativeRevision;
|
|
1895
|
+
let stateHash: EditableArtifact["stateHash"];
|
|
1896
|
+
if (state.snapshot) {
|
|
1897
|
+
if (
|
|
1898
|
+
state.snapshot.modality !== artifact.modality ||
|
|
1899
|
+
state.snapshot.artifactId !== artifact.id
|
|
1900
|
+
) {
|
|
1901
|
+
throw new Error("Serialized snapshot basis has the wrong modality or identity");
|
|
1902
|
+
}
|
|
1903
|
+
if (state.snapshot.nativeRevision !== state.baseNativeRevision) {
|
|
1904
|
+
throw new Error("Serialized snapshot disagrees with its native revision checkpoint");
|
|
1905
|
+
}
|
|
1906
|
+
headSequence = state.snapshot.coveredHeadSequence;
|
|
1907
|
+
stateHash = state.snapshot.stateHash;
|
|
1908
|
+
} else {
|
|
1909
|
+
stateHash = state.committedTransactionTail[0]?.priorStateHash ?? artifact.stateHash;
|
|
1910
|
+
}
|
|
1911
|
+
for (const committed of state.committedTransactionTail) {
|
|
1912
|
+
if (
|
|
1913
|
+
committed.modality !== artifact.modality ||
|
|
1914
|
+
committed.sequenceStart !== headSequence + 1 ||
|
|
1915
|
+
committed.sequenceEnd !== committed.sequenceStart ||
|
|
1916
|
+
committed.priorNativeRevision !== nativeRevision ||
|
|
1917
|
+
committed.priorStateHash !== stateHash
|
|
1918
|
+
) {
|
|
1919
|
+
throw new Error("Serialized transaction tail is not contiguous with its snapshot");
|
|
1920
|
+
}
|
|
1921
|
+
headSequence = committed.sequenceEnd;
|
|
1922
|
+
nativeRevision = committed.nativeRevision;
|
|
1923
|
+
stateHash = committed.stateHash;
|
|
1924
|
+
}
|
|
1925
|
+
if (headSequence !== artifact.headSequence || stateHash !== artifact.stateHash) {
|
|
1926
|
+
throw new Error("Serialized kernel basis does not reconstruct its artifact head");
|
|
1927
|
+
}
|
|
1928
|
+
return nativeRevision;
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
function validateSerializedKernelResult(input: {
|
|
1932
|
+
artifact: EditableArtifactSerialized;
|
|
1933
|
+
intent: ValidatedEditableArtifactMutationIntent;
|
|
1934
|
+
intentBytes: Uint8Array;
|
|
1935
|
+
requestHash: EditableArtifactReceipt["requestHash"];
|
|
1936
|
+
serverTransactionId: EditableArtifactReceipt["serverTransactionId"];
|
|
1937
|
+
priorNativeRevision: number;
|
|
1938
|
+
result: Awaited<ReturnType<AuthoritativeEditableArtifactKernelPort["applyTransaction"]>>;
|
|
1939
|
+
}): {
|
|
1940
|
+
committedTransactionBytes: Uint8Array;
|
|
1941
|
+
nativeReceiptBytes: Uint8Array;
|
|
1942
|
+
nativeRevision: number;
|
|
1943
|
+
commandCount: number;
|
|
1944
|
+
stateHash: EditableArtifact["stateHash"];
|
|
1945
|
+
kernelVersion: string;
|
|
1946
|
+
modelSchemaVersion: number;
|
|
1947
|
+
} {
|
|
1948
|
+
const { artifact, intent, result } = input;
|
|
1949
|
+
if (result.modality !== artifact.modality) {
|
|
1950
|
+
throw new EditableArtifactKernelContractError(
|
|
1951
|
+
"Kernel result modality does not match the durable artifact",
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1954
|
+
if (!(result.nativeReceiptBytes instanceof Uint8Array)) {
|
|
1955
|
+
throw new EditableArtifactKernelContractError(
|
|
1956
|
+
"Kernel returned an invalid native serialized receipt",
|
|
1957
|
+
);
|
|
1958
|
+
}
|
|
1959
|
+
assertPositiveSafeInteger(result.modelSchemaVersion, "kernel model schema version");
|
|
1960
|
+
if (result.modelSchemaVersion !== intent.modelSchemaVersion) {
|
|
1961
|
+
throw new EditableArtifactKernelContractError(
|
|
1962
|
+
"Kernel result model schema does not match the canonical intent",
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
try {
|
|
1966
|
+
assertBoundedKernelVersion(result.kernelVersion);
|
|
1967
|
+
} catch {
|
|
1968
|
+
throw new EditableArtifactKernelContractError(
|
|
1969
|
+
`Kernel version must contain 1-${EDITABLE_ARTIFACT_KERNEL_VERSION_MAX_BYTES} well-formed UTF-8 bytes`,
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
const stateHash = editableArtifactStateHash(result.resultingStateHash);
|
|
1973
|
+
const committedTransactionBytes = encodeEditableArtifactSerializedCommit({
|
|
1974
|
+
modality: artifact.modality,
|
|
1975
|
+
transactionId: input.serverTransactionId,
|
|
1976
|
+
parentHeadSequence: artifact.headSequence,
|
|
1977
|
+
resultHeadSequence: artifact.headSequence + 1,
|
|
1978
|
+
priorNativeRevision: input.priorNativeRevision,
|
|
1979
|
+
priorStateHash: artifact.stateHash,
|
|
1980
|
+
stateHash,
|
|
1981
|
+
intentBytes: input.intentBytes,
|
|
1982
|
+
nativeReceiptBytes: result.nativeReceiptBytes,
|
|
1983
|
+
});
|
|
1984
|
+
const summary = decodeEditableArtifactSerializedCommit(
|
|
1985
|
+
committedTransactionBytes,
|
|
1986
|
+
artifact.modality,
|
|
1987
|
+
);
|
|
1988
|
+
if (summary.requestHash !== input.requestHash) {
|
|
1989
|
+
throw new EditableArtifactKernelContractError(
|
|
1990
|
+
"Serialized commit does not bind the exact verified request hash",
|
|
1991
|
+
);
|
|
1992
|
+
}
|
|
1993
|
+
return Object.freeze({
|
|
1994
|
+
committedTransactionBytes,
|
|
1995
|
+
nativeReceiptBytes: summary.nativeReceiptBytes.slice(),
|
|
1996
|
+
nativeRevision: summary.nativeReceipt.revision,
|
|
1997
|
+
commandCount: summary.nativeReceipt.commandCount,
|
|
1998
|
+
stateHash,
|
|
1999
|
+
kernelVersion: result.kernelVersion,
|
|
2000
|
+
modelSchemaVersion: result.modelSchemaVersion,
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
function validateKernelResult(
|
|
2005
|
+
artifact: EditableArtifactSpreadsheet,
|
|
2006
|
+
actor: EditableArtifactActor,
|
|
2007
|
+
intent: EditableArtifactMutationIntent,
|
|
2008
|
+
resolvedCausalBase: EditableArtifactSpreadsheet["causalFrontier"],
|
|
2009
|
+
result: Awaited<ReturnType<AuthoritativeEditableArtifactKernelPort["applyTransaction"]>>,
|
|
2010
|
+
): {
|
|
2011
|
+
committedTransactionBytes: Uint8Array;
|
|
2012
|
+
serverTransactionId: EditableArtifactReceipt["serverTransactionId"];
|
|
2013
|
+
dot: EditableArtifactKernelOperation["dot"];
|
|
2014
|
+
operationIds: readonly EditableArtifactKernelOperation["operationId"][];
|
|
2015
|
+
priorStateHash: EditableArtifact["stateHash"];
|
|
2016
|
+
resultingCausalFrontier: EditableArtifactSpreadsheet["causalFrontier"];
|
|
2017
|
+
stateHash: EditableArtifact["stateHash"];
|
|
2018
|
+
kernelVersion: string;
|
|
2019
|
+
modelSchemaVersion: number;
|
|
2020
|
+
operationProtocolVersion: number;
|
|
2021
|
+
} {
|
|
2022
|
+
if (result.modality !== "spreadsheet") {
|
|
2023
|
+
throw new EditableArtifactKernelContractError(
|
|
2024
|
+
"Kernel result modality does not match the durable spreadsheet",
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2027
|
+
if (
|
|
2028
|
+
!(result.committedTransactionBytes instanceof Uint8Array) ||
|
|
2029
|
+
result.committedTransactionBytes.byteLength < 1 ||
|
|
2030
|
+
result.committedTransactionBytes.byteLength > EDITABLE_ARTIFACT_MAX_COMMITTED_TRANSACTION_BYTES
|
|
2031
|
+
) {
|
|
2032
|
+
throw new EditableArtifactKernelContractError(
|
|
2033
|
+
"Authoritative kernel returned an invalid canonical OGACO envelope",
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
// There is one source of truth: metadata is inspected from the exact whole
|
|
2037
|
+
// Rust-authored envelope. A separately supplied host summary could otherwise
|
|
2038
|
+
// disagree with the bytes ultimately persisted and replayed.
|
|
2039
|
+
const summary = decodeCommittedTransactionSummary(result.committedTransactionBytes);
|
|
2040
|
+
if (
|
|
2041
|
+
summary.operationIds.length < 1 ||
|
|
2042
|
+
summary.operationIds.length > EDITABLE_ARTIFACT_MAX_OPERATIONS_PER_TRANSACTION
|
|
2043
|
+
) {
|
|
2044
|
+
throw new EditableArtifactKernelContractError(
|
|
2045
|
+
"Authoritative kernel returned an invalid operation count",
|
|
2046
|
+
);
|
|
2047
|
+
}
|
|
2048
|
+
if (summary.operationProtocolVersion !== EDITABLE_ARTIFACT_OPERATION_PROTOCOL_VERSION) {
|
|
2049
|
+
throw new EditableArtifactKernelContractError(
|
|
2050
|
+
"Authoritative kernel returned an unsupported operation protocol version",
|
|
2051
|
+
);
|
|
2052
|
+
}
|
|
2053
|
+
assertPositiveSafeInteger(result.modelSchemaVersion, "kernel model schema version");
|
|
2054
|
+
if (result.modelSchemaVersion !== intent.modelSchemaVersion) {
|
|
2055
|
+
throw new EditableArtifactKernelContractError(
|
|
2056
|
+
"Kernel result model schema does not match the canonical intent",
|
|
2057
|
+
);
|
|
2058
|
+
}
|
|
2059
|
+
try {
|
|
2060
|
+
assertBoundedKernelVersion(result.kernelVersion);
|
|
2061
|
+
} catch {
|
|
2062
|
+
throw new EditableArtifactKernelContractError(
|
|
2063
|
+
`Kernel version must contain 1-${EDITABLE_ARTIFACT_KERNEL_VERSION_MAX_BYTES} well-formed UTF-8 bytes`,
|
|
2064
|
+
);
|
|
2065
|
+
}
|
|
2066
|
+
const serverTransactionId = editableArtifactTransactionId(summary.transactionId);
|
|
2067
|
+
const priorStateHash = editableArtifactStateHash(summary.priorStateHash);
|
|
2068
|
+
if (priorStateHash !== artifact.stateHash) {
|
|
2069
|
+
throw new EditableArtifactKernelContractError(
|
|
2070
|
+
"Kernel committed transaction prior state does not match its authoritative basis",
|
|
2071
|
+
);
|
|
2072
|
+
}
|
|
2073
|
+
const stateHash = editableArtifactStateHash(summary.stateHash);
|
|
2074
|
+
const normalizedResolvedBase = editableArtifactCausalFrontier(
|
|
2075
|
+
summary.resolvedCausalBase.map((entry) => ({
|
|
2076
|
+
replicaId: editableArtifactReplicaId(entry.replicaId),
|
|
2077
|
+
counter: entry.counter,
|
|
2078
|
+
})),
|
|
2079
|
+
);
|
|
2080
|
+
if (!causalFrontiersEqual(normalizedResolvedBase, resolvedCausalBase)) {
|
|
2081
|
+
throw new EditableArtifactKernelContractError(
|
|
2082
|
+
"Kernel committed transaction resolved base does not match server authority",
|
|
2083
|
+
);
|
|
2084
|
+
}
|
|
2085
|
+
const resultingCausalFrontier = editableArtifactCausalFrontier(
|
|
2086
|
+
summary.resultingCausalFrontier.map((entry) => ({
|
|
2087
|
+
replicaId: editableArtifactReplicaId(entry.replicaId),
|
|
2088
|
+
counter: entry.counter,
|
|
2089
|
+
})),
|
|
2090
|
+
);
|
|
2091
|
+
const dotFrontier = editableArtifactCausalFrontier([
|
|
2092
|
+
{
|
|
2093
|
+
replicaId: editableArtifactReplicaId(summary.dot.replicaId),
|
|
2094
|
+
counter: summary.dot.counter,
|
|
2095
|
+
},
|
|
2096
|
+
]);
|
|
2097
|
+
const dot = dotFrontier[0]!;
|
|
2098
|
+
if (dot.replicaId !== actor.replicaId) {
|
|
2099
|
+
throw new EditableArtifactKernelContractError(
|
|
2100
|
+
"Kernel transaction dot is not owned by the submitting replica",
|
|
2101
|
+
);
|
|
2102
|
+
}
|
|
2103
|
+
if (dot.counter !== intent.replicaCounter) {
|
|
2104
|
+
throw new EditableArtifactKernelContractError(
|
|
2105
|
+
"Kernel transaction dot does not match the authored replica counter",
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
const seenOperationIds = new Set<string>();
|
|
2109
|
+
const operationIds = summary.operationIds.map((rawOperationId) => {
|
|
2110
|
+
const operationId = editableArtifactOperationId(rawOperationId);
|
|
2111
|
+
if (seenOperationIds.has(operationId)) {
|
|
2112
|
+
throw new EditableArtifactKernelContractError("Kernel returned duplicate operation ids");
|
|
2113
|
+
}
|
|
2114
|
+
seenOperationIds.add(operationId);
|
|
2115
|
+
return operationId;
|
|
2116
|
+
});
|
|
2117
|
+
const expectedFrontier = mergeCausalFrontiers(
|
|
2118
|
+
artifact.causalFrontier,
|
|
2119
|
+
editableArtifactCausalFrontier([
|
|
2120
|
+
{ replicaId: actor.replicaId, counter: intent.replicaCounter },
|
|
2121
|
+
]),
|
|
2122
|
+
);
|
|
2123
|
+
if (!causalFrontiersEqual(resultingCausalFrontier, expectedFrontier)) {
|
|
2124
|
+
throw new EditableArtifactKernelContractError(
|
|
2125
|
+
"Kernel resulting frontier does not exactly match committed operation dots",
|
|
2126
|
+
);
|
|
2127
|
+
}
|
|
2128
|
+
return Object.freeze({
|
|
2129
|
+
committedTransactionBytes: result.committedTransactionBytes.slice(),
|
|
2130
|
+
serverTransactionId,
|
|
2131
|
+
dot,
|
|
2132
|
+
operationIds: Object.freeze(operationIds),
|
|
2133
|
+
priorStateHash,
|
|
2134
|
+
resultingCausalFrontier,
|
|
2135
|
+
stateHash,
|
|
2136
|
+
kernelVersion: result.kernelVersion,
|
|
2137
|
+
modelSchemaVersion: result.modelSchemaVersion,
|
|
2138
|
+
operationProtocolVersion: summary.operationProtocolVersion,
|
|
2139
|
+
});
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
function transactionOutboxRecord(
|
|
2143
|
+
outboxId: ReturnType<typeof editableArtifactOutboxId>,
|
|
2144
|
+
receipt: ApplyEditableArtifactTransactionResult["receipt"],
|
|
2145
|
+
): EditableArtifactLiveOutboxRecord {
|
|
2146
|
+
const event =
|
|
2147
|
+
receipt.modality === "spreadsheet"
|
|
2148
|
+
? Object.freeze({
|
|
2149
|
+
kind: "transaction_committed" as const,
|
|
2150
|
+
schemaVersion: 1 as const,
|
|
2151
|
+
scope: receipt.scope,
|
|
2152
|
+
artifactId: receipt.artifactId,
|
|
2153
|
+
modality: receipt.modality,
|
|
2154
|
+
serverTransactionId: receipt.serverTransactionId,
|
|
2155
|
+
sequenceStart: receipt.sequenceStart,
|
|
2156
|
+
sequenceEnd: receipt.sequenceEnd,
|
|
2157
|
+
stateHash: receipt.stateHash,
|
|
2158
|
+
operationProtocolVersion: receipt.operationProtocolVersion,
|
|
2159
|
+
committedAt: receipt.committedAt,
|
|
2160
|
+
})
|
|
2161
|
+
: Object.freeze({
|
|
2162
|
+
kind: "transaction_committed" as const,
|
|
2163
|
+
schemaVersion: 1 as const,
|
|
2164
|
+
scope: receipt.scope,
|
|
2165
|
+
artifactId: receipt.artifactId,
|
|
2166
|
+
modality: receipt.modality,
|
|
2167
|
+
serverTransactionId: receipt.serverTransactionId,
|
|
2168
|
+
sequenceStart: receipt.sequenceStart,
|
|
2169
|
+
sequenceEnd: receipt.sequenceEnd,
|
|
2170
|
+
stateHash: receipt.stateHash,
|
|
2171
|
+
commitProtocolVersion: receipt.commitProtocolVersion,
|
|
2172
|
+
committedAt: receipt.committedAt,
|
|
2173
|
+
});
|
|
2174
|
+
return Object.freeze({
|
|
2175
|
+
outboxId,
|
|
2176
|
+
event,
|
|
2177
|
+
state: "pending",
|
|
2178
|
+
attemptCount: 0,
|
|
2179
|
+
leaseOwner: null,
|
|
2180
|
+
leaseExpiresAt: null,
|
|
2181
|
+
nextAttemptAt: receipt.committedAt,
|
|
2182
|
+
lastErrorCode: null,
|
|
2183
|
+
publishedAt: null,
|
|
2184
|
+
deadLetteredAt: null,
|
|
2185
|
+
createdAt: receipt.committedAt,
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
function validateSnapshotRequest(
|
|
2190
|
+
request: PublishEditableArtifactSnapshotRequest,
|
|
2191
|
+
): PublishEditableArtifactSnapshotRequest {
|
|
2192
|
+
const record = plainDataRecord(request, "snapshot request");
|
|
2193
|
+
const rawModality = dataProperty(record, "modality", true);
|
|
2194
|
+
if (
|
|
2195
|
+
rawModality !== "spreadsheet" &&
|
|
2196
|
+
rawModality !== "document" &&
|
|
2197
|
+
rawModality !== "presentation"
|
|
2198
|
+
) {
|
|
2199
|
+
throw new TypeError("Snapshot modality is invalid");
|
|
2200
|
+
}
|
|
2201
|
+
rejectUnknownKeys(
|
|
2202
|
+
record,
|
|
2203
|
+
rawModality === "spreadsheet"
|
|
2204
|
+
? [
|
|
2205
|
+
"modality",
|
|
2206
|
+
"snapshotId",
|
|
2207
|
+
"blobReference",
|
|
2208
|
+
"byteSize",
|
|
2209
|
+
"contentHash",
|
|
2210
|
+
"mimeType",
|
|
2211
|
+
"coveredHeadSequence",
|
|
2212
|
+
"coveredCausalFrontier",
|
|
2213
|
+
"stateHash",
|
|
2214
|
+
"modelSchemaVersion",
|
|
2215
|
+
"operationProtocolVersion",
|
|
2216
|
+
"kernelVersion",
|
|
2217
|
+
"crdtStateVersion",
|
|
2218
|
+
"verifiedAt",
|
|
2219
|
+
]
|
|
2220
|
+
: [
|
|
2221
|
+
"modality",
|
|
2222
|
+
"snapshotId",
|
|
2223
|
+
"blobReference",
|
|
2224
|
+
"byteSize",
|
|
2225
|
+
"contentHash",
|
|
2226
|
+
"mimeType",
|
|
2227
|
+
"coveredHeadSequence",
|
|
2228
|
+
"stateHash",
|
|
2229
|
+
"modelSchemaVersion",
|
|
2230
|
+
"kernelVersion",
|
|
2231
|
+
"nativeRevision",
|
|
2232
|
+
"verifiedAt",
|
|
2233
|
+
],
|
|
2234
|
+
"snapshot request",
|
|
2235
|
+
);
|
|
2236
|
+
const rawSnapshotId = dataProperty(record, "snapshotId", true);
|
|
2237
|
+
const rawBlobReference = dataProperty(record, "blobReference", true);
|
|
2238
|
+
const rawByteSize = dataProperty(record, "byteSize", true);
|
|
2239
|
+
const rawContentHash = dataProperty(record, "contentHash", true);
|
|
2240
|
+
const rawMimeType = dataProperty(record, "mimeType", true);
|
|
2241
|
+
const rawCoveredHeadSequence = dataProperty(record, "coveredHeadSequence", true);
|
|
2242
|
+
const rawStateHash = dataProperty(record, "stateHash", true);
|
|
2243
|
+
const rawModelSchemaVersion = dataProperty(record, "modelSchemaVersion", true);
|
|
2244
|
+
const rawKernelVersion = dataProperty(record, "kernelVersion", true);
|
|
2245
|
+
const rawVerifiedAt = dataProperty(record, "verifiedAt", true);
|
|
2246
|
+
if (
|
|
2247
|
+
typeof rawSnapshotId !== "string" ||
|
|
2248
|
+
typeof rawBlobReference !== "string" ||
|
|
2249
|
+
typeof rawByteSize !== "number" ||
|
|
2250
|
+
typeof rawContentHash !== "string" ||
|
|
2251
|
+
typeof rawCoveredHeadSequence !== "number" ||
|
|
2252
|
+
typeof rawStateHash !== "string" ||
|
|
2253
|
+
typeof rawModelSchemaVersion !== "number" ||
|
|
2254
|
+
typeof rawKernelVersion !== "string" ||
|
|
2255
|
+
typeof rawVerifiedAt !== "string"
|
|
2256
|
+
) {
|
|
2257
|
+
throw new TypeError("Snapshot request fields are malformed");
|
|
2258
|
+
}
|
|
2259
|
+
if (rawMimeType !== "application/vnd.opengeni.editable-artifact-snapshot") {
|
|
2260
|
+
throw new TypeError("Snapshot MIME type is unsupported");
|
|
2261
|
+
}
|
|
2262
|
+
const snapshotId = editableArtifactSnapshotId(rawSnapshotId);
|
|
2263
|
+
assertBoundedOpaqueReference(rawBlobReference, "snapshot blob reference");
|
|
2264
|
+
assertPositiveSafeInteger(rawByteSize, "snapshot byte size");
|
|
2265
|
+
const contentHash = editableArtifactContentHash(rawContentHash);
|
|
2266
|
+
assertNonnegativeSafeInteger(rawCoveredHeadSequence, "snapshot covered head sequence");
|
|
2267
|
+
const stateHash = editableArtifactStateHash(rawStateHash);
|
|
2268
|
+
assertPositiveSafeInteger(rawModelSchemaVersion, "snapshot model schema version");
|
|
2269
|
+
assertBoundedKernelVersion(rawKernelVersion);
|
|
2270
|
+
assertIsoTimestamp(rawVerifiedAt, "snapshot verified timestamp");
|
|
2271
|
+
const common = {
|
|
2272
|
+
modality: rawModality,
|
|
2273
|
+
snapshotId,
|
|
2274
|
+
blobReference: rawBlobReference,
|
|
2275
|
+
byteSize: rawByteSize,
|
|
2276
|
+
contentHash,
|
|
2277
|
+
mimeType: "application/vnd.opengeni.editable-artifact-snapshot",
|
|
2278
|
+
coveredHeadSequence: rawCoveredHeadSequence,
|
|
2279
|
+
stateHash,
|
|
2280
|
+
modelSchemaVersion: rawModelSchemaVersion,
|
|
2281
|
+
kernelVersion: rawKernelVersion,
|
|
2282
|
+
verifiedAt: rawVerifiedAt,
|
|
2283
|
+
} as const;
|
|
2284
|
+
if (rawModality === "spreadsheet") {
|
|
2285
|
+
const rawCoveredCausalFrontier = dataProperty(record, "coveredCausalFrontier", true);
|
|
2286
|
+
const rawOperationProtocolVersion = dataProperty(record, "operationProtocolVersion", true);
|
|
2287
|
+
const rawCrdtStateVersion = dataProperty(record, "crdtStateVersion", true);
|
|
2288
|
+
if (
|
|
2289
|
+
!Array.isArray(rawCoveredCausalFrontier) ||
|
|
2290
|
+
typeof rawOperationProtocolVersion !== "number" ||
|
|
2291
|
+
typeof rawCrdtStateVersion !== "number"
|
|
2292
|
+
) {
|
|
2293
|
+
throw new TypeError("Spreadsheet snapshot fields are malformed");
|
|
2294
|
+
}
|
|
2295
|
+
assertPositiveSafeInteger(rawOperationProtocolVersion, "snapshot operation protocol version");
|
|
2296
|
+
assertPositiveSafeInteger(rawCrdtStateVersion, "snapshot CRDT state version");
|
|
2297
|
+
return Object.freeze({
|
|
2298
|
+
...common,
|
|
2299
|
+
modality: "spreadsheet",
|
|
2300
|
+
coveredCausalFrontier: editableArtifactCausalFrontier(
|
|
2301
|
+
rawCoveredCausalFrontier as EditableArtifactCausalFrontier,
|
|
2302
|
+
),
|
|
2303
|
+
operationProtocolVersion: rawOperationProtocolVersion,
|
|
2304
|
+
crdtStateVersion: rawCrdtStateVersion,
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2307
|
+
const rawNativeRevision = dataProperty(record, "nativeRevision", true);
|
|
2308
|
+
if (typeof rawNativeRevision !== "number") {
|
|
2309
|
+
throw new TypeError("Serialized snapshot native revision is malformed");
|
|
2310
|
+
}
|
|
2311
|
+
assertNonnegativeSafeInteger(rawNativeRevision, "snapshot native revision");
|
|
2312
|
+
return Object.freeze({
|
|
2313
|
+
...common,
|
|
2314
|
+
modality: rawModality,
|
|
2315
|
+
nativeRevision: rawNativeRevision,
|
|
2316
|
+
});
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
function sameSnapshotRequest(
|
|
2320
|
+
existing: EditableArtifactSnapshotMetadata,
|
|
2321
|
+
candidate: PublishEditableArtifactSnapshotRequest,
|
|
2322
|
+
): boolean {
|
|
2323
|
+
if (
|
|
2324
|
+
existing.modality !== candidate.modality ||
|
|
2325
|
+
!(
|
|
2326
|
+
existing.snapshotId === candidate.snapshotId &&
|
|
2327
|
+
existing.blobReference === candidate.blobReference &&
|
|
2328
|
+
existing.byteSize === candidate.byteSize &&
|
|
2329
|
+
existing.contentHash === candidate.contentHash &&
|
|
2330
|
+
existing.mimeType === candidate.mimeType &&
|
|
2331
|
+
existing.coveredHeadSequence === candidate.coveredHeadSequence &&
|
|
2332
|
+
existing.stateHash === candidate.stateHash &&
|
|
2333
|
+
existing.modelSchemaVersion === candidate.modelSchemaVersion &&
|
|
2334
|
+
existing.kernelVersion === candidate.kernelVersion &&
|
|
2335
|
+
existing.verifiedAt === candidate.verifiedAt
|
|
2336
|
+
)
|
|
2337
|
+
) {
|
|
2338
|
+
return false;
|
|
2339
|
+
}
|
|
2340
|
+
if (existing.modality === "spreadsheet" && candidate.modality === "spreadsheet") {
|
|
2341
|
+
return (
|
|
2342
|
+
causalFrontiersEqual(existing.coveredCausalFrontier, candidate.coveredCausalFrontier) &&
|
|
2343
|
+
existing.operationProtocolVersion === candidate.operationProtocolVersion &&
|
|
2344
|
+
existing.crdtStateVersion === candidate.crdtStateVersion
|
|
2345
|
+
);
|
|
2346
|
+
}
|
|
2347
|
+
return (
|
|
2348
|
+
existing.modality !== "spreadsheet" &&
|
|
2349
|
+
candidate.modality !== "spreadsheet" &&
|
|
2350
|
+
existing.nativeRevision === candidate.nativeRevision
|
|
2351
|
+
);
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
function snapshotOutboxRecord(
|
|
2355
|
+
outboxId: ReturnType<typeof editableArtifactOutboxId>,
|
|
2356
|
+
snapshot: EditableArtifactSnapshotMetadata,
|
|
2357
|
+
): EditableArtifactLiveOutboxRecord {
|
|
2358
|
+
const event =
|
|
2359
|
+
snapshot.modality === "spreadsheet"
|
|
2360
|
+
? Object.freeze({
|
|
2361
|
+
kind: "snapshot_published" as const,
|
|
2362
|
+
schemaVersion: 1 as const,
|
|
2363
|
+
scope: snapshot.scope,
|
|
2364
|
+
artifactId: snapshot.artifactId,
|
|
2365
|
+
modality: snapshot.modality,
|
|
2366
|
+
snapshotId: snapshot.snapshotId,
|
|
2367
|
+
coveredHeadSequence: snapshot.coveredHeadSequence,
|
|
2368
|
+
stateHash: snapshot.stateHash,
|
|
2369
|
+
operationProtocolVersion: snapshot.operationProtocolVersion,
|
|
2370
|
+
publishedAt: snapshot.publishedAt,
|
|
2371
|
+
})
|
|
2372
|
+
: Object.freeze({
|
|
2373
|
+
kind: "snapshot_published" as const,
|
|
2374
|
+
schemaVersion: 1 as const,
|
|
2375
|
+
scope: snapshot.scope,
|
|
2376
|
+
artifactId: snapshot.artifactId,
|
|
2377
|
+
modality: snapshot.modality,
|
|
2378
|
+
snapshotId: snapshot.snapshotId,
|
|
2379
|
+
coveredHeadSequence: snapshot.coveredHeadSequence,
|
|
2380
|
+
stateHash: snapshot.stateHash,
|
|
2381
|
+
publishedAt: snapshot.publishedAt,
|
|
2382
|
+
});
|
|
2383
|
+
return Object.freeze({
|
|
2384
|
+
outboxId,
|
|
2385
|
+
event,
|
|
2386
|
+
state: "pending",
|
|
2387
|
+
attemptCount: 0,
|
|
2388
|
+
leaseOwner: null,
|
|
2389
|
+
leaseExpiresAt: null,
|
|
2390
|
+
nextAttemptAt: snapshot.publishedAt,
|
|
2391
|
+
lastErrorCode: null,
|
|
2392
|
+
publishedAt: null,
|
|
2393
|
+
deadLetteredAt: null,
|
|
2394
|
+
createdAt: snapshot.publishedAt,
|
|
2395
|
+
});
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
function canonicalNow(clock: EditableArtifactClockPort): string {
|
|
2399
|
+
const now = clock.now();
|
|
2400
|
+
if (Number.isNaN(now.getTime())) throw new TypeError("Artifact clock returned an invalid date");
|
|
2401
|
+
return now.toISOString();
|
|
2402
|
+
}
|