@powerhousedao/reactor 6.2.3-dev.43 → 6.2.3-dev.45

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 (30) hide show
  1. package/dist/{document-indexer-B3yKal-y.js → document-indexer-lfgXu9ph.js} +13 -4
  2. package/dist/document-indexer-lfgXu9ph.js.map +1 -0
  3. package/dist/drive-container-types-CifLQjd3.js +3766 -0
  4. package/dist/drive-container-types-CifLQjd3.js.map +1 -0
  5. package/dist/entry.js +99 -3
  6. package/dist/entry.js.map +1 -1
  7. package/dist/index-81SYQKda.d.ts +6353 -0
  8. package/dist/index-81SYQKda.d.ts.map +1 -0
  9. package/dist/index.d.ts +16 -6339
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +53 -49
  12. package/dist/index.js.map +1 -1
  13. package/dist/load-spec-f2ppXyKZ.js +3848 -0
  14. package/dist/load-spec-f2ppXyKZ.js.map +1 -0
  15. package/dist/projection-entry.js +2 -3
  16. package/dist/projection-entry.js.map +1 -1
  17. package/dist/{projection-shard-manager-BZPCQK-4.js → projection-shard-manager-XMam-rTs.js} +2 -2
  18. package/dist/{projection-shard-manager-BZPCQK-4.js.map → projection-shard-manager-XMam-rTs.js.map} +1 -1
  19. package/dist/rpc.d.ts +318 -0
  20. package/dist/rpc.d.ts.map +1 -0
  21. package/dist/rpc.js +570 -0
  22. package/dist/rpc.js.map +1 -0
  23. package/dist/{worker-handle-CefOwokd.js → worker-handle-BKuaSp9T.js} +2 -2
  24. package/dist/{worker-handle-CefOwokd.js.map → worker-handle-BKuaSp9T.js.map} +1 -1
  25. package/package.json +10 -4
  26. package/dist/build-worker-executor-Bonu2jUB.js +0 -105
  27. package/dist/build-worker-executor-Bonu2jUB.js.map +0 -1
  28. package/dist/document-indexer-B3yKal-y.js.map +0 -1
  29. package/dist/forwarding-logger-BKDP4JnD.js +0 -7593
  30. package/dist/forwarding-logger-BKDP4JnD.js.map +0 -1
@@ -0,0 +1,3848 @@
1
+ import { DOCUMENT_DELETED_REASON, DOCUMENT_SCOPE_ACTION_TYPES, DowngradeNotSupportedError as DowngradeNotSupportedError$1, PURGE_DOCUMENT, appendWithoutApplying, applyDeleteDocumentAction, applyDeleteDocumentAction as applyDeleteDocumentAction$1, applyUpgradeDocumentAction, applyUpgradeDocumentAction as applyUpgradeDocumentAction$1, baseReducerVersion, createDocumentState, createPresignedHeader, defaultBaseState, deriveOperationId, isDenied, isPurgeMarker, mentionedGroupIds, normalizeDocumentModelVersion, operationOutcome, purgedProtocolVersions, targetDocumentId, withProtocolVersions } from "@powerhousedao/shared/document-model";
2
+ import { v4 } from "uuid";
3
+ import { Migrator, TableNode, sql } from "kysely";
4
+ //#region \0rolldown/runtime.js
5
+ var __defProp = Object.defineProperty;
6
+ var __exportAll = (all, no_symbols) => {
7
+ let target = {};
8
+ for (var name in all) __defProp(target, name, {
9
+ get: all[name],
10
+ enumerable: true
11
+ });
12
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
13
+ return target;
14
+ };
15
+ //#endregion
16
+ //#region src/shared/utils.ts
17
+ function matchesScope(view = {}, scope) {
18
+ if (view.scopes) return view.scopes.includes(scope);
19
+ return true;
20
+ }
21
+ function yieldToMain() {
22
+ const s = globalThis.scheduler;
23
+ if (s?.yield) return s.yield();
24
+ return new Promise((resolve) => setTimeout(resolve, 0));
25
+ }
26
+ const defaultAbortError = () => /* @__PURE__ */ new Error("Operation aborted");
27
+ function throwIfAborted(signal, makeError = defaultAbortError) {
28
+ if (signal?.aborted) throw makeError();
29
+ }
30
+ /**
31
+ * Validates PagingOptions and returns a normalized offset and limit.
32
+ * Throws if the cursor is not empty and not a non-negative integer, or if
33
+ * limit is less than 1. When `paging` is undefined, returns offset 0 and
34
+ * the caller-supplied `defaultLimit`.
35
+ */
36
+ function parsePagingOptions(paging, defaultLimit) {
37
+ if (paging === void 0) return {
38
+ offset: 0,
39
+ limit: defaultLimit
40
+ };
41
+ if (!Number.isInteger(paging.limit) || paging.limit < 1) throw new Error(`Invalid paging limit: ${String(paging.limit)} (must be an integer >= 1)`);
42
+ if (paging.cursor === "") return {
43
+ offset: 0,
44
+ limit: paging.limit
45
+ };
46
+ const parsed = Number(paging.cursor);
47
+ if (!Number.isInteger(parsed) || parsed < 0) throw new Error(`Invalid paging cursor: ${JSON.stringify(paging.cursor)} (must be empty or a non-negative integer)`);
48
+ return {
49
+ offset: parsed,
50
+ limit: paging.limit
51
+ };
52
+ }
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
86
+ //#region src/shared/errors.ts
87
+ /**
88
+ * Error thrown when attempting to access a deleted document.
89
+ */
90
+ var DocumentDeletedError = class DocumentDeletedError extends Error {
91
+ documentId;
92
+ deletedAtUtcIso;
93
+ constructor(documentId, deletedAtUtcIso = null) {
94
+ const message = deletedAtUtcIso ? `Document ${documentId} was deleted at ${deletedAtUtcIso}` : `Document ${documentId} has been deleted`;
95
+ super(message);
96
+ this.name = "DocumentDeletedError";
97
+ this.documentId = documentId;
98
+ this.deletedAtUtcIso = deletedAtUtcIso;
99
+ Error.captureStackTrace(this, DocumentDeletedError);
100
+ }
101
+ static isError(error) {
102
+ return Error.isError(error) && error.name === "DocumentDeletedError";
103
+ }
104
+ };
105
+ /**
106
+ * Error thrown when the auth policy denies an action at the executor gate.
107
+ */
108
+ var AuthorizationDeniedError = class AuthorizationDeniedError extends Error {
109
+ documentId;
110
+ scope;
111
+ operation;
112
+ subject;
113
+ constructor(documentId, scope, operation, subject) {
114
+ super(`Authorization denied: ${subject ?? "anonymous"} may not execute ${operation} in scope "${scope}" of document ${documentId}`);
115
+ this.name = "AuthorizationDeniedError";
116
+ this.documentId = documentId;
117
+ this.scope = scope;
118
+ this.operation = operation;
119
+ this.subject = subject;
120
+ Error.captureStackTrace(this, AuthorizationDeniedError);
121
+ }
122
+ static isError(error) {
123
+ return Error.isError(error) && error.name === "AuthorizationDeniedError";
124
+ }
125
+ };
126
+ /**
127
+ * An auth operation did not strictly exceed the newest timestamp in its stream.
128
+ *
129
+ * Terminal and asymmetric by design: no ordering rule can reconcile two replicas
130
+ * that each accepted an auth operation offline, because either order hands one
131
+ * authority the other never granted, so the replica ahead holds the arrival.
132
+ */
133
+ var AuthTimestampNotMonotonicError = class AuthTimestampNotMonotonicError extends Error {
134
+ documentId;
135
+ branch;
136
+ timestampUtcMs;
137
+ newestTimestampUtcMs;
138
+ constructor(documentId, branch, timestampUtcMs, newestTimestampUtcMs) {
139
+ super(`Auth timestamp not monotonic: ${timestampUtcMs} does not exceed ${newestTimestampUtcMs} in the auth stream of document ${documentId} on branch ${branch}`);
140
+ this.name = "AuthTimestampNotMonotonicError";
141
+ this.documentId = documentId;
142
+ this.branch = branch;
143
+ this.timestampUtcMs = timestampUtcMs;
144
+ this.newestTimestampUtcMs = newestTimestampUtcMs;
145
+ Error.captureStackTrace(this, AuthTimestampNotMonotonicError);
146
+ }
147
+ static isError(error) {
148
+ return Error.isError(error) && error.name === "AuthTimestampNotMonotonicError";
149
+ }
150
+ };
151
+ /**
152
+ * An operation or action carried a timestamp that is not an ISO-8601 UTC
153
+ * instant.
154
+ *
155
+ * Terminal rather than retryable: the value does not change between attempts,
156
+ * so a retry re-runs the whole job to fail identically. Quarantining, unlike a
157
+ * held auth operation — this is malformed data rather than two replicas
158
+ * disagreeing, and nothing further from that source should be trusted until it
159
+ * is looked at.
160
+ */
161
+ var InvalidOperationTimestampError = class InvalidOperationTimestampError extends Error {
162
+ documentId;
163
+ scope;
164
+ timestampUtcMs;
165
+ constructor(documentId, scope, timestampUtcMs, context) {
166
+ super(`Invalid timestamp "${timestampUtcMs}" on ${context} in scope "${scope}" of document ${documentId}`);
167
+ this.name = "InvalidOperationTimestampError";
168
+ this.documentId = documentId;
169
+ this.scope = scope;
170
+ this.timestampUtcMs = timestampUtcMs;
171
+ Error.captureStackTrace(this, InvalidOperationTimestampError);
172
+ }
173
+ static isError(error) {
174
+ return Error.isError(error) && error.name === "InvalidOperationTimestampError";
175
+ }
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
+ };
194
+ /**
195
+ * A load would move more operations than the bound allows, indicating a real
196
+ * divergence between local and incoming history. Counts only first-time moves,
197
+ * so a re-evaluation pass's re-appends do not make busy documents
198
+ * revocation-proof. Terminal: the condition is deterministic.
199
+ */
200
+ var ExcessiveReshuffleError = class ExcessiveReshuffleError extends Error {
201
+ documentId;
202
+ scope;
203
+ count;
204
+ threshold;
205
+ constructor(documentId, scope, count, threshold) {
206
+ super(`Excessive reshuffle detected: ${count} operations in scope "${scope}" of document ${documentId} exceeds the threshold of ${threshold}. This indicates a significant divergence between local and incoming operations.`);
207
+ this.name = "ExcessiveReshuffleError";
208
+ this.documentId = documentId;
209
+ this.scope = scope;
210
+ this.count = count;
211
+ this.threshold = threshold;
212
+ Error.captureStackTrace(this, ExcessiveReshuffleError);
213
+ }
214
+ static isError(error) {
215
+ return Error.isError(error) && error.name === "ExcessiveReshuffleError";
216
+ }
217
+ };
218
+ /** The message carries the code: only name and message cross the queue. */
219
+ var InvalidSignatureError = class InvalidSignatureError extends Error {
220
+ documentId;
221
+ code;
222
+ reason;
223
+ constructor(documentId, code, reason) {
224
+ super(`Invalid signature in document ${documentId} [${code}]: ${reason}`);
225
+ this.name = "InvalidSignatureError";
226
+ this.documentId = documentId;
227
+ this.code = code;
228
+ this.reason = reason;
229
+ Error.captureStackTrace(this, InvalidSignatureError);
230
+ }
231
+ static isError(error) {
232
+ return Error.isError(error) && error.name === "InvalidSignatureError";
233
+ }
234
+ };
235
+ /**
236
+ * An UPGRADE_DOCUMENT action's preconditions (fromVersion and the per-scope
237
+ * revision snapshot) did not match the document state the executor loaded.
238
+ *
239
+ * Terminal rather than retryable: the action carries the client's snapshot,
240
+ * which stays stale no matter how often the job re-runs. The client is
241
+ * expected to re-read the document and submit a fresh action instead.
242
+ */
243
+ var UpgradePreconditionFailedError = class UpgradePreconditionFailedError extends Error {
244
+ documentId;
245
+ detail;
246
+ constructor(documentId, detail) {
247
+ super(`Upgrade precondition failed for document ${documentId}: ${detail}`);
248
+ this.name = "UpgradePreconditionFailedError";
249
+ this.documentId = documentId;
250
+ this.detail = detail;
251
+ Error.captureStackTrace(this, UpgradePreconditionFailedError);
252
+ }
253
+ static isError(error) {
254
+ return Error.isError(error) && error.name === "UpgradePreconditionFailedError";
255
+ }
256
+ };
257
+ /**
258
+ * Error thrown when a document is not found (no operations exist for the document ID).
259
+ */
260
+ var DocumentNotFoundError = class DocumentNotFoundError extends Error {
261
+ documentId;
262
+ /**
263
+ * @param message Overrides the default text. A handler that knows which of
264
+ * several documents an action reads - a relationship's source, say - says so
265
+ * here rather than rewrapping in a bare Error, which would strip the name the
266
+ * executor classifies by.
267
+ */
268
+ constructor(documentId, message) {
269
+ super(message ?? `Document ${documentId} not found`);
270
+ this.name = "DocumentNotFoundError";
271
+ this.documentId = documentId;
272
+ Error.captureStackTrace(this, DocumentNotFoundError);
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
+ }
347
+ static isError(error) {
348
+ return Error.isError(error) && error.name === "ReservedActionError";
349
+ }
350
+ };
351
+ /**
352
+ * An authorization preflight was asked for while the reactor's decision model
353
+ * is off, so there is no model to answer from.
354
+ *
355
+ * Thrown rather than answered from the legacy host-side permission tables. The
356
+ * two systems do not compose: the tables record which addresses a host lets
357
+ * near a drive, the policy records what a document's own grants permit, and an
358
+ * answer stitched from both would report an admission verdict neither system
359
+ * would reach. A caller that cannot get a prediction disables nothing, which
360
+ * leaves the submit path -- and its real gate -- as the only authority.
361
+ *
362
+ * Detection is by `name`, not `instanceof`: the SharedWorker RPC boundary
363
+ * rebuilds a thrown error from `{ name, message, stack, cause }` alone
364
+ * (`reactor-browser/src/rpc/error-info.ts`), so the class identity and any
365
+ * custom field are lost in transit. This error therefore carries no fields.
366
+ */
367
+ var AuthEnforcementDisabledError = class AuthEnforcementDisabledError extends Error {
368
+ constructor() {
369
+ super("Authorization evaluation requires the authEnforcement feature flag; this reactor holds no decision model, and the legacy host-table permission system cannot answer for one");
370
+ this.name = "AuthEnforcementDisabledError";
371
+ Error.captureStackTrace(this, AuthEnforcementDisabledError);
372
+ }
373
+ static isError(error) {
374
+ return Error.isError(error) && error.name === "AuthEnforcementDisabledError";
375
+ }
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
+ };
417
+ //#endregion
418
+ //#region src/storage/interfaces.ts
419
+ /**
420
+ * Thrown when an operation with the same identity already exists in the store.
421
+ */
422
+ var DuplicateOperationError = class extends Error {
423
+ constructor(description) {
424
+ super(`Duplicate operation: ${description}`);
425
+ this.name = "DuplicateOperationError";
426
+ }
427
+ };
428
+ /**
429
+ * Thrown when a concurrent write conflict is detected during an atomic apply.
430
+ */
431
+ var OptimisticLockError = class extends Error {
432
+ constructor(message) {
433
+ super(message);
434
+ this.name = "OptimisticLockError";
435
+ }
436
+ };
437
+ /**
438
+ * Thrown when the caller-provided revision does not match the current
439
+ * stored revision, indicating a stale read.
440
+ */
441
+ var RevisionMismatchError = class extends Error {
442
+ constructor(expected, actual) {
443
+ super(`Revision mismatch: expected ${expected}, got ${actual}`);
444
+ this.name = "RevisionMismatchError";
445
+ }
446
+ };
447
+ /**
448
+ * A create based on an empty stream that already has operations: the id is
449
+ * taken, and no retry can resolve it. Matched by `name`, all the RPC boundary keeps.
450
+ */
451
+ var DocumentAlreadyExistsError = class extends Error {
452
+ documentId;
453
+ constructor(documentId, scope, headRevision) {
454
+ super(`Document ${documentId} already exists: create requested revision 0 but the "${scope}" stream is at revision ${headRevision}`);
455
+ this.name = "DocumentAlreadyExistsError";
456
+ this.documentId = documentId;
457
+ }
458
+ static isError(error) {
459
+ return Error.isError(error) && error.name === "DocumentAlreadyExistsError";
460
+ }
461
+ };
462
+ /** Error history keeps messages, not classes, so failures match by prefix. */
463
+ const APPEND_CONDITION_FAILED_PREFIX = "Append condition failed: ";
464
+ /**
465
+ * A read-set stream grew before the append committed. A concurrency
466
+ * conflict, not a fault: the caller retries against the new stream heads.
467
+ */
468
+ var AppendConditionFailedError = class extends Error {
469
+ condition;
470
+ constructor(condition) {
471
+ const streams = condition.streams.map((s) => `${s.documentId}:${s.scope}:${s.branch}@${s.revision}`).join(", ");
472
+ super(`${APPEND_CONDITION_FAILED_PREFIX}a read-set stream advanced [${streams}]`);
473
+ this.condition = condition;
474
+ this.name = "AppendConditionFailedError";
475
+ }
476
+ static isError(error) {
477
+ return Error.isError(error) && error.name === "AppendConditionFailedError";
478
+ }
479
+ /** True when a recorded error message is an append-condition failure. */
480
+ static isFailureMessage(message) {
481
+ return message.startsWith(APPEND_CONDITION_FAILED_PREFIX);
482
+ }
483
+ };
484
+ /**
485
+ * Which sense of "exists" a caller of {@link IDocumentView.exists} means.
486
+ */
487
+ let DocumentExistence = /* @__PURE__ */ function(DocumentExistence) {
488
+ /**
489
+ * A document that is present and not deleted. This is the question a read
490
+ * asks: a deleted document is not readable, so it does not exist.
491
+ */
492
+ DocumentExistence["LiveOnly"] = "LiveOnly";
493
+ /**
494
+ * Whether the id is taken, deleted or not. This is the question a write
495
+ * asks: a deleted document keeps its operation stream, so its id can never
496
+ * be reused, and answering from snapshots alone would say otherwise whenever
497
+ * a snapshot row is missing while the stream is intact.
498
+ */
499
+ DocumentExistence["IncludingDeleted"] = "IncludingDeleted";
500
+ return DocumentExistence;
501
+ }({});
502
+ //#endregion
503
+ //#region src/executor/util.ts
504
+ /** Actions the reactor reduces itself, onto the document scope. */
505
+ const DOCUMENT_SCOPE_ACTIONS = DOCUMENT_SCOPE_ACTION_TYPES;
506
+ /**
507
+ * `CREATE_DOCUMENT` is exempt by necessity: it runs before the document exists,
508
+ * so building a decision model would throw and defer the job forever.
509
+ */
510
+ const GATED_DOCUMENT_ACTIONS = new Set([...DOCUMENT_SCOPE_ACTIONS].filter((type) => type !== "CREATE_DOCUMENT"));
511
+ /**
512
+ * Creates a PHDocument from a CREATE_DOCUMENT action input.
513
+ * Reconstructs the document header and initializes the base state.
514
+ *
515
+ * @param action - The CREATE_DOCUMENT action containing the document parameters
516
+ * @returns A newly constructed PHDocument with initialized header and base state
517
+ */
518
+ function createDocumentFromAction(action) {
519
+ const input = action.input;
520
+ const header = createPresignedHeader();
521
+ header.id = input.documentId;
522
+ header.documentType = input.model;
523
+ if (input.signing) {
524
+ header.createdAtUtcIso = input.signing.createdAtUtcIso;
525
+ header.lastModifiedAtUtcIso = input.signing.createdAtUtcIso;
526
+ header.sig = {
527
+ publicKey: input.signing.publicKey,
528
+ nonce: input.signing.nonce
529
+ };
530
+ }
531
+ if (input.slug !== void 0) header.slug = input.slug;
532
+ if (!header.slug) header.slug = input.documentId;
533
+ if (input.name !== void 0) header.name = input.name;
534
+ if (input.branch !== void 0) header.branch = input.branch;
535
+ if (input.meta !== void 0) header.meta = input.meta;
536
+ if (input.protocolVersions !== void 0) header.protocolVersions = input.protocolVersions;
537
+ const baseState = defaultBaseState();
538
+ return {
539
+ header,
540
+ operations: {},
541
+ state: baseState,
542
+ initialState: baseState,
543
+ clipboard: []
544
+ };
545
+ }
546
+ /**
547
+ * Calculate the next operation index for a specific scope.
548
+ * Each scope maintains its own independent index sequence.
549
+ *
550
+ * Per-scope indexing means:
551
+ * - Each scope (document, global, local, etc.) has independent indexes
552
+ * - Indexes start at 0 for each scope
553
+ * - Different scopes can have operations with the same index value
554
+ *
555
+ * This function uses header.revision which is populated by the cache/storage layer
556
+ * and contains the next available index for each scope. This design avoids requiring
557
+ * the full operation history to be loaded, which is crucial for snapshot-based caching.
558
+ *
559
+ * @param document - The document whose header.revision to inspect
560
+ * @param scope - The scope to calculate the next index for
561
+ * @returns The next available index in the specified scope
562
+ */
563
+ const getNextIndexForScope = (document, scope) => {
564
+ return document.header.revision[scope] || 0;
565
+ };
566
+ /**
567
+ * Creates an empty consistency token with no coordinates.
568
+ * Used when a job is registered or fails without writing operations.
569
+ *
570
+ * @returns A consistency token with an empty coordinates array
571
+ */
572
+ function createEmptyConsistencyToken() {
573
+ return {
574
+ version: 1,
575
+ createdAtUtcIso: (/* @__PURE__ */ new Date()).toISOString(),
576
+ coordinates: []
577
+ };
578
+ }
579
+ /**
580
+ * Creates a consistency token from operations written during job execution.
581
+ * Maps each operation to a consistency coordinate tracking (documentId, scope, branch, operationIndex).
582
+ * If no operations are provided, returns an empty token.
583
+ *
584
+ * @param operationsWithContext - Array of operations with their execution context
585
+ * @returns A consistency token representing all operations written
586
+ */
587
+ function createConsistencyToken(operationsWithContext) {
588
+ if (operationsWithContext.length === 0) return createEmptyConsistencyToken();
589
+ const coordinates = [];
590
+ for (let i = 0; i < operationsWithContext.length; i++) {
591
+ const opWithContext = operationsWithContext[i];
592
+ coordinates.push({
593
+ documentId: opWithContext.context.documentId,
594
+ scope: opWithContext.context.scope,
595
+ branch: opWithContext.context.branch,
596
+ operationIndex: opWithContext.operation.index
597
+ });
598
+ }
599
+ return {
600
+ version: 1,
601
+ createdAtUtcIso: (/* @__PURE__ */ new Date()).toISOString(),
602
+ coordinates
603
+ };
604
+ }
605
+ function createOperation(action, index, skip, context) {
606
+ return {
607
+ id: deriveOperationId(context.documentId, context.scope, context.branch, action.id),
608
+ index,
609
+ timestampUtcMs: action.timestampUtcMs || (/* @__PURE__ */ new Date()).toISOString(),
610
+ hash: "",
611
+ skip,
612
+ action
613
+ };
614
+ }
615
+ function updateDocumentRevision(document, scope, operationIndex) {
616
+ document.header.revision = {
617
+ ...document.header.revision,
618
+ [scope]: operationIndex + 1
619
+ };
620
+ }
621
+ function buildSuccessResult(job, operation, documentId, documentType, resultingState, startTime) {
622
+ return {
623
+ job,
624
+ success: true,
625
+ operations: [operation],
626
+ operationsWithContext: [{
627
+ operation,
628
+ context: {
629
+ documentId,
630
+ scope: job.scope,
631
+ branch: job.branch,
632
+ documentType,
633
+ resultingState,
634
+ ordinal: 0
635
+ }
636
+ }],
637
+ duration: Date.now() - startTime
638
+ };
639
+ }
640
+ function buildErrorResult(job, error, startTime) {
641
+ return {
642
+ job,
643
+ success: false,
644
+ error,
645
+ duration: Date.now() - startTime
646
+ };
647
+ }
648
+ /**
649
+ * The error a refusal surfaces as. Both classes are already terminal in the job
650
+ * result handler, so a refusal never burns a retry.
651
+ */
652
+ function refusalError(reason, documentId, deletedAtUtcIso, action) {
653
+ if (reason === DOCUMENT_DELETED_REASON) return new DocumentDeletedError(documentId, deletedAtUtcIso);
654
+ return new AuthorizationDeniedError(documentId, action.scope, action.type, action.context?.signer?.user.address);
655
+ }
656
+ /**
657
+ * Whether this operation is part of the document's creation. The create and the
658
+ * upgrade from version zero hold the first two indexes for the life of the
659
+ * document, so a reshuffle has to leave them where they are.
660
+ */
661
+ function isGenesisOperation(operation) {
662
+ if (operation.action.type === "CREATE_DOCUMENT") return true;
663
+ if (operation.action.type !== "UPGRADE_DOCUMENT") return false;
664
+ return operation.action.input.fromVersion === 0;
665
+ }
666
+ /**
667
+ * The distinct streams a job wrote, collected so a rollback knows what to evict.
668
+ *
669
+ * A write records its stream on every apply, and a long run applies the same
670
+ * stream hundreds of times, so this keeps one entry per stream rather than one
671
+ * per write: the eviction only cares which streams were touched, and the
672
+ * successful jobs that never read this back pay for a lookup instead of an
673
+ * allocation.
674
+ */
675
+ var TouchedStreams = class {
676
+ streams = /* @__PURE__ */ new Map();
677
+ add(documentId, scope, branch) {
678
+ const key = `${documentId}\u0000${scope}\u0000${branch}`;
679
+ if (this.streams.has(key)) return;
680
+ this.streams.set(key, {
681
+ documentId,
682
+ scope,
683
+ branch
684
+ });
685
+ }
686
+ [Symbol.iterator]() {
687
+ return this.streams.values();
688
+ }
689
+ };
690
+ /**
691
+ * The ids of the actions the caller handed to a job. Load and reevaluation
692
+ * jobs write operations nobody submitted, so they report none.
693
+ */
694
+ function submittedActionIds(job) {
695
+ return job.kind === "mutation" ? job.actions.map((action) => action.id) : [];
696
+ }
697
+ /**
698
+ * Reports what became of each submitted action.
699
+ *
700
+ * A job's operations can include ones it only moved to a new index, so only
701
+ * those carrying a submitted action are reported. Returns undefined when the
702
+ * job submitted nothing, which keeps `JobInfo.result` null for the jobs that
703
+ * have no caller to answer to.
704
+ */
705
+ function summarizeSubmittedActions(operations, submitted) {
706
+ if (!submitted || submitted.length === 0) return;
707
+ const ids = new Set(submitted);
708
+ const actions = [];
709
+ for (const { operation, context } of operations) {
710
+ if (!ids.has(operation.action.id)) continue;
711
+ actions.push({
712
+ actionId: operation.action.id,
713
+ scope: context.scope,
714
+ index: operation.index,
715
+ ...operationOutcome(operation)
716
+ });
717
+ }
718
+ if (actions.length === 0) return;
719
+ return {
720
+ actions,
721
+ allApplied: actions.every((action) => action.kind === "applied")
722
+ };
723
+ }
724
+ /** Tombstones a job has read, each under the shared lock it took first. */
725
+ var PurgeFence = class {
726
+ locks;
727
+ checked;
728
+ purged;
729
+ constructor(locks, checked, purged) {
730
+ this.locks = locks;
731
+ this.checked = checked;
732
+ this.purged = purged;
733
+ }
734
+ /** Locks and reads an id the job start did not resolve. */
735
+ async isPurged(documentId) {
736
+ await this.isPurgedMany([documentId]);
737
+ return this.purged.has(documentId);
738
+ }
739
+ /** Locks and reads the unresolved ids in one lock and one lookup. */
740
+ async isPurgedMany(documentIds) {
741
+ const unchecked = [...new Set(documentIds)].filter((id) => !this.checked.has(id));
742
+ if (unchecked.length > 0) {
743
+ await this.locks.shared(unchecked);
744
+ const found = await this.locks.purged(unchecked);
745
+ for (const id of unchecked) this.checked.add(id);
746
+ for (const id of found) this.purged.add(id);
747
+ }
748
+ return new Set(documentIds.filter((id) => this.purged.has(id)));
749
+ }
750
+ };
751
+ /** Locks and checks an id on its first read; a purged id reads as purged. */
752
+ var FencedWriteCache = class {
753
+ inner;
754
+ purgeFence;
755
+ evict;
756
+ evicted = /* @__PURE__ */ new Set();
757
+ constructor(inner, purgeFence, evict) {
758
+ this.inner = inner;
759
+ this.purgeFence = purgeFence;
760
+ this.evict = evict;
761
+ }
762
+ /** Fences ids known before their reads in one round trip. */
763
+ async fence(documentIds) {
764
+ await this.purgeFence.isPurgedMany(documentIds);
765
+ }
766
+ async getState(documentId, scope, branch, targetRevision, signal) {
767
+ if (await this.purgeFence.isPurged(documentId)) {
768
+ if (!this.evicted.has(documentId)) {
769
+ this.evicted.add(documentId);
770
+ this.evict(documentId);
771
+ }
772
+ throw new DocumentPurgedError(documentId);
773
+ }
774
+ return this.inner.getState(documentId, scope, branch, targetRevision, signal);
775
+ }
776
+ putState(documentId, scope, branch, revision, document, position) {
777
+ this.inner.putState(documentId, scope, branch, revision, document, position);
778
+ }
779
+ putRun(documentId, scope, branch, run) {
780
+ this.inner.putRun(documentId, scope, branch, run);
781
+ }
782
+ invalidate(documentId, scope, branch) {
783
+ return this.inner.invalidate(documentId, scope, branch);
784
+ }
785
+ clear() {
786
+ this.inner.clear();
787
+ }
788
+ startup() {
789
+ return this.inner.startup();
790
+ }
791
+ shutdown() {
792
+ return this.inner.shutdown();
793
+ }
794
+ };
795
+ /** The target whose membership an ADD or REMOVE_RELATIONSHIP writes. */
796
+ function relationshipTarget(action) {
797
+ if (action.type !== "ADD_RELATIONSHIP" && action.type !== "REMOVE_RELATIONSHIP") return;
798
+ const target = action.input?.targetId;
799
+ return typeof target === "string" && target.length > 0 ? target : void 0;
800
+ }
801
+ /** The ids a job's locks cover, sorted: its write ids and groups it names. */
802
+ function jobWriteIds(job) {
803
+ const ids = /* @__PURE__ */ new Set([job.documentId]);
804
+ const actions = [...job.actions, ...job.operations.map((operation) => operation.action)];
805
+ for (const action of actions) {
806
+ if (DOCUMENT_SCOPE_ACTIONS.has(action.type)) ids.add(targetDocumentId(action, job.documentId));
807
+ const target = relationshipTarget(action);
808
+ if (target !== void 0) ids.add(target);
809
+ if (action.scope === "auth") for (const groupId of mentionedGroupIds(action)) ids.add(groupId);
810
+ }
811
+ return [...ids].sort();
812
+ }
813
+ //#endregion
814
+ //#region src/registry/errors.ts
815
+ /**
816
+ * Error thrown when a document model module is not found in the registry.
817
+ */
818
+ var ModuleNotFoundError = class extends Error {
819
+ documentType;
820
+ requestedVersion;
821
+ constructor(documentType, version) {
822
+ const versionSuffix = version !== void 0 ? ` version ${version}` : "";
823
+ super(`Document model module not found for type: ${documentType}${versionSuffix}`);
824
+ this.name = "ModuleNotFoundError";
825
+ this.documentType = documentType;
826
+ this.requestedVersion = version;
827
+ }
828
+ static isError(error) {
829
+ return Error.isError(error) && error.name === "ModuleNotFoundError";
830
+ }
831
+ };
832
+ /** The host has the model only as a live module, which workers cannot import. */
833
+ var ModelNotWorkerImportableError = class extends Error {
834
+ documentType;
835
+ constructor(documentType, version) {
836
+ const versionSuffix = version !== void 0 ? ` version ${version}` : "";
837
+ super(`Document model ${documentType}${versionSuffix} is registered on the host only as a live module, so executor workers cannot load it. Register it from a { filePath } or { packageName } source.`);
838
+ this.name = "ModelNotWorkerImportableError";
839
+ this.documentType = documentType;
840
+ }
841
+ static isError(error) {
842
+ return Error.isError(error) && error.name === "ModelNotWorkerImportableError";
843
+ }
844
+ };
845
+ /**
846
+ * Error thrown when attempting to register a module that already exists.
847
+ */
848
+ var DuplicateModuleError = class extends Error {
849
+ constructor(documentType, version) {
850
+ const versionSuffix = version !== void 0 ? ` (version ${version})` : "";
851
+ super(`Document model module already registered for type: ${documentType}${versionSuffix}`);
852
+ this.name = "DuplicateModuleError";
853
+ }
854
+ static isError(error) {
855
+ return Error.isError(error) && error.name === "DuplicateModuleError";
856
+ }
857
+ };
858
+ /**
859
+ * Error thrown when a module is invalid or malformed.
860
+ */
861
+ var InvalidModuleError = class extends Error {
862
+ constructor(message) {
863
+ super(`Invalid document model module: ${message}`);
864
+ this.name = "InvalidModuleError";
865
+ }
866
+ };
867
+ /**
868
+ * Error thrown when attempting to register an upgrade manifest that already exists.
869
+ */
870
+ var DuplicateManifestError = class extends Error {
871
+ constructor(documentType) {
872
+ super(`Upgrade manifest already registered for type: ${documentType}`);
873
+ this.name = "DuplicateManifestError";
874
+ }
875
+ static isError(error) {
876
+ return Error.isError(error) && error.name === "DuplicateManifestError";
877
+ }
878
+ };
879
+ /**
880
+ * Error thrown when an upgrade manifest is not found.
881
+ */
882
+ var ManifestNotFoundError = class extends Error {
883
+ constructor(documentType) {
884
+ super(`Upgrade manifest not found for type: ${documentType}`);
885
+ this.name = "ManifestNotFoundError";
886
+ }
887
+ };
888
+ /**
889
+ * Error thrown when a required upgrade transition is missing from the manifest.
890
+ */
891
+ var MissingUpgradeTransitionError = class extends Error {
892
+ constructor(documentType, fromVersion, toVersion) {
893
+ super(`Missing upgrade transition for ${documentType}: v${fromVersion} to v${toVersion}`);
894
+ this.name = "MissingUpgradeTransitionError";
895
+ }
896
+ };
897
+ /**
898
+ * Error thrown when getUpgradeReducer is called with a non-single-step version increment.
899
+ */
900
+ var InvalidUpgradeStepError = class extends Error {
901
+ constructor(documentType, fromVersion, toVersion) {
902
+ super(`Invalid upgrade step for ${documentType}: must be single version increment, got v${fromVersion} to v${toVersion}`);
903
+ this.name = "InvalidUpgradeStepError";
904
+ }
905
+ };
906
+ //#endregion
907
+ //#region src/cache/collection-membership-cache.ts
908
+ var CollectionMembershipCache = class CollectionMembershipCache {
909
+ operationIndex;
910
+ cache = /* @__PURE__ */ new Map();
911
+ constructor(operationIndex) {
912
+ this.operationIndex = operationIndex;
913
+ }
914
+ withScopedIndex(operationIndex) {
915
+ const scoped = new CollectionMembershipCache(operationIndex);
916
+ scoped.cache = this.cache;
917
+ return scoped;
918
+ }
919
+ async getCollectionsForDocuments(documentIds) {
920
+ const result = {};
921
+ const missing = [];
922
+ for (const docId of documentIds) {
923
+ const cached = this.cache.get(docId);
924
+ if (cached !== void 0) result[docId] = cached;
925
+ else missing.push(docId);
926
+ }
927
+ if (missing.length > 0) {
928
+ const fromDb = await this.operationIndex.getCollectionsForDocuments(missing);
929
+ for (const docId of missing) {
930
+ const collections = fromDb[docId] ?? [];
931
+ result[docId] = collections;
932
+ this.cache.set(docId, collections);
933
+ }
934
+ }
935
+ return result;
936
+ }
937
+ invalidate(documentId) {
938
+ this.cache.delete(documentId);
939
+ }
940
+ };
941
+ //#endregion
942
+ //#region src/cache/lru/lru-tracker.ts
943
+ var LRUNode = class {
944
+ key;
945
+ prev;
946
+ next;
947
+ constructor(key) {
948
+ this.key = key;
949
+ this.prev = void 0;
950
+ this.next = void 0;
951
+ }
952
+ };
953
+ var LRUTracker = class {
954
+ map;
955
+ head;
956
+ tail;
957
+ constructor() {
958
+ this.map = /* @__PURE__ */ new Map();
959
+ this.head = void 0;
960
+ this.tail = void 0;
961
+ }
962
+ get size() {
963
+ return this.map.size;
964
+ }
965
+ touch(key) {
966
+ const node = this.map.get(key);
967
+ if (node) this.moveToFront(node);
968
+ else this.addToFront(key);
969
+ }
970
+ evict() {
971
+ if (!this.tail) return;
972
+ const key = this.tail.key;
973
+ this.remove(key);
974
+ return key;
975
+ }
976
+ remove(key) {
977
+ const node = this.map.get(key);
978
+ if (!node) return;
979
+ this.removeNode(node);
980
+ this.map.delete(key);
981
+ }
982
+ clear() {
983
+ this.map.clear();
984
+ this.head = void 0;
985
+ this.tail = void 0;
986
+ }
987
+ addToFront(key) {
988
+ const node = new LRUNode(key);
989
+ this.map.set(key, node);
990
+ if (!this.head) {
991
+ this.head = node;
992
+ this.tail = node;
993
+ } else {
994
+ node.next = this.head;
995
+ this.head.prev = node;
996
+ this.head = node;
997
+ }
998
+ }
999
+ moveToFront(node) {
1000
+ if (node === this.head) return;
1001
+ this.removeNode(node);
1002
+ node.prev = void 0;
1003
+ node.next = this.head;
1004
+ if (this.head) this.head.prev = node;
1005
+ this.head = node;
1006
+ if (!this.tail) this.tail = node;
1007
+ }
1008
+ removeNode(node) {
1009
+ if (node.prev) node.prev.next = node.next;
1010
+ else this.head = node.next;
1011
+ if (node.next) node.next.prev = node.prev;
1012
+ else this.tail = node.prev;
1013
+ }
1014
+ };
1015
+ //#endregion
1016
+ //#region src/cache/document-meta-cache.ts
1017
+ /**
1018
+ * In-memory document metadata cache with LRU eviction.
1019
+ *
1020
+ * Caches PHDocumentState per (documentId, branch) key. On cache miss,
1021
+ * rebuilds from document scope operations. Provides an explicit cross-scope
1022
+ * contract for accessing document scope metadata.
1023
+ *
1024
+ * **Thread Safety:**
1025
+ * Not thread-safe. Designed for single-threaded job executor environment.
1026
+ */
1027
+ var DocumentMetaCache = class DocumentMetaCache {
1028
+ cache;
1029
+ lruTracker;
1030
+ operationStore;
1031
+ config;
1032
+ constructor(operationStore, config) {
1033
+ this.operationStore = operationStore;
1034
+ this.config = { maxDocuments: config.maxDocuments };
1035
+ this.cache = /* @__PURE__ */ new Map();
1036
+ this.lruTracker = new LRUTracker();
1037
+ }
1038
+ withScopedStore(operationStore) {
1039
+ const scoped = new DocumentMetaCache(operationStore, this.config);
1040
+ scoped.cache = this.cache;
1041
+ scoped.lruTracker = this.lruTracker;
1042
+ return scoped;
1043
+ }
1044
+ async startup() {
1045
+ return Promise.resolve();
1046
+ }
1047
+ async shutdown() {
1048
+ return Promise.resolve();
1049
+ }
1050
+ async getDocumentMeta(documentId, branch, signal) {
1051
+ if (signal?.aborted) throw new Error("Operation aborted");
1052
+ const key = this.makeKey(documentId, branch);
1053
+ const cached = this.cache.get(key);
1054
+ if (cached) {
1055
+ this.lruTracker.touch(key);
1056
+ return cached;
1057
+ }
1058
+ const meta = await this.rebuildLatest(documentId, branch, signal);
1059
+ this.putDocumentMeta(documentId, branch, meta);
1060
+ return meta;
1061
+ }
1062
+ async rebuildAtRevision(documentId, branch, targetRevision, signal) {
1063
+ if (signal?.aborted) throw new Error("Operation aborted");
1064
+ return this.rebuildFromOperations(documentId, branch, targetRevision, signal);
1065
+ }
1066
+ putDocumentMeta(documentId, branch, meta) {
1067
+ const key = this.makeKey(documentId, branch);
1068
+ if (!this.cache.has(key) && this.cache.size >= this.config.maxDocuments) {
1069
+ const evictKey = this.lruTracker.evict();
1070
+ if (evictKey) this.cache.delete(evictKey);
1071
+ }
1072
+ this.cache.set(key, structuredClone(meta));
1073
+ this.lruTracker.touch(key);
1074
+ }
1075
+ invalidate(documentId, branch) {
1076
+ let evicted = 0;
1077
+ if (branch === void 0) {
1078
+ for (const key of this.cache.keys()) if (key.startsWith(`${documentId}:`)) {
1079
+ this.cache.delete(key);
1080
+ this.lruTracker.remove(key);
1081
+ evicted++;
1082
+ }
1083
+ } else {
1084
+ const key = this.makeKey(documentId, branch);
1085
+ if (this.cache.has(key)) {
1086
+ this.cache.delete(key);
1087
+ this.lruTracker.remove(key);
1088
+ evicted = 1;
1089
+ }
1090
+ }
1091
+ return evicted;
1092
+ }
1093
+ clear() {
1094
+ this.cache.clear();
1095
+ this.lruTracker.clear();
1096
+ }
1097
+ makeKey(documentId, branch) {
1098
+ return `${documentId}:${branch}`;
1099
+ }
1100
+ async rebuildLatest(documentId, branch, signal) {
1101
+ return this.rebuildFromOperations(documentId, branch, void 0, signal);
1102
+ }
1103
+ async rebuildFromOperations(documentId, branch, targetRevision, signal) {
1104
+ const docScopeOps = await this.operationStore.getSince(documentId, "document", branch, -1, void 0, void 0, signal);
1105
+ if (docScopeOps.results.length === 0) throw new DocumentNotFoundError(documentId);
1106
+ const createOp = docScopeOps.results[0];
1107
+ if (isPurgeMarker(createOp)) return purgedMeta(createOp.action);
1108
+ if (createOp.action.type !== "CREATE_DOCUMENT") throw new Error(`Invalid document: first operation must be CREATE_DOCUMENT, found ${createOp.action.type}`);
1109
+ const createAction = createOp.action;
1110
+ const documentType = createAction.input.model;
1111
+ let document = createDocumentFromAction(createAction);
1112
+ let documentScopeRevision = 0;
1113
+ let purged = false;
1114
+ for (const op of docScopeOps.results) {
1115
+ if (targetRevision !== void 0 && op.index > targetRevision) break;
1116
+ documentScopeRevision = op.index;
1117
+ if (op.action.type === "UPGRADE_DOCUMENT") {
1118
+ const upgradeAction = op.action;
1119
+ document = applyUpgradeDocumentAction$1(document, upgradeAction);
1120
+ } else if (op.action.type === "DELETE_DOCUMENT" || op.action.type === PURGE_DOCUMENT) {
1121
+ document = applyDeleteDocumentAction$1(document, op.action);
1122
+ purged ||= op.action.type === PURGE_DOCUMENT;
1123
+ }
1124
+ }
1125
+ return {
1126
+ state: document.state.document,
1127
+ documentType,
1128
+ protocolVersions: purged ? purgedProtocolVersions() : createAction.input.protocolVersions,
1129
+ documentScopeRevision: documentScopeRevision + 1
1130
+ };
1131
+ }
1132
+ };
1133
+ /** A lone marker has no CREATE_DOCUMENT; the marker describes the stream. */
1134
+ function purgedMeta(action) {
1135
+ return {
1136
+ state: createDocumentState({
1137
+ isDeleted: true,
1138
+ deletedAtUtcIso: action.input.purgedAtUtcIso
1139
+ }),
1140
+ documentType: action.input.documentType,
1141
+ protocolVersions: purgedProtocolVersions(),
1142
+ documentScopeRevision: 1
1143
+ };
1144
+ }
1145
+ //#endregion
1146
+ //#region src/catch-up/settled-watermark.ts
1147
+ const MAX_PENDING_PROBES = 64;
1148
+ /** pg_sequence_last_value needs 10; pg_current_* replaced txid_* in 13. */
1149
+ function snapshotFunctionsFor(serverVersionNum) {
1150
+ if (serverVersionNum >= 13e4) return {
1151
+ snapshot: "pg_current_snapshot",
1152
+ currentXid: "pg_current_xact_id",
1153
+ xidIfAssigned: "pg_current_xact_id_if_assigned"
1154
+ };
1155
+ if (serverVersionNum >= 1e5) return {
1156
+ snapshot: "txid_current_snapshot",
1157
+ currentXid: "txid_current",
1158
+ xidIfAssigned: "txid_current_if_assigned"
1159
+ };
1160
+ throw new Error(`PostgreSQL server_version_num ${serverVersionNum} is not supported: read-side catch-up needs 10 or later`);
1161
+ }
1162
+ /** Parses the `xmin:xmax:xip,...` text form of a snapshot. */
1163
+ function parseSnapshot(text) {
1164
+ const parts = text.split(":");
1165
+ if (parts.length !== 3) throw new Error(`Malformed snapshot: ${text}`);
1166
+ const [xmin, xmax, xip] = parts;
1167
+ return {
1168
+ xmin: BigInt(xmin),
1169
+ xmax: BigInt(xmax),
1170
+ xip: xip === "" ? [] : xip.split(",").map((xid) => BigInt(xid))
1171
+ };
1172
+ }
1173
+ async function readSnapshotFunctions(db) {
1174
+ const result = await sql`select current_setting('server_version_num') as version`.execute(db);
1175
+ return snapshotFunctionsFor(Number(result.rows[0].version));
1176
+ }
1177
+ /** A probe settles once an xmin passes the xid it took after its head. */
1178
+ var ProbeSettler = class {
1179
+ settled = 0;
1180
+ lastHead = 0;
1181
+ covered = 0;
1182
+ lastXip = [];
1183
+ pending = [];
1184
+ get settledThrough() {
1185
+ return this.settled;
1186
+ }
1187
+ get head() {
1188
+ return this.lastHead;
1189
+ }
1190
+ /** The highest head a settled or pending probe covers. */
1191
+ get probedHead() {
1192
+ return this.covered;
1193
+ }
1194
+ observe(head, xid, snapshot, nowUtcMs) {
1195
+ this.lastHead = Math.max(this.lastHead, head);
1196
+ this.lastXip = snapshot.xip;
1197
+ if (xid !== null && head > this.covered) {
1198
+ this.covered = head;
1199
+ this.pending.push({
1200
+ head,
1201
+ xid,
1202
+ takenAtUtcMs: nowUtcMs
1203
+ });
1204
+ if (this.pending.length > MAX_PENDING_PROBES) this.pending.shift();
1205
+ }
1206
+ const stillPending = [];
1207
+ for (const probe of this.pending) if (snapshot.xmin > probe.xid) this.settled = Math.max(this.settled, probe.head);
1208
+ else stillPending.push(probe);
1209
+ this.pending = stillPending.filter((probe) => probe.head > this.settled);
1210
+ return this.settled;
1211
+ }
1212
+ /** Xids an unsettled probe still waits on. */
1213
+ waitingOn() {
1214
+ if (this.pending.length === 0) return [];
1215
+ const xid = this.pending[0].xid;
1216
+ return this.lastXip.filter((open) => open < xid).map((open) => open.toString());
1217
+ }
1218
+ stalledSinceUtcMs() {
1219
+ return this.pending[0]?.takenAtUtcMs;
1220
+ }
1221
+ };
1222
+ function operationIndexTableName(db) {
1223
+ const from = db.selectFrom("operation_index_operations").selectAll().toOperationNode().from?.froms[0];
1224
+ const schema = from !== void 0 && TableNode.is(from) ? from.table.schema?.name : void 0;
1225
+ const quote = (identifier) => `"${identifier.replace(/"/g, "\"\"")}"`;
1226
+ return schema === void 0 ? quote("operation_index_operations") : `${quote(schema)}.${quote("operation_index_operations")}`;
1227
+ }
1228
+ /** Reads the head, takes and commits an xid, then reads a snapshot, in order. */
1229
+ function createKyselyWatermarkProbe(db) {
1230
+ let resolved = null;
1231
+ const resolve = async () => {
1232
+ const fns = await readSnapshotFunctions(db);
1233
+ const table = operationIndexTableName(db);
1234
+ const sequence = (await sql`select pg_get_serial_sequence(${table}, 'ordinal') as sequence`.execute(db)).rows[0]?.sequence;
1235
+ if (!sequence) throw new Error(`No ordinal sequence found for ${table}`);
1236
+ return {
1237
+ fns,
1238
+ sequence
1239
+ };
1240
+ };
1241
+ return async (probedHead) => {
1242
+ resolved ??= resolve().catch((error) => {
1243
+ resolved = null;
1244
+ throw error;
1245
+ });
1246
+ const { fns, sequence } = await resolved;
1247
+ 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];
1248
+ const head = Number(headRow.head);
1249
+ let xid = null;
1250
+ if (headRow.outside_write && head > probedHead) {
1251
+ const xidResult = await sql`select ${sql.raw(fns.currentXid)}()::text as xid`.execute(db);
1252
+ xid = BigInt(xidResult.rows[0].xid);
1253
+ }
1254
+ 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];
1255
+ return {
1256
+ head,
1257
+ xid,
1258
+ snapshot: parseSnapshot(row.snapshot),
1259
+ outsideWrite: headRow.outside_write && row.outside_write
1260
+ };
1261
+ };
1262
+ }
1263
+ /** Knows when an ordinal gap is final by asking which transactions are open. */
1264
+ var SettledWatermark = class {
1265
+ probe;
1266
+ logger;
1267
+ now;
1268
+ settler = new ProbeSettler();
1269
+ listeners = /* @__PURE__ */ new Set();
1270
+ current;
1271
+ next;
1272
+ warnedInsideWrite = false;
1273
+ constructor(probe, logger, now = Date.now) {
1274
+ this.probe = probe;
1275
+ this.logger = logger;
1276
+ this.now = now;
1277
+ }
1278
+ get settledThrough() {
1279
+ return this.settler.settledThrough;
1280
+ }
1281
+ refresh(signal) {
1282
+ signal?.throwIfAborted();
1283
+ if (this.next !== void 0) return this.next;
1284
+ if (this.current === void 0) return this.start();
1285
+ const next = this.current.catch(() => this.settledThrough).then(() => {
1286
+ this.next = void 0;
1287
+ return this.start();
1288
+ });
1289
+ this.next = next;
1290
+ return next;
1291
+ }
1292
+ onAdvance(listener) {
1293
+ this.listeners.add(listener);
1294
+ return () => {
1295
+ this.listeners.delete(listener);
1296
+ };
1297
+ }
1298
+ status() {
1299
+ const stalledSinceUtcMs = this.settler.head > this.settledThrough ? this.settler.stalledSinceUtcMs() : void 0;
1300
+ return {
1301
+ head: this.settler.head,
1302
+ settledThrough: this.settledThrough,
1303
+ waitingOn: this.settler.waitingOn(),
1304
+ ...stalledSinceUtcMs !== void 0 ? { stalledSinceUtcMs } : {}
1305
+ };
1306
+ }
1307
+ start() {
1308
+ const run = this.probeOnce().finally(() => {
1309
+ if (this.current === run) this.current = void 0;
1310
+ });
1311
+ this.current = run;
1312
+ return run;
1313
+ }
1314
+ async probeOnce() {
1315
+ const reading = await this.probe(this.settler.probedHead);
1316
+ if (!reading.outsideWrite) {
1317
+ if (!this.warnedInsideWrite) {
1318
+ this.warnedInsideWrite = true;
1319
+ this.logger.warn("settled watermark probe ran inside another session's write transaction; discarded");
1320
+ }
1321
+ return this.settledThrough;
1322
+ }
1323
+ const before = this.settledThrough;
1324
+ const after = this.settler.observe(reading.head, reading.xid, reading.snapshot, this.now());
1325
+ if (after > before) for (const listener of this.listeners) try {
1326
+ listener(after);
1327
+ } catch (error) {
1328
+ this.logger.error("settled watermark listener failed: @error", error);
1329
+ }
1330
+ return after;
1331
+ }
1332
+ };
1333
+ /** Sessions holding these xids open; empty without pg_stat_activity. */
1334
+ async function describeWaitingSessions(db, xids) {
1335
+ if (xids.length === 0) return [];
1336
+ const epochless = xids.map((xid) => (BigInt(xid) % 2n ** 32n).toString());
1337
+ try {
1338
+ return (await sql`select pid, application_name, state, xact_start, backend_xid::text as xid
1339
+ from pg_stat_activity
1340
+ where backend_xid::text in (${sql.join(epochless)})`.execute(db)).rows.map((row) => ({
1341
+ pid: Number(row.pid),
1342
+ applicationName: row.application_name ?? "",
1343
+ state: row.state ?? "",
1344
+ xactStart: row.xact_start === null ? null : new Date(row.xact_start).toISOString(),
1345
+ xid: row.xid
1346
+ }));
1347
+ } catch {
1348
+ return [];
1349
+ }
1350
+ }
1351
+ //#endregion
1352
+ //#region src/storage/kysely/document-purges.ts
1353
+ /** Class key of every purge lock; the stream locks use the single-key space. */
1354
+ const PURGE_NS = 1347571013;
1355
+ /** Ids share this many lock keys, so a job's lock count stays bounded. */
1356
+ const PURGE_LOCK_BUCKETS = 1024;
1357
+ async function acquirePurgeLocks(trx, ids, mode) {
1358
+ const unique = [...new Set(ids)];
1359
+ if (unique.length === 0) return;
1360
+ const lock = sql.raw(mode === "shared" ? "pg_advisory_xact_lock_shared" : "pg_advisory_xact_lock");
1361
+ await sql`
1362
+ with keys as materialized (
1363
+ select distinct hashtext(id) & ${sql.lit(1023)} as key
1364
+ from unnest(${unique}::text[]) as t(id)
1365
+ order by key
1366
+ )
1367
+ select ${lock}(${sql.lit(PURGE_NS)}, key) from keys order by key
1368
+ `.execute(trx);
1369
+ }
1370
+ /** The ids among `ids` that have a tombstone, read in the handle's schema. */
1371
+ async function findPurged(db, ids) {
1372
+ const unique = [...new Set(ids)];
1373
+ if (unique.length === 0) return /* @__PURE__ */ new Set();
1374
+ const rows = await db.selectFrom("document_purges").select("documentId").where(sql`"documentId" = any(${unique}::text[])`).execute();
1375
+ return new Set(rows.map((row) => row.documentId));
1376
+ }
1377
+ /** Every tombstoned id, read in the handle's schema. */
1378
+ async function listPurged(db) {
1379
+ return (await db.selectFrom("document_purges").select("documentId").orderBy("documentId").execute()).map((row) => row.documentId);
1380
+ }
1381
+ /** Bind slots per statement: 16-bit on the wire, read signed in-process. */
1382
+ const MAX_BIND_PARAMETERS = 32767;
1383
+ /** Splits rows into runs that each bind at most MAX_BIND_PARAMETERS. */
1384
+ function chunkRows(rows) {
1385
+ if (rows.length === 0) return [];
1386
+ const columnsPerRow = Math.max(1, Object.keys(rows[0]).length);
1387
+ const perChunk = Math.max(1, Math.floor(MAX_BIND_PARAMETERS / columnsPerRow));
1388
+ if (rows.length <= perChunk) return [rows];
1389
+ const chunks = [];
1390
+ for (let i = 0; i < rows.length; i += perChunk) chunks.push(rows.slice(i, i + perChunk));
1391
+ return chunks;
1392
+ }
1393
+ /** Per database: the function that assigns the transaction's xid. */
1394
+ const xidFunctions = /* @__PURE__ */ new WeakMap();
1395
+ var KyselyOperationIndexTxn = class {
1396
+ collections = [];
1397
+ collectionMemberships = [];
1398
+ collectionRemovals = [];
1399
+ groupReferences = [];
1400
+ operations = [];
1401
+ membershipInvalidations = /* @__PURE__ */ new Set();
1402
+ /** Called by the commit as it writes each document_collections row. */
1403
+ recordMembershipInvalidation(documentId) {
1404
+ this.membershipInvalidations.add(documentId);
1405
+ }
1406
+ getMembershipInvalidations() {
1407
+ return [...this.membershipInvalidations];
1408
+ }
1409
+ createCollection(collectionId) {
1410
+ this.collections.push(collectionId);
1411
+ }
1412
+ addToCollection(collectionId, documentId) {
1413
+ const lastOpIndex = this.operations.length - 1;
1414
+ if (lastOpIndex < 0) throw new Error("addToCollection must be called after write() - no operations in transaction");
1415
+ this.collectionMemberships.push({
1416
+ collectionId,
1417
+ documentId,
1418
+ operationIndex: lastOpIndex
1419
+ });
1420
+ }
1421
+ removeFromCollection(collectionId, documentId) {
1422
+ const lastOpIndex = this.operations.length - 1;
1423
+ if (lastOpIndex < 0) throw new Error("removeFromCollection must be called after write() - no operations in transaction");
1424
+ this.collectionRemovals.push({
1425
+ collectionId,
1426
+ documentId,
1427
+ operationIndex: lastOpIndex
1428
+ });
1429
+ }
1430
+ recordGroupReferences(documentId, groupIds) {
1431
+ const lastOpIndex = this.operations.length - 1;
1432
+ if (lastOpIndex < 0) throw new Error("recordGroupReferences must be called after write() - no operations in transaction");
1433
+ if (groupIds.length === 0) return;
1434
+ this.groupReferences.push({
1435
+ documentId,
1436
+ groupIds,
1437
+ operationIndex: lastOpIndex
1438
+ });
1439
+ }
1440
+ write(operations) {
1441
+ this.operations.push(...operations);
1442
+ }
1443
+ getCollections() {
1444
+ return this.collections;
1445
+ }
1446
+ getGroupReferenceRecords() {
1447
+ return this.groupReferences;
1448
+ }
1449
+ getCollectionMembershipRecords() {
1450
+ return this.collectionMemberships;
1451
+ }
1452
+ getCollectionRemovals() {
1453
+ return this.collectionRemovals;
1454
+ }
1455
+ getOperations() {
1456
+ return this.operations;
1457
+ }
1458
+ };
1459
+ var KyselyOperationIndex = class KyselyOperationIndex {
1460
+ db;
1461
+ trx;
1462
+ liveIds;
1463
+ constructor(db) {
1464
+ this.db = db;
1465
+ }
1466
+ get queryExecutor() {
1467
+ return this.trx ?? this.db;
1468
+ }
1469
+ /** `liveIds`: ids the transaction read untombstoned under its shared lock. */
1470
+ withTransaction(trx, liveIds) {
1471
+ const instance = new KyselyOperationIndex(this.db);
1472
+ instance.trx = trx;
1473
+ instance.liveIds = liveIds;
1474
+ return instance;
1475
+ }
1476
+ start() {
1477
+ return new KyselyOperationIndexTxn();
1478
+ }
1479
+ async commit(txn, signal) {
1480
+ if (signal?.aborted) throw new Error("Operation aborted");
1481
+ const kyselyTxn = txn;
1482
+ if (this.trx) return this.executeCommit(this.trx, kyselyTxn);
1483
+ let resultOrdinals = [];
1484
+ await this.db.transaction().execute(async (trx) => {
1485
+ resultOrdinals = await this.executeCommit(trx, kyselyTxn);
1486
+ });
1487
+ return resultOrdinals;
1488
+ }
1489
+ /**
1490
+ * A policy-driven join: keeps the earliest join so a rediscovered reference
1491
+ * never shrinks a backfill window remotes already rely on, and reopens a
1492
+ * closed membership because a policy reference is not a removable one.
1493
+ */
1494
+ async joinKeepingEarliest(trx, kyselyTxn, documentId, collectionId, ordinal) {
1495
+ kyselyTxn.recordMembershipInvalidation(documentId);
1496
+ await trx.insertInto("document_collections").values({
1497
+ documentId,
1498
+ collectionId,
1499
+ joinedOrdinal: ordinal,
1500
+ leftOrdinal: null
1501
+ }).onConflict((oc) => oc.columns(["documentId", "collectionId"]).doUpdateSet({
1502
+ joinedOrdinal: sql`LEAST("document_collections"."joinedOrdinal", EXCLUDED."joinedOrdinal")`,
1503
+ leftOrdinal: null
1504
+ })).execute();
1505
+ }
1506
+ async executeCommit(trx, kyselyTxn) {
1507
+ const collections = kyselyTxn.getCollections();
1508
+ const memberships = kyselyTxn.getCollectionMembershipRecords();
1509
+ const removals = kyselyTxn.getCollectionRemovals();
1510
+ const groupReferences = kyselyTxn.getGroupReferenceRecords();
1511
+ const operations = kyselyTxn.getOperations();
1512
+ if (collections.length > 0) {
1513
+ const collectionRows = collections.map((collectionId) => ({
1514
+ documentId: collectionId,
1515
+ collectionId,
1516
+ joinedOrdinal: BigInt(0),
1517
+ leftOrdinal: null
1518
+ }));
1519
+ for (const collectionId of collections) kyselyTxn.recordMembershipInvalidation(collectionId);
1520
+ for (const chunk of chunkRows(collectionRows)) await trx.insertInto("document_collections").values(chunk).onConflict((oc) => oc.doNothing()).execute();
1521
+ }
1522
+ let operationOrdinals = [];
1523
+ if (operations.length > 0) {
1524
+ await this.refusePurgedOperations(trx, operations);
1525
+ await this.assignXid(trx);
1526
+ const operationRows = operations.map((op) => ({
1527
+ opId: op.id || "",
1528
+ documentId: op.documentId,
1529
+ documentType: op.documentType,
1530
+ scope: op.scope,
1531
+ branch: op.branch,
1532
+ timestampUtcMs: op.timestampUtcMs,
1533
+ index: op.index,
1534
+ skip: op.skip,
1535
+ hash: op.hash,
1536
+ action: op.action,
1537
+ deniedReason: op.deniedReason ?? null,
1538
+ sourceRemote: op.sourceRemote
1539
+ }));
1540
+ for (const chunk of chunkRows(operationRows)) {
1541
+ const insertedOps = await trx.insertInto("operation_index_operations").values(chunk).returning("ordinal").execute();
1542
+ operationOrdinals = operationOrdinals.concat(insertedOps.map((row) => row.ordinal));
1543
+ }
1544
+ }
1545
+ if (memberships.length > 0) for (const m of memberships) {
1546
+ const ordinal = operationOrdinals[m.operationIndex];
1547
+ kyselyTxn.recordMembershipInvalidation(m.documentId);
1548
+ await trx.insertInto("document_collections").values({
1549
+ documentId: m.documentId,
1550
+ collectionId: m.collectionId,
1551
+ joinedOrdinal: BigInt(ordinal),
1552
+ leftOrdinal: null
1553
+ }).onConflict((oc) => oc.columns(["documentId", "collectionId"]).doUpdateSet({
1554
+ joinedOrdinal: BigInt(ordinal),
1555
+ leftOrdinal: null
1556
+ })).execute();
1557
+ const references = await trx.selectFrom("group_references").select("groupId").where("documentId", "=", m.documentId).execute();
1558
+ for (const { groupId } of references) await this.joinKeepingEarliest(trx, kyselyTxn, groupId, m.collectionId, BigInt(ordinal));
1559
+ }
1560
+ if (removals.length > 0) for (const r of removals) {
1561
+ const ordinal = operationOrdinals[r.operationIndex];
1562
+ kyselyTxn.recordMembershipInvalidation(r.documentId);
1563
+ await trx.updateTable("document_collections").set({ leftOrdinal: BigInt(ordinal) }).where("collectionId", "=", r.collectionId).where("documentId", "=", r.documentId).where("leftOrdinal", "is", null).execute();
1564
+ }
1565
+ if (groupReferences.length > 0) for (const record of groupReferences) {
1566
+ const ordinal = operationOrdinals[record.operationIndex];
1567
+ await trx.insertInto("group_references").values(record.groupIds.map((groupId) => ({
1568
+ documentId: record.documentId,
1569
+ groupId
1570
+ }))).onConflict((oc) => oc.doNothing()).execute();
1571
+ const rows = await trx.selectFrom("document_collections").select("collectionId").where("documentId", "=", record.documentId).execute();
1572
+ for (const groupId of record.groupIds) for (const { collectionId } of rows) await this.joinKeepingEarliest(trx, kyselyTxn, groupId, collectionId, BigInt(ordinal));
1573
+ }
1574
+ return operationOrdinals;
1575
+ }
1576
+ async getCollectionsInRange(after, through, among, signal) {
1577
+ signal?.throwIfAborted();
1578
+ if (through <= after || among?.length === 0) return [];
1579
+ 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);
1580
+ if (among !== void 0) query = query.where("dc.collectionId", "in", [...among]);
1581
+ return (await query.execute()).map((row) => row.collectionId);
1582
+ }
1583
+ async getOrdinalsInRange(after, through, limit, signal) {
1584
+ signal?.throwIfAborted();
1585
+ if (through <= after || limit <= 0) return [];
1586
+ 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));
1587
+ }
1588
+ async getByOrdinals(ordinals, signal) {
1589
+ signal?.throwIfAborted();
1590
+ if (ordinals.length === 0) return [];
1591
+ const results = [];
1592
+ for (let i = 0; i < ordinals.length; i += MAX_BIND_PARAMETERS) {
1593
+ const chunk = ordinals.slice(i, i + MAX_BIND_PARAMETERS);
1594
+ const rows = await this.queryExecutor.selectFrom("operation_index_operations").selectAll().where("ordinal", "in", chunk).orderBy("ordinal", "asc").execute();
1595
+ for (const row of rows) results.push(this.rowToOperationWithContext(row));
1596
+ }
1597
+ results.sort((a, b) => a.context.ordinal - b.context.ordinal);
1598
+ return results;
1599
+ }
1600
+ async getStreamAfter(stream, after, signal, limit) {
1601
+ signal?.throwIfAborted();
1602
+ 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));
1603
+ }
1604
+ async getGroupReferencers(groupId, signal) {
1605
+ if (signal?.aborted) throw new Error("Operation aborted");
1606
+ return (await this.queryExecutor.selectFrom("group_references").select("documentId").where("groupId", "=", groupId).orderBy("documentId").execute()).map((row) => row.documentId);
1607
+ }
1608
+ async getOrdinalsByOpIds(documentId, scope, branch, opIds, signal) {
1609
+ signal?.throwIfAborted();
1610
+ if (opIds.length === 0) return /* @__PURE__ */ new Map();
1611
+ 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();
1612
+ return new Map(rows.map((row) => [row.opId, Number(row.ordinal)]));
1613
+ }
1614
+ async find(collectionId, cursor, view, paging, signal) {
1615
+ if (signal?.aborted) throw new Error("Operation aborted");
1616
+ const outerCursor = cursor ?? -1;
1617
+ const limit = paging?.limit ?? 500;
1618
+ const pagingCursorOrdinal = paging?.cursor !== void 0 ? Number.parseInt(paging.cursor, 10) : -1;
1619
+ const buildBranch = (kind) => {
1620
+ let qb = this.queryExecutor.selectFrom("operation_index_operations as oi").innerJoin("document_collections as dc", "oi.documentId", "dc.documentId").selectAll("oi").select(["dc.documentId", "dc.collectionId"]).where("dc.collectionId", "=", collectionId).where(sql`(dc."leftOrdinal" IS NULL OR oi.ordinal < dc."leftOrdinal")`);
1621
+ if (kind === "joiner") qb = qb.where("dc.joinedOrdinal", ">", BigInt(outerCursor)).where("oi.ordinal", "<=", outerCursor);
1622
+ else qb = qb.where("oi.ordinal", ">", outerCursor);
1623
+ qb = qb.where("oi.ordinal", ">", pagingCursorOrdinal);
1624
+ if (view?.branch) qb = qb.where("oi.branch", "=", view.branch);
1625
+ if (view?.scopes && view.scopes.length > 0) qb = qb.where("oi.scope", "in", view.scopes);
1626
+ if (view?.excludeSourceRemote) qb = qb.where("oi.sourceRemote", "!=", view.excludeSourceRemote);
1627
+ if (view?.throughOrdinal !== void 0) qb = qb.where("oi.ordinal", "<=", view.throughOrdinal).where("dc.joinedOrdinal", "<=", BigInt(view.throughOrdinal));
1628
+ return qb;
1629
+ };
1630
+ const rows = await buildBranch("joiner").unionAll(buildBranch("newOps")).orderBy("ordinal", "asc").limit(limit + 1).execute();
1631
+ let hasMore = false;
1632
+ let items = rows;
1633
+ if (rows.length > limit) {
1634
+ hasMore = true;
1635
+ items = rows.slice(0, limit);
1636
+ }
1637
+ const nextCursor = hasMore && items.length > 0 ? items[items.length - 1].ordinal.toString() : void 0;
1638
+ const cursorValue = paging?.cursor || "0";
1639
+ return {
1640
+ results: items.map((row) => this.rowToOperationIndexEntry(row)),
1641
+ options: {
1642
+ cursor: cursorValue,
1643
+ limit
1644
+ },
1645
+ nextCursor,
1646
+ next: hasMore ? () => this.find(collectionId, cursor, view, {
1647
+ cursor: nextCursor,
1648
+ limit
1649
+ }, signal) : void 0
1650
+ };
1651
+ }
1652
+ async get(documentId, view, paging, signal) {
1653
+ if (signal?.aborted) throw new Error("Operation aborted");
1654
+ const limit = paging?.limit ?? 500;
1655
+ let query = this.queryExecutor.selectFrom("operation_index_operations").selectAll().where("documentId", "=", documentId).orderBy("ordinal", "asc");
1656
+ if (view?.branch) query = query.where("branch", "=", view.branch);
1657
+ if (view?.scopes && view.scopes.length > 0) query = query.where("scope", "in", view.scopes);
1658
+ if (paging?.cursor) {
1659
+ const cursorOrdinal = Number.parseInt(paging.cursor, 10);
1660
+ query = query.where("ordinal", ">", cursorOrdinal);
1661
+ }
1662
+ query = query.limit(limit + 1);
1663
+ const rows = await query.execute();
1664
+ let hasMore = false;
1665
+ let items = rows;
1666
+ if (rows.length > limit) {
1667
+ hasMore = true;
1668
+ items = rows.slice(0, limit);
1669
+ }
1670
+ const nextCursor = hasMore && items.length > 0 ? items[items.length - 1].ordinal.toString() : void 0;
1671
+ const cursorValue = paging?.cursor || "0";
1672
+ return {
1673
+ results: items.map((row) => this.rowToOperationIndexEntry(row)),
1674
+ options: {
1675
+ cursor: cursorValue,
1676
+ limit
1677
+ },
1678
+ nextCursor,
1679
+ next: hasMore ? () => this.get(documentId, view, {
1680
+ cursor: nextCursor,
1681
+ limit
1682
+ }, signal) : void 0
1683
+ };
1684
+ }
1685
+ async getSinceOrdinal(ordinal, paging, signal) {
1686
+ if (signal?.aborted) throw new Error("Operation aborted");
1687
+ const limit = paging?.limit ?? 500;
1688
+ let query = this.queryExecutor.selectFrom("operation_index_operations").selectAll().where("ordinal", ">", ordinal).orderBy("ordinal", "asc");
1689
+ if (paging?.cursor) {
1690
+ const cursorOrdinal = Number.parseInt(paging.cursor, 10);
1691
+ query = query.where("ordinal", ">", cursorOrdinal);
1692
+ }
1693
+ query = query.limit(limit + 1);
1694
+ const rows = await query.execute();
1695
+ let hasMore = false;
1696
+ let items = rows;
1697
+ if (rows.length > limit) {
1698
+ hasMore = true;
1699
+ items = rows.slice(0, limit);
1700
+ }
1701
+ const nextCursor = hasMore && items.length > 0 ? items[items.length - 1].ordinal.toString() : void 0;
1702
+ const cursorValue = paging?.cursor || "0";
1703
+ return {
1704
+ results: items.map((row) => this.rowToOperationWithContext(row)),
1705
+ options: {
1706
+ cursor: cursorValue,
1707
+ limit
1708
+ },
1709
+ nextCursor,
1710
+ next: hasMore ? () => this.getSinceOrdinal(ordinal, {
1711
+ cursor: nextCursor,
1712
+ limit
1713
+ }, signal) : void 0
1714
+ };
1715
+ }
1716
+ /** Backstop to the store's refusal: only a marker indexes a purged id. */
1717
+ async refusePurgedOperations(trx, operations) {
1718
+ const ids = new Set(operations.filter((operation) => !isPurgeMarker(operation)).map((operation) => operation.documentId).filter((id) => !this.liveIds?.has(id)));
1719
+ const purged = await findPurged(trx, ids);
1720
+ for (const id of ids) if (purged.has(id)) throw new DocumentPurgedError(id);
1721
+ }
1722
+ /** Takes the xid before the first ordinal, as the settled watermark needs. */
1723
+ async assignXid(trx) {
1724
+ let fn = xidFunctions.get(this.db);
1725
+ if (fn === void 0) {
1726
+ fn = readSnapshotFunctions(trx).then((fns) => fns.currentXid);
1727
+ xidFunctions.set(this.db, fn);
1728
+ fn.catch(() => xidFunctions.delete(this.db));
1729
+ }
1730
+ const name = await fn;
1731
+ await sql`select ${sql.raw(name)}()`.execute(trx);
1732
+ }
1733
+ rowToOperationWithContext(row) {
1734
+ return {
1735
+ operation: {
1736
+ index: row.index,
1737
+ timestampUtcMs: row.timestampUtcMs,
1738
+ hash: row.hash,
1739
+ skip: row.skip,
1740
+ action: row.action,
1741
+ deniedReason: row.deniedReason ?? void 0,
1742
+ id: row.opId
1743
+ },
1744
+ context: {
1745
+ documentId: row.documentId,
1746
+ documentType: row.documentType,
1747
+ scope: row.scope,
1748
+ branch: row.branch,
1749
+ ordinal: row.ordinal
1750
+ }
1751
+ };
1752
+ }
1753
+ rowToOperationIndexEntry(row) {
1754
+ return {
1755
+ ordinal: row.ordinal,
1756
+ documentId: row.documentId,
1757
+ documentType: row.documentType,
1758
+ branch: row.branch,
1759
+ scope: row.scope,
1760
+ index: row.index,
1761
+ timestampUtcMs: row.timestampUtcMs,
1762
+ hash: row.hash,
1763
+ skip: row.skip,
1764
+ action: row.action,
1765
+ deniedReason: row.deniedReason ?? void 0,
1766
+ id: row.opId,
1767
+ sourceRemote: row.sourceRemote
1768
+ };
1769
+ }
1770
+ async getLatestTimestampForCollection(collectionId, signal) {
1771
+ if (signal?.aborted) throw new Error("Operation aborted");
1772
+ return (await this.queryExecutor.selectFrom("operation_index_operations as oi").innerJoin("document_collections as dc", "oi.documentId", "dc.documentId").select("oi.timestampUtcMs").where("dc.collectionId", "=", collectionId).where(sql`(dc."leftOrdinal" IS NULL OR oi.ordinal < dc."leftOrdinal")`).orderBy("oi.ordinal", "desc").limit(1).executeTakeFirst())?.timestampUtcMs ?? null;
1773
+ }
1774
+ async getCollectionsForDocuments(documentIds) {
1775
+ if (documentIds.length === 0) return {};
1776
+ const rows = await this.queryExecutor.selectFrom("document_collections").select(["documentId", "collectionId"]).where("documentId", "in", documentIds).where("leftOrdinal", "is", null).execute();
1777
+ const result = {};
1778
+ for (const row of rows) {
1779
+ if (!(row.documentId in result)) result[row.documentId] = [];
1780
+ result[row.documentId].push(row.collectionId);
1781
+ }
1782
+ return result;
1783
+ }
1784
+ };
1785
+ //#endregion
1786
+ //#region src/cache/buffer/ring-buffer.ts
1787
+ /**
1788
+ * RingBuffer is a generic circular buffer implementation that stores a fixed number
1789
+ * of items. When the buffer is full, new items overwrite the oldest items.
1790
+ *
1791
+ * This implementation maintains O(1) time complexity for push operations and provides
1792
+ * items in chronological order (oldest to newest) via getAll().
1793
+ *
1794
+ * @template T - The type of items stored in the buffer
1795
+ */
1796
+ var RingBuffer = class {
1797
+ buffer;
1798
+ head = 0;
1799
+ size = 0;
1800
+ capacity;
1801
+ constructor(capacity) {
1802
+ if (capacity <= 0) throw new Error("Ring buffer capacity must be greater than 0");
1803
+ this.capacity = capacity;
1804
+ this.buffer = new Array(capacity);
1805
+ }
1806
+ /**
1807
+ * Adds an item to the buffer. If the buffer is full, overwrites the oldest item.
1808
+ *
1809
+ * @param item - The item to add
1810
+ */
1811
+ push(item) {
1812
+ const index = (this.head + this.size) % this.capacity;
1813
+ if (this.size < this.capacity) {
1814
+ this.buffer[index] = item;
1815
+ this.size++;
1816
+ } else {
1817
+ this.buffer[this.head] = item;
1818
+ this.head = (this.head + 1) % this.capacity;
1819
+ }
1820
+ }
1821
+ /**
1822
+ * Returns all items in the buffer in chronological order (oldest to newest).
1823
+ *
1824
+ * @returns Array of items in insertion order
1825
+ */
1826
+ getAll() {
1827
+ if (this.size === 0) return [];
1828
+ const result = [];
1829
+ for (let i = 0; i < this.size; i++) {
1830
+ const index = (this.head + i) % this.capacity;
1831
+ result.push(this.buffer[index]);
1832
+ }
1833
+ return result;
1834
+ }
1835
+ /**
1836
+ * Clears all items from the buffer.
1837
+ */
1838
+ clear() {
1839
+ this.buffer = new Array(this.capacity);
1840
+ this.head = 0;
1841
+ this.size = 0;
1842
+ }
1843
+ /**
1844
+ * Gets the current number of items in the buffer.
1845
+ */
1846
+ get length() {
1847
+ return this.size;
1848
+ }
1849
+ };
1850
+ //#endregion
1851
+ //#region src/cache/kysely-write-cache.ts
1852
+ /**
1853
+ * The last operation index a keyframe's document reflects for the scope. A
1854
+ * keyframe only exists for a scope that has operations, so a missing entry
1855
+ * means the stored row is corrupt.
1856
+ */
1857
+ function keyframeRevision(keyframe, documentId, scope) {
1858
+ const nextIndex = keyframe.document.header.revision[scope];
1859
+ if (typeof nextIndex !== "number") throw new Error(`Corrupt keyframe for document ${documentId} at revision ${keyframe.revision}: header carries no ${scope} revision`);
1860
+ return nextIndex - 1;
1861
+ }
1862
+ function extractModuleVersion(doc) {
1863
+ const v = doc.state.document.version;
1864
+ return normalizeDocumentModelVersion(v);
1865
+ }
1866
+ /** The highest revision held, latest push winning a tie. */
1867
+ function highestRevision(snapshots) {
1868
+ let newest = void 0;
1869
+ for (const snapshot of snapshots) if (!newest || snapshot.revision >= newest.revision) newest = snapshot;
1870
+ return newest;
1871
+ }
1872
+ /**
1873
+ * Copies a document far enough that the caller cannot write through it. Inside
1874
+ * this class, callers only ever replace whole fields on these four, so one
1875
+ * level each is enough.
1876
+ */
1877
+ function copyDocument(document) {
1878
+ return {
1879
+ ...document,
1880
+ header: { ...document.header },
1881
+ state: { ...document.state },
1882
+ operations: { ...document.operations }
1883
+ };
1884
+ }
1885
+ var KyselyWriteCache = class KyselyWriteCache {
1886
+ streams;
1887
+ lruTracker;
1888
+ keyframeStore;
1889
+ operationStore;
1890
+ registry;
1891
+ config;
1892
+ reads = /* @__PURE__ */ new Map();
1893
+ constructor(keyframeStore, operationStore, registry, config) {
1894
+ this.keyframeStore = keyframeStore;
1895
+ this.operationStore = operationStore;
1896
+ this.registry = registry;
1897
+ this.config = {
1898
+ maxDocuments: config.maxDocuments,
1899
+ ringBufferSize: config.ringBufferSize,
1900
+ keyframeInterval: config.keyframeInterval
1901
+ };
1902
+ this.streams = /* @__PURE__ */ new Map();
1903
+ this.lruTracker = new LRUTracker();
1904
+ }
1905
+ withScopedStores(operationStore, keyframeStore) {
1906
+ const scoped = new KyselyWriteCache(keyframeStore, operationStore, this.registry, this.config);
1907
+ scoped.streams = this.streams;
1908
+ scoped.lruTracker = this.lruTracker;
1909
+ scoped.reads = this.reads;
1910
+ return scoped;
1911
+ }
1912
+ /**
1913
+ * Initializes the write cache.
1914
+ * Currently a no-op as keyframe store lifecycle is managed externally.
1915
+ */
1916
+ async startup() {
1917
+ return Promise.resolve();
1918
+ }
1919
+ /**
1920
+ * Shuts down the write cache.
1921
+ * Currently a no-op as keyframe store lifecycle is managed externally.
1922
+ */
1923
+ async shutdown() {
1924
+ return Promise.resolve();
1925
+ }
1926
+ /**
1927
+ * Retrieves document state at a specific revision from cache or rebuilds it.
1928
+ *
1929
+ * Note: this returns a _shallow_ copy of the document.
1930
+ *
1931
+ * Cache hit path: Returns cached snapshot if available (O(1))
1932
+ * Warm miss path: Rebuilds from cached base revision + incremental ops
1933
+ * Cold miss path: Rebuilds from keyframe or from scratch using all operations
1934
+ *
1935
+ * @param documentId - The document identifier
1936
+ * @param scope - The operation scope
1937
+ * @param branch - The operation branch
1938
+ * @param targetRevision - The target revision, or undefined for newest
1939
+ * @param signal - Optional abort signal to cancel the operation
1940
+ * @returns The document at the target revision
1941
+ * @throws {Error} "Operation aborted" if signal is aborted
1942
+ * @throws {ModuleNotFoundError} If document type not registered in registry
1943
+ * @throws {DocumentPurgedError} If the stream's only row is a purge marker
1944
+ * @throws {Error} "Failed to rebuild document" if operation store fails
1945
+ * @throws {Error} If reducer throws during operation application
1946
+ * @throws {Error} If document serialization fails
1947
+ */
1948
+ async getState(documentId, scope, branch, targetRevision, signal) {
1949
+ const read = { evicted: false };
1950
+ let pending = this.reads.get(documentId);
1951
+ if (!pending) {
1952
+ pending = /* @__PURE__ */ new Set();
1953
+ this.reads.set(documentId, pending);
1954
+ }
1955
+ pending.add(read);
1956
+ try {
1957
+ return await this.readState(documentId, scope, branch, targetRevision, signal, read);
1958
+ } finally {
1959
+ pending.delete(read);
1960
+ if (pending.size === 0) this.reads.delete(documentId);
1961
+ }
1962
+ }
1963
+ async readState(documentId, scope, branch, targetRevision, signal, read) {
1964
+ if (signal?.aborted) throw new Error("Operation aborted");
1965
+ const streamKey = this.makeStreamKey(documentId, scope, branch);
1966
+ const stream = this.streams.get(streamKey);
1967
+ if (stream) {
1968
+ const snapshots = stream.ringBuffer.getAll();
1969
+ if (targetRevision === void 0) {
1970
+ const newest = highestRevision(snapshots);
1971
+ if (newest?.position === "head") {
1972
+ this.lruTracker.touch(streamKey);
1973
+ return copyDocument(newest.document);
1974
+ }
1975
+ if (newest) {
1976
+ const document = await this.warmMissRebuild(newest.document, newest.revision, documentId, scope, branch, void 0, signal);
1977
+ this.storeUnlessEvicted(read, documentId, scope, branch, (document.header.revision[scope] ?? 0) - 1, document, "head");
1978
+ this.lruTracker.touch(streamKey);
1979
+ return document;
1980
+ }
1981
+ } else {
1982
+ const exactMatch = snapshots.findLast((s) => s.revision === targetRevision);
1983
+ if (exactMatch) {
1984
+ this.lruTracker.touch(streamKey);
1985
+ return copyDocument(exactMatch.document);
1986
+ }
1987
+ const newestOlder = this.findNearestOlderSnapshot(snapshots, targetRevision);
1988
+ if (newestOlder) {
1989
+ const document = await this.warmMissRebuild(newestOlder.document, newestOlder.revision, documentId, scope, branch, targetRevision, signal);
1990
+ this.storeUnlessEvicted(read, documentId, scope, branch, targetRevision, document, "historical");
1991
+ this.lruTracker.touch(streamKey);
1992
+ return document;
1993
+ }
1994
+ }
1995
+ }
1996
+ const document = await this.coldMissRebuild(documentId, scope, branch, targetRevision, signal);
1997
+ const revision = targetRevision ?? (document.header.revision[scope] ?? 0) - 1;
1998
+ this.storeUnlessEvicted(read, documentId, scope, branch, revision, document, targetRevision === void 0 ? "head" : "historical");
1999
+ return document;
2000
+ }
2001
+ /**
2002
+ * Stores a document snapshot in the cache at a specific revision.
2003
+ *
2004
+ * The cached document is a shallow copy of the input with its operation history
2005
+ * truncated to the last operation per scope and its clipboard cleared. This keeps
2006
+ * memory use and copy costs constant regardless of operation count. Consumers of
2007
+ * getState() must not rely on the full operation history being present; the only
2008
+ * guaranteed invariant is that operations[scope].at(-1) reflects the latest
2009
+ * operation index for each scope.
2010
+ *
2011
+ * Updates LRU tracker and may evict least recently used stream if at capacity.
2012
+ * Asynchronously persists keyframes at configured intervals (fire-and-forget).
2013
+ *
2014
+ * @param documentId - The document identifier
2015
+ * @param scope - The operation scope
2016
+ * @param branch - The operation branch
2017
+ * @param revision - The revision number
2018
+ * @param document - The document to cache
2019
+ * @throws {Error} If document serialization fails
2020
+ */
2021
+ putState(documentId, scope, branch, revision, document, position) {
2022
+ this.store(documentId, scope, branch, revision, document, position);
2023
+ }
2024
+ /**
2025
+ * Stores the run's head and mints a keyframe for every interval the run
2026
+ * crossed on its way there. Only the head enters the ring buffer; the
2027
+ * earlier revisions are keyframe candidates and nothing more.
2028
+ */
2029
+ putRun(documentId, scope, branch, run) {
2030
+ if (run.length === 0) return;
2031
+ for (const entry of run.slice(0, -1)) this.persistKeyframe(documentId, scope, branch, entry.revision, entry.document);
2032
+ const head = run[run.length - 1];
2033
+ this.store(documentId, scope, branch, head.revision, head.document, "head");
2034
+ }
2035
+ storeUnlessEvicted(read, documentId, scope, branch, revision, document, position) {
2036
+ if (!read.evicted) this.store(documentId, scope, branch, revision, document, position);
2037
+ }
2038
+ store(documentId, scope, branch, revision, document, position) {
2039
+ const streamKey = this.makeStreamKey(documentId, scope, branch);
2040
+ const stream = this.getOrCreateStream(streamKey);
2041
+ const snapshot = {
2042
+ revision,
2043
+ document: {
2044
+ ...copyDocument(document),
2045
+ operations: Object.fromEntries(Object.entries(document.operations).map(([k, ops]) => [k, ops.length ? [ops.at(-1)] : []])),
2046
+ clipboard: []
2047
+ },
2048
+ position
2049
+ };
2050
+ stream.ringBuffer.push(snapshot);
2051
+ this.persistKeyframe(documentId, scope, branch, revision, document);
2052
+ }
2053
+ /** Persists the snapshot if this revision is one the interval falls on. */
2054
+ persistKeyframe(documentId, scope, branch, revision, document) {
2055
+ if (!this.isKeyframeRevision(revision)) return;
2056
+ this.keyframeStore.putKeyframe(documentId, scope, branch, revision, {
2057
+ ...document,
2058
+ operations: {},
2059
+ clipboard: []
2060
+ }).catch((err) => {
2061
+ console.error(`Failed to persist keyframe ${documentId}@${revision}:`, err);
2062
+ });
2063
+ }
2064
+ /**
2065
+ * Invalidates cached document streams.
2066
+ *
2067
+ * Supports three invalidation scopes:
2068
+ * - Document-level: invalidate(documentId) - removes all streams for document
2069
+ * - Scope-level: invalidate(documentId, scope) - removes all branches for scope
2070
+ * - Stream-level: invalidate(documentId, scope, branch) - removes specific stream
2071
+ *
2072
+ * @param documentId - The document identifier
2073
+ * @param scope - Optional scope to narrow invalidation
2074
+ * @param branch - Optional branch to narrow invalidation (requires scope)
2075
+ * @returns The number of streams evicted
2076
+ */
2077
+ invalidate(documentId, scope, branch) {
2078
+ for (const read of this.reads.get(documentId) ?? []) read.evicted = true;
2079
+ let evicted = 0;
2080
+ if (scope === void 0 && branch === void 0) {
2081
+ for (const [key] of this.streams.entries()) if (key.startsWith(`${documentId}:`)) {
2082
+ this.streams.delete(key);
2083
+ this.lruTracker.remove(key);
2084
+ evicted++;
2085
+ }
2086
+ } else if (scope !== void 0 && branch === void 0) {
2087
+ for (const [key] of this.streams.entries()) if (key.startsWith(`${documentId}:${scope}:`)) {
2088
+ this.streams.delete(key);
2089
+ this.lruTracker.remove(key);
2090
+ evicted++;
2091
+ }
2092
+ } else if (scope !== void 0 && branch !== void 0) {
2093
+ const key = this.makeStreamKey(documentId, scope, branch);
2094
+ if (this.streams.has(key)) {
2095
+ this.streams.delete(key);
2096
+ this.lruTracker.remove(key);
2097
+ evicted = 1;
2098
+ }
2099
+ }
2100
+ return evicted;
2101
+ }
2102
+ /**
2103
+ * Clears the entire cache, removing all cached document streams.
2104
+ * Resets LRU tracking state. This operation always succeeds.
2105
+ */
2106
+ clear() {
2107
+ for (const pending of this.reads.values()) for (const read of pending) read.evicted = true;
2108
+ this.streams.clear();
2109
+ this.lruTracker.clear();
2110
+ }
2111
+ /**
2112
+ * Retrieves a specific stream for a document. Exposed on the implementation
2113
+ * for testing, but not on the interface.
2114
+ *
2115
+ * @internal
2116
+ */
2117
+ getStream(documentId, scope, branch) {
2118
+ const key = this.makeStreamKey(documentId, scope, branch);
2119
+ return this.streams.get(key);
2120
+ }
2121
+ async findNearestKeyframe(documentId, scope, branch, targetRevision, signal) {
2122
+ if (targetRevision === Number.MAX_SAFE_INTEGER || targetRevision <= 0) return;
2123
+ const keyframe = await this.keyframeStore.findNearestKeyframe(documentId, scope, branch, targetRevision, signal);
2124
+ if (!keyframe) return;
2125
+ return {
2126
+ revision: Math.min(keyframeRevision(keyframe, documentId, scope), keyframe.revision),
2127
+ document: keyframe.document
2128
+ };
2129
+ }
2130
+ /**
2131
+ * Rebuilds a scope from a keyframe or from the whole operation history.
2132
+ *
2133
+ * The document scope is always rebuilt first, because it carries the type,
2134
+ * the upgrades and the deletion marker. Its version-changing upgrades are not
2135
+ * applied there though: an upgrade reducer must see the state the requested
2136
+ * scope has reached at that upgrade's boundary, so each one is held back and
2137
+ * applied when the replay below crosses the boundary that
2138
+ * resolveModuleVersionForOp derives from it. Upgrades whose boundary lies past
2139
+ * the last replayed operation are applied at the end. Creation-time 0->N seed
2140
+ * upgrades carry the initial state, so they still apply immediately.
2141
+ */
2142
+ async coldMissRebuild(documentId, scope, branch, targetRevision, signal, fromKeyframe = true) {
2143
+ const effectiveTargetRevision = targetRevision || Number.MAX_SAFE_INTEGER;
2144
+ const keyframe = fromKeyframe ? await this.findNearestKeyframe(documentId, scope, branch, effectiveTargetRevision, signal) : void 0;
2145
+ const documentScopeBound = scope === "document" ? targetRevision : void 0;
2146
+ let document;
2147
+ let startRevision;
2148
+ let documentType;
2149
+ const validatedUpgrades = [];
2150
+ const pendingUpgrades = [];
2151
+ let lastDocumentScopeOperation;
2152
+ if (keyframe) {
2153
+ document = keyframe.document;
2154
+ startRevision = keyframe.revision;
2155
+ documentType = keyframe.document.header.documentType;
2156
+ const documentScopeResume = scope === "document" ? keyframe.revision : keyframeRevision(keyframe, documentId, "document");
2157
+ const docScopeOpsAfterKeyframe = await this.operationStore.getSince(documentId, "document", branch, documentScopeResume, void 0, void 0, signal);
2158
+ for (const operation of docScopeOpsAfterKeyframe.results) {
2159
+ if (documentScopeBound !== void 0 && operation.index > documentScopeBound) break;
2160
+ lastDocumentScopeOperation = operation;
2161
+ if (operation.error || isDenied(operation)) continue;
2162
+ if (operation.action.type === "UPGRADE_DOCUMENT") {
2163
+ const upgradeAction = operation.action;
2164
+ const fromVersion = upgradeAction.input.fromVersion;
2165
+ const toVersion = upgradeAction.input.toVersion;
2166
+ if (fromVersion > 0 && fromVersion < toVersion) {
2167
+ let upgradePath;
2168
+ try {
2169
+ upgradePath = this.registry.computeUpgradePath(documentType, fromVersion, toVersion);
2170
+ } catch (err) {
2171
+ if (upgradeAction.input.initialState !== void 0) upgradePath = void 0;
2172
+ else throw new Error(`Failed to rebuild document ${documentId}: no upgrade manifest for ${documentType} v${fromVersion}→v${toVersion} and no initialState snapshot. ${err instanceof Error ? err.message : String(err)}`, { cause: err });
2173
+ }
2174
+ validatedUpgrades.push({
2175
+ fromVersion,
2176
+ toVersion,
2177
+ revision: upgradeAction.input.revision,
2178
+ timestampUtcMs: operation.timestampUtcMs
2179
+ });
2180
+ pendingUpgrades.push({
2181
+ action: upgradeAction,
2182
+ upgradePath,
2183
+ index: operation.index,
2184
+ subsequentDeletes: []
2185
+ });
2186
+ }
2187
+ } else if (operation.action.type === "DELETE_DOCUMENT" || operation.action.type === PURGE_DOCUMENT) {
2188
+ applyDeleteDocumentAction(document, operation.action);
2189
+ for (const pending of pendingUpgrades) pending.subsequentDeletes.push(operation.action);
2190
+ }
2191
+ }
2192
+ } else {
2193
+ startRevision = -1;
2194
+ const createOpResult = await this.operationStore.getSince(documentId, "document", branch, -1, void 0, {
2195
+ cursor: "0",
2196
+ limit: 1
2197
+ }, signal);
2198
+ if (createOpResult.results.length === 0) throw new DocumentNotFoundError(documentId);
2199
+ const createOp = createOpResult.results[0];
2200
+ if (isPurgeMarker(createOp)) throw new DocumentPurgedError(documentId);
2201
+ 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}`);
2202
+ const documentCreateAction = createOp.action;
2203
+ documentType = documentCreateAction.input.model;
2204
+ if (!documentType) throw new Error(`Failed to rebuild document ${documentId}: CREATE_DOCUMENT action missing model in input`);
2205
+ document = createDocumentFromAction(documentCreateAction);
2206
+ lastDocumentScopeOperation = createOp;
2207
+ let docModule = this.registry.getModule(documentType, extractModuleVersion(document));
2208
+ const docScopeOps = await this.operationStore.getSince(documentId, "document", branch, 0, void 0, void 0, signal);
2209
+ for (const operation of docScopeOps.results) {
2210
+ if (documentScopeBound !== void 0 && operation.index > documentScopeBound) break;
2211
+ lastDocumentScopeOperation = operation;
2212
+ if (operation.index === 0) continue;
2213
+ if (operation.error || isDenied(operation)) continue;
2214
+ if (operation.action.type === "UPGRADE_DOCUMENT") {
2215
+ const upgradeAction = operation.action;
2216
+ const fromVersion = upgradeAction.input.fromVersion;
2217
+ const toVersion = upgradeAction.input.toVersion;
2218
+ if (fromVersion > 0 && fromVersion < toVersion) {
2219
+ let upgradePath;
2220
+ try {
2221
+ upgradePath = this.registry.computeUpgradePath(documentType, fromVersion, toVersion);
2222
+ } catch (err) {
2223
+ if (upgradeAction.input.initialState !== void 0) upgradePath = void 0;
2224
+ else throw new Error(`Failed to rebuild document ${documentId}: no upgrade manifest for ${documentType} v${fromVersion}→v${toVersion} and no initialState snapshot. ${err instanceof Error ? err.message : String(err)}`, { cause: err });
2225
+ }
2226
+ validatedUpgrades.push({
2227
+ fromVersion,
2228
+ toVersion,
2229
+ revision: upgradeAction.input.revision,
2230
+ timestampUtcMs: operation.timestampUtcMs
2231
+ });
2232
+ pendingUpgrades.push({
2233
+ action: upgradeAction,
2234
+ upgradePath,
2235
+ index: operation.index,
2236
+ subsequentDeletes: []
2237
+ });
2238
+ } else document = applyUpgradeDocumentAction(document, upgradeAction, void 0);
2239
+ docModule = this.registry.getModule(documentType, normalizeDocumentModelVersion(toVersion));
2240
+ } else if (operation.action.type === "DELETE_DOCUMENT" || operation.action.type === PURGE_DOCUMENT) {
2241
+ applyDeleteDocumentAction(document, operation.action);
2242
+ for (const pending of pendingUpgrades) pending.subsequentDeletes.push(operation.action);
2243
+ } else {
2244
+ const protocolVersion = baseReducerVersion(document.header);
2245
+ document = withProtocolVersions(docModule.reducer(document, operation.action, void 0, {
2246
+ skip: operation.skip,
2247
+ protocolVersion
2248
+ }), document.header.protocolVersions);
2249
+ }
2250
+ }
2251
+ }
2252
+ if (scope === "document") {
2253
+ document = this.applyPendingUpgrades(document, pendingUpgrades, Number.MAX_SAFE_INTEGER);
2254
+ const last = lastDocumentScopeOperation ?? await this.operationAt(documentId, "document", branch, startRevision, signal);
2255
+ document.operations = {
2256
+ ...document.operations,
2257
+ document: last ? [last] : []
2258
+ };
2259
+ return this.stampRevisions(document, documentId, scope, branch, targetRevision, signal);
2260
+ }
2261
+ if (keyframe) {
2262
+ const resumeOperation = await this.operationAt(documentId, scope, branch, startRevision, signal);
2263
+ if (resumeOperation) document.operations = {
2264
+ ...document.operations,
2265
+ [scope]: [resumeOperation]
2266
+ };
2267
+ }
2268
+ const moduleCache = /* @__PURE__ */ new Map();
2269
+ const getModuleCached = (version) => {
2270
+ const key = version ?? 0;
2271
+ let mod = moduleCache.get(key);
2272
+ if (!mod) {
2273
+ mod = this.registry.getModule(documentType, version);
2274
+ moduleCache.set(key, mod);
2275
+ }
2276
+ return mod;
2277
+ };
2278
+ const finalVersion = validatedUpgrades.at(-1)?.toVersion ?? extractModuleVersion(document);
2279
+ let cursor = void 0;
2280
+ const pageSize = 100;
2281
+ let hasMorePages;
2282
+ let needsFullReplay = false;
2283
+ do {
2284
+ if (signal?.aborted) throw new Error("Operation aborted");
2285
+ const paging = {
2286
+ cursor: cursor || "0",
2287
+ limit: pageSize
2288
+ };
2289
+ try {
2290
+ const result = await this.operationStore.getSince(documentId, scope, branch, startRevision, void 0, paging, signal);
2291
+ for (const operation of result.results) {
2292
+ if (targetRevision !== void 0 && operation.index > targetRevision) break;
2293
+ if (keyframe && operation.skip > 0) {
2294
+ needsFullReplay = true;
2295
+ break;
2296
+ }
2297
+ const moduleVersion = this.resolveModuleVersionForOp(operation.index, operation.timestampUtcMs, scope, validatedUpgrades, finalVersion);
2298
+ document = this.applyPendingUpgrades(document, pendingUpgrades, moduleVersion ?? Number.MAX_SAFE_INTEGER);
2299
+ if (isDenied(operation)) document = appendWithoutApplying(document, operation, scope);
2300
+ else {
2301
+ const protocolVersion = baseReducerVersion(document.header);
2302
+ document = withProtocolVersions(getModuleCached(moduleVersion).reducer(document, operation.action, void 0, {
2303
+ skip: operation.skip,
2304
+ protocolVersion,
2305
+ replayOptions: { operation },
2306
+ skipIndexValidation: true
2307
+ }), document.header.protocolVersions);
2308
+ }
2309
+ }
2310
+ const reachedTarget = targetRevision !== void 0 && result.results.some((op) => op.index >= targetRevision);
2311
+ hasMorePages = Boolean(result.nextCursor) && !reachedTarget && !needsFullReplay;
2312
+ if (hasMorePages) cursor = result.nextCursor;
2313
+ } catch (err) {
2314
+ throw new Error(`Failed to rebuild document ${documentId}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
2315
+ }
2316
+ } while (hasMorePages);
2317
+ if (needsFullReplay) return this.coldMissRebuild(documentId, scope, branch, targetRevision, signal, false);
2318
+ document = this.applyTailPendingUpgrades(document, pendingUpgrades, scope, targetRevision);
2319
+ document = await this.stampRevisions(document, documentId, scope, branch, targetRevision, signal);
2320
+ if (pendingUpgrades.length > 0) {
2321
+ const firstHeldBack = pendingUpgrades[0];
2322
+ const stamped = document.header.revision["document"] ?? 0;
2323
+ document.header.revision = {
2324
+ ...document.header.revision,
2325
+ document: Math.min(stamped, firstHeldBack.index)
2326
+ };
2327
+ }
2328
+ return document;
2329
+ }
2330
+ /**
2331
+ * Applies and removes every held-back upgrade whose target version is at or
2332
+ * below `throughVersion`, in the order the document scope recorded them.
2333
+ */
2334
+ applyPendingUpgrades(document, pendingUpgrades, throughVersion) {
2335
+ while (pendingUpgrades.length > 0) {
2336
+ const pending = pendingUpgrades[0];
2337
+ if (throughVersion < pending.action.input.toVersion) break;
2338
+ pendingUpgrades.shift();
2339
+ document = this.applyPendingUpgrade(document, pending);
2340
+ }
2341
+ return document;
2342
+ }
2343
+ /**
2344
+ * Applies the remaining held-back upgrades after the requested scope's
2345
+ * replay has finished. A head read applies them all. A positional read
2346
+ * applies only those whose boundary for this scope lies at or before the
2347
+ * target position: applying a later one would label migrated state with a
2348
+ * pre-upgrade revision, and a keyframe stored from that poisons every
2349
+ * rebuild that resumes from it. Boundaries come from the upgrade's revision
2350
+ * snapshot; an upgrade without one records no position for this scope, and
2351
+ * the replay loop not having crossed it already places it past the target.
2352
+ */
2353
+ applyTailPendingUpgrades(document, pendingUpgrades, scope, targetRevision) {
2354
+ while (pendingUpgrades.length > 0) {
2355
+ const pending = pendingUpgrades[0];
2356
+ if (targetRevision !== void 0) {
2357
+ const snapshot = pending.action.input.revision;
2358
+ if (snapshot === void 0) break;
2359
+ if ((snapshot[scope] ?? 0) > targetRevision) break;
2360
+ }
2361
+ pendingUpgrades.shift();
2362
+ document = this.applyPendingUpgrade(document, pending);
2363
+ }
2364
+ return document;
2365
+ }
2366
+ /**
2367
+ * Applies one held-back upgrade, then re-applies the deletes the document
2368
+ * scope recorded after it so the hold-back cannot invert their order.
2369
+ */
2370
+ applyPendingUpgrade(document, pending) {
2371
+ document = applyUpgradeDocumentAction(document, pending.action, pending.upgradePath);
2372
+ for (const deleteAction of pending.subsequentDeletes) document = applyDeleteDocumentAction(document, deleteAction);
2373
+ return document;
2374
+ }
2375
+ /**
2376
+ * Copies the current document revisions onto the document. Overwrites the
2377
+ * requested scope revision with the target revision, if provided.
2378
+ */
2379
+ async stampRevisions(document, documentId, scope, branch, targetRevision, signal) {
2380
+ const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
2381
+ document.header.revision = revisions.revision;
2382
+ if (targetRevision !== void 0) document.header.revision = {
2383
+ ...document.header.revision,
2384
+ [scope]: targetRevision + 1
2385
+ };
2386
+ document.header.lastModifiedAtUtcIso = revisions.latestTimestamp;
2387
+ return document;
2388
+ }
2389
+ /** The stored operation at `index`, or undefined if it is no longer there. */
2390
+ async operationAt(documentId, scope, branch, index, signal) {
2391
+ if (index < 0) return;
2392
+ const operation = (await this.operationStore.getSince(documentId, scope, branch, index - 1, void 0, {
2393
+ cursor: "0",
2394
+ limit: 1
2395
+ }, signal)).results[0];
2396
+ return operation && operation.index === index ? operation : void 0;
2397
+ }
2398
+ /**
2399
+ * Resolves which module version to use for a given operation in phase 2.
2400
+ *
2401
+ * Uses the validated-upgrade boundary rules from D7:
2402
+ * - If `input.revision` is present: op.index < revision[scope] → before the upgrade boundary
2403
+ * - Otherwise: timestamp fallback
2404
+ * - Falls back to final module version when neither is decidable
2405
+ */
2406
+ resolveModuleVersionForOp(opIndex, opTimestamp, scope, validatedUpgrades, finalVersion) {
2407
+ if (validatedUpgrades.length === 0) return finalVersion;
2408
+ let currentVersion = validatedUpgrades[0]?.fromVersion;
2409
+ for (const upgrade of validatedUpgrades) {
2410
+ let beforeUpgrade;
2411
+ if (upgrade.revision !== void 0) beforeUpgrade = opIndex < (upgrade.revision[scope] ?? 0);
2412
+ else beforeUpgrade = opTimestamp < upgrade.timestampUtcMs;
2413
+ if (beforeUpgrade) return currentVersion;
2414
+ currentVersion = upgrade.toVersion;
2415
+ }
2416
+ return currentVersion;
2417
+ }
2418
+ async warmMissRebuild(baseDocument, baseRevision, documentId, scope, branch, targetRevision, signal) {
2419
+ const documentType = baseDocument.header.documentType;
2420
+ const docScopeNextIndex = baseDocument.header.revision["document"] ?? 0;
2421
+ 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);
2422
+ const module = this.registry.getModule(documentType, extractModuleVersion(baseDocument));
2423
+ let document = copyDocument(baseDocument);
2424
+ let needsFullReplay = false;
2425
+ try {
2426
+ const pagedResults = await this.operationStore.getSince(documentId, scope, branch, baseRevision, void 0, void 0, signal);
2427
+ for (const operation of pagedResults.results) {
2428
+ if (signal?.aborted) throw new Error("Operation aborted");
2429
+ if (targetRevision !== void 0 && operation.index > targetRevision) break;
2430
+ if (operation.skip > 0) {
2431
+ needsFullReplay = true;
2432
+ break;
2433
+ }
2434
+ if (isDenied(operation)) document = appendWithoutApplying(document, operation, scope);
2435
+ else {
2436
+ const protocolVersion = baseReducerVersion(document.header);
2437
+ document = withProtocolVersions(module.reducer(document, operation.action, void 0, {
2438
+ skip: operation.skip,
2439
+ protocolVersion,
2440
+ replayOptions: { operation },
2441
+ skipIndexValidation: true
2442
+ }), document.header.protocolVersions);
2443
+ }
2444
+ if (targetRevision !== void 0 && operation.index === targetRevision) break;
2445
+ }
2446
+ } catch (err) {
2447
+ throw new Error(`Failed to rebuild document ${documentId}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
2448
+ }
2449
+ if (needsFullReplay) return this.coldMissRebuild(documentId, scope, branch, targetRevision, signal, false);
2450
+ const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
2451
+ document.header.revision = revisions.revision;
2452
+ if (targetRevision !== void 0) document.header.revision = {
2453
+ ...document.header.revision,
2454
+ [scope]: targetRevision + 1
2455
+ };
2456
+ document.header.lastModifiedAtUtcIso = revisions.latestTimestamp;
2457
+ return document;
2458
+ }
2459
+ findNearestOlderSnapshot(snapshots, targetRevision) {
2460
+ let nearest = void 0;
2461
+ for (const snapshot of snapshots) if (snapshot.revision < targetRevision) {
2462
+ if (!nearest || snapshot.revision > nearest.revision) nearest = snapshot;
2463
+ }
2464
+ return nearest;
2465
+ }
2466
+ makeStreamKey(documentId, scope, branch) {
2467
+ return `${documentId}:${scope}:${branch}`;
2468
+ }
2469
+ getOrCreateStream(key) {
2470
+ let stream = this.streams.get(key);
2471
+ if (!stream) {
2472
+ if (this.streams.size >= this.config.maxDocuments) {
2473
+ const evictKey = this.lruTracker.evict();
2474
+ if (evictKey) this.streams.delete(evictKey);
2475
+ }
2476
+ stream = {
2477
+ key,
2478
+ ringBuffer: new RingBuffer(this.config.ringBufferSize)
2479
+ };
2480
+ this.streams.set(key, stream);
2481
+ }
2482
+ this.lruTracker.touch(key);
2483
+ return stream;
2484
+ }
2485
+ isKeyframeRevision(revision) {
2486
+ return revision > 0 && revision % this.config.keyframeInterval === 0;
2487
+ }
2488
+ };
2489
+ //#endregion
2490
+ //#region src/events/event-bus.ts
2491
+ var EventBus = class {
2492
+ eventTypeToSubscribers = /* @__PURE__ */ new Map();
2493
+ subscribe(type, subscriber) {
2494
+ let list = this.eventTypeToSubscribers.get(type);
2495
+ if (!list) {
2496
+ list = [];
2497
+ this.eventTypeToSubscribers.set(type, list);
2498
+ }
2499
+ list.push(subscriber);
2500
+ let done = false;
2501
+ return () => {
2502
+ if (done) return;
2503
+ done = true;
2504
+ const arr = this.eventTypeToSubscribers.get(type);
2505
+ if (!arr) return;
2506
+ const idx = arr.indexOf(subscriber);
2507
+ if (idx !== -1) arr.splice(idx, 1);
2508
+ if (arr.length === 0) this.eventTypeToSubscribers.delete(type);
2509
+ };
2510
+ }
2511
+ async emit(type, data) {
2512
+ const list = this.eventTypeToSubscribers.get(type);
2513
+ if (!list || list.length === 0) return;
2514
+ const snapshot = list.slice();
2515
+ const errors = [];
2516
+ for (const fn of snapshot) try {
2517
+ await Promise.resolve(fn(type, data));
2518
+ } catch (err) {
2519
+ errors.push(err);
2520
+ }
2521
+ if (errors.length > 0) throw new EventBusAggregateError(errors);
2522
+ }
2523
+ };
2524
+ //#endregion
2525
+ //#region src/registry/implementation.ts
2526
+ /**
2527
+ * In-memory implementation of the IDocumentModelRegistry interface.
2528
+ * Manages document model modules with version-aware storage and upgrade manifest support.
2529
+ */
2530
+ var DocumentModelRegistry = class {
2531
+ modules = [];
2532
+ manifests = [];
2533
+ registerModules(...modules) {
2534
+ return modules.map((module) => {
2535
+ try {
2536
+ const documentType = module.documentModel.global.id;
2537
+ const version = module.version ?? 1;
2538
+ for (let i = 0; i < this.modules.length; i++) {
2539
+ const existing = this.modules[i];
2540
+ const existingType = existing.documentModel.global.id;
2541
+ const existingVersion = existing.version ?? 1;
2542
+ if (existingType === documentType && existingVersion === version) throw new DuplicateModuleError(documentType, version);
2543
+ }
2544
+ this.modules.push(module);
2545
+ return {
2546
+ status: "success",
2547
+ item: module
2548
+ };
2549
+ } catch (error) {
2550
+ return {
2551
+ status: "error",
2552
+ item: module,
2553
+ error: error instanceof Error ? error : new Error(String(error))
2554
+ };
2555
+ }
2556
+ });
2557
+ }
2558
+ unregisterModules(...documentTypes) {
2559
+ let allFound = true;
2560
+ for (const documentType of documentTypes) {
2561
+ if (!this.modules.some((m) => m.documentModel.global.id === documentType)) allFound = false;
2562
+ this.modules = this.modules.filter((m) => m.documentModel.global.id !== documentType);
2563
+ }
2564
+ return allFound;
2565
+ }
2566
+ getModule(documentType, version) {
2567
+ let latestModule;
2568
+ let latestVersion = -1;
2569
+ for (let i = 0; i < this.modules.length; i++) {
2570
+ const module = this.modules[i];
2571
+ const moduleType = module.documentModel.global.id;
2572
+ const moduleVersion = module.version ?? 1;
2573
+ if (moduleType === documentType) {
2574
+ if (version !== void 0 && moduleVersion === version) return module;
2575
+ if (moduleVersion > latestVersion) {
2576
+ latestModule = module;
2577
+ latestVersion = moduleVersion;
2578
+ }
2579
+ }
2580
+ }
2581
+ if (version === void 0 && latestModule !== void 0) return latestModule;
2582
+ throw new ModuleNotFoundError(documentType, version);
2583
+ }
2584
+ getAllModules() {
2585
+ return [...this.modules];
2586
+ }
2587
+ clear() {
2588
+ this.modules = [];
2589
+ this.manifests = [];
2590
+ }
2591
+ getSupportedVersions(documentType) {
2592
+ const versions = [];
2593
+ for (const module of this.modules) if (module.documentModel.global.id === documentType) versions.push(module.version ?? 1);
2594
+ if (versions.length === 0) throw new ModuleNotFoundError(documentType);
2595
+ return versions.sort((a, b) => a - b);
2596
+ }
2597
+ getLatestVersion(documentType) {
2598
+ let latest = -1;
2599
+ let found = false;
2600
+ for (const module of this.modules) if (module.documentModel.global.id === documentType) {
2601
+ found = true;
2602
+ const version = module.version ?? 1;
2603
+ if (version > latest) latest = version;
2604
+ }
2605
+ if (!found) throw new ModuleNotFoundError(documentType);
2606
+ return latest;
2607
+ }
2608
+ registerUpgradeManifests(...manifestsToRegister) {
2609
+ return manifestsToRegister.map((manifestToRegister) => {
2610
+ try {
2611
+ if (!manifestToRegister.documentType) throw new Error("Upgrade manifest is missing a documentType");
2612
+ for (const registeredManifest of this.manifests) if (registeredManifest.documentType === manifestToRegister.documentType) throw new DuplicateManifestError(manifestToRegister.documentType);
2613
+ this.manifests.push(manifestToRegister);
2614
+ return {
2615
+ status: "success",
2616
+ item: manifestToRegister
2617
+ };
2618
+ } catch (error) {
2619
+ return {
2620
+ status: "error",
2621
+ item: manifestToRegister,
2622
+ error: error instanceof Error ? error : new Error(String(error))
2623
+ };
2624
+ }
2625
+ });
2626
+ }
2627
+ unregisterUpgradeManifests(...documentTypes) {
2628
+ let allFound = true;
2629
+ for (const documentType of documentTypes) {
2630
+ if (!this.manifests.some((m) => m.documentType === documentType)) allFound = false;
2631
+ this.manifests = this.manifests.filter((m) => m.documentType !== documentType);
2632
+ }
2633
+ return allFound;
2634
+ }
2635
+ getUpgradeManifest(documentType) {
2636
+ for (let i = 0; i < this.manifests.length; i++) if (this.manifests[i].documentType === documentType) return this.manifests[i];
2637
+ throw new ManifestNotFoundError(documentType);
2638
+ }
2639
+ computeUpgradePath(documentType, fromVersion, toVersion) {
2640
+ if (fromVersion === toVersion) return [];
2641
+ if (toVersion < fromVersion) throw new DowngradeNotSupportedError$1(documentType, fromVersion, toVersion);
2642
+ const manifest = this.getUpgradeManifest(documentType);
2643
+ const path = [];
2644
+ for (let v = fromVersion + 1; v <= toVersion; v++) {
2645
+ const key = `v${v}`;
2646
+ if (!(key in manifest.upgrades)) throw new MissingUpgradeTransitionError(documentType, v - 1, v);
2647
+ const transition = manifest.upgrades[key];
2648
+ path.push(transition);
2649
+ }
2650
+ return path;
2651
+ }
2652
+ getUpgradeReducer(documentType, fromVersion, toVersion) {
2653
+ if (toVersion !== fromVersion + 1) throw new InvalidUpgradeStepError(documentType, fromVersion, toVersion);
2654
+ const manifest = this.getUpgradeManifest(documentType);
2655
+ const key = `v${toVersion}`;
2656
+ if (!(key in manifest.upgrades)) throw new MissingUpgradeTransitionError(documentType, fromVersion, toVersion);
2657
+ return manifest.upgrades[key].upgradeReducer;
2658
+ }
2659
+ };
2660
+ //#endregion
2661
+ //#region src/storage/kysely/keyframe-store.ts
2662
+ var KyselyKeyframeStore = class KyselyKeyframeStore {
2663
+ db;
2664
+ trx;
2665
+ constructor(db) {
2666
+ this.db = db;
2667
+ }
2668
+ get queryExecutor() {
2669
+ return this.trx ?? this.db;
2670
+ }
2671
+ withTransaction(trx) {
2672
+ const instance = new KyselyKeyframeStore(this.db);
2673
+ instance.trx = trx;
2674
+ return instance;
2675
+ }
2676
+ async putKeyframe(documentId, scope, branch, revision, document, signal) {
2677
+ if (signal?.aborted) throw new Error("Operation aborted");
2678
+ const write = async (trx) => {
2679
+ await acquirePurgeLocks(trx, [documentId], "shared");
2680
+ if ((await findPurged(trx, [documentId])).has(documentId)) return;
2681
+ await trx.insertInto("Keyframe").values({
2682
+ documentId,
2683
+ documentType: document.header.documentType,
2684
+ scope,
2685
+ branch,
2686
+ revision,
2687
+ document
2688
+ }).onConflict((oc) => oc.columns([
2689
+ "documentId",
2690
+ "scope",
2691
+ "branch",
2692
+ "revision"
2693
+ ]).doUpdateSet({ document })).execute();
2694
+ };
2695
+ if (this.trx) {
2696
+ await write(this.trx);
2697
+ return;
2698
+ }
2699
+ await this.db.transaction().execute(write);
2700
+ }
2701
+ async findNearestKeyframe(documentId, scope, branch, targetRevision, signal) {
2702
+ if (signal?.aborted) throw new Error("Operation aborted");
2703
+ const row = await this.queryExecutor.selectFrom("Keyframe").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("revision", "<=", targetRevision).orderBy("revision", "desc").limit(1).executeTakeFirst();
2704
+ if (!row) return;
2705
+ return {
2706
+ revision: row.revision,
2707
+ document: row.document
2708
+ };
2709
+ }
2710
+ async listKeyframes(documentId, scope, branch, signal) {
2711
+ if (signal?.aborted) throw new Error("Operation aborted");
2712
+ let query = this.queryExecutor.selectFrom("Keyframe").selectAll().where("documentId", "=", documentId).orderBy("revision", "asc");
2713
+ if (scope !== void 0) query = query.where("scope", "=", scope);
2714
+ if (branch !== void 0) query = query.where("branch", "=", branch);
2715
+ return (await query.execute()).map((row) => ({
2716
+ scope: row.scope,
2717
+ branch: row.branch,
2718
+ revision: row.revision,
2719
+ document: row.document
2720
+ }));
2721
+ }
2722
+ async deleteKeyframes(documentId, scope, branch, signal) {
2723
+ if (signal?.aborted) throw new Error("Operation aborted");
2724
+ let query = this.queryExecutor.deleteFrom("Keyframe").where("documentId", "=", documentId);
2725
+ if (scope !== void 0 && branch !== void 0) query = query.where("scope", "=", scope).where("branch", "=", branch);
2726
+ else if (scope !== void 0) query = query.where("scope", "=", scope);
2727
+ const result = await query.executeTakeFirst();
2728
+ return Number(result.numDeletedRows || 0n);
2729
+ }
2730
+ };
2731
+ //#endregion
2732
+ //#region src/storage/kysely/pagination.ts
2733
+ const DEFAULT_LIMIT = 100;
2734
+ function paginateRows(rows, paging, cursorOf, toItem, refetch) {
2735
+ let hasMore = false;
2736
+ let items = rows;
2737
+ if (paging?.limit && rows.length > paging.limit) {
2738
+ hasMore = true;
2739
+ items = rows.slice(0, paging.limit);
2740
+ }
2741
+ const nextCursor = hasMore && items.length > 0 ? cursorOf(items[items.length - 1]).toString() : void 0;
2742
+ const cursor = paging?.cursor || "0";
2743
+ const limit = paging?.limit || DEFAULT_LIMIT;
2744
+ return {
2745
+ results: items.map(toItem),
2746
+ options: {
2747
+ cursor,
2748
+ limit
2749
+ },
2750
+ nextCursor,
2751
+ next: hasMore ? () => refetch(nextCursor, limit) : void 0
2752
+ };
2753
+ }
2754
+ //#endregion
2755
+ //#region src/storage/txn.ts
2756
+ var AtomicTransaction = class {
2757
+ documentId;
2758
+ documentType;
2759
+ scope;
2760
+ branch;
2761
+ baseRevision;
2762
+ operations = [];
2763
+ constructor(documentId, documentType, scope, branch, baseRevision) {
2764
+ this.documentId = documentId;
2765
+ this.documentType = documentType;
2766
+ this.scope = scope;
2767
+ this.branch = branch;
2768
+ this.baseRevision = baseRevision;
2769
+ }
2770
+ addOperations(...operations) {
2771
+ for (const op of operations) this.operations.push({
2772
+ jobId: v4(),
2773
+ opId: op.id,
2774
+ prevOpId: "",
2775
+ documentId: this.documentId,
2776
+ documentType: this.documentType,
2777
+ scope: this.scope,
2778
+ branch: this.branch,
2779
+ timestampUtcMs: new Date(op.timestampUtcMs),
2780
+ index: op.index,
2781
+ action: JSON.stringify(op.action),
2782
+ skip: op.skip,
2783
+ error: op.error || null,
2784
+ deniedReason: op.deniedReason || null,
2785
+ hash: op.hash
2786
+ });
2787
+ }
2788
+ getOperations() {
2789
+ return this.operations;
2790
+ }
2791
+ };
2792
+ //#endregion
2793
+ //#region src/storage/kysely/store.ts
2794
+ var _UniqueConstraintContext = class extends Error {
2795
+ documentId;
2796
+ scope;
2797
+ branch;
2798
+ revision;
2799
+ stagedOps;
2800
+ constructor(documentId, scope, branch, revision, stagedOps) {
2801
+ super("unique constraint");
2802
+ this.documentId = documentId;
2803
+ this.scope = scope;
2804
+ this.branch = branch;
2805
+ this.revision = revision;
2806
+ this.stagedOps = stagedOps;
2807
+ this.name = "UniqueConstraintContext";
2808
+ }
2809
+ };
2810
+ var KyselyOperationStore = class KyselyOperationStore {
2811
+ db;
2812
+ trx;
2813
+ liveIds;
2814
+ constructor(db) {
2815
+ this.db = db;
2816
+ }
2817
+ get queryExecutor() {
2818
+ return this.trx ?? this.db;
2819
+ }
2820
+ /** `liveIds`: ids the transaction read untombstoned under its shared lock. */
2821
+ withTransaction(trx, liveIds) {
2822
+ const instance = new KyselyOperationStore(this.db);
2823
+ instance.trx = trx;
2824
+ instance.liveIds = liveIds;
2825
+ return instance;
2826
+ }
2827
+ async apply(documentId, documentType, scope, branch, revision, fn, signal, condition) {
2828
+ if (this.trx) {
2829
+ let executeResult = null;
2830
+ let uniqueCtx = null;
2831
+ try {
2832
+ executeResult = await this.executeApply(this.trx, documentId, documentType, scope, branch, revision, fn, signal, condition);
2833
+ } catch (error) {
2834
+ if (error instanceof _UniqueConstraintContext) uniqueCtx = error;
2835
+ else throw error;
2836
+ }
2837
+ if (uniqueCtx !== null) return this.resolveUniqueConstraint(uniqueCtx);
2838
+ return executeResult;
2839
+ } else {
2840
+ let transactionResult = null;
2841
+ let uniqueCtx = null;
2842
+ try {
2843
+ transactionResult = await this.db.transaction().execute(async (trx) => {
2844
+ return this.executeApply(trx, documentId, documentType, scope, branch, revision, fn, signal, condition);
2845
+ });
2846
+ } catch (error) {
2847
+ if (error instanceof _UniqueConstraintContext) uniqueCtx = error;
2848
+ else throw error;
2849
+ }
2850
+ if (uniqueCtx !== null) return this.resolveUniqueConstraint(uniqueCtx);
2851
+ return transactionResult;
2852
+ }
2853
+ }
2854
+ async resolveUniqueConstraint(ctx) {
2855
+ let replayOps = null;
2856
+ try {
2857
+ replayOps = await this.findIdempotentReplay(this.db, ctx.documentId, ctx.scope, ctx.branch, ctx.revision, ctx.stagedOps);
2858
+ } catch {}
2859
+ if (replayOps !== null) return replayOps;
2860
+ const op = ctx.stagedOps[0];
2861
+ throw new DuplicateOperationError(`${op.opId} at index ${op.index} with skip ${op.skip}`);
2862
+ }
2863
+ async executeApply(trx, documentId, documentType, scope, branch, revision, fn, signal, condition) {
2864
+ throwIfAborted(signal);
2865
+ const atomicTxn = new AtomicTransaction(documentId, documentType, scope, branch, revision);
2866
+ await fn(atomicTxn);
2867
+ const operations = atomicTxn.getOperations();
2868
+ if (operations.length === 0) return [];
2869
+ const purgedHead = this.liveIds?.has(documentId) ? void 0 : await this.refusePurgedAppend(trx, documentId, operations);
2870
+ if (purgedHead !== void 0) return purgedHead;
2871
+ if (condition) await this.acquireStreamLocks(trx, documentId, scope, branch, condition);
2872
+ const latestOp = await trx.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).orderBy("index", "desc").limit(1).executeTakeFirst();
2873
+ const currentRevision = latestOp ? latestOp.index : -1;
2874
+ if (currentRevision !== revision - 1) {
2875
+ let replayOps = null;
2876
+ try {
2877
+ replayOps = await this.findIdempotentReplay(trx, documentId, scope, branch, revision, operations);
2878
+ } catch {}
2879
+ if (replayOps !== null) return replayOps;
2880
+ if (revision === 0 && this.isCreate(operations)) throw new DocumentAlreadyExistsError(documentId, scope, currentRevision);
2881
+ throw new RevisionMismatchError(currentRevision + 1, revision);
2882
+ }
2883
+ let prevOpId = latestOp?.opId || "";
2884
+ for (const op of operations) {
2885
+ op.prevOpId = prevOpId;
2886
+ prevOpId = op.opId;
2887
+ }
2888
+ let insertedCount = operations.length;
2889
+ try {
2890
+ if (condition && condition.streams.length > 0) insertedCount = await this.insertGuarded(trx, operations, condition);
2891
+ else await trx.insertInto("Operation").values(operations).execute();
2892
+ } catch (error) {
2893
+ if (error instanceof Error && error.message.includes("unique constraint")) throw new _UniqueConstraintContext(documentId, scope, branch, revision, operations);
2894
+ throw error;
2895
+ }
2896
+ if (insertedCount !== operations.length) throw new AppendConditionFailedError(condition);
2897
+ return operations.map((op) => ({
2898
+ index: op.index,
2899
+ timestampUtcMs: op.timestampUtcMs.toISOString(),
2900
+ hash: op.hash,
2901
+ skip: op.skip,
2902
+ error: op.error || void 0,
2903
+ deniedReason: op.deniedReason || void 0,
2904
+ id: op.opId,
2905
+ action: JSON.parse(op.action)
2906
+ }));
2907
+ }
2908
+ /**
2909
+ * Locks the written stream and every read-set stream, in sorted key order
2910
+ * so that overlapping concurrent appends serialize rather than deadlock.
2911
+ * The locks are still taken one row at a time, so the query preserves that
2912
+ * order. It must stay separate from the guarded insert, which would
2913
+ * otherwise read a snapshot taken before the locks were held.
2914
+ */
2915
+ async acquireStreamLocks(trx, documentId, scope, branch, condition) {
2916
+ const keys = /* @__PURE__ */ new Set([`${documentId}:${scope}:${branch}`]);
2917
+ for (const stream of condition.streams) keys.add(`${stream.documentId}:${stream.scope}:${stream.branch}`);
2918
+ const sortedKeys = sql.join([...keys].sort());
2919
+ await sql`
2920
+ with ordered as materialized (
2921
+ select key
2922
+ from unnest(array[${sortedKeys}]::text[]) with ordinality as t(key, ord)
2923
+ order by ord
2924
+ )
2925
+ select pg_advisory_xact_lock(hashtext(key)) from ordered
2926
+ `.execute(trx);
2927
+ }
2928
+ /**
2929
+ * Inserts the staged operations with the condition compiled in as a WHERE
2930
+ * NOT EXISTS guard, making the check and the append one statement. Returns
2931
+ * the rows inserted; zero means the guard failed and nothing was written.
2932
+ */
2933
+ async insertGuarded(trx, operations, condition) {
2934
+ const branches = operations.map((op) => trx.selectNoFrom([
2935
+ sql`${op.jobId}::text`.as("jobId"),
2936
+ sql`${op.opId}::text`.as("opId"),
2937
+ sql`${op.prevOpId}::text`.as("prevOpId"),
2938
+ sql`${op.documentId}::text`.as("documentId"),
2939
+ sql`${op.documentType}::text`.as("documentType"),
2940
+ sql`${op.scope}::text`.as("scope"),
2941
+ sql`${op.branch}::text`.as("branch"),
2942
+ sql`${op.timestampUtcMs}::timestamptz`.as("timestampUtcMs"),
2943
+ sql`${op.index}::integer`.as("index"),
2944
+ sql`${op.action}::jsonb`.as("action"),
2945
+ sql`${op.skip}::integer`.as("skip"),
2946
+ sql`${op.error ?? null}::text`.as("error"),
2947
+ sql`${op.deniedReason ?? null}::text`.as("deniedReason"),
2948
+ sql`${op.hash}::text`.as("hash")
2949
+ ]).where((eb) => eb.not(eb.exists(eb.selectFrom("Operation").select("Operation.id").where((web) => web.or(condition.streams.map((s) => web.and([
2950
+ web("Operation.documentId", "=", s.documentId),
2951
+ web("Operation.scope", "=", s.scope),
2952
+ web("Operation.branch", "=", s.branch),
2953
+ web("Operation.index", ">", s.revision)
2954
+ ]))))))));
2955
+ let expression = branches[0];
2956
+ for (let i = 1; i < branches.length; i++) expression = expression.unionAll(branches[i]);
2957
+ return (await trx.insertInto("Operation").columns([
2958
+ "jobId",
2959
+ "opId",
2960
+ "prevOpId",
2961
+ "documentId",
2962
+ "documentType",
2963
+ "scope",
2964
+ "branch",
2965
+ "timestampUtcMs",
2966
+ "index",
2967
+ "action",
2968
+ "skip",
2969
+ "error",
2970
+ "deniedReason",
2971
+ "hash"
2972
+ ]).expression(expression).returning("id").execute()).length;
2973
+ }
2974
+ async findIdempotentReplay(executor, documentId, scope, branch, revision, stagedOps) {
2975
+ const minIndex = revision;
2976
+ const maxIndex = revision + stagedOps.length - 1;
2977
+ const storedRows = await executor.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("index", ">=", minIndex).where("index", "<=", maxIndex).orderBy("index", "asc").execute();
2978
+ if (storedRows.length !== stagedOps.length) return null;
2979
+ for (let i = 0; i < stagedOps.length; i++) {
2980
+ const staged = stagedOps[i];
2981
+ const stored = storedRows[i];
2982
+ if (stored.opId !== staged.opId || stored.index !== staged.index || stored.skip !== staged.skip) return null;
2983
+ }
2984
+ return storedRows.map((row) => this.rowToOperation(row));
2985
+ }
2986
+ /** A purged stream refuses appends; a marker gets the stored one back. */
2987
+ async refusePurgedAppend(trx, documentId, operations) {
2988
+ if (!(await findPurged(trx, [documentId])).has(documentId)) return;
2989
+ if (!operations.every((operation) => this.isMarker(operation))) throw new DocumentPurgedError(documentId);
2990
+ return (await trx.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where(sql`action->>'type' = ${PURGE_DOCUMENT}`).orderBy("index", "asc").execute()).map((row) => this.rowToOperation(row));
2991
+ }
2992
+ isMarker(operation) {
2993
+ return this.actionType(operation) === PURGE_DOCUMENT;
2994
+ }
2995
+ actionType(operation) {
2996
+ let action = operation.action;
2997
+ if (typeof action === "string") try {
2998
+ action = JSON.parse(action);
2999
+ } catch {
3000
+ return;
3001
+ }
3002
+ if (typeof action === "object" && action !== null && "type" in action && typeof action.type === "string") return action.type;
3003
+ }
3004
+ /** True when the staged write creates a document rather than appending to one. */
3005
+ isCreate(operations) {
3006
+ for (const operation of operations) {
3007
+ let action = operation.action;
3008
+ if (typeof action === "string") try {
3009
+ action = JSON.parse(action);
3010
+ } catch {
3011
+ continue;
3012
+ }
3013
+ if (typeof action === "object" && action !== null && "type" in action && typeof action.type === "string" && action.type === "CREATE_DOCUMENT") return true;
3014
+ }
3015
+ return false;
3016
+ }
3017
+ /**
3018
+ * The paging cursor here encodes the index to resume from (one past the
3019
+ * last row returned), not the last row's own index. This keeps "0" an
3020
+ * unambiguous start-of-stream sentinel even when a page ends at index 0
3021
+ * (e.g. `limit: 1` on a fresh stream), which would otherwise make
3022
+ * `nextCursor` equal the start cursor and loop a caller that walks pages
3023
+ * forever.
3024
+ */
3025
+ async getSince(documentId, scope, branch, revision, filter, paging, signal) {
3026
+ throwIfAborted(signal);
3027
+ let query = this.queryExecutor.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("index", ">", revision).orderBy("index", "asc");
3028
+ if (filter) {
3029
+ if (filter.actionTypes && filter.actionTypes.length > 0) {
3030
+ const actionTypesArray = filter.actionTypes.map((t) => `'${t.replace(/'/g, "''")}'`).join(",");
3031
+ query = query.where(sql`action->>'type' = ANY(ARRAY[${sql.raw(actionTypesArray)}]::text[])`);
3032
+ }
3033
+ if (filter.timestampFrom) query = query.where("timestampUtcMs", ">=", new Date(filter.timestampFrom));
3034
+ if (filter.timestampTo) query = query.where("timestampUtcMs", "<=", new Date(filter.timestampTo));
3035
+ if (filter.sinceRevision !== void 0) query = query.where("index", ">=", filter.sinceRevision);
3036
+ }
3037
+ if (paging) {
3038
+ const cursorValue = Number.parseInt(paging.cursor, 10);
3039
+ if (Number.isFinite(cursorValue) && cursorValue > 0) query = query.where("index", ">=", cursorValue);
3040
+ if (paging.limit) query = query.limit(paging.limit + 1);
3041
+ }
3042
+ return paginateRows(await query.execute(), paging, (row) => row.index + 1, (row) => this.rowToOperation(row), (cursor, limit) => this.getSince(documentId, scope, branch, revision, filter, {
3043
+ cursor,
3044
+ limit
3045
+ }, signal));
3046
+ }
3047
+ async getSinceId(id, paging, signal) {
3048
+ throwIfAborted(signal);
3049
+ let query = this.queryExecutor.selectFrom("Operation").selectAll().where("id", ">", id).orderBy("id", "asc");
3050
+ if (paging) {
3051
+ const cursorValue = Number.parseInt(paging.cursor, 10);
3052
+ if (cursorValue > 0) query = query.where("id", ">", cursorValue);
3053
+ if (paging.limit) query = query.limit(paging.limit + 1);
3054
+ }
3055
+ return paginateRows(await query.execute(), paging, (row) => row.id, (row) => this.rowToOperationWithContext(row), (cursor, limit) => this.getSinceId(id, {
3056
+ cursor,
3057
+ limit
3058
+ }, signal));
3059
+ }
3060
+ /**
3061
+ * The paging cursor here encodes the index to resume from (one past the
3062
+ * last row returned), not the last row's own index, for the same reason as
3063
+ * `getSince`: a page ending at index 0 must not produce a cursor that is
3064
+ * indistinguishable from the start-of-stream sentinel.
3065
+ */
3066
+ async getConflicting(documentId, scope, branch, minTimestamp, paging, signal) {
3067
+ throwIfAborted(signal);
3068
+ let query = this.queryExecutor.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("timestampUtcMs", ">=", new Date(minTimestamp)).orderBy("index", "asc");
3069
+ if (paging) {
3070
+ const cursorValue = Number.parseInt(paging.cursor, 10);
3071
+ if (Number.isFinite(cursorValue) && cursorValue > 0) query = query.where("index", ">=", cursorValue);
3072
+ if (paging.limit) query = query.limit(paging.limit + 1);
3073
+ }
3074
+ return paginateRows(await query.execute(), paging, (row) => row.index + 1, (row) => this.rowToOperation(row), (cursor, limit) => this.getConflicting(documentId, scope, branch, minTimestamp, {
3075
+ cursor,
3076
+ limit
3077
+ }, signal));
3078
+ }
3079
+ async getRevisions(documentId, branch, signal) {
3080
+ throwIfAborted(signal);
3081
+ 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();
3082
+ const latest = await this.queryExecutor.selectFrom("Operation").select((eb) => eb.fn.max("timestampUtcMs").as("latestTimestamp")).where("documentId", "=", documentId).where("branch", "=", branch).executeTakeFirst();
3083
+ const revision = {};
3084
+ for (const row of scopeRevisions) revision[row.scope] = row.index + 1;
3085
+ return {
3086
+ revision,
3087
+ latestTimestamp: latest?.latestTimestamp ? new Date(latest.latestTimestamp).toISOString() : (/* @__PURE__ */ new Date(0)).toISOString()
3088
+ };
3089
+ }
3090
+ async getStreamLatestTimestamp(documentId, scope, branch, signal) {
3091
+ 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();
3092
+ return latest?.latestTimestamp ? new Date(latest.latestTimestamp).toISOString() : void 0;
3093
+ }
3094
+ async findOperationIds(documentId, scope, branch, opIds, signal) {
3095
+ throwIfAborted(signal);
3096
+ if (opIds.length === 0) return /* @__PURE__ */ new Set();
3097
+ const rows = await this.queryExecutor.selectFrom("Operation").select("opId").distinct().where("opId", "in", opIds).where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).execute();
3098
+ return new Set(rows.map((row) => row.opId));
3099
+ }
3100
+ async getOperationsByIds(documentId, scope, branch, opIds, signal) {
3101
+ throwIfAborted(signal);
3102
+ if (opIds.length === 0) return [];
3103
+ 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();
3104
+ const latest = /* @__PURE__ */ new Map();
3105
+ for (const row of rows) latest.set(row.opId, this.rowToOperation(row));
3106
+ return [...latest.values()];
3107
+ }
3108
+ rowToOperation(row) {
3109
+ return {
3110
+ index: row.index,
3111
+ timestampUtcMs: row.timestampUtcMs.toISOString(),
3112
+ hash: row.hash,
3113
+ skip: row.skip,
3114
+ error: row.error || void 0,
3115
+ deniedReason: row.deniedReason || void 0,
3116
+ id: row.opId,
3117
+ action: row.action
3118
+ };
3119
+ }
3120
+ rowToOperationWithContext(row) {
3121
+ return {
3122
+ operation: this.rowToOperation(row),
3123
+ context: {
3124
+ documentId: row.documentId,
3125
+ documentType: row.documentType,
3126
+ scope: row.scope,
3127
+ branch: row.branch,
3128
+ ordinal: row.id
3129
+ }
3130
+ };
3131
+ }
3132
+ };
3133
+ //#endregion
3134
+ //#region src/storage/pool-instrumentation.ts
3135
+ /**
3136
+ * Wraps an existing pg.Pool with acquire-wait timing and an event
3137
+ * subscription surface. The pool is mutated in place: pool.connect()
3138
+ * is replaced with a timing wrapper so all callers (Kysely included)
3139
+ * pick up the instrumentation transparently.
3140
+ */
3141
+ function instrumentPgPool(pool, name) {
3142
+ const listeners = /* @__PURE__ */ new Set();
3143
+ const originalConnect = pool.connect.bind(pool);
3144
+ const wrappedConnect = async () => {
3145
+ const start = performance.now();
3146
+ const client = await originalConnect();
3147
+ const durationMs = performance.now() - start;
3148
+ for (const listener of listeners) try {
3149
+ listener(durationMs);
3150
+ } catch {}
3151
+ return client;
3152
+ };
3153
+ pool.connect = wrappedConnect;
3154
+ return {
3155
+ name,
3156
+ getStats() {
3157
+ return {
3158
+ size: pool.totalCount,
3159
+ idle: pool.idleCount,
3160
+ waiting: pool.waitingCount
3161
+ };
3162
+ },
3163
+ onAcquire(listener) {
3164
+ listeners.add(listener);
3165
+ return () => {
3166
+ listeners.delete(listener);
3167
+ };
3168
+ }
3169
+ };
3170
+ }
3171
+ function createForwardingPoolInstrumentation(name) {
3172
+ const listeners = /* @__PURE__ */ new Set();
3173
+ let stats = {
3174
+ size: 0,
3175
+ idle: 0,
3176
+ waiting: 0
3177
+ };
3178
+ return {
3179
+ name,
3180
+ getStats() {
3181
+ return stats;
3182
+ },
3183
+ onAcquire(listener) {
3184
+ listeners.add(listener);
3185
+ return () => {
3186
+ listeners.delete(listener);
3187
+ };
3188
+ },
3189
+ pushSamples(durations) {
3190
+ for (const durationMs of durations) for (const listener of listeners) try {
3191
+ listener(durationMs);
3192
+ } catch {}
3193
+ },
3194
+ updateStats(next) {
3195
+ stats = next;
3196
+ }
3197
+ };
3198
+ }
3199
+ //#endregion
3200
+ //#region src/storage/migrations/001_create_operation_table.ts
3201
+ var _001_create_operation_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$25 });
3202
+ async function up$25(db) {
3203
+ 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", [
3204
+ "documentId",
3205
+ "scope",
3206
+ "branch",
3207
+ "index"
3208
+ ]).addUniqueConstraint("unique_operation_instance", [
3209
+ "opId",
3210
+ "index",
3211
+ "skip"
3212
+ ]).execute();
3213
+ await db.schema.createIndex("streamOperations").on("Operation").columns([
3214
+ "documentId",
3215
+ "scope",
3216
+ "branch",
3217
+ "id"
3218
+ ]).execute();
3219
+ await db.schema.createIndex("branchlessStreamOperations").on("Operation").columns([
3220
+ "documentId",
3221
+ "scope",
3222
+ "id"
3223
+ ]).execute();
3224
+ }
3225
+ //#endregion
3226
+ //#region src/storage/migrations/002_create_keyframe_table.ts
3227
+ var _002_create_keyframe_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$24 });
3228
+ async function up$24(db) {
3229
+ 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", [
3230
+ "documentId",
3231
+ "scope",
3232
+ "branch",
3233
+ "revision"
3234
+ ]).execute();
3235
+ await db.schema.createIndex("keyframe_lookup").on("Keyframe").columns([
3236
+ "documentId",
3237
+ "scope",
3238
+ "branch",
3239
+ "revision"
3240
+ ]).execute();
3241
+ }
3242
+ //#endregion
3243
+ //#region src/storage/migrations/003_create_document_table.ts
3244
+ var _003_create_document_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$23 });
3245
+ async function up$23(db) {
3246
+ 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();
3247
+ }
3248
+ //#endregion
3249
+ //#region src/storage/migrations/004_create_document_relationship_table.ts
3250
+ var _004_create_document_relationship_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$22 });
3251
+ async function up$22(db) {
3252
+ 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", [
3253
+ "sourceId",
3254
+ "targetId",
3255
+ "relationshipType"
3256
+ ]).execute();
3257
+ await db.schema.createIndex("idx_relationship_source").on("DocumentRelationship").column("sourceId").execute();
3258
+ await db.schema.createIndex("idx_relationship_target").on("DocumentRelationship").column("targetId").execute();
3259
+ await db.schema.createIndex("idx_relationship_type").on("DocumentRelationship").column("relationshipType").execute();
3260
+ }
3261
+ //#endregion
3262
+ //#region src/storage/migrations/005_create_indexer_state_table.ts
3263
+ var _005_create_indexer_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$21 });
3264
+ async function up$21(db) {
3265
+ 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();
3266
+ }
3267
+ //#endregion
3268
+ //#region src/storage/migrations/006_create_document_snapshot_table.ts
3269
+ var _006_create_document_snapshot_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$20 });
3270
+ async function up$20(db) {
3271
+ 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", [
3272
+ "documentId",
3273
+ "scope",
3274
+ "branch"
3275
+ ]).execute();
3276
+ await db.schema.createIndex("idx_slug_scope_branch").on("DocumentSnapshot").columns([
3277
+ "slug",
3278
+ "scope",
3279
+ "branch"
3280
+ ]).execute();
3281
+ await db.schema.createIndex("idx_doctype_scope_branch").on("DocumentSnapshot").columns([
3282
+ "documentType",
3283
+ "scope",
3284
+ "branch"
3285
+ ]).execute();
3286
+ await db.schema.createIndex("idx_last_updated").on("DocumentSnapshot").column("lastUpdatedAt").execute();
3287
+ await db.schema.createIndex("idx_is_deleted").on("DocumentSnapshot").column("isDeleted").execute();
3288
+ }
3289
+ //#endregion
3290
+ //#region src/storage/migrations/007_create_slug_mapping_table.ts
3291
+ var _007_create_slug_mapping_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$19 });
3292
+ async function up$19(db) {
3293
+ 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", [
3294
+ "documentId",
3295
+ "scope",
3296
+ "branch"
3297
+ ]).execute();
3298
+ await db.schema.createIndex("idx_slug_documentid").on("SlugMapping").column("documentId").execute();
3299
+ }
3300
+ //#endregion
3301
+ //#region src/storage/migrations/008_create_view_state_table.ts
3302
+ var _008_create_view_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$18 });
3303
+ async function up$18(db) {
3304
+ 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();
3305
+ }
3306
+ //#endregion
3307
+ //#region src/storage/migrations/009_create_operation_index_tables.ts
3308
+ var _009_create_operation_index_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$17 });
3309
+ async function up$17(db) {
3310
+ 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();
3311
+ await db.schema.createIndex("idx_document_collections_collectionId").on("document_collections").column("collectionId").execute();
3312
+ await db.schema.createIndex("idx_doc_collections_collection_range").on("document_collections").columns(["collectionId", "joinedOrdinal"]).execute();
3313
+ await db.schema.createTable("operation_index_operations").addColumn("ordinal", "serial", (col) => col.primaryKey()).addColumn("opId", "text", (col) => col.notNull()).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", "text", (col) => col.notNull()).addColumn("writeTimestampUtcMs", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("index", "integer", (col) => col.notNull()).addColumn("skip", "integer", (col) => col.notNull()).addColumn("hash", "text", (col) => col.notNull()).addColumn("action", "jsonb", (col) => col.notNull()).execute();
3314
+ await db.schema.createIndex("idx_operation_index_operations_document").on("operation_index_operations").columns([
3315
+ "documentId",
3316
+ "branch",
3317
+ "scope"
3318
+ ]).execute();
3319
+ await db.schema.createIndex("idx_operation_index_operations_ordinal").on("operation_index_operations").column("ordinal").execute();
3320
+ }
3321
+ //#endregion
3322
+ //#region src/storage/migrations/010_create_sync_tables.ts
3323
+ var _010_create_sync_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$16 });
3324
+ async function up$16(db) {
3325
+ 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();
3326
+ await db.schema.createIndex("idx_sync_remotes_collection").on("sync_remotes").column("collection_id").execute();
3327
+ 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();
3328
+ await db.schema.createIndex("idx_sync_cursors_ordinal").on("sync_cursors").column("cursor_ordinal").execute();
3329
+ }
3330
+ //#endregion
3331
+ //#region src/storage/migrations/011_add_cursor_type_column.ts
3332
+ var _011_add_cursor_type_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$15 });
3333
+ async function up$15(db) {
3334
+ await db.deleteFrom("sync_cursors").where("remote_name", "like", "outbox::%").execute();
3335
+ await db.deleteFrom("sync_remotes").where("name", "like", "outbox::%").execute();
3336
+ await db.schema.dropTable("sync_cursors").execute();
3337
+ await db.schema.createTable("sync_cursors").addColumn("remote_name", "text", (col) => col.notNull()).addColumn("cursor_type", "text", (col) => col.notNull().defaultTo("inbox")).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()`)).addPrimaryKeyConstraint("sync_cursors_pk", ["remote_name", "cursor_type"]).execute();
3338
+ await db.schema.createIndex("idx_sync_cursors_ordinal").on("sync_cursors").column("cursor_ordinal").execute();
3339
+ }
3340
+ //#endregion
3341
+ //#region src/storage/migrations/012_add_source_remote_column.ts
3342
+ var _012_add_source_remote_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$14 });
3343
+ async function up$14(db) {
3344
+ await db.schema.alterTable("operation_index_operations").addColumn("sourceRemote", "text", (col) => col.notNull().defaultTo("")).execute();
3345
+ }
3346
+ //#endregion
3347
+ //#region src/storage/migrations/013_create_sync_dead_letters_table.ts
3348
+ var _013_create_sync_dead_letters_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$13 });
3349
+ async function up$13(db) {
3350
+ 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();
3351
+ await db.schema.createIndex("idx_sync_dead_letters_remote").on("sync_dead_letters").column("remote_name").execute();
3352
+ }
3353
+ //#endregion
3354
+ //#region src/storage/migrations/014_create_processor_cursor_table.ts
3355
+ var _014_create_processor_cursor_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$12 });
3356
+ async function up$12(db) {
3357
+ 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();
3358
+ }
3359
+ //#endregion
3360
+ //#region src/storage/migrations/015_add_operation_denied_reason.ts
3361
+ var _015_add_operation_denied_reason_exports = /* @__PURE__ */ __exportAll({
3362
+ down: () => down$11,
3363
+ up: () => up$11
3364
+ });
3365
+ /**
3366
+ * Records why authorization refused an operation. Separate from `error` so a
3367
+ * denial is distinguishable from a reducer failure without matching on a
3368
+ * message. Null for every operation written before decisions were enforced.
3369
+ */
3370
+ async function up$11(db) {
3371
+ await db.schema.alterTable("Operation").addColumn("deniedReason", "text").execute();
3372
+ await db.schema.alterTable("operation_index_operations").addColumn("deniedReason", "text").execute();
3373
+ }
3374
+ async function down$11(db) {
3375
+ await db.schema.alterTable("operation_index_operations").dropColumn("deniedReason").execute();
3376
+ await db.schema.alterTable("Operation").dropColumn("deniedReason").execute();
3377
+ }
3378
+ //#endregion
3379
+ //#region src/storage/migrations/016_add_dead_letter_error_type.ts
3380
+ var _016_add_dead_letter_error_type_exports = /* @__PURE__ */ __exportAll({
3381
+ down: () => down$10,
3382
+ up: () => up$10
3383
+ });
3384
+ /**
3385
+ * The classification a dead letter falls into, stored because it decides whether
3386
+ * the document stays quarantined and the in-memory error is gone after a restart.
3387
+ * Defaulted rather than nullable, so a pre-existing row rehydrates.
3388
+ */
3389
+ async function up$10(db) {
3390
+ await db.schema.alterTable("sync_dead_letters").addColumn("error_type", "text", (col) => col.notNull().defaultTo("UNCLASSIFIED")).execute();
3391
+ }
3392
+ async function down$10(db) {
3393
+ await db.schema.alterTable("sync_dead_letters").dropColumn("error_type").execute();
3394
+ }
3395
+ //#endregion
3396
+ //#region src/storage/migrations/017_create_group_references.ts
3397
+ var _017_create_group_references_exports = /* @__PURE__ */ __exportAll({
3398
+ down: () => down$9,
3399
+ up: () => up$9
3400
+ });
3401
+ /**
3402
+ * One row per (document, group) reference ever discovered from an auth
3403
+ * operation's input. Rows are never updated or deleted: auth evaluation is
3404
+ * positional, so a grant that named a group at any position keeps that
3405
+ * group's stream in the document's read-set even after a later operation
3406
+ * removes the reference. Read by documentId for the groups a document
3407
+ * requires (sync), and by groupId for the documents a group change affects
3408
+ * (re-evaluation).
3409
+ */
3410
+ async function up$9(db) {
3411
+ 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();
3412
+ await db.schema.createIndex("idx_group_references_groupId").on("group_references").column("groupId").execute();
3413
+ }
3414
+ async function down$9(db) {
3415
+ await db.schema.dropTable("group_references").execute();
3416
+ }
3417
+ //#endregion
3418
+ //#region src/storage/migrations/018_add_sync_remote_bound_address.ts
3419
+ var _018_add_sync_remote_bound_address_exports = /* @__PURE__ */ __exportAll({
3420
+ down: () => down$8,
3421
+ up: () => up$8
3422
+ });
3423
+ /**
3424
+ * The address a sync channel is bound to, so a channel created by one subject
3425
+ * cannot be polled by another.
3426
+ *
3427
+ * Nullable rather than defaulted: null is a channel nobody has claimed, which is
3428
+ * what every pre-existing row is and what an anonymously created channel stays
3429
+ * until its first authenticated poll adopts it. A default would claim them all
3430
+ * for one address.
3431
+ */
3432
+ async function up$8(db) {
3433
+ await db.schema.alterTable("sync_remotes").addColumn("bound_address", "text").execute();
3434
+ }
3435
+ async function down$8(db) {
3436
+ await db.schema.alterTable("sync_remotes").dropColumn("bound_address").execute();
3437
+ }
3438
+ //#endregion
3439
+ //#region src/storage/migrations/019_require_action_id.ts
3440
+ var _019_require_action_id_exports = /* @__PURE__ */ __exportAll({
3441
+ down: () => down$7,
3442
+ up: () => up$7
3443
+ });
3444
+ /**
3445
+ * Makes an operation whose action carries no id physically unstorable.
3446
+ *
3447
+ * The id is not decoration: `deriveOperationId` hashes it into the operation id
3448
+ * and replay dedupes incoming operations by it, so an action without one
3449
+ * collapses every id-less operation on a document/scope/branch onto a single
3450
+ * derived operation id. The API rejects such an action now, and this is the
3451
+ * last line of defense behind it.
3452
+ *
3453
+ * Both tables are constrained because sync reads operations from the index
3454
+ * rather than the operation table, so poison reaching only the index would
3455
+ * still be served to a replica.
3456
+ *
3457
+ * Pre-existing rows are backfilled rather than left behind a NOT VALID
3458
+ * constraint: a row the index and the operation table disagree about is worse
3459
+ * than a missing id, because dedup keys off the value each side serves. The
3460
+ * backfill therefore mints one id per operation and writes that same id to both
3461
+ * tables, joined on the identity they share. Rewriting the action is safe: the
3462
+ * operation hash is taken over the resulting state, not over the action, and a
3463
+ * signature is verified from the params carried in the signature tuple, which
3464
+ * do not include the action id.
3465
+ *
3466
+ * The empty string is rejected alongside null. It derives the same colliding
3467
+ * operation id as an absent id, so admitting it would leave the hole open.
3468
+ */
3469
+ async function up$7(db) {
3470
+ 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();
3471
+ 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();
3472
+ 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();
3473
+ await db.schema.alterTable("Operation").addCheckConstraint("action_must_have_id", sql`action->>'id' is not null and action->>'id' <> ''`).execute();
3474
+ await db.schema.alterTable("operation_index_operations").addCheckConstraint("action_must_have_id", sql`action->>'id' is not null and action->>'id' <> ''`).execute();
3475
+ }
3476
+ /**
3477
+ * Only the constraints are dropped. The backfilled ids stay: they are the ids
3478
+ * their operations are now known by, and reverting them would reintroduce the
3479
+ * collision the migration removed.
3480
+ */
3481
+ async function down$7(db) {
3482
+ await db.schema.alterTable("operation_index_operations").dropConstraint("action_must_have_id").execute();
3483
+ await db.schema.alterTable("Operation").dropConstraint("action_must_have_id").execute();
3484
+ }
3485
+ //#endregion
3486
+ //#region src/storage/migrations/020_add_snapshot_operation_ordinal.ts
3487
+ var _020_add_snapshot_operation_ordinal_exports = /* @__PURE__ */ __exportAll({
3488
+ down: () => down$6,
3489
+ up: () => up$6
3490
+ });
3491
+ /** Orders header row writes across scopes; `lastOperationIndex` is per scope. */
3492
+ async function up$6(db) {
3493
+ await db.schema.alterTable("DocumentSnapshot").addColumn("lastOperationOrdinal", "integer", (col) => col.notNull().defaultTo(0)).execute();
3494
+ }
3495
+ async function down$6(db) {
3496
+ await db.schema.alterTable("DocumentSnapshot").dropColumn("lastOperationOrdinal").execute();
3497
+ }
3498
+ //#endregion
3499
+ //#region src/storage/migrations/021_add_sync_remote_peer.ts
3500
+ var _021_add_sync_remote_peer_exports = /* @__PURE__ */ __exportAll({
3501
+ down: () => down$5,
3502
+ up: () => up$5
3503
+ });
3504
+ /**
3505
+ * The peer's manifest as canonical JSON, and when it was heard. Both null: not
3506
+ * yet heard. A time with no manifest: a silent peer, which gets the baselines.
3507
+ */
3508
+ async function up$5(db) {
3509
+ await db.schema.alterTable("sync_remotes").addColumn("peer_manifest", "text").addColumn("peer_manifest_at_utc_ms", "bigint").execute();
3510
+ }
3511
+ async function down$5(db) {
3512
+ await db.schema.alterTable("sync_remotes").dropColumn("peer_manifest").dropColumn("peer_manifest_at_utc_ms").execute();
3513
+ }
3514
+ //#endregion
3515
+ //#region src/storage/migrations/022_create_sync_holds.ts
3516
+ var _022_create_sync_holds_exports = /* @__PURE__ */ __exportAll({
3517
+ down: () => down$4,
3518
+ up: () => up$4
3519
+ });
3520
+ /** Documents held back from a remote whose peer cannot run their versions. */
3521
+ async function up$4(db) {
3522
+ 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", [
3523
+ "remote_name",
3524
+ "document_id",
3525
+ "branch"
3526
+ ]).execute();
3527
+ }
3528
+ async function down$4(db) {
3529
+ await db.schema.dropTable("sync_holds").execute();
3530
+ }
3531
+ //#endregion
3532
+ //#region src/storage/migrations/023_index_created_protocol_versions.ts
3533
+ var _023_index_created_protocol_versions_exports = /* @__PURE__ */ __exportAll({
3534
+ down: () => down$3,
3535
+ up: () => up$3
3536
+ });
3537
+ /**
3538
+ * One entry per document created with protocolVersions, keyed by a hash so an
3539
+ * oversized header cannot make its creation unstorable. Startup reads the
3540
+ * distinct values without visiting every document.
3541
+ */
3542
+ async function up$3(db) {
3543
+ 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();
3544
+ }
3545
+ async function down$3(db) {
3546
+ await db.schema.dropIndex("idx_operation_created_protocol_versions").execute();
3547
+ }
3548
+ //#endregion
3549
+ //#region src/storage/migrations/024_create_document_purges.ts
3550
+ var _024_create_document_purges_exports = /* @__PURE__ */ __exportAll({
3551
+ down: () => down$2,
3552
+ up: () => up$2
3553
+ });
3554
+ /** One row per purged document: the tombstone. Written once, never deleted. */
3555
+ async function up$2(db) {
3556
+ 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();
3557
+ }
3558
+ async function down$2(db) {
3559
+ await db.schema.dropTable("document_purges").execute();
3560
+ }
3561
+ //#endregion
3562
+ //#region src/storage/migrations/025_create_sync_received_markers.ts
3563
+ var _025_create_sync_received_markers_exports = /* @__PURE__ */ __exportAll({
3564
+ down: () => down$1,
3565
+ up: () => up$1
3566
+ });
3567
+ /** Received purge markers awaiting their outcome; removed with their remote. */
3568
+ async function up$1(db) {
3569
+ 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();
3570
+ }
3571
+ async function down$1(db) {
3572
+ await db.schema.dropTable("sync_received_markers").execute();
3573
+ }
3574
+ //#endregion
3575
+ //#region src/storage/migrations/026_create_sync_purge_refusals.ts
3576
+ var _026_create_sync_purge_refusals_exports = /* @__PURE__ */ __exportAll({
3577
+ down: () => down,
3578
+ up: () => up
3579
+ });
3580
+ /** A remote's refusal of a purge marker; payload-free, kept like the tombstone. */
3581
+ async function up(db) {
3582
+ 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", [
3583
+ "remote_name",
3584
+ "document_id",
3585
+ "branch"
3586
+ ]).execute();
3587
+ await db.schema.createIndex("idx_sync_purge_refusals_document").on("sync_purge_refusals").column("document_id").execute();
3588
+ }
3589
+ async function down(db) {
3590
+ await db.schema.dropTable("sync_purge_refusals").execute();
3591
+ }
3592
+ //#endregion
3593
+ //#region src/storage/migrations/migrator.ts
3594
+ const REACTOR_SCHEMA = "reactor";
3595
+ const migrations = {
3596
+ "001_create_operation_table": _001_create_operation_table_exports,
3597
+ "002_create_keyframe_table": _002_create_keyframe_table_exports,
3598
+ "003_create_document_table": _003_create_document_table_exports,
3599
+ "004_create_document_relationship_table": _004_create_document_relationship_table_exports,
3600
+ "005_create_indexer_state_table": _005_create_indexer_state_table_exports,
3601
+ "006_create_document_snapshot_table": _006_create_document_snapshot_table_exports,
3602
+ "007_create_slug_mapping_table": _007_create_slug_mapping_table_exports,
3603
+ "008_create_view_state_table": _008_create_view_state_table_exports,
3604
+ "009_create_operation_index_tables": _009_create_operation_index_tables_exports,
3605
+ "010_create_sync_tables": _010_create_sync_tables_exports,
3606
+ "011_add_cursor_type_column": _011_add_cursor_type_column_exports,
3607
+ "012_add_source_remote_column": _012_add_source_remote_column_exports,
3608
+ "013_create_sync_dead_letters_table": _013_create_sync_dead_letters_table_exports,
3609
+ "014_create_processor_cursor_table": _014_create_processor_cursor_table_exports,
3610
+ "015_add_operation_denied_reason": _015_add_operation_denied_reason_exports,
3611
+ "016_add_dead_letter_error_type": _016_add_dead_letter_error_type_exports,
3612
+ "017_create_group_references": _017_create_group_references_exports,
3613
+ "018_add_sync_remote_bound_address": _018_add_sync_remote_bound_address_exports,
3614
+ "019_require_action_id": _019_require_action_id_exports,
3615
+ "020_add_snapshot_operation_ordinal": _020_add_snapshot_operation_ordinal_exports,
3616
+ "021_add_sync_remote_peer": _021_add_sync_remote_peer_exports,
3617
+ "022_create_sync_holds": _022_create_sync_holds_exports,
3618
+ "023_index_created_protocol_versions": _023_index_created_protocol_versions_exports,
3619
+ "024_create_document_purges": _024_create_document_purges_exports,
3620
+ "025_create_sync_received_markers": _025_create_sync_received_markers_exports,
3621
+ "026_create_sync_purge_refusals": _026_create_sync_purge_refusals_exports
3622
+ };
3623
+ var ProgrammaticMigrationProvider = class {
3624
+ getMigrations() {
3625
+ return Promise.resolve(migrations);
3626
+ }
3627
+ };
3628
+ /**
3629
+ * Applies every pending migration, or every one up to and including `upTo`.
3630
+ *
3631
+ * The bound exists so a test can reach the schema a data migration is written
3632
+ * against, populate it, and then migrate across the migration under test.
3633
+ */
3634
+ async function runMigrations(db, schema = REACTOR_SCHEMA, upTo) {
3635
+ try {
3636
+ await sql`CREATE SCHEMA IF NOT EXISTS ${sql.id(schema)}`.execute(db);
3637
+ } catch (error) {
3638
+ return {
3639
+ success: false,
3640
+ migrationsExecuted: [],
3641
+ error: error instanceof Error ? error : /* @__PURE__ */ new Error("Failed to create schema")
3642
+ };
3643
+ }
3644
+ const migrator = new Migrator({
3645
+ db: db.withSchema(schema),
3646
+ provider: new ProgrammaticMigrationProvider(),
3647
+ migrationTableSchema: schema
3648
+ });
3649
+ let error;
3650
+ let results;
3651
+ try {
3652
+ const result = upTo ? await migrator.migrateTo(upTo) : await migrator.migrateToLatest();
3653
+ error = result.error;
3654
+ results = result.results;
3655
+ } catch (e) {
3656
+ error = e;
3657
+ results = [];
3658
+ }
3659
+ const migrationsExecuted = results?.map((result) => result.migrationName) ?? [];
3660
+ if (error) return {
3661
+ success: false,
3662
+ migrationsExecuted,
3663
+ error: error instanceof Error ? error : /* @__PURE__ */ new Error("Unknown migration error")
3664
+ };
3665
+ return {
3666
+ success: true,
3667
+ migrationsExecuted
3668
+ };
3669
+ }
3670
+ async function getMigrationStatus(db, schema = REACTOR_SCHEMA) {
3671
+ return await new Migrator({
3672
+ db: db.withSchema(schema),
3673
+ provider: new ProgrammaticMigrationProvider(),
3674
+ migrationTableSchema: schema
3675
+ }).getMigrations();
3676
+ }
3677
+ //#endregion
3678
+ //#region src/executor/worker/error-info.ts
3679
+ const MAX_STACK_LENGTH = 8192;
3680
+ const MAX_CAUSE_DEPTH = 8;
3681
+ function toErrorInfo(err, depth = 0) {
3682
+ if (depth > MAX_CAUSE_DEPTH) return {
3683
+ name: "Error",
3684
+ message: "cause chain truncated"
3685
+ };
3686
+ if (err instanceof Error) {
3687
+ const info = {
3688
+ name: err.name || "Error",
3689
+ message: err.message
3690
+ };
3691
+ if (typeof err.stack === "string") info.stack = err.stack.length > MAX_STACK_LENGTH ? err.stack.slice(0, MAX_STACK_LENGTH) : err.stack;
3692
+ if (err.cause !== void 0) info.cause = toErrorInfo(err.cause, depth + 1);
3693
+ const documentId = err.documentId;
3694
+ if (typeof documentId === "string") info.documentId = documentId;
3695
+ const { documentType, requestedVersion } = err;
3696
+ if (typeof documentType === "string") info.documentType = documentType;
3697
+ if (typeof requestedVersion === "number") info.requestedVersion = requestedVersion;
3698
+ return info;
3699
+ }
3700
+ if (typeof err === "string") return {
3701
+ name: "Error",
3702
+ message: err
3703
+ };
3704
+ if (err && typeof err === "object") {
3705
+ const record = err;
3706
+ return {
3707
+ name: typeof record.name === "string" ? record.name : "Error",
3708
+ message: typeof record.message === "string" ? record.message : safeStringify(err)
3709
+ };
3710
+ }
3711
+ return {
3712
+ name: "Error",
3713
+ message: safeStringify(err)
3714
+ };
3715
+ }
3716
+ function safeStringify(value) {
3717
+ if (value === null || value === void 0) return String(value);
3718
+ if (typeof value === "string") return value;
3719
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
3720
+ try {
3721
+ return JSON.stringify(value);
3722
+ } catch {
3723
+ return "[unserializable]";
3724
+ }
3725
+ }
3726
+ function fromErrorInfo(info) {
3727
+ const err = new Error(info.message);
3728
+ Object.defineProperty(err, "name", {
3729
+ value: info.name,
3730
+ configurable: true,
3731
+ writable: true
3732
+ });
3733
+ if (info.stack !== void 0) err.stack = info.stack;
3734
+ if (info.documentId !== void 0) Object.defineProperty(err, "documentId", {
3735
+ value: info.documentId,
3736
+ configurable: true,
3737
+ writable: true
3738
+ });
3739
+ for (const key of ["documentType", "requestedVersion"]) if (info[key] !== void 0) Object.defineProperty(err, key, {
3740
+ value: info[key],
3741
+ configurable: true,
3742
+ writable: true
3743
+ });
3744
+ if (info.cause !== void 0) Object.defineProperty(err, "cause", {
3745
+ value: fromErrorInfo(info.cause),
3746
+ configurable: true,
3747
+ writable: true
3748
+ });
3749
+ return err;
3750
+ }
3751
+ //#endregion
3752
+ //#region src/executor/worker/sanitize.ts
3753
+ const MAX_DEPTH = 8;
3754
+ function errorToInfo(err) {
3755
+ return toErrorInfo(err);
3756
+ }
3757
+ function sanitizeArg(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
3758
+ if (depth > MAX_DEPTH) return "[Truncated]";
3759
+ if (value === null || value === void 0) return null;
3760
+ if (typeof value === "boolean") return value;
3761
+ if (typeof value === "number") return value;
3762
+ if (typeof value === "string") return value;
3763
+ if (typeof value === "bigint") return value.toString();
3764
+ if (typeof value === "function" || typeof value === "symbol") return null;
3765
+ if (value instanceof Error) return toErrorInfo(value);
3766
+ if (value instanceof Date) return value.toISOString();
3767
+ if (value instanceof Map) {
3768
+ const obj = {};
3769
+ for (const [k, v] of value.entries()) obj[String(k)] = sanitizeArg(v, depth + 1, seen);
3770
+ return obj;
3771
+ }
3772
+ if (value instanceof Set) {
3773
+ const arr = [];
3774
+ for (const v of value.values()) arr.push(sanitizeArg(v, depth + 1, seen));
3775
+ return arr;
3776
+ }
3777
+ if (Array.isArray(value)) {
3778
+ if (seen.has(value)) return "[Circular]";
3779
+ seen.add(value);
3780
+ const result = value.map((v) => sanitizeArg(v, depth + 1, seen));
3781
+ seen.delete(value);
3782
+ return result;
3783
+ }
3784
+ if (typeof value === "object") {
3785
+ const proto = Object.getPrototypeOf(value);
3786
+ if (proto !== Object.prototype && proto !== null) return { __nonClonable: value.constructor?.name || "Unknown" };
3787
+ if (seen.has(value)) return "[Circular]";
3788
+ seen.add(value);
3789
+ const result = {};
3790
+ for (const [k, v] of Object.entries(value)) result[k] = sanitizeArg(v, depth + 1, seen);
3791
+ seen.delete(value);
3792
+ return result;
3793
+ }
3794
+ return null;
3795
+ }
3796
+ //#endregion
3797
+ //#region src/executor/worker/forwarding-logger.ts
3798
+ function createForwardingLogger(post, level = "info", tags = []) {
3799
+ function safePost(msg) {
3800
+ try {
3801
+ post(msg);
3802
+ } catch {
3803
+ try {
3804
+ process.stderr.write(`[forwarding-logger] postMessage failed: ${msg.level} ${msg.message}\n`);
3805
+ } catch {}
3806
+ }
3807
+ }
3808
+ function emit(wireLevel, message, args) {
3809
+ const sanitizedArgs = args.map((a) => sanitizeArg(a));
3810
+ safePost({
3811
+ type: "log",
3812
+ level: wireLevel,
3813
+ message,
3814
+ args: tags.length > 0 ? [sanitizeArg({ tags }), ...sanitizedArgs] : sanitizedArgs,
3815
+ timestamp: Date.now()
3816
+ });
3817
+ }
3818
+ return {
3819
+ level,
3820
+ verbose: (message, ...args) => emit("debug", message, args),
3821
+ debug: (message, ...args) => emit("debug", message, args),
3822
+ info: (message, ...args) => emit("info", message, args),
3823
+ warn: (message, ...args) => emit("warn", message, args),
3824
+ error: (message, ...args) => emit("error", message, args),
3825
+ errorHandler: ((...data) => {
3826
+ const first = data[0];
3827
+ emit("error", typeof first === "string" ? first : "", data.slice(1));
3828
+ }),
3829
+ child: (childTags) => createForwardingLogger(post, level, [...tags, ...childTags])
3830
+ };
3831
+ }
3832
+ //#endregion
3833
+ //#region src/executor/worker/load-spec.ts
3834
+ /** Imports `spec.module` and calls the export with `spec.initArgs`, or returns it if not a function. */
3835
+ async function defaultLoadFactory(spec) {
3836
+ const ref = spec.module;
3837
+ const exported = (await ("filePath" in ref ? import(new URL(`file://${ref.filePath}`).href) : import(ref.packageName)))[ref.exportName];
3838
+ if (typeof exported === "function") return exported(spec.initArgs);
3839
+ return exported;
3840
+ }
3841
+ /** Loads the document model module a manifest entry's spec names. */
3842
+ async function loadDocumentModelSpec(spec) {
3843
+ return await defaultLoadFactory(spec);
3844
+ }
3845
+ //#endregion
3846
+ export { relationshipTarget as $, CollectionMembershipCache as A, matchesScope as At, TouchedStreams as B, ProbeSettler as C, RelationshipNotFoundError as Ct, parseSnapshot as D, UpgradePreconditionFailedError as Dt, describeWaitingSessions as E, UnsupportedStoredProtocolError as Et, ModuleNotFoundError as F, createConsistencyToken as G, applyUpgradeDocumentAction$1 as H, DOCUMENT_SCOPE_ACTIONS as I, createOperation as J, createDocumentFromAction as K, FencedWriteCache as L, DuplicateModuleError as M, throwIfAborted as Mt, InvalidModuleError as N, yieldToMain as Nt, snapshotFunctionsFor as O, EventBusAggregateError as Ot, ModelNotWorkerImportableError as P, __exportAll as Pt, refusalError as Q, GATED_DOCUMENT_ACTIONS as R, listPurged as S, PurgeTooLargeError as St, createKyselyWatermarkProbe as T, UnsupportedProtocolVersionError as Tt, buildErrorResult as U, applyDeleteDocumentAction$1 as V, buildSuccessResult as W, isGenesisOperation as X, getNextIndexForScope as Y, jobWriteIds as Z, KyselyOperationIndex as _, DocumentPurgedError as _t, sanitizeArg as a, AppendConditionFailedError as at, acquirePurgeLocks as b, InvalidOperationTimestampError as bt, getMigrationStatus as c, DuplicateOperationError as ct, instrumentPgPool as d, AuthEnforcementDisabledError as dt, submittedActionIds as et, KyselyOperationStore as f, AuthTimestampNotMonotonicError as ft, KyselyWriteCache as g, DocumentNotFoundError as gt, EventBus as h, DocumentNotDeletedError as ht, errorToInfo as i, APPEND_CONDITION_FAILED_PREFIX as it, DuplicateManifestError as j, parsePagingOptions as jt, DocumentMetaCache as k, ReactorEventTypes as kt, runMigrations as l, OptimisticLockError as lt, DocumentModelRegistry as m, DocumentDeletedError as mt, loadDocumentModelSpec as n, targetDocumentId as nt, fromErrorInfo as o, DocumentAlreadyExistsError as ot, KyselyKeyframeStore as p, AuthorizationDeniedError as pt, createEmptyConsistencyToken as q, createForwardingLogger as r, updateDocumentRevision as rt, REACTOR_SCHEMA as s, DocumentExistence as st, defaultLoadFactory as t, summarizeSubmittedActions as tt, createForwardingPoolInstrumentation as u, RevisionMismatchError as ut, PURGE_LOCK_BUCKETS as v, ExcessiveReshuffleError as vt, SettledWatermark as w, ReservedActionError as wt, findPurged as x, InvalidSignatureError as xt, PURGE_NS as y, GroupInUseError as yt, PurgeFence as z };
3847
+
3848
+ //# sourceMappingURL=load-spec-f2ppXyKZ.js.map