@powerhousedao/reactor 6.2.3-dev.5 → 6.2.3-dev.51

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