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

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-B9IfcRij.js} +30 -8
  2. package/dist/build-worker-executor-B9IfcRij.js.map +1 -0
  3. package/dist/{document-indexer-C5Gsa1B3.js → document-indexer-CZqIpdz2.js} +981 -104
  4. package/dist/document-indexer-CZqIpdz2.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-DMqDs9tE.js} +2631 -369
  11. package/dist/forwarding-logger-DMqDs9tE.js.map +1 -0
  12. package/dist/index.d.ts +4811 -3193
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +4103 -880
  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-BSK-GEyP.js} +24 -4
  20. package/dist/projection-shard-manager-BSK-GEyP.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-D-19Jmpz.js} +30 -17
  28. package/dist/worker-handle-D-19Jmpz.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
  /**
@@ -978,6 +1295,8 @@ var InvalidUpgradeStepError = class extends Error {
978
1295
  };
979
1296
  //#endregion
980
1297
  //#region src/executor/types.ts
1298
+ /** 200k operations purge in 1.6 s on local Postgres; jobs time out at 30 s. */
1299
+ const DEFAULT_MAX_PURGE_OPERATIONS = 2e5;
981
1300
  /** How long a deferred job waits for its document before it fails. */
982
1301
  const DEFAULT_DEFERRED_JOB_TTL_MS = 3e4;
983
1302
  /**
@@ -993,6 +1312,7 @@ const JobExecutorEventTypes = {
993
1312
  //#endregion
994
1313
  //#region src/cache/collection-membership-cache.ts
995
1314
  var CollectionMembershipCache = class CollectionMembershipCache {
1315
+ operationIndex;
996
1316
  cache = /* @__PURE__ */ new Map();
997
1317
  constructor(operationIndex) {
998
1318
  this.operationIndex = operationIndex;
@@ -1190,26 +1510,294 @@ var DocumentMetaCache = class DocumentMetaCache {
1190
1510
  const docScopeOps = await this.operationStore.getSince(documentId, "document", branch, -1, void 0, void 0, signal);
1191
1511
  if (docScopeOps.results.length === 0) throw new DocumentNotFoundError(documentId);
1192
1512
  const createOp = docScopeOps.results[0];
1513
+ if (isPurgeMarker(createOp)) return purgedMeta(createOp.action);
1193
1514
  if (createOp.action.type !== "CREATE_DOCUMENT") throw new Error(`Invalid document: first operation must be CREATE_DOCUMENT, found ${createOp.action.type}`);
1194
1515
  const createAction = createOp.action;
1195
1516
  const documentType = createAction.input.model;
1196
1517
  let document = createDocumentFromAction(createAction);
1197
1518
  let documentScopeRevision = 0;
1519
+ let purged = false;
1198
1520
  for (const op of docScopeOps.results) {
1199
1521
  if (targetRevision !== void 0 && op.index > targetRevision) break;
1200
1522
  documentScopeRevision = op.index;
1201
1523
  if (op.action.type === "UPGRADE_DOCUMENT") {
1202
1524
  const upgradeAction = op.action;
1203
1525
  document = applyUpgradeDocumentAction$1(document, upgradeAction);
1204
- } else if (op.action.type === "DELETE_DOCUMENT") document = applyDeleteDocumentAction$1(document, op.action);
1526
+ } else if (op.action.type === "DELETE_DOCUMENT" || op.action.type === PURGE_DOCUMENT) {
1527
+ document = applyDeleteDocumentAction$1(document, op.action);
1528
+ purged ||= op.action.type === PURGE_DOCUMENT;
1529
+ }
1205
1530
  }
1206
1531
  return {
1207
1532
  state: document.state.document,
1208
1533
  documentType,
1534
+ protocolVersions: purged ? purgedProtocolVersions() : createAction.input.protocolVersions,
1209
1535
  documentScopeRevision: documentScopeRevision + 1
1210
1536
  };
1211
1537
  }
1212
1538
  };
1539
+ /** A lone marker has no CREATE_DOCUMENT; the marker describes the stream. */
1540
+ function purgedMeta(action) {
1541
+ return {
1542
+ state: createDocumentState({
1543
+ isDeleted: true,
1544
+ deletedAtUtcIso: action.input.purgedAtUtcIso
1545
+ }),
1546
+ documentType: action.input.documentType,
1547
+ protocolVersions: purgedProtocolVersions(),
1548
+ documentScopeRevision: 1
1549
+ };
1550
+ }
1551
+ //#endregion
1552
+ //#region src/catch-up/settled-watermark.ts
1553
+ const MAX_PENDING_PROBES = 64;
1554
+ /** pg_sequence_last_value needs 10; pg_current_* replaced txid_* in 13. */
1555
+ function snapshotFunctionsFor(serverVersionNum) {
1556
+ if (serverVersionNum >= 13e4) return {
1557
+ snapshot: "pg_current_snapshot",
1558
+ currentXid: "pg_current_xact_id",
1559
+ xidIfAssigned: "pg_current_xact_id_if_assigned"
1560
+ };
1561
+ if (serverVersionNum >= 1e5) return {
1562
+ snapshot: "txid_current_snapshot",
1563
+ currentXid: "txid_current",
1564
+ xidIfAssigned: "txid_current_if_assigned"
1565
+ };
1566
+ throw new Error(`PostgreSQL server_version_num ${serverVersionNum} is not supported: read-side catch-up needs 10 or later`);
1567
+ }
1568
+ /** Parses the `xmin:xmax:xip,...` text form of a snapshot. */
1569
+ function parseSnapshot(text) {
1570
+ const parts = text.split(":");
1571
+ if (parts.length !== 3) throw new Error(`Malformed snapshot: ${text}`);
1572
+ const [xmin, xmax, xip] = parts;
1573
+ return {
1574
+ xmin: BigInt(xmin),
1575
+ xmax: BigInt(xmax),
1576
+ xip: xip === "" ? [] : xip.split(",").map((xid) => BigInt(xid))
1577
+ };
1578
+ }
1579
+ async function readSnapshotFunctions(db) {
1580
+ const result = await sql`select current_setting('server_version_num') as version`.execute(db);
1581
+ return snapshotFunctionsFor(Number(result.rows[0].version));
1582
+ }
1583
+ /** A probe settles once an xmin passes the xid it took after its head. */
1584
+ var ProbeSettler = class {
1585
+ settled = 0;
1586
+ lastHead = 0;
1587
+ covered = 0;
1588
+ lastXip = [];
1589
+ pending = [];
1590
+ get settledThrough() {
1591
+ return this.settled;
1592
+ }
1593
+ get head() {
1594
+ return this.lastHead;
1595
+ }
1596
+ /** The highest head a settled or pending probe covers. */
1597
+ get probedHead() {
1598
+ return this.covered;
1599
+ }
1600
+ observe(head, xid, snapshot, nowUtcMs) {
1601
+ this.lastHead = Math.max(this.lastHead, head);
1602
+ this.lastXip = snapshot.xip;
1603
+ if (xid !== null && head > this.covered) {
1604
+ this.covered = head;
1605
+ this.pending.push({
1606
+ head,
1607
+ xid,
1608
+ takenAtUtcMs: nowUtcMs
1609
+ });
1610
+ if (this.pending.length > MAX_PENDING_PROBES) this.pending.shift();
1611
+ }
1612
+ const stillPending = [];
1613
+ for (const probe of this.pending) if (snapshot.xmin > probe.xid) this.settled = Math.max(this.settled, probe.head);
1614
+ else stillPending.push(probe);
1615
+ this.pending = stillPending.filter((probe) => probe.head > this.settled);
1616
+ return this.settled;
1617
+ }
1618
+ /** Xids an unsettled probe still waits on. */
1619
+ waitingOn() {
1620
+ if (this.pending.length === 0) return [];
1621
+ const xid = this.pending[0].xid;
1622
+ return this.lastXip.filter((open) => open < xid).map((open) => open.toString());
1623
+ }
1624
+ stalledSinceUtcMs() {
1625
+ return this.pending[0]?.takenAtUtcMs;
1626
+ }
1627
+ };
1628
+ function operationIndexTableName(db) {
1629
+ const from = db.selectFrom("operation_index_operations").selectAll().toOperationNode().from?.froms[0];
1630
+ const schema = from !== void 0 && TableNode.is(from) ? from.table.schema?.name : void 0;
1631
+ const quote = (identifier) => `"${identifier.replace(/"/g, "\"\"")}"`;
1632
+ return schema === void 0 ? quote("operation_index_operations") : `${quote(schema)}.${quote("operation_index_operations")}`;
1633
+ }
1634
+ /** Reads the head, takes and commits an xid, then reads a snapshot, in order. */
1635
+ function createKyselyWatermarkProbe(db) {
1636
+ let resolved = null;
1637
+ const resolve = async () => {
1638
+ const fns = await readSnapshotFunctions(db);
1639
+ const table = operationIndexTableName(db);
1640
+ const sequence = (await sql`select pg_get_serial_sequence(${table}, 'ordinal') as sequence`.execute(db)).rows[0]?.sequence;
1641
+ if (!sequence) throw new Error(`No ordinal sequence found for ${table}`);
1642
+ return {
1643
+ fns,
1644
+ sequence
1645
+ };
1646
+ };
1647
+ return async (probedHead) => {
1648
+ resolved ??= resolve().catch((error) => {
1649
+ resolved = null;
1650
+ throw error;
1651
+ });
1652
+ const { fns, sequence } = await resolved;
1653
+ 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];
1654
+ const head = Number(headRow.head);
1655
+ let xid = null;
1656
+ if (headRow.outside_write && head > probedHead) {
1657
+ const xidResult = await sql`select ${sql.raw(fns.currentXid)}()::text as xid`.execute(db);
1658
+ xid = BigInt(xidResult.rows[0].xid);
1659
+ }
1660
+ 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];
1661
+ return {
1662
+ head,
1663
+ xid,
1664
+ snapshot: parseSnapshot(row.snapshot),
1665
+ outsideWrite: headRow.outside_write && row.outside_write
1666
+ };
1667
+ };
1668
+ }
1669
+ /** Knows when an ordinal gap is final by asking which transactions are open. */
1670
+ var SettledWatermark = class {
1671
+ probe;
1672
+ logger;
1673
+ now;
1674
+ settler = new ProbeSettler();
1675
+ listeners = /* @__PURE__ */ new Set();
1676
+ current;
1677
+ next;
1678
+ warnedInsideWrite = false;
1679
+ constructor(probe, logger, now = Date.now) {
1680
+ this.probe = probe;
1681
+ this.logger = logger;
1682
+ this.now = now;
1683
+ }
1684
+ get settledThrough() {
1685
+ return this.settler.settledThrough;
1686
+ }
1687
+ refresh(signal) {
1688
+ signal?.throwIfAborted();
1689
+ if (this.next !== void 0) return this.next;
1690
+ if (this.current === void 0) return this.start();
1691
+ const next = this.current.catch(() => this.settledThrough).then(() => {
1692
+ this.next = void 0;
1693
+ return this.start();
1694
+ });
1695
+ this.next = next;
1696
+ return next;
1697
+ }
1698
+ onAdvance(listener) {
1699
+ this.listeners.add(listener);
1700
+ return () => {
1701
+ this.listeners.delete(listener);
1702
+ };
1703
+ }
1704
+ status() {
1705
+ const stalledSinceUtcMs = this.settler.head > this.settledThrough ? this.settler.stalledSinceUtcMs() : void 0;
1706
+ return {
1707
+ head: this.settler.head,
1708
+ settledThrough: this.settledThrough,
1709
+ waitingOn: this.settler.waitingOn(),
1710
+ ...stalledSinceUtcMs !== void 0 ? { stalledSinceUtcMs } : {}
1711
+ };
1712
+ }
1713
+ start() {
1714
+ const run = this.probeOnce().finally(() => {
1715
+ if (this.current === run) this.current = void 0;
1716
+ });
1717
+ this.current = run;
1718
+ return run;
1719
+ }
1720
+ async probeOnce() {
1721
+ const reading = await this.probe(this.settler.probedHead);
1722
+ if (!reading.outsideWrite) {
1723
+ if (!this.warnedInsideWrite) {
1724
+ this.warnedInsideWrite = true;
1725
+ this.logger.warn("settled watermark probe ran inside another session's write transaction; discarded");
1726
+ }
1727
+ return this.settledThrough;
1728
+ }
1729
+ const before = this.settledThrough;
1730
+ const after = this.settler.observe(reading.head, reading.xid, reading.snapshot, this.now());
1731
+ if (after > before) for (const listener of this.listeners) try {
1732
+ listener(after);
1733
+ } catch (error) {
1734
+ this.logger.error("settled watermark listener failed: @error", error);
1735
+ }
1736
+ return after;
1737
+ }
1738
+ };
1739
+ /** Sessions holding these xids open; empty without pg_stat_activity. */
1740
+ async function describeWaitingSessions(db, xids) {
1741
+ if (xids.length === 0) return [];
1742
+ const epochless = xids.map((xid) => (BigInt(xid) % 2n ** 32n).toString());
1743
+ try {
1744
+ return (await sql`select pid, application_name, state, xact_start, backend_xid::text as xid
1745
+ from pg_stat_activity
1746
+ where backend_xid::text in (${sql.join(epochless)})`.execute(db)).rows.map((row) => ({
1747
+ pid: Number(row.pid),
1748
+ applicationName: row.application_name ?? "",
1749
+ state: row.state ?? "",
1750
+ xactStart: row.xact_start === null ? null : new Date(row.xact_start).toISOString(),
1751
+ xid: row.xid
1752
+ }));
1753
+ } catch {
1754
+ return [];
1755
+ }
1756
+ }
1757
+ //#endregion
1758
+ //#region src/storage/kysely/document-purges.ts
1759
+ /** Class key of every purge lock; the stream locks use the single-key space. */
1760
+ const PURGE_NS = 1347571013;
1761
+ /** Ids share this many lock keys, so a job's lock count stays bounded. */
1762
+ const PURGE_LOCK_BUCKETS = 1024;
1763
+ async function acquirePurgeLocks(trx, ids, mode) {
1764
+ const unique = [...new Set(ids)];
1765
+ if (unique.length === 0) return;
1766
+ const lock = sql.raw(mode === "shared" ? "pg_advisory_xact_lock_shared" : "pg_advisory_xact_lock");
1767
+ await sql`
1768
+ with keys as materialized (
1769
+ select distinct hashtext(id) & ${sql.lit(1023)} as key
1770
+ from unnest(${unique}::text[]) as t(id)
1771
+ order by key
1772
+ )
1773
+ select ${lock}(${sql.lit(PURGE_NS)}, key) from keys order by key
1774
+ `.execute(trx);
1775
+ }
1776
+ /** The ids among `ids` that have a tombstone, read in the handle's schema. */
1777
+ async function findPurged(db, ids) {
1778
+ const unique = [...new Set(ids)];
1779
+ if (unique.length === 0) return /* @__PURE__ */ new Set();
1780
+ const rows = await db.selectFrom("document_purges").select("documentId").where(sql`"documentId" = any(${unique}::text[])`).execute();
1781
+ return new Set(rows.map((row) => row.documentId));
1782
+ }
1783
+ /** Every tombstoned id, read in the handle's schema. */
1784
+ async function listPurged(db) {
1785
+ return (await db.selectFrom("document_purges").select("documentId").orderBy("documentId").execute()).map((row) => row.documentId);
1786
+ }
1787
+ /** Bind slots per statement: 16-bit on the wire, read signed in-process. */
1788
+ const MAX_BIND_PARAMETERS = 32767;
1789
+ /** Splits rows into runs that each bind at most MAX_BIND_PARAMETERS. */
1790
+ function chunkRows(rows) {
1791
+ if (rows.length === 0) return [];
1792
+ const columnsPerRow = Math.max(1, Object.keys(rows[0]).length);
1793
+ const perChunk = Math.max(1, Math.floor(MAX_BIND_PARAMETERS / columnsPerRow));
1794
+ if (rows.length <= perChunk) return [rows];
1795
+ const chunks = [];
1796
+ for (let i = 0; i < rows.length; i += perChunk) chunks.push(rows.slice(i, i + perChunk));
1797
+ return chunks;
1798
+ }
1799
+ /** Per database: the function that assigns the transaction's xid. */
1800
+ const xidFunctions = /* @__PURE__ */ new WeakMap();
1213
1801
  var KyselyOperationIndexTxn = class {
1214
1802
  collections = [];
1215
1803
  collectionMemberships = [];
@@ -1275,16 +1863,20 @@ var KyselyOperationIndexTxn = class {
1275
1863
  }
1276
1864
  };
1277
1865
  var KyselyOperationIndex = class KyselyOperationIndex {
1866
+ db;
1278
1867
  trx;
1868
+ liveIds;
1279
1869
  constructor(db) {
1280
1870
  this.db = db;
1281
1871
  }
1282
1872
  get queryExecutor() {
1283
1873
  return this.trx ?? this.db;
1284
1874
  }
1285
- withTransaction(trx) {
1875
+ /** `liveIds`: ids the transaction read untombstoned under its shared lock. */
1876
+ withTransaction(trx, liveIds) {
1286
1877
  const instance = new KyselyOperationIndex(this.db);
1287
1878
  instance.trx = trx;
1879
+ instance.liveIds = liveIds;
1288
1880
  return instance;
1289
1881
  }
1290
1882
  start() {
@@ -1331,10 +1923,12 @@ var KyselyOperationIndex = class KyselyOperationIndex {
1331
1923
  leftOrdinal: null
1332
1924
  }));
1333
1925
  for (const collectionId of collections) kyselyTxn.recordMembershipInvalidation(collectionId);
1334
- await trx.insertInto("document_collections").values(collectionRows).onConflict((oc) => oc.doNothing()).execute();
1926
+ for (const chunk of chunkRows(collectionRows)) await trx.insertInto("document_collections").values(chunk).onConflict((oc) => oc.doNothing()).execute();
1335
1927
  }
1336
1928
  let operationOrdinals = [];
1337
1929
  if (operations.length > 0) {
1930
+ await this.refusePurgedOperations(trx, operations);
1931
+ await this.assignXid(trx);
1338
1932
  const operationRows = operations.map((op) => ({
1339
1933
  opId: op.id || "",
1340
1934
  documentId: op.documentId,
@@ -1349,7 +1943,10 @@ var KyselyOperationIndex = class KyselyOperationIndex {
1349
1943
  deniedReason: op.deniedReason ?? null,
1350
1944
  sourceRemote: op.sourceRemote
1351
1945
  }));
1352
- operationOrdinals = (await trx.insertInto("operation_index_operations").values(operationRows).returning("ordinal").execute()).map((row) => row.ordinal);
1946
+ for (const chunk of chunkRows(operationRows)) {
1947
+ const insertedOps = await trx.insertInto("operation_index_operations").values(chunk).returning("ordinal").execute();
1948
+ operationOrdinals = operationOrdinals.concat(insertedOps.map((row) => row.ordinal));
1949
+ }
1353
1950
  }
1354
1951
  if (memberships.length > 0) for (const m of memberships) {
1355
1952
  const ordinal = operationOrdinals[m.operationIndex];
@@ -1382,10 +1979,44 @@ var KyselyOperationIndex = class KyselyOperationIndex {
1382
1979
  }
1383
1980
  return operationOrdinals;
1384
1981
  }
1982
+ async getCollectionsInRange(after, through, among, signal) {
1983
+ signal?.throwIfAborted();
1984
+ if (through <= after || among?.length === 0) return [];
1985
+ 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);
1986
+ if (among !== void 0) query = query.where("dc.collectionId", "in", [...among]);
1987
+ return (await query.execute()).map((row) => row.collectionId);
1988
+ }
1989
+ async getOrdinalsInRange(after, through, limit, signal) {
1990
+ signal?.throwIfAborted();
1991
+ if (through <= after || limit <= 0) return [];
1992
+ 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));
1993
+ }
1994
+ async getByOrdinals(ordinals, signal) {
1995
+ signal?.throwIfAborted();
1996
+ if (ordinals.length === 0) return [];
1997
+ const results = [];
1998
+ for (let i = 0; i < ordinals.length; i += MAX_BIND_PARAMETERS) {
1999
+ const chunk = ordinals.slice(i, i + MAX_BIND_PARAMETERS);
2000
+ const rows = await this.queryExecutor.selectFrom("operation_index_operations").selectAll().where("ordinal", "in", chunk).orderBy("ordinal", "asc").execute();
2001
+ for (const row of rows) results.push(this.rowToOperationWithContext(row));
2002
+ }
2003
+ results.sort((a, b) => a.context.ordinal - b.context.ordinal);
2004
+ return results;
2005
+ }
2006
+ async getStreamAfter(stream, after, signal, limit) {
2007
+ signal?.throwIfAborted();
2008
+ 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));
2009
+ }
1385
2010
  async getGroupReferencers(groupId, signal) {
1386
2011
  if (signal?.aborted) throw new Error("Operation aborted");
1387
2012
  return (await this.queryExecutor.selectFrom("group_references").select("documentId").where("groupId", "=", groupId).orderBy("documentId").execute()).map((row) => row.documentId);
1388
2013
  }
2014
+ async getOrdinalsByOpIds(documentId, scope, branch, opIds, signal) {
2015
+ signal?.throwIfAborted();
2016
+ if (opIds.length === 0) return /* @__PURE__ */ new Map();
2017
+ 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();
2018
+ return new Map(rows.map((row) => [row.opId, Number(row.ordinal)]));
2019
+ }
1389
2020
  async find(collectionId, cursor, view, paging, signal) {
1390
2021
  if (signal?.aborted) throw new Error("Operation aborted");
1391
2022
  const outerCursor = cursor ?? -1;
@@ -1399,6 +2030,7 @@ var KyselyOperationIndex = class KyselyOperationIndex {
1399
2030
  if (view?.branch) qb = qb.where("oi.branch", "=", view.branch);
1400
2031
  if (view?.scopes && view.scopes.length > 0) qb = qb.where("oi.scope", "in", view.scopes);
1401
2032
  if (view?.excludeSourceRemote) qb = qb.where("oi.sourceRemote", "!=", view.excludeSourceRemote);
2033
+ if (view?.throughOrdinal !== void 0) qb = qb.where("oi.ordinal", "<=", view.throughOrdinal).where("dc.joinedOrdinal", "<=", BigInt(view.throughOrdinal));
1402
2034
  return qb;
1403
2035
  };
1404
2036
  const rows = await buildBranch("joiner").unionAll(buildBranch("newOps")).orderBy("ordinal", "asc").limit(limit + 1).execute();
@@ -1487,6 +2119,23 @@ var KyselyOperationIndex = class KyselyOperationIndex {
1487
2119
  }, signal) : void 0
1488
2120
  };
1489
2121
  }
2122
+ /** Backstop to the store's refusal: only a marker indexes a purged id. */
2123
+ async refusePurgedOperations(trx, operations) {
2124
+ const ids = new Set(operations.filter((operation) => !isPurgeMarker(operation)).map((operation) => operation.documentId).filter((id) => !this.liveIds?.has(id)));
2125
+ const purged = await findPurged(trx, ids);
2126
+ for (const id of ids) if (purged.has(id)) throw new DocumentPurgedError(id);
2127
+ }
2128
+ /** Takes the xid before the first ordinal, as the settled watermark needs. */
2129
+ async assignXid(trx) {
2130
+ let fn = xidFunctions.get(this.db);
2131
+ if (fn === void 0) {
2132
+ fn = readSnapshotFunctions(trx).then((fns) => fns.currentXid);
2133
+ xidFunctions.set(this.db, fn);
2134
+ fn.catch(() => xidFunctions.delete(this.db));
2135
+ }
2136
+ const name = await fn;
2137
+ await sql`select ${sql.raw(name)}()`.execute(trx);
2138
+ }
1490
2139
  rowToOperationWithContext(row) {
1491
2140
  return {
1492
2141
  operation: {
@@ -1605,21 +2254,6 @@ var RingBuffer = class {
1605
2254
  }
1606
2255
  };
1607
2256
  //#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
2257
  //#region src/cache/kysely-write-cache.ts
1624
2258
  /**
1625
2259
  * The last operation index a keyframe's document reflects for the scope. A
@@ -1654,42 +2288,6 @@ function copyDocument(document) {
1654
2288
  operations: { ...document.operations }
1655
2289
  };
1656
2290
  }
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
2291
  var KyselyWriteCache = class KyselyWriteCache {
1694
2292
  streams;
1695
2293
  lruTracker;
@@ -1697,6 +2295,7 @@ var KyselyWriteCache = class KyselyWriteCache {
1697
2295
  operationStore;
1698
2296
  registry;
1699
2297
  config;
2298
+ reads = /* @__PURE__ */ new Map();
1700
2299
  constructor(keyframeStore, operationStore, registry, config) {
1701
2300
  this.keyframeStore = keyframeStore;
1702
2301
  this.operationStore = operationStore;
@@ -1713,6 +2312,7 @@ var KyselyWriteCache = class KyselyWriteCache {
1713
2312
  const scoped = new KyselyWriteCache(keyframeStore, operationStore, this.registry, this.config);
1714
2313
  scoped.streams = this.streams;
1715
2314
  scoped.lruTracker = this.lruTracker;
2315
+ scoped.reads = this.reads;
1716
2316
  return scoped;
1717
2317
  }
1718
2318
  /**
@@ -1746,11 +2346,27 @@ var KyselyWriteCache = class KyselyWriteCache {
1746
2346
  * @returns The document at the target revision
1747
2347
  * @throws {Error} "Operation aborted" if signal is aborted
1748
2348
  * @throws {ModuleNotFoundError} If document type not registered in registry
2349
+ * @throws {DocumentPurgedError} If the stream's only row is a purge marker
1749
2350
  * @throws {Error} "Failed to rebuild document" if operation store fails
1750
2351
  * @throws {Error} If reducer throws during operation application
1751
2352
  * @throws {Error} If document serialization fails
1752
2353
  */
1753
2354
  async getState(documentId, scope, branch, targetRevision, signal) {
2355
+ const read = { evicted: false };
2356
+ let pending = this.reads.get(documentId);
2357
+ if (!pending) {
2358
+ pending = /* @__PURE__ */ new Set();
2359
+ this.reads.set(documentId, pending);
2360
+ }
2361
+ pending.add(read);
2362
+ try {
2363
+ return await this.readState(documentId, scope, branch, targetRevision, signal, read);
2364
+ } finally {
2365
+ pending.delete(read);
2366
+ if (pending.size === 0) this.reads.delete(documentId);
2367
+ }
2368
+ }
2369
+ async readState(documentId, scope, branch, targetRevision, signal, read) {
1754
2370
  if (signal?.aborted) throw new Error("Operation aborted");
1755
2371
  const streamKey = this.makeStreamKey(documentId, scope, branch);
1756
2372
  const stream = this.streams.get(streamKey);
@@ -1758,13 +2374,13 @@ var KyselyWriteCache = class KyselyWriteCache {
1758
2374
  const snapshots = stream.ringBuffer.getAll();
1759
2375
  if (targetRevision === void 0) {
1760
2376
  const newest = highestRevision(snapshots);
1761
- if (newest?.position === SnapshotPosition.Head) {
2377
+ if (newest?.position === "head") {
1762
2378
  this.lruTracker.touch(streamKey);
1763
2379
  return copyDocument(newest.document);
1764
2380
  }
1765
2381
  if (newest) {
1766
2382
  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);
2383
+ this.storeUnlessEvicted(read, documentId, scope, branch, (document.header.revision[scope] ?? 0) - 1, document, "head");
1768
2384
  this.lruTracker.touch(streamKey);
1769
2385
  return document;
1770
2386
  }
@@ -1777,7 +2393,7 @@ var KyselyWriteCache = class KyselyWriteCache {
1777
2393
  const newestOlder = this.findNearestOlderSnapshot(snapshots, targetRevision);
1778
2394
  if (newestOlder) {
1779
2395
  const document = await this.warmMissRebuild(newestOlder.document, newestOlder.revision, documentId, scope, branch, targetRevision, signal);
1780
- this.store(documentId, scope, branch, targetRevision, document, SnapshotPosition.Historical);
2396
+ this.storeUnlessEvicted(read, documentId, scope, branch, targetRevision, document, "historical");
1781
2397
  this.lruTracker.touch(streamKey);
1782
2398
  return document;
1783
2399
  }
@@ -1785,7 +2401,7 @@ var KyselyWriteCache = class KyselyWriteCache {
1785
2401
  }
1786
2402
  const document = await this.coldMissRebuild(documentId, scope, branch, targetRevision, signal);
1787
2403
  const revision = targetRevision ?? (document.header.revision[scope] ?? 0) - 1;
1788
- this.store(documentId, scope, branch, revision, document, targetRevision === void 0 ? SnapshotPosition.Head : SnapshotPosition.Historical);
2404
+ this.storeUnlessEvicted(read, documentId, scope, branch, revision, document, targetRevision === void 0 ? "head" : "historical");
1789
2405
  return document;
1790
2406
  }
1791
2407
  /**
@@ -1820,7 +2436,10 @@ var KyselyWriteCache = class KyselyWriteCache {
1820
2436
  if (run.length === 0) return;
1821
2437
  for (const entry of run.slice(0, -1)) this.persistKeyframe(documentId, scope, branch, entry.revision, entry.document);
1822
2438
  const head = run[run.length - 1];
1823
- this.store(documentId, scope, branch, head.revision, head.document, SnapshotPosition.Head);
2439
+ this.store(documentId, scope, branch, head.revision, head.document, "head");
2440
+ }
2441
+ storeUnlessEvicted(read, documentId, scope, branch, revision, document, position) {
2442
+ if (!read.evicted) this.store(documentId, scope, branch, revision, document, position);
1824
2443
  }
1825
2444
  store(documentId, scope, branch, revision, document, position) {
1826
2445
  const streamKey = this.makeStreamKey(documentId, scope, branch);
@@ -1862,6 +2481,7 @@ var KyselyWriteCache = class KyselyWriteCache {
1862
2481
  * @returns The number of streams evicted
1863
2482
  */
1864
2483
  invalidate(documentId, scope, branch) {
2484
+ for (const read of this.reads.get(documentId) ?? []) read.evicted = true;
1865
2485
  let evicted = 0;
1866
2486
  if (scope === void 0 && branch === void 0) {
1867
2487
  for (const [key] of this.streams.entries()) if (key.startsWith(`${documentId}:`)) {
@@ -1890,6 +2510,7 @@ var KyselyWriteCache = class KyselyWriteCache {
1890
2510
  * Resets LRU tracking state. This operation always succeeds.
1891
2511
  */
1892
2512
  clear() {
2513
+ for (const pending of this.reads.values()) for (const read of pending) read.evicted = true;
1893
2514
  this.streams.clear();
1894
2515
  this.lruTracker.clear();
1895
2516
  }
@@ -1924,9 +2545,9 @@ var KyselyWriteCache = class KyselyWriteCache {
1924
2545
  * the last replayed operation are applied at the end. Creation-time 0->N seed
1925
2546
  * upgrades carry the initial state, so they still apply immediately.
1926
2547
  */
1927
- async coldMissRebuild(documentId, scope, branch, targetRevision, signal) {
2548
+ async coldMissRebuild(documentId, scope, branch, targetRevision, signal, fromKeyframe = true) {
1928
2549
  const effectiveTargetRevision = targetRevision || Number.MAX_SAFE_INTEGER;
1929
- const keyframe = await this.findNearestKeyframe(documentId, scope, branch, effectiveTargetRevision, signal);
2550
+ const keyframe = fromKeyframe ? await this.findNearestKeyframe(documentId, scope, branch, effectiveTargetRevision, signal) : void 0;
1930
2551
  const documentScopeBound = scope === "document" ? targetRevision : void 0;
1931
2552
  let document;
1932
2553
  let startRevision;
@@ -1969,7 +2590,7 @@ var KyselyWriteCache = class KyselyWriteCache {
1969
2590
  subsequentDeletes: []
1970
2591
  });
1971
2592
  }
1972
- } else if (operation.action.type === "DELETE_DOCUMENT") {
2593
+ } else if (operation.action.type === "DELETE_DOCUMENT" || operation.action.type === PURGE_DOCUMENT) {
1973
2594
  applyDeleteDocumentAction(document, operation.action);
1974
2595
  for (const pending of pendingUpgrades) pending.subsequentDeletes.push(operation.action);
1975
2596
  }
@@ -1982,6 +2603,7 @@ var KyselyWriteCache = class KyselyWriteCache {
1982
2603
  }, signal);
1983
2604
  if (createOpResult.results.length === 0) throw new DocumentNotFoundError(documentId);
1984
2605
  const createOp = createOpResult.results[0];
2606
+ if (isPurgeMarker(createOp)) throw new DocumentPurgedError(documentId);
1985
2607
  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
2608
  const documentCreateAction = createOp.action;
1987
2609
  documentType = documentCreateAction.input.model;
@@ -2021,15 +2643,15 @@ var KyselyWriteCache = class KyselyWriteCache {
2021
2643
  });
2022
2644
  } else document = applyUpgradeDocumentAction(document, upgradeAction, void 0);
2023
2645
  docModule = this.registry.getModule(documentType, normalizeDocumentModelVersion(toVersion));
2024
- } else if (operation.action.type === "DELETE_DOCUMENT") {
2646
+ } else if (operation.action.type === "DELETE_DOCUMENT" || operation.action.type === PURGE_DOCUMENT) {
2025
2647
  applyDeleteDocumentAction(document, operation.action);
2026
2648
  for (const pending of pendingUpgrades) pending.subsequentDeletes.push(operation.action);
2027
2649
  } else {
2028
2650
  const protocolVersion = baseReducerVersion(document.header);
2029
- document = docModule.reducer(document, operation.action, void 0, {
2651
+ document = withProtocolVersions(docModule.reducer(document, operation.action, void 0, {
2030
2652
  skip: operation.skip,
2031
2653
  protocolVersion
2032
- });
2654
+ }), document.header.protocolVersions);
2033
2655
  }
2034
2656
  }
2035
2657
  }
@@ -2063,6 +2685,7 @@ var KyselyWriteCache = class KyselyWriteCache {
2063
2685
  let cursor = void 0;
2064
2686
  const pageSize = 100;
2065
2687
  let hasMorePages;
2688
+ let needsFullReplay = false;
2066
2689
  do {
2067
2690
  if (signal?.aborted) throw new Error("Operation aborted");
2068
2691
  const paging = {
@@ -2073,26 +2696,31 @@ var KyselyWriteCache = class KyselyWriteCache {
2073
2696
  const result = await this.operationStore.getSince(documentId, scope, branch, startRevision, void 0, paging, signal);
2074
2697
  for (const operation of result.results) {
2075
2698
  if (targetRevision !== void 0 && operation.index > targetRevision) break;
2699
+ if (keyframe && operation.skip > 0) {
2700
+ needsFullReplay = true;
2701
+ break;
2702
+ }
2076
2703
  const moduleVersion = this.resolveModuleVersionForOp(operation.index, operation.timestampUtcMs, scope, validatedUpgrades, finalVersion);
2077
2704
  document = this.applyPendingUpgrades(document, pendingUpgrades, moduleVersion ?? Number.MAX_SAFE_INTEGER);
2078
2705
  if (isDenied(operation)) document = appendWithoutApplying(document, operation, scope);
2079
2706
  else {
2080
2707
  const protocolVersion = baseReducerVersion(document.header);
2081
- document = getModuleCached(moduleVersion).reducer(document, operation.action, void 0, {
2708
+ document = withProtocolVersions(getModuleCached(moduleVersion).reducer(document, operation.action, void 0, {
2082
2709
  skip: operation.skip,
2083
2710
  protocolVersion,
2084
2711
  replayOptions: { operation },
2085
2712
  skipIndexValidation: true
2086
- });
2713
+ }), document.header.protocolVersions);
2087
2714
  }
2088
2715
  }
2089
2716
  const reachedTarget = targetRevision !== void 0 && result.results.some((op) => op.index >= targetRevision);
2090
- hasMorePages = Boolean(result.nextCursor) && !reachedTarget;
2717
+ hasMorePages = Boolean(result.nextCursor) && !reachedTarget && !needsFullReplay;
2091
2718
  if (hasMorePages) cursor = result.nextCursor;
2092
2719
  } catch (err) {
2093
2720
  throw new Error(`Failed to rebuild document ${documentId}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
2094
2721
  }
2095
2722
  } while (hasMorePages);
2723
+ if (needsFullReplay) return this.coldMissRebuild(documentId, scope, branch, targetRevision, signal, false);
2096
2724
  document = this.applyTailPendingUpgrades(document, pendingUpgrades, scope, targetRevision);
2097
2725
  document = await this.stampRevisions(document, documentId, scope, branch, targetRevision, signal);
2098
2726
  if (pendingUpgrades.length > 0) {
@@ -2199,26 +2827,32 @@ var KyselyWriteCache = class KyselyWriteCache {
2199
2827
  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
2828
  const module = this.registry.getModule(documentType, extractModuleVersion(baseDocument));
2201
2829
  let document = copyDocument(baseDocument);
2830
+ let needsFullReplay = false;
2202
2831
  try {
2203
2832
  const pagedResults = await this.operationStore.getSince(documentId, scope, branch, baseRevision, void 0, void 0, signal);
2204
2833
  for (const operation of pagedResults.results) {
2205
2834
  if (signal?.aborted) throw new Error("Operation aborted");
2206
2835
  if (targetRevision !== void 0 && operation.index > targetRevision) break;
2836
+ if (operation.skip > 0) {
2837
+ needsFullReplay = true;
2838
+ break;
2839
+ }
2207
2840
  if (isDenied(operation)) document = appendWithoutApplying(document, operation, scope);
2208
2841
  else {
2209
2842
  const protocolVersion = baseReducerVersion(document.header);
2210
- document = module.reducer(document, operation.action, void 0, {
2843
+ document = withProtocolVersions(module.reducer(document, operation.action, void 0, {
2211
2844
  skip: operation.skip,
2212
2845
  protocolVersion,
2213
2846
  replayOptions: { operation },
2214
2847
  skipIndexValidation: true
2215
- });
2848
+ }), document.header.protocolVersions);
2216
2849
  }
2217
2850
  if (targetRevision !== void 0 && operation.index === targetRevision) break;
2218
2851
  }
2219
2852
  } catch (err) {
2220
2853
  throw new Error(`Failed to rebuild document ${documentId}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
2221
2854
  }
2855
+ if (needsFullReplay) return this.coldMissRebuild(documentId, scope, branch, targetRevision, signal, false);
2222
2856
  const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
2223
2857
  document.header.revision = revisions.revision;
2224
2858
  if (targetRevision !== void 0) document.header.revision = {
@@ -2337,13 +2971,165 @@ function validateFeatureFlags(flags, prerequisites) {
2337
2971
  }
2338
2972
  }
2339
2973
  //#endregion
2340
- //#region src/executor/execution-scope.ts
2341
- var DefaultExecutionScope = class {
2342
- constructor(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache) {
2343
- this.operationStore = operationStore;
2344
- this.operationIndex = operationIndex;
2345
- this.writeCache = writeCache;
2346
- this.documentMetaCache = documentMetaCache;
2974
+ //#region src/storage/kysely/document-purger.ts
2975
+ /** An `Operation` row whose DELETE_DOCUMENT applied: not errored, not denied. */
2976
+ const appliedDelete = sql`action->>'type' = 'DELETE_DOCUMENT' and coalesce(error, '') = '' and coalesce("deniedReason", '') = ''`;
2977
+ /** Rows removed per statement from the tables a document can fill. */
2978
+ const DEFAULT_PURGE_DELETE_BATCH = 5e3;
2979
+ /** A purge transaction's statements; bound to the job's transaction. */
2980
+ var KyselyDocumentPurger = class {
2981
+ batchSize;
2982
+ db;
2983
+ constructor(db, batchSize = DEFAULT_PURGE_DELETE_BATCH) {
2984
+ this.batchSize = batchSize;
2985
+ this.db = db;
2986
+ }
2987
+ /** Every stream the document holds operations in. */
2988
+ async streams(documentId) {
2989
+ return this.db.selectFrom("Operation").select(["scope", "branch"]).distinct().where("documentId", "=", documentId).orderBy("scope").orderBy("branch").execute();
2990
+ }
2991
+ async operationCount(documentId) {
2992
+ const row = await this.db.selectFrom("operation_index_operations").select((eb) => eb.fn.countAll().as("count")).where("documentId", "=", documentId).executeTakeFirst();
2993
+ return Number(row?.count ?? 0);
2994
+ }
2995
+ /** Untombstoned documents group_references records as naming the group. */
2996
+ async groupReferencers(groupId) {
2997
+ const ids = (await this.db.selectFrom("group_references").select("documentId").where("groupId", "=", groupId).where("documentId", "<>", groupId).orderBy("documentId").execute()).map((row) => row.documentId);
2998
+ const purged = await findPurged(this.db, ids);
2999
+ return ids.filter((id) => !purged.has(id));
3000
+ }
3001
+ /** Survivors whose accepted auth history names the group; refusals do not. */
3002
+ async groupReferencersInHistory(groupId) {
3003
+ const referencing = [];
3004
+ 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);
3005
+ return referencing;
3006
+ }
3007
+ /** The document's auth-scope operations, per branch, in index order. */
3008
+ async authOperations(documentId) {
3009
+ const rows = await this.db.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", "auth").orderBy("branch").orderBy("index").execute();
3010
+ const byBranch = /* @__PURE__ */ new Map();
3011
+ for (const row of rows) {
3012
+ const operations = byBranch.get(row.branch) ?? [];
3013
+ operations.push({
3014
+ index: row.index,
3015
+ timestampUtcMs: row.timestampUtcMs.toISOString(),
3016
+ hash: row.hash,
3017
+ skip: row.skip,
3018
+ error: row.error || void 0,
3019
+ deniedReason: row.deniedReason || void 0,
3020
+ id: row.opId,
3021
+ action: row.action
3022
+ });
3023
+ byBranch.set(row.branch, operations);
3024
+ }
3025
+ return byBranch;
3026
+ }
3027
+ /** Every document ever in the collection, but its owner and its own row. */
3028
+ async collectionMembers(collectionId, ownerId, branch) {
3029
+ 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) => {
3030
+ const openElsewhere = Boolean(row.openElsewhere);
3031
+ return {
3032
+ documentId: row.documentId,
3033
+ openElsewhere,
3034
+ required: !openElsewhere && (Boolean(row.openHere) || Boolean(row.deleted))
3035
+ };
3036
+ });
3037
+ }
3038
+ async remoteCollection(remoteName) {
3039
+ return (await this.db.selectFrom("sync_remotes").select("collection_id").where("name", "=", remoteName).executeTakeFirst())?.collection_id;
3040
+ }
3041
+ /** Deletes every row about the document; the counts are per table. */
3042
+ async deleteRows(documentId) {
3043
+ const removed = {};
3044
+ removed.Operation = await this.deleteBatched(async () => {
3045
+ const result = await this.db.deleteFrom("Operation").where("id", "in", (eb) => eb.selectFrom("Operation").select("id").where("documentId", "=", documentId).limit(this.batchSize)).executeTakeFirst();
3046
+ return Number(result.numDeletedRows);
3047
+ });
3048
+ removed.operation_index_operations = await this.deleteBatched(async () => {
3049
+ 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();
3050
+ return Number(result.numDeletedRows);
3051
+ });
3052
+ removed.Keyframe = await this.deleteBatched(async () => {
3053
+ const result = await this.db.deleteFrom("Keyframe").where("id", "in", (eb) => eb.selectFrom("Keyframe").select("id").where("documentId", "=", documentId).limit(this.batchSize)).executeTakeFirst();
3054
+ return Number(result.numDeletedRows);
3055
+ });
3056
+ removed.DocumentSnapshot = await this.deleteBatched(async () => {
3057
+ const result = await this.db.deleteFrom("DocumentSnapshot").where("id", "in", (eb) => eb.selectFrom("DocumentSnapshot").select("id").where("documentId", "=", documentId).limit(this.batchSize)).executeTakeFirst();
3058
+ return Number(result.numDeletedRows);
3059
+ });
3060
+ removed.SlugMapping = await this.deleteWhere("SlugMapping", "documentId", documentId);
3061
+ 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();
3062
+ removed.DocumentRelationship = Number(relationships?.count ?? 0);
3063
+ removed.Document = await this.deleteWhere("Document", "id", documentId);
3064
+ removed.group_references = await this.deleteWhere("group_references", "documentId", documentId);
3065
+ removed.sync_dead_letters = await this.deleteWhere("sync_dead_letters", "document_id", documentId);
3066
+ removed.sync_holds = await this.deleteWhere("sync_holds", "document_id", documentId);
3067
+ return removed;
3068
+ }
3069
+ async writeTombstone(tombstone) {
3070
+ const values = {
3071
+ documentId: tombstone.documentId,
3072
+ ordinal: tombstone.ordinal,
3073
+ removedRows: JSON.stringify(tombstone.removedRows),
3074
+ purgedAtUtc: tombstone.purgedAtUtc,
3075
+ requestId: tombstone.requestId
3076
+ };
3077
+ await this.db.insertInto("document_purges").values(values).onConflict((oc) => oc.column("documentId").doUpdateSet({
3078
+ ordinal: values.ordinal,
3079
+ removedRows: values.removedRows,
3080
+ purgedAtUtc: values.purgedAtUtc,
3081
+ requestId: values.requestId
3082
+ })).execute();
3083
+ }
3084
+ /** Reopens every membership at the marker ordinal, plus `collectionIds`. */
3085
+ async reopenMemberships(documentId, ordinal, collectionIds = []) {
3086
+ await this.db.updateTable("document_collections").set({
3087
+ joinedOrdinal: BigInt(ordinal),
3088
+ leftOrdinal: null
3089
+ }).where("documentId", "=", documentId).execute();
3090
+ for (const collectionId of new Set(collectionIds)) await this.db.insertInto("document_collections").values({
3091
+ documentId,
3092
+ collectionId,
3093
+ joinedOrdinal: BigInt(ordinal),
3094
+ leftOrdinal: null
3095
+ }).onConflict((oc) => oc.columns(["documentId", "collectionId"]).doUpdateSet({
3096
+ joinedOrdinal: BigInt(ordinal),
3097
+ leftOrdinal: null
3098
+ })).execute();
3099
+ return (await this.db.selectFrom("document_collections").select("collectionId").where("documentId", "=", documentId).orderBy("collectionId").execute()).map((row) => row.collectionId);
3100
+ }
3101
+ async deleteBatched(step) {
3102
+ let total = 0;
3103
+ for (;;) {
3104
+ const deleted = await step();
3105
+ total += deleted;
3106
+ if (deleted < this.batchSize) return total;
3107
+ }
3108
+ }
3109
+ async deleteWhere(table, column, documentId) {
3110
+ const result = await this.db.deleteFrom(table).where(column, "=", documentId).executeTakeFirst();
3111
+ return Number(result.numDeletedRows);
3112
+ }
3113
+ };
3114
+ //#endregion
3115
+ //#region src/executor/execution-scope.ts
3116
+ /** For scopes without a transaction to hold a lock in. */
3117
+ const NOOP_DOCUMENT_LOCKS = {
3118
+ shared: () => Promise.resolve(),
3119
+ exclusive: () => Promise.resolve(),
3120
+ purged: () => Promise.resolve(/* @__PURE__ */ new Set())
3121
+ };
3122
+ var DefaultExecutionScope = class {
3123
+ operationStore;
3124
+ operationIndex;
3125
+ writeCache;
3126
+ documentMetaCache;
3127
+ collectionMembershipCache;
3128
+ constructor(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache) {
3129
+ this.operationStore = operationStore;
3130
+ this.operationIndex = operationIndex;
3131
+ this.writeCache = writeCache;
3132
+ this.documentMetaCache = documentMetaCache;
2347
3133
  this.collectionMembershipCache = collectionMembershipCache;
2348
3134
  }
2349
3135
  async run(fn, signal) {
@@ -2353,11 +3139,19 @@ var DefaultExecutionScope = class {
2353
3139
  operationIndex: this.operationIndex,
2354
3140
  writeCache: this.writeCache,
2355
3141
  documentMetaCache: this.documentMetaCache,
2356
- collectionMembershipCache: this.collectionMembershipCache
3142
+ collectionMembershipCache: this.collectionMembershipCache,
3143
+ documentLocks: NOOP_DOCUMENT_LOCKS
2357
3144
  });
2358
3145
  }
2359
3146
  };
2360
3147
  var KyselyExecutionScope = class {
3148
+ db;
3149
+ operationStore;
3150
+ operationIndex;
3151
+ keyframeStore;
3152
+ writeCache;
3153
+ documentMetaCache;
3154
+ collectionMembershipCache;
2361
3155
  constructor(db, operationStore, operationIndex, keyframeStore, writeCache, documentMetaCache, collectionMembershipCache) {
2362
3156
  this.db = db;
2363
3157
  this.operationStore = operationStore;
@@ -2370,28 +3164,46 @@ var KyselyExecutionScope = class {
2370
3164
  async run(fn, signal) {
2371
3165
  signal?.throwIfAborted();
2372
3166
  return this.db.transaction().execute(async (trx) => {
2373
- const scopedOperationStore = this.operationStore.withTransaction(trx);
2374
- const scopedOperationIndex = this.operationIndex.withTransaction(trx);
3167
+ const sharedLocked = /* @__PURE__ */ new Set();
3168
+ const liveIds = /* @__PURE__ */ new Set();
3169
+ const scopedOperationStore = this.operationStore.withTransaction(trx, liveIds);
3170
+ const scopedOperationIndex = this.operationIndex.withTransaction(trx, liveIds);
2375
3171
  const scopedKeyframeStore = this.keyframeStore.withTransaction(trx);
2376
3172
  return fn({
2377
3173
  operationStore: scopedOperationStore,
2378
3174
  operationIndex: scopedOperationIndex,
2379
3175
  writeCache: this.writeCache.withScopedStores(scopedOperationStore, scopedKeyframeStore),
2380
3176
  documentMetaCache: this.documentMetaCache.withScopedStore(scopedOperationStore),
2381
- collectionMembershipCache: this.collectionMembershipCache.withScopedIndex(scopedOperationIndex)
3177
+ collectionMembershipCache: this.collectionMembershipCache.withScopedIndex(scopedOperationIndex),
3178
+ documentLocks: {
3179
+ shared: async (ids) => {
3180
+ const list = [...ids];
3181
+ await acquirePurgeLocks(trx, list, "shared");
3182
+ for (const id of list) sharedLocked.add(id);
3183
+ },
3184
+ exclusive: (id) => acquirePurgeLocks(trx, [id], "exclusive"),
3185
+ purged: async (ids) => {
3186
+ const list = [...ids];
3187
+ const found = await findPurged(trx, list);
3188
+ for (const id of list) if (sharedLocked.has(id) && !found.has(id)) liveIds.add(id);
3189
+ return found;
3190
+ }
3191
+ },
3192
+ purger: new KyselyDocumentPurger(trx)
2382
3193
  });
2383
3194
  });
2384
3195
  }
2385
3196
  };
2386
3197
  //#endregion
2387
3198
  //#region src/utils/reshuffle.ts
2388
- const STRICT_ORDER_ACTION_TYPES = new Set([
3199
+ const STRICT_ORDER_ACTION_TYPES = /* @__PURE__ */ new Set([
2389
3200
  "CREATE_DOCUMENT",
2390
3201
  "DELETE_DOCUMENT",
2391
3202
  "UPGRADE_DOCUMENT",
2392
3203
  "ADD_RELATIONSHIP",
2393
3204
  "REMOVE_RELATIONSHIP",
2394
3205
  "UPDATE_RELATIONSHIP",
3206
+ "PURGE_DOCUMENT",
2395
3207
  "ADD_FOLDER",
2396
3208
  "UPDATE_FOLDER",
2397
3209
  "REMOVE_FOLDER"
@@ -2597,6 +3409,15 @@ async function evaluateByPosition(model, target, subject, stores, signal) {
2597
3409
  const derivedSet = derivedReadSet(definition);
2598
3410
  if (!definition.evaluatesScope(scope)) return operations.map(() => void 0);
2599
3411
  const evaluating = new Set(operations.map((operation) => operation.id));
3412
+ const creation = operations.find((operation) => operation.action.type === "CREATE_DOCUMENT" && operation.action.input.documentId === target.documentId);
3413
+ async function stateBefore(query) {
3414
+ try {
3415
+ return await writeCache.getState(query.documentId, query.scope, query.branch, -1, signal);
3416
+ } catch (error) {
3417
+ if (creation !== void 0 && query.documentId === target.documentId && error instanceof DocumentNotFoundError && !(error instanceof DocumentPurgedError)) return createDocumentFromAction(creation.action);
3418
+ throw error;
3419
+ }
3420
+ }
2600
3421
  const readStreams = await Promise.all(readSet.map(async (stream) => ({
2601
3422
  stream,
2602
3423
  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 +3430,7 @@ async function evaluateByPosition(model, target, subject, stores, signal) {
2609
3430
  const histories = [];
2610
3431
  for (const read of readStreams) {
2611
3432
  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);
3433
+ const before = await stateBefore(read.stream.query);
2613
3434
  walked.push({
2614
3435
  streamKey: streamKey(read.stream.query),
2615
3436
  scope: read.stream.query.scope,
@@ -2631,7 +3452,7 @@ async function evaluateByPosition(model, target, subject, stores, signal) {
2631
3452
  branch: target.branch
2632
3453
  };
2633
3454
  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);
3455
+ const before = await stateBefore(query);
2635
3456
  evaluatedStateKey = streamKey(query);
2636
3457
  walked.push({
2637
3458
  streamKey: evaluatedStateKey,
@@ -2702,58 +3523,36 @@ async function evaluateByPosition(model, target, subject, stores, signal) {
2702
3523
  return operations.map((operation) => reasons.get(operation.id));
2703
3524
  }
2704
3525
  //#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
3526
  //#region src/executor/document-action-handler.ts
2750
3527
  var DocumentActionHandler = class {
2751
- constructor(registry, logger, driveContainerTypes, featureFlags, decisionModel) {
3528
+ registry;
3529
+ logger;
3530
+ driveContainerTypes;
3531
+ featureFlags;
3532
+ decisionModel;
3533
+ protocolSupport;
3534
+ loggedUnregisteredProtocols = /* @__PURE__ */ new Set();
3535
+ constructor(registry, logger, driveContainerTypes, featureFlags, decisionModel, protocolSupport) {
2752
3536
  this.registry = registry;
2753
3537
  this.logger = logger;
2754
3538
  this.driveContainerTypes = driveContainerTypes;
2755
3539
  this.featureFlags = featureFlags;
2756
3540
  this.decisionModel = decisionModel;
3541
+ this.protocolSupport = protocolSupport;
3542
+ }
3543
+ /** Keys this reactor does not register are admitted: it cannot judge them. */
3544
+ unsupportedProtocol(documentId, protocolVersions) {
3545
+ for (const [protocol, version] of Object.entries(protocolVersions ?? {})) {
3546
+ const supported = this.protocolSupport[protocol];
3547
+ if (supported === void 0) {
3548
+ if (!this.loggedUnregisteredProtocols.has(protocol)) {
3549
+ this.loggedUnregisteredProtocols.add(protocol);
3550
+ this.logger.info("Admitting document @documentId with unregistered protocol @protocol @version", documentId, protocol, version);
3551
+ }
3552
+ continue;
3553
+ }
3554
+ if (!supported.includes(version)) return new UnsupportedProtocolVersionError(documentId, protocol, version);
3555
+ }
2757
3556
  }
2758
3557
  /** Whether the write arrives with its evaluation already decided. */
2759
3558
  alreadyEvaluated(executing) {
@@ -2771,6 +3570,7 @@ var DocumentActionHandler = class {
2771
3570
  case "ADD_RELATIONSHIP": return this.executeAddRelationship(write, executing);
2772
3571
  case "REMOVE_RELATIONSHIP": return this.executeRemoveRelationship(write, executing);
2773
3572
  case "UPDATE_RELATIONSHIP": return this.executeUpdateRelationship(write, executing);
3573
+ case "PURGE_DOCUMENT": return buildErrorResult(executing.job, new ReservedActionError(executing.job.documentId, action.type), executing.startTime);
2774
3574
  default: return buildErrorResult(executing.job, /* @__PURE__ */ new Error(`Unknown document action type: ${action.type}`), executing.startTime);
2775
3575
  }
2776
3576
  }
@@ -2840,7 +3640,7 @@ var DocumentActionHandler = class {
2840
3640
  [job.scope]: [...standing.operations[job.scope] ?? [], operation]
2841
3641
  };
2842
3642
  executing.touchedStreams.add(job.documentId, job.scope, job.branch);
2843
- stores.writeCache.putState(job.documentId, job.scope, job.branch, operation.index, standing, SnapshotPosition.Head);
3643
+ stores.writeCache.putState(job.documentId, job.scope, job.branch, operation.index, standing, "head");
2844
3644
  indexTxn.write([{
2845
3645
  ...operation,
2846
3646
  documentId: job.documentId,
@@ -2852,6 +3652,7 @@ var DocumentActionHandler = class {
2852
3652
  stores.documentMetaCache.putDocumentMeta(job.documentId, job.branch, {
2853
3653
  state: standing.state.document,
2854
3654
  documentType: standing.header.documentType,
3655
+ protocolVersions: standing.header.protocolVersions,
2855
3656
  documentScopeRevision: operation.index + 1
2856
3657
  });
2857
3658
  return buildSuccessResult(job, operation, job.documentId, standing.header.documentType, JSON.stringify({
@@ -2868,6 +3669,9 @@ var DocumentActionHandler = class {
2868
3669
  error: /* @__PURE__ */ new Error(`CREATE_DOCUMENT must be in "document" scope, got "${job.scope}"`),
2869
3670
  duration: Date.now() - startTime
2870
3671
  };
3672
+ const input = action.input;
3673
+ const unsupported = this.unsupportedProtocol(input.documentId, input.protocolVersions);
3674
+ if (unsupported) return buildErrorResult(job, unsupported, startTime);
2871
3675
  const document = createDocumentFromAction(action);
2872
3676
  let operation = createOperation(action, 0, skip, {
2873
3677
  documentId: document.header.id,
@@ -2875,8 +3679,8 @@ var DocumentActionHandler = class {
2875
3679
  branch: job.branch
2876
3680
  });
2877
3681
  const resultingStateObj = {
2878
- header: document.header,
2879
- ...document.state
3682
+ ...document.state,
3683
+ header: document.header
2880
3684
  };
2881
3685
  const resultingState = JSON.stringify(resultingStateObj);
2882
3686
  const writeResult = await this.writeOperationToStore({
@@ -2893,7 +3697,7 @@ var DocumentActionHandler = class {
2893
3697
  [job.scope]: [...document.operations[job.scope] ?? [], operation]
2894
3698
  };
2895
3699
  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);
3700
+ stores.writeCache.putState(document.header.id, job.scope, job.branch, operation.index, document, "head");
2897
3701
  indexTxn.write([{
2898
3702
  ...operation,
2899
3703
  documentId: document.header.id,
@@ -2910,6 +3714,7 @@ var DocumentActionHandler = class {
2910
3714
  stores.documentMetaCache.putDocumentMeta(document.header.id, job.branch, {
2911
3715
  state: document.state.document,
2912
3716
  documentType: document.header.documentType,
3717
+ protocolVersions: document.header.protocolVersions,
2913
3718
  documentScopeRevision: 1
2914
3719
  });
2915
3720
  return buildSuccessResult(job, operation, document.header.id, document.header.documentType, resultingState, startTime);
@@ -2953,7 +3758,7 @@ var DocumentActionHandler = class {
2953
3758
  [job.scope]: [...document.operations[job.scope] ?? [], operation]
2954
3759
  };
2955
3760
  executing.touchedStreams.add(documentId, job.scope, job.branch);
2956
- stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document, SnapshotPosition.Head);
3761
+ stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document, "head");
2957
3762
  indexTxn.write([{
2958
3763
  ...operation,
2959
3764
  documentId,
@@ -2965,6 +3770,7 @@ var DocumentActionHandler = class {
2965
3770
  stores.documentMetaCache.putDocumentMeta(documentId, job.branch, {
2966
3771
  state: document.state.document,
2967
3772
  documentType: document.header.documentType,
3773
+ protocolVersions: document.header.protocolVersions,
2968
3774
  documentScopeRevision: operation.index + 1
2969
3775
  });
2970
3776
  return buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
@@ -3003,7 +3809,7 @@ var DocumentActionHandler = class {
3003
3809
  } catch (error) {
3004
3810
  return buildErrorResult(job, /* @__PURE__ */ new Error(`Failed to fetch revisions for upgrade: ${error instanceof Error ? error.message : String(error)}`), startTime);
3005
3811
  }
3006
- const revisionScopes = new Set([...Object.keys(input.revision), ...Object.keys(actualRevisions)]);
3812
+ const revisionScopes = /* @__PURE__ */ new Set([...Object.keys(input.revision), ...Object.keys(actualRevisions)]);
3007
3813
  for (const revisionScope of revisionScopes) {
3008
3814
  const snapshot = input.revision[revisionScope] ?? 0;
3009
3815
  const actual = actualRevisions[revisionScope] ?? 0;
@@ -3045,8 +3851,8 @@ var DocumentActionHandler = class {
3045
3851
  branch: job.branch
3046
3852
  });
3047
3853
  const resultingStateObj = {
3048
- header: document.header,
3049
- ...document.state
3854
+ ...document.state,
3855
+ header: document.header
3050
3856
  };
3051
3857
  if (fromVersion > 0) resultingStateObj.__migrated = true;
3052
3858
  const resultingState = JSON.stringify(resultingStateObj);
@@ -3064,7 +3870,7 @@ var DocumentActionHandler = class {
3064
3870
  [job.scope]: [...document.operations[job.scope] ?? [], operation]
3065
3871
  };
3066
3872
  executing.touchedStreams.add(documentId, job.scope, job.branch);
3067
- stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document, SnapshotPosition.Head);
3873
+ stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document, "head");
3068
3874
  for (const scope of otherScopes) executing.postCommitInvalidations.push({
3069
3875
  documentId,
3070
3876
  scope,
@@ -3081,22 +3887,41 @@ var DocumentActionHandler = class {
3081
3887
  stores.documentMetaCache.putDocumentMeta(documentId, job.branch, {
3082
3888
  state: document.state.document,
3083
3889
  documentType: document.header.documentType,
3890
+ protocolVersions: document.header.protocolVersions,
3084
3891
  documentScopeRevision: operation.index + 1
3085
3892
  });
3086
3893
  return buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
3087
3894
  }
3088
- executeAddRelationship(write, executing) {
3895
+ /** A submitted write naming a purged target was refused at job start. */
3896
+ async isTargetPurged(write, executing) {
3897
+ const target = relationshipTarget(write.action);
3898
+ if (target === void 0 || !executing.purgeFence) return false;
3899
+ return executing.purgeFence.isPurged(target);
3900
+ }
3901
+ async executeAddRelationship(write, executing) {
3902
+ let targetPurged;
3903
+ try {
3904
+ targetPurged = await this.isTargetPurged(write, executing);
3905
+ } catch (error) {
3906
+ return buildErrorResult(executing.job, error instanceof Error ? error : new Error(String(error)), executing.startTime);
3907
+ }
3089
3908
  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)) {
3909
+ if (!targetPurged && this.driveContainerTypes.has(sourceDoc.header.documentType)) {
3091
3910
  const collectionId = DriveCollectionId.forDrive(input.sourceId, j.branch).key;
3092
3911
  txn.addToCollection(collectionId, input.targetId);
3093
3912
  s.collectionMembershipCache.invalidate(input.targetId);
3094
3913
  }
3095
3914
  });
3096
3915
  }
3097
- executeRemoveRelationship(write, executing) {
3916
+ async executeRemoveRelationship(write, executing) {
3917
+ let targetPurged;
3918
+ try {
3919
+ targetPurged = await this.isTargetPurged(write, executing);
3920
+ } catch (error) {
3921
+ return buildErrorResult(executing.job, error instanceof Error ? error : new Error(String(error)), executing.startTime);
3922
+ }
3098
3923
  return this.withRelationshipAction("REMOVE_RELATIONSHIP", write, executing, null, ({ indexTxn: txn, stores: s, sourceDoc, input, job: j }) => {
3099
- if (this.driveContainerTypes.has(sourceDoc.header.documentType)) {
3924
+ if (!targetPurged && this.driveContainerTypes.has(sourceDoc.header.documentType)) {
3100
3925
  const collectionId = DriveCollectionId.forDrive(input.sourceId, j.branch).key;
3101
3926
  txn.removeFromCollection(collectionId, input.targetId);
3102
3927
  s.collectionMembershipCache.invalidate(input.targetId);
@@ -3120,6 +3945,7 @@ var DocumentActionHandler = class {
3120
3945
  try {
3121
3946
  sourceDoc = await stores.writeCache.getState(input.sourceId, "document", job.branch, void 0, signal);
3122
3947
  } catch (error) {
3948
+ if (DocumentPurgedError.isError(error)) return buildErrorResult(job, error, startTime);
3123
3949
  if (DocumentNotFoundError.isError(error)) return buildErrorResult(job, new DocumentNotFoundError(input.sourceId, `${actionTypeName}: source document ${input.sourceId} not found`), startTime);
3124
3950
  return buildErrorResult(job, /* @__PURE__ */ new Error(`${actionTypeName}: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
3125
3951
  }
@@ -3150,7 +3976,7 @@ var DocumentActionHandler = class {
3150
3976
  const resultingState = JSON.stringify(resultingStateObj);
3151
3977
  executing.touchedStreams.add(input.sourceId, job.scope, job.branch);
3152
3978
  executing.touchedStreams.add(input.targetId, job.scope, job.branch);
3153
- stores.writeCache.putState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc, SnapshotPosition.Head);
3979
+ stores.writeCache.putState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc, "head");
3154
3980
  indexTxn.write([{
3155
3981
  ...operation,
3156
3982
  documentId: input.sourceId,
@@ -3169,6 +3995,7 @@ var DocumentActionHandler = class {
3169
3995
  stores.documentMetaCache.putDocumentMeta(input.sourceId, job.branch, {
3170
3996
  state: sourceDoc.state.document,
3171
3997
  documentType: sourceDoc.header.documentType,
3998
+ protocolVersions: sourceDoc.header.protocolVersions,
3172
3999
  documentScopeRevision: operation.index + 1
3173
4000
  });
3174
4001
  return buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
@@ -3197,55 +4024,669 @@ var DocumentActionHandler = class {
3197
4024
  }
3198
4025
  };
3199
4026
  //#endregion
3200
- //#region src/executor/signature-verifier.ts
3201
- var SignatureVerifier = class {
3202
- constructor(verifier) {
3203
- this.verifier = verifier;
4027
+ //#region src/signer/passthrough-signer.ts
4028
+ /**
4029
+ * A no-op signer that returns empty values for all methods.
4030
+ * Used when signing is not required.
4031
+ */
4032
+ var PassthroughSigner = class {
4033
+ publicKey = {};
4034
+ sign() {
4035
+ return Promise.resolve(/* @__PURE__ */ new Uint8Array(0));
3204
4036
  }
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`);
4037
+ verify() {
4038
+ return Promise.resolve();
4039
+ }
4040
+ signAction() {
4041
+ return Promise.resolve([
4042
+ "",
4043
+ "",
4044
+ "",
4045
+ "",
4046
+ ""
4047
+ ]);
4048
+ }
4049
+ };
4050
+ //#endregion
4051
+ //#region src/signer/trust-policy.ts
4052
+ /**
4053
+ * The policy admission asks: the reactor's own key signing as its own user is
4054
+ * accepted, then the host's policy decides, else the default does. `own` is
4055
+ * read on every ask, since its user changes on login.
4056
+ */
4057
+ function admissionTrustPolicy(own, authEnforcement, policy) {
4058
+ return { authorizeSigner(signer, key, documentId) {
4059
+ const identity = actionSignerIdentity(own);
4060
+ if (identity.app.key !== "" && key === identity.app.key) {
4061
+ if (sameUser(actionSignerIdentity(signer).user, identity.user)) return Promise.resolve(true);
3228
4062
  }
4063
+ if (policy) return policy.authorizeSigner(signer, key, documentId);
4064
+ return Promise.resolve(!authEnforcement);
4065
+ } };
4066
+ }
4067
+ function sameUser(a, b) {
4068
+ return a.address.toLowerCase() === b.address.toLowerCase() && a.networkId === b.networkId && a.chainId === b.chainId;
4069
+ }
4070
+ //#endregion
4071
+ //#region src/signer/did-key.ts
4072
+ const DID_KEY_PREFIX = "did:key:z";
4073
+ const P256_MULTICODEC = [128, 36];
4074
+ const MAX_CACHED_KEYS = 1024;
4075
+ const P = BigInt("0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff");
4076
+ const A = BigInt("0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc");
4077
+ const B = BigInt("0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b");
4078
+ /** A did:key that is not a compressed P-256 public key. */
4079
+ var MalformedDidKeyError = class extends Error {
4080
+ constructor(did, detail) {
4081
+ super(`Malformed did:key ${did}: ${detail}`);
4082
+ this.name = "MalformedDidKeyError";
3229
4083
  }
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}`);
4084
+ };
4085
+ const keyCache = /* @__PURE__ */ new Map();
4086
+ /** Cached per process: decompressing the point costs a BigInt modPow. */
4087
+ async function importDidKey(did) {
4088
+ const cached = keyCache.get(did);
4089
+ if (cached) {
4090
+ keyCache.delete(did);
4091
+ keyCache.set(did, cached);
4092
+ return cached;
4093
+ }
4094
+ const raw = decompressP256(compressedPointFromDid(did), did);
4095
+ const key = await crypto.subtle.importKey("raw", raw.buffer, {
4096
+ name: "ECDSA",
4097
+ namedCurve: "P-256"
4098
+ }, false, ["verify"]);
4099
+ keyCache.set(did, key);
4100
+ if (keyCache.size > MAX_CACHED_KEYS) {
4101
+ const oldest = keyCache.keys().next().value;
4102
+ if (oldest !== void 0) keyCache.delete(oldest);
4103
+ }
4104
+ return key;
4105
+ }
4106
+ function compressedPointFromDid(did) {
4107
+ if (!did.startsWith(DID_KEY_PREFIX)) throw new MalformedDidKeyError(did, "not a base58btc did:key");
4108
+ const decoded = base58Decode(did.slice(9));
4109
+ if (!decoded || decoded.length !== 35) throw new MalformedDidKeyError(did, "not a compressed P-256 key");
4110
+ if (decoded[0] !== P256_MULTICODEC[0] || decoded[1] !== P256_MULTICODEC[1]) throw new MalformedDidKeyError(did, "not a P-256 multicodec");
4111
+ return decoded.subarray(2);
4112
+ }
4113
+ function decompressP256(compressed, did) {
4114
+ const prefix = compressed[0];
4115
+ if (prefix !== 2 && prefix !== 3) throw new MalformedDidKeyError(did, `compression prefix ${prefix}`);
4116
+ let x = BigInt(0);
4117
+ for (let i = 1; i < compressed.length; i++) x = x << BigInt(8) | BigInt(compressed[i]);
4118
+ if (x >= P) throw new MalformedDidKeyError(did, "x outside the field");
4119
+ const ySquared = (modPow(x, BigInt(3), P) + A * x + B) % P;
4120
+ let y = modPow(ySquared, (P + BigInt(1)) / BigInt(4), P);
4121
+ if (y * y % P !== ySquared) throw new MalformedDidKeyError(did, "x is not on the curve");
4122
+ if (y % BigInt(2) === BigInt(0) !== (prefix === 2)) y = P - y;
4123
+ const uncompressed = /* @__PURE__ */ new Uint8Array(65);
4124
+ uncompressed[0] = 4;
4125
+ uncompressed.set(bigIntToBytes(x), 1);
4126
+ uncompressed.set(bigIntToBytes(y), 33);
4127
+ return uncompressed;
4128
+ }
4129
+ function modPow(base, exp, mod) {
4130
+ let result = BigInt(1);
4131
+ let b = base % mod;
4132
+ let e = exp;
4133
+ while (e > BigInt(0)) {
4134
+ if (e & BigInt(1)) result = result * b % mod;
4135
+ e >>= BigInt(1);
4136
+ b = b * b % mod;
4137
+ }
4138
+ return result;
4139
+ }
4140
+ function bigIntToBytes(value) {
4141
+ const bytes = /* @__PURE__ */ new Uint8Array(32);
4142
+ let v = value;
4143
+ for (let i = 31; i >= 0; i--) {
4144
+ bytes[i] = Number(v & BigInt(255));
4145
+ v >>= BigInt(8);
4146
+ }
4147
+ return bytes;
4148
+ }
4149
+ //#endregion
4150
+ //#region src/signer/verify-action-signature.ts
4151
+ const RENOWN_HASH_LENGTH = 44;
4152
+ const SHARED_HASH_LENGTH = 28;
4153
+ const HEX_SIGNATURE = /^(0x)?([0-9a-fA-F]{2})+$/;
4154
+ /**
4155
+ * Integrity only: identity binding and the live-id check sit with the caller.
4156
+ * `operation` is the incoming operation at load admission.
4157
+ */
4158
+ async function verifyActionSignature(action, target, path, operation) {
4159
+ const policy = target.policy ?? "legacy";
4160
+ const refusal = policyRefusal(action, policy);
4161
+ if (refusal) return refusal;
4162
+ const shape = signerShape(action.context?.signer);
4163
+ if (shape.kind === "unsigned") return policy === "v2-required" ? refuse("unsigned", "UNSIGNED_REQUIRED", `action ${action.id} is unsigned but ${target.documentId} requires v2 signatures`) : {
4164
+ ok: true,
4165
+ scheme: "unsigned"
4166
+ };
4167
+ if (shape.kind === "malformed") return refuse("legacy-unknown", "MALFORMED_TUPLE", `action ${action.id} ${shape.reason}`);
4168
+ const { signer, tuple } = shape;
4169
+ const scheme = schemeOf(tuple[2]);
4170
+ if (tuple[1] !== signer.app.key) return refuse(scheme, "KEY_MISMATCH", `action ${action.id} tuple key does not match signer.app.key`);
4171
+ 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`);
4172
+ if (scheme === "legacy-unknown" && path === "mutation") return refuse(scheme, "MALFORMED_TUPLE", `action ${action.id} carries a hash of unknown length ${tuple[2].length}`);
4173
+ if (scheme === "v2") {
4174
+ const refusal = await checkV2(action, tuple, target, operation);
4175
+ if (refusal) return refusal;
4176
+ } else if (path === "mutation") {
4177
+ if (await legacyHash(scheme, action, target) !== tuple[2]) return refuse(scheme, "HASH_MISMATCH", `action ${action.id} does not match the hash its signature covers`);
4178
+ }
4179
+ return verifyEcdsa(tuple, scheme, action.id);
4180
+ }
4181
+ /** Refusals a document's policy makes before any tuple is read. */
4182
+ function policyRefusal(action, policy) {
4183
+ if (action.type !== "CREATE_DOCUMENT") return;
4184
+ const scheme = schemeOfAction(action);
4185
+ const input = action.input;
4186
+ const documentId = input?.documentId ?? "";
4187
+ 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;
4188
+ const expected = derivedCreateId(input);
4189
+ if (expected === documentId) return;
4190
+ 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}`);
4191
+ }
4192
+ function derivedCreateId(input) {
4193
+ if (!input?.signing || !input.protocolVersions) return;
4194
+ try {
4195
+ return deriveDocumentId({
4196
+ documentType: input.model,
4197
+ createdAtUtcIso: input.signing.createdAtUtcIso,
4198
+ nonce: input.signing.nonce,
4199
+ protocolVersions: input.protocolVersions
4200
+ });
4201
+ } catch {
4202
+ return;
4203
+ }
4204
+ }
4205
+ function schemeOfAction(action) {
4206
+ const shape = signerShape(action.context?.signer);
4207
+ if (shape.kind === "unsigned") return "unsigned";
4208
+ return shape.kind === "signed" ? schemeOf(shape.tuple[2]) : "legacy-unknown";
4209
+ }
4210
+ /** Peers send arbitrary JSON, so nothing past `context` is assumed. */
4211
+ function signerShape(value) {
4212
+ if (value === void 0 || value === null) return { kind: "unsigned" };
4213
+ if (!isRecord(value)) return {
4214
+ kind: "malformed",
4215
+ reason: "has a signer that is not an object"
4216
+ };
4217
+ const { app, user, signatures } = value;
4218
+ if (app !== void 0 && app !== null && !isRecord(app)) return {
4219
+ kind: "malformed",
4220
+ reason: "has a signer.app that is not an object"
4221
+ };
4222
+ const key = isRecord(app) ? app.key : void 0;
4223
+ if (key !== void 0 && key !== null && typeof key !== "string") return {
4224
+ kind: "malformed",
4225
+ reason: "has a signer.app.key that is not a string"
4226
+ };
4227
+ if (!key) return { kind: "unsigned" };
4228
+ if (!isRecord(user) || typeof user.address !== "string") return {
4229
+ kind: "malformed",
4230
+ reason: "has a signer without a user address"
4231
+ };
4232
+ if (!Array.isArray(signatures)) return {
4233
+ kind: "malformed",
4234
+ reason: "has a signer whose signatures are not a list"
4235
+ };
4236
+ const tuple = signatures.at(-1);
4237
+ if (!isTuple(tuple)) return {
4238
+ kind: "malformed",
4239
+ reason: signatures.length === 0 ? "has a signer but no signatures" : "carries a malformed signature tuple"
4240
+ };
4241
+ return {
4242
+ kind: "signed",
4243
+ signer: value,
4244
+ tuple
4245
+ };
4246
+ }
4247
+ function isRecord(value) {
4248
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4249
+ }
4250
+ async function checkV2(action, tuple, target, operation) {
4251
+ const problem = v2TupleProblem(tuple);
4252
+ if (problem) return refuse("v2", "MALFORMED_TUPLE", `action ${action.id}: ${problem}`);
4253
+ if (operation && !sameInstant(operation.timestampUtcMs, action.timestampUtcMs)) return refuse("v2", "TIMESTAMP_MISMATCH", `action ${action.id} is stamped ${action.timestampUtcMs} but its operation ${operation.timestampUtcMs}`);
4254
+ let expected;
4255
+ try {
4256
+ expected = await hashActionV2(action, target, action.context.signer);
4257
+ } catch (error) {
4258
+ return refuse("v2", "HASH_MISMATCH", `action ${action.id} has no v2 hash: ${error instanceof Error ? error.message : String(error)}`);
4259
+ }
4260
+ if (expected !== tuple[2]) return refuse("v2", "HASH_MISMATCH", `action ${action.id} does not match the hash its signature covers`);
4261
+ }
4262
+ function sameInstant(a, b) {
4263
+ const left = Date.parse(a);
4264
+ return !Number.isNaN(left) && left === Date.parse(b);
4265
+ }
4266
+ function schemeOf(hash) {
4267
+ if (isV2ActionHash(hash)) return "v2";
4268
+ if (hash.length === RENOWN_HASH_LENGTH) return "legacy-renown";
4269
+ if (hash.length === SHARED_HASH_LENGTH) return "legacy-shared";
4270
+ return "legacy-unknown";
4271
+ }
4272
+ async function legacyHash(scheme, action, target) {
4273
+ if (scheme === "legacy-renown") {
4274
+ const payload = [
4275
+ action.scope,
4276
+ action.type,
4277
+ JSON.stringify(action.input)
4278
+ ].join("");
4279
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(payload));
4280
+ return bytesToBase64(new Uint8Array(digest));
4281
+ }
4282
+ const signer = action.context.signer;
4283
+ return buildOperationSignatureParams({
4284
+ documentId: target.documentId,
4285
+ signer,
4286
+ action,
4287
+ previousStateHash: ""
4288
+ })[2];
4289
+ }
4290
+ async function verifyEcdsa(tuple, scheme, actionId) {
4291
+ const [timestamp, key, hash, prevStateHash, signatureHex] = tuple;
4292
+ if (!HEX_SIGNATURE.test(signatureHex)) return refuse(scheme, "MALFORMED_TUPLE", `action ${actionId} signature is not hex`);
4293
+ let cryptoKey;
4294
+ try {
4295
+ cryptoKey = await importDidKey(key);
4296
+ } catch (error) {
4297
+ return refuse(scheme, "MALFORMED_TUPLE", `action ${actionId}: ${error instanceof Error ? error.message : String(error)}`);
4298
+ }
4299
+ const message = buildOperationSignatureMessage([
4300
+ timestamp,
4301
+ key,
4302
+ hash,
4303
+ prevStateHash
4304
+ ]);
4305
+ let valid;
4306
+ try {
4307
+ valid = await crypto.subtle.verify({
4308
+ name: "ECDSA",
4309
+ hash: "SHA-256"
4310
+ }, cryptoKey, hexToBytes(signatureHex).buffer, message.buffer);
4311
+ } catch {
4312
+ valid = false;
4313
+ }
4314
+ if (!valid) return refuse(scheme, "BAD_SIGNATURE", `action ${actionId} signature does not verify under its key`);
4315
+ return {
4316
+ ok: true,
4317
+ scheme
4318
+ };
4319
+ }
4320
+ function isTuple(value) {
4321
+ return Array.isArray(value) && value.length === 5 && value.every((element) => typeof element === "string");
4322
+ }
4323
+ function refuse(scheme, code, reason) {
4324
+ return {
4325
+ ok: false,
4326
+ scheme,
4327
+ code,
4328
+ reason
4329
+ };
4330
+ }
4331
+ function hexToBytes(hex) {
4332
+ const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
4333
+ const bytes = new Uint8Array(clean.length / 2);
4334
+ for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(clean.substring(i * 2, i * 2 + 2), 16);
4335
+ return bytes;
4336
+ }
4337
+ function bytesToBase64(bytes) {
4338
+ let binary = "";
4339
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
4340
+ return btoa(binary);
4341
+ }
4342
+ //#endregion
4343
+ //#region src/executor/synthesized-signing.ts
4344
+ /** Submitted types whose stored action the reducer writes in their place. */
4345
+ const SYNTHESIZING_TYPES = /* @__PURE__ */ new Set(["UNDO", "REDO"]);
4346
+ /**
4347
+ * The id of the action synthesized from `submittedId`. Derived, so the stored
4348
+ * operation answers for the submitted id at the live-id check.
4349
+ */
4350
+ function synthesizedActionId(submittedId) {
4351
+ return hashBrowser(`synthesized:${submittedId}`, "sha1", "hex").slice(0, 32);
4352
+ }
4353
+ /**
4354
+ * Whether `operation` stores an action the reducer made from `submitted`
4355
+ * rather than `submitted` itself or an operation already in `history`.
4356
+ */
4357
+ function isSynthesized(submitted, operation, history) {
4358
+ if (!SYNTHESIZING_TYPES.has(submitted.type)) return false;
4359
+ const id = operation.action.id;
4360
+ if (!id) return true;
4361
+ if (id === submitted.id) return false;
4362
+ return !history.some((existing) => existing.action.id === id);
4363
+ }
4364
+ /**
4365
+ * Signs the synthesized action of `operation` in place as `signer`, under the
4366
+ * id {@link synthesizedActionId} derives from the submitted one. A REDO's
4367
+ * rebuilt action has no timestamp, so it takes the submitted one; the
4368
+ * operation timestamp is set to the action's, which a peer requires of a v2
4369
+ * tuple.
4370
+ */
4371
+ async function signSynthesized(operation, submitted, target, signer, signal) {
4372
+ const synthesized = operation.action;
4373
+ const id = synthesizedActionId(submitted.id);
4374
+ const timestampUtcMs = operation.timestampUtcMs || synthesized.timestampUtcMs || submitted.timestampUtcMs;
4375
+ const action = {
4376
+ id,
4377
+ timestampUtcMs,
4378
+ type: synthesized.type,
4379
+ input: synthesized.input,
4380
+ scope: synthesized.scope
4381
+ };
4382
+ const signature = await signer.signAction(action, {
4383
+ documentId: target.documentId,
4384
+ branch: target.branch
4385
+ }, signal);
4386
+ operation.action = {
4387
+ ...action,
4388
+ context: { signer: {
4389
+ ...actionSignerIdentity(signer),
4390
+ signatures: [signature]
4391
+ } }
4392
+ };
4393
+ operation.id = deriveOperationId(target.documentId, target.scope, target.branch, id);
4394
+ operation.timestampUtcMs = timestampUtcMs;
4395
+ }
4396
+ //#endregion
4397
+ //#region src/executor/signature-admission.ts
4398
+ const DEFAULT_TRUST_TIMEOUT_MS = 1e4;
4399
+ /** Runs once per write, when this reactor first stores it; never on re-appends. */
4400
+ var SignatureAdmission = class {
4401
+ mode;
4402
+ logger;
4403
+ eventBus;
4404
+ policySource;
4405
+ trustPolicy;
4406
+ trustTimeoutMs;
4407
+ constructor(mode, logger, eventBus, policySource = "meta", trust = {
4408
+ authEnforcement: false,
4409
+ timeoutMs: DEFAULT_TRUST_TIMEOUT_MS
4410
+ }) {
4411
+ this.mode = mode;
4412
+ this.logger = logger;
4413
+ this.eventBus = eventBus;
4414
+ this.policySource = policySource;
4415
+ this.trustPolicy = admissionTrustPolicy(trust.signer ?? new PassthroughSigner(), trust.authEnforcement, trust.policy);
4416
+ this.trustTimeoutMs = trust.timeoutMs;
4417
+ }
4418
+ /** The first refusal of a mutation's submitted actions, when enforcing. */
4419
+ async admitMutation(job, stores, signal) {
4420
+ const { operationStore } = stores;
4421
+ const candidates = job.actions.map((action) => candidate(action, mutationStream(action, job)));
4422
+ const live = await this.liveOperationIds(candidates, operationStore, signal);
4423
+ const committed = await this.storedAsSubmitted(candidates, live, isRetry(job), operationStore, signal);
4424
+ await resolvePolicies(candidates, job.actions, (documentId, branch) => storedPolicy(stores, this.policySource, documentId, branch, signal));
4425
+ const submitted = /* @__PURE__ */ new Set();
4426
+ for (const entry of candidates) {
4427
+ if (committed.has(entry)) continue;
4428
+ const verdict = await this.verdict(entry, live, submitted, "mutation", signal);
4429
+ submitted.add(entry.opId);
4430
+ if (verdict.ok) continue;
4431
+ const refusal = this.record(job, entry, verdict, "mutation");
4432
+ if (refusal) return {
4433
+ kind: "refused",
4434
+ error: refusal
4435
+ };
4436
+ }
4437
+ return {
4438
+ kind: "admitted",
4439
+ committed: [...committed.values()]
4440
+ };
4441
+ }
4442
+ /** The incoming operations a load drops, empty unless enforcing. */
4443
+ async admitLoad(job, operations, stores, signal) {
4444
+ const { operationStore } = stores;
4445
+ const stream = {
4446
+ documentId: job.documentId,
4447
+ scope: job.scope,
4448
+ branch: job.branch
4449
+ };
4450
+ const candidates = operations.map((operation) => ({
4451
+ ...candidate(operation.action, stream),
4452
+ operation
4453
+ }));
4454
+ const live = await this.liveOperationIds(candidates, operationStore, signal);
4455
+ const redelivered = await this.redelivered(candidates, live, operationStore, signal);
4456
+ await resolvePolicies(candidates, job.operations.map((operation) => operation.action), (documentId, branch) => storedPolicy(stores, this.policySource, documentId, branch, signal));
4457
+ const dropped = /* @__PURE__ */ new Set();
4458
+ for (let i = 0; i < candidates.length; i++) {
4459
+ const verdict = await this.verdict(candidates[i], live, void 0, "load", signal, redelivered.has(candidates[i]));
4460
+ if (verdict.ok) continue;
4461
+ if (this.record(job, candidates[i], verdict, "load")) dropped.add(operations[i]);
4462
+ }
4463
+ return dropped;
4464
+ }
4465
+ /** A marker is held to v2 and enforced whatever the policy and mode. */
4466
+ async admitMarker(job, marker, signal) {
4467
+ const entry = {
4468
+ ...candidate(marker.action, {
4469
+ documentId: job.documentId,
4470
+ scope: "document",
4471
+ branch: job.branch
4472
+ }),
4473
+ operation: marker,
4474
+ policy: "v2-required"
4475
+ };
4476
+ const verdict = await this.verdict(entry, /* @__PURE__ */ new Set(), void 0, "load", signal);
4477
+ if (verdict.ok) return;
4478
+ return this.record(job, entry, verdict, "load", true);
4479
+ }
4480
+ async verdict(entry, live, submitted, path, signal, known = false) {
4481
+ const verdict = await verifyActionSignature(entry.action, {
4482
+ documentId: entry.stream.documentId,
4483
+ branch: entry.stream.branch,
4484
+ policy: entry.policy
4485
+ }, path, entry.operation);
4486
+ if (!verdict.ok) return verdict;
4487
+ if (!known && (isLive(entry, live) || submitted?.has(entry.opId))) return {
4488
+ ok: false,
4489
+ scheme: verdict.scheme,
4490
+ code: "DUPLICATE_ACTION",
4491
+ reason: `action ${entry.action.id} is already in the stream`
4492
+ };
4493
+ if (verdict.scheme === "unsigned") {
4494
+ const address = entry.action.context?.signer?.user?.address;
4495
+ return address ? {
4496
+ ok: false,
4497
+ scheme: "unsigned",
4498
+ code: "UNSIGNED_IDENTITY",
4499
+ reason: `action ${entry.action.id} is unsigned but claims to act as ${address}`
4500
+ } : verdict;
4501
+ }
4502
+ if (known) return verdict;
4503
+ const signer = entry.action.context.signer;
4504
+ if (!await this.authorize(signer, entry.stream.documentId, signal)) return {
4505
+ ok: false,
4506
+ scheme: verdict.scheme,
4507
+ code: "SIGNER_UNAUTHORIZED",
4508
+ reason: `action ${entry.action.id}: key ${signer.app.key} may not sign as ${signer.user.address}`
4509
+ };
4510
+ return verdict;
4511
+ }
4512
+ /** Throws when the policy does, times out or the job is aborted. */
4513
+ async authorize(signer, documentId, signal) {
4514
+ signal?.throwIfAborted();
4515
+ let timer;
4516
+ let onAbort;
4517
+ const stop = new Promise((_, reject) => {
4518
+ timer = setTimeout(() => {
4519
+ reject(/* @__PURE__ */ new Error(`signature trust policy did not answer for ${signer.app.key} within ${this.trustTimeoutMs}ms`));
4520
+ }, this.trustTimeoutMs);
4521
+ if (signal) {
4522
+ onAbort = () => reject(signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason)));
4523
+ signal.addEventListener("abort", onAbort, { once: true });
3244
4524
  }
3245
- if (!isValid) throw new InvalidSignatureError(documentId, `Operation ${operation.id} at index ${operation.index} signature verification returned false`);
4525
+ });
4526
+ try {
4527
+ return await Promise.race([this.trustPolicy.authorizeSigner(signer, signer.app.key, documentId), stop]);
4528
+ } finally {
4529
+ clearTimeout(timer);
4530
+ if (onAbort) signal?.removeEventListener("abort", onAbort);
3246
4531
  }
3247
4532
  }
4533
+ /**
4534
+ * The live candidates stored exactly as submitted and still standing: one
4535
+ * an undo or a reshuffle has since superseded is refused, not re-emitted with
4536
+ * the state it once produced. A synthesized operation
4537
+ * holds another action, so it cannot be compared; only a retry, whose first
4538
+ * attempt this job was, takes it as committed.
4539
+ */
4540
+ async storedAsSubmitted(candidates, live, retry, operationStore, signal) {
4541
+ const committed = /* @__PURE__ */ new Map();
4542
+ const toCompare = [];
4543
+ for (const entry of candidates) if (entry.synthesizedOpId && live.has(entry.synthesizedOpId)) {
4544
+ if (retry) committed.set(entry, committedWrite(entry, entry.synthesizedOpId));
4545
+ } else if (live.has(entry.opId)) toCompare.push(entry);
4546
+ for (const [stream, entries] of byStream(toCompare)) {
4547
+ const stored = await operationStore.getOperationsByIds(stream.documentId, stream.scope, stream.branch, entries.map((entry) => entry.opId), signal);
4548
+ const byId = new Map(stored.map((operation) => [operation.id, operation]));
4549
+ const matching = entries.filter((entry) => {
4550
+ const operation = byId.get(entry.opId);
4551
+ return operation && sameContent(operation.action, entry.action);
4552
+ });
4553
+ if (matching.length === 0) continue;
4554
+ 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);
4555
+ for (const entry of matching) if (!isSuperseded(byId.get(entry.opId), later.results)) committed.set(entry, committedWrite(entry, entry.opId));
4556
+ }
4557
+ return committed;
4558
+ }
4559
+ /**
4560
+ * The load candidates stored exactly as they arrive, timestamp included: a
4561
+ * peer re-sending what this stream holds, not a replay of its action.
4562
+ */
4563
+ async redelivered(candidates, live, operationStore, signal) {
4564
+ const found = /* @__PURE__ */ new Set();
4565
+ const toCompare = candidates.filter((entry) => live.has(entry.opId) && !(entry.synthesizedOpId && live.has(entry.synthesizedOpId)));
4566
+ for (const [stream, entries] of byStream(toCompare)) {
4567
+ const stored = await operationStore.getOperationsByIds(stream.documentId, stream.scope, stream.branch, entries.map((entry) => entry.opId), signal);
4568
+ 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);
4569
+ }
4570
+ return found;
4571
+ }
4572
+ async liveOperationIds(candidates, operationStore, signal) {
4573
+ const live = /* @__PURE__ */ new Set();
4574
+ for (const [stream, entries] of byStream(candidates)) {
4575
+ const found = await operationStore.findOperationIds(stream.documentId, stream.scope, stream.branch, entries.flatMap((entry) => entry.synthesizedOpId ? [entry.opId, entry.synthesizedOpId] : [entry.opId]), signal);
4576
+ for (const opId of found) live.add(opId);
4577
+ }
4578
+ return live;
4579
+ }
4580
+ /** Logs and counts a refusal; returns the error only when enforcing. */
4581
+ record(job, entry, refusal, path, enforced = this.mode === "enforce") {
4582
+ const event = {
4583
+ jobId: job.id,
4584
+ documentId: entry.stream.documentId,
4585
+ scope: entry.stream.scope,
4586
+ branch: entry.stream.branch,
4587
+ actionId: entry.action.id,
4588
+ code: refusal.code,
4589
+ scheme: refusal.scheme,
4590
+ path,
4591
+ enforced,
4592
+ reason: refusal.reason
4593
+ };
4594
+ this.logger.warn(enforced ? "Signature refused: @Refusal" : "Signature refusal logged, write admitted: @Refusal", event);
4595
+ this.eventBus.emit(ReactorEventTypes.SIGNATURE_REFUSED, event).catch((error) => {
4596
+ this.logger.error("Failed to emit SIGNATURE_REFUSED: @Error", error);
4597
+ });
4598
+ return enforced ? new InvalidSignatureError(entry.stream.documentId, refusal.code, refusal.reason) : void 0;
4599
+ }
3248
4600
  };
4601
+ /**
4602
+ * A CREATE_DOCUMENT is verified under its own input. Anything else takes the
4603
+ * stored document's policy, or, for a document this job creates, that CREATE's.
4604
+ */
4605
+ async function resolvePolicies(candidates, jobActions, stored) {
4606
+ const resolved = /* @__PURE__ */ new Map();
4607
+ for (const entry of candidates) {
4608
+ if (entry.action.type === "CREATE_DOCUMENT") {
4609
+ entry.policy = createPolicy(entry.action);
4610
+ continue;
4611
+ }
4612
+ const { documentId, branch } = entry.stream;
4613
+ const key = `${documentId}\u0000${branch}`;
4614
+ let policy = resolved.get(key);
4615
+ if (policy === void 0) {
4616
+ policy = await stored(documentId, branch) ?? createdPolicy(jobActions, documentId) ?? "legacy";
4617
+ resolved.set(key, policy);
4618
+ }
4619
+ entry.policy = policy;
4620
+ }
4621
+ }
4622
+ async function storedPolicy(stores, source, documentId, branch, signal) {
4623
+ try {
4624
+ if (source === "write-cache") {
4625
+ const document = await stores.writeCache.getState(documentId, "document", branch, void 0, signal);
4626
+ return signaturePolicyOf(document.header);
4627
+ }
4628
+ const meta = await stores.documentMetaCache.getDocumentMeta(documentId, branch, signal);
4629
+ return signaturePolicyOf(meta.protocolVersions);
4630
+ } catch (error) {
4631
+ if (DocumentNotFoundError.isError(error)) return;
4632
+ throw error;
4633
+ }
4634
+ }
4635
+ function createdPolicy(actions, documentId) {
4636
+ const create = actions.find((action) => action.type === "CREATE_DOCUMENT" && action.input?.documentId === documentId);
4637
+ return create ? createPolicy(create) : void 0;
4638
+ }
4639
+ function createPolicy(action) {
4640
+ return signaturePolicyOf(action.input);
4641
+ }
4642
+ function candidate(action, stream) {
4643
+ const opId = (actionId) => deriveOperationId(stream.documentId, stream.scope, stream.branch, actionId);
4644
+ return {
4645
+ action,
4646
+ stream,
4647
+ opId: opId(action.id),
4648
+ synthesizedOpId: SYNTHESIZING_TYPES.has(action.type) ? opId(synthesizedActionId(action.id)) : void 0
4649
+ };
4650
+ }
4651
+ function isLive(entry, live) {
4652
+ return live.has(entry.opId) || entry.synthesizedOpId !== void 0 && live.has(entry.synthesizedOpId);
4653
+ }
4654
+ function mutationStream(action, job) {
4655
+ return {
4656
+ ...actionSigningTarget(action, job.documentId, job.branch),
4657
+ scope: job.scope
4658
+ };
4659
+ }
4660
+ function byStream(candidates) {
4661
+ const groups = /* @__PURE__ */ new Map();
4662
+ for (const entry of candidates) {
4663
+ const key = `${entry.stream.documentId}\u0000${entry.stream.scope}\u0000${entry.stream.branch}`;
4664
+ const group = groups.get(key) ?? [entry.stream, []];
4665
+ group[1].push(entry);
4666
+ groups.set(key, group);
4667
+ }
4668
+ return [...groups.values()];
4669
+ }
4670
+ function isRetry(job) {
4671
+ return job.errorHistory.length > 0 || (job.retryCount ?? 0) > 0;
4672
+ }
4673
+ function committedWrite(entry, opId) {
4674
+ return {
4675
+ ...entry.stream,
4676
+ actionId: entry.action.id,
4677
+ opId
4678
+ };
4679
+ }
4680
+ function isSuperseded(operation, later) {
4681
+ return later.some((next) => next.index > operation.index && next.skip > 0 && next.index - next.skip <= operation.index);
4682
+ }
4683
+ function sameContent(stored, submitted) {
4684
+ try {
4685
+ return canonicalJson(stored) === canonicalJson(submitted);
4686
+ } catch {
4687
+ return false;
4688
+ }
4689
+ }
3249
4690
  //#endregion
3250
4691
  //#region src/executor/simple-job-executor.ts
3251
4692
  const MAX_SKIP_THRESHOLD = 1e3;
@@ -3262,7 +4703,32 @@ function isValidISOTimestamp(value) {
3262
4703
  * returned JobResult there, which is what the queue, the worker protocol and
3263
4704
  * every test expect a failed job to look like.
3264
4705
  */
4706
+ /** The ids of a purge job's request, which a drive purge may precede. */
4707
+ function purgeRequestDocumentIds(job) {
4708
+ const ids = job.meta.purgeRequestDocumentIds;
4709
+ return Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
4710
+ }
4711
+ const MARKER_INPUT_KEYS = /* @__PURE__ */ new Set([
4712
+ "documentId",
4713
+ "documentType",
4714
+ "purgedAtUtcIso",
4715
+ "requestId"
4716
+ ]);
4717
+ /** A peer's marker must name this job's id on main and carry nothing else. */
4718
+ function malformedMarker(job) {
4719
+ const action = job.operations.find((operation) => isPurgeMarker(operation))?.action;
4720
+ const input = action?.input;
4721
+ const refuse = (reason) => new InvalidSignatureError(job.documentId, "ID_MISMATCH", `marker ${action?.id ?? "?"} ${reason}`);
4722
+ if (typeof input !== "object" || input === null) return refuse("carries no input");
4723
+ const extra = Object.keys(input).filter((key) => !MARKER_INPUT_KEYS.has(key));
4724
+ if (extra.length > 0) return refuse(`carries unexpected input ${extra.join(", ")}`);
4725
+ for (const key of MARKER_INPUT_KEYS) if (typeof input[key] !== "string" || input[key] === "") return refuse(`input.${key} is not a non-empty string`);
4726
+ const purgedAt = Date.parse(input.purgedAtUtcIso);
4727
+ if (Number.isNaN(purgedAt) || new Date(purgedAt).toISOString() !== input.purgedAtUtcIso) return refuse("input.purgedAtUtcIso is not an ISO timestamp");
4728
+ if (input.documentId !== job.documentId || job.branch !== "main" || action?.scope !== "document") return refuse(`does not purge ${job.documentId} on document/main`);
4729
+ }
3265
4730
  var JobRollbackSignal = class extends Error {
4731
+ result;
3266
4732
  constructor(result) {
3267
4733
  super("job rolled back");
3268
4734
  this.result = result;
@@ -3270,13 +4736,27 @@ var JobRollbackSignal = class extends Error {
3270
4736
  }
3271
4737
  };
3272
4738
  var SimpleJobExecutor = class {
4739
+ logger;
4740
+ registry;
4741
+ operationStore;
4742
+ eventBus;
4743
+ writeCache;
4744
+ operationIndex;
4745
+ documentMetaCache;
4746
+ collectionMembershipCache;
4747
+ driveContainerTypes;
3273
4748
  config;
3274
4749
  featureFlags;
3275
4750
  decisionModel;
3276
- signatureVerifierModule;
4751
+ signatureAdmission;
3277
4752
  documentActionHandler;
3278
4753
  executionScope;
3279
- constructor(logger, registry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, driveContainerTypes, config, signatureVerifier, executionScope) {
4754
+ signer;
4755
+ /**
4756
+ * `signer` signs the operations the reducer synthesizes; unsigned if omitted.
4757
+ * `trustPolicy` decides which keys may sign as which users at admission.
4758
+ */
4759
+ constructor(logger, registry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, driveContainerTypes, config, executionScope, signer, trustPolicy) {
3280
4760
  this.logger = logger;
3281
4761
  this.registry = registry;
3282
4762
  this.operationStore = operationStore;
@@ -3286,6 +4766,8 @@ var SimpleJobExecutor = class {
3286
4766
  this.documentMetaCache = documentMetaCache;
3287
4767
  this.collectionMembershipCache = collectionMembershipCache;
3288
4768
  this.driveContainerTypes = driveContainerTypes;
4769
+ this.signer = signer ?? new PassthroughSigner();
4770
+ this.featureFlags = resolveFeatureFlags(config.featureFlags);
3289
4771
  this.config = {
3290
4772
  featureFlags: config.featureFlags ?? {},
3291
4773
  maxSkipThreshold: config.maxSkipThreshold ?? MAX_SKIP_THRESHOLD,
@@ -3295,12 +4777,19 @@ var SimpleJobExecutor = class {
3295
4777
  retryBaseDelayMs: config.retryBaseDelayMs ?? 100,
3296
4778
  retryMaxDelayMs: config.retryMaxDelayMs ?? 5e3,
3297
4779
  yieldDeadlineMs: config.yieldDeadlineMs ?? 50,
3298
- batchApplies: config.batchApplies ?? true
4780
+ batchApplies: config.batchApplies ?? true,
4781
+ signatureVerification: config.signatureVerification ?? "enforce",
4782
+ protocolSupport: config.protocolSupport ?? localSupports(PEER_CAPABILITIES, this.featureFlags).protocols,
4783
+ maxPurgeOperations: config.maxPurgeOperations ?? 2e5
3299
4784
  };
3300
- this.featureFlags = resolveFeatureFlags(config.featureFlags);
3301
4785
  this.decisionModel = selectDecisionModel(this.featureFlags, registry);
3302
- this.signatureVerifierModule = new SignatureVerifier(signatureVerifier);
3303
- this.documentActionHandler = new DocumentActionHandler(registry, logger, driveContainerTypes, this.featureFlags, this.decisionModel);
4786
+ this.signatureAdmission = new SignatureAdmission(this.config.signatureVerification, logger, eventBus, this.featureFlags.documentDecisions ? "write-cache" : "meta", {
4787
+ signer: this.signer,
4788
+ authEnforcement: this.featureFlags.authEnforcement,
4789
+ policy: trustPolicy,
4790
+ timeoutMs: Math.min(DEFAULT_TRUST_TIMEOUT_MS, this.config.jobTimeoutMs / 2)
4791
+ });
4792
+ this.documentActionHandler = new DocumentActionHandler(registry, logger, driveContainerTypes, this.featureFlags, this.decisionModel, this.config.protocolSupport);
3304
4793
  this.executionScope = executionScope ?? new DefaultExecutionScope(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache);
3305
4794
  }
3306
4795
  /**
@@ -3327,6 +4816,7 @@ var SimpleJobExecutor = class {
3327
4816
  const touchedStreams = new TouchedStreams();
3328
4817
  const postCommitInvalidations = [];
3329
4818
  const postCommitMembershipInvalidations = [];
4819
+ const postCommitMetaInvalidations = [];
3330
4820
  let outcome;
3331
4821
  try {
3332
4822
  outcome = await this.executionScope.run(async (stores) => {
@@ -3337,7 +4827,8 @@ var SimpleJobExecutor = class {
3337
4827
  signal,
3338
4828
  touchedStreams,
3339
4829
  postCommitInvalidations,
3340
- postCommitMembershipInvalidations
4830
+ postCommitMembershipInvalidations,
4831
+ postCommitMetaInvalidations
3341
4832
  });
3342
4833
  if (!scoped.result.success) throw new JobRollbackSignal(scoped.result);
3343
4834
  return scoped;
@@ -3349,6 +4840,7 @@ var SimpleJobExecutor = class {
3349
4840
  }
3350
4841
  for (const entry of postCommitInvalidations) this.writeCache.invalidate(entry.documentId, entry.scope, entry.branch);
3351
4842
  for (const documentId of postCommitMembershipInvalidations) this.collectionMembershipCache.invalidate(documentId);
4843
+ for (const documentId of postCommitMetaInvalidations) this.documentMetaCache.invalidate(documentId);
3352
4844
  const { pendingEvent } = outcome;
3353
4845
  if (pendingEvent) this.eventBus.emit(ReactorEventTypes.JOB_WRITE_READY, pendingEvent).catch((error) => {
3354
4846
  this.logger.error("Failed to emit JOB_WRITE_READY event: @Event : @Error", pendingEvent, error);
@@ -3364,8 +4856,38 @@ var SimpleJobExecutor = class {
3364
4856
  * has not committed yet has nothing to announce.
3365
4857
  */
3366
4858
  async executeInScope(params) {
3367
- const { job, startTime, stores, signal, touchedStreams, postCommitInvalidations, postCommitMembershipInvalidations } = params;
4859
+ const { job, startTime, stores: scopeStores, signal, touchedStreams, postCommitInvalidations, postCommitMembershipInvalidations } = params;
3368
4860
  let pendingEvent;
4861
+ if (job.kind === "mutation") {
4862
+ const reserved = job.actions.find((action) => isPurgeMarker(action));
4863
+ if (reserved) return { result: buildErrorResult(job, new ReservedActionError(job.documentId, reserved.type), startTime) };
4864
+ }
4865
+ if (job.kind === "purge" || job.kind === "load" && job.operations.some((op) => isPurgeMarker(op))) return this.executePurge(params);
4866
+ const lockedIds = jobWriteIds(job);
4867
+ const lockFailure = (error) => ({ result: buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime) });
4868
+ try {
4869
+ await scopeStores.documentLocks.shared(lockedIds);
4870
+ } catch (error) {
4871
+ return lockFailure(error);
4872
+ }
4873
+ let purged;
4874
+ try {
4875
+ purged = await scopeStores.documentLocks.purged(lockedIds);
4876
+ } catch (error) {
4877
+ return lockFailure(error);
4878
+ }
4879
+ const purgedRefusal = this.purgedRefusal(job, purged);
4880
+ if (purgedRefusal) return { result: buildErrorResult(job, purgedRefusal, startTime) };
4881
+ const purgeFence = new PurgeFence(scopeStores.documentLocks, new Set(lockedIds), purged);
4882
+ const stores = {
4883
+ ...scopeStores,
4884
+ writeCache: new FencedWriteCache(scopeStores.writeCache, purgeFence, (documentId) => {
4885
+ scopeStores.writeCache.invalidate(documentId);
4886
+ scopeStores.documentMetaCache.invalidate(documentId);
4887
+ })
4888
+ };
4889
+ const unsupported = await this.unsupportedStoredProtocol(job, stores, signal);
4890
+ if (unsupported) return { result: buildErrorResult(job, unsupported, startTime) };
3369
4891
  const indexTxn = stores.operationIndex.start();
3370
4892
  if (job.kind === "load") {
3371
4893
  const loadResult = await this.executeLoadJob({
@@ -3378,7 +4900,8 @@ var SimpleJobExecutor = class {
3378
4900
  evaluatedByPosition: false,
3379
4901
  postCommitInvalidations,
3380
4902
  postCommitMembershipInvalidations,
3381
- touchedStreams
4903
+ touchedStreams,
4904
+ purgeFence
3382
4905
  });
3383
4906
  if (loadResult.success && loadResult.operationsWithContext) {
3384
4907
  const ordinals = await stores.operationIndex.commit(indexTxn, signal);
@@ -3408,7 +4931,8 @@ var SimpleJobExecutor = class {
3408
4931
  evaluatedByPosition: false,
3409
4932
  postCommitInvalidations,
3410
4933
  postCommitMembershipInvalidations,
3411
- touchedStreams
4934
+ touchedStreams,
4935
+ purgeFence
3412
4936
  });
3413
4937
  if (reevalResult.success && reevalResult.operationsWithContext) {
3414
4938
  const ordinals = await stores.operationIndex.commit(indexTxn, signal);
@@ -3429,10 +4953,29 @@ var SimpleJobExecutor = class {
3429
4953
  pendingEvent
3430
4954
  };
3431
4955
  }
3432
- const positioned = await this.positionByTimestamp(job, stores, signal);
4956
+ let admission;
4957
+ try {
4958
+ admission = await this.signatureAdmission.admitMutation(job, stores, signal);
4959
+ } catch (error) {
4960
+ return { result: buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime) };
4961
+ }
4962
+ if (admission.kind === "refused") return { result: buildErrorResult(job, admission.error, startTime) };
4963
+ let committed;
4964
+ try {
4965
+ committed = await this.reloadCommitted(admission.committed, stores, signal);
4966
+ } catch (error) {
4967
+ return { result: buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime) };
4968
+ }
4969
+ const committedIds = new Set(admission.committed.map((write) => write.actionId));
4970
+ const fresh = committedIds.size === 0 ? job : {
4971
+ ...job,
4972
+ actions: job.actions.filter((action) => !committedIds.has(action.id))
4973
+ };
4974
+ if (fresh.actions.length === 0) return this.committedOutcome(job, committed, [], [], stores, startTime);
4975
+ const positioned = await this.positionByTimestamp(fresh, stores, signal);
3433
4976
  if (positioned.error) return { result: buildErrorResult(job, positioned.error, startTime) };
3434
4977
  const executing = {
3435
- job,
4978
+ job: fresh,
3436
4979
  startTime,
3437
4980
  indexTxn,
3438
4981
  stores,
@@ -3441,7 +4984,8 @@ var SimpleJobExecutor = class {
3441
4984
  evaluatedByPosition: positioned.evaluatedByPosition,
3442
4985
  postCommitInvalidations,
3443
4986
  postCommitMembershipInvalidations,
3444
- touchedStreams
4987
+ touchedStreams,
4988
+ purgeFence
3445
4989
  };
3446
4990
  const actionResult = await this.processActions(positioned.writes, executing);
3447
4991
  if (!actionResult.success) return { result: {
@@ -3462,13 +5006,291 @@ var SimpleJobExecutor = class {
3462
5006
  } };
3463
5007
  const ordinals = await stores.operationIndex.commit(indexTxn, signal);
3464
5008
  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);
5009
+ for (let i = 0; i < actionResult.operationsWithContext.length; i++) actionResult.operationsWithContext[i].context.ordinal = ordinals[i];
5010
+ return this.committedOutcome(job, committed, actionResult.operationsWithContext, actionResult.generatedOperations, stores, startTime);
5011
+ }
5012
+ /** A load names only its own id purged; a foreign one is ID_MISMATCH. */
5013
+ purgedRefusal(job, purged) {
5014
+ if (purged.size === 0) return;
5015
+ if (purged.has(job.documentId)) return new DocumentPurgedError(job.documentId);
5016
+ const actions = [...job.actions, ...job.operations.map((operation) => operation.action)];
5017
+ for (const action of actions) {
5018
+ if (!DOCUMENT_SCOPE_ACTIONS.has(action.type)) continue;
5019
+ const target = targetDocumentId(action, job.documentId);
5020
+ if (!purged.has(target)) continue;
5021
+ if (job.kind === "mutation") return new DocumentPurgedError(target);
5022
+ return new InvalidSignatureError(job.documentId, "ID_MISMATCH", `${action.type} ${action.id} in ${job.documentId} writes purged ${target}`);
5023
+ }
5024
+ if (job.kind !== "mutation") return;
5025
+ for (const action of job.actions) {
5026
+ const target = relationshipTarget(action);
5027
+ if (action.type === "ADD_RELATIONSHIP" && target !== void 0 && purged.has(target)) return new DocumentPurgedError(target, `${action.type} target ${target} was purged`);
5028
+ if (action.scope !== "auth") continue;
5029
+ const group = mentionedGroupIds(action).find((id) => purged.has(id));
5030
+ if (group !== void 0) return new DocumentPurgedError(group, `${action.type} names purged group ${group}`);
5031
+ }
5032
+ }
5033
+ /** A purge job or a marker load: the exclusive lock is the job's only lock. */
5034
+ async executePurge(params) {
5035
+ const { job, startTime, stores } = params;
5036
+ const fail = (error) => ({ result: buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime) });
5037
+ if (job.kind === "load") {
5038
+ const malformed = malformedMarker(job);
5039
+ if (malformed) return fail(malformed);
5040
+ }
5041
+ const purger = stores.purger;
5042
+ if (!purger) return fail(/* @__PURE__ */ new Error(`Purge of ${job.documentId} needs a transactional scope`));
5043
+ try {
5044
+ await stores.documentLocks.exclusive(job.documentId);
5045
+ } catch (error) {
5046
+ return fail(error);
5047
+ }
5048
+ let alreadyPurged;
5049
+ try {
5050
+ alreadyPurged = (await stores.documentLocks.purged([job.documentId])).has(job.documentId);
5051
+ } catch (error) {
5052
+ return fail(error);
5053
+ }
5054
+ if (alreadyPurged) return {
5055
+ result: {
5056
+ job,
5057
+ success: true,
5058
+ operations: [],
5059
+ operationsWithContext: [],
5060
+ duration: Date.now() - startTime
5061
+ },
5062
+ pendingEvent: {
5063
+ jobId: job.id,
5064
+ operations: [],
5065
+ jobMeta: job.meta,
5066
+ collectionMemberships: {}
5067
+ }
5068
+ };
5069
+ let commit;
5070
+ try {
5071
+ commit = job.kind === "purge" ? await this.preparePurgeJob(params, purger) : await this.preparePurgeLoad(params, purger);
5072
+ } catch (error) {
5073
+ return fail(error);
5074
+ }
5075
+ if (commit instanceof Error) return fail(commit);
5076
+ try {
5077
+ return await this.commitPurge(params, commit);
5078
+ } catch (error) {
5079
+ return fail(error);
5080
+ }
5081
+ }
5082
+ /** Preconditions 1-5, then the marker this host signs. */
5083
+ async preparePurgeJob(params, purger) {
5084
+ const { job, stores, signal } = params;
5085
+ const documentId = job.documentId;
5086
+ if (!job.purge) return /* @__PURE__ */ new Error(`Purge job ${job.id} carries no purge options`);
5087
+ const held = await this.heldStreams(purger, documentId);
5088
+ if (held.branches.length === 0) return new DocumentNotDeletedError(documentId, `Document ${documentId} is not held here`);
5089
+ let documentType;
5090
+ for (const branch of held.branches) {
5091
+ let meta;
5092
+ try {
5093
+ meta = await stores.documentMetaCache.getDocumentMeta(documentId, branch, signal);
5094
+ } catch (error) {
5095
+ return new DocumentNotDeletedError(documentId, `Document ${documentId} has no readable state on ${branch}: ${error instanceof Error ? error.message : String(error)}`);
5096
+ }
5097
+ if (!meta.state.isDeleted) return new DocumentNotDeletedError(documentId, `Document ${documentId} is not deleted on branch ${branch}`);
5098
+ documentType ??= meta.documentType;
5099
+ }
5100
+ if (documentType === void 0) return new DocumentNotDeletedError(documentId);
5101
+ if (documentType === groupDocumentType) {
5102
+ const referencers = await purger.groupReferencersInHistory(documentId);
5103
+ if (referencers.length > 0) return new GroupInUseError(documentId, referencers);
5104
+ }
5105
+ let survivors = [];
5106
+ if (this.driveContainerTypes.has(documentType)) {
5107
+ const requested = new Set(purgeRequestDocumentIds(job));
5108
+ const members = await this.driveMembers(purger, stores, documentId, held.branches);
5109
+ const blocking = members.required.filter((id) => !requested.has(id));
5110
+ if (blocking.length > 0) return new DocumentNotDeletedError(documentId, `Drive ${documentId} cannot be purged: ${blocking.join(", ")} neither purged nor in the request`);
5111
+ survivors = members.survivors;
5112
+ }
5113
+ const operationCount = await purger.operationCount(documentId);
5114
+ if (operationCount > this.config.maxPurgeOperations && !job.purge.allowLarge) return new PurgeTooLargeError(documentId, operationCount, this.config.maxPurgeOperations);
5115
+ return {
5116
+ purger,
5117
+ marker: await this.signMarker(documentId, documentType, job.purge.requestId, signal),
5118
+ documentType,
5119
+ held,
5120
+ sourceRemote: "",
5121
+ collectionIds: [],
5122
+ survivors
5123
+ };
5124
+ }
5125
+ /** A peer's marker, admitted alone; a receiver cannot refuse an erasure. */
5126
+ async preparePurgeLoad(params, purger) {
5127
+ const { job, stores, signal } = params;
5128
+ const documentId = job.documentId;
5129
+ const marker = job.operations.find((operation) => isPurgeMarker(operation));
5130
+ const refusal = await this.signatureAdmission.admitMarker(job, marker, signal);
5131
+ if (refusal) return refusal;
5132
+ const held = await this.heldStreams(purger, documentId);
5133
+ let documentType = marker.action.input.documentType;
5134
+ let appliedDeletion = held.branches.length > 0;
5135
+ if (held.branches.length > 0) try {
5136
+ const meta = await stores.documentMetaCache.getDocumentMeta(documentId, held.branches.includes("main") ? "main" : held.branches[0], signal);
5137
+ documentType = meta.documentType;
5138
+ appliedDeletion = !meta.state.isDeleted;
5139
+ } catch {}
5140
+ let survivors = [];
5141
+ if (this.driveContainerTypes.has(documentType)) {
5142
+ const members = await this.driveMembers(purger, stores, documentId, held.branches);
5143
+ survivors = [...members.required, ...members.survivors];
5144
+ }
5145
+ const sourceRemote = typeof job.meta.sourceRemote === "string" ? job.meta.sourceRemote : "";
5146
+ const collectionId = sourceRemote ? await purger.remoteCollection(sourceRemote) : void 0;
5147
+ return {
5148
+ purger,
5149
+ marker: purgeMarkerOperation(marker.action),
5150
+ documentType,
5151
+ held,
5152
+ sourceRemote,
5153
+ collectionIds: collectionId === void 0 ? [] : [collectionId],
5154
+ survivors,
5155
+ ...appliedDeletion ? { appliedDeletion: true } : {}
5156
+ };
5157
+ }
5158
+ /** Execution: delete, write the marker and its twin, tombstone, reopen. */
5159
+ async commitPurge(params, commit) {
5160
+ const { job, startTime, stores, signal, touchedStreams } = params;
5161
+ const { purger, marker, documentType, held, sourceRemote } = commit;
5162
+ const documentId = job.documentId;
5163
+ const streams = [...held.streams, {
5164
+ scope: "document",
5165
+ branch: "main"
5166
+ }];
5167
+ for (const stream of streams) {
5168
+ touchedStreams.add(documentId, stream.scope, stream.branch);
5169
+ params.postCommitInvalidations.push({
5170
+ documentId,
5171
+ ...stream
5172
+ });
5173
+ }
5174
+ params.postCommitMetaInvalidations.push(documentId);
5175
+ const removedRows = await purger.deleteRows(documentId);
5176
+ const [stored] = await stores.operationStore.apply(documentId, documentType, "document", "main", 0, (txn) => {
5177
+ txn.addOperations(marker);
5178
+ }, signal);
5179
+ const indexTxn = stores.operationIndex.start();
5180
+ indexTxn.write([{
5181
+ ...stored,
5182
+ documentId,
5183
+ documentType,
5184
+ scope: "document",
5185
+ branch: "main",
5186
+ sourceRemote
5187
+ }]);
5188
+ const [ordinal] = await stores.operationIndex.commit(indexTxn, signal);
5189
+ await purger.writeTombstone({
5190
+ documentId,
5191
+ ordinal,
5192
+ removedRows,
5193
+ purgedAtUtc: marker.action.input.purgedAtUtcIso,
5194
+ requestId: marker.action.input.requestId
5195
+ });
5196
+ const collections = await purger.reopenMemberships(documentId, ordinal, commit.collectionIds);
5197
+ params.postCommitMembershipInvalidations.push(documentId, ...commit.survivors, ...indexTxn.getMembershipInvalidations());
5198
+ const operationWithContext = {
5199
+ operation: stored,
5200
+ context: {
5201
+ documentId,
5202
+ scope: "document",
5203
+ branch: "main",
5204
+ documentType,
5205
+ ordinal,
5206
+ ...commit.appliedDeletion ? { appliedDeletion: true } : {}
5207
+ }
5208
+ };
5209
+ return {
5210
+ result: {
5211
+ job,
5212
+ success: true,
5213
+ operations: [stored],
5214
+ operationsWithContext: [operationWithContext],
5215
+ duration: Date.now() - startTime
5216
+ },
5217
+ pendingEvent: {
5218
+ jobId: job.id,
5219
+ operations: [operationWithContext],
5220
+ jobMeta: job.meta,
5221
+ collectionMemberships: { [documentId]: collections }
5222
+ }
5223
+ };
5224
+ }
5225
+ async heldStreams(purger, documentId) {
5226
+ const streams = await purger.streams(documentId);
5227
+ return {
5228
+ streams,
5229
+ branches: [...new Set(streams.filter((stream) => stream.scope === "document").map((stream) => stream.branch))]
5230
+ };
5231
+ }
5232
+ /** Members a drive purge needs gone, and those living on elsewhere. */
5233
+ async driveMembers(purger, stores, driveId, branches) {
5234
+ const required = /* @__PURE__ */ new Set();
5235
+ const survivors = /* @__PURE__ */ new Set();
5236
+ for (const branch of branches.length > 0 ? branches : ["main"]) {
5237
+ const collectionId = DriveCollectionId.forDrive(driveId, branch).key;
5238
+ const members = await purger.collectionMembers(collectionId, driveId, branch);
5239
+ const purged = await stores.documentLocks.purged(members.map((member) => member.documentId));
5240
+ for (const member of members) {
5241
+ if (purged.has(member.documentId)) continue;
5242
+ if (member.required) required.add(member.documentId);
5243
+ else survivors.add(member.documentId);
5244
+ }
5245
+ }
5246
+ return {
5247
+ required: [...required],
5248
+ survivors: [...survivors]
5249
+ };
5250
+ }
5251
+ /** The marker, signed by this executor's signer as any peer admits it. */
5252
+ async signMarker(documentId, documentType, requestId, signal) {
5253
+ const action = purgeDocumentAction({
5254
+ documentId,
5255
+ documentType,
5256
+ requestId
5257
+ });
5258
+ const signature = await this.signer.signAction(action, {
5259
+ documentId,
5260
+ branch: "main"
5261
+ }, signal);
5262
+ return purgeMarkerOperation({
5263
+ ...action,
5264
+ context: { signer: {
5265
+ ...actionSignerIdentity(this.signer),
5266
+ signatures: [signature]
5267
+ } }
5268
+ });
5269
+ }
5270
+ /** A stored document at a version this reactor does not run is read-only here. */
5271
+ async unsupportedStoredProtocol(job, stores, signal) {
5272
+ let versions;
5273
+ try {
5274
+ 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;
5275
+ } catch {
5276
+ return;
5277
+ }
5278
+ return this.documentActionHandler.unsupportedProtocol(job.documentId, versions);
5279
+ }
5280
+ /**
5281
+ * A successful mutation's result and the write-ready event it owes, carrying
5282
+ * what earlier attempts committed ahead of what this one wrote.
5283
+ */
5284
+ async committedOutcome(job, reloaded, written, generated, stores, startTime) {
5285
+ const operationsWithContext = [...reloaded, ...written];
5286
+ let pendingEvent;
5287
+ if (operationsWithContext.length > 0) {
5288
+ const collectionMemberships = await this.getCollectionMembershipsForOperations(operationsWithContext, stores);
3468
5289
  pendingEvent = {
3469
5290
  jobId: job.id,
3470
- operations: actionResult.operationsWithContext,
5291
+ operations: operationsWithContext,
3471
5292
  jobMeta: job.meta,
5293
+ submittedActionIds: submittedActionIds(job),
3472
5294
  collectionMemberships
3473
5295
  };
3474
5296
  }
@@ -3476,14 +5298,62 @@ var SimpleJobExecutor = class {
3476
5298
  result: {
3477
5299
  job,
3478
5300
  success: true,
3479
- operations: actionResult.generatedOperations,
3480
- operationsWithContext: actionResult.operationsWithContext,
5301
+ operations: [...reloaded.map((entry) => entry.operation), ...generated],
5302
+ operationsWithContext,
3481
5303
  duration: Date.now() - startTime
3482
5304
  },
3483
5305
  pendingEvent
3484
5306
  };
3485
5307
  }
3486
5308
  /**
5309
+ * The stored operations of writes an earlier attempt committed, as a fresh
5310
+ * write would report them. State is rebuilt from the write cache, and left
5311
+ * out when it cannot be.
5312
+ */
5313
+ async reloadCommitted(writes, stores, signal) {
5314
+ const streams = /* @__PURE__ */ new Map();
5315
+ for (const write of writes) {
5316
+ const key = `${write.documentId}\u0000${write.scope}\u0000${write.branch}`;
5317
+ streams.set(key, [...streams.get(key) ?? [], write]);
5318
+ }
5319
+ const reloaded = [];
5320
+ for (const group of streams.values()) {
5321
+ const { documentId, scope, branch } = group[0];
5322
+ const opIds = [...new Set(group.map((write) => write.opId))];
5323
+ const operations = await stores.operationStore.getOperationsByIds(documentId, scope, branch, opIds, signal);
5324
+ const ordinals = await stores.operationIndex.getOrdinalsByOpIds(documentId, scope, branch, opIds, signal);
5325
+ const { documentType } = await stores.documentMetaCache.getDocumentMeta(documentId, branch, signal);
5326
+ for (const operation of operations.sort((a, b) => a.index - b.index)) reloaded.push({
5327
+ operation,
5328
+ context: {
5329
+ documentId,
5330
+ scope,
5331
+ branch,
5332
+ documentType,
5333
+ resultingState: await this.rebuiltState(stores, {
5334
+ documentId,
5335
+ scope,
5336
+ branch
5337
+ }, operation, signal),
5338
+ ordinal: ordinals.get(operation.id) ?? 0
5339
+ }
5340
+ });
5341
+ }
5342
+ return reloaded;
5343
+ }
5344
+ async rebuiltState(stores, stream, operation, signal) {
5345
+ try {
5346
+ const document = await stores.writeCache.getState(stream.documentId, stream.scope, stream.branch, operation.index, signal);
5347
+ return JSON.stringify({
5348
+ ...document.state,
5349
+ header: document.header
5350
+ });
5351
+ } catch (error) {
5352
+ this.logger.warn("No state for committed operation @OperationId in @Stream: @Error", operation.id, stream, error);
5353
+ return;
5354
+ }
5355
+ }
5356
+ /**
3487
5357
  * Drops the cached state of every stream a job wrote, after its transaction
3488
5358
  * did not commit.
3489
5359
  *
@@ -3512,16 +5382,6 @@ var SimpleJobExecutor = class {
3512
5382
  const actions = writes.map((write) => write.action);
3513
5383
  const generatedOperations = [];
3514
5384
  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
5385
  for (const action of actions) if (action.timestampUtcMs && !isValidISOTimestamp(action.timestampUtcMs)) return {
3526
5386
  success: false,
3527
5387
  generatedOperations,
@@ -3619,7 +5479,7 @@ var SimpleJobExecutor = class {
3619
5479
  if (docMeta.state.isDeleted) return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
3620
5480
  documentVersion = docMeta.state.version;
3621
5481
  }
3622
- if (isUndoRedo(action) || action.type === "PRUNE" || skip > 0) stores.writeCache.invalidate(job.documentId, job.scope, job.branch);
5482
+ if (isUndoRedo(action) || skip > 0) stores.writeCache.invalidate(job.documentId, job.scope, job.branch);
3623
5483
  let document;
3624
5484
  if (baseDocument !== void 0) document = baseDocument;
3625
5485
  else try {
@@ -3679,7 +5539,7 @@ var SimpleJobExecutor = class {
3679
5539
  branch: job.branch,
3680
5540
  protocolVersion
3681
5541
  };
3682
- updatedDocument = module.reducer(document, action, void 0, reducerOptions);
5542
+ updatedDocument = withProtocolVersions(module.reducer(document, action, void 0, reducerOptions), document.header.protocolVersions);
3683
5543
  } catch (error) {
3684
5544
  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
5545
  const enhancedError = new Error(contextMessage);
@@ -3691,6 +5551,15 @@ var SimpleJobExecutor = class {
3691
5551
  if (operations.length === 0) return buildErrorResult(job, /* @__PURE__ */ new Error("No operation generated from action"), startTime);
3692
5552
  const newOperation = operations[operations.length - 1];
3693
5553
  if (!isUndoRedo(action)) newOperation.skip = skip;
5554
+ if (deniedReason === void 0 && sourceOperation === void 0 && !executing.replayingAcceptedHistory && isSynthesized(action, newOperation, document.operations[scope] ?? [])) try {
5555
+ await signSynthesized(newOperation, action, {
5556
+ documentId: job.documentId,
5557
+ scope,
5558
+ branch: job.branch
5559
+ }, this.signer, signal);
5560
+ } catch (error) {
5561
+ return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
5562
+ }
3694
5563
  const resultingState = JSON.stringify({
3695
5564
  ...updatedDocument.state,
3696
5565
  header: updatedDocument.header
@@ -3791,7 +5660,7 @@ var SimpleJobExecutor = class {
3791
5660
  * apply and its own reasons for it.
3792
5661
  * - A positional or replayed run carries skips and re-appended operations,
3793
5662
  * 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
5663
+ * - UNDO, REDO and NOOP-with-skip each invalidate the write cache to
3795
5664
  * force a full-history rebuild, so they cannot be reduced against state
3796
5665
  * threaded from the write before them.
3797
5666
  * - The auth scope decides later writes against the policy earlier ones
@@ -3809,7 +5678,7 @@ var SimpleJobExecutor = class {
3809
5678
  if (scope === "auth" || scope === "document") return false;
3810
5679
  return writes.every((write) => {
3811
5680
  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";
5681
+ return write.skip === 0 && write.deniedReason === void 0 && write.sourceOperation === void 0 && !DOCUMENT_SCOPE_ACTIONS.has(type) && !isUndoRedo(write.action) && type !== "NOOP";
3813
5682
  });
3814
5683
  }
3815
5684
  /**
@@ -3932,13 +5801,22 @@ var SimpleJobExecutor = class {
3932
5801
  }
3933
5802
  if (!backdated) return plain();
3934
5803
  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) {
5804
+ const nothingToMove = async () => {
3936
5805
  if (!this.featureFlags.authEnforcement) return plain();
3937
5806
  return this.evaluatePositioned(job, stores, this.appendedOperations(job, revisions.revision[job.scope] ?? 0), signal);
3938
- }
5807
+ };
5808
+ if (conflicting.length === 0) return nothingToMove();
3939
5809
  const nextIndex = revisions.revision[job.scope] ?? 0;
3940
5810
  let firstConflicting = conflicting[0].index;
3941
5811
  for (const operation of conflicting) if (operation.index < firstConflicting) firstConflicting = operation.index;
5812
+ const stored = (await stores.operationStore.getSince(job.documentId, job.scope, job.branch, firstConflicting - 1, void 0, void 0, signal)).results;
5813
+ const conflictingIds = new Set(conflicting.map((operation) => operation.id));
5814
+ const effective = garbageCollect(sortOperations(stored)).filter((operation) => !isGenesisOperation(operation));
5815
+ const firstMoving = effective.findIndex((operation) => conflictingIds.has(operation.id));
5816
+ const moving = firstMoving === -1 ? [] : effective.slice(firstMoving);
5817
+ if (moving.length === 0) return nothingToMove();
5818
+ let firstRetracted = moving[0].index;
5819
+ for (const operation of moving) firstRetracted = Math.min(firstRetracted, operation.index - operation.skip);
3942
5820
  const incoming = job.actions.map((action, i) => ({
3943
5821
  id: action.id,
3944
5822
  index: nextIndex + i,
@@ -3949,8 +5827,8 @@ var SimpleJobExecutor = class {
3949
5827
  }));
3950
5828
  const merged = reshuffleByTimestamp({
3951
5829
  index: nextIndex,
3952
- skip: retractionSkip(nextIndex, firstConflicting)
3953
- }, conflicting, incoming);
5830
+ skip: retractionSkip(nextIndex, firstRetracted)
5831
+ }, moving, incoming);
3954
5832
  stores.writeCache.invalidate(job.documentId, job.scope, job.branch);
3955
5833
  if (!this.featureFlags.authEnforcement) return {
3956
5834
  writes: merged.map((operation) => ({
@@ -4098,7 +5976,7 @@ var SimpleJobExecutor = class {
4098
5976
  stores.writeCache.invalidate(job.documentId, scope, job.branch);
4099
5977
  const result = await this.processActions(tail.map((operation, i) => ({
4100
5978
  action: operation.action,
4101
- skip: i === 0 ? retractionSkip(nextIndex, tail[0].index) : 0,
5979
+ skip: i === 0 ? retractionSkip(nextIndex, tail[0].index - tail[0].skip) : 0,
4102
5980
  sourceRemote: "",
4103
5981
  deniedReason: reevaluated[firstChange + i]
4104
5982
  })), {
@@ -4187,36 +6065,32 @@ var SimpleJobExecutor = class {
4187
6065
  error: new InvalidOperationTimestampError(job.documentId, scope, operation.timestampUtcMs, `operation (index: ${operation.index})`),
4188
6066
  duration: Date.now() - startTime
4189
6067
  };
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));
6068
+ const dropped = /* @__PURE__ */ new Set();
6069
+ const admitted = /* @__PURE__ */ new Set();
6070
+ let selection = await this.selectLoadWrites(job, job.operations, stores, signal);
6071
+ for (;;) {
6072
+ const unadmitted = selection.incomingOpsToApply.filter((operation) => !admitted.has(operation));
6073
+ if (unadmitted.length === 0) break;
6074
+ let refused;
4206
6075
  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;
6076
+ refused = await this.signatureAdmission.admitLoad(job, unadmitted, stores, signal);
6077
+ } catch (error) {
6078
+ return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
4210
6079
  }
6080
+ for (const operation of unadmitted) if (refused.has(operation)) dropped.add(operation);
6081
+ else admitted.add(operation);
6082
+ if (refused.size === 0) break;
6083
+ const remaining = job.operations.filter((operation) => !dropped.has(operation));
6084
+ if (remaining.length === 0) return {
6085
+ job,
6086
+ success: true,
6087
+ operations: [],
6088
+ operationsWithContext: [],
6089
+ duration: Date.now() - startTime
6090
+ };
6091
+ selection = await this.selectLoadWrites(job, remaining, stores, signal);
4211
6092
  }
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
- });
6093
+ const { nonSupersededOps, allOpsFromMinConflictingIndex, incomingOpsToApply } = selection;
4220
6094
  const existingOpsToReshuffle = monotonicAuthStream ? [] : nonSupersededOps.filter((operation) => !isGenesisOperation(operation));
4221
6095
  const actionIdCounts = /* @__PURE__ */ new Map();
4222
6096
  for (const operation of allOpsFromMinConflictingIndex) actionIdCounts.set(operation.action.id, (actionIdCounts.get(operation.action.id) ?? 0) + 1);
@@ -4237,14 +6111,6 @@ var SimpleJobExecutor = class {
4237
6111
  const logicalSkip = latestRevision - minLogicalIndex;
4238
6112
  if (logicalSkip > skipCount) skipCount = logicalSkip;
4239
6113
  }
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
6114
  if (incomingOpsToApply.length === 0) return {
4249
6115
  job,
4250
6116
  success: true,
@@ -4272,7 +6138,8 @@ var SimpleJobExecutor = class {
4272
6138
  ...operation,
4273
6139
  id: operation.id
4274
6140
  })));
4275
- for (const operation of reshuffledOperations) if (operation.action.type === "NOOP" && operation.skip === 0) operation.skip = 1;
6141
+ const incomingIds = new Set(incomingOpsToApply.map((operation) => operation.action.id));
6142
+ for (const operation of reshuffledOperations) if (operation.action.type === "NOOP" && operation.skip === 0 && incomingIds.has(operation.action.id)) operation.skip = 1;
4276
6143
  let deniedReasons;
4277
6144
  if (this.featureFlags.documentDecisions) try {
4278
6145
  deniedReasons = await evaluateByPosition(this.decisionModel, {
@@ -4324,6 +6191,71 @@ var SimpleJobExecutor = class {
4324
6191
  duration: Date.now() - startTime
4325
6192
  };
4326
6193
  }
6194
+ /** The conflicting window a load opens, and which of its operations are new. */
6195
+ async selectLoadWrites(job, operations, stores, signal) {
6196
+ const scope = job.scope;
6197
+ let minIncomingIndex = Number.POSITIVE_INFINITY;
6198
+ let minIncomingTimestamp = operations[0]?.timestampUtcMs || "";
6199
+ for (const operation of operations) {
6200
+ minIncomingIndex = Math.min(minIncomingIndex, operation.index);
6201
+ const ts = operation.timestampUtcMs || "";
6202
+ if (Date.parse(ts) < Date.parse(minIncomingTimestamp)) minIncomingTimestamp = ts;
6203
+ }
6204
+ let conflictingOps;
6205
+ try {
6206
+ conflictingOps = (await stores.operationStore.getConflicting(job.documentId, scope, job.branch, minIncomingTimestamp, void 0, signal)).results;
6207
+ } catch {
6208
+ conflictingOps = [];
6209
+ }
6210
+ let allOpsFromMinConflictingIndex = conflictingOps;
6211
+ if (conflictingOps.length > 0) {
6212
+ const minConflictingIndex = Math.min(...conflictingOps.map((op) => op.index));
6213
+ try {
6214
+ allOpsFromMinConflictingIndex = (await stores.operationStore.getSince(job.documentId, scope, job.branch, minConflictingIndex - 1, void 0, void 0, signal)).results;
6215
+ } catch {
6216
+ allOpsFromMinConflictingIndex = conflictingOps;
6217
+ }
6218
+ }
6219
+ const incomingActionIds = new Set(operations.map((op) => op.action.id));
6220
+ const supersededBy = (op) => {
6221
+ let nearest;
6222
+ 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;
6223
+ return nearest;
6224
+ };
6225
+ const undoneActionIds = /* @__PURE__ */ new Set();
6226
+ let predecessorBound = minIncomingIndex;
6227
+ const rowsByActionId = /* @__PURE__ */ new Map();
6228
+ for (const op of conflictingOps) if (incomingActionIds.has(op.action.id)) rowsByActionId.set(op.action.id, [...rowsByActionId.get(op.action.id) ?? [], op]);
6229
+ const liveOps = /* @__PURE__ */ new Set();
6230
+ for (const op of conflictingOps) if (supersededBy(op) === void 0) liveOps.add(op);
6231
+ for (const [actionId, rows] of rowsByActionId) {
6232
+ if (rows.some((row) => liveOps.has(row))) continue;
6233
+ if (rows.some((row) => supersededBy(row)?.action.type === "NOOP")) undoneActionIds.add(actionId);
6234
+ else predecessorBound = Math.min(predecessorBound, Math.max(...rows.map((row) => row.index)));
6235
+ }
6236
+ const nonSupersededOps = conflictingOps.filter((op) => {
6237
+ if (op.index < predecessorBound && !incomingActionIds.has(op.action.id)) return false;
6238
+ return liveOps.has(op);
6239
+ });
6240
+ let rewoundFrom = Number.POSITIVE_INFINITY;
6241
+ for (const op of nonSupersededOps) if (!isGenesisOperation(op)) rewoundFrom = Math.min(rewoundFrom, op.index - op.skip);
6242
+ const reappended = new Set(nonSupersededOps.map((op) => op.index));
6243
+ for (const op of allOpsFromMinConflictingIndex) if (op.index >= rewoundFrom && !reappended.has(op.index) && supersededBy(op) === void 0) nonSupersededOps.push(op);
6244
+ const existingActionIds = new Set(nonSupersededOps.map((op) => op.action.id));
6245
+ const seenIncomingActionIds = /* @__PURE__ */ new Set();
6246
+ const incomingOpsToApply = operations.filter((op) => {
6247
+ if (existingActionIds.has(op.action.id)) return false;
6248
+ if (undoneActionIds.has(op.action.id)) return false;
6249
+ if (seenIncomingActionIds.has(op.action.id)) return false;
6250
+ seenIncomingActionIds.add(op.action.id);
6251
+ return true;
6252
+ });
6253
+ return {
6254
+ nonSupersededOps,
6255
+ allOpsFromMinConflictingIndex,
6256
+ incomingOpsToApply
6257
+ };
6258
+ }
4327
6259
  accumulateResultOrReturnError(result, generatedOperations, operationsWithContext) {
4328
6260
  if (!result.success) return result;
4329
6261
  if (result.operations && result.operations.length > 0) generatedOperations.push(...result.operations);
@@ -4470,6 +6402,7 @@ var DocumentModelRegistry = class {
4470
6402
  //#endregion
4471
6403
  //#region src/storage/kysely/keyframe-store.ts
4472
6404
  var KyselyKeyframeStore = class KyselyKeyframeStore {
6405
+ db;
4473
6406
  trx;
4474
6407
  constructor(db) {
4475
6408
  this.db = db;
@@ -4484,19 +6417,28 @@ var KyselyKeyframeStore = class KyselyKeyframeStore {
4484
6417
  }
4485
6418
  async putKeyframe(documentId, scope, branch, revision, document, signal) {
4486
6419
  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();
6420
+ const write = async (trx) => {
6421
+ await acquirePurgeLocks(trx, [documentId], "shared");
6422
+ if ((await findPurged(trx, [documentId])).has(documentId)) return;
6423
+ await trx.insertInto("Keyframe").values({
6424
+ documentId,
6425
+ documentType: document.header.documentType,
6426
+ scope,
6427
+ branch,
6428
+ revision,
6429
+ document
6430
+ }).onConflict((oc) => oc.columns([
6431
+ "documentId",
6432
+ "scope",
6433
+ "branch",
6434
+ "revision"
6435
+ ]).doUpdateSet({ document })).execute();
6436
+ };
6437
+ if (this.trx) {
6438
+ await write(this.trx);
6439
+ return;
6440
+ }
6441
+ await this.db.transaction().execute(write);
4500
6442
  }
4501
6443
  async findNearestKeyframe(documentId, scope, branch, targetRevision, signal) {
4502
6444
  if (signal?.aborted) throw new Error("Operation aborted");
@@ -4554,6 +6496,11 @@ function paginateRows(rows, paging, cursorOf, toItem, refetch) {
4554
6496
  //#endregion
4555
6497
  //#region src/storage/txn.ts
4556
6498
  var AtomicTransaction = class {
6499
+ documentId;
6500
+ documentType;
6501
+ scope;
6502
+ branch;
6503
+ baseRevision;
4557
6504
  operations = [];
4558
6505
  constructor(documentId, documentType, scope, branch, baseRevision) {
4559
6506
  this.documentId = documentId;
@@ -4587,6 +6534,11 @@ var AtomicTransaction = class {
4587
6534
  //#endregion
4588
6535
  //#region src/storage/kysely/store.ts
4589
6536
  var _UniqueConstraintContext = class extends Error {
6537
+ documentId;
6538
+ scope;
6539
+ branch;
6540
+ revision;
6541
+ stagedOps;
4590
6542
  constructor(documentId, scope, branch, revision, stagedOps) {
4591
6543
  super("unique constraint");
4592
6544
  this.documentId = documentId;
@@ -4598,16 +6550,20 @@ var _UniqueConstraintContext = class extends Error {
4598
6550
  }
4599
6551
  };
4600
6552
  var KyselyOperationStore = class KyselyOperationStore {
6553
+ db;
4601
6554
  trx;
6555
+ liveIds;
4602
6556
  constructor(db) {
4603
6557
  this.db = db;
4604
6558
  }
4605
6559
  get queryExecutor() {
4606
6560
  return this.trx ?? this.db;
4607
6561
  }
4608
- withTransaction(trx) {
6562
+ /** `liveIds`: ids the transaction read untombstoned under its shared lock. */
6563
+ withTransaction(trx, liveIds) {
4609
6564
  const instance = new KyselyOperationStore(this.db);
4610
6565
  instance.trx = trx;
6566
+ instance.liveIds = liveIds;
4611
6567
  return instance;
4612
6568
  }
4613
6569
  async apply(documentId, documentType, scope, branch, revision, fn, signal, condition) {
@@ -4652,6 +6608,8 @@ var KyselyOperationStore = class KyselyOperationStore {
4652
6608
  await fn(atomicTxn);
4653
6609
  const operations = atomicTxn.getOperations();
4654
6610
  if (operations.length === 0) return [];
6611
+ const purgedHead = this.liveIds?.has(documentId) ? void 0 : await this.refusePurgedAppend(trx, documentId, operations);
6612
+ if (purgedHead !== void 0) return purgedHead;
4655
6613
  if (condition) await this.acquireStreamLocks(trx, documentId, scope, branch, condition);
4656
6614
  const latestOp = await trx.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).orderBy("index", "desc").limit(1).executeTakeFirst();
4657
6615
  const currentRevision = latestOp ? latestOp.index : -1;
@@ -4697,12 +6655,13 @@ var KyselyOperationStore = class KyselyOperationStore {
4697
6655
  * otherwise read a snapshot taken before the locks were held.
4698
6656
  */
4699
6657
  async acquireStreamLocks(trx, documentId, scope, branch, condition) {
4700
- const keys = new Set([`${documentId}:${scope}:${branch}`]);
6658
+ const keys = /* @__PURE__ */ new Set([`${documentId}:${scope}:${branch}`]);
4701
6659
  for (const stream of condition.streams) keys.add(`${stream.documentId}:${stream.scope}:${stream.branch}`);
6660
+ const sortedKeys = sql.join([...keys].sort());
4702
6661
  await sql`
4703
6662
  with ordered as materialized (
4704
6663
  select key
4705
- from unnest(array[${sql.join([...keys].sort())}]::text[]) with ordinality as t(key, ord)
6664
+ from unnest(array[${sortedKeys}]::text[]) with ordinality as t(key, ord)
4706
6665
  order by ord
4707
6666
  )
4708
6667
  select pg_advisory_xact_lock(hashtext(key)) from ordered
@@ -4766,6 +6725,24 @@ var KyselyOperationStore = class KyselyOperationStore {
4766
6725
  }
4767
6726
  return storedRows.map((row) => this.rowToOperation(row));
4768
6727
  }
6728
+ /** A purged stream refuses appends; a marker gets the stored one back. */
6729
+ async refusePurgedAppend(trx, documentId, operations) {
6730
+ if (!(await findPurged(trx, [documentId])).has(documentId)) return;
6731
+ if (!operations.every((operation) => this.isMarker(operation))) throw new DocumentPurgedError(documentId);
6732
+ return (await trx.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where(sql`action->>'type' = ${PURGE_DOCUMENT}`).orderBy("index", "asc").execute()).map((row) => this.rowToOperation(row));
6733
+ }
6734
+ isMarker(operation) {
6735
+ return this.actionType(operation) === PURGE_DOCUMENT;
6736
+ }
6737
+ actionType(operation) {
6738
+ let action = operation.action;
6739
+ if (typeof action === "string") try {
6740
+ action = JSON.parse(action);
6741
+ } catch {
6742
+ return;
6743
+ }
6744
+ if (typeof action === "object" && action !== null && "type" in action && typeof action.type === "string") return action.type;
6745
+ }
4769
6746
  /** True when the staged write creates a document rather than appending to one. */
4770
6747
  isCreate(operations) {
4771
6748
  for (const operation of operations) {
@@ -4779,6 +6756,14 @@ var KyselyOperationStore = class KyselyOperationStore {
4779
6756
  }
4780
6757
  return false;
4781
6758
  }
6759
+ /**
6760
+ * The paging cursor here encodes the index to resume from (one past the
6761
+ * last row returned), not the last row's own index. This keeps "0" an
6762
+ * unambiguous start-of-stream sentinel even when a page ends at index 0
6763
+ * (e.g. `limit: 1` on a fresh stream), which would otherwise make
6764
+ * `nextCursor` equal the start cursor and loop a caller that walks pages
6765
+ * forever.
6766
+ */
4782
6767
  async getSince(documentId, scope, branch, revision, filter, paging, signal) {
4783
6768
  throwIfAborted(signal);
4784
6769
  let query = this.queryExecutor.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("index", ">", revision).orderBy("index", "asc");
@@ -4793,10 +6778,10 @@ var KyselyOperationStore = class KyselyOperationStore {
4793
6778
  }
4794
6779
  if (paging) {
4795
6780
  const cursorValue = Number.parseInt(paging.cursor, 10);
4796
- if (cursorValue > 0) query = query.where("index", ">", cursorValue);
6781
+ if (Number.isFinite(cursorValue) && cursorValue > 0) query = query.where("index", ">=", cursorValue);
4797
6782
  if (paging.limit) query = query.limit(paging.limit + 1);
4798
6783
  }
4799
- return paginateRows(await query.execute(), paging, (row) => row.index, (row) => this.rowToOperation(row), (cursor, limit) => this.getSince(documentId, scope, branch, revision, filter, {
6784
+ return paginateRows(await query.execute(), paging, (row) => row.index + 1, (row) => this.rowToOperation(row), (cursor, limit) => this.getSince(documentId, scope, branch, revision, filter, {
4800
6785
  cursor,
4801
6786
  limit
4802
6787
  }, signal));
@@ -4814,26 +6799,28 @@ var KyselyOperationStore = class KyselyOperationStore {
4814
6799
  limit
4815
6800
  }, signal));
4816
6801
  }
6802
+ /**
6803
+ * The paging cursor here encodes the index to resume from (one past the
6804
+ * last row returned), not the last row's own index, for the same reason as
6805
+ * `getSince`: a page ending at index 0 must not produce a cursor that is
6806
+ * indistinguishable from the start-of-stream sentinel.
6807
+ */
4817
6808
  async getConflicting(documentId, scope, branch, minTimestamp, paging, signal) {
4818
6809
  throwIfAborted(signal);
4819
6810
  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
6811
  if (paging) {
4821
6812
  const cursorValue = Number.parseInt(paging.cursor, 10);
4822
- if (cursorValue > 0) query = query.where("index", ">", cursorValue);
6813
+ if (Number.isFinite(cursorValue) && cursorValue > 0) query = query.where("index", ">=", cursorValue);
4823
6814
  if (paging.limit) query = query.limit(paging.limit + 1);
4824
6815
  }
4825
- return paginateRows(await query.execute(), paging, (row) => row.index, (row) => this.rowToOperation(row), (cursor, limit) => this.getConflicting(documentId, scope, branch, minTimestamp, {
6816
+ return paginateRows(await query.execute(), paging, (row) => row.index + 1, (row) => this.rowToOperation(row), (cursor, limit) => this.getConflicting(documentId, scope, branch, minTimestamp, {
4826
6817
  cursor,
4827
6818
  limit
4828
6819
  }, signal));
4829
6820
  }
4830
6821
  async getRevisions(documentId, branch, signal) {
4831
6822
  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();
6823
+ 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
6824
  const latest = await this.queryExecutor.selectFrom("Operation").select((eb) => eb.fn.max("timestampUtcMs").as("latestTimestamp")).where("documentId", "=", documentId).where("branch", "=", branch).executeTakeFirst();
4838
6825
  const revision = {};
4839
6826
  for (const row of scopeRevisions) revision[row.scope] = row.index + 1;
@@ -4846,6 +6833,20 @@ var KyselyOperationStore = class KyselyOperationStore {
4846
6833
  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
6834
  return latest?.latestTimestamp ? new Date(latest.latestTimestamp).toISOString() : void 0;
4848
6835
  }
6836
+ async findOperationIds(documentId, scope, branch, opIds, signal) {
6837
+ throwIfAborted(signal);
6838
+ if (opIds.length === 0) return /* @__PURE__ */ new Set();
6839
+ const rows = await this.queryExecutor.selectFrom("Operation").select("opId").distinct().where("opId", "in", opIds).where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).execute();
6840
+ return new Set(rows.map((row) => row.opId));
6841
+ }
6842
+ async getOperationsByIds(documentId, scope, branch, opIds, signal) {
6843
+ throwIfAborted(signal);
6844
+ if (opIds.length === 0) return [];
6845
+ 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();
6846
+ const latest = /* @__PURE__ */ new Map();
6847
+ for (const row of rows) latest.set(row.opId, this.rowToOperation(row));
6848
+ return [...latest.values()];
6849
+ }
4849
6850
  rowToOperation(row) {
4850
6851
  return {
4851
6852
  index: row.index,
@@ -4939,8 +6940,8 @@ function createForwardingPoolInstrumentation(name) {
4939
6940
  }
4940
6941
  //#endregion
4941
6942
  //#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) {
6943
+ var _001_create_operation_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$25 });
6944
+ async function up$25(db) {
4944
6945
  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
6946
  "documentId",
4946
6947
  "scope",
@@ -4965,8 +6966,8 @@ async function up$18(db) {
4965
6966
  }
4966
6967
  //#endregion
4967
6968
  //#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) {
6969
+ var _002_create_keyframe_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$24 });
6970
+ async function up$24(db) {
4970
6971
  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
6972
  "documentId",
4972
6973
  "scope",
@@ -4982,14 +6983,14 @@ async function up$17(db) {
4982
6983
  }
4983
6984
  //#endregion
4984
6985
  //#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) {
6986
+ var _003_create_document_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$23 });
6987
+ async function up$23(db) {
4987
6988
  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
6989
  }
4989
6990
  //#endregion
4990
6991
  //#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) {
6992
+ var _004_create_document_relationship_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$22 });
6993
+ async function up$22(db) {
4993
6994
  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
6995
  "sourceId",
4995
6996
  "targetId",
@@ -5001,14 +7002,14 @@ async function up$15(db) {
5001
7002
  }
5002
7003
  //#endregion
5003
7004
  //#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) {
7005
+ var _005_create_indexer_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$21 });
7006
+ async function up$21(db) {
5006
7007
  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
7008
  }
5008
7009
  //#endregion
5009
7010
  //#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) {
7011
+ var _006_create_document_snapshot_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$20 });
7012
+ async function up$20(db) {
5012
7013
  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
7014
  "documentId",
5014
7015
  "scope",
@@ -5029,8 +7030,8 @@ async function up$13(db) {
5029
7030
  }
5030
7031
  //#endregion
5031
7032
  //#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) {
7033
+ var _007_create_slug_mapping_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$19 });
7034
+ async function up$19(db) {
5034
7035
  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
7036
  "documentId",
5036
7037
  "scope",
@@ -5040,14 +7041,14 @@ async function up$12(db) {
5040
7041
  }
5041
7042
  //#endregion
5042
7043
  //#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) {
7044
+ var _008_create_view_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$18 });
7045
+ async function up$18(db) {
5045
7046
  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
7047
  }
5047
7048
  //#endregion
5048
7049
  //#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) {
7050
+ var _009_create_operation_index_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$17 });
7051
+ async function up$17(db) {
5051
7052
  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
7053
  await db.schema.createIndex("idx_document_collections_collectionId").on("document_collections").column("collectionId").execute();
5053
7054
  await db.schema.createIndex("idx_doc_collections_collection_range").on("document_collections").columns(["collectionId", "joinedOrdinal"]).execute();
@@ -5061,8 +7062,8 @@ async function up$10(db) {
5061
7062
  }
5062
7063
  //#endregion
5063
7064
  //#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) {
7065
+ var _010_create_sync_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$16 });
7066
+ async function up$16(db) {
5066
7067
  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
7068
  await db.schema.createIndex("idx_sync_remotes_collection").on("sync_remotes").column("collection_id").execute();
5068
7069
  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 +7071,8 @@ async function up$9(db) {
5070
7071
  }
5071
7072
  //#endregion
5072
7073
  //#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) {
7074
+ var _011_add_cursor_type_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$15 });
7075
+ async function up$15(db) {
5075
7076
  await db.deleteFrom("sync_cursors").where("remote_name", "like", "outbox::%").execute();
5076
7077
  await db.deleteFrom("sync_remotes").where("name", "like", "outbox::%").execute();
5077
7078
  await db.schema.dropTable("sync_cursors").execute();
@@ -5080,64 +7081,64 @@ async function up$8(db) {
5080
7081
  }
5081
7082
  //#endregion
5082
7083
  //#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) {
7084
+ var _012_add_source_remote_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$14 });
7085
+ async function up$14(db) {
5085
7086
  await db.schema.alterTable("operation_index_operations").addColumn("sourceRemote", "text", (col) => col.notNull().defaultTo("")).execute();
5086
7087
  }
5087
7088
  //#endregion
5088
7089
  //#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) {
7090
+ var _013_create_sync_dead_letters_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$13 });
7091
+ async function up$13(db) {
5091
7092
  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
7093
  await db.schema.createIndex("idx_sync_dead_letters_remote").on("sync_dead_letters").column("remote_name").execute();
5093
7094
  }
5094
7095
  //#endregion
5095
7096
  //#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) {
7097
+ var _014_create_processor_cursor_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$12 });
7098
+ async function up$12(db) {
5098
7099
  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
7100
  }
5100
7101
  //#endregion
5101
7102
  //#region src/storage/migrations/015_add_operation_denied_reason.ts
5102
7103
  var _015_add_operation_denied_reason_exports = /* @__PURE__ */ __exportAll({
5103
- down: () => down$4,
5104
- up: () => up$4
7104
+ down: () => down$11,
7105
+ up: () => up$11
5105
7106
  });
5106
7107
  /**
5107
7108
  * Records why authorization refused an operation. Separate from `error` so a
5108
7109
  * denial is distinguishable from a reducer failure without matching on a
5109
7110
  * message. Null for every operation written before decisions were enforced.
5110
7111
  */
5111
- async function up$4(db) {
7112
+ async function up$11(db) {
5112
7113
  await db.schema.alterTable("Operation").addColumn("deniedReason", "text").execute();
5113
7114
  await db.schema.alterTable("operation_index_operations").addColumn("deniedReason", "text").execute();
5114
7115
  }
5115
- async function down$4(db) {
7116
+ async function down$11(db) {
5116
7117
  await db.schema.alterTable("operation_index_operations").dropColumn("deniedReason").execute();
5117
7118
  await db.schema.alterTable("Operation").dropColumn("deniedReason").execute();
5118
7119
  }
5119
7120
  //#endregion
5120
7121
  //#region src/storage/migrations/016_add_dead_letter_error_type.ts
5121
7122
  var _016_add_dead_letter_error_type_exports = /* @__PURE__ */ __exportAll({
5122
- down: () => down$3,
5123
- up: () => up$3
7123
+ down: () => down$10,
7124
+ up: () => up$10
5124
7125
  });
5125
7126
  /**
5126
7127
  * The classification a dead letter falls into, stored because it decides whether
5127
7128
  * the document stays quarantined and the in-memory error is gone after a restart.
5128
7129
  * Defaulted rather than nullable, so a pre-existing row rehydrates.
5129
7130
  */
5130
- async function up$3(db) {
7131
+ async function up$10(db) {
5131
7132
  await db.schema.alterTable("sync_dead_letters").addColumn("error_type", "text", (col) => col.notNull().defaultTo("UNCLASSIFIED")).execute();
5132
7133
  }
5133
- async function down$3(db) {
7134
+ async function down$10(db) {
5134
7135
  await db.schema.alterTable("sync_dead_letters").dropColumn("error_type").execute();
5135
7136
  }
5136
7137
  //#endregion
5137
7138
  //#region src/storage/migrations/017_create_group_references.ts
5138
7139
  var _017_create_group_references_exports = /* @__PURE__ */ __exportAll({
5139
- down: () => down$2,
5140
- up: () => up$2
7140
+ down: () => down$9,
7141
+ up: () => up$9
5141
7142
  });
5142
7143
  /**
5143
7144
  * One row per (document, group) reference ever discovered from an auth
@@ -5148,18 +7149,18 @@ var _017_create_group_references_exports = /* @__PURE__ */ __exportAll({
5148
7149
  * requires (sync), and by groupId for the documents a group change affects
5149
7150
  * (re-evaluation).
5150
7151
  */
5151
- async function up$2(db) {
7152
+ async function up$9(db) {
5152
7153
  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
7154
  await db.schema.createIndex("idx_group_references_groupId").on("group_references").column("groupId").execute();
5154
7155
  }
5155
- async function down$2(db) {
7156
+ async function down$9(db) {
5156
7157
  await db.schema.dropTable("group_references").execute();
5157
7158
  }
5158
7159
  //#endregion
5159
7160
  //#region src/storage/migrations/018_add_sync_remote_bound_address.ts
5160
7161
  var _018_add_sync_remote_bound_address_exports = /* @__PURE__ */ __exportAll({
5161
- down: () => down$1,
5162
- up: () => up$1
7162
+ down: () => down$8,
7163
+ up: () => up$8
5163
7164
  });
5164
7165
  /**
5165
7166
  * The address a sync channel is bound to, so a channel created by one subject
@@ -5170,17 +7171,17 @@ var _018_add_sync_remote_bound_address_exports = /* @__PURE__ */ __exportAll({
5170
7171
  * until its first authenticated poll adopts it. A default would claim them all
5171
7172
  * for one address.
5172
7173
  */
5173
- async function up$1(db) {
7174
+ async function up$8(db) {
5174
7175
  await db.schema.alterTable("sync_remotes").addColumn("bound_address", "text").execute();
5175
7176
  }
5176
- async function down$1(db) {
7177
+ async function down$8(db) {
5177
7178
  await db.schema.alterTable("sync_remotes").dropColumn("bound_address").execute();
5178
7179
  }
5179
7180
  //#endregion
5180
7181
  //#region src/storage/migrations/019_require_action_id.ts
5181
7182
  var _019_require_action_id_exports = /* @__PURE__ */ __exportAll({
5182
- down: () => down,
5183
- up: () => up
7183
+ down: () => down$7,
7184
+ up: () => up$7
5184
7185
  });
5185
7186
  /**
5186
7187
  * Makes an operation whose action carries no id physically unstorable.
@@ -5207,7 +7208,7 @@ var _019_require_action_id_exports = /* @__PURE__ */ __exportAll({
5207
7208
  * The empty string is rejected alongside null. It derives the same colliding
5208
7209
  * operation id as an absent id, so admitting it would leave the hole open.
5209
7210
  */
5210
- async function up(db) {
7211
+ async function up$7(db) {
5211
7212
  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
7213
  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
7214
  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 +7220,118 @@ async function up(db) {
5219
7220
  * their operations are now known by, and reverting them would reintroduce the
5220
7221
  * collision the migration removed.
5221
7222
  */
5222
- async function down(db) {
7223
+ async function down$7(db) {
5223
7224
  await db.schema.alterTable("operation_index_operations").dropConstraint("action_must_have_id").execute();
5224
7225
  await db.schema.alterTable("Operation").dropConstraint("action_must_have_id").execute();
5225
7226
  }
5226
7227
  //#endregion
7228
+ //#region src/storage/migrations/020_add_snapshot_operation_ordinal.ts
7229
+ var _020_add_snapshot_operation_ordinal_exports = /* @__PURE__ */ __exportAll({
7230
+ down: () => down$6,
7231
+ up: () => up$6
7232
+ });
7233
+ /** Orders header row writes across scopes; `lastOperationIndex` is per scope. */
7234
+ async function up$6(db) {
7235
+ await db.schema.alterTable("DocumentSnapshot").addColumn("lastOperationOrdinal", "integer", (col) => col.notNull().defaultTo(0)).execute();
7236
+ }
7237
+ async function down$6(db) {
7238
+ await db.schema.alterTable("DocumentSnapshot").dropColumn("lastOperationOrdinal").execute();
7239
+ }
7240
+ //#endregion
7241
+ //#region src/storage/migrations/021_add_sync_remote_peer.ts
7242
+ var _021_add_sync_remote_peer_exports = /* @__PURE__ */ __exportAll({
7243
+ down: () => down$5,
7244
+ up: () => up$5
7245
+ });
7246
+ /**
7247
+ * The peer's manifest as canonical JSON, and when it was heard. Both null: not
7248
+ * yet heard. A time with no manifest: a silent peer, which gets the baselines.
7249
+ */
7250
+ async function up$5(db) {
7251
+ await db.schema.alterTable("sync_remotes").addColumn("peer_manifest", "text").addColumn("peer_manifest_at_utc_ms", "bigint").execute();
7252
+ }
7253
+ async function down$5(db) {
7254
+ await db.schema.alterTable("sync_remotes").dropColumn("peer_manifest").dropColumn("peer_manifest_at_utc_ms").execute();
7255
+ }
7256
+ //#endregion
7257
+ //#region src/storage/migrations/022_create_sync_holds.ts
7258
+ var _022_create_sync_holds_exports = /* @__PURE__ */ __exportAll({
7259
+ down: () => down$4,
7260
+ up: () => up$4
7261
+ });
7262
+ /** Documents held back from a remote whose peer cannot run their versions. */
7263
+ async function up$4(db) {
7264
+ 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", [
7265
+ "remote_name",
7266
+ "document_id",
7267
+ "branch"
7268
+ ]).execute();
7269
+ }
7270
+ async function down$4(db) {
7271
+ await db.schema.dropTable("sync_holds").execute();
7272
+ }
7273
+ //#endregion
7274
+ //#region src/storage/migrations/023_index_created_protocol_versions.ts
7275
+ var _023_index_created_protocol_versions_exports = /* @__PURE__ */ __exportAll({
7276
+ down: () => down$3,
7277
+ up: () => up$3
7278
+ });
7279
+ /**
7280
+ * One entry per document created with protocolVersions, keyed by a hash so an
7281
+ * oversized header cannot make its creation unstorable. Startup reads the
7282
+ * distinct values without visiting every document.
7283
+ */
7284
+ async function up$3(db) {
7285
+ 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();
7286
+ }
7287
+ async function down$3(db) {
7288
+ await db.schema.dropIndex("idx_operation_created_protocol_versions").execute();
7289
+ }
7290
+ //#endregion
7291
+ //#region src/storage/migrations/024_create_document_purges.ts
7292
+ var _024_create_document_purges_exports = /* @__PURE__ */ __exportAll({
7293
+ down: () => down$2,
7294
+ up: () => up$2
7295
+ });
7296
+ /** One row per purged document: the tombstone. Written once, never deleted. */
7297
+ async function up$2(db) {
7298
+ 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();
7299
+ }
7300
+ async function down$2(db) {
7301
+ await db.schema.dropTable("document_purges").execute();
7302
+ }
7303
+ //#endregion
7304
+ //#region src/storage/migrations/025_create_sync_received_markers.ts
7305
+ var _025_create_sync_received_markers_exports = /* @__PURE__ */ __exportAll({
7306
+ down: () => down$1,
7307
+ up: () => up$1
7308
+ });
7309
+ /** Received purge markers awaiting their outcome; removed with their remote. */
7310
+ async function up$1(db) {
7311
+ 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();
7312
+ }
7313
+ async function down$1(db) {
7314
+ await db.schema.dropTable("sync_received_markers").execute();
7315
+ }
7316
+ //#endregion
7317
+ //#region src/storage/migrations/026_create_sync_purge_refusals.ts
7318
+ var _026_create_sync_purge_refusals_exports = /* @__PURE__ */ __exportAll({
7319
+ down: () => down,
7320
+ up: () => up
7321
+ });
7322
+ /** A remote's refusal of a purge marker; payload-free, kept like the tombstone. */
7323
+ async function up(db) {
7324
+ 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", [
7325
+ "remote_name",
7326
+ "document_id",
7327
+ "branch"
7328
+ ]).execute();
7329
+ await db.schema.createIndex("idx_sync_purge_refusals_document").on("sync_purge_refusals").column("document_id").execute();
7330
+ }
7331
+ async function down(db) {
7332
+ await db.schema.dropTable("sync_purge_refusals").execute();
7333
+ }
7334
+ //#endregion
5227
7335
  //#region src/storage/migrations/migrator.ts
5228
7336
  const REACTOR_SCHEMA = "reactor";
5229
7337
  const migrations = {
@@ -5245,7 +7353,14 @@ const migrations = {
5245
7353
  "016_add_dead_letter_error_type": _016_add_dead_letter_error_type_exports,
5246
7354
  "017_create_group_references": _017_create_group_references_exports,
5247
7355
  "018_add_sync_remote_bound_address": _018_add_sync_remote_bound_address_exports,
5248
- "019_require_action_id": _019_require_action_id_exports
7356
+ "019_require_action_id": _019_require_action_id_exports,
7357
+ "020_add_snapshot_operation_ordinal": _020_add_snapshot_operation_ordinal_exports,
7358
+ "021_add_sync_remote_peer": _021_add_sync_remote_peer_exports,
7359
+ "022_create_sync_holds": _022_create_sync_holds_exports,
7360
+ "023_index_created_protocol_versions": _023_index_created_protocol_versions_exports,
7361
+ "024_create_document_purges": _024_create_document_purges_exports,
7362
+ "025_create_sync_received_markers": _025_create_sync_received_markers_exports,
7363
+ "026_create_sync_purge_refusals": _026_create_sync_purge_refusals_exports
5249
7364
  };
5250
7365
  var ProgrammaticMigrationProvider = class {
5251
7366
  getMigrations() {
@@ -5303,8 +7418,155 @@ async function getMigrationStatus(db, schema = REACTOR_SCHEMA) {
5303
7418
  }
5304
7419
  //#endregion
5305
7420
  //#region src/core/drive-container-types.ts
5306
- const DEFAULT_DRIVE_CONTAINER_TYPES = new Set(["powerhouse/document-drive", "powerhouse/reactor-drive"]);
7421
+ const DEFAULT_DRIVE_CONTAINER_TYPES = /* @__PURE__ */ new Set(["powerhouse/document-drive", "powerhouse/reactor-drive"]);
7422
+ //#endregion
7423
+ //#region src/executor/worker/error-info.ts
7424
+ const MAX_STACK_LENGTH = 8192;
7425
+ const MAX_CAUSE_DEPTH = 8;
7426
+ function toErrorInfo(err, depth = 0) {
7427
+ if (depth > MAX_CAUSE_DEPTH) return {
7428
+ name: "Error",
7429
+ message: "cause chain truncated"
7430
+ };
7431
+ if (err instanceof Error) {
7432
+ const info = {
7433
+ name: err.name || "Error",
7434
+ message: err.message
7435
+ };
7436
+ if (typeof err.stack === "string") info.stack = err.stack.length > MAX_STACK_LENGTH ? err.stack.slice(0, MAX_STACK_LENGTH) : err.stack;
7437
+ if (err.cause !== void 0) info.cause = toErrorInfo(err.cause, depth + 1);
7438
+ const documentId = err.documentId;
7439
+ if (typeof documentId === "string") info.documentId = documentId;
7440
+ return info;
7441
+ }
7442
+ if (typeof err === "string") return {
7443
+ name: "Error",
7444
+ message: err
7445
+ };
7446
+ if (err && typeof err === "object") {
7447
+ const record = err;
7448
+ return {
7449
+ name: typeof record.name === "string" ? record.name : "Error",
7450
+ message: typeof record.message === "string" ? record.message : safeStringify(err)
7451
+ };
7452
+ }
7453
+ return {
7454
+ name: "Error",
7455
+ message: safeStringify(err)
7456
+ };
7457
+ }
7458
+ function safeStringify(value) {
7459
+ if (value === null || value === void 0) return String(value);
7460
+ if (typeof value === "string") return value;
7461
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
7462
+ try {
7463
+ return JSON.stringify(value);
7464
+ } catch {
7465
+ return "[unserializable]";
7466
+ }
7467
+ }
7468
+ function fromErrorInfo(info) {
7469
+ const err = new Error(info.message);
7470
+ Object.defineProperty(err, "name", {
7471
+ value: info.name,
7472
+ configurable: true,
7473
+ writable: true
7474
+ });
7475
+ if (info.stack !== void 0) err.stack = info.stack;
7476
+ if (info.documentId !== void 0) Object.defineProperty(err, "documentId", {
7477
+ value: info.documentId,
7478
+ configurable: true,
7479
+ writable: true
7480
+ });
7481
+ if (info.cause !== void 0) Object.defineProperty(err, "cause", {
7482
+ value: fromErrorInfo(info.cause),
7483
+ configurable: true,
7484
+ writable: true
7485
+ });
7486
+ return err;
7487
+ }
7488
+ //#endregion
7489
+ //#region src/executor/worker/sanitize.ts
7490
+ const MAX_DEPTH = 8;
7491
+ function errorToInfo(err) {
7492
+ return toErrorInfo(err);
7493
+ }
7494
+ function sanitizeArg(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
7495
+ if (depth > MAX_DEPTH) return "[Truncated]";
7496
+ if (value === null || value === void 0) return null;
7497
+ if (typeof value === "boolean") return value;
7498
+ if (typeof value === "number") return value;
7499
+ if (typeof value === "string") return value;
7500
+ if (typeof value === "bigint") return value.toString();
7501
+ if (typeof value === "function" || typeof value === "symbol") return null;
7502
+ if (value instanceof Error) return toErrorInfo(value);
7503
+ if (value instanceof Date) return value.toISOString();
7504
+ if (value instanceof Map) {
7505
+ const obj = {};
7506
+ for (const [k, v] of value.entries()) obj[String(k)] = sanitizeArg(v, depth + 1, seen);
7507
+ return obj;
7508
+ }
7509
+ if (value instanceof Set) {
7510
+ const arr = [];
7511
+ for (const v of value.values()) arr.push(sanitizeArg(v, depth + 1, seen));
7512
+ return arr;
7513
+ }
7514
+ if (Array.isArray(value)) {
7515
+ if (seen.has(value)) return "[Circular]";
7516
+ seen.add(value);
7517
+ const result = value.map((v) => sanitizeArg(v, depth + 1, seen));
7518
+ seen.delete(value);
7519
+ return result;
7520
+ }
7521
+ if (typeof value === "object") {
7522
+ const proto = Object.getPrototypeOf(value);
7523
+ if (proto !== Object.prototype && proto !== null) return { __nonClonable: value.constructor?.name || "Unknown" };
7524
+ if (seen.has(value)) return "[Circular]";
7525
+ seen.add(value);
7526
+ const result = {};
7527
+ for (const [k, v] of Object.entries(value)) result[k] = sanitizeArg(v, depth + 1, seen);
7528
+ seen.delete(value);
7529
+ return result;
7530
+ }
7531
+ return null;
7532
+ }
7533
+ //#endregion
7534
+ //#region src/executor/worker/forwarding-logger.ts
7535
+ function createForwardingLogger(post, level = "info", tags = []) {
7536
+ function safePost(msg) {
7537
+ try {
7538
+ post(msg);
7539
+ } catch {
7540
+ try {
7541
+ process.stderr.write(`[forwarding-logger] postMessage failed: ${msg.level} ${msg.message}\n`);
7542
+ } catch {}
7543
+ }
7544
+ }
7545
+ function emit(wireLevel, message, args) {
7546
+ const sanitizedArgs = args.map((a) => sanitizeArg(a));
7547
+ safePost({
7548
+ type: "log",
7549
+ level: wireLevel,
7550
+ message,
7551
+ args: tags.length > 0 ? [sanitizeArg({ tags }), ...sanitizedArgs] : sanitizedArgs,
7552
+ timestamp: Date.now()
7553
+ });
7554
+ }
7555
+ return {
7556
+ level,
7557
+ verbose: (message, ...args) => emit("debug", message, args),
7558
+ debug: (message, ...args) => emit("debug", message, args),
7559
+ info: (message, ...args) => emit("info", message, args),
7560
+ warn: (message, ...args) => emit("warn", message, args),
7561
+ error: (message, ...args) => emit("error", message, args),
7562
+ errorHandler: ((...data) => {
7563
+ const first = data[0];
7564
+ emit("error", typeof first === "string" ? first : "", data.slice(1));
7565
+ }),
7566
+ child: (childTags) => createForwardingLogger(post, level, [...tags, ...childTags])
7567
+ };
7568
+ }
5307
7569
  //#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 };
7570
+ export { selectDecisionModel as $, ProbeSettler as A, matchesScope as At, JobExecutorEventTypes as B, KyselyWriteCache as C, RelationshipNotFoundError as Ct, acquirePurgeLocks as D, UpgradePreconditionFailedError as Dt, PURGE_NS as E, UnsupportedStoredProtocolError as Et, snapshotFunctionsFor as F, GATED_DOCUMENT_ACTIONS as G, DuplicateModuleError as H, DocumentMetaCache as I, createEmptyConsistencyToken as J, createConsistencyToken as K, CollectionMembershipCache as L, createKyselyWatermarkProbe as M, throwIfAborted as Mt, describeWaitingSessions as N, yieldToMain as Nt, findPurged as O, EventBusAggregateError as Ot, parseSnapshot as P, __exportAll as Pt, decideAtHead as Q, DEFAULT_DEFERRED_JOB_TTL_MS as R, EventBus as S, PurgeTooLargeError as St, PURGE_LOCK_BUCKETS as T, UnsupportedProtocolVersionError as Tt, InvalidModuleError as U, DuplicateManifestError as V, ModuleNotFoundError as W, summarizeSubmittedActions as X, submittedActionIds as Y, targetDocumentId as Z, KyselyExecutionScope as _, DocumentPurgedError as _t, DEFAULT_DRIVE_CONTAINER_TYPES as a, AppendConditionFailedError as at, appliedDelete as b, InvalidOperationTimestampError as bt, runMigrations as c, DuplicateOperationError as ct, KyselyOperationStore as d, AuthEnforcementDisabledError as dt, documentDecisionModel as et, KyselyKeyframeStore as f, AuthTimestampNotMonotonicError as ft, PassthroughSigner as g, DocumentNotFoundError as gt, verifyActionSignature as h, DocumentNotDeletedError as ht, fromErrorInfo as i, APPEND_CONDITION_FAILED_PREFIX as it, SettledWatermark as j, parsePagingOptions as jt, listPurged as k, ReactorEventTypes as kt, createForwardingPoolInstrumentation as l, OptimisticLockError as lt, SimpleJobExecutor as m, DocumentDeletedError as mt, errorToInfo as n, buildDecisionModel as nt, REACTOR_SCHEMA as o, DocumentAlreadyExistsError as ot, DocumentModelRegistry as p, AuthorizationDeniedError as pt, createDocumentFromAction as q, sanitizeArg as r, DriveCollectionId as rt, getMigrationStatus as s, DocumentExistence as st, createForwardingLogger as t, authDecisionModel as tt, instrumentPgPool as u, RevisionMismatchError as ut, DEFAULT_PURGE_DELETE_BATCH as v, ExcessiveReshuffleError as vt, KyselyOperationIndex as w, ReservedActionError as wt, resolveFeatureFlags as x, InvalidSignatureError as xt, KyselyDocumentPurger as y, GroupInUseError as yt, DEFAULT_MAX_PURGE_OPERATIONS as z };
5309
7571
 
5310
- //# sourceMappingURL=drive-container-types-yZrksiJR.js.map
7572
+ //# sourceMappingURL=forwarding-logger-DMqDs9tE.js.map