@powerhousedao/reactor 6.2.3-dev.4 → 6.2.3-dev.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/{build-worker-executor-DnU3ZU4H.js → build-worker-executor-Bonu2jUB.js} +30 -8
  2. package/dist/build-worker-executor-Bonu2jUB.js.map +1 -0
  3. package/dist/{document-indexer-C5Gsa1B3.js → document-indexer-B3yKal-y.js} +981 -104
  4. package/dist/document-indexer-B3yKal-y.js.map +1 -0
  5. package/dist/entry.d.ts +2 -1
  6. package/dist/entry.js +13 -6
  7. package/dist/entry.js.map +1 -1
  8. package/dist/{errors-D3S6Eysd.js → errors-BM_ULVWd.js} +9 -1
  9. package/dist/{errors-D3S6Eysd.js.map → errors-BM_ULVWd.js.map} +1 -1
  10. package/dist/{drive-container-types-yZrksiJR.js → forwarding-logger-BKDP4JnD.js} +2650 -367
  11. package/dist/forwarding-logger-BKDP4JnD.js.map +1 -0
  12. package/dist/index.d.ts +4819 -3185
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +4146 -892
  15. package/dist/index.js.map +1 -1
  16. package/dist/projection-entry.d.ts +2 -1
  17. package/dist/projection-entry.js +99 -21
  18. package/dist/projection-entry.js.map +1 -1
  19. package/dist/{projection-shard-manager-CPQc5XHL.js → projection-shard-manager-BZPCQK-4.js} +24 -4
  20. package/dist/projection-shard-manager-BZPCQK-4.js.map +1 -0
  21. package/dist/{projection-worker-wI4PwcV2.js → projection-worker-D8uNFdwX.js} +1 -1
  22. package/dist/{projection-worker-wI4PwcV2.js.map → projection-worker-D8uNFdwX.js.map} +1 -1
  23. package/dist/{transport-CuogVKN_.js → transport-B3G50KVd.js} +1 -1
  24. package/dist/{transport-CuogVKN_.js.map → transport-B3G50KVd.js.map} +1 -1
  25. package/dist/{transport-ByGviWdZ.js → transport-CZDHzfvy.js} +1 -1
  26. package/dist/{transport-ByGviWdZ.js.map → transport-CZDHzfvy.js.map} +1 -1
  27. package/dist/{worker-handle-CrERzl8s.js → worker-handle-CefOwokd.js} +30 -17
  28. package/dist/worker-handle-CefOwokd.js.map +1 -0
  29. package/package.json +20 -10
  30. package/dist/build-worker-executor-DnU3ZU4H.js.map +0 -1
  31. package/dist/document-indexer-C5Gsa1B3.js.map +0 -1
  32. package/dist/drive-container-types-yZrksiJR.js.map +0 -1
  33. package/dist/error-info-Cpu4OY3o.js +0 -62
  34. package/dist/error-info-Cpu4OY3o.js.map +0 -1
  35. package/dist/forwarding-logger-BBkMSxuJ.js +0 -85
  36. package/dist/forwarding-logger-BBkMSxuJ.js.map +0 -1
  37. package/dist/projection-shard-manager-CPQc5XHL.js.map +0 -1
  38. package/dist/types-DMKLa0Ok.js +0 -33
  39. package/dist/types-DMKLa0Ok.js.map +0 -1
  40. package/dist/worker-HPysBfhx.js +0 -22
  41. package/dist/worker-HPysBfhx.js.map +0 -1
  42. package/dist/worker-handle-CrERzl8s.js.map +0 -1
@@ -1,7 +1,6 @@
1
- import { n as ReactorEventTypes, t as EventBusAggregateError } from "./types-DMKLa0Ok.js";
2
- import { AUTH_ACTION_TYPES, AUTH_DENIED_BY_GRANT_REASON, AUTH_NO_GRANT_REASON, AUTH_VERSION_UNSUPPORTED_REASON, DOCUMENT_DELETED_REASON, DowngradeNotSupportedError as DowngradeNotSupportedError$1, appendWithoutApplying, applyAuthAction, applyDeleteDocumentAction, applyDeleteDocumentAction as applyDeleteDocumentAction$1, applyUpgradeDocumentAction, applyUpgradeDocumentAction as applyUpgradeDocumentAction$1, baseReducerVersion, createPresignedHeader, decide, defaultBaseState, deriveOperationId, evaluate, garbageCollect, groupDocumentType, groupMembershipActionTypes, hashDocumentStateForScope, isDenied, isUndoRedo, mentionedGroupIds, normalizeDocumentModelVersion, referencedGroupIds, sortOperations } from "@powerhousedao/shared/document-model";
1
+ import { AUTH_ACTION_TYPES, AUTH_DENIED_BY_GRANT_REASON, AUTH_NO_GRANT_REASON, AUTH_VERSION_UNSUPPORTED_REASON, DOCUMENT_DELETED_REASON, DOCUMENT_SCOPE_ACTION_TYPES, DowngradeNotSupportedError as DowngradeNotSupportedError$1, PEER_CAPABILITIES, PURGE_DOCUMENT, actionSignerIdentity, actionSigningTarget, appendWithoutApplying, applyAuthAction, applyDeleteDocumentAction, applyDeleteDocumentAction as applyDeleteDocumentAction$1, applyUpgradeDocumentAction, applyUpgradeDocumentAction as applyUpgradeDocumentAction$1, base58Decode, baseReducerVersion, buildOperationSignatureMessage, buildOperationSignatureParams, canonicalJson, createDocumentState, createPresignedHeader, decide, defaultBaseState, deriveDocumentId, deriveOperationId, evaluate, garbageCollect, groupDocumentType, groupMembershipActionTypes, hashActionV2, hashBrowser, hashDocumentStateForScope, isDenied, isDerivedDocumentId, isPurgeMarker, isUndoRedo, isV2ActionHash, localSupports, mentionedGroupIds, normalizeDocumentModelVersion, operationOutcome, purgeDocumentAction, purgeMarkerOperation, purgedProtocolVersions, referencedGroupIds, signaturePolicyOf, sortOperations, targetDocumentId, v2TupleProblem, withProtocolVersions } from "@powerhousedao/shared/document-model";
3
2
  import { v4 } from "uuid";
4
- import { Migrator, sql } from "kysely";
3
+ import { Migrator, TableNode, sql } from "kysely";
5
4
  //#region \0rolldown/runtime.js
6
5
  var __defProp = Object.defineProperty;
7
6
  var __exportAll = (all, no_symbols) => {
@@ -52,6 +51,38 @@ function parsePagingOptions(paging, defaultLimit) {
52
51
  };
53
52
  }
54
53
  //#endregion
54
+ //#region src/events/types.ts
55
+ /**
56
+ * Custom error class that aggregates multiple errors from event subscribers.
57
+ */
58
+ var EventBusAggregateError = class extends Error {
59
+ errors;
60
+ constructor(errors) {
61
+ const message = `EventBus emit failed with ${errors.length} error(s): ${errors.map((e) => {
62
+ if (e && typeof e === "object" && "message" in e) return e.message;
63
+ return String(e);
64
+ }).join("; ")}`;
65
+ super(message);
66
+ this.name = "EventBusAggregateError";
67
+ this.errors = errors;
68
+ }
69
+ };
70
+ /**
71
+ * Event types for reactor lifecycle events.
72
+ */
73
+ const ReactorEventTypes = {
74
+ JOB_PENDING: 10001,
75
+ JOB_RUNNING: 10002,
76
+ JOB_WRITE_READY: 10003,
77
+ JOB_READ_READY: 10004,
78
+ JOB_FAILED: 10005,
79
+ READMODEL_BATCH_COMPLETED: 10006,
80
+ READMODEL_INDEXED: 10007,
81
+ MODEL_LOADED: 10008,
82
+ SIGNATURE_REFUSED: 10009,
83
+ CATCHUP_SWEPT: 10010
84
+ };
85
+ //#endregion
55
86
  //#region src/shared/errors.ts
56
87
  /**
57
88
  * Error thrown when attempting to access a deleted document.
@@ -143,6 +174,23 @@ var InvalidOperationTimestampError = class InvalidOperationTimestampError extend
143
174
  return Error.isError(error) && error.name === "InvalidOperationTimestampError";
144
175
  }
145
176
  };
177
+ /** A document requires a protocol version this reactor does not run. Terminal. */
178
+ var UnsupportedProtocolVersionError = class UnsupportedProtocolVersionError extends Error {
179
+ documentId;
180
+ protocol;
181
+ version;
182
+ constructor(documentId, protocol, version) {
183
+ super(`Document ${documentId} requires ${protocol} ${version}, which this reactor does not support`);
184
+ this.name = "UnsupportedProtocolVersionError";
185
+ this.documentId = documentId;
186
+ this.protocol = protocol;
187
+ this.version = version;
188
+ Error.captureStackTrace(this, UnsupportedProtocolVersionError);
189
+ }
190
+ static isError(error) {
191
+ return Error.isError(error) && error.name === "UnsupportedProtocolVersionError";
192
+ }
193
+ };
146
194
  /**
147
195
  * A load would move more operations than the bound allows, indicating a real
148
196
  * divergence between local and incoming history. Counts only first-time moves,
@@ -167,19 +215,22 @@ var ExcessiveReshuffleError = class ExcessiveReshuffleError extends Error {
167
215
  return Error.isError(error) && error.name === "ExcessiveReshuffleError";
168
216
  }
169
217
  };
170
- /**
171
- * Error thrown when an operation has an invalid signature.
172
- */
218
+ /** The message carries the code: only name and message cross the queue. */
173
219
  var InvalidSignatureError = class InvalidSignatureError extends Error {
174
220
  documentId;
221
+ code;
175
222
  reason;
176
- constructor(documentId, reason) {
177
- super(`Invalid signature in document ${documentId}: ${reason}`);
223
+ constructor(documentId, code, reason) {
224
+ super(`Invalid signature in document ${documentId} [${code}]: ${reason}`);
178
225
  this.name = "InvalidSignatureError";
179
226
  this.documentId = documentId;
227
+ this.code = code;
180
228
  this.reason = reason;
181
229
  Error.captureStackTrace(this, InvalidSignatureError);
182
230
  }
231
+ static isError(error) {
232
+ return Error.isError(error) && error.name === "InvalidSignatureError";
233
+ }
183
234
  };
184
235
  /**
185
236
  * An UPGRADE_DOCUMENT action's preconditions (fromVersion and the per-scope
@@ -220,8 +271,81 @@ var DocumentNotFoundError = class DocumentNotFoundError extends Error {
220
271
  this.documentId = documentId;
221
272
  Error.captureStackTrace(this, DocumentNotFoundError);
222
273
  }
274
+ /** Also true for DocumentPurgedError: only the name crosses the queue. */
275
+ static isError(error) {
276
+ return Error.isError(error) && (error.name === "DocumentNotFoundError" || error.name === "DocumentPurgedError");
277
+ }
278
+ };
279
+ /** The document was purged; readers treat it as absent. Terminal. */
280
+ var DocumentPurgedError = class DocumentPurgedError extends DocumentNotFoundError {
281
+ purged = true;
282
+ constructor(documentId, message) {
283
+ super(documentId, message ?? `Document ${documentId} was purged`);
284
+ this.name = "DocumentPurgedError";
285
+ Error.captureStackTrace(this, DocumentPurgedError);
286
+ }
287
+ static isError(error) {
288
+ return Error.isError(error) && error.name === "DocumentPurgedError";
289
+ }
290
+ };
291
+ /** A purge was asked for a document that is not deleted. Terminal. */
292
+ var DocumentNotDeletedError = class DocumentNotDeletedError extends Error {
293
+ documentId;
294
+ constructor(documentId, message) {
295
+ super(message ?? `Document ${documentId} is not deleted`);
296
+ this.name = "DocumentNotDeletedError";
297
+ this.documentId = documentId;
298
+ Error.captureStackTrace(this, DocumentNotDeletedError);
299
+ }
300
+ static isError(error) {
301
+ return Error.isError(error) && error.name === "DocumentNotDeletedError";
302
+ }
303
+ };
304
+ /** A surviving document's accepted auth history names the group. Terminal. */
305
+ var GroupInUseError = class GroupInUseError extends Error {
306
+ groupId;
307
+ referencingDocumentIds;
308
+ constructor(groupId, referencingDocumentIds) {
309
+ super(`Group ${groupId} cannot be purged: the auth history of ${referencingDocumentIds.join(", ")} names it`);
310
+ this.name = "GroupInUseError";
311
+ this.groupId = groupId;
312
+ this.referencingDocumentIds = referencingDocumentIds;
313
+ Error.captureStackTrace(this, GroupInUseError);
314
+ }
315
+ static isError(error) {
316
+ return Error.isError(error) && error.name === "GroupInUseError";
317
+ }
318
+ };
319
+ /** A purge above maxPurgeOperations without allowLarge. Terminal. */
320
+ var PurgeTooLargeError = class PurgeTooLargeError extends Error {
321
+ documentId;
322
+ operationCount;
323
+ maxPurgeOperations;
324
+ constructor(documentId, operationCount, maxPurgeOperations) {
325
+ super(`Document ${documentId} has ${operationCount} operations, above the purge limit of ${maxPurgeOperations}; request it with allowLarge`);
326
+ this.name = "PurgeTooLargeError";
327
+ this.documentId = documentId;
328
+ this.operationCount = operationCount;
329
+ this.maxPurgeOperations = maxPurgeOperations;
330
+ Error.captureStackTrace(this, PurgeTooLargeError);
331
+ }
332
+ static isError(error) {
333
+ return Error.isError(error) && error.name === "PurgeTooLargeError";
334
+ }
335
+ };
336
+ /** A regular write carried an action type only the reactor issues. Terminal. */
337
+ var ReservedActionError = class ReservedActionError extends Error {
338
+ documentId;
339
+ actionType;
340
+ constructor(documentId, actionType) {
341
+ super(`Action ${actionType} is reserved and cannot be submitted to document ${documentId}`);
342
+ this.name = "ReservedActionError";
343
+ this.documentId = documentId;
344
+ this.actionType = actionType;
345
+ Error.captureStackTrace(this, ReservedActionError);
346
+ }
223
347
  static isError(error) {
224
- return Error.isError(error) && error.name === "DocumentNotFoundError";
348
+ return Error.isError(error) && error.name === "ReservedActionError";
225
349
  }
226
350
  };
227
351
  /**
@@ -250,6 +374,46 @@ var AuthEnforcementDisabledError = class AuthEnforcementDisabledError extends Er
250
374
  return Error.isError(error) && error.name === "AuthEnforcementDisabledError";
251
375
  }
252
376
  };
377
+ /**
378
+ * Error thrown when a relationship edge an operation names does not exist.
379
+ *
380
+ * Detection is by `name`, not `instanceof`: the SharedWorker RPC boundary
381
+ * rebuilds a thrown error from `{ name, message, stack, cause }` alone
382
+ * (`reactor-browser/src/rpc/error-info.ts`), so the class identity is lost in
383
+ * transit.
384
+ */
385
+ var RelationshipNotFoundError = class RelationshipNotFoundError extends Error {
386
+ sourceId;
387
+ targetId;
388
+ relationshipType;
389
+ constructor(sourceId, targetId, relationshipType) {
390
+ super(`No ${relationshipType} relationship from ${sourceId} to ${targetId}`);
391
+ this.name = "RelationshipNotFoundError";
392
+ this.sourceId = sourceId;
393
+ this.targetId = targetId;
394
+ this.relationshipType = relationshipType;
395
+ Error.captureStackTrace(this, RelationshipNotFoundError);
396
+ }
397
+ static isError(error) {
398
+ return Error.isError(error) && error.name === "RelationshipNotFoundError";
399
+ }
400
+ };
401
+ /** The store holds documents at protocol versions this reactor does not run. */
402
+ var UnsupportedStoredProtocolError = class UnsupportedStoredProtocolError extends Error {
403
+ versions;
404
+ documents;
405
+ constructor(versions, documents) {
406
+ const named = versions.map(({ protocol, version }) => `${protocol} ${version}`).join(", ");
407
+ super(`${documents} stored document(s) require ${named}, which this reactor does not run. Start a build that runs them, or accept them read-only with withUnsupportedStoredDocuments("read-only")`);
408
+ this.name = "UnsupportedStoredProtocolError";
409
+ this.versions = versions;
410
+ this.documents = documents;
411
+ Error.captureStackTrace(this, UnsupportedStoredProtocolError);
412
+ }
413
+ static isError(error) {
414
+ return Error.isError(error) && error.name === "UnsupportedStoredProtocolError";
415
+ }
416
+ };
253
417
  //#endregion
254
418
  //#region src/storage/interfaces.ts
255
419
  /**
@@ -302,6 +466,7 @@ const APPEND_CONDITION_FAILED_PREFIX = "Append condition failed: ";
302
466
  * conflict, not a fault: the caller retries against the new stream heads.
303
467
  */
304
468
  var AppendConditionFailedError = class extends Error {
469
+ condition;
305
470
  constructor(condition) {
306
471
  const streams = condition.streams.map((s) => `${s.documentId}:${s.scope}:${s.branch}@${s.revision}`).join(", ");
307
472
  super(`${APPEND_CONDITION_FAILED_PREFIX}a read-set stream advanced [${streams}]`);
@@ -335,6 +500,52 @@ let DocumentExistence = /* @__PURE__ */ function(DocumentExistence) {
335
500
  return DocumentExistence;
336
501
  }({});
337
502
  //#endregion
503
+ //#region src/cache/operation-index-types.ts
504
+ const DRIVE_COLLECTION_PREFIX = "drive.";
505
+ /**
506
+ * Identifies the collection a remote synchronizes. Collections are drive-level
507
+ * abstractions (document-drive and reactor-drive), so a collection id is the
508
+ * drive document id plus the branch it scopes to rather than an opaque string.
509
+ *
510
+ * The canonical string form (`drive.${branch}.${driveId}`) is produced only by
511
+ * `key` and parsed only by `fromKey`; that string is the wire and storage
512
+ * representation and is byte-for-byte identical to the legacy
513
+ * `driveCollectionId(branch, driveId)` output, so existing `document_collections`
514
+ * rows and persisted remotes remain valid without migration.
515
+ */
516
+ var DriveCollectionId = class DriveCollectionId {
517
+ driveId;
518
+ branch;
519
+ constructor(driveId, branch) {
520
+ this.driveId = driveId;
521
+ this.branch = branch;
522
+ }
523
+ static forDrive(driveId, branch = "main") {
524
+ return new DriveCollectionId(driveId, branch);
525
+ }
526
+ /**
527
+ * The single deserializer for the wire/storage form. `branch` may contain
528
+ * dots, while `driveId` is a dot-free document id, so the drive id is the
529
+ * final dot-delimited segment.
530
+ */
531
+ static fromKey(key) {
532
+ if (!key.startsWith(DRIVE_COLLECTION_PREFIX)) throw new Error(`Unsupported collection id: ${key}`);
533
+ const rest = key.slice(6);
534
+ const lastDot = rest.lastIndexOf(".");
535
+ if (lastDot === -1 || lastDot === rest.length - 1) throw new Error(`Malformed drive collection id: ${key}`);
536
+ return new DriveCollectionId(rest.slice(lastDot + 1), rest.slice(0, lastDot));
537
+ }
538
+ get key() {
539
+ return `${DRIVE_COLLECTION_PREFIX}${this.branch}.${this.driveId}`;
540
+ }
541
+ toString() {
542
+ return this.key;
543
+ }
544
+ equals(other) {
545
+ return this.driveId === other.driveId && this.branch === other.branch;
546
+ }
547
+ };
548
+ //#endregion
338
549
  //#region src/decision/build-decision-model.ts
339
550
  /**
340
551
  * Reads each projection's stream through the supplied reader, recording the
@@ -352,9 +563,13 @@ async function buildDecisionModel(reader, definition, target, signal) {
352
563
  model[key] = (await readStream(reader, projection.query, reads, signal)).state;
353
564
  }
354
565
  const staticModel = { ...model };
566
+ const derived = [];
355
567
  for (const [key, projection] of projections) {
356
568
  if (typeof projection.query !== "function") continue;
357
- const queries = projection.query(staticModel);
569
+ derived.push([key, projection.query(staticModel)]);
570
+ }
571
+ if (derived.length > 0 && reader.fence) await reader.fence(derived.flatMap(([, queries]) => queries.map((q) => q.documentId)));
572
+ for (const [key, queries] of derived) {
358
573
  const value = {};
359
574
  for (const query of queries) {
360
575
  let read;
@@ -690,34 +905,13 @@ function selectDecisionModel(flags, registry) {
690
905
  //#endregion
691
906
  //#region src/executor/util.ts
692
907
  /** Actions the reactor reduces itself, onto the document scope. */
693
- const DOCUMENT_SCOPE_ACTIONS = new Set([
694
- "CREATE_DOCUMENT",
695
- "DELETE_DOCUMENT",
696
- "UPGRADE_DOCUMENT",
697
- "ADD_RELATIONSHIP",
698
- "REMOVE_RELATIONSHIP",
699
- "UPDATE_RELATIONSHIP"
700
- ]);
908
+ const DOCUMENT_SCOPE_ACTIONS = DOCUMENT_SCOPE_ACTION_TYPES;
701
909
  /**
702
910
  * `CREATE_DOCUMENT` is exempt by necessity: it runs before the document exists,
703
911
  * so building a decision model would throw and defer the job forever.
704
912
  */
705
913
  const GATED_DOCUMENT_ACTIONS = new Set([...DOCUMENT_SCOPE_ACTIONS].filter((type) => type !== "CREATE_DOCUMENT"));
706
914
  /**
707
- * The document a document-scope action writes to, which is not always the job's
708
- * own document: delete and upgrade name it in `input.documentId`, and the
709
- * relationship actions in `input.sourceId`. `execute` only checks that a batch
710
- * shares one scope, so a caller can submit an action whose target is a document
711
- * other than the one the job is keyed by. The policy gate has to follow the
712
- * action rather than the job, or it decides against a policy the caller may
713
- * control instead of the one guarding the write.
714
- */
715
- function targetDocumentId(action, fallback) {
716
- const input = action.input;
717
- if (action.type === "ADD_RELATIONSHIP" || action.type === "REMOVE_RELATIONSHIP" || action.type === "UPDATE_RELATIONSHIP") return typeof input?.sourceId === "string" && input.sourceId.length > 0 ? input.sourceId : fallback;
718
- return typeof input?.documentId === "string" && input.documentId.length > 0 ? input.documentId : fallback;
719
- }
720
- /**
721
915
  * Creates a PHDocument from a CREATE_DOCUMENT action input.
722
916
  * Reconstructs the document header and initializes the base state.
723
917
  *
@@ -896,6 +1090,129 @@ var TouchedStreams = class {
896
1090
  return this.streams.values();
897
1091
  }
898
1092
  };
1093
+ /**
1094
+ * The ids of the actions the caller handed to a job. Load and reevaluation
1095
+ * jobs write operations nobody submitted, so they report none.
1096
+ */
1097
+ function submittedActionIds(job) {
1098
+ return job.kind === "mutation" ? job.actions.map((action) => action.id) : [];
1099
+ }
1100
+ /**
1101
+ * Reports what became of each submitted action.
1102
+ *
1103
+ * A job's operations can include ones it only moved to a new index, so only
1104
+ * those carrying a submitted action are reported. Returns undefined when the
1105
+ * job submitted nothing, which keeps `JobInfo.result` null for the jobs that
1106
+ * have no caller to answer to.
1107
+ */
1108
+ function summarizeSubmittedActions(operations, submitted) {
1109
+ if (!submitted || submitted.length === 0) return;
1110
+ const ids = new Set(submitted);
1111
+ const actions = [];
1112
+ for (const { operation, context } of operations) {
1113
+ if (!ids.has(operation.action.id)) continue;
1114
+ actions.push({
1115
+ actionId: operation.action.id,
1116
+ scope: context.scope,
1117
+ index: operation.index,
1118
+ ...operationOutcome(operation)
1119
+ });
1120
+ }
1121
+ if (actions.length === 0) return;
1122
+ return {
1123
+ actions,
1124
+ allApplied: actions.every((action) => action.kind === "applied")
1125
+ };
1126
+ }
1127
+ /** Tombstones a job has read, each under the shared lock it took first. */
1128
+ var PurgeFence = class {
1129
+ locks;
1130
+ checked;
1131
+ purged;
1132
+ constructor(locks, checked, purged) {
1133
+ this.locks = locks;
1134
+ this.checked = checked;
1135
+ this.purged = purged;
1136
+ }
1137
+ /** Locks and reads an id the job start did not resolve. */
1138
+ async isPurged(documentId) {
1139
+ await this.isPurgedMany([documentId]);
1140
+ return this.purged.has(documentId);
1141
+ }
1142
+ /** Locks and reads the unresolved ids in one lock and one lookup. */
1143
+ async isPurgedMany(documentIds) {
1144
+ const unchecked = [...new Set(documentIds)].filter((id) => !this.checked.has(id));
1145
+ if (unchecked.length > 0) {
1146
+ await this.locks.shared(unchecked);
1147
+ const found = await this.locks.purged(unchecked);
1148
+ for (const id of unchecked) this.checked.add(id);
1149
+ for (const id of found) this.purged.add(id);
1150
+ }
1151
+ return new Set(documentIds.filter((id) => this.purged.has(id)));
1152
+ }
1153
+ };
1154
+ /** Locks and checks an id on its first read; a purged id reads as purged. */
1155
+ var FencedWriteCache = class {
1156
+ inner;
1157
+ purgeFence;
1158
+ evict;
1159
+ evicted = /* @__PURE__ */ new Set();
1160
+ constructor(inner, purgeFence, evict) {
1161
+ this.inner = inner;
1162
+ this.purgeFence = purgeFence;
1163
+ this.evict = evict;
1164
+ }
1165
+ /** Fences ids known before their reads in one round trip. */
1166
+ async fence(documentIds) {
1167
+ await this.purgeFence.isPurgedMany(documentIds);
1168
+ }
1169
+ async getState(documentId, scope, branch, targetRevision, signal) {
1170
+ if (await this.purgeFence.isPurged(documentId)) {
1171
+ if (!this.evicted.has(documentId)) {
1172
+ this.evicted.add(documentId);
1173
+ this.evict(documentId);
1174
+ }
1175
+ throw new DocumentPurgedError(documentId);
1176
+ }
1177
+ return this.inner.getState(documentId, scope, branch, targetRevision, signal);
1178
+ }
1179
+ putState(documentId, scope, branch, revision, document, position) {
1180
+ this.inner.putState(documentId, scope, branch, revision, document, position);
1181
+ }
1182
+ putRun(documentId, scope, branch, run) {
1183
+ this.inner.putRun(documentId, scope, branch, run);
1184
+ }
1185
+ invalidate(documentId, scope, branch) {
1186
+ return this.inner.invalidate(documentId, scope, branch);
1187
+ }
1188
+ clear() {
1189
+ this.inner.clear();
1190
+ }
1191
+ startup() {
1192
+ return this.inner.startup();
1193
+ }
1194
+ shutdown() {
1195
+ return this.inner.shutdown();
1196
+ }
1197
+ };
1198
+ /** The target whose membership an ADD or REMOVE_RELATIONSHIP writes. */
1199
+ function relationshipTarget(action) {
1200
+ if (action.type !== "ADD_RELATIONSHIP" && action.type !== "REMOVE_RELATIONSHIP") return;
1201
+ const target = action.input?.targetId;
1202
+ return typeof target === "string" && target.length > 0 ? target : void 0;
1203
+ }
1204
+ /** The ids a job's locks cover, sorted: its write ids and groups it names. */
1205
+ function jobWriteIds(job) {
1206
+ const ids = /* @__PURE__ */ new Set([job.documentId]);
1207
+ const actions = [...job.actions, ...job.operations.map((operation) => operation.action)];
1208
+ for (const action of actions) {
1209
+ if (DOCUMENT_SCOPE_ACTIONS.has(action.type)) ids.add(targetDocumentId(action, job.documentId));
1210
+ const target = relationshipTarget(action);
1211
+ if (target !== void 0) ids.add(target);
1212
+ if (action.scope === "auth") for (const groupId of mentionedGroupIds(action)) ids.add(groupId);
1213
+ }
1214
+ return [...ids].sort();
1215
+ }
899
1216
  //#endregion
900
1217
  //#region src/registry/errors.ts
901
1218
  /**
@@ -915,6 +1232,19 @@ var ModuleNotFoundError = class extends Error {
915
1232
  return Error.isError(error) && error.name === "ModuleNotFoundError";
916
1233
  }
917
1234
  };
1235
+ /** The host has the model only as a live module, which workers cannot import. */
1236
+ var ModelNotWorkerImportableError = class extends Error {
1237
+ documentType;
1238
+ constructor(documentType, version) {
1239
+ const versionSuffix = version !== void 0 ? ` version ${version}` : "";
1240
+ super(`Document model ${documentType}${versionSuffix} is registered on the host only as a live module, so executor workers cannot load it. Register it from a { filePath } or { packageName } source.`);
1241
+ this.name = "ModelNotWorkerImportableError";
1242
+ this.documentType = documentType;
1243
+ }
1244
+ static isError(error) {
1245
+ return Error.isError(error) && error.name === "ModelNotWorkerImportableError";
1246
+ }
1247
+ };
918
1248
  /**
919
1249
  * Error thrown when attempting to register a module that already exists.
920
1250
  */
@@ -978,6 +1308,8 @@ var InvalidUpgradeStepError = class extends Error {
978
1308
  };
979
1309
  //#endregion
980
1310
  //#region src/executor/types.ts
1311
+ /** 200k operations purge in 1.6 s on local Postgres; jobs time out at 30 s. */
1312
+ const DEFAULT_MAX_PURGE_OPERATIONS = 2e5;
981
1313
  /** How long a deferred job waits for its document before it fails. */
982
1314
  const DEFAULT_DEFERRED_JOB_TTL_MS = 3e4;
983
1315
  /**
@@ -993,6 +1325,7 @@ const JobExecutorEventTypes = {
993
1325
  //#endregion
994
1326
  //#region src/cache/collection-membership-cache.ts
995
1327
  var CollectionMembershipCache = class CollectionMembershipCache {
1328
+ operationIndex;
996
1329
  cache = /* @__PURE__ */ new Map();
997
1330
  constructor(operationIndex) {
998
1331
  this.operationIndex = operationIndex;
@@ -1190,26 +1523,294 @@ var DocumentMetaCache = class DocumentMetaCache {
1190
1523
  const docScopeOps = await this.operationStore.getSince(documentId, "document", branch, -1, void 0, void 0, signal);
1191
1524
  if (docScopeOps.results.length === 0) throw new DocumentNotFoundError(documentId);
1192
1525
  const createOp = docScopeOps.results[0];
1526
+ if (isPurgeMarker(createOp)) return purgedMeta(createOp.action);
1193
1527
  if (createOp.action.type !== "CREATE_DOCUMENT") throw new Error(`Invalid document: first operation must be CREATE_DOCUMENT, found ${createOp.action.type}`);
1194
1528
  const createAction = createOp.action;
1195
1529
  const documentType = createAction.input.model;
1196
1530
  let document = createDocumentFromAction(createAction);
1197
1531
  let documentScopeRevision = 0;
1532
+ let purged = false;
1198
1533
  for (const op of docScopeOps.results) {
1199
1534
  if (targetRevision !== void 0 && op.index > targetRevision) break;
1200
1535
  documentScopeRevision = op.index;
1201
1536
  if (op.action.type === "UPGRADE_DOCUMENT") {
1202
1537
  const upgradeAction = op.action;
1203
1538
  document = applyUpgradeDocumentAction$1(document, upgradeAction);
1204
- } else if (op.action.type === "DELETE_DOCUMENT") document = applyDeleteDocumentAction$1(document, op.action);
1539
+ } else if (op.action.type === "DELETE_DOCUMENT" || op.action.type === PURGE_DOCUMENT) {
1540
+ document = applyDeleteDocumentAction$1(document, op.action);
1541
+ purged ||= op.action.type === PURGE_DOCUMENT;
1542
+ }
1205
1543
  }
1206
1544
  return {
1207
1545
  state: document.state.document,
1208
1546
  documentType,
1547
+ protocolVersions: purged ? purgedProtocolVersions() : createAction.input.protocolVersions,
1209
1548
  documentScopeRevision: documentScopeRevision + 1
1210
1549
  };
1211
1550
  }
1212
1551
  };
1552
+ /** A lone marker has no CREATE_DOCUMENT; the marker describes the stream. */
1553
+ function purgedMeta(action) {
1554
+ return {
1555
+ state: createDocumentState({
1556
+ isDeleted: true,
1557
+ deletedAtUtcIso: action.input.purgedAtUtcIso
1558
+ }),
1559
+ documentType: action.input.documentType,
1560
+ protocolVersions: purgedProtocolVersions(),
1561
+ documentScopeRevision: 1
1562
+ };
1563
+ }
1564
+ //#endregion
1565
+ //#region src/catch-up/settled-watermark.ts
1566
+ const MAX_PENDING_PROBES = 64;
1567
+ /** pg_sequence_last_value needs 10; pg_current_* replaced txid_* in 13. */
1568
+ function snapshotFunctionsFor(serverVersionNum) {
1569
+ if (serverVersionNum >= 13e4) return {
1570
+ snapshot: "pg_current_snapshot",
1571
+ currentXid: "pg_current_xact_id",
1572
+ xidIfAssigned: "pg_current_xact_id_if_assigned"
1573
+ };
1574
+ if (serverVersionNum >= 1e5) return {
1575
+ snapshot: "txid_current_snapshot",
1576
+ currentXid: "txid_current",
1577
+ xidIfAssigned: "txid_current_if_assigned"
1578
+ };
1579
+ throw new Error(`PostgreSQL server_version_num ${serverVersionNum} is not supported: read-side catch-up needs 10 or later`);
1580
+ }
1581
+ /** Parses the `xmin:xmax:xip,...` text form of a snapshot. */
1582
+ function parseSnapshot(text) {
1583
+ const parts = text.split(":");
1584
+ if (parts.length !== 3) throw new Error(`Malformed snapshot: ${text}`);
1585
+ const [xmin, xmax, xip] = parts;
1586
+ return {
1587
+ xmin: BigInt(xmin),
1588
+ xmax: BigInt(xmax),
1589
+ xip: xip === "" ? [] : xip.split(",").map((xid) => BigInt(xid))
1590
+ };
1591
+ }
1592
+ async function readSnapshotFunctions(db) {
1593
+ const result = await sql`select current_setting('server_version_num') as version`.execute(db);
1594
+ return snapshotFunctionsFor(Number(result.rows[0].version));
1595
+ }
1596
+ /** A probe settles once an xmin passes the xid it took after its head. */
1597
+ var ProbeSettler = class {
1598
+ settled = 0;
1599
+ lastHead = 0;
1600
+ covered = 0;
1601
+ lastXip = [];
1602
+ pending = [];
1603
+ get settledThrough() {
1604
+ return this.settled;
1605
+ }
1606
+ get head() {
1607
+ return this.lastHead;
1608
+ }
1609
+ /** The highest head a settled or pending probe covers. */
1610
+ get probedHead() {
1611
+ return this.covered;
1612
+ }
1613
+ observe(head, xid, snapshot, nowUtcMs) {
1614
+ this.lastHead = Math.max(this.lastHead, head);
1615
+ this.lastXip = snapshot.xip;
1616
+ if (xid !== null && head > this.covered) {
1617
+ this.covered = head;
1618
+ this.pending.push({
1619
+ head,
1620
+ xid,
1621
+ takenAtUtcMs: nowUtcMs
1622
+ });
1623
+ if (this.pending.length > MAX_PENDING_PROBES) this.pending.shift();
1624
+ }
1625
+ const stillPending = [];
1626
+ for (const probe of this.pending) if (snapshot.xmin > probe.xid) this.settled = Math.max(this.settled, probe.head);
1627
+ else stillPending.push(probe);
1628
+ this.pending = stillPending.filter((probe) => probe.head > this.settled);
1629
+ return this.settled;
1630
+ }
1631
+ /** Xids an unsettled probe still waits on. */
1632
+ waitingOn() {
1633
+ if (this.pending.length === 0) return [];
1634
+ const xid = this.pending[0].xid;
1635
+ return this.lastXip.filter((open) => open < xid).map((open) => open.toString());
1636
+ }
1637
+ stalledSinceUtcMs() {
1638
+ return this.pending[0]?.takenAtUtcMs;
1639
+ }
1640
+ };
1641
+ function operationIndexTableName(db) {
1642
+ const from = db.selectFrom("operation_index_operations").selectAll().toOperationNode().from?.froms[0];
1643
+ const schema = from !== void 0 && TableNode.is(from) ? from.table.schema?.name : void 0;
1644
+ const quote = (identifier) => `"${identifier.replace(/"/g, "\"\"")}"`;
1645
+ return schema === void 0 ? quote("operation_index_operations") : `${quote(schema)}.${quote("operation_index_operations")}`;
1646
+ }
1647
+ /** Reads the head, takes and commits an xid, then reads a snapshot, in order. */
1648
+ function createKyselyWatermarkProbe(db) {
1649
+ let resolved = null;
1650
+ const resolve = async () => {
1651
+ const fns = await readSnapshotFunctions(db);
1652
+ const table = operationIndexTableName(db);
1653
+ const sequence = (await sql`select pg_get_serial_sequence(${table}, 'ordinal') as sequence`.execute(db)).rows[0]?.sequence;
1654
+ if (!sequence) throw new Error(`No ordinal sequence found for ${table}`);
1655
+ return {
1656
+ fns,
1657
+ sequence
1658
+ };
1659
+ };
1660
+ return async (probedHead) => {
1661
+ resolved ??= resolve().catch((error) => {
1662
+ resolved = null;
1663
+ throw error;
1664
+ });
1665
+ const { fns, sequence } = await resolved;
1666
+ const headRow = (await sql`select coalesce(pg_sequence_last_value(${sequence}::regclass), 0) as head, ${sql.raw(fns.xidIfAssigned)}() is null as outside_write`.execute(db)).rows[0];
1667
+ const head = Number(headRow.head);
1668
+ let xid = null;
1669
+ if (headRow.outside_write && head > probedHead) {
1670
+ const xidResult = await sql`select ${sql.raw(fns.currentXid)}()::text as xid`.execute(db);
1671
+ xid = BigInt(xidResult.rows[0].xid);
1672
+ }
1673
+ const row = (await sql`select ${sql.raw(fns.snapshot)}()::text as snapshot, ${sql.raw(fns.xidIfAssigned)}() is null as outside_write`.execute(db)).rows[0];
1674
+ return {
1675
+ head,
1676
+ xid,
1677
+ snapshot: parseSnapshot(row.snapshot),
1678
+ outsideWrite: headRow.outside_write && row.outside_write
1679
+ };
1680
+ };
1681
+ }
1682
+ /** Knows when an ordinal gap is final by asking which transactions are open. */
1683
+ var SettledWatermark = class {
1684
+ probe;
1685
+ logger;
1686
+ now;
1687
+ settler = new ProbeSettler();
1688
+ listeners = /* @__PURE__ */ new Set();
1689
+ current;
1690
+ next;
1691
+ warnedInsideWrite = false;
1692
+ constructor(probe, logger, now = Date.now) {
1693
+ this.probe = probe;
1694
+ this.logger = logger;
1695
+ this.now = now;
1696
+ }
1697
+ get settledThrough() {
1698
+ return this.settler.settledThrough;
1699
+ }
1700
+ refresh(signal) {
1701
+ signal?.throwIfAborted();
1702
+ if (this.next !== void 0) return this.next;
1703
+ if (this.current === void 0) return this.start();
1704
+ const next = this.current.catch(() => this.settledThrough).then(() => {
1705
+ this.next = void 0;
1706
+ return this.start();
1707
+ });
1708
+ this.next = next;
1709
+ return next;
1710
+ }
1711
+ onAdvance(listener) {
1712
+ this.listeners.add(listener);
1713
+ return () => {
1714
+ this.listeners.delete(listener);
1715
+ };
1716
+ }
1717
+ status() {
1718
+ const stalledSinceUtcMs = this.settler.head > this.settledThrough ? this.settler.stalledSinceUtcMs() : void 0;
1719
+ return {
1720
+ head: this.settler.head,
1721
+ settledThrough: this.settledThrough,
1722
+ waitingOn: this.settler.waitingOn(),
1723
+ ...stalledSinceUtcMs !== void 0 ? { stalledSinceUtcMs } : {}
1724
+ };
1725
+ }
1726
+ start() {
1727
+ const run = this.probeOnce().finally(() => {
1728
+ if (this.current === run) this.current = void 0;
1729
+ });
1730
+ this.current = run;
1731
+ return run;
1732
+ }
1733
+ async probeOnce() {
1734
+ const reading = await this.probe(this.settler.probedHead);
1735
+ if (!reading.outsideWrite) {
1736
+ if (!this.warnedInsideWrite) {
1737
+ this.warnedInsideWrite = true;
1738
+ this.logger.warn("settled watermark probe ran inside another session's write transaction; discarded");
1739
+ }
1740
+ return this.settledThrough;
1741
+ }
1742
+ const before = this.settledThrough;
1743
+ const after = this.settler.observe(reading.head, reading.xid, reading.snapshot, this.now());
1744
+ if (after > before) for (const listener of this.listeners) try {
1745
+ listener(after);
1746
+ } catch (error) {
1747
+ this.logger.error("settled watermark listener failed: @error", error);
1748
+ }
1749
+ return after;
1750
+ }
1751
+ };
1752
+ /** Sessions holding these xids open; empty without pg_stat_activity. */
1753
+ async function describeWaitingSessions(db, xids) {
1754
+ if (xids.length === 0) return [];
1755
+ const epochless = xids.map((xid) => (BigInt(xid) % 2n ** 32n).toString());
1756
+ try {
1757
+ return (await sql`select pid, application_name, state, xact_start, backend_xid::text as xid
1758
+ from pg_stat_activity
1759
+ where backend_xid::text in (${sql.join(epochless)})`.execute(db)).rows.map((row) => ({
1760
+ pid: Number(row.pid),
1761
+ applicationName: row.application_name ?? "",
1762
+ state: row.state ?? "",
1763
+ xactStart: row.xact_start === null ? null : new Date(row.xact_start).toISOString(),
1764
+ xid: row.xid
1765
+ }));
1766
+ } catch {
1767
+ return [];
1768
+ }
1769
+ }
1770
+ //#endregion
1771
+ //#region src/storage/kysely/document-purges.ts
1772
+ /** Class key of every purge lock; the stream locks use the single-key space. */
1773
+ const PURGE_NS = 1347571013;
1774
+ /** Ids share this many lock keys, so a job's lock count stays bounded. */
1775
+ const PURGE_LOCK_BUCKETS = 1024;
1776
+ async function acquirePurgeLocks(trx, ids, mode) {
1777
+ const unique = [...new Set(ids)];
1778
+ if (unique.length === 0) return;
1779
+ const lock = sql.raw(mode === "shared" ? "pg_advisory_xact_lock_shared" : "pg_advisory_xact_lock");
1780
+ await sql`
1781
+ with keys as materialized (
1782
+ select distinct hashtext(id) & ${sql.lit(1023)} as key
1783
+ from unnest(${unique}::text[]) as t(id)
1784
+ order by key
1785
+ )
1786
+ select ${lock}(${sql.lit(PURGE_NS)}, key) from keys order by key
1787
+ `.execute(trx);
1788
+ }
1789
+ /** The ids among `ids` that have a tombstone, read in the handle's schema. */
1790
+ async function findPurged(db, ids) {
1791
+ const unique = [...new Set(ids)];
1792
+ if (unique.length === 0) return /* @__PURE__ */ new Set();
1793
+ const rows = await db.selectFrom("document_purges").select("documentId").where(sql`"documentId" = any(${unique}::text[])`).execute();
1794
+ return new Set(rows.map((row) => row.documentId));
1795
+ }
1796
+ /** Every tombstoned id, read in the handle's schema. */
1797
+ async function listPurged(db) {
1798
+ return (await db.selectFrom("document_purges").select("documentId").orderBy("documentId").execute()).map((row) => row.documentId);
1799
+ }
1800
+ /** Bind slots per statement: 16-bit on the wire, read signed in-process. */
1801
+ const MAX_BIND_PARAMETERS = 32767;
1802
+ /** Splits rows into runs that each bind at most MAX_BIND_PARAMETERS. */
1803
+ function chunkRows(rows) {
1804
+ if (rows.length === 0) return [];
1805
+ const columnsPerRow = Math.max(1, Object.keys(rows[0]).length);
1806
+ const perChunk = Math.max(1, Math.floor(MAX_BIND_PARAMETERS / columnsPerRow));
1807
+ if (rows.length <= perChunk) return [rows];
1808
+ const chunks = [];
1809
+ for (let i = 0; i < rows.length; i += perChunk) chunks.push(rows.slice(i, i + perChunk));
1810
+ return chunks;
1811
+ }
1812
+ /** Per database: the function that assigns the transaction's xid. */
1813
+ const xidFunctions = /* @__PURE__ */ new WeakMap();
1213
1814
  var KyselyOperationIndexTxn = class {
1214
1815
  collections = [];
1215
1816
  collectionMemberships = [];
@@ -1275,16 +1876,20 @@ var KyselyOperationIndexTxn = class {
1275
1876
  }
1276
1877
  };
1277
1878
  var KyselyOperationIndex = class KyselyOperationIndex {
1879
+ db;
1278
1880
  trx;
1881
+ liveIds;
1279
1882
  constructor(db) {
1280
1883
  this.db = db;
1281
1884
  }
1282
1885
  get queryExecutor() {
1283
1886
  return this.trx ?? this.db;
1284
1887
  }
1285
- withTransaction(trx) {
1888
+ /** `liveIds`: ids the transaction read untombstoned under its shared lock. */
1889
+ withTransaction(trx, liveIds) {
1286
1890
  const instance = new KyselyOperationIndex(this.db);
1287
1891
  instance.trx = trx;
1892
+ instance.liveIds = liveIds;
1288
1893
  return instance;
1289
1894
  }
1290
1895
  start() {
@@ -1331,10 +1936,12 @@ var KyselyOperationIndex = class KyselyOperationIndex {
1331
1936
  leftOrdinal: null
1332
1937
  }));
1333
1938
  for (const collectionId of collections) kyselyTxn.recordMembershipInvalidation(collectionId);
1334
- await trx.insertInto("document_collections").values(collectionRows).onConflict((oc) => oc.doNothing()).execute();
1939
+ for (const chunk of chunkRows(collectionRows)) await trx.insertInto("document_collections").values(chunk).onConflict((oc) => oc.doNothing()).execute();
1335
1940
  }
1336
1941
  let operationOrdinals = [];
1337
1942
  if (operations.length > 0) {
1943
+ await this.refusePurgedOperations(trx, operations);
1944
+ await this.assignXid(trx);
1338
1945
  const operationRows = operations.map((op) => ({
1339
1946
  opId: op.id || "",
1340
1947
  documentId: op.documentId,
@@ -1349,7 +1956,10 @@ var KyselyOperationIndex = class KyselyOperationIndex {
1349
1956
  deniedReason: op.deniedReason ?? null,
1350
1957
  sourceRemote: op.sourceRemote
1351
1958
  }));
1352
- operationOrdinals = (await trx.insertInto("operation_index_operations").values(operationRows).returning("ordinal").execute()).map((row) => row.ordinal);
1959
+ for (const chunk of chunkRows(operationRows)) {
1960
+ const insertedOps = await trx.insertInto("operation_index_operations").values(chunk).returning("ordinal").execute();
1961
+ operationOrdinals = operationOrdinals.concat(insertedOps.map((row) => row.ordinal));
1962
+ }
1353
1963
  }
1354
1964
  if (memberships.length > 0) for (const m of memberships) {
1355
1965
  const ordinal = operationOrdinals[m.operationIndex];
@@ -1382,10 +1992,44 @@ var KyselyOperationIndex = class KyselyOperationIndex {
1382
1992
  }
1383
1993
  return operationOrdinals;
1384
1994
  }
1995
+ async getCollectionsInRange(after, through, among, signal) {
1996
+ signal?.throwIfAborted();
1997
+ if (through <= after || among?.length === 0) return [];
1998
+ let query = this.queryExecutor.selectFrom("operation_index_operations as oi").innerJoin("document_collections as dc", "oi.documentId", "dc.documentId").select("dc.collectionId").distinct().where("oi.ordinal", ">", after).where("oi.ordinal", "<=", through);
1999
+ if (among !== void 0) query = query.where("dc.collectionId", "in", [...among]);
2000
+ return (await query.execute()).map((row) => row.collectionId);
2001
+ }
2002
+ async getOrdinalsInRange(after, through, limit, signal) {
2003
+ signal?.throwIfAborted();
2004
+ if (through <= after || limit <= 0) return [];
2005
+ return (await this.queryExecutor.selectFrom("operation_index_operations").select("ordinal").where("ordinal", ">", after).where("ordinal", "<=", through).orderBy("ordinal", "asc").limit(limit).execute()).map((row) => Number(row.ordinal));
2006
+ }
2007
+ async getByOrdinals(ordinals, signal) {
2008
+ signal?.throwIfAborted();
2009
+ if (ordinals.length === 0) return [];
2010
+ const results = [];
2011
+ for (let i = 0; i < ordinals.length; i += MAX_BIND_PARAMETERS) {
2012
+ const chunk = ordinals.slice(i, i + MAX_BIND_PARAMETERS);
2013
+ const rows = await this.queryExecutor.selectFrom("operation_index_operations").selectAll().where("ordinal", "in", chunk).orderBy("ordinal", "asc").execute();
2014
+ for (const row of rows) results.push(this.rowToOperationWithContext(row));
2015
+ }
2016
+ results.sort((a, b) => a.context.ordinal - b.context.ordinal);
2017
+ return results;
2018
+ }
2019
+ async getStreamAfter(stream, after, signal, limit) {
2020
+ signal?.throwIfAborted();
2021
+ return (await this.queryExecutor.selectFrom("operation_index_operations").selectAll().where("documentId", "=", stream.documentId).where("branch", "=", stream.branch).where("scope", "=", stream.scope).where("ordinal", ">", after).orderBy("ordinal", "asc").$if(limit !== void 0, (qb) => qb.limit(limit)).execute()).map((row) => this.rowToOperationWithContext(row));
2022
+ }
1385
2023
  async getGroupReferencers(groupId, signal) {
1386
2024
  if (signal?.aborted) throw new Error("Operation aborted");
1387
2025
  return (await this.queryExecutor.selectFrom("group_references").select("documentId").where("groupId", "=", groupId).orderBy("documentId").execute()).map((row) => row.documentId);
1388
2026
  }
2027
+ async getOrdinalsByOpIds(documentId, scope, branch, opIds, signal) {
2028
+ signal?.throwIfAborted();
2029
+ if (opIds.length === 0) return /* @__PURE__ */ new Map();
2030
+ const rows = await this.queryExecutor.selectFrom("operation_index_operations").select("opId").select((eb) => eb.fn.max("ordinal").as("ordinal")).where("documentId", "=", documentId).where("branch", "=", branch).where("scope", "=", scope).where("opId", "in", opIds).groupBy("opId").execute();
2031
+ return new Map(rows.map((row) => [row.opId, Number(row.ordinal)]));
2032
+ }
1389
2033
  async find(collectionId, cursor, view, paging, signal) {
1390
2034
  if (signal?.aborted) throw new Error("Operation aborted");
1391
2035
  const outerCursor = cursor ?? -1;
@@ -1399,6 +2043,7 @@ var KyselyOperationIndex = class KyselyOperationIndex {
1399
2043
  if (view?.branch) qb = qb.where("oi.branch", "=", view.branch);
1400
2044
  if (view?.scopes && view.scopes.length > 0) qb = qb.where("oi.scope", "in", view.scopes);
1401
2045
  if (view?.excludeSourceRemote) qb = qb.where("oi.sourceRemote", "!=", view.excludeSourceRemote);
2046
+ if (view?.throughOrdinal !== void 0) qb = qb.where("oi.ordinal", "<=", view.throughOrdinal).where("dc.joinedOrdinal", "<=", BigInt(view.throughOrdinal));
1402
2047
  return qb;
1403
2048
  };
1404
2049
  const rows = await buildBranch("joiner").unionAll(buildBranch("newOps")).orderBy("ordinal", "asc").limit(limit + 1).execute();
@@ -1487,6 +2132,23 @@ var KyselyOperationIndex = class KyselyOperationIndex {
1487
2132
  }, signal) : void 0
1488
2133
  };
1489
2134
  }
2135
+ /** Backstop to the store's refusal: only a marker indexes a purged id. */
2136
+ async refusePurgedOperations(trx, operations) {
2137
+ const ids = new Set(operations.filter((operation) => !isPurgeMarker(operation)).map((operation) => operation.documentId).filter((id) => !this.liveIds?.has(id)));
2138
+ const purged = await findPurged(trx, ids);
2139
+ for (const id of ids) if (purged.has(id)) throw new DocumentPurgedError(id);
2140
+ }
2141
+ /** Takes the xid before the first ordinal, as the settled watermark needs. */
2142
+ async assignXid(trx) {
2143
+ let fn = xidFunctions.get(this.db);
2144
+ if (fn === void 0) {
2145
+ fn = readSnapshotFunctions(trx).then((fns) => fns.currentXid);
2146
+ xidFunctions.set(this.db, fn);
2147
+ fn.catch(() => xidFunctions.delete(this.db));
2148
+ }
2149
+ const name = await fn;
2150
+ await sql`select ${sql.raw(name)}()`.execute(trx);
2151
+ }
1490
2152
  rowToOperationWithContext(row) {
1491
2153
  return {
1492
2154
  operation: {
@@ -1605,21 +2267,6 @@ var RingBuffer = class {
1605
2267
  }
1606
2268
  };
1607
2269
  //#endregion
1608
- //#region src/cache/write-cache-types.ts
1609
- /**
1610
- * Where a snapshot sits in its stream.
1611
- *
1612
- * - `Head`: the newest revision of the stream when it was stored. Only these
1613
- * can answer a read that asks for the head.
1614
- * - `Historical`: state at an earlier revision. Usable as a starting point to
1615
- * replay forward from, and as an answer to a read for that same revision.
1616
- */
1617
- let SnapshotPosition = /* @__PURE__ */ function(SnapshotPosition) {
1618
- SnapshotPosition["Head"] = "head";
1619
- SnapshotPosition["Historical"] = "historical";
1620
- return SnapshotPosition;
1621
- }({});
1622
- //#endregion
1623
2270
  //#region src/cache/kysely-write-cache.ts
1624
2271
  /**
1625
2272
  * The last operation index a keyframe's document reflects for the scope. A
@@ -1654,42 +2301,6 @@ function copyDocument(document) {
1654
2301
  operations: { ...document.operations }
1655
2302
  };
1656
2303
  }
1657
- /**
1658
- * In-memory write cache with keyframe persistence for PHDocuments.
1659
- *
1660
- * Caches document snapshots in ring buffers with LRU eviction. On cache miss,
1661
- * rebuilds documents from nearest keyframe or full operation history.
1662
- *
1663
- * **Performance Characteristics:**
1664
- * - Cache hit: O(1) lookup in ring buffer
1665
- * - Cold miss: O(n) where n is total operation count, or O(k) where k is operations since keyframe
1666
- * - Warm miss: O(m) where m is operations since cached revision
1667
- * - Eviction: O(1) for LRU tracking and removal
1668
- *
1669
- * **Thread Safety:**
1670
- * Not thread-safe. Designed for single-threaded job executor environment.
1671
- * External synchronization required for concurrent access across multiple executors.
1672
- *
1673
- * **Example:**
1674
- * ```typescript
1675
- * const cache = new KyselyWriteCache(
1676
- * keyframeStore,
1677
- * operationStore,
1678
- * registry,
1679
- * { maxDocuments: 1000, ringBufferSize: 10, keyframeInterval: 10 }
1680
- * );
1681
- *
1682
- * await cache.startup();
1683
- *
1684
- * // Retrieve or rebuild document
1685
- * const doc = await cache.getState(docId, docType, scope, branch, revision);
1686
- *
1687
- * // Cache result after job execution
1688
- * cache.putState(docId, docType, scope, branch, newRevision, updatedDoc);
1689
- *
1690
- * await cache.shutdown();
1691
- * ```
1692
- */
1693
2304
  var KyselyWriteCache = class KyselyWriteCache {
1694
2305
  streams;
1695
2306
  lruTracker;
@@ -1697,6 +2308,7 @@ var KyselyWriteCache = class KyselyWriteCache {
1697
2308
  operationStore;
1698
2309
  registry;
1699
2310
  config;
2311
+ reads = /* @__PURE__ */ new Map();
1700
2312
  constructor(keyframeStore, operationStore, registry, config) {
1701
2313
  this.keyframeStore = keyframeStore;
1702
2314
  this.operationStore = operationStore;
@@ -1713,6 +2325,7 @@ var KyselyWriteCache = class KyselyWriteCache {
1713
2325
  const scoped = new KyselyWriteCache(keyframeStore, operationStore, this.registry, this.config);
1714
2326
  scoped.streams = this.streams;
1715
2327
  scoped.lruTracker = this.lruTracker;
2328
+ scoped.reads = this.reads;
1716
2329
  return scoped;
1717
2330
  }
1718
2331
  /**
@@ -1746,11 +2359,27 @@ var KyselyWriteCache = class KyselyWriteCache {
1746
2359
  * @returns The document at the target revision
1747
2360
  * @throws {Error} "Operation aborted" if signal is aborted
1748
2361
  * @throws {ModuleNotFoundError} If document type not registered in registry
2362
+ * @throws {DocumentPurgedError} If the stream's only row is a purge marker
1749
2363
  * @throws {Error} "Failed to rebuild document" if operation store fails
1750
2364
  * @throws {Error} If reducer throws during operation application
1751
2365
  * @throws {Error} If document serialization fails
1752
2366
  */
1753
2367
  async getState(documentId, scope, branch, targetRevision, signal) {
2368
+ const read = { evicted: false };
2369
+ let pending = this.reads.get(documentId);
2370
+ if (!pending) {
2371
+ pending = /* @__PURE__ */ new Set();
2372
+ this.reads.set(documentId, pending);
2373
+ }
2374
+ pending.add(read);
2375
+ try {
2376
+ return await this.readState(documentId, scope, branch, targetRevision, signal, read);
2377
+ } finally {
2378
+ pending.delete(read);
2379
+ if (pending.size === 0) this.reads.delete(documentId);
2380
+ }
2381
+ }
2382
+ async readState(documentId, scope, branch, targetRevision, signal, read) {
1754
2383
  if (signal?.aborted) throw new Error("Operation aborted");
1755
2384
  const streamKey = this.makeStreamKey(documentId, scope, branch);
1756
2385
  const stream = this.streams.get(streamKey);
@@ -1758,13 +2387,13 @@ var KyselyWriteCache = class KyselyWriteCache {
1758
2387
  const snapshots = stream.ringBuffer.getAll();
1759
2388
  if (targetRevision === void 0) {
1760
2389
  const newest = highestRevision(snapshots);
1761
- if (newest?.position === SnapshotPosition.Head) {
2390
+ if (newest?.position === "head") {
1762
2391
  this.lruTracker.touch(streamKey);
1763
2392
  return copyDocument(newest.document);
1764
2393
  }
1765
2394
  if (newest) {
1766
2395
  const document = await this.warmMissRebuild(newest.document, newest.revision, documentId, scope, branch, void 0, signal);
1767
- this.store(documentId, scope, branch, (document.header.revision[scope] ?? 0) - 1, document, SnapshotPosition.Head);
2396
+ this.storeUnlessEvicted(read, documentId, scope, branch, (document.header.revision[scope] ?? 0) - 1, document, "head");
1768
2397
  this.lruTracker.touch(streamKey);
1769
2398
  return document;
1770
2399
  }
@@ -1777,7 +2406,7 @@ var KyselyWriteCache = class KyselyWriteCache {
1777
2406
  const newestOlder = this.findNearestOlderSnapshot(snapshots, targetRevision);
1778
2407
  if (newestOlder) {
1779
2408
  const document = await this.warmMissRebuild(newestOlder.document, newestOlder.revision, documentId, scope, branch, targetRevision, signal);
1780
- this.store(documentId, scope, branch, targetRevision, document, SnapshotPosition.Historical);
2409
+ this.storeUnlessEvicted(read, documentId, scope, branch, targetRevision, document, "historical");
1781
2410
  this.lruTracker.touch(streamKey);
1782
2411
  return document;
1783
2412
  }
@@ -1785,7 +2414,7 @@ var KyselyWriteCache = class KyselyWriteCache {
1785
2414
  }
1786
2415
  const document = await this.coldMissRebuild(documentId, scope, branch, targetRevision, signal);
1787
2416
  const revision = targetRevision ?? (document.header.revision[scope] ?? 0) - 1;
1788
- this.store(documentId, scope, branch, revision, document, targetRevision === void 0 ? SnapshotPosition.Head : SnapshotPosition.Historical);
2417
+ this.storeUnlessEvicted(read, documentId, scope, branch, revision, document, targetRevision === void 0 ? "head" : "historical");
1789
2418
  return document;
1790
2419
  }
1791
2420
  /**
@@ -1820,7 +2449,10 @@ var KyselyWriteCache = class KyselyWriteCache {
1820
2449
  if (run.length === 0) return;
1821
2450
  for (const entry of run.slice(0, -1)) this.persistKeyframe(documentId, scope, branch, entry.revision, entry.document);
1822
2451
  const head = run[run.length - 1];
1823
- this.store(documentId, scope, branch, head.revision, head.document, SnapshotPosition.Head);
2452
+ this.store(documentId, scope, branch, head.revision, head.document, "head");
2453
+ }
2454
+ storeUnlessEvicted(read, documentId, scope, branch, revision, document, position) {
2455
+ if (!read.evicted) this.store(documentId, scope, branch, revision, document, position);
1824
2456
  }
1825
2457
  store(documentId, scope, branch, revision, document, position) {
1826
2458
  const streamKey = this.makeStreamKey(documentId, scope, branch);
@@ -1862,6 +2494,7 @@ var KyselyWriteCache = class KyselyWriteCache {
1862
2494
  * @returns The number of streams evicted
1863
2495
  */
1864
2496
  invalidate(documentId, scope, branch) {
2497
+ for (const read of this.reads.get(documentId) ?? []) read.evicted = true;
1865
2498
  let evicted = 0;
1866
2499
  if (scope === void 0 && branch === void 0) {
1867
2500
  for (const [key] of this.streams.entries()) if (key.startsWith(`${documentId}:`)) {
@@ -1890,6 +2523,7 @@ var KyselyWriteCache = class KyselyWriteCache {
1890
2523
  * Resets LRU tracking state. This operation always succeeds.
1891
2524
  */
1892
2525
  clear() {
2526
+ for (const pending of this.reads.values()) for (const read of pending) read.evicted = true;
1893
2527
  this.streams.clear();
1894
2528
  this.lruTracker.clear();
1895
2529
  }
@@ -1924,9 +2558,9 @@ var KyselyWriteCache = class KyselyWriteCache {
1924
2558
  * the last replayed operation are applied at the end. Creation-time 0->N seed
1925
2559
  * upgrades carry the initial state, so they still apply immediately.
1926
2560
  */
1927
- async coldMissRebuild(documentId, scope, branch, targetRevision, signal) {
2561
+ async coldMissRebuild(documentId, scope, branch, targetRevision, signal, fromKeyframe = true) {
1928
2562
  const effectiveTargetRevision = targetRevision || Number.MAX_SAFE_INTEGER;
1929
- const keyframe = await this.findNearestKeyframe(documentId, scope, branch, effectiveTargetRevision, signal);
2563
+ const keyframe = fromKeyframe ? await this.findNearestKeyframe(documentId, scope, branch, effectiveTargetRevision, signal) : void 0;
1930
2564
  const documentScopeBound = scope === "document" ? targetRevision : void 0;
1931
2565
  let document;
1932
2566
  let startRevision;
@@ -1969,7 +2603,7 @@ var KyselyWriteCache = class KyselyWriteCache {
1969
2603
  subsequentDeletes: []
1970
2604
  });
1971
2605
  }
1972
- } else if (operation.action.type === "DELETE_DOCUMENT") {
2606
+ } else if (operation.action.type === "DELETE_DOCUMENT" || operation.action.type === PURGE_DOCUMENT) {
1973
2607
  applyDeleteDocumentAction(document, operation.action);
1974
2608
  for (const pending of pendingUpgrades) pending.subsequentDeletes.push(operation.action);
1975
2609
  }
@@ -1982,6 +2616,7 @@ var KyselyWriteCache = class KyselyWriteCache {
1982
2616
  }, signal);
1983
2617
  if (createOpResult.results.length === 0) throw new DocumentNotFoundError(documentId);
1984
2618
  const createOp = createOpResult.results[0];
2619
+ if (isPurgeMarker(createOp)) throw new DocumentPurgedError(documentId);
1985
2620
  if (createOp.action.type !== "CREATE_DOCUMENT") throw new Error(`Failed to rebuild document ${documentId}: first operation in document scope must be CREATE_DOCUMENT, found ${createOp.action.type}`);
1986
2621
  const documentCreateAction = createOp.action;
1987
2622
  documentType = documentCreateAction.input.model;
@@ -2021,15 +2656,15 @@ var KyselyWriteCache = class KyselyWriteCache {
2021
2656
  });
2022
2657
  } else document = applyUpgradeDocumentAction(document, upgradeAction, void 0);
2023
2658
  docModule = this.registry.getModule(documentType, normalizeDocumentModelVersion(toVersion));
2024
- } else if (operation.action.type === "DELETE_DOCUMENT") {
2659
+ } else if (operation.action.type === "DELETE_DOCUMENT" || operation.action.type === PURGE_DOCUMENT) {
2025
2660
  applyDeleteDocumentAction(document, operation.action);
2026
2661
  for (const pending of pendingUpgrades) pending.subsequentDeletes.push(operation.action);
2027
2662
  } else {
2028
2663
  const protocolVersion = baseReducerVersion(document.header);
2029
- document = docModule.reducer(document, operation.action, void 0, {
2664
+ document = withProtocolVersions(docModule.reducer(document, operation.action, void 0, {
2030
2665
  skip: operation.skip,
2031
2666
  protocolVersion
2032
- });
2667
+ }), document.header.protocolVersions);
2033
2668
  }
2034
2669
  }
2035
2670
  }
@@ -2063,6 +2698,7 @@ var KyselyWriteCache = class KyselyWriteCache {
2063
2698
  let cursor = void 0;
2064
2699
  const pageSize = 100;
2065
2700
  let hasMorePages;
2701
+ let needsFullReplay = false;
2066
2702
  do {
2067
2703
  if (signal?.aborted) throw new Error("Operation aborted");
2068
2704
  const paging = {
@@ -2073,26 +2709,31 @@ var KyselyWriteCache = class KyselyWriteCache {
2073
2709
  const result = await this.operationStore.getSince(documentId, scope, branch, startRevision, void 0, paging, signal);
2074
2710
  for (const operation of result.results) {
2075
2711
  if (targetRevision !== void 0 && operation.index > targetRevision) break;
2712
+ if (keyframe && operation.skip > 0) {
2713
+ needsFullReplay = true;
2714
+ break;
2715
+ }
2076
2716
  const moduleVersion = this.resolveModuleVersionForOp(operation.index, operation.timestampUtcMs, scope, validatedUpgrades, finalVersion);
2077
2717
  document = this.applyPendingUpgrades(document, pendingUpgrades, moduleVersion ?? Number.MAX_SAFE_INTEGER);
2078
2718
  if (isDenied(operation)) document = appendWithoutApplying(document, operation, scope);
2079
2719
  else {
2080
2720
  const protocolVersion = baseReducerVersion(document.header);
2081
- document = getModuleCached(moduleVersion).reducer(document, operation.action, void 0, {
2721
+ document = withProtocolVersions(getModuleCached(moduleVersion).reducer(document, operation.action, void 0, {
2082
2722
  skip: operation.skip,
2083
2723
  protocolVersion,
2084
2724
  replayOptions: { operation },
2085
2725
  skipIndexValidation: true
2086
- });
2726
+ }), document.header.protocolVersions);
2087
2727
  }
2088
2728
  }
2089
2729
  const reachedTarget = targetRevision !== void 0 && result.results.some((op) => op.index >= targetRevision);
2090
- hasMorePages = Boolean(result.nextCursor) && !reachedTarget;
2730
+ hasMorePages = Boolean(result.nextCursor) && !reachedTarget && !needsFullReplay;
2091
2731
  if (hasMorePages) cursor = result.nextCursor;
2092
2732
  } catch (err) {
2093
2733
  throw new Error(`Failed to rebuild document ${documentId}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
2094
2734
  }
2095
2735
  } while (hasMorePages);
2736
+ if (needsFullReplay) return this.coldMissRebuild(documentId, scope, branch, targetRevision, signal, false);
2096
2737
  document = this.applyTailPendingUpgrades(document, pendingUpgrades, scope, targetRevision);
2097
2738
  document = await this.stampRevisions(document, documentId, scope, branch, targetRevision, signal);
2098
2739
  if (pendingUpgrades.length > 0) {
@@ -2199,26 +2840,32 @@ var KyselyWriteCache = class KyselyWriteCache {
2199
2840
  if ((await this.operationStore.getSince(documentId, "document", branch, docScopeNextIndex - 1, void 0, void 0, signal)).results.length > 0) return this.coldMissRebuild(documentId, scope, branch, targetRevision, signal);
2200
2841
  const module = this.registry.getModule(documentType, extractModuleVersion(baseDocument));
2201
2842
  let document = copyDocument(baseDocument);
2843
+ let needsFullReplay = false;
2202
2844
  try {
2203
2845
  const pagedResults = await this.operationStore.getSince(documentId, scope, branch, baseRevision, void 0, void 0, signal);
2204
2846
  for (const operation of pagedResults.results) {
2205
2847
  if (signal?.aborted) throw new Error("Operation aborted");
2206
2848
  if (targetRevision !== void 0 && operation.index > targetRevision) break;
2849
+ if (operation.skip > 0) {
2850
+ needsFullReplay = true;
2851
+ break;
2852
+ }
2207
2853
  if (isDenied(operation)) document = appendWithoutApplying(document, operation, scope);
2208
2854
  else {
2209
2855
  const protocolVersion = baseReducerVersion(document.header);
2210
- document = module.reducer(document, operation.action, void 0, {
2856
+ document = withProtocolVersions(module.reducer(document, operation.action, void 0, {
2211
2857
  skip: operation.skip,
2212
2858
  protocolVersion,
2213
2859
  replayOptions: { operation },
2214
2860
  skipIndexValidation: true
2215
- });
2861
+ }), document.header.protocolVersions);
2216
2862
  }
2217
2863
  if (targetRevision !== void 0 && operation.index === targetRevision) break;
2218
2864
  }
2219
2865
  } catch (err) {
2220
2866
  throw new Error(`Failed to rebuild document ${documentId}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
2221
2867
  }
2868
+ if (needsFullReplay) return this.coldMissRebuild(documentId, scope, branch, targetRevision, signal, false);
2222
2869
  const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
2223
2870
  document.header.revision = revisions.revision;
2224
2871
  if (targetRevision !== void 0) document.header.revision = {
@@ -2337,8 +2984,160 @@ function validateFeatureFlags(flags, prerequisites) {
2337
2984
  }
2338
2985
  }
2339
2986
  //#endregion
2987
+ //#region src/storage/kysely/document-purger.ts
2988
+ /** An `Operation` row whose DELETE_DOCUMENT applied: not errored, not denied. */
2989
+ const appliedDelete = sql`action->>'type' = 'DELETE_DOCUMENT' and coalesce(error, '') = '' and coalesce("deniedReason", '') = ''`;
2990
+ /** Rows removed per statement from the tables a document can fill. */
2991
+ const DEFAULT_PURGE_DELETE_BATCH = 5e3;
2992
+ /** A purge transaction's statements; bound to the job's transaction. */
2993
+ var KyselyDocumentPurger = class {
2994
+ batchSize;
2995
+ db;
2996
+ constructor(db, batchSize = DEFAULT_PURGE_DELETE_BATCH) {
2997
+ this.batchSize = batchSize;
2998
+ this.db = db;
2999
+ }
3000
+ /** Every stream the document holds operations in. */
3001
+ async streams(documentId) {
3002
+ return this.db.selectFrom("Operation").select(["scope", "branch"]).distinct().where("documentId", "=", documentId).orderBy("scope").orderBy("branch").execute();
3003
+ }
3004
+ async operationCount(documentId) {
3005
+ const row = await this.db.selectFrom("operation_index_operations").select((eb) => eb.fn.countAll().as("count")).where("documentId", "=", documentId).executeTakeFirst();
3006
+ return Number(row?.count ?? 0);
3007
+ }
3008
+ /** Untombstoned documents group_references records as naming the group. */
3009
+ async groupReferencers(groupId) {
3010
+ const ids = (await this.db.selectFrom("group_references").select("documentId").where("groupId", "=", groupId).where("documentId", "<>", groupId).orderBy("documentId").execute()).map((row) => row.documentId);
3011
+ const purged = await findPurged(this.db, ids);
3012
+ return ids.filter((id) => !purged.has(id));
3013
+ }
3014
+ /** Survivors whose accepted auth history names the group; refusals do not. */
3015
+ async groupReferencersInHistory(groupId) {
3016
+ const referencing = [];
3017
+ for (const documentId of await this.groupReferencers(groupId)) if ([...(await this.authOperations(documentId)).values()].some((operations) => garbageCollect(sortOperations(operations)).some((operation) => operation.deniedReason === void 0 && operation.error === void 0 && mentionedGroupIds(operation.action).includes(groupId)))) referencing.push(documentId);
3018
+ return referencing;
3019
+ }
3020
+ /** The document's auth-scope operations, per branch, in index order. */
3021
+ async authOperations(documentId) {
3022
+ const rows = await this.db.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", "auth").orderBy("branch").orderBy("index").execute();
3023
+ const byBranch = /* @__PURE__ */ new Map();
3024
+ for (const row of rows) {
3025
+ const operations = byBranch.get(row.branch) ?? [];
3026
+ operations.push({
3027
+ index: row.index,
3028
+ timestampUtcMs: row.timestampUtcMs.toISOString(),
3029
+ hash: row.hash,
3030
+ skip: row.skip,
3031
+ error: row.error || void 0,
3032
+ deniedReason: row.deniedReason || void 0,
3033
+ id: row.opId,
3034
+ action: row.action
3035
+ });
3036
+ byBranch.set(row.branch, operations);
3037
+ }
3038
+ return byBranch;
3039
+ }
3040
+ /** Every document ever in the collection, but its owner and its own row. */
3041
+ async collectionMembers(collectionId, ownerId, branch) {
3042
+ return (await this.db.selectFrom("document_collections as dc").select("dc.documentId").select(sql`dc."leftOrdinal" is null`.as("openHere")).select((eb) => eb.exists(eb.selectFrom("Operation as op").select("op.opId").whereRef("op.documentId", "=", "dc.documentId").where("op.branch", "=", branch).where("op.scope", "=", "document").where(appliedDelete)).as("deleted")).select((eb) => eb.exists(eb.selectFrom("document_collections as other").select("other.documentId").whereRef("other.documentId", "=", "dc.documentId").where("other.collectionId", "<>", collectionId).where("other.leftOrdinal", "is", null)).as("openElsewhere")).where("dc.collectionId", "=", collectionId).where("dc.documentId", "not in", [ownerId, collectionId]).orderBy("dc.documentId").execute()).map((row) => {
3043
+ const openElsewhere = Boolean(row.openElsewhere);
3044
+ return {
3045
+ documentId: row.documentId,
3046
+ openElsewhere,
3047
+ required: !openElsewhere && (Boolean(row.openHere) || Boolean(row.deleted))
3048
+ };
3049
+ });
3050
+ }
3051
+ async remoteCollection(remoteName) {
3052
+ return (await this.db.selectFrom("sync_remotes").select("collection_id").where("name", "=", remoteName).executeTakeFirst())?.collection_id;
3053
+ }
3054
+ /** Deletes every row about the document; the counts are per table. */
3055
+ async deleteRows(documentId) {
3056
+ const removed = {};
3057
+ removed.Operation = await this.deleteBatched(async () => {
3058
+ const result = await this.db.deleteFrom("Operation").where("id", "in", (eb) => eb.selectFrom("Operation").select("id").where("documentId", "=", documentId).limit(this.batchSize)).executeTakeFirst();
3059
+ return Number(result.numDeletedRows);
3060
+ });
3061
+ removed.operation_index_operations = await this.deleteBatched(async () => {
3062
+ const result = await this.db.deleteFrom("operation_index_operations").where("ordinal", "in", (eb) => eb.selectFrom("operation_index_operations").select("ordinal").where("documentId", "=", documentId).limit(this.batchSize)).executeTakeFirst();
3063
+ return Number(result.numDeletedRows);
3064
+ });
3065
+ removed.Keyframe = await this.deleteBatched(async () => {
3066
+ const result = await this.db.deleteFrom("Keyframe").where("id", "in", (eb) => eb.selectFrom("Keyframe").select("id").where("documentId", "=", documentId).limit(this.batchSize)).executeTakeFirst();
3067
+ return Number(result.numDeletedRows);
3068
+ });
3069
+ removed.DocumentSnapshot = await this.deleteBatched(async () => {
3070
+ const result = await this.db.deleteFrom("DocumentSnapshot").where("id", "in", (eb) => eb.selectFrom("DocumentSnapshot").select("id").where("documentId", "=", documentId).limit(this.batchSize)).executeTakeFirst();
3071
+ return Number(result.numDeletedRows);
3072
+ });
3073
+ removed.SlugMapping = await this.deleteWhere("SlugMapping", "documentId", documentId);
3074
+ const relationships = await this.db.selectFrom("DocumentRelationship").select((eb) => eb.fn.countAll().as("count")).where((eb) => eb.or([eb("sourceId", "=", documentId), eb("targetId", "=", documentId)])).executeTakeFirst();
3075
+ removed.DocumentRelationship = Number(relationships?.count ?? 0);
3076
+ removed.Document = await this.deleteWhere("Document", "id", documentId);
3077
+ removed.group_references = await this.deleteWhere("group_references", "documentId", documentId);
3078
+ removed.sync_dead_letters = await this.deleteWhere("sync_dead_letters", "document_id", documentId);
3079
+ removed.sync_holds = await this.deleteWhere("sync_holds", "document_id", documentId);
3080
+ return removed;
3081
+ }
3082
+ async writeTombstone(tombstone) {
3083
+ const values = {
3084
+ documentId: tombstone.documentId,
3085
+ ordinal: tombstone.ordinal,
3086
+ removedRows: JSON.stringify(tombstone.removedRows),
3087
+ purgedAtUtc: tombstone.purgedAtUtc,
3088
+ requestId: tombstone.requestId
3089
+ };
3090
+ await this.db.insertInto("document_purges").values(values).onConflict((oc) => oc.column("documentId").doUpdateSet({
3091
+ ordinal: values.ordinal,
3092
+ removedRows: values.removedRows,
3093
+ purgedAtUtc: values.purgedAtUtc,
3094
+ requestId: values.requestId
3095
+ })).execute();
3096
+ }
3097
+ /** Reopens every membership at the marker ordinal, plus `collectionIds`. */
3098
+ async reopenMemberships(documentId, ordinal, collectionIds = []) {
3099
+ await this.db.updateTable("document_collections").set({
3100
+ joinedOrdinal: BigInt(ordinal),
3101
+ leftOrdinal: null
3102
+ }).where("documentId", "=", documentId).execute();
3103
+ for (const collectionId of new Set(collectionIds)) await this.db.insertInto("document_collections").values({
3104
+ documentId,
3105
+ collectionId,
3106
+ joinedOrdinal: BigInt(ordinal),
3107
+ leftOrdinal: null
3108
+ }).onConflict((oc) => oc.columns(["documentId", "collectionId"]).doUpdateSet({
3109
+ joinedOrdinal: BigInt(ordinal),
3110
+ leftOrdinal: null
3111
+ })).execute();
3112
+ return (await this.db.selectFrom("document_collections").select("collectionId").where("documentId", "=", documentId).orderBy("collectionId").execute()).map((row) => row.collectionId);
3113
+ }
3114
+ async deleteBatched(step) {
3115
+ let total = 0;
3116
+ for (;;) {
3117
+ const deleted = await step();
3118
+ total += deleted;
3119
+ if (deleted < this.batchSize) return total;
3120
+ }
3121
+ }
3122
+ async deleteWhere(table, column, documentId) {
3123
+ const result = await this.db.deleteFrom(table).where(column, "=", documentId).executeTakeFirst();
3124
+ return Number(result.numDeletedRows);
3125
+ }
3126
+ };
3127
+ //#endregion
2340
3128
  //#region src/executor/execution-scope.ts
3129
+ /** For scopes without a transaction to hold a lock in. */
3130
+ const NOOP_DOCUMENT_LOCKS = {
3131
+ shared: () => Promise.resolve(),
3132
+ exclusive: () => Promise.resolve(),
3133
+ purged: () => Promise.resolve(/* @__PURE__ */ new Set())
3134
+ };
2341
3135
  var DefaultExecutionScope = class {
3136
+ operationStore;
3137
+ operationIndex;
3138
+ writeCache;
3139
+ documentMetaCache;
3140
+ collectionMembershipCache;
2342
3141
  constructor(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache) {
2343
3142
  this.operationStore = operationStore;
2344
3143
  this.operationIndex = operationIndex;
@@ -2353,11 +3152,19 @@ var DefaultExecutionScope = class {
2353
3152
  operationIndex: this.operationIndex,
2354
3153
  writeCache: this.writeCache,
2355
3154
  documentMetaCache: this.documentMetaCache,
2356
- collectionMembershipCache: this.collectionMembershipCache
3155
+ collectionMembershipCache: this.collectionMembershipCache,
3156
+ documentLocks: NOOP_DOCUMENT_LOCKS
2357
3157
  });
2358
3158
  }
2359
3159
  };
2360
3160
  var KyselyExecutionScope = class {
3161
+ db;
3162
+ operationStore;
3163
+ operationIndex;
3164
+ keyframeStore;
3165
+ writeCache;
3166
+ documentMetaCache;
3167
+ collectionMembershipCache;
2361
3168
  constructor(db, operationStore, operationIndex, keyframeStore, writeCache, documentMetaCache, collectionMembershipCache) {
2362
3169
  this.db = db;
2363
3170
  this.operationStore = operationStore;
@@ -2370,28 +3177,46 @@ var KyselyExecutionScope = class {
2370
3177
  async run(fn, signal) {
2371
3178
  signal?.throwIfAborted();
2372
3179
  return this.db.transaction().execute(async (trx) => {
2373
- const scopedOperationStore = this.operationStore.withTransaction(trx);
2374
- const scopedOperationIndex = this.operationIndex.withTransaction(trx);
3180
+ const sharedLocked = /* @__PURE__ */ new Set();
3181
+ const liveIds = /* @__PURE__ */ new Set();
3182
+ const scopedOperationStore = this.operationStore.withTransaction(trx, liveIds);
3183
+ const scopedOperationIndex = this.operationIndex.withTransaction(trx, liveIds);
2375
3184
  const scopedKeyframeStore = this.keyframeStore.withTransaction(trx);
2376
3185
  return fn({
2377
3186
  operationStore: scopedOperationStore,
2378
3187
  operationIndex: scopedOperationIndex,
2379
3188
  writeCache: this.writeCache.withScopedStores(scopedOperationStore, scopedKeyframeStore),
2380
3189
  documentMetaCache: this.documentMetaCache.withScopedStore(scopedOperationStore),
2381
- collectionMembershipCache: this.collectionMembershipCache.withScopedIndex(scopedOperationIndex)
3190
+ collectionMembershipCache: this.collectionMembershipCache.withScopedIndex(scopedOperationIndex),
3191
+ documentLocks: {
3192
+ shared: async (ids) => {
3193
+ const list = [...ids];
3194
+ await acquirePurgeLocks(trx, list, "shared");
3195
+ for (const id of list) sharedLocked.add(id);
3196
+ },
3197
+ exclusive: (id) => acquirePurgeLocks(trx, [id], "exclusive"),
3198
+ purged: async (ids) => {
3199
+ const list = [...ids];
3200
+ const found = await findPurged(trx, list);
3201
+ for (const id of list) if (sharedLocked.has(id) && !found.has(id)) liveIds.add(id);
3202
+ return found;
3203
+ }
3204
+ },
3205
+ purger: new KyselyDocumentPurger(trx)
2382
3206
  });
2383
3207
  });
2384
3208
  }
2385
3209
  };
2386
3210
  //#endregion
2387
3211
  //#region src/utils/reshuffle.ts
2388
- const STRICT_ORDER_ACTION_TYPES = new Set([
3212
+ const STRICT_ORDER_ACTION_TYPES = /* @__PURE__ */ new Set([
2389
3213
  "CREATE_DOCUMENT",
2390
3214
  "DELETE_DOCUMENT",
2391
3215
  "UPGRADE_DOCUMENT",
2392
3216
  "ADD_RELATIONSHIP",
2393
3217
  "REMOVE_RELATIONSHIP",
2394
3218
  "UPDATE_RELATIONSHIP",
3219
+ "PURGE_DOCUMENT",
2395
3220
  "ADD_FOLDER",
2396
3221
  "UPDATE_FOLDER",
2397
3222
  "REMOVE_FOLDER"
@@ -2597,6 +3422,15 @@ async function evaluateByPosition(model, target, subject, stores, signal) {
2597
3422
  const derivedSet = derivedReadSet(definition);
2598
3423
  if (!definition.evaluatesScope(scope)) return operations.map(() => void 0);
2599
3424
  const evaluating = new Set(operations.map((operation) => operation.id));
3425
+ const creation = operations.find((operation) => operation.action.type === "CREATE_DOCUMENT" && operation.action.input.documentId === target.documentId);
3426
+ async function stateBefore(query) {
3427
+ try {
3428
+ return await writeCache.getState(query.documentId, query.scope, query.branch, -1, signal);
3429
+ } catch (error) {
3430
+ if (creation !== void 0 && query.documentId === target.documentId && error instanceof DocumentNotFoundError && !(error instanceof DocumentPurgedError)) return createDocumentFromAction(creation.action);
3431
+ throw error;
3432
+ }
3433
+ }
2600
3434
  const readStreams = await Promise.all(readSet.map(async (stream) => ({
2601
3435
  stream,
2602
3436
  operations: (await operationStore.getSince(stream.query.documentId, stream.query.scope, stream.query.branch, -1, { actionTypes: stream.decidingActions }, void 0, signal)).results.filter((operation) => !evaluating.has(operation.id))
@@ -2609,7 +3443,7 @@ async function evaluateByPosition(model, target, subject, stores, signal) {
2609
3443
  const histories = [];
2610
3444
  for (const read of readStreams) {
2611
3445
  const streamOperations = read.stream === writtenProjection ? [...read.operations, ...operations] : read.operations;
2612
- const before = await writeCache.getState(read.stream.query.documentId, read.stream.query.scope, read.stream.query.branch, -1, signal);
3446
+ const before = await stateBefore(read.stream.query);
2613
3447
  walked.push({
2614
3448
  streamKey: streamKey(read.stream.query),
2615
3449
  scope: read.stream.query.scope,
@@ -2631,7 +3465,7 @@ async function evaluateByPosition(model, target, subject, stores, signal) {
2631
3465
  branch: target.branch
2632
3466
  };
2633
3467
  const storedOperations = (await operationStore.getSince(query.documentId, query.scope, query.branch, -1, void 0, void 0, signal)).results.filter((operation) => !evaluating.has(operation.id));
2634
- const before = await writeCache.getState(query.documentId, query.scope, query.branch, -1, signal);
3468
+ const before = await stateBefore(query);
2635
3469
  evaluatedStateKey = streamKey(query);
2636
3470
  walked.push({
2637
3471
  streamKey: evaluatedStateKey,
@@ -2702,58 +3536,36 @@ async function evaluateByPosition(model, target, subject, stores, signal) {
2702
3536
  return operations.map((operation) => reasons.get(operation.id));
2703
3537
  }
2704
3538
  //#endregion
2705
- //#region src/cache/operation-index-types.ts
2706
- const DRIVE_COLLECTION_PREFIX = "drive.";
2707
- /**
2708
- * Identifies the collection a remote synchronizes. Collections are drive-level
2709
- * abstractions (document-drive and reactor-drive), so a collection id is the
2710
- * drive document id plus the branch it scopes to rather than an opaque string.
2711
- *
2712
- * The canonical string form (`drive.${branch}.${driveId}`) is produced only by
2713
- * `key` and parsed only by `fromKey`; that string is the wire and storage
2714
- * representation and is byte-for-byte identical to the legacy
2715
- * `driveCollectionId(branch, driveId)` output, so existing `document_collections`
2716
- * rows and persisted remotes remain valid without migration.
2717
- */
2718
- var DriveCollectionId = class DriveCollectionId {
2719
- constructor(driveId, branch) {
2720
- this.driveId = driveId;
2721
- this.branch = branch;
2722
- }
2723
- static forDrive(driveId, branch = "main") {
2724
- return new DriveCollectionId(driveId, branch);
2725
- }
2726
- /**
2727
- * The single deserializer for the wire/storage form. `branch` may contain
2728
- * dots, while `driveId` is a dot-free document id, so the drive id is the
2729
- * final dot-delimited segment.
2730
- */
2731
- static fromKey(key) {
2732
- if (!key.startsWith(DRIVE_COLLECTION_PREFIX)) throw new Error(`Unsupported collection id: ${key}`);
2733
- const rest = key.slice(6);
2734
- const lastDot = rest.lastIndexOf(".");
2735
- if (lastDot === -1 || lastDot === rest.length - 1) throw new Error(`Malformed drive collection id: ${key}`);
2736
- return new DriveCollectionId(rest.slice(lastDot + 1), rest.slice(0, lastDot));
2737
- }
2738
- get key() {
2739
- return `${DRIVE_COLLECTION_PREFIX}${this.branch}.${this.driveId}`;
2740
- }
2741
- toString() {
2742
- return this.key;
2743
- }
2744
- equals(other) {
2745
- return this.driveId === other.driveId && this.branch === other.branch;
2746
- }
2747
- };
2748
- //#endregion
2749
3539
  //#region src/executor/document-action-handler.ts
2750
3540
  var DocumentActionHandler = class {
2751
- constructor(registry, logger, driveContainerTypes, featureFlags, decisionModel) {
3541
+ registry;
3542
+ logger;
3543
+ driveContainerTypes;
3544
+ featureFlags;
3545
+ decisionModel;
3546
+ protocolSupport;
3547
+ loggedUnregisteredProtocols = /* @__PURE__ */ new Set();
3548
+ constructor(registry, logger, driveContainerTypes, featureFlags, decisionModel, protocolSupport) {
2752
3549
  this.registry = registry;
2753
3550
  this.logger = logger;
2754
3551
  this.driveContainerTypes = driveContainerTypes;
2755
3552
  this.featureFlags = featureFlags;
2756
3553
  this.decisionModel = decisionModel;
3554
+ this.protocolSupport = protocolSupport;
3555
+ }
3556
+ /** Keys this reactor does not register are admitted: it cannot judge them. */
3557
+ unsupportedProtocol(documentId, protocolVersions) {
3558
+ for (const [protocol, version] of Object.entries(protocolVersions ?? {})) {
3559
+ const supported = this.protocolSupport[protocol];
3560
+ if (supported === void 0) {
3561
+ if (!this.loggedUnregisteredProtocols.has(protocol)) {
3562
+ this.loggedUnregisteredProtocols.add(protocol);
3563
+ this.logger.info("Admitting document @documentId with unregistered protocol @protocol @version", documentId, protocol, version);
3564
+ }
3565
+ continue;
3566
+ }
3567
+ if (!supported.includes(version)) return new UnsupportedProtocolVersionError(documentId, protocol, version);
3568
+ }
2757
3569
  }
2758
3570
  /** Whether the write arrives with its evaluation already decided. */
2759
3571
  alreadyEvaluated(executing) {
@@ -2771,6 +3583,7 @@ var DocumentActionHandler = class {
2771
3583
  case "ADD_RELATIONSHIP": return this.executeAddRelationship(write, executing);
2772
3584
  case "REMOVE_RELATIONSHIP": return this.executeRemoveRelationship(write, executing);
2773
3585
  case "UPDATE_RELATIONSHIP": return this.executeUpdateRelationship(write, executing);
3586
+ case "PURGE_DOCUMENT": return buildErrorResult(executing.job, new ReservedActionError(executing.job.documentId, action.type), executing.startTime);
2774
3587
  default: return buildErrorResult(executing.job, /* @__PURE__ */ new Error(`Unknown document action type: ${action.type}`), executing.startTime);
2775
3588
  }
2776
3589
  }
@@ -2840,7 +3653,7 @@ var DocumentActionHandler = class {
2840
3653
  [job.scope]: [...standing.operations[job.scope] ?? [], operation]
2841
3654
  };
2842
3655
  executing.touchedStreams.add(job.documentId, job.scope, job.branch);
2843
- stores.writeCache.putState(job.documentId, job.scope, job.branch, operation.index, standing, SnapshotPosition.Head);
3656
+ stores.writeCache.putState(job.documentId, job.scope, job.branch, operation.index, standing, "head");
2844
3657
  indexTxn.write([{
2845
3658
  ...operation,
2846
3659
  documentId: job.documentId,
@@ -2852,6 +3665,7 @@ var DocumentActionHandler = class {
2852
3665
  stores.documentMetaCache.putDocumentMeta(job.documentId, job.branch, {
2853
3666
  state: standing.state.document,
2854
3667
  documentType: standing.header.documentType,
3668
+ protocolVersions: standing.header.protocolVersions,
2855
3669
  documentScopeRevision: operation.index + 1
2856
3670
  });
2857
3671
  return buildSuccessResult(job, operation, job.documentId, standing.header.documentType, JSON.stringify({
@@ -2868,6 +3682,9 @@ var DocumentActionHandler = class {
2868
3682
  error: /* @__PURE__ */ new Error(`CREATE_DOCUMENT must be in "document" scope, got "${job.scope}"`),
2869
3683
  duration: Date.now() - startTime
2870
3684
  };
3685
+ const input = action.input;
3686
+ const unsupported = this.unsupportedProtocol(input.documentId, input.protocolVersions);
3687
+ if (unsupported) return buildErrorResult(job, unsupported, startTime);
2871
3688
  const document = createDocumentFromAction(action);
2872
3689
  let operation = createOperation(action, 0, skip, {
2873
3690
  documentId: document.header.id,
@@ -2875,8 +3692,8 @@ var DocumentActionHandler = class {
2875
3692
  branch: job.branch
2876
3693
  });
2877
3694
  const resultingStateObj = {
2878
- header: document.header,
2879
- ...document.state
3695
+ ...document.state,
3696
+ header: document.header
2880
3697
  };
2881
3698
  const resultingState = JSON.stringify(resultingStateObj);
2882
3699
  const writeResult = await this.writeOperationToStore({
@@ -2893,7 +3710,7 @@ var DocumentActionHandler = class {
2893
3710
  [job.scope]: [...document.operations[job.scope] ?? [], operation]
2894
3711
  };
2895
3712
  executing.touchedStreams.add(document.header.id, job.scope, job.branch);
2896
- stores.writeCache.putState(document.header.id, job.scope, job.branch, operation.index, document, SnapshotPosition.Head);
3713
+ stores.writeCache.putState(document.header.id, job.scope, job.branch, operation.index, document, "head");
2897
3714
  indexTxn.write([{
2898
3715
  ...operation,
2899
3716
  documentId: document.header.id,
@@ -2910,6 +3727,7 @@ var DocumentActionHandler = class {
2910
3727
  stores.documentMetaCache.putDocumentMeta(document.header.id, job.branch, {
2911
3728
  state: document.state.document,
2912
3729
  documentType: document.header.documentType,
3730
+ protocolVersions: document.header.protocolVersions,
2913
3731
  documentScopeRevision: 1
2914
3732
  });
2915
3733
  return buildSuccessResult(job, operation, document.header.id, document.header.documentType, resultingState, startTime);
@@ -2953,7 +3771,7 @@ var DocumentActionHandler = class {
2953
3771
  [job.scope]: [...document.operations[job.scope] ?? [], operation]
2954
3772
  };
2955
3773
  executing.touchedStreams.add(documentId, job.scope, job.branch);
2956
- stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document, SnapshotPosition.Head);
3774
+ stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document, "head");
2957
3775
  indexTxn.write([{
2958
3776
  ...operation,
2959
3777
  documentId,
@@ -2965,6 +3783,7 @@ var DocumentActionHandler = class {
2965
3783
  stores.documentMetaCache.putDocumentMeta(documentId, job.branch, {
2966
3784
  state: document.state.document,
2967
3785
  documentType: document.header.documentType,
3786
+ protocolVersions: document.header.protocolVersions,
2968
3787
  documentScopeRevision: operation.index + 1
2969
3788
  });
2970
3789
  return buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
@@ -3003,7 +3822,7 @@ var DocumentActionHandler = class {
3003
3822
  } catch (error) {
3004
3823
  return buildErrorResult(job, /* @__PURE__ */ new Error(`Failed to fetch revisions for upgrade: ${error instanceof Error ? error.message : String(error)}`), startTime);
3005
3824
  }
3006
- const revisionScopes = new Set([...Object.keys(input.revision), ...Object.keys(actualRevisions)]);
3825
+ const revisionScopes = /* @__PURE__ */ new Set([...Object.keys(input.revision), ...Object.keys(actualRevisions)]);
3007
3826
  for (const revisionScope of revisionScopes) {
3008
3827
  const snapshot = input.revision[revisionScope] ?? 0;
3009
3828
  const actual = actualRevisions[revisionScope] ?? 0;
@@ -3045,8 +3864,8 @@ var DocumentActionHandler = class {
3045
3864
  branch: job.branch
3046
3865
  });
3047
3866
  const resultingStateObj = {
3048
- header: document.header,
3049
- ...document.state
3867
+ ...document.state,
3868
+ header: document.header
3050
3869
  };
3051
3870
  if (fromVersion > 0) resultingStateObj.__migrated = true;
3052
3871
  const resultingState = JSON.stringify(resultingStateObj);
@@ -3064,7 +3883,7 @@ var DocumentActionHandler = class {
3064
3883
  [job.scope]: [...document.operations[job.scope] ?? [], operation]
3065
3884
  };
3066
3885
  executing.touchedStreams.add(documentId, job.scope, job.branch);
3067
- stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document, SnapshotPosition.Head);
3886
+ stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document, "head");
3068
3887
  for (const scope of otherScopes) executing.postCommitInvalidations.push({
3069
3888
  documentId,
3070
3889
  scope,
@@ -3081,22 +3900,41 @@ var DocumentActionHandler = class {
3081
3900
  stores.documentMetaCache.putDocumentMeta(documentId, job.branch, {
3082
3901
  state: document.state.document,
3083
3902
  documentType: document.header.documentType,
3903
+ protocolVersions: document.header.protocolVersions,
3084
3904
  documentScopeRevision: operation.index + 1
3085
3905
  });
3086
3906
  return buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
3087
3907
  }
3088
- executeAddRelationship(write, executing) {
3908
+ /** A submitted write naming a purged target was refused at job start. */
3909
+ async isTargetPurged(write, executing) {
3910
+ const target = relationshipTarget(write.action);
3911
+ if (target === void 0 || !executing.purgeFence) return false;
3912
+ return executing.purgeFence.isPurged(target);
3913
+ }
3914
+ async executeAddRelationship(write, executing) {
3915
+ let targetPurged;
3916
+ try {
3917
+ targetPurged = await this.isTargetPurged(write, executing);
3918
+ } catch (error) {
3919
+ return buildErrorResult(executing.job, error instanceof Error ? error : new Error(String(error)), executing.startTime);
3920
+ }
3089
3921
  return this.withRelationshipAction("ADD_RELATIONSHIP", write, executing, (input) => input.sourceId === input.targetId ? /* @__PURE__ */ new Error("ADD_RELATIONSHIP: sourceId and targetId cannot be the same (self-relationships not allowed)") : null, ({ indexTxn: txn, stores: s, sourceDoc, input, job: j }) => {
3090
- if (this.driveContainerTypes.has(sourceDoc.header.documentType)) {
3922
+ if (!targetPurged && this.driveContainerTypes.has(sourceDoc.header.documentType)) {
3091
3923
  const collectionId = DriveCollectionId.forDrive(input.sourceId, j.branch).key;
3092
3924
  txn.addToCollection(collectionId, input.targetId);
3093
3925
  s.collectionMembershipCache.invalidate(input.targetId);
3094
3926
  }
3095
3927
  });
3096
3928
  }
3097
- executeRemoveRelationship(write, executing) {
3929
+ async executeRemoveRelationship(write, executing) {
3930
+ let targetPurged;
3931
+ try {
3932
+ targetPurged = await this.isTargetPurged(write, executing);
3933
+ } catch (error) {
3934
+ return buildErrorResult(executing.job, error instanceof Error ? error : new Error(String(error)), executing.startTime);
3935
+ }
3098
3936
  return this.withRelationshipAction("REMOVE_RELATIONSHIP", write, executing, null, ({ indexTxn: txn, stores: s, sourceDoc, input, job: j }) => {
3099
- if (this.driveContainerTypes.has(sourceDoc.header.documentType)) {
3937
+ if (!targetPurged && this.driveContainerTypes.has(sourceDoc.header.documentType)) {
3100
3938
  const collectionId = DriveCollectionId.forDrive(input.sourceId, j.branch).key;
3101
3939
  txn.removeFromCollection(collectionId, input.targetId);
3102
3940
  s.collectionMembershipCache.invalidate(input.targetId);
@@ -3120,6 +3958,7 @@ var DocumentActionHandler = class {
3120
3958
  try {
3121
3959
  sourceDoc = await stores.writeCache.getState(input.sourceId, "document", job.branch, void 0, signal);
3122
3960
  } catch (error) {
3961
+ if (DocumentPurgedError.isError(error)) return buildErrorResult(job, error, startTime);
3123
3962
  if (DocumentNotFoundError.isError(error)) return buildErrorResult(job, new DocumentNotFoundError(input.sourceId, `${actionTypeName}: source document ${input.sourceId} not found`), startTime);
3124
3963
  return buildErrorResult(job, /* @__PURE__ */ new Error(`${actionTypeName}: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
3125
3964
  }
@@ -3150,7 +3989,7 @@ var DocumentActionHandler = class {
3150
3989
  const resultingState = JSON.stringify(resultingStateObj);
3151
3990
  executing.touchedStreams.add(input.sourceId, job.scope, job.branch);
3152
3991
  executing.touchedStreams.add(input.targetId, job.scope, job.branch);
3153
- stores.writeCache.putState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc, SnapshotPosition.Head);
3992
+ stores.writeCache.putState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc, "head");
3154
3993
  indexTxn.write([{
3155
3994
  ...operation,
3156
3995
  documentId: input.sourceId,
@@ -3169,6 +4008,7 @@ var DocumentActionHandler = class {
3169
4008
  stores.documentMetaCache.putDocumentMeta(input.sourceId, job.branch, {
3170
4009
  state: sourceDoc.state.document,
3171
4010
  documentType: sourceDoc.header.documentType,
4011
+ protocolVersions: sourceDoc.header.protocolVersions,
3172
4012
  documentScopeRevision: operation.index + 1
3173
4013
  });
3174
4014
  return buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
@@ -3197,55 +4037,669 @@ var DocumentActionHandler = class {
3197
4037
  }
3198
4038
  };
3199
4039
  //#endregion
3200
- //#region src/executor/signature-verifier.ts
3201
- var SignatureVerifier = class {
3202
- constructor(verifier) {
3203
- this.verifier = verifier;
4040
+ //#region src/signer/passthrough-signer.ts
4041
+ /**
4042
+ * A no-op signer that returns empty values for all methods.
4043
+ * Used when signing is not required.
4044
+ */
4045
+ var PassthroughSigner = class {
4046
+ publicKey = {};
4047
+ sign() {
4048
+ return Promise.resolve(/* @__PURE__ */ new Uint8Array(0));
3204
4049
  }
3205
- async verifyActions(documentId, branch, actions) {
3206
- if (!this.verifier) return;
3207
- for (const action of actions) {
3208
- const signer = action.context?.signer;
3209
- if (!signer) continue;
3210
- if (signer.signatures.length === 0) throw new InvalidSignatureError(documentId, `Action ${action.id} has signer but no signatures`);
3211
- const publicKey = signer.app.key;
3212
- let isValid;
3213
- try {
3214
- const tempOperation = {
3215
- id: deriveOperationId(documentId, action.scope, branch, action.id),
3216
- index: 0,
3217
- timestampUtcMs: action.timestampUtcMs || (/* @__PURE__ */ new Date()).toISOString(),
3218
- hash: "",
3219
- skip: 0,
3220
- action
3221
- };
3222
- isValid = await this.verifier(tempOperation, publicKey);
3223
- } catch (error) {
3224
- const errorMessage = error instanceof Error ? error.message : String(error);
3225
- throw new InvalidSignatureError(documentId, `Action ${action.id} verification failed: ${errorMessage}`);
3226
- }
3227
- if (!isValid) throw new InvalidSignatureError(documentId, `Action ${action.id} signature verification returned false`);
4050
+ verify() {
4051
+ return Promise.resolve();
4052
+ }
4053
+ signAction() {
4054
+ return Promise.resolve([
4055
+ "",
4056
+ "",
4057
+ "",
4058
+ "",
4059
+ ""
4060
+ ]);
4061
+ }
4062
+ };
4063
+ //#endregion
4064
+ //#region src/signer/trust-policy.ts
4065
+ /**
4066
+ * The policy admission asks: the reactor's own key signing as its own user is
4067
+ * accepted, then the host's policy decides, else the default does. `own` is
4068
+ * read on every ask, since its user changes on login.
4069
+ */
4070
+ function admissionTrustPolicy(own, authEnforcement, policy) {
4071
+ return { authorizeSigner(signer, key, documentId) {
4072
+ const identity = actionSignerIdentity(own);
4073
+ if (identity.app.key !== "" && key === identity.app.key) {
4074
+ if (sameUser(actionSignerIdentity(signer).user, identity.user)) return Promise.resolve(true);
3228
4075
  }
4076
+ if (policy) return policy.authorizeSigner(signer, key, documentId);
4077
+ return Promise.resolve(!authEnforcement);
4078
+ } };
4079
+ }
4080
+ function sameUser(a, b) {
4081
+ return a.address.toLowerCase() === b.address.toLowerCase() && a.networkId === b.networkId && a.chainId === b.chainId;
4082
+ }
4083
+ //#endregion
4084
+ //#region src/signer/did-key.ts
4085
+ const DID_KEY_PREFIX = "did:key:z";
4086
+ const P256_MULTICODEC = [128, 36];
4087
+ const MAX_CACHED_KEYS = 1024;
4088
+ const P = BigInt("0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff");
4089
+ const A = BigInt("0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc");
4090
+ const B = BigInt("0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b");
4091
+ /** A did:key that is not a compressed P-256 public key. */
4092
+ var MalformedDidKeyError = class extends Error {
4093
+ constructor(did, detail) {
4094
+ super(`Malformed did:key ${did}: ${detail}`);
4095
+ this.name = "MalformedDidKeyError";
3229
4096
  }
3230
- async verifyOperations(documentId, operations) {
3231
- if (!this.verifier) return;
3232
- for (let i = 0; i < operations.length; i++) {
3233
- const operation = operations[i];
3234
- const signer = operation.action.context?.signer;
3235
- if (!signer) continue;
3236
- if (signer.signatures.length === 0) throw new InvalidSignatureError(documentId, `Operation ${operation.id} at index ${operation.index} has signer but no signatures`);
3237
- const publicKey = signer.app.key;
3238
- let isValid;
3239
- try {
3240
- isValid = await this.verifier(operation, publicKey);
3241
- } catch (error) {
3242
- const errorMessage = error instanceof Error ? error.message : String(error);
3243
- throw new InvalidSignatureError(documentId, `Operation ${operation.id} at index ${operation.index} verification failed: ${errorMessage}`);
4097
+ };
4098
+ const keyCache = /* @__PURE__ */ new Map();
4099
+ /** Cached per process: decompressing the point costs a BigInt modPow. */
4100
+ async function importDidKey(did) {
4101
+ const cached = keyCache.get(did);
4102
+ if (cached) {
4103
+ keyCache.delete(did);
4104
+ keyCache.set(did, cached);
4105
+ return cached;
4106
+ }
4107
+ const raw = decompressP256(compressedPointFromDid(did), did);
4108
+ const key = await crypto.subtle.importKey("raw", raw.buffer, {
4109
+ name: "ECDSA",
4110
+ namedCurve: "P-256"
4111
+ }, false, ["verify"]);
4112
+ keyCache.set(did, key);
4113
+ if (keyCache.size > MAX_CACHED_KEYS) {
4114
+ const oldest = keyCache.keys().next().value;
4115
+ if (oldest !== void 0) keyCache.delete(oldest);
4116
+ }
4117
+ return key;
4118
+ }
4119
+ function compressedPointFromDid(did) {
4120
+ if (!did.startsWith(DID_KEY_PREFIX)) throw new MalformedDidKeyError(did, "not a base58btc did:key");
4121
+ const decoded = base58Decode(did.slice(9));
4122
+ if (!decoded || decoded.length !== 35) throw new MalformedDidKeyError(did, "not a compressed P-256 key");
4123
+ if (decoded[0] !== P256_MULTICODEC[0] || decoded[1] !== P256_MULTICODEC[1]) throw new MalformedDidKeyError(did, "not a P-256 multicodec");
4124
+ return decoded.subarray(2);
4125
+ }
4126
+ function decompressP256(compressed, did) {
4127
+ const prefix = compressed[0];
4128
+ if (prefix !== 2 && prefix !== 3) throw new MalformedDidKeyError(did, `compression prefix ${prefix}`);
4129
+ let x = BigInt(0);
4130
+ for (let i = 1; i < compressed.length; i++) x = x << BigInt(8) | BigInt(compressed[i]);
4131
+ if (x >= P) throw new MalformedDidKeyError(did, "x outside the field");
4132
+ const ySquared = (modPow(x, BigInt(3), P) + A * x + B) % P;
4133
+ let y = modPow(ySquared, (P + BigInt(1)) / BigInt(4), P);
4134
+ if (y * y % P !== ySquared) throw new MalformedDidKeyError(did, "x is not on the curve");
4135
+ if (y % BigInt(2) === BigInt(0) !== (prefix === 2)) y = P - y;
4136
+ const uncompressed = /* @__PURE__ */ new Uint8Array(65);
4137
+ uncompressed[0] = 4;
4138
+ uncompressed.set(bigIntToBytes(x), 1);
4139
+ uncompressed.set(bigIntToBytes(y), 33);
4140
+ return uncompressed;
4141
+ }
4142
+ function modPow(base, exp, mod) {
4143
+ let result = BigInt(1);
4144
+ let b = base % mod;
4145
+ let e = exp;
4146
+ while (e > BigInt(0)) {
4147
+ if (e & BigInt(1)) result = result * b % mod;
4148
+ e >>= BigInt(1);
4149
+ b = b * b % mod;
4150
+ }
4151
+ return result;
4152
+ }
4153
+ function bigIntToBytes(value) {
4154
+ const bytes = /* @__PURE__ */ new Uint8Array(32);
4155
+ let v = value;
4156
+ for (let i = 31; i >= 0; i--) {
4157
+ bytes[i] = Number(v & BigInt(255));
4158
+ v >>= BigInt(8);
4159
+ }
4160
+ return bytes;
4161
+ }
4162
+ //#endregion
4163
+ //#region src/signer/verify-action-signature.ts
4164
+ const RENOWN_HASH_LENGTH = 44;
4165
+ const SHARED_HASH_LENGTH = 28;
4166
+ const HEX_SIGNATURE = /^(0x)?([0-9a-fA-F]{2})+$/;
4167
+ /**
4168
+ * Integrity only: identity binding and the live-id check sit with the caller.
4169
+ * `operation` is the incoming operation at load admission.
4170
+ */
4171
+ async function verifyActionSignature(action, target, path, operation) {
4172
+ const policy = target.policy ?? "legacy";
4173
+ const refusal = policyRefusal(action, policy);
4174
+ if (refusal) return refusal;
4175
+ const shape = signerShape(action.context?.signer);
4176
+ if (shape.kind === "unsigned") return policy === "v2-required" ? refuse("unsigned", "UNSIGNED_REQUIRED", `action ${action.id} is unsigned but ${target.documentId} requires v2 signatures`) : {
4177
+ ok: true,
4178
+ scheme: "unsigned"
4179
+ };
4180
+ if (shape.kind === "malformed") return refuse("legacy-unknown", "MALFORMED_TUPLE", `action ${action.id} ${shape.reason}`);
4181
+ const { signer, tuple } = shape;
4182
+ const scheme = schemeOf(tuple[2]);
4183
+ if (tuple[1] !== signer.app.key) return refuse(scheme, "KEY_MISMATCH", `action ${action.id} tuple key does not match signer.app.key`);
4184
+ if (scheme !== "v2" && policy === "v2-required") return refuse(scheme, "SCHEME_BELOW_POLICY", `action ${action.id} carries a legacy tuple but ${target.documentId} requires v2 signatures`);
4185
+ if (scheme === "legacy-unknown" && path === "mutation") return refuse(scheme, "MALFORMED_TUPLE", `action ${action.id} carries a hash of unknown length ${tuple[2].length}`);
4186
+ if (scheme === "v2") {
4187
+ const refusal = await checkV2(action, tuple, target, operation);
4188
+ if (refusal) return refusal;
4189
+ } else if (path === "mutation") {
4190
+ if (await legacyHash(scheme, action, target) !== tuple[2]) return refuse(scheme, "HASH_MISMATCH", `action ${action.id} does not match the hash its signature covers`);
4191
+ }
4192
+ return verifyEcdsa(tuple, scheme, action.id);
4193
+ }
4194
+ /** Refusals a document's policy makes before any tuple is read. */
4195
+ function policyRefusal(action, policy) {
4196
+ if (action.type !== "CREATE_DOCUMENT") return;
4197
+ const scheme = schemeOfAction(action);
4198
+ const input = action.input;
4199
+ const documentId = input?.documentId ?? "";
4200
+ if (policy === "legacy") return isDerivedDocumentId(documentId) ? refuse(scheme, "ID_MISMATCH", `action ${action.id}: a legacy document cannot take the content-addressed id ${documentId}`) : void 0;
4201
+ const expected = derivedCreateId(input);
4202
+ if (expected === documentId) return;
4203
+ return refuse(scheme, "ID_MISMATCH", expected === void 0 ? `action ${action.id} creates v2-required ${documentId} without the header params its id is derived from` : `action ${action.id} creates v2-required ${documentId}, but its header params derive ${expected}`);
4204
+ }
4205
+ function derivedCreateId(input) {
4206
+ if (!input?.signing || !input.protocolVersions) return;
4207
+ try {
4208
+ return deriveDocumentId({
4209
+ documentType: input.model,
4210
+ createdAtUtcIso: input.signing.createdAtUtcIso,
4211
+ nonce: input.signing.nonce,
4212
+ protocolVersions: input.protocolVersions
4213
+ });
4214
+ } catch {
4215
+ return;
4216
+ }
4217
+ }
4218
+ function schemeOfAction(action) {
4219
+ const shape = signerShape(action.context?.signer);
4220
+ if (shape.kind === "unsigned") return "unsigned";
4221
+ return shape.kind === "signed" ? schemeOf(shape.tuple[2]) : "legacy-unknown";
4222
+ }
4223
+ /** Peers send arbitrary JSON, so nothing past `context` is assumed. */
4224
+ function signerShape(value) {
4225
+ if (value === void 0 || value === null) return { kind: "unsigned" };
4226
+ if (!isRecord(value)) return {
4227
+ kind: "malformed",
4228
+ reason: "has a signer that is not an object"
4229
+ };
4230
+ const { app, user, signatures } = value;
4231
+ if (app !== void 0 && app !== null && !isRecord(app)) return {
4232
+ kind: "malformed",
4233
+ reason: "has a signer.app that is not an object"
4234
+ };
4235
+ const key = isRecord(app) ? app.key : void 0;
4236
+ if (key !== void 0 && key !== null && typeof key !== "string") return {
4237
+ kind: "malformed",
4238
+ reason: "has a signer.app.key that is not a string"
4239
+ };
4240
+ if (!key) return { kind: "unsigned" };
4241
+ if (!isRecord(user) || typeof user.address !== "string") return {
4242
+ kind: "malformed",
4243
+ reason: "has a signer without a user address"
4244
+ };
4245
+ if (!Array.isArray(signatures)) return {
4246
+ kind: "malformed",
4247
+ reason: "has a signer whose signatures are not a list"
4248
+ };
4249
+ const tuple = signatures.at(-1);
4250
+ if (!isTuple(tuple)) return {
4251
+ kind: "malformed",
4252
+ reason: signatures.length === 0 ? "has a signer but no signatures" : "carries a malformed signature tuple"
4253
+ };
4254
+ return {
4255
+ kind: "signed",
4256
+ signer: value,
4257
+ tuple
4258
+ };
4259
+ }
4260
+ function isRecord(value) {
4261
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4262
+ }
4263
+ async function checkV2(action, tuple, target, operation) {
4264
+ const problem = v2TupleProblem(tuple);
4265
+ if (problem) return refuse("v2", "MALFORMED_TUPLE", `action ${action.id}: ${problem}`);
4266
+ if (operation && !sameInstant(operation.timestampUtcMs, action.timestampUtcMs)) return refuse("v2", "TIMESTAMP_MISMATCH", `action ${action.id} is stamped ${action.timestampUtcMs} but its operation ${operation.timestampUtcMs}`);
4267
+ let expected;
4268
+ try {
4269
+ expected = await hashActionV2(action, target, action.context.signer);
4270
+ } catch (error) {
4271
+ return refuse("v2", "HASH_MISMATCH", `action ${action.id} has no v2 hash: ${error instanceof Error ? error.message : String(error)}`);
4272
+ }
4273
+ if (expected !== tuple[2]) return refuse("v2", "HASH_MISMATCH", `action ${action.id} does not match the hash its signature covers`);
4274
+ }
4275
+ function sameInstant(a, b) {
4276
+ const left = Date.parse(a);
4277
+ return !Number.isNaN(left) && left === Date.parse(b);
4278
+ }
4279
+ function schemeOf(hash) {
4280
+ if (isV2ActionHash(hash)) return "v2";
4281
+ if (hash.length === RENOWN_HASH_LENGTH) return "legacy-renown";
4282
+ if (hash.length === SHARED_HASH_LENGTH) return "legacy-shared";
4283
+ return "legacy-unknown";
4284
+ }
4285
+ async function legacyHash(scheme, action, target) {
4286
+ if (scheme === "legacy-renown") {
4287
+ const payload = [
4288
+ action.scope,
4289
+ action.type,
4290
+ JSON.stringify(action.input)
4291
+ ].join("");
4292
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(payload));
4293
+ return bytesToBase64(new Uint8Array(digest));
4294
+ }
4295
+ const signer = action.context.signer;
4296
+ return buildOperationSignatureParams({
4297
+ documentId: target.documentId,
4298
+ signer,
4299
+ action,
4300
+ previousStateHash: ""
4301
+ })[2];
4302
+ }
4303
+ async function verifyEcdsa(tuple, scheme, actionId) {
4304
+ const [timestamp, key, hash, prevStateHash, signatureHex] = tuple;
4305
+ if (!HEX_SIGNATURE.test(signatureHex)) return refuse(scheme, "MALFORMED_TUPLE", `action ${actionId} signature is not hex`);
4306
+ let cryptoKey;
4307
+ try {
4308
+ cryptoKey = await importDidKey(key);
4309
+ } catch (error) {
4310
+ return refuse(scheme, "MALFORMED_TUPLE", `action ${actionId}: ${error instanceof Error ? error.message : String(error)}`);
4311
+ }
4312
+ const message = buildOperationSignatureMessage([
4313
+ timestamp,
4314
+ key,
4315
+ hash,
4316
+ prevStateHash
4317
+ ]);
4318
+ let valid;
4319
+ try {
4320
+ valid = await crypto.subtle.verify({
4321
+ name: "ECDSA",
4322
+ hash: "SHA-256"
4323
+ }, cryptoKey, hexToBytes(signatureHex).buffer, message.buffer);
4324
+ } catch {
4325
+ valid = false;
4326
+ }
4327
+ if (!valid) return refuse(scheme, "BAD_SIGNATURE", `action ${actionId} signature does not verify under its key`);
4328
+ return {
4329
+ ok: true,
4330
+ scheme
4331
+ };
4332
+ }
4333
+ function isTuple(value) {
4334
+ return Array.isArray(value) && value.length === 5 && value.every((element) => typeof element === "string");
4335
+ }
4336
+ function refuse(scheme, code, reason) {
4337
+ return {
4338
+ ok: false,
4339
+ scheme,
4340
+ code,
4341
+ reason
4342
+ };
4343
+ }
4344
+ function hexToBytes(hex) {
4345
+ const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
4346
+ const bytes = new Uint8Array(clean.length / 2);
4347
+ for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(clean.substring(i * 2, i * 2 + 2), 16);
4348
+ return bytes;
4349
+ }
4350
+ function bytesToBase64(bytes) {
4351
+ let binary = "";
4352
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
4353
+ return btoa(binary);
4354
+ }
4355
+ //#endregion
4356
+ //#region src/executor/synthesized-signing.ts
4357
+ /** Submitted types whose stored action the reducer writes in their place. */
4358
+ const SYNTHESIZING_TYPES = /* @__PURE__ */ new Set(["UNDO", "REDO"]);
4359
+ /**
4360
+ * The id of the action synthesized from `submittedId`. Derived, so the stored
4361
+ * operation answers for the submitted id at the live-id check.
4362
+ */
4363
+ function synthesizedActionId(submittedId) {
4364
+ return hashBrowser(`synthesized:${submittedId}`, "sha1", "hex").slice(0, 32);
4365
+ }
4366
+ /**
4367
+ * Whether `operation` stores an action the reducer made from `submitted`
4368
+ * rather than `submitted` itself or an operation already in `history`.
4369
+ */
4370
+ function isSynthesized(submitted, operation, history) {
4371
+ if (!SYNTHESIZING_TYPES.has(submitted.type)) return false;
4372
+ const id = operation.action.id;
4373
+ if (!id) return true;
4374
+ if (id === submitted.id) return false;
4375
+ return !history.some((existing) => existing.action.id === id);
4376
+ }
4377
+ /**
4378
+ * Signs the synthesized action of `operation` in place as `signer`, under the
4379
+ * id {@link synthesizedActionId} derives from the submitted one. A REDO's
4380
+ * rebuilt action has no timestamp, so it takes the submitted one; the
4381
+ * operation timestamp is set to the action's, which a peer requires of a v2
4382
+ * tuple.
4383
+ */
4384
+ async function signSynthesized(operation, submitted, target, signer, signal) {
4385
+ const synthesized = operation.action;
4386
+ const id = synthesizedActionId(submitted.id);
4387
+ const timestampUtcMs = operation.timestampUtcMs || synthesized.timestampUtcMs || submitted.timestampUtcMs;
4388
+ const action = {
4389
+ id,
4390
+ timestampUtcMs,
4391
+ type: synthesized.type,
4392
+ input: synthesized.input,
4393
+ scope: synthesized.scope
4394
+ };
4395
+ const signature = await signer.signAction(action, {
4396
+ documentId: target.documentId,
4397
+ branch: target.branch
4398
+ }, signal);
4399
+ operation.action = {
4400
+ ...action,
4401
+ context: { signer: {
4402
+ ...actionSignerIdentity(signer),
4403
+ signatures: [signature]
4404
+ } }
4405
+ };
4406
+ operation.id = deriveOperationId(target.documentId, target.scope, target.branch, id);
4407
+ operation.timestampUtcMs = timestampUtcMs;
4408
+ }
4409
+ //#endregion
4410
+ //#region src/executor/signature-admission.ts
4411
+ const DEFAULT_TRUST_TIMEOUT_MS = 1e4;
4412
+ /** Runs once per write, when this reactor first stores it; never on re-appends. */
4413
+ var SignatureAdmission = class {
4414
+ mode;
4415
+ logger;
4416
+ eventBus;
4417
+ policySource;
4418
+ trustPolicy;
4419
+ trustTimeoutMs;
4420
+ constructor(mode, logger, eventBus, policySource = "meta", trust = {
4421
+ authEnforcement: false,
4422
+ timeoutMs: DEFAULT_TRUST_TIMEOUT_MS
4423
+ }) {
4424
+ this.mode = mode;
4425
+ this.logger = logger;
4426
+ this.eventBus = eventBus;
4427
+ this.policySource = policySource;
4428
+ this.trustPolicy = admissionTrustPolicy(trust.signer ?? new PassthroughSigner(), trust.authEnforcement, trust.policy);
4429
+ this.trustTimeoutMs = trust.timeoutMs;
4430
+ }
4431
+ /** The first refusal of a mutation's submitted actions, when enforcing. */
4432
+ async admitMutation(job, stores, signal) {
4433
+ const { operationStore } = stores;
4434
+ const candidates = job.actions.map((action) => candidate(action, mutationStream(action, job)));
4435
+ const live = await this.liveOperationIds(candidates, operationStore, signal);
4436
+ const committed = await this.storedAsSubmitted(candidates, live, isRetry(job), operationStore, signal);
4437
+ await resolvePolicies(candidates, job.actions, (documentId, branch) => storedPolicy(stores, this.policySource, documentId, branch, signal));
4438
+ const submitted = /* @__PURE__ */ new Set();
4439
+ for (const entry of candidates) {
4440
+ if (committed.has(entry)) continue;
4441
+ const verdict = await this.verdict(entry, live, submitted, "mutation", signal);
4442
+ submitted.add(entry.opId);
4443
+ if (verdict.ok) continue;
4444
+ const refusal = this.record(job, entry, verdict, "mutation");
4445
+ if (refusal) return {
4446
+ kind: "refused",
4447
+ error: refusal
4448
+ };
4449
+ }
4450
+ return {
4451
+ kind: "admitted",
4452
+ committed: [...committed.values()]
4453
+ };
4454
+ }
4455
+ /** The incoming operations a load drops, empty unless enforcing. */
4456
+ async admitLoad(job, operations, stores, signal) {
4457
+ const { operationStore } = stores;
4458
+ const stream = {
4459
+ documentId: job.documentId,
4460
+ scope: job.scope,
4461
+ branch: job.branch
4462
+ };
4463
+ const candidates = operations.map((operation) => ({
4464
+ ...candidate(operation.action, stream),
4465
+ operation
4466
+ }));
4467
+ const live = await this.liveOperationIds(candidates, operationStore, signal);
4468
+ const redelivered = await this.redelivered(candidates, live, operationStore, signal);
4469
+ await resolvePolicies(candidates, job.operations.map((operation) => operation.action), (documentId, branch) => storedPolicy(stores, this.policySource, documentId, branch, signal));
4470
+ const dropped = /* @__PURE__ */ new Set();
4471
+ for (let i = 0; i < candidates.length; i++) {
4472
+ const verdict = await this.verdict(candidates[i], live, void 0, "load", signal, redelivered.has(candidates[i]));
4473
+ if (verdict.ok) continue;
4474
+ if (this.record(job, candidates[i], verdict, "load")) dropped.add(operations[i]);
4475
+ }
4476
+ return dropped;
4477
+ }
4478
+ /** A marker is held to v2 and enforced whatever the policy and mode. */
4479
+ async admitMarker(job, marker, signal) {
4480
+ const entry = {
4481
+ ...candidate(marker.action, {
4482
+ documentId: job.documentId,
4483
+ scope: "document",
4484
+ branch: job.branch
4485
+ }),
4486
+ operation: marker,
4487
+ policy: "v2-required"
4488
+ };
4489
+ const verdict = await this.verdict(entry, /* @__PURE__ */ new Set(), void 0, "load", signal);
4490
+ if (verdict.ok) return;
4491
+ return this.record(job, entry, verdict, "load", true);
4492
+ }
4493
+ async verdict(entry, live, submitted, path, signal, known = false) {
4494
+ const verdict = await verifyActionSignature(entry.action, {
4495
+ documentId: entry.stream.documentId,
4496
+ branch: entry.stream.branch,
4497
+ policy: entry.policy
4498
+ }, path, entry.operation);
4499
+ if (!verdict.ok) return verdict;
4500
+ if (!known && (isLive(entry, live) || submitted?.has(entry.opId))) return {
4501
+ ok: false,
4502
+ scheme: verdict.scheme,
4503
+ code: "DUPLICATE_ACTION",
4504
+ reason: `action ${entry.action.id} is already in the stream`
4505
+ };
4506
+ if (verdict.scheme === "unsigned") {
4507
+ const address = entry.action.context?.signer?.user?.address;
4508
+ return address ? {
4509
+ ok: false,
4510
+ scheme: "unsigned",
4511
+ code: "UNSIGNED_IDENTITY",
4512
+ reason: `action ${entry.action.id} is unsigned but claims to act as ${address}`
4513
+ } : verdict;
4514
+ }
4515
+ if (known) return verdict;
4516
+ const signer = entry.action.context.signer;
4517
+ if (!await this.authorize(signer, entry.stream.documentId, signal)) return {
4518
+ ok: false,
4519
+ scheme: verdict.scheme,
4520
+ code: "SIGNER_UNAUTHORIZED",
4521
+ reason: `action ${entry.action.id}: key ${signer.app.key} may not sign as ${signer.user.address}`
4522
+ };
4523
+ return verdict;
4524
+ }
4525
+ /** Throws when the policy does, times out or the job is aborted. */
4526
+ async authorize(signer, documentId, signal) {
4527
+ signal?.throwIfAborted();
4528
+ let timer;
4529
+ let onAbort;
4530
+ const stop = new Promise((_, reject) => {
4531
+ timer = setTimeout(() => {
4532
+ reject(/* @__PURE__ */ new Error(`signature trust policy did not answer for ${signer.app.key} within ${this.trustTimeoutMs}ms`));
4533
+ }, this.trustTimeoutMs);
4534
+ if (signal) {
4535
+ onAbort = () => reject(signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason)));
4536
+ signal.addEventListener("abort", onAbort, { once: true });
3244
4537
  }
3245
- if (!isValid) throw new InvalidSignatureError(documentId, `Operation ${operation.id} at index ${operation.index} signature verification returned false`);
4538
+ });
4539
+ try {
4540
+ return await Promise.race([this.trustPolicy.authorizeSigner(signer, signer.app.key, documentId), stop]);
4541
+ } finally {
4542
+ clearTimeout(timer);
4543
+ if (onAbort) signal?.removeEventListener("abort", onAbort);
4544
+ }
4545
+ }
4546
+ /**
4547
+ * The live candidates stored exactly as submitted and still standing: one
4548
+ * an undo or a reshuffle has since superseded is refused, not re-emitted with
4549
+ * the state it once produced. A synthesized operation
4550
+ * holds another action, so it cannot be compared; only a retry, whose first
4551
+ * attempt this job was, takes it as committed.
4552
+ */
4553
+ async storedAsSubmitted(candidates, live, retry, operationStore, signal) {
4554
+ const committed = /* @__PURE__ */ new Map();
4555
+ const toCompare = [];
4556
+ for (const entry of candidates) if (entry.synthesizedOpId && live.has(entry.synthesizedOpId)) {
4557
+ if (retry) committed.set(entry, committedWrite(entry, entry.synthesizedOpId));
4558
+ } else if (live.has(entry.opId)) toCompare.push(entry);
4559
+ for (const [stream, entries] of byStream(toCompare)) {
4560
+ const stored = await operationStore.getOperationsByIds(stream.documentId, stream.scope, stream.branch, entries.map((entry) => entry.opId), signal);
4561
+ const byId = new Map(stored.map((operation) => [operation.id, operation]));
4562
+ const matching = entries.filter((entry) => {
4563
+ const operation = byId.get(entry.opId);
4564
+ return operation && sameContent(operation.action, entry.action);
4565
+ });
4566
+ if (matching.length === 0) continue;
4567
+ const later = await operationStore.getSince(stream.documentId, stream.scope, stream.branch, Math.min(...matching.map((entry) => byId.get(entry.opId).index)), void 0, void 0, signal);
4568
+ for (const entry of matching) if (!isSuperseded(byId.get(entry.opId), later.results)) committed.set(entry, committedWrite(entry, entry.opId));
3246
4569
  }
4570
+ return committed;
4571
+ }
4572
+ /**
4573
+ * The load candidates stored exactly as they arrive, timestamp included: a
4574
+ * peer re-sending what this stream holds, not a replay of its action.
4575
+ */
4576
+ async redelivered(candidates, live, operationStore, signal) {
4577
+ const found = /* @__PURE__ */ new Set();
4578
+ const toCompare = candidates.filter((entry) => live.has(entry.opId) && !(entry.synthesizedOpId && live.has(entry.synthesizedOpId)));
4579
+ for (const [stream, entries] of byStream(toCompare)) {
4580
+ const stored = await operationStore.getOperationsByIds(stream.documentId, stream.scope, stream.branch, entries.map((entry) => entry.opId), signal);
4581
+ for (const entry of entries) if (stored.some((operation) => operation.id === entry.opId && Date.parse(operation.timestampUtcMs) === Date.parse(entry.operation?.timestampUtcMs ?? "") && sameContent(operation.action, entry.action))) found.add(entry);
4582
+ }
4583
+ return found;
4584
+ }
4585
+ async liveOperationIds(candidates, operationStore, signal) {
4586
+ const live = /* @__PURE__ */ new Set();
4587
+ for (const [stream, entries] of byStream(candidates)) {
4588
+ const found = await operationStore.findOperationIds(stream.documentId, stream.scope, stream.branch, entries.flatMap((entry) => entry.synthesizedOpId ? [entry.opId, entry.synthesizedOpId] : [entry.opId]), signal);
4589
+ for (const opId of found) live.add(opId);
4590
+ }
4591
+ return live;
4592
+ }
4593
+ /** Logs and counts a refusal; returns the error only when enforcing. */
4594
+ record(job, entry, refusal, path, enforced = this.mode === "enforce") {
4595
+ const event = {
4596
+ jobId: job.id,
4597
+ documentId: entry.stream.documentId,
4598
+ scope: entry.stream.scope,
4599
+ branch: entry.stream.branch,
4600
+ actionId: entry.action.id,
4601
+ code: refusal.code,
4602
+ scheme: refusal.scheme,
4603
+ path,
4604
+ enforced,
4605
+ reason: refusal.reason
4606
+ };
4607
+ this.logger.warn(enforced ? "Signature refused: @Refusal" : "Signature refusal logged, write admitted: @Refusal", event);
4608
+ this.eventBus.emit(ReactorEventTypes.SIGNATURE_REFUSED, event).catch((error) => {
4609
+ this.logger.error("Failed to emit SIGNATURE_REFUSED: @Error", error);
4610
+ });
4611
+ return enforced ? new InvalidSignatureError(entry.stream.documentId, refusal.code, refusal.reason) : void 0;
3247
4612
  }
3248
4613
  };
4614
+ /**
4615
+ * A CREATE_DOCUMENT is verified under its own input. Anything else takes the
4616
+ * stored document's policy, or, for a document this job creates, that CREATE's.
4617
+ */
4618
+ async function resolvePolicies(candidates, jobActions, stored) {
4619
+ const resolved = /* @__PURE__ */ new Map();
4620
+ for (const entry of candidates) {
4621
+ if (entry.action.type === "CREATE_DOCUMENT") {
4622
+ entry.policy = createPolicy(entry.action);
4623
+ continue;
4624
+ }
4625
+ const { documentId, branch } = entry.stream;
4626
+ const key = `${documentId}\u0000${branch}`;
4627
+ let policy = resolved.get(key);
4628
+ if (policy === void 0) {
4629
+ policy = await stored(documentId, branch) ?? createdPolicy(jobActions, documentId) ?? "legacy";
4630
+ resolved.set(key, policy);
4631
+ }
4632
+ entry.policy = policy;
4633
+ }
4634
+ }
4635
+ async function storedPolicy(stores, source, documentId, branch, signal) {
4636
+ try {
4637
+ if (source === "write-cache") {
4638
+ const document = await stores.writeCache.getState(documentId, "document", branch, void 0, signal);
4639
+ return signaturePolicyOf(document.header);
4640
+ }
4641
+ const meta = await stores.documentMetaCache.getDocumentMeta(documentId, branch, signal);
4642
+ return signaturePolicyOf(meta.protocolVersions);
4643
+ } catch (error) {
4644
+ if (DocumentNotFoundError.isError(error)) return;
4645
+ throw error;
4646
+ }
4647
+ }
4648
+ function createdPolicy(actions, documentId) {
4649
+ const create = actions.find((action) => action.type === "CREATE_DOCUMENT" && action.input?.documentId === documentId);
4650
+ return create ? createPolicy(create) : void 0;
4651
+ }
4652
+ function createPolicy(action) {
4653
+ return signaturePolicyOf(action.input);
4654
+ }
4655
+ function candidate(action, stream) {
4656
+ const opId = (actionId) => deriveOperationId(stream.documentId, stream.scope, stream.branch, actionId);
4657
+ return {
4658
+ action,
4659
+ stream,
4660
+ opId: opId(action.id),
4661
+ synthesizedOpId: SYNTHESIZING_TYPES.has(action.type) ? opId(synthesizedActionId(action.id)) : void 0
4662
+ };
4663
+ }
4664
+ function isLive(entry, live) {
4665
+ return live.has(entry.opId) || entry.synthesizedOpId !== void 0 && live.has(entry.synthesizedOpId);
4666
+ }
4667
+ function mutationStream(action, job) {
4668
+ return {
4669
+ ...actionSigningTarget(action, job.documentId, job.branch),
4670
+ scope: job.scope
4671
+ };
4672
+ }
4673
+ function byStream(candidates) {
4674
+ const groups = /* @__PURE__ */ new Map();
4675
+ for (const entry of candidates) {
4676
+ const key = `${entry.stream.documentId}\u0000${entry.stream.scope}\u0000${entry.stream.branch}`;
4677
+ const group = groups.get(key) ?? [entry.stream, []];
4678
+ group[1].push(entry);
4679
+ groups.set(key, group);
4680
+ }
4681
+ return [...groups.values()];
4682
+ }
4683
+ function isRetry(job) {
4684
+ return job.errorHistory.length > 0 || (job.retryCount ?? 0) > 0;
4685
+ }
4686
+ function committedWrite(entry, opId) {
4687
+ return {
4688
+ ...entry.stream,
4689
+ actionId: entry.action.id,
4690
+ opId
4691
+ };
4692
+ }
4693
+ function isSuperseded(operation, later) {
4694
+ return later.some((next) => next.index > operation.index && next.skip > 0 && next.index - next.skip <= operation.index);
4695
+ }
4696
+ function sameContent(stored, submitted) {
4697
+ try {
4698
+ return canonicalJson(stored) === canonicalJson(submitted);
4699
+ } catch {
4700
+ return false;
4701
+ }
4702
+ }
3249
4703
  //#endregion
3250
4704
  //#region src/executor/simple-job-executor.ts
3251
4705
  const MAX_SKIP_THRESHOLD = 1e3;
@@ -3262,7 +4716,32 @@ function isValidISOTimestamp(value) {
3262
4716
  * returned JobResult there, which is what the queue, the worker protocol and
3263
4717
  * every test expect a failed job to look like.
3264
4718
  */
4719
+ /** The ids of a purge job's request, which a drive purge may precede. */
4720
+ function purgeRequestDocumentIds(job) {
4721
+ const ids = job.meta.purgeRequestDocumentIds;
4722
+ return Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
4723
+ }
4724
+ const MARKER_INPUT_KEYS = /* @__PURE__ */ new Set([
4725
+ "documentId",
4726
+ "documentType",
4727
+ "purgedAtUtcIso",
4728
+ "requestId"
4729
+ ]);
4730
+ /** A peer's marker must name this job's id on main and carry nothing else. */
4731
+ function malformedMarker(job) {
4732
+ const action = job.operations.find((operation) => isPurgeMarker(operation))?.action;
4733
+ const input = action?.input;
4734
+ const refuse = (reason) => new InvalidSignatureError(job.documentId, "ID_MISMATCH", `marker ${action?.id ?? "?"} ${reason}`);
4735
+ if (typeof input !== "object" || input === null) return refuse("carries no input");
4736
+ const extra = Object.keys(input).filter((key) => !MARKER_INPUT_KEYS.has(key));
4737
+ if (extra.length > 0) return refuse(`carries unexpected input ${extra.join(", ")}`);
4738
+ for (const key of MARKER_INPUT_KEYS) if (typeof input[key] !== "string" || input[key] === "") return refuse(`input.${key} is not a non-empty string`);
4739
+ const purgedAt = Date.parse(input.purgedAtUtcIso);
4740
+ if (Number.isNaN(purgedAt) || new Date(purgedAt).toISOString() !== input.purgedAtUtcIso) return refuse("input.purgedAtUtcIso is not an ISO timestamp");
4741
+ if (input.documentId !== job.documentId || job.branch !== "main" || action?.scope !== "document") return refuse(`does not purge ${job.documentId} on document/main`);
4742
+ }
3265
4743
  var JobRollbackSignal = class extends Error {
4744
+ result;
3266
4745
  constructor(result) {
3267
4746
  super("job rolled back");
3268
4747
  this.result = result;
@@ -3270,13 +4749,27 @@ var JobRollbackSignal = class extends Error {
3270
4749
  }
3271
4750
  };
3272
4751
  var SimpleJobExecutor = class {
4752
+ logger;
4753
+ registry;
4754
+ operationStore;
4755
+ eventBus;
4756
+ writeCache;
4757
+ operationIndex;
4758
+ documentMetaCache;
4759
+ collectionMembershipCache;
4760
+ driveContainerTypes;
3273
4761
  config;
3274
4762
  featureFlags;
3275
4763
  decisionModel;
3276
- signatureVerifierModule;
4764
+ signatureAdmission;
3277
4765
  documentActionHandler;
3278
4766
  executionScope;
3279
- constructor(logger, registry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, driveContainerTypes, config, signatureVerifier, executionScope) {
4767
+ signer;
4768
+ /**
4769
+ * `signer` signs the operations the reducer synthesizes; unsigned if omitted.
4770
+ * `trustPolicy` decides which keys may sign as which users at admission.
4771
+ */
4772
+ constructor(logger, registry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, driveContainerTypes, config, executionScope, signer, trustPolicy) {
3280
4773
  this.logger = logger;
3281
4774
  this.registry = registry;
3282
4775
  this.operationStore = operationStore;
@@ -3286,6 +4779,8 @@ var SimpleJobExecutor = class {
3286
4779
  this.documentMetaCache = documentMetaCache;
3287
4780
  this.collectionMembershipCache = collectionMembershipCache;
3288
4781
  this.driveContainerTypes = driveContainerTypes;
4782
+ this.signer = signer ?? new PassthroughSigner();
4783
+ this.featureFlags = resolveFeatureFlags(config.featureFlags);
3289
4784
  this.config = {
3290
4785
  featureFlags: config.featureFlags ?? {},
3291
4786
  maxSkipThreshold: config.maxSkipThreshold ?? MAX_SKIP_THRESHOLD,
@@ -3295,12 +4790,19 @@ var SimpleJobExecutor = class {
3295
4790
  retryBaseDelayMs: config.retryBaseDelayMs ?? 100,
3296
4791
  retryMaxDelayMs: config.retryMaxDelayMs ?? 5e3,
3297
4792
  yieldDeadlineMs: config.yieldDeadlineMs ?? 50,
3298
- batchApplies: config.batchApplies ?? true
4793
+ batchApplies: config.batchApplies ?? true,
4794
+ signatureVerification: config.signatureVerification ?? "enforce",
4795
+ protocolSupport: config.protocolSupport ?? localSupports(PEER_CAPABILITIES, this.featureFlags).protocols,
4796
+ maxPurgeOperations: config.maxPurgeOperations ?? 2e5
3299
4797
  };
3300
- this.featureFlags = resolveFeatureFlags(config.featureFlags);
3301
4798
  this.decisionModel = selectDecisionModel(this.featureFlags, registry);
3302
- this.signatureVerifierModule = new SignatureVerifier(signatureVerifier);
3303
- this.documentActionHandler = new DocumentActionHandler(registry, logger, driveContainerTypes, this.featureFlags, this.decisionModel);
4799
+ this.signatureAdmission = new SignatureAdmission(this.config.signatureVerification, logger, eventBus, this.featureFlags.documentDecisions ? "write-cache" : "meta", {
4800
+ signer: this.signer,
4801
+ authEnforcement: this.featureFlags.authEnforcement,
4802
+ policy: trustPolicy,
4803
+ timeoutMs: Math.min(DEFAULT_TRUST_TIMEOUT_MS, this.config.jobTimeoutMs / 2)
4804
+ });
4805
+ this.documentActionHandler = new DocumentActionHandler(registry, logger, driveContainerTypes, this.featureFlags, this.decisionModel, this.config.protocolSupport);
3304
4806
  this.executionScope = executionScope ?? new DefaultExecutionScope(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache);
3305
4807
  }
3306
4808
  /**
@@ -3327,6 +4829,7 @@ var SimpleJobExecutor = class {
3327
4829
  const touchedStreams = new TouchedStreams();
3328
4830
  const postCommitInvalidations = [];
3329
4831
  const postCommitMembershipInvalidations = [];
4832
+ const postCommitMetaInvalidations = [];
3330
4833
  let outcome;
3331
4834
  try {
3332
4835
  outcome = await this.executionScope.run(async (stores) => {
@@ -3337,7 +4840,8 @@ var SimpleJobExecutor = class {
3337
4840
  signal,
3338
4841
  touchedStreams,
3339
4842
  postCommitInvalidations,
3340
- postCommitMembershipInvalidations
4843
+ postCommitMembershipInvalidations,
4844
+ postCommitMetaInvalidations
3341
4845
  });
3342
4846
  if (!scoped.result.success) throw new JobRollbackSignal(scoped.result);
3343
4847
  return scoped;
@@ -3349,6 +4853,7 @@ var SimpleJobExecutor = class {
3349
4853
  }
3350
4854
  for (const entry of postCommitInvalidations) this.writeCache.invalidate(entry.documentId, entry.scope, entry.branch);
3351
4855
  for (const documentId of postCommitMembershipInvalidations) this.collectionMembershipCache.invalidate(documentId);
4856
+ for (const documentId of postCommitMetaInvalidations) this.documentMetaCache.invalidate(documentId);
3352
4857
  const { pendingEvent } = outcome;
3353
4858
  if (pendingEvent) this.eventBus.emit(ReactorEventTypes.JOB_WRITE_READY, pendingEvent).catch((error) => {
3354
4859
  this.logger.error("Failed to emit JOB_WRITE_READY event: @Event : @Error", pendingEvent, error);
@@ -3364,8 +4869,38 @@ var SimpleJobExecutor = class {
3364
4869
  * has not committed yet has nothing to announce.
3365
4870
  */
3366
4871
  async executeInScope(params) {
3367
- const { job, startTime, stores, signal, touchedStreams, postCommitInvalidations, postCommitMembershipInvalidations } = params;
4872
+ const { job, startTime, stores: scopeStores, signal, touchedStreams, postCommitInvalidations, postCommitMembershipInvalidations } = params;
3368
4873
  let pendingEvent;
4874
+ if (job.kind === "mutation") {
4875
+ const reserved = job.actions.find((action) => isPurgeMarker(action));
4876
+ if (reserved) return { result: buildErrorResult(job, new ReservedActionError(job.documentId, reserved.type), startTime) };
4877
+ }
4878
+ if (job.kind === "purge" || job.kind === "load" && job.operations.some((op) => isPurgeMarker(op))) return this.executePurge(params);
4879
+ const lockedIds = jobWriteIds(job);
4880
+ const lockFailure = (error) => ({ result: buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime) });
4881
+ try {
4882
+ await scopeStores.documentLocks.shared(lockedIds);
4883
+ } catch (error) {
4884
+ return lockFailure(error);
4885
+ }
4886
+ let purged;
4887
+ try {
4888
+ purged = await scopeStores.documentLocks.purged(lockedIds);
4889
+ } catch (error) {
4890
+ return lockFailure(error);
4891
+ }
4892
+ const purgedRefusal = this.purgedRefusal(job, purged);
4893
+ if (purgedRefusal) return { result: buildErrorResult(job, purgedRefusal, startTime) };
4894
+ const purgeFence = new PurgeFence(scopeStores.documentLocks, new Set(lockedIds), purged);
4895
+ const stores = {
4896
+ ...scopeStores,
4897
+ writeCache: new FencedWriteCache(scopeStores.writeCache, purgeFence, (documentId) => {
4898
+ scopeStores.writeCache.invalidate(documentId);
4899
+ scopeStores.documentMetaCache.invalidate(documentId);
4900
+ })
4901
+ };
4902
+ const unsupported = await this.unsupportedStoredProtocol(job, stores, signal);
4903
+ if (unsupported) return { result: buildErrorResult(job, unsupported, startTime) };
3369
4904
  const indexTxn = stores.operationIndex.start();
3370
4905
  if (job.kind === "load") {
3371
4906
  const loadResult = await this.executeLoadJob({
@@ -3378,7 +4913,8 @@ var SimpleJobExecutor = class {
3378
4913
  evaluatedByPosition: false,
3379
4914
  postCommitInvalidations,
3380
4915
  postCommitMembershipInvalidations,
3381
- touchedStreams
4916
+ touchedStreams,
4917
+ purgeFence
3382
4918
  });
3383
4919
  if (loadResult.success && loadResult.operationsWithContext) {
3384
4920
  const ordinals = await stores.operationIndex.commit(indexTxn, signal);
@@ -3408,7 +4944,8 @@ var SimpleJobExecutor = class {
3408
4944
  evaluatedByPosition: false,
3409
4945
  postCommitInvalidations,
3410
4946
  postCommitMembershipInvalidations,
3411
- touchedStreams
4947
+ touchedStreams,
4948
+ purgeFence
3412
4949
  });
3413
4950
  if (reevalResult.success && reevalResult.operationsWithContext) {
3414
4951
  const ordinals = await stores.operationIndex.commit(indexTxn, signal);
@@ -3429,10 +4966,29 @@ var SimpleJobExecutor = class {
3429
4966
  pendingEvent
3430
4967
  };
3431
4968
  }
3432
- const positioned = await this.positionByTimestamp(job, stores, signal);
4969
+ let admission;
4970
+ try {
4971
+ admission = await this.signatureAdmission.admitMutation(job, stores, signal);
4972
+ } catch (error) {
4973
+ return { result: buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime) };
4974
+ }
4975
+ if (admission.kind === "refused") return { result: buildErrorResult(job, admission.error, startTime) };
4976
+ let committed;
4977
+ try {
4978
+ committed = await this.reloadCommitted(admission.committed, stores, signal);
4979
+ } catch (error) {
4980
+ return { result: buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime) };
4981
+ }
4982
+ const committedIds = new Set(admission.committed.map((write) => write.actionId));
4983
+ const fresh = committedIds.size === 0 ? job : {
4984
+ ...job,
4985
+ actions: job.actions.filter((action) => !committedIds.has(action.id))
4986
+ };
4987
+ if (fresh.actions.length === 0) return this.committedOutcome(job, committed, [], [], stores, startTime);
4988
+ const positioned = await this.positionByTimestamp(fresh, stores, signal);
3433
4989
  if (positioned.error) return { result: buildErrorResult(job, positioned.error, startTime) };
3434
4990
  const executing = {
3435
- job,
4991
+ job: fresh,
3436
4992
  startTime,
3437
4993
  indexTxn,
3438
4994
  stores,
@@ -3441,7 +4997,8 @@ var SimpleJobExecutor = class {
3441
4997
  evaluatedByPosition: positioned.evaluatedByPosition,
3442
4998
  postCommitInvalidations,
3443
4999
  postCommitMembershipInvalidations,
3444
- touchedStreams
5000
+ touchedStreams,
5001
+ purgeFence
3445
5002
  };
3446
5003
  const actionResult = await this.processActions(positioned.writes, executing);
3447
5004
  if (!actionResult.success) return { result: {
@@ -3462,13 +5019,291 @@ var SimpleJobExecutor = class {
3462
5019
  } };
3463
5020
  const ordinals = await stores.operationIndex.commit(indexTxn, signal);
3464
5021
  postCommitMembershipInvalidations.push(...indexTxn.getMembershipInvalidations());
3465
- if (actionResult.operationsWithContext.length > 0) {
3466
- for (let i = 0; i < actionResult.operationsWithContext.length; i++) actionResult.operationsWithContext[i].context.ordinal = ordinals[i];
3467
- const collectionMemberships = await this.getCollectionMembershipsForOperations(actionResult.operationsWithContext, stores);
5022
+ for (let i = 0; i < actionResult.operationsWithContext.length; i++) actionResult.operationsWithContext[i].context.ordinal = ordinals[i];
5023
+ return this.committedOutcome(job, committed, actionResult.operationsWithContext, actionResult.generatedOperations, stores, startTime);
5024
+ }
5025
+ /** A load names only its own id purged; a foreign one is ID_MISMATCH. */
5026
+ purgedRefusal(job, purged) {
5027
+ if (purged.size === 0) return;
5028
+ if (purged.has(job.documentId)) return new DocumentPurgedError(job.documentId);
5029
+ const actions = [...job.actions, ...job.operations.map((operation) => operation.action)];
5030
+ for (const action of actions) {
5031
+ if (!DOCUMENT_SCOPE_ACTIONS.has(action.type)) continue;
5032
+ const target = targetDocumentId(action, job.documentId);
5033
+ if (!purged.has(target)) continue;
5034
+ if (job.kind === "mutation") return new DocumentPurgedError(target);
5035
+ return new InvalidSignatureError(job.documentId, "ID_MISMATCH", `${action.type} ${action.id} in ${job.documentId} writes purged ${target}`);
5036
+ }
5037
+ if (job.kind !== "mutation") return;
5038
+ for (const action of job.actions) {
5039
+ const target = relationshipTarget(action);
5040
+ if (action.type === "ADD_RELATIONSHIP" && target !== void 0 && purged.has(target)) return new DocumentPurgedError(target, `${action.type} target ${target} was purged`);
5041
+ if (action.scope !== "auth") continue;
5042
+ const group = mentionedGroupIds(action).find((id) => purged.has(id));
5043
+ if (group !== void 0) return new DocumentPurgedError(group, `${action.type} names purged group ${group}`);
5044
+ }
5045
+ }
5046
+ /** A purge job or a marker load: the exclusive lock is the job's only lock. */
5047
+ async executePurge(params) {
5048
+ const { job, startTime, stores } = params;
5049
+ const fail = (error) => ({ result: buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime) });
5050
+ if (job.kind === "load") {
5051
+ const malformed = malformedMarker(job);
5052
+ if (malformed) return fail(malformed);
5053
+ }
5054
+ const purger = stores.purger;
5055
+ if (!purger) return fail(/* @__PURE__ */ new Error(`Purge of ${job.documentId} needs a transactional scope`));
5056
+ try {
5057
+ await stores.documentLocks.exclusive(job.documentId);
5058
+ } catch (error) {
5059
+ return fail(error);
5060
+ }
5061
+ let alreadyPurged;
5062
+ try {
5063
+ alreadyPurged = (await stores.documentLocks.purged([job.documentId])).has(job.documentId);
5064
+ } catch (error) {
5065
+ return fail(error);
5066
+ }
5067
+ if (alreadyPurged) return {
5068
+ result: {
5069
+ job,
5070
+ success: true,
5071
+ operations: [],
5072
+ operationsWithContext: [],
5073
+ duration: Date.now() - startTime
5074
+ },
5075
+ pendingEvent: {
5076
+ jobId: job.id,
5077
+ operations: [],
5078
+ jobMeta: job.meta,
5079
+ collectionMemberships: {}
5080
+ }
5081
+ };
5082
+ let commit;
5083
+ try {
5084
+ commit = job.kind === "purge" ? await this.preparePurgeJob(params, purger) : await this.preparePurgeLoad(params, purger);
5085
+ } catch (error) {
5086
+ return fail(error);
5087
+ }
5088
+ if (commit instanceof Error) return fail(commit);
5089
+ try {
5090
+ return await this.commitPurge(params, commit);
5091
+ } catch (error) {
5092
+ return fail(error);
5093
+ }
5094
+ }
5095
+ /** Preconditions 1-5, then the marker this host signs. */
5096
+ async preparePurgeJob(params, purger) {
5097
+ const { job, stores, signal } = params;
5098
+ const documentId = job.documentId;
5099
+ if (!job.purge) return /* @__PURE__ */ new Error(`Purge job ${job.id} carries no purge options`);
5100
+ const held = await this.heldStreams(purger, documentId);
5101
+ if (held.branches.length === 0) return new DocumentNotDeletedError(documentId, `Document ${documentId} is not held here`);
5102
+ let documentType;
5103
+ for (const branch of held.branches) {
5104
+ let meta;
5105
+ try {
5106
+ meta = await stores.documentMetaCache.getDocumentMeta(documentId, branch, signal);
5107
+ } catch (error) {
5108
+ return new DocumentNotDeletedError(documentId, `Document ${documentId} has no readable state on ${branch}: ${error instanceof Error ? error.message : String(error)}`);
5109
+ }
5110
+ if (!meta.state.isDeleted) return new DocumentNotDeletedError(documentId, `Document ${documentId} is not deleted on branch ${branch}`);
5111
+ documentType ??= meta.documentType;
5112
+ }
5113
+ if (documentType === void 0) return new DocumentNotDeletedError(documentId);
5114
+ if (documentType === groupDocumentType) {
5115
+ const referencers = await purger.groupReferencersInHistory(documentId);
5116
+ if (referencers.length > 0) return new GroupInUseError(documentId, referencers);
5117
+ }
5118
+ let survivors = [];
5119
+ if (this.driveContainerTypes.has(documentType)) {
5120
+ const requested = new Set(purgeRequestDocumentIds(job));
5121
+ const members = await this.driveMembers(purger, stores, documentId, held.branches);
5122
+ const blocking = members.required.filter((id) => !requested.has(id));
5123
+ if (blocking.length > 0) return new DocumentNotDeletedError(documentId, `Drive ${documentId} cannot be purged: ${blocking.join(", ")} neither purged nor in the request`);
5124
+ survivors = members.survivors;
5125
+ }
5126
+ const operationCount = await purger.operationCount(documentId);
5127
+ if (operationCount > this.config.maxPurgeOperations && !job.purge.allowLarge) return new PurgeTooLargeError(documentId, operationCount, this.config.maxPurgeOperations);
5128
+ return {
5129
+ purger,
5130
+ marker: await this.signMarker(documentId, documentType, job.purge.requestId, signal),
5131
+ documentType,
5132
+ held,
5133
+ sourceRemote: "",
5134
+ collectionIds: [],
5135
+ survivors
5136
+ };
5137
+ }
5138
+ /** A peer's marker, admitted alone; a receiver cannot refuse an erasure. */
5139
+ async preparePurgeLoad(params, purger) {
5140
+ const { job, stores, signal } = params;
5141
+ const documentId = job.documentId;
5142
+ const marker = job.operations.find((operation) => isPurgeMarker(operation));
5143
+ const refusal = await this.signatureAdmission.admitMarker(job, marker, signal);
5144
+ if (refusal) return refusal;
5145
+ const held = await this.heldStreams(purger, documentId);
5146
+ let documentType = marker.action.input.documentType;
5147
+ let appliedDeletion = held.branches.length > 0;
5148
+ if (held.branches.length > 0) try {
5149
+ const meta = await stores.documentMetaCache.getDocumentMeta(documentId, held.branches.includes("main") ? "main" : held.branches[0], signal);
5150
+ documentType = meta.documentType;
5151
+ appliedDeletion = !meta.state.isDeleted;
5152
+ } catch {}
5153
+ let survivors = [];
5154
+ if (this.driveContainerTypes.has(documentType)) {
5155
+ const members = await this.driveMembers(purger, stores, documentId, held.branches);
5156
+ survivors = [...members.required, ...members.survivors];
5157
+ }
5158
+ const sourceRemote = typeof job.meta.sourceRemote === "string" ? job.meta.sourceRemote : "";
5159
+ const collectionId = sourceRemote ? await purger.remoteCollection(sourceRemote) : void 0;
5160
+ return {
5161
+ purger,
5162
+ marker: purgeMarkerOperation(marker.action),
5163
+ documentType,
5164
+ held,
5165
+ sourceRemote,
5166
+ collectionIds: collectionId === void 0 ? [] : [collectionId],
5167
+ survivors,
5168
+ ...appliedDeletion ? { appliedDeletion: true } : {}
5169
+ };
5170
+ }
5171
+ /** Execution: delete, write the marker and its twin, tombstone, reopen. */
5172
+ async commitPurge(params, commit) {
5173
+ const { job, startTime, stores, signal, touchedStreams } = params;
5174
+ const { purger, marker, documentType, held, sourceRemote } = commit;
5175
+ const documentId = job.documentId;
5176
+ const streams = [...held.streams, {
5177
+ scope: "document",
5178
+ branch: "main"
5179
+ }];
5180
+ for (const stream of streams) {
5181
+ touchedStreams.add(documentId, stream.scope, stream.branch);
5182
+ params.postCommitInvalidations.push({
5183
+ documentId,
5184
+ ...stream
5185
+ });
5186
+ }
5187
+ params.postCommitMetaInvalidations.push(documentId);
5188
+ const removedRows = await purger.deleteRows(documentId);
5189
+ const [stored] = await stores.operationStore.apply(documentId, documentType, "document", "main", 0, (txn) => {
5190
+ txn.addOperations(marker);
5191
+ }, signal);
5192
+ const indexTxn = stores.operationIndex.start();
5193
+ indexTxn.write([{
5194
+ ...stored,
5195
+ documentId,
5196
+ documentType,
5197
+ scope: "document",
5198
+ branch: "main",
5199
+ sourceRemote
5200
+ }]);
5201
+ const [ordinal] = await stores.operationIndex.commit(indexTxn, signal);
5202
+ await purger.writeTombstone({
5203
+ documentId,
5204
+ ordinal,
5205
+ removedRows,
5206
+ purgedAtUtc: marker.action.input.purgedAtUtcIso,
5207
+ requestId: marker.action.input.requestId
5208
+ });
5209
+ const collections = await purger.reopenMemberships(documentId, ordinal, commit.collectionIds);
5210
+ params.postCommitMembershipInvalidations.push(documentId, ...commit.survivors, ...indexTxn.getMembershipInvalidations());
5211
+ const operationWithContext = {
5212
+ operation: stored,
5213
+ context: {
5214
+ documentId,
5215
+ scope: "document",
5216
+ branch: "main",
5217
+ documentType,
5218
+ ordinal,
5219
+ ...commit.appliedDeletion ? { appliedDeletion: true } : {}
5220
+ }
5221
+ };
5222
+ return {
5223
+ result: {
5224
+ job,
5225
+ success: true,
5226
+ operations: [stored],
5227
+ operationsWithContext: [operationWithContext],
5228
+ duration: Date.now() - startTime
5229
+ },
5230
+ pendingEvent: {
5231
+ jobId: job.id,
5232
+ operations: [operationWithContext],
5233
+ jobMeta: job.meta,
5234
+ collectionMemberships: { [documentId]: collections }
5235
+ }
5236
+ };
5237
+ }
5238
+ async heldStreams(purger, documentId) {
5239
+ const streams = await purger.streams(documentId);
5240
+ return {
5241
+ streams,
5242
+ branches: [...new Set(streams.filter((stream) => stream.scope === "document").map((stream) => stream.branch))]
5243
+ };
5244
+ }
5245
+ /** Members a drive purge needs gone, and those living on elsewhere. */
5246
+ async driveMembers(purger, stores, driveId, branches) {
5247
+ const required = /* @__PURE__ */ new Set();
5248
+ const survivors = /* @__PURE__ */ new Set();
5249
+ for (const branch of branches.length > 0 ? branches : ["main"]) {
5250
+ const collectionId = DriveCollectionId.forDrive(driveId, branch).key;
5251
+ const members = await purger.collectionMembers(collectionId, driveId, branch);
5252
+ const purged = await stores.documentLocks.purged(members.map((member) => member.documentId));
5253
+ for (const member of members) {
5254
+ if (purged.has(member.documentId)) continue;
5255
+ if (member.required) required.add(member.documentId);
5256
+ else survivors.add(member.documentId);
5257
+ }
5258
+ }
5259
+ return {
5260
+ required: [...required],
5261
+ survivors: [...survivors]
5262
+ };
5263
+ }
5264
+ /** The marker, signed by this executor's signer as any peer admits it. */
5265
+ async signMarker(documentId, documentType, requestId, signal) {
5266
+ const action = purgeDocumentAction({
5267
+ documentId,
5268
+ documentType,
5269
+ requestId
5270
+ });
5271
+ const signature = await this.signer.signAction(action, {
5272
+ documentId,
5273
+ branch: "main"
5274
+ }, signal);
5275
+ return purgeMarkerOperation({
5276
+ ...action,
5277
+ context: { signer: {
5278
+ ...actionSignerIdentity(this.signer),
5279
+ signatures: [signature]
5280
+ } }
5281
+ });
5282
+ }
5283
+ /** A stored document at a version this reactor does not run is read-only here. */
5284
+ async unsupportedStoredProtocol(job, stores, signal) {
5285
+ let versions;
5286
+ try {
5287
+ versions = this.featureFlags.documentDecisions && job.kind !== "load" ? (await stores.writeCache.getState(job.documentId, "document", job.branch, void 0, signal)).header.protocolVersions : (await stores.documentMetaCache.getDocumentMeta(job.documentId, job.branch, signal)).protocolVersions;
5288
+ } catch {
5289
+ return;
5290
+ }
5291
+ return this.documentActionHandler.unsupportedProtocol(job.documentId, versions);
5292
+ }
5293
+ /**
5294
+ * A successful mutation's result and the write-ready event it owes, carrying
5295
+ * what earlier attempts committed ahead of what this one wrote.
5296
+ */
5297
+ async committedOutcome(job, reloaded, written, generated, stores, startTime) {
5298
+ const operationsWithContext = [...reloaded, ...written];
5299
+ let pendingEvent;
5300
+ if (operationsWithContext.length > 0) {
5301
+ const collectionMemberships = await this.getCollectionMembershipsForOperations(operationsWithContext, stores);
3468
5302
  pendingEvent = {
3469
5303
  jobId: job.id,
3470
- operations: actionResult.operationsWithContext,
5304
+ operations: operationsWithContext,
3471
5305
  jobMeta: job.meta,
5306
+ submittedActionIds: submittedActionIds(job),
3472
5307
  collectionMemberships
3473
5308
  };
3474
5309
  }
@@ -3476,14 +5311,62 @@ var SimpleJobExecutor = class {
3476
5311
  result: {
3477
5312
  job,
3478
5313
  success: true,
3479
- operations: actionResult.generatedOperations,
3480
- operationsWithContext: actionResult.operationsWithContext,
5314
+ operations: [...reloaded.map((entry) => entry.operation), ...generated],
5315
+ operationsWithContext,
3481
5316
  duration: Date.now() - startTime
3482
5317
  },
3483
5318
  pendingEvent
3484
5319
  };
3485
5320
  }
3486
5321
  /**
5322
+ * The stored operations of writes an earlier attempt committed, as a fresh
5323
+ * write would report them. State is rebuilt from the write cache, and left
5324
+ * out when it cannot be.
5325
+ */
5326
+ async reloadCommitted(writes, stores, signal) {
5327
+ const streams = /* @__PURE__ */ new Map();
5328
+ for (const write of writes) {
5329
+ const key = `${write.documentId}\u0000${write.scope}\u0000${write.branch}`;
5330
+ streams.set(key, [...streams.get(key) ?? [], write]);
5331
+ }
5332
+ const reloaded = [];
5333
+ for (const group of streams.values()) {
5334
+ const { documentId, scope, branch } = group[0];
5335
+ const opIds = [...new Set(group.map((write) => write.opId))];
5336
+ const operations = await stores.operationStore.getOperationsByIds(documentId, scope, branch, opIds, signal);
5337
+ const ordinals = await stores.operationIndex.getOrdinalsByOpIds(documentId, scope, branch, opIds, signal);
5338
+ const { documentType } = await stores.documentMetaCache.getDocumentMeta(documentId, branch, signal);
5339
+ for (const operation of operations.sort((a, b) => a.index - b.index)) reloaded.push({
5340
+ operation,
5341
+ context: {
5342
+ documentId,
5343
+ scope,
5344
+ branch,
5345
+ documentType,
5346
+ resultingState: await this.rebuiltState(stores, {
5347
+ documentId,
5348
+ scope,
5349
+ branch
5350
+ }, operation, signal),
5351
+ ordinal: ordinals.get(operation.id) ?? 0
5352
+ }
5353
+ });
5354
+ }
5355
+ return reloaded;
5356
+ }
5357
+ async rebuiltState(stores, stream, operation, signal) {
5358
+ try {
5359
+ const document = await stores.writeCache.getState(stream.documentId, stream.scope, stream.branch, operation.index, signal);
5360
+ return JSON.stringify({
5361
+ ...document.state,
5362
+ header: document.header
5363
+ });
5364
+ } catch (error) {
5365
+ this.logger.warn("No state for committed operation @OperationId in @Stream: @Error", operation.id, stream, error);
5366
+ return;
5367
+ }
5368
+ }
5369
+ /**
3487
5370
  * Drops the cached state of every stream a job wrote, after its transaction
3488
5371
  * did not commit.
3489
5372
  *
@@ -3512,16 +5395,6 @@ var SimpleJobExecutor = class {
3512
5395
  const actions = writes.map((write) => write.action);
3513
5396
  const generatedOperations = [];
3514
5397
  const operationsWithContext = [];
3515
- try {
3516
- await this.signatureVerifierModule.verifyActions(job.documentId, job.branch, actions);
3517
- } catch (error) {
3518
- return {
3519
- success: false,
3520
- generatedOperations,
3521
- operationsWithContext,
3522
- error: error instanceof Error ? error : new Error(String(error))
3523
- };
3524
- }
3525
5398
  for (const action of actions) if (action.timestampUtcMs && !isValidISOTimestamp(action.timestampUtcMs)) return {
3526
5399
  success: false,
3527
5400
  generatedOperations,
@@ -3619,7 +5492,7 @@ var SimpleJobExecutor = class {
3619
5492
  if (docMeta.state.isDeleted) return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
3620
5493
  documentVersion = docMeta.state.version;
3621
5494
  }
3622
- if (isUndoRedo(action) || action.type === "PRUNE" || skip > 0) stores.writeCache.invalidate(job.documentId, job.scope, job.branch);
5495
+ if (isUndoRedo(action) || skip > 0) stores.writeCache.invalidate(job.documentId, job.scope, job.branch);
3623
5496
  let document;
3624
5497
  if (baseDocument !== void 0) document = baseDocument;
3625
5498
  else try {
@@ -3679,18 +5552,27 @@ var SimpleJobExecutor = class {
3679
5552
  branch: job.branch,
3680
5553
  protocolVersion
3681
5554
  };
3682
- updatedDocument = module.reducer(document, action, void 0, reducerOptions);
5555
+ updatedDocument = withProtocolVersions(module.reducer(document, action, void 0, reducerOptions), document.header.protocolVersions);
3683
5556
  } catch (error) {
3684
5557
  const contextMessage = `Failed to apply action to document:\n Action type: ${action.type}\n Document ID: ${job.documentId}\n Document type: ${document.header.documentType}\n Scope: ${job.scope}\n Original error: ${error instanceof Error ? error.message : String(error)}`;
3685
5558
  const enhancedError = new Error(contextMessage);
3686
5559
  if (error instanceof Error && error.stack) enhancedError.stack = `${contextMessage}\n\nOriginal stack trace:\n${error.stack}`;
3687
5560
  return buildErrorResult(job, enhancedError, startTime);
3688
5561
  }
3689
- const scope = job.scope;
3690
- const operations = updatedDocument.operations[scope];
3691
- if (operations.length === 0) return buildErrorResult(job, /* @__PURE__ */ new Error("No operation generated from action"), startTime);
3692
- const newOperation = operations[operations.length - 1];
3693
- if (!isUndoRedo(action)) newOperation.skip = skip;
5562
+ const scope = job.scope;
5563
+ const operations = updatedDocument.operations[scope];
5564
+ if (operations.length === 0) return buildErrorResult(job, /* @__PURE__ */ new Error("No operation generated from action"), startTime);
5565
+ const newOperation = operations[operations.length - 1];
5566
+ if (!isUndoRedo(action)) newOperation.skip = skip;
5567
+ if (deniedReason === void 0 && sourceOperation === void 0 && !executing.replayingAcceptedHistory && isSynthesized(action, newOperation, document.operations[scope] ?? [])) try {
5568
+ await signSynthesized(newOperation, action, {
5569
+ documentId: job.documentId,
5570
+ scope,
5571
+ branch: job.branch
5572
+ }, this.signer, signal);
5573
+ } catch (error) {
5574
+ return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
5575
+ }
3694
5576
  const resultingState = JSON.stringify({
3695
5577
  ...updatedDocument.state,
3696
5578
  header: updatedDocument.header
@@ -3791,7 +5673,7 @@ var SimpleJobExecutor = class {
3791
5673
  * apply and its own reasons for it.
3792
5674
  * - A positional or replayed run carries skips and re-appended operations,
3793
5675
  * whose indices are not a simple ascending run from the head.
3794
- * - UNDO, REDO, PRUNE and NOOP-with-skip each invalidate the write cache to
5676
+ * - UNDO, REDO and NOOP-with-skip each invalidate the write cache to
3795
5677
  * force a full-history rebuild, so they cannot be reduced against state
3796
5678
  * threaded from the write before them.
3797
5679
  * - The auth scope decides later writes against the policy earlier ones
@@ -3809,7 +5691,7 @@ var SimpleJobExecutor = class {
3809
5691
  if (scope === "auth" || scope === "document") return false;
3810
5692
  return writes.every((write) => {
3811
5693
  const type = write.action.type;
3812
- return write.skip === 0 && write.deniedReason === void 0 && write.sourceOperation === void 0 && !DOCUMENT_SCOPE_ACTIONS.has(type) && !isUndoRedo(write.action) && type !== "PRUNE" && type !== "NOOP";
5694
+ return write.skip === 0 && write.deniedReason === void 0 && write.sourceOperation === void 0 && !DOCUMENT_SCOPE_ACTIONS.has(type) && !isUndoRedo(write.action) && type !== "NOOP";
3813
5695
  });
3814
5696
  }
3815
5697
  /**
@@ -3932,13 +5814,22 @@ var SimpleJobExecutor = class {
3932
5814
  }
3933
5815
  if (!backdated) return plain();
3934
5816
  const conflicting = (await stores.operationStore.getConflicting(job.documentId, job.scope, job.branch, earliest, void 0, signal)).results.filter((operation) => !isGenesisOperation(operation));
3935
- if (conflicting.length === 0) {
5817
+ const nothingToMove = async () => {
3936
5818
  if (!this.featureFlags.authEnforcement) return plain();
3937
5819
  return this.evaluatePositioned(job, stores, this.appendedOperations(job, revisions.revision[job.scope] ?? 0), signal);
3938
- }
5820
+ };
5821
+ if (conflicting.length === 0) return nothingToMove();
3939
5822
  const nextIndex = revisions.revision[job.scope] ?? 0;
3940
5823
  let firstConflicting = conflicting[0].index;
3941
5824
  for (const operation of conflicting) if (operation.index < firstConflicting) firstConflicting = operation.index;
5825
+ const stored = (await stores.operationStore.getSince(job.documentId, job.scope, job.branch, firstConflicting - 1, void 0, void 0, signal)).results;
5826
+ const conflictingIds = new Set(conflicting.map((operation) => operation.id));
5827
+ const effective = garbageCollect(sortOperations(stored)).filter((operation) => !isGenesisOperation(operation));
5828
+ const firstMoving = effective.findIndex((operation) => conflictingIds.has(operation.id));
5829
+ const moving = firstMoving === -1 ? [] : effective.slice(firstMoving);
5830
+ if (moving.length === 0) return nothingToMove();
5831
+ let firstRetracted = moving[0].index;
5832
+ for (const operation of moving) firstRetracted = Math.min(firstRetracted, operation.index - operation.skip);
3942
5833
  const incoming = job.actions.map((action, i) => ({
3943
5834
  id: action.id,
3944
5835
  index: nextIndex + i,
@@ -3949,8 +5840,8 @@ var SimpleJobExecutor = class {
3949
5840
  }));
3950
5841
  const merged = reshuffleByTimestamp({
3951
5842
  index: nextIndex,
3952
- skip: retractionSkip(nextIndex, firstConflicting)
3953
- }, conflicting, incoming);
5843
+ skip: retractionSkip(nextIndex, firstRetracted)
5844
+ }, moving, incoming);
3954
5845
  stores.writeCache.invalidate(job.documentId, job.scope, job.branch);
3955
5846
  if (!this.featureFlags.authEnforcement) return {
3956
5847
  writes: merged.map((operation) => ({
@@ -4098,7 +5989,7 @@ var SimpleJobExecutor = class {
4098
5989
  stores.writeCache.invalidate(job.documentId, scope, job.branch);
4099
5990
  const result = await this.processActions(tail.map((operation, i) => ({
4100
5991
  action: operation.action,
4101
- skip: i === 0 ? retractionSkip(nextIndex, tail[0].index) : 0,
5992
+ skip: i === 0 ? retractionSkip(nextIndex, tail[0].index - tail[0].skip) : 0,
4102
5993
  sourceRemote: "",
4103
5994
  deniedReason: reevaluated[firstChange + i]
4104
5995
  })), {
@@ -4187,36 +6078,32 @@ var SimpleJobExecutor = class {
4187
6078
  error: new InvalidOperationTimestampError(job.documentId, scope, operation.timestampUtcMs, `operation (index: ${operation.index})`),
4188
6079
  duration: Date.now() - startTime
4189
6080
  };
4190
- let minIncomingIndex = Number.POSITIVE_INFINITY;
4191
- let minIncomingTimestamp = job.operations[0]?.timestampUtcMs || "";
4192
- for (const operation of job.operations) {
4193
- minIncomingIndex = Math.min(minIncomingIndex, operation.index);
4194
- const ts = operation.timestampUtcMs || "";
4195
- if (Date.parse(ts) < Date.parse(minIncomingTimestamp)) minIncomingTimestamp = ts;
4196
- }
4197
- let conflictingOps;
4198
- try {
4199
- conflictingOps = (await stores.operationStore.getConflicting(job.documentId, scope, job.branch, minIncomingTimestamp, void 0, signal)).results;
4200
- } catch {
4201
- conflictingOps = [];
4202
- }
4203
- let allOpsFromMinConflictingIndex = conflictingOps;
4204
- if (conflictingOps.length > 0) {
4205
- const minConflictingIndex = Math.min(...conflictingOps.map((op) => op.index));
6081
+ const dropped = /* @__PURE__ */ new Set();
6082
+ const admitted = /* @__PURE__ */ new Set();
6083
+ let selection = await this.selectLoadWrites(job, job.operations, stores, signal);
6084
+ for (;;) {
6085
+ const unadmitted = selection.incomingOpsToApply.filter((operation) => !admitted.has(operation));
6086
+ if (unadmitted.length === 0) break;
6087
+ let refused;
4206
6088
  try {
4207
- allOpsFromMinConflictingIndex = (await stores.operationStore.getSince(job.documentId, scope, job.branch, minConflictingIndex - 1, void 0, void 0, signal)).results;
4208
- } catch {
4209
- allOpsFromMinConflictingIndex = conflictingOps;
6089
+ refused = await this.signatureAdmission.admitLoad(job, unadmitted, stores, signal);
6090
+ } catch (error) {
6091
+ return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
4210
6092
  }
6093
+ for (const operation of unadmitted) if (refused.has(operation)) dropped.add(operation);
6094
+ else admitted.add(operation);
6095
+ if (refused.size === 0) break;
6096
+ const remaining = job.operations.filter((operation) => !dropped.has(operation));
6097
+ if (remaining.length === 0) return {
6098
+ job,
6099
+ success: true,
6100
+ operations: [],
6101
+ operationsWithContext: [],
6102
+ duration: Date.now() - startTime
6103
+ };
6104
+ selection = await this.selectLoadWrites(job, remaining, stores, signal);
4211
6105
  }
4212
- const incomingActionIds = new Set(job.operations.map((op) => op.action.id));
4213
- const nonSupersededOps = conflictingOps.filter((op) => {
4214
- if (op.index < minIncomingIndex && !incomingActionIds.has(op.action.id)) return false;
4215
- for (const laterOp of allOpsFromMinConflictingIndex) if (laterOp.index > op.index && laterOp.skip > 0) {
4216
- if (laterOp.index - laterOp.skip <= op.index) return false;
4217
- }
4218
- return true;
4219
- });
6106
+ const { nonSupersededOps, allOpsFromMinConflictingIndex, incomingOpsToApply } = selection;
4220
6107
  const existingOpsToReshuffle = monotonicAuthStream ? [] : nonSupersededOps.filter((operation) => !isGenesisOperation(operation));
4221
6108
  const actionIdCounts = /* @__PURE__ */ new Map();
4222
6109
  for (const operation of allOpsFromMinConflictingIndex) actionIdCounts.set(operation.action.id, (actionIdCounts.get(operation.action.id) ?? 0) + 1);
@@ -4237,14 +6124,6 @@ var SimpleJobExecutor = class {
4237
6124
  const logicalSkip = latestRevision - minLogicalIndex;
4238
6125
  if (logicalSkip > skipCount) skipCount = logicalSkip;
4239
6126
  }
4240
- const existingActionIds = new Set(nonSupersededOps.map((op) => op.action.id));
4241
- const seenIncomingActionIds = /* @__PURE__ */ new Set();
4242
- const incomingOpsToApply = job.operations.filter((op) => {
4243
- if (existingActionIds.has(op.action.id)) return false;
4244
- if (seenIncomingActionIds.has(op.action.id)) return false;
4245
- seenIncomingActionIds.add(op.action.id);
4246
- return true;
4247
- });
4248
6127
  if (incomingOpsToApply.length === 0) return {
4249
6128
  job,
4250
6129
  success: true,
@@ -4272,7 +6151,8 @@ var SimpleJobExecutor = class {
4272
6151
  ...operation,
4273
6152
  id: operation.id
4274
6153
  })));
4275
- for (const operation of reshuffledOperations) if (operation.action.type === "NOOP" && operation.skip === 0) operation.skip = 1;
6154
+ const incomingIds = new Set(incomingOpsToApply.map((operation) => operation.action.id));
6155
+ for (const operation of reshuffledOperations) if (operation.action.type === "NOOP" && operation.skip === 0 && incomingIds.has(operation.action.id)) operation.skip = 1;
4276
6156
  let deniedReasons;
4277
6157
  if (this.featureFlags.documentDecisions) try {
4278
6158
  deniedReasons = await evaluateByPosition(this.decisionModel, {
@@ -4324,6 +6204,71 @@ var SimpleJobExecutor = class {
4324
6204
  duration: Date.now() - startTime
4325
6205
  };
4326
6206
  }
6207
+ /** The conflicting window a load opens, and which of its operations are new. */
6208
+ async selectLoadWrites(job, operations, stores, signal) {
6209
+ const scope = job.scope;
6210
+ let minIncomingIndex = Number.POSITIVE_INFINITY;
6211
+ let minIncomingTimestamp = operations[0]?.timestampUtcMs || "";
6212
+ for (const operation of operations) {
6213
+ minIncomingIndex = Math.min(minIncomingIndex, operation.index);
6214
+ const ts = operation.timestampUtcMs || "";
6215
+ if (Date.parse(ts) < Date.parse(minIncomingTimestamp)) minIncomingTimestamp = ts;
6216
+ }
6217
+ let conflictingOps;
6218
+ try {
6219
+ conflictingOps = (await stores.operationStore.getConflicting(job.documentId, scope, job.branch, minIncomingTimestamp, void 0, signal)).results;
6220
+ } catch {
6221
+ conflictingOps = [];
6222
+ }
6223
+ let allOpsFromMinConflictingIndex = conflictingOps;
6224
+ if (conflictingOps.length > 0) {
6225
+ const minConflictingIndex = Math.min(...conflictingOps.map((op) => op.index));
6226
+ try {
6227
+ allOpsFromMinConflictingIndex = (await stores.operationStore.getSince(job.documentId, scope, job.branch, minConflictingIndex - 1, void 0, void 0, signal)).results;
6228
+ } catch {
6229
+ allOpsFromMinConflictingIndex = conflictingOps;
6230
+ }
6231
+ }
6232
+ const incomingActionIds = new Set(operations.map((op) => op.action.id));
6233
+ const supersededBy = (op) => {
6234
+ let nearest;
6235
+ for (const laterOp of allOpsFromMinConflictingIndex) if (laterOp.index > op.index && laterOp.skip > 0 && laterOp.index - laterOp.skip <= op.index && (nearest === void 0 || laterOp.index < nearest.index)) nearest = laterOp;
6236
+ return nearest;
6237
+ };
6238
+ const undoneActionIds = /* @__PURE__ */ new Set();
6239
+ let predecessorBound = minIncomingIndex;
6240
+ const rowsByActionId = /* @__PURE__ */ new Map();
6241
+ for (const op of conflictingOps) if (incomingActionIds.has(op.action.id)) rowsByActionId.set(op.action.id, [...rowsByActionId.get(op.action.id) ?? [], op]);
6242
+ const liveOps = /* @__PURE__ */ new Set();
6243
+ for (const op of conflictingOps) if (supersededBy(op) === void 0) liveOps.add(op);
6244
+ for (const [actionId, rows] of rowsByActionId) {
6245
+ if (rows.some((row) => liveOps.has(row))) continue;
6246
+ if (rows.some((row) => supersededBy(row)?.action.type === "NOOP")) undoneActionIds.add(actionId);
6247
+ else predecessorBound = Math.min(predecessorBound, Math.max(...rows.map((row) => row.index)));
6248
+ }
6249
+ const nonSupersededOps = conflictingOps.filter((op) => {
6250
+ if (op.index < predecessorBound && !incomingActionIds.has(op.action.id)) return false;
6251
+ return liveOps.has(op);
6252
+ });
6253
+ let rewoundFrom = Number.POSITIVE_INFINITY;
6254
+ for (const op of nonSupersededOps) if (!isGenesisOperation(op)) rewoundFrom = Math.min(rewoundFrom, op.index - op.skip);
6255
+ const reappended = new Set(nonSupersededOps.map((op) => op.index));
6256
+ for (const op of allOpsFromMinConflictingIndex) if (op.index >= rewoundFrom && !reappended.has(op.index) && supersededBy(op) === void 0) nonSupersededOps.push(op);
6257
+ const existingActionIds = new Set(nonSupersededOps.map((op) => op.action.id));
6258
+ const seenIncomingActionIds = /* @__PURE__ */ new Set();
6259
+ const incomingOpsToApply = operations.filter((op) => {
6260
+ if (existingActionIds.has(op.action.id)) return false;
6261
+ if (undoneActionIds.has(op.action.id)) return false;
6262
+ if (seenIncomingActionIds.has(op.action.id)) return false;
6263
+ seenIncomingActionIds.add(op.action.id);
6264
+ return true;
6265
+ });
6266
+ return {
6267
+ nonSupersededOps,
6268
+ allOpsFromMinConflictingIndex,
6269
+ incomingOpsToApply
6270
+ };
6271
+ }
4327
6272
  accumulateResultOrReturnError(result, generatedOperations, operationsWithContext) {
4328
6273
  if (!result.success) return result;
4329
6274
  if (result.operations && result.operations.length > 0) generatedOperations.push(...result.operations);
@@ -4470,6 +6415,7 @@ var DocumentModelRegistry = class {
4470
6415
  //#endregion
4471
6416
  //#region src/storage/kysely/keyframe-store.ts
4472
6417
  var KyselyKeyframeStore = class KyselyKeyframeStore {
6418
+ db;
4473
6419
  trx;
4474
6420
  constructor(db) {
4475
6421
  this.db = db;
@@ -4484,19 +6430,28 @@ var KyselyKeyframeStore = class KyselyKeyframeStore {
4484
6430
  }
4485
6431
  async putKeyframe(documentId, scope, branch, revision, document, signal) {
4486
6432
  if (signal?.aborted) throw new Error("Operation aborted");
4487
- await this.queryExecutor.insertInto("Keyframe").values({
4488
- documentId,
4489
- documentType: document.header.documentType,
4490
- scope,
4491
- branch,
4492
- revision,
4493
- document
4494
- }).onConflict((oc) => oc.columns([
4495
- "documentId",
4496
- "scope",
4497
- "branch",
4498
- "revision"
4499
- ]).doUpdateSet({ document })).execute();
6433
+ const write = async (trx) => {
6434
+ await acquirePurgeLocks(trx, [documentId], "shared");
6435
+ if ((await findPurged(trx, [documentId])).has(documentId)) return;
6436
+ await trx.insertInto("Keyframe").values({
6437
+ documentId,
6438
+ documentType: document.header.documentType,
6439
+ scope,
6440
+ branch,
6441
+ revision,
6442
+ document
6443
+ }).onConflict((oc) => oc.columns([
6444
+ "documentId",
6445
+ "scope",
6446
+ "branch",
6447
+ "revision"
6448
+ ]).doUpdateSet({ document })).execute();
6449
+ };
6450
+ if (this.trx) {
6451
+ await write(this.trx);
6452
+ return;
6453
+ }
6454
+ await this.db.transaction().execute(write);
4500
6455
  }
4501
6456
  async findNearestKeyframe(documentId, scope, branch, targetRevision, signal) {
4502
6457
  if (signal?.aborted) throw new Error("Operation aborted");
@@ -4554,6 +6509,11 @@ function paginateRows(rows, paging, cursorOf, toItem, refetch) {
4554
6509
  //#endregion
4555
6510
  //#region src/storage/txn.ts
4556
6511
  var AtomicTransaction = class {
6512
+ documentId;
6513
+ documentType;
6514
+ scope;
6515
+ branch;
6516
+ baseRevision;
4557
6517
  operations = [];
4558
6518
  constructor(documentId, documentType, scope, branch, baseRevision) {
4559
6519
  this.documentId = documentId;
@@ -4587,6 +6547,11 @@ var AtomicTransaction = class {
4587
6547
  //#endregion
4588
6548
  //#region src/storage/kysely/store.ts
4589
6549
  var _UniqueConstraintContext = class extends Error {
6550
+ documentId;
6551
+ scope;
6552
+ branch;
6553
+ revision;
6554
+ stagedOps;
4590
6555
  constructor(documentId, scope, branch, revision, stagedOps) {
4591
6556
  super("unique constraint");
4592
6557
  this.documentId = documentId;
@@ -4598,16 +6563,20 @@ var _UniqueConstraintContext = class extends Error {
4598
6563
  }
4599
6564
  };
4600
6565
  var KyselyOperationStore = class KyselyOperationStore {
6566
+ db;
4601
6567
  trx;
6568
+ liveIds;
4602
6569
  constructor(db) {
4603
6570
  this.db = db;
4604
6571
  }
4605
6572
  get queryExecutor() {
4606
6573
  return this.trx ?? this.db;
4607
6574
  }
4608
- withTransaction(trx) {
6575
+ /** `liveIds`: ids the transaction read untombstoned under its shared lock. */
6576
+ withTransaction(trx, liveIds) {
4609
6577
  const instance = new KyselyOperationStore(this.db);
4610
6578
  instance.trx = trx;
6579
+ instance.liveIds = liveIds;
4611
6580
  return instance;
4612
6581
  }
4613
6582
  async apply(documentId, documentType, scope, branch, revision, fn, signal, condition) {
@@ -4652,6 +6621,8 @@ var KyselyOperationStore = class KyselyOperationStore {
4652
6621
  await fn(atomicTxn);
4653
6622
  const operations = atomicTxn.getOperations();
4654
6623
  if (operations.length === 0) return [];
6624
+ const purgedHead = this.liveIds?.has(documentId) ? void 0 : await this.refusePurgedAppend(trx, documentId, operations);
6625
+ if (purgedHead !== void 0) return purgedHead;
4655
6626
  if (condition) await this.acquireStreamLocks(trx, documentId, scope, branch, condition);
4656
6627
  const latestOp = await trx.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).orderBy("index", "desc").limit(1).executeTakeFirst();
4657
6628
  const currentRevision = latestOp ? latestOp.index : -1;
@@ -4697,12 +6668,13 @@ var KyselyOperationStore = class KyselyOperationStore {
4697
6668
  * otherwise read a snapshot taken before the locks were held.
4698
6669
  */
4699
6670
  async acquireStreamLocks(trx, documentId, scope, branch, condition) {
4700
- const keys = new Set([`${documentId}:${scope}:${branch}`]);
6671
+ const keys = /* @__PURE__ */ new Set([`${documentId}:${scope}:${branch}`]);
4701
6672
  for (const stream of condition.streams) keys.add(`${stream.documentId}:${stream.scope}:${stream.branch}`);
6673
+ const sortedKeys = sql.join([...keys].sort());
4702
6674
  await sql`
4703
6675
  with ordered as materialized (
4704
6676
  select key
4705
- from unnest(array[${sql.join([...keys].sort())}]::text[]) with ordinality as t(key, ord)
6677
+ from unnest(array[${sortedKeys}]::text[]) with ordinality as t(key, ord)
4706
6678
  order by ord
4707
6679
  )
4708
6680
  select pg_advisory_xact_lock(hashtext(key)) from ordered
@@ -4766,6 +6738,24 @@ var KyselyOperationStore = class KyselyOperationStore {
4766
6738
  }
4767
6739
  return storedRows.map((row) => this.rowToOperation(row));
4768
6740
  }
6741
+ /** A purged stream refuses appends; a marker gets the stored one back. */
6742
+ async refusePurgedAppend(trx, documentId, operations) {
6743
+ if (!(await findPurged(trx, [documentId])).has(documentId)) return;
6744
+ if (!operations.every((operation) => this.isMarker(operation))) throw new DocumentPurgedError(documentId);
6745
+ return (await trx.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where(sql`action->>'type' = ${PURGE_DOCUMENT}`).orderBy("index", "asc").execute()).map((row) => this.rowToOperation(row));
6746
+ }
6747
+ isMarker(operation) {
6748
+ return this.actionType(operation) === PURGE_DOCUMENT;
6749
+ }
6750
+ actionType(operation) {
6751
+ let action = operation.action;
6752
+ if (typeof action === "string") try {
6753
+ action = JSON.parse(action);
6754
+ } catch {
6755
+ return;
6756
+ }
6757
+ if (typeof action === "object" && action !== null && "type" in action && typeof action.type === "string") return action.type;
6758
+ }
4769
6759
  /** True when the staged write creates a document rather than appending to one. */
4770
6760
  isCreate(operations) {
4771
6761
  for (const operation of operations) {
@@ -4779,6 +6769,14 @@ var KyselyOperationStore = class KyselyOperationStore {
4779
6769
  }
4780
6770
  return false;
4781
6771
  }
6772
+ /**
6773
+ * The paging cursor here encodes the index to resume from (one past the
6774
+ * last row returned), not the last row's own index. This keeps "0" an
6775
+ * unambiguous start-of-stream sentinel even when a page ends at index 0
6776
+ * (e.g. `limit: 1` on a fresh stream), which would otherwise make
6777
+ * `nextCursor` equal the start cursor and loop a caller that walks pages
6778
+ * forever.
6779
+ */
4782
6780
  async getSince(documentId, scope, branch, revision, filter, paging, signal) {
4783
6781
  throwIfAborted(signal);
4784
6782
  let query = this.queryExecutor.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("index", ">", revision).orderBy("index", "asc");
@@ -4793,10 +6791,10 @@ var KyselyOperationStore = class KyselyOperationStore {
4793
6791
  }
4794
6792
  if (paging) {
4795
6793
  const cursorValue = Number.parseInt(paging.cursor, 10);
4796
- if (cursorValue > 0) query = query.where("index", ">", cursorValue);
6794
+ if (Number.isFinite(cursorValue) && cursorValue > 0) query = query.where("index", ">=", cursorValue);
4797
6795
  if (paging.limit) query = query.limit(paging.limit + 1);
4798
6796
  }
4799
- return paginateRows(await query.execute(), paging, (row) => row.index, (row) => this.rowToOperation(row), (cursor, limit) => this.getSince(documentId, scope, branch, revision, filter, {
6797
+ return paginateRows(await query.execute(), paging, (row) => row.index + 1, (row) => this.rowToOperation(row), (cursor, limit) => this.getSince(documentId, scope, branch, revision, filter, {
4800
6798
  cursor,
4801
6799
  limit
4802
6800
  }, signal));
@@ -4814,26 +6812,28 @@ var KyselyOperationStore = class KyselyOperationStore {
4814
6812
  limit
4815
6813
  }, signal));
4816
6814
  }
6815
+ /**
6816
+ * The paging cursor here encodes the index to resume from (one past the
6817
+ * last row returned), not the last row's own index, for the same reason as
6818
+ * `getSince`: a page ending at index 0 must not produce a cursor that is
6819
+ * indistinguishable from the start-of-stream sentinel.
6820
+ */
4817
6821
  async getConflicting(documentId, scope, branch, minTimestamp, paging, signal) {
4818
6822
  throwIfAborted(signal);
4819
6823
  let query = this.queryExecutor.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("timestampUtcMs", ">=", new Date(minTimestamp)).orderBy("index", "asc");
4820
6824
  if (paging) {
4821
6825
  const cursorValue = Number.parseInt(paging.cursor, 10);
4822
- if (cursorValue > 0) query = query.where("index", ">", cursorValue);
6826
+ if (Number.isFinite(cursorValue) && cursorValue > 0) query = query.where("index", ">=", cursorValue);
4823
6827
  if (paging.limit) query = query.limit(paging.limit + 1);
4824
6828
  }
4825
- return paginateRows(await query.execute(), paging, (row) => row.index, (row) => this.rowToOperation(row), (cursor, limit) => this.getConflicting(documentId, scope, branch, minTimestamp, {
6829
+ return paginateRows(await query.execute(), paging, (row) => row.index + 1, (row) => this.rowToOperation(row), (cursor, limit) => this.getConflicting(documentId, scope, branch, minTimestamp, {
4826
6830
  cursor,
4827
6831
  limit
4828
6832
  }, signal));
4829
6833
  }
4830
6834
  async getRevisions(documentId, branch, signal) {
4831
6835
  throwIfAborted(signal);
4832
- const scopeRevisions = await this.queryExecutor.selectFrom("Operation as o1").select([
4833
- "o1.scope",
4834
- "o1.index",
4835
- "o1.timestampUtcMs"
4836
- ]).where("o1.documentId", "=", documentId).where("o1.branch", "=", branch).where((eb) => eb("o1.index", "=", eb.selectFrom("Operation as o2").select((eb2) => eb2.fn.max("o2.index").as("maxIndex")).where("o2.documentId", "=", eb.ref("o1.documentId")).where("o2.branch", "=", eb.ref("o1.branch")).where("o2.scope", "=", eb.ref("o1.scope")))).execute();
6836
+ const scopeRevisions = await this.queryExecutor.selectFrom("Operation").select("scope").select((eb) => eb.fn.max("index").as("index")).where("documentId", "=", documentId).where("branch", "=", branch).groupBy("scope").execute();
4837
6837
  const latest = await this.queryExecutor.selectFrom("Operation").select((eb) => eb.fn.max("timestampUtcMs").as("latestTimestamp")).where("documentId", "=", documentId).where("branch", "=", branch).executeTakeFirst();
4838
6838
  const revision = {};
4839
6839
  for (const row of scopeRevisions) revision[row.scope] = row.index + 1;
@@ -4846,6 +6846,20 @@ var KyselyOperationStore = class KyselyOperationStore {
4846
6846
  const latest = await this.queryExecutor.selectFrom("Operation").select((eb) => eb.fn.max("timestampUtcMs").as("latestTimestamp")).where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).executeTakeFirst();
4847
6847
  return latest?.latestTimestamp ? new Date(latest.latestTimestamp).toISOString() : void 0;
4848
6848
  }
6849
+ async findOperationIds(documentId, scope, branch, opIds, signal) {
6850
+ throwIfAborted(signal);
6851
+ if (opIds.length === 0) return /* @__PURE__ */ new Set();
6852
+ const rows = await this.queryExecutor.selectFrom("Operation").select("opId").distinct().where("opId", "in", opIds).where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).execute();
6853
+ return new Set(rows.map((row) => row.opId));
6854
+ }
6855
+ async getOperationsByIds(documentId, scope, branch, opIds, signal) {
6856
+ throwIfAborted(signal);
6857
+ if (opIds.length === 0) return [];
6858
+ const rows = await this.queryExecutor.selectFrom("Operation").selectAll().where("opId", "in", opIds).where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).orderBy("index", "asc").execute();
6859
+ const latest = /* @__PURE__ */ new Map();
6860
+ for (const row of rows) latest.set(row.opId, this.rowToOperation(row));
6861
+ return [...latest.values()];
6862
+ }
4849
6863
  rowToOperation(row) {
4850
6864
  return {
4851
6865
  index: row.index,
@@ -4939,8 +6953,8 @@ function createForwardingPoolInstrumentation(name) {
4939
6953
  }
4940
6954
  //#endregion
4941
6955
  //#region src/storage/migrations/001_create_operation_table.ts
4942
- var _001_create_operation_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$18 });
4943
- async function up$18(db) {
6956
+ var _001_create_operation_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$25 });
6957
+ async function up$25(db) {
4944
6958
  await db.schema.createTable("Operation").addColumn("id", "serial", (col) => col.primaryKey()).addColumn("jobId", "text", (col) => col.notNull()).addColumn("opId", "text", (col) => col.notNull()).addColumn("prevOpId", "text", (col) => col.notNull()).addColumn("writeTimestampUtcMs", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("documentId", "text", (col) => col.notNull()).addColumn("documentType", "text", (col) => col.notNull()).addColumn("scope", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("timestampUtcMs", "timestamptz", (col) => col.notNull()).addColumn("index", "integer", (col) => col.notNull()).addColumn("action", "jsonb", (col) => col.notNull()).addColumn("skip", "integer", (col) => col.notNull()).addColumn("error", "text").addColumn("hash", "text", (col) => col.notNull()).addUniqueConstraint("unique_revision", [
4945
6959
  "documentId",
4946
6960
  "scope",
@@ -4965,8 +6979,8 @@ async function up$18(db) {
4965
6979
  }
4966
6980
  //#endregion
4967
6981
  //#region src/storage/migrations/002_create_keyframe_table.ts
4968
- var _002_create_keyframe_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$17 });
4969
- async function up$17(db) {
6982
+ var _002_create_keyframe_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$24 });
6983
+ async function up$24(db) {
4970
6984
  await db.schema.createTable("Keyframe").addColumn("id", "serial", (col) => col.primaryKey()).addColumn("documentId", "text", (col) => col.notNull()).addColumn("documentType", "text", (col) => col.notNull()).addColumn("scope", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("revision", "integer", (col) => col.notNull()).addColumn("document", "jsonb", (col) => col.notNull()).addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addUniqueConstraint("unique_keyframe", [
4971
6985
  "documentId",
4972
6986
  "scope",
@@ -4982,14 +6996,14 @@ async function up$17(db) {
4982
6996
  }
4983
6997
  //#endregion
4984
6998
  //#region src/storage/migrations/003_create_document_table.ts
4985
- var _003_create_document_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$16 });
4986
- async function up$16(db) {
6999
+ var _003_create_document_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$23 });
7000
+ async function up$23(db) {
4987
7001
  await db.schema.createTable("Document").addColumn("id", "text", (col) => col.primaryKey()).addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
4988
7002
  }
4989
7003
  //#endregion
4990
7004
  //#region src/storage/migrations/004_create_document_relationship_table.ts
4991
- var _004_create_document_relationship_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$15 });
4992
- async function up$15(db) {
7005
+ var _004_create_document_relationship_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$22 });
7006
+ async function up$22(db) {
4993
7007
  await db.schema.createTable("DocumentRelationship").addColumn("id", "text", (col) => col.primaryKey()).addColumn("sourceId", "text", (col) => col.notNull().references("Document.id").onDelete("cascade")).addColumn("targetId", "text", (col) => col.notNull().references("Document.id").onDelete("cascade")).addColumn("relationshipType", "text", (col) => col.notNull()).addColumn("metadata", "jsonb").addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addUniqueConstraint("unique_source_target_type", [
4994
7008
  "sourceId",
4995
7009
  "targetId",
@@ -5001,14 +7015,14 @@ async function up$15(db) {
5001
7015
  }
5002
7016
  //#endregion
5003
7017
  //#region src/storage/migrations/005_create_indexer_state_table.ts
5004
- var _005_create_indexer_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$14 });
5005
- async function up$14(db) {
7018
+ var _005_create_indexer_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$21 });
7019
+ async function up$21(db) {
5006
7020
  await db.schema.createTable("IndexerState").addColumn("id", "integer", (col) => col.primaryKey().generatedAlwaysAsIdentity()).addColumn("lastOperationId", "integer", (col) => col.notNull()).addColumn("lastOperationTimestamp", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
5007
7021
  }
5008
7022
  //#endregion
5009
7023
  //#region src/storage/migrations/006_create_document_snapshot_table.ts
5010
- var _006_create_document_snapshot_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$13 });
5011
- async function up$13(db) {
7024
+ var _006_create_document_snapshot_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$20 });
7025
+ async function up$20(db) {
5012
7026
  await db.schema.createTable("DocumentSnapshot").addColumn("id", "text", (col) => col.primaryKey()).addColumn("documentId", "text", (col) => col.notNull()).addColumn("slug", "text").addColumn("name", "text").addColumn("scope", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("content", "jsonb", (col) => col.notNull()).addColumn("documentType", "text", (col) => col.notNull()).addColumn("lastOperationIndex", "integer", (col) => col.notNull()).addColumn("lastOperationHash", "text", (col) => col.notNull()).addColumn("lastUpdatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("snapshotVersion", "integer", (col) => col.notNull().defaultTo(1)).addColumn("identifiers", "jsonb").addColumn("metadata", "jsonb").addColumn("isDeleted", "boolean", (col) => col.notNull().defaultTo(false)).addColumn("deletedAt", "timestamptz").addUniqueConstraint("unique_doc_scope_branch", [
5013
7027
  "documentId",
5014
7028
  "scope",
@@ -5029,8 +7043,8 @@ async function up$13(db) {
5029
7043
  }
5030
7044
  //#endregion
5031
7045
  //#region src/storage/migrations/007_create_slug_mapping_table.ts
5032
- var _007_create_slug_mapping_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$12 });
5033
- async function up$12(db) {
7046
+ var _007_create_slug_mapping_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$19 });
7047
+ async function up$19(db) {
5034
7048
  await db.schema.createTable("SlugMapping").addColumn("slug", "text", (col) => col.primaryKey()).addColumn("documentId", "text", (col) => col.notNull()).addColumn("scope", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addUniqueConstraint("unique_docid_scope_branch", [
5035
7049
  "documentId",
5036
7050
  "scope",
@@ -5040,14 +7054,14 @@ async function up$12(db) {
5040
7054
  }
5041
7055
  //#endregion
5042
7056
  //#region src/storage/migrations/008_create_view_state_table.ts
5043
- var _008_create_view_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$11 });
5044
- async function up$11(db) {
7057
+ var _008_create_view_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$18 });
7058
+ async function up$18(db) {
5045
7059
  await db.schema.createTable("ViewState").addColumn("readModelId", "text", (col) => col.primaryKey()).addColumn("lastOrdinal", "integer", (col) => col.notNull().defaultTo(0)).addColumn("lastOperationTimestamp", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
5046
7060
  }
5047
7061
  //#endregion
5048
7062
  //#region src/storage/migrations/009_create_operation_index_tables.ts
5049
- var _009_create_operation_index_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$10 });
5050
- async function up$10(db) {
7063
+ var _009_create_operation_index_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$17 });
7064
+ async function up$17(db) {
5051
7065
  await db.schema.createTable("document_collections").addColumn("documentId", "text", (col) => col.notNull()).addColumn("collectionId", "text", (col) => col.notNull()).addColumn("joinedOrdinal", "bigint", (col) => col.notNull().defaultTo(0)).addColumn("leftOrdinal", "bigint").addPrimaryKeyConstraint("document_collections_pkey", ["documentId", "collectionId"]).execute();
5052
7066
  await db.schema.createIndex("idx_document_collections_collectionId").on("document_collections").column("collectionId").execute();
5053
7067
  await db.schema.createIndex("idx_doc_collections_collection_range").on("document_collections").columns(["collectionId", "joinedOrdinal"]).execute();
@@ -5061,8 +7075,8 @@ async function up$10(db) {
5061
7075
  }
5062
7076
  //#endregion
5063
7077
  //#region src/storage/migrations/010_create_sync_tables.ts
5064
- var _010_create_sync_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$9 });
5065
- async function up$9(db) {
7078
+ var _010_create_sync_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$16 });
7079
+ async function up$16(db) {
5066
7080
  await db.schema.createTable("sync_remotes").addColumn("name", "text", (col) => col.primaryKey()).addColumn("collection_id", "text", (col) => col.notNull()).addColumn("channel_type", "text", (col) => col.notNull()).addColumn("channel_id", "text", (col) => col.notNull().defaultTo("")).addColumn("remote_name", "text", (col) => col.notNull().defaultTo("")).addColumn("channel_parameters", "jsonb", (col) => col.notNull().defaultTo(sql`'{}'::jsonb`)).addColumn("filter_document_ids", "jsonb").addColumn("filter_scopes", "jsonb").addColumn("filter_branch", "text", (col) => col.notNull().defaultTo("main")).addColumn("push_state", "text", (col) => col.notNull().defaultTo("idle")).addColumn("push_last_success_utc_ms", "text").addColumn("push_last_failure_utc_ms", "text").addColumn("push_failure_count", "integer", (col) => col.notNull().defaultTo(0)).addColumn("pull_state", "text", (col) => col.notNull().defaultTo("idle")).addColumn("pull_last_success_utc_ms", "text").addColumn("pull_last_failure_utc_ms", "text").addColumn("pull_failure_count", "integer", (col) => col.notNull().defaultTo(0)).addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
5067
7081
  await db.schema.createIndex("idx_sync_remotes_collection").on("sync_remotes").column("collection_id").execute();
5068
7082
  await db.schema.createTable("sync_cursors").addColumn("remote_name", "text", (col) => col.primaryKey().references("sync_remotes.name").onDelete("cascade")).addColumn("cursor_ordinal", "bigint", (col) => col.notNull().defaultTo(0)).addColumn("last_synced_at_utc_ms", "text").addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
@@ -5070,8 +7084,8 @@ async function up$9(db) {
5070
7084
  }
5071
7085
  //#endregion
5072
7086
  //#region src/storage/migrations/011_add_cursor_type_column.ts
5073
- var _011_add_cursor_type_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$8 });
5074
- async function up$8(db) {
7087
+ var _011_add_cursor_type_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$15 });
7088
+ async function up$15(db) {
5075
7089
  await db.deleteFrom("sync_cursors").where("remote_name", "like", "outbox::%").execute();
5076
7090
  await db.deleteFrom("sync_remotes").where("name", "like", "outbox::%").execute();
5077
7091
  await db.schema.dropTable("sync_cursors").execute();
@@ -5080,64 +7094,64 @@ async function up$8(db) {
5080
7094
  }
5081
7095
  //#endregion
5082
7096
  //#region src/storage/migrations/012_add_source_remote_column.ts
5083
- var _012_add_source_remote_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$7 });
5084
- async function up$7(db) {
7097
+ var _012_add_source_remote_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$14 });
7098
+ async function up$14(db) {
5085
7099
  await db.schema.alterTable("operation_index_operations").addColumn("sourceRemote", "text", (col) => col.notNull().defaultTo("")).execute();
5086
7100
  }
5087
7101
  //#endregion
5088
7102
  //#region src/storage/migrations/013_create_sync_dead_letters_table.ts
5089
- var _013_create_sync_dead_letters_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$6 });
5090
- async function up$6(db) {
7103
+ var _013_create_sync_dead_letters_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$13 });
7104
+ async function up$13(db) {
5091
7105
  await db.schema.createTable("sync_dead_letters").addColumn("ordinal", "serial", (col) => col.primaryKey()).addColumn("id", "text", (col) => col.unique().notNull()).addColumn("job_id", "text", (col) => col.notNull()).addColumn("job_dependencies", "jsonb", (col) => col.notNull().defaultTo(sql`'[]'::jsonb`)).addColumn("remote_name", "text", (col) => col.notNull().references("sync_remotes.name").onDelete("cascade")).addColumn("document_id", "text", (col) => col.notNull()).addColumn("scopes", "jsonb", (col) => col.notNull().defaultTo(sql`'[]'::jsonb`)).addColumn("branch", "text", (col) => col.notNull()).addColumn("operations", "jsonb", (col) => col.notNull().defaultTo(sql`'[]'::jsonb`)).addColumn("error_source", "text", (col) => col.notNull()).addColumn("error_message", "text", (col) => col.notNull()).addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
5092
7106
  await db.schema.createIndex("idx_sync_dead_letters_remote").on("sync_dead_letters").column("remote_name").execute();
5093
7107
  }
5094
7108
  //#endregion
5095
7109
  //#region src/storage/migrations/014_create_processor_cursor_table.ts
5096
- var _014_create_processor_cursor_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$5 });
5097
- async function up$5(db) {
7110
+ var _014_create_processor_cursor_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$12 });
7111
+ async function up$12(db) {
5098
7112
  await db.schema.createTable("ProcessorCursor").addColumn("processorId", "text", (col) => col.primaryKey()).addColumn("factoryId", "text", (col) => col.notNull()).addColumn("driveId", "text", (col) => col.notNull()).addColumn("processorIndex", "integer", (col) => col.notNull()).addColumn("lastOrdinal", "integer", (col) => col.notNull().defaultTo(sql`0`)).addColumn("status", "text", (col) => col.notNull().defaultTo(sql`'active'`)).addColumn("lastError", "text").addColumn("lastErrorTimestamp", "timestamptz").addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
5099
7113
  }
5100
7114
  //#endregion
5101
7115
  //#region src/storage/migrations/015_add_operation_denied_reason.ts
5102
7116
  var _015_add_operation_denied_reason_exports = /* @__PURE__ */ __exportAll({
5103
- down: () => down$4,
5104
- up: () => up$4
7117
+ down: () => down$11,
7118
+ up: () => up$11
5105
7119
  });
5106
7120
  /**
5107
7121
  * Records why authorization refused an operation. Separate from `error` so a
5108
7122
  * denial is distinguishable from a reducer failure without matching on a
5109
7123
  * message. Null for every operation written before decisions were enforced.
5110
7124
  */
5111
- async function up$4(db) {
7125
+ async function up$11(db) {
5112
7126
  await db.schema.alterTable("Operation").addColumn("deniedReason", "text").execute();
5113
7127
  await db.schema.alterTable("operation_index_operations").addColumn("deniedReason", "text").execute();
5114
7128
  }
5115
- async function down$4(db) {
7129
+ async function down$11(db) {
5116
7130
  await db.schema.alterTable("operation_index_operations").dropColumn("deniedReason").execute();
5117
7131
  await db.schema.alterTable("Operation").dropColumn("deniedReason").execute();
5118
7132
  }
5119
7133
  //#endregion
5120
7134
  //#region src/storage/migrations/016_add_dead_letter_error_type.ts
5121
7135
  var _016_add_dead_letter_error_type_exports = /* @__PURE__ */ __exportAll({
5122
- down: () => down$3,
5123
- up: () => up$3
7136
+ down: () => down$10,
7137
+ up: () => up$10
5124
7138
  });
5125
7139
  /**
5126
7140
  * The classification a dead letter falls into, stored because it decides whether
5127
7141
  * the document stays quarantined and the in-memory error is gone after a restart.
5128
7142
  * Defaulted rather than nullable, so a pre-existing row rehydrates.
5129
7143
  */
5130
- async function up$3(db) {
7144
+ async function up$10(db) {
5131
7145
  await db.schema.alterTable("sync_dead_letters").addColumn("error_type", "text", (col) => col.notNull().defaultTo("UNCLASSIFIED")).execute();
5132
7146
  }
5133
- async function down$3(db) {
7147
+ async function down$10(db) {
5134
7148
  await db.schema.alterTable("sync_dead_letters").dropColumn("error_type").execute();
5135
7149
  }
5136
7150
  //#endregion
5137
7151
  //#region src/storage/migrations/017_create_group_references.ts
5138
7152
  var _017_create_group_references_exports = /* @__PURE__ */ __exportAll({
5139
- down: () => down$2,
5140
- up: () => up$2
7153
+ down: () => down$9,
7154
+ up: () => up$9
5141
7155
  });
5142
7156
  /**
5143
7157
  * One row per (document, group) reference ever discovered from an auth
@@ -5148,18 +7162,18 @@ var _017_create_group_references_exports = /* @__PURE__ */ __exportAll({
5148
7162
  * requires (sync), and by groupId for the documents a group change affects
5149
7163
  * (re-evaluation).
5150
7164
  */
5151
- async function up$2(db) {
7165
+ async function up$9(db) {
5152
7166
  await db.schema.createTable("group_references").addColumn("documentId", "text", (col) => col.notNull()).addColumn("groupId", "text", (col) => col.notNull()).addPrimaryKeyConstraint("group_references_pkey", ["documentId", "groupId"]).execute();
5153
7167
  await db.schema.createIndex("idx_group_references_groupId").on("group_references").column("groupId").execute();
5154
7168
  }
5155
- async function down$2(db) {
7169
+ async function down$9(db) {
5156
7170
  await db.schema.dropTable("group_references").execute();
5157
7171
  }
5158
7172
  //#endregion
5159
7173
  //#region src/storage/migrations/018_add_sync_remote_bound_address.ts
5160
7174
  var _018_add_sync_remote_bound_address_exports = /* @__PURE__ */ __exportAll({
5161
- down: () => down$1,
5162
- up: () => up$1
7175
+ down: () => down$8,
7176
+ up: () => up$8
5163
7177
  });
5164
7178
  /**
5165
7179
  * The address a sync channel is bound to, so a channel created by one subject
@@ -5170,17 +7184,17 @@ var _018_add_sync_remote_bound_address_exports = /* @__PURE__ */ __exportAll({
5170
7184
  * until its first authenticated poll adopts it. A default would claim them all
5171
7185
  * for one address.
5172
7186
  */
5173
- async function up$1(db) {
7187
+ async function up$8(db) {
5174
7188
  await db.schema.alterTable("sync_remotes").addColumn("bound_address", "text").execute();
5175
7189
  }
5176
- async function down$1(db) {
7190
+ async function down$8(db) {
5177
7191
  await db.schema.alterTable("sync_remotes").dropColumn("bound_address").execute();
5178
7192
  }
5179
7193
  //#endregion
5180
7194
  //#region src/storage/migrations/019_require_action_id.ts
5181
7195
  var _019_require_action_id_exports = /* @__PURE__ */ __exportAll({
5182
- down: () => down,
5183
- up: () => up
7196
+ down: () => down$7,
7197
+ up: () => up$7
5184
7198
  });
5185
7199
  /**
5186
7200
  * Makes an operation whose action carries no id physically unstorable.
@@ -5207,7 +7221,7 @@ var _019_require_action_id_exports = /* @__PURE__ */ __exportAll({
5207
7221
  * The empty string is rejected alongside null. It derives the same colliding
5208
7222
  * operation id as an absent id, so admitting it would leave the hole open.
5209
7223
  */
5210
- async function up(db) {
7224
+ async function up$7(db) {
5211
7225
  await db.updateTable("Operation").set({ action: sql`jsonb_set(action, '{id}', to_jsonb(gen_random_uuid()::text))` }).where(sql`jsonb_typeof(action) = 'object' and coalesce(action->>'id', '') = ''`).execute();
5212
7226
  await db.updateTable("operation_index_operations as oio").from("Operation as op").set({ action: sql`jsonb_set(oio.action, '{id}', to_jsonb(op.action->>'id'))` }).whereRef("oio.opId", "=", "op.opId").whereRef("oio.index", "=", "op.index").whereRef("oio.skip", "=", "op.skip").where(sql`jsonb_typeof(oio.action) = 'object' and coalesce(oio.action->>'id', '') = ''`).where(sql`coalesce(op.action->>'id', '') <> ''`).execute();
5213
7227
  await db.updateTable("operation_index_operations").set({ action: sql`jsonb_set(action, '{id}', to_jsonb(gen_random_uuid()::text))` }).where(sql`jsonb_typeof(action) = 'object' and coalesce(action->>'id', '') = ''`).execute();
@@ -5219,11 +7233,118 @@ async function up(db) {
5219
7233
  * their operations are now known by, and reverting them would reintroduce the
5220
7234
  * collision the migration removed.
5221
7235
  */
5222
- async function down(db) {
7236
+ async function down$7(db) {
5223
7237
  await db.schema.alterTable("operation_index_operations").dropConstraint("action_must_have_id").execute();
5224
7238
  await db.schema.alterTable("Operation").dropConstraint("action_must_have_id").execute();
5225
7239
  }
5226
7240
  //#endregion
7241
+ //#region src/storage/migrations/020_add_snapshot_operation_ordinal.ts
7242
+ var _020_add_snapshot_operation_ordinal_exports = /* @__PURE__ */ __exportAll({
7243
+ down: () => down$6,
7244
+ up: () => up$6
7245
+ });
7246
+ /** Orders header row writes across scopes; `lastOperationIndex` is per scope. */
7247
+ async function up$6(db) {
7248
+ await db.schema.alterTable("DocumentSnapshot").addColumn("lastOperationOrdinal", "integer", (col) => col.notNull().defaultTo(0)).execute();
7249
+ }
7250
+ async function down$6(db) {
7251
+ await db.schema.alterTable("DocumentSnapshot").dropColumn("lastOperationOrdinal").execute();
7252
+ }
7253
+ //#endregion
7254
+ //#region src/storage/migrations/021_add_sync_remote_peer.ts
7255
+ var _021_add_sync_remote_peer_exports = /* @__PURE__ */ __exportAll({
7256
+ down: () => down$5,
7257
+ up: () => up$5
7258
+ });
7259
+ /**
7260
+ * The peer's manifest as canonical JSON, and when it was heard. Both null: not
7261
+ * yet heard. A time with no manifest: a silent peer, which gets the baselines.
7262
+ */
7263
+ async function up$5(db) {
7264
+ await db.schema.alterTable("sync_remotes").addColumn("peer_manifest", "text").addColumn("peer_manifest_at_utc_ms", "bigint").execute();
7265
+ }
7266
+ async function down$5(db) {
7267
+ await db.schema.alterTable("sync_remotes").dropColumn("peer_manifest").dropColumn("peer_manifest_at_utc_ms").execute();
7268
+ }
7269
+ //#endregion
7270
+ //#region src/storage/migrations/022_create_sync_holds.ts
7271
+ var _022_create_sync_holds_exports = /* @__PURE__ */ __exportAll({
7272
+ down: () => down$4,
7273
+ up: () => up$4
7274
+ });
7275
+ /** Documents held back from a remote whose peer cannot run their versions. */
7276
+ async function up$4(db) {
7277
+ await db.schema.createTable("sync_holds").addColumn("remote_name", "text", (col) => col.notNull().references("sync_remotes.name").onDelete("cascade")).addColumn("document_id", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("protocol", "text", (col) => col.notNull()).addColumn("version", "integer", (col) => col.notNull()).addColumn("held_at_utc_ms", "bigint", (col) => col.notNull()).addPrimaryKeyConstraint("sync_holds_pkey", [
7278
+ "remote_name",
7279
+ "document_id",
7280
+ "branch"
7281
+ ]).execute();
7282
+ }
7283
+ async function down$4(db) {
7284
+ await db.schema.dropTable("sync_holds").execute();
7285
+ }
7286
+ //#endregion
7287
+ //#region src/storage/migrations/023_index_created_protocol_versions.ts
7288
+ var _023_index_created_protocol_versions_exports = /* @__PURE__ */ __exportAll({
7289
+ down: () => down$3,
7290
+ up: () => up$3
7291
+ });
7292
+ /**
7293
+ * One entry per document created with protocolVersions, keyed by a hash so an
7294
+ * oversized header cannot make its creation unstorable. Startup reads the
7295
+ * distinct values without visiting every document.
7296
+ */
7297
+ async function up$3(db) {
7298
+ await db.schema.createIndex("idx_operation_created_protocol_versions").on("Operation").expression(sql`md5((action->'input'->'protocolVersions')::text)`).where(sql`scope = 'document' and "index" = 0 and (action->'input'->'protocolVersions') is not null`).execute();
7299
+ }
7300
+ async function down$3(db) {
7301
+ await db.schema.dropIndex("idx_operation_created_protocol_versions").execute();
7302
+ }
7303
+ //#endregion
7304
+ //#region src/storage/migrations/024_create_document_purges.ts
7305
+ var _024_create_document_purges_exports = /* @__PURE__ */ __exportAll({
7306
+ down: () => down$2,
7307
+ up: () => up$2
7308
+ });
7309
+ /** One row per purged document: the tombstone. Written once, never deleted. */
7310
+ async function up$2(db) {
7311
+ await db.schema.createTable("document_purges").addColumn("documentId", "text", (col) => col.primaryKey()).addColumn("ordinal", "bigint", (col) => col.notNull()).addColumn("removedRows", "jsonb", (col) => col.notNull()).addColumn("purgedAtUtc", "timestamptz", (col) => col.notNull()).addColumn("requestId", "text", (col) => col.notNull()).execute();
7312
+ }
7313
+ async function down$2(db) {
7314
+ await db.schema.dropTable("document_purges").execute();
7315
+ }
7316
+ //#endregion
7317
+ //#region src/storage/migrations/025_create_sync_received_markers.ts
7318
+ var _025_create_sync_received_markers_exports = /* @__PURE__ */ __exportAll({
7319
+ down: () => down$1,
7320
+ up: () => up$1
7321
+ });
7322
+ /** Received purge markers awaiting their outcome; removed with their remote. */
7323
+ async function up$1(db) {
7324
+ await db.schema.createTable("sync_received_markers").addColumn("remote_name", "text", (col) => col.notNull()).addColumn("marker_id", "text", (col) => col.notNull()).addColumn("document_id", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("operation", "jsonb", (col) => col.notNull()).addColumn("received_at_utc_ms", "bigint", (col) => col.notNull()).addPrimaryKeyConstraint("sync_received_markers_pkey", ["remote_name", "marker_id"]).execute();
7325
+ }
7326
+ async function down$1(db) {
7327
+ await db.schema.dropTable("sync_received_markers").execute();
7328
+ }
7329
+ //#endregion
7330
+ //#region src/storage/migrations/026_create_sync_purge_refusals.ts
7331
+ var _026_create_sync_purge_refusals_exports = /* @__PURE__ */ __exportAll({
7332
+ down: () => down,
7333
+ up: () => up
7334
+ });
7335
+ /** A remote's refusal of a purge marker; payload-free, kept like the tombstone. */
7336
+ async function up(db) {
7337
+ await db.schema.createTable("sync_purge_refusals").addColumn("remote_name", "text", (col) => col.notNull()).addColumn("document_id", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("refused_at_utc_ms", "bigint", (col) => col.notNull()).addPrimaryKeyConstraint("sync_purge_refusals_pkey", [
7338
+ "remote_name",
7339
+ "document_id",
7340
+ "branch"
7341
+ ]).execute();
7342
+ await db.schema.createIndex("idx_sync_purge_refusals_document").on("sync_purge_refusals").column("document_id").execute();
7343
+ }
7344
+ async function down(db) {
7345
+ await db.schema.dropTable("sync_purge_refusals").execute();
7346
+ }
7347
+ //#endregion
5227
7348
  //#region src/storage/migrations/migrator.ts
5228
7349
  const REACTOR_SCHEMA = "reactor";
5229
7350
  const migrations = {
@@ -5245,7 +7366,14 @@ const migrations = {
5245
7366
  "016_add_dead_letter_error_type": _016_add_dead_letter_error_type_exports,
5246
7367
  "017_create_group_references": _017_create_group_references_exports,
5247
7368
  "018_add_sync_remote_bound_address": _018_add_sync_remote_bound_address_exports,
5248
- "019_require_action_id": _019_require_action_id_exports
7369
+ "019_require_action_id": _019_require_action_id_exports,
7370
+ "020_add_snapshot_operation_ordinal": _020_add_snapshot_operation_ordinal_exports,
7371
+ "021_add_sync_remote_peer": _021_add_sync_remote_peer_exports,
7372
+ "022_create_sync_holds": _022_create_sync_holds_exports,
7373
+ "023_index_created_protocol_versions": _023_index_created_protocol_versions_exports,
7374
+ "024_create_document_purges": _024_create_document_purges_exports,
7375
+ "025_create_sync_received_markers": _025_create_sync_received_markers_exports,
7376
+ "026_create_sync_purge_refusals": _026_create_sync_purge_refusals_exports
5249
7377
  };
5250
7378
  var ProgrammaticMigrationProvider = class {
5251
7379
  getMigrations() {
@@ -5303,8 +7431,163 @@ async function getMigrationStatus(db, schema = REACTOR_SCHEMA) {
5303
7431
  }
5304
7432
  //#endregion
5305
7433
  //#region src/core/drive-container-types.ts
5306
- const DEFAULT_DRIVE_CONTAINER_TYPES = new Set(["powerhouse/document-drive", "powerhouse/reactor-drive"]);
7434
+ const DEFAULT_DRIVE_CONTAINER_TYPES = /* @__PURE__ */ new Set(["powerhouse/document-drive", "powerhouse/reactor-drive"]);
7435
+ //#endregion
7436
+ //#region src/executor/worker/error-info.ts
7437
+ const MAX_STACK_LENGTH = 8192;
7438
+ const MAX_CAUSE_DEPTH = 8;
7439
+ function toErrorInfo(err, depth = 0) {
7440
+ if (depth > MAX_CAUSE_DEPTH) return {
7441
+ name: "Error",
7442
+ message: "cause chain truncated"
7443
+ };
7444
+ if (err instanceof Error) {
7445
+ const info = {
7446
+ name: err.name || "Error",
7447
+ message: err.message
7448
+ };
7449
+ if (typeof err.stack === "string") info.stack = err.stack.length > MAX_STACK_LENGTH ? err.stack.slice(0, MAX_STACK_LENGTH) : err.stack;
7450
+ if (err.cause !== void 0) info.cause = toErrorInfo(err.cause, depth + 1);
7451
+ const documentId = err.documentId;
7452
+ if (typeof documentId === "string") info.documentId = documentId;
7453
+ const { documentType, requestedVersion } = err;
7454
+ if (typeof documentType === "string") info.documentType = documentType;
7455
+ if (typeof requestedVersion === "number") info.requestedVersion = requestedVersion;
7456
+ return info;
7457
+ }
7458
+ if (typeof err === "string") return {
7459
+ name: "Error",
7460
+ message: err
7461
+ };
7462
+ if (err && typeof err === "object") {
7463
+ const record = err;
7464
+ return {
7465
+ name: typeof record.name === "string" ? record.name : "Error",
7466
+ message: typeof record.message === "string" ? record.message : safeStringify(err)
7467
+ };
7468
+ }
7469
+ return {
7470
+ name: "Error",
7471
+ message: safeStringify(err)
7472
+ };
7473
+ }
7474
+ function safeStringify(value) {
7475
+ if (value === null || value === void 0) return String(value);
7476
+ if (typeof value === "string") return value;
7477
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
7478
+ try {
7479
+ return JSON.stringify(value);
7480
+ } catch {
7481
+ return "[unserializable]";
7482
+ }
7483
+ }
7484
+ function fromErrorInfo(info) {
7485
+ const err = new Error(info.message);
7486
+ Object.defineProperty(err, "name", {
7487
+ value: info.name,
7488
+ configurable: true,
7489
+ writable: true
7490
+ });
7491
+ if (info.stack !== void 0) err.stack = info.stack;
7492
+ if (info.documentId !== void 0) Object.defineProperty(err, "documentId", {
7493
+ value: info.documentId,
7494
+ configurable: true,
7495
+ writable: true
7496
+ });
7497
+ for (const key of ["documentType", "requestedVersion"]) if (info[key] !== void 0) Object.defineProperty(err, key, {
7498
+ value: info[key],
7499
+ configurable: true,
7500
+ writable: true
7501
+ });
7502
+ if (info.cause !== void 0) Object.defineProperty(err, "cause", {
7503
+ value: fromErrorInfo(info.cause),
7504
+ configurable: true,
7505
+ writable: true
7506
+ });
7507
+ return err;
7508
+ }
7509
+ //#endregion
7510
+ //#region src/executor/worker/sanitize.ts
7511
+ const MAX_DEPTH = 8;
7512
+ function errorToInfo(err) {
7513
+ return toErrorInfo(err);
7514
+ }
7515
+ function sanitizeArg(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
7516
+ if (depth > MAX_DEPTH) return "[Truncated]";
7517
+ if (value === null || value === void 0) return null;
7518
+ if (typeof value === "boolean") return value;
7519
+ if (typeof value === "number") return value;
7520
+ if (typeof value === "string") return value;
7521
+ if (typeof value === "bigint") return value.toString();
7522
+ if (typeof value === "function" || typeof value === "symbol") return null;
7523
+ if (value instanceof Error) return toErrorInfo(value);
7524
+ if (value instanceof Date) return value.toISOString();
7525
+ if (value instanceof Map) {
7526
+ const obj = {};
7527
+ for (const [k, v] of value.entries()) obj[String(k)] = sanitizeArg(v, depth + 1, seen);
7528
+ return obj;
7529
+ }
7530
+ if (value instanceof Set) {
7531
+ const arr = [];
7532
+ for (const v of value.values()) arr.push(sanitizeArg(v, depth + 1, seen));
7533
+ return arr;
7534
+ }
7535
+ if (Array.isArray(value)) {
7536
+ if (seen.has(value)) return "[Circular]";
7537
+ seen.add(value);
7538
+ const result = value.map((v) => sanitizeArg(v, depth + 1, seen));
7539
+ seen.delete(value);
7540
+ return result;
7541
+ }
7542
+ if (typeof value === "object") {
7543
+ const proto = Object.getPrototypeOf(value);
7544
+ if (proto !== Object.prototype && proto !== null) return { __nonClonable: value.constructor?.name || "Unknown" };
7545
+ if (seen.has(value)) return "[Circular]";
7546
+ seen.add(value);
7547
+ const result = {};
7548
+ for (const [k, v] of Object.entries(value)) result[k] = sanitizeArg(v, depth + 1, seen);
7549
+ seen.delete(value);
7550
+ return result;
7551
+ }
7552
+ return null;
7553
+ }
7554
+ //#endregion
7555
+ //#region src/executor/worker/forwarding-logger.ts
7556
+ function createForwardingLogger(post, level = "info", tags = []) {
7557
+ function safePost(msg) {
7558
+ try {
7559
+ post(msg);
7560
+ } catch {
7561
+ try {
7562
+ process.stderr.write(`[forwarding-logger] postMessage failed: ${msg.level} ${msg.message}\n`);
7563
+ } catch {}
7564
+ }
7565
+ }
7566
+ function emit(wireLevel, message, args) {
7567
+ const sanitizedArgs = args.map((a) => sanitizeArg(a));
7568
+ safePost({
7569
+ type: "log",
7570
+ level: wireLevel,
7571
+ message,
7572
+ args: tags.length > 0 ? [sanitizeArg({ tags }), ...sanitizedArgs] : sanitizedArgs,
7573
+ timestamp: Date.now()
7574
+ });
7575
+ }
7576
+ return {
7577
+ level,
7578
+ verbose: (message, ...args) => emit("debug", message, args),
7579
+ debug: (message, ...args) => emit("debug", message, args),
7580
+ info: (message, ...args) => emit("info", message, args),
7581
+ warn: (message, ...args) => emit("warn", message, args),
7582
+ error: (message, ...args) => emit("error", message, args),
7583
+ errorHandler: ((...data) => {
7584
+ const first = data[0];
7585
+ emit("error", typeof first === "string" ? first : "", data.slice(1));
7586
+ }),
7587
+ child: (childTags) => createForwardingLogger(post, level, [...tags, ...childTags])
7588
+ };
7589
+ }
5307
7590
  //#endregion
5308
- export { selectDecisionModel as A, RevisionMismatchError as B, InvalidModuleError as C, createEmptyConsistencyToken as D, createConsistencyToken as E, AppendConditionFailedError as F, DocumentNotFoundError as G, AuthTimestampNotMonotonicError as H, DocumentAlreadyExistsError as I, UpgradePreconditionFailedError as J, ExcessiveReshuffleError as K, DocumentExistence as L, authDecisionModel as M, buildDecisionModel as N, targetDocumentId as O, APPEND_CONDITION_FAILED_PREFIX as P, __exportAll as Q, DuplicateOperationError as R, DuplicateModuleError as S, GATED_DOCUMENT_ACTIONS as T, AuthorizationDeniedError as U, AuthEnforcementDisabledError as V, DocumentDeletedError as W, parsePagingOptions as X, matchesScope as Y, throwIfAborted as Z, DocumentMetaCache as _, createForwardingPoolInstrumentation as a, JobExecutorEventTypes as b, KyselyKeyframeStore as c, DriveCollectionId as d, KyselyExecutionScope as f, KyselyOperationIndex as g, KyselyWriteCache as h, runMigrations as i, documentDecisionModel as j, decideAtHead as k, DocumentModelRegistry as l, EventBus as m, REACTOR_SCHEMA as n, instrumentPgPool as o, resolveFeatureFlags as p, InvalidOperationTimestampError as q, getMigrationStatus as r, KyselyOperationStore as s, DEFAULT_DRIVE_CONTAINER_TYPES as t, SimpleJobExecutor as u, CollectionMembershipCache as v, ModuleNotFoundError as w, DuplicateManifestError as x, DEFAULT_DEFERRED_JOB_TTL_MS as y, OptimisticLockError as z };
7591
+ export { decideAtHead as $, ProbeSettler as A, ReactorEventTypes as At, JobExecutorEventTypes as B, KyselyWriteCache as C, PurgeTooLargeError as Ct, acquirePurgeLocks as D, UnsupportedStoredProtocolError as Dt, PURGE_NS as E, UnsupportedProtocolVersionError as Et, snapshotFunctionsFor as F, __exportAll as Ft, ModuleNotFoundError as G, DuplicateModuleError as H, DocumentMetaCache as I, createDocumentFromAction as J, GATED_DOCUMENT_ACTIONS as K, CollectionMembershipCache as L, createKyselyWatermarkProbe as M, parsePagingOptions as Mt, describeWaitingSessions as N, throwIfAborted as Nt, findPurged as O, UpgradePreconditionFailedError as Ot, parseSnapshot as P, yieldToMain as Pt, targetDocumentId as Q, DEFAULT_DEFERRED_JOB_TTL_MS as R, EventBus as S, InvalidSignatureError as St, PURGE_LOCK_BUCKETS as T, ReservedActionError as Tt, InvalidModuleError as U, DuplicateManifestError as V, ModelNotWorkerImportableError as W, submittedActionIds as X, createEmptyConsistencyToken as Y, summarizeSubmittedActions as Z, KyselyExecutionScope as _, DocumentNotFoundError as _t, DEFAULT_DRIVE_CONTAINER_TYPES as a, APPEND_CONDITION_FAILED_PREFIX as at, appliedDelete as b, GroupInUseError as bt, runMigrations as c, DocumentExistence as ct, KyselyOperationStore as d, RevisionMismatchError as dt, selectDecisionModel as et, KyselyKeyframeStore as f, AuthEnforcementDisabledError as ft, PassthroughSigner as g, DocumentNotDeletedError as gt, verifyActionSignature as h, DocumentDeletedError as ht, fromErrorInfo as i, DriveCollectionId as it, SettledWatermark as j, matchesScope as jt, listPurged as k, EventBusAggregateError as kt, createForwardingPoolInstrumentation as l, DuplicateOperationError as lt, SimpleJobExecutor as m, AuthorizationDeniedError as mt, errorToInfo as n, authDecisionModel as nt, REACTOR_SCHEMA as o, AppendConditionFailedError as ot, DocumentModelRegistry as p, AuthTimestampNotMonotonicError as pt, createConsistencyToken as q, sanitizeArg as r, buildDecisionModel as rt, getMigrationStatus as s, DocumentAlreadyExistsError as st, createForwardingLogger as t, documentDecisionModel as tt, instrumentPgPool as u, OptimisticLockError as ut, DEFAULT_PURGE_DELETE_BATCH as v, DocumentPurgedError as vt, KyselyOperationIndex as w, RelationshipNotFoundError as wt, resolveFeatureFlags as x, InvalidOperationTimestampError as xt, KyselyDocumentPurger as y, ExcessiveReshuffleError as yt, DEFAULT_MAX_PURGE_OPERATIONS as z };
5309
7592
 
5310
- //# sourceMappingURL=drive-container-types-yZrksiJR.js.map
7593
+ //# sourceMappingURL=forwarding-logger-BKDP4JnD.js.map