@powerhousedao/reactor 6.1.0-dev.0 → 6.1.0-dev.10

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 (37) hide show
  1. package/dist/build-worker-executor-DDVXB921.js +83 -0
  2. package/dist/build-worker-executor-DDVXB921.js.map +1 -0
  3. package/dist/document-indexer-B2iLRB0o.js +917 -0
  4. package/dist/document-indexer-B2iLRB0o.js.map +1 -0
  5. package/dist/drive-container-types-BNpMlgT_.js +2964 -0
  6. package/dist/drive-container-types-BNpMlgT_.js.map +1 -0
  7. package/dist/entry.d.ts +1 -0
  8. package/dist/entry.js +313 -0
  9. package/dist/entry.js.map +1 -0
  10. package/dist/error-info-Cpu4OY3o.js +62 -0
  11. package/dist/error-info-Cpu4OY3o.js.map +1 -0
  12. package/dist/errors-D3S6Eysd.js +56 -0
  13. package/dist/errors-D3S6Eysd.js.map +1 -0
  14. package/dist/forwarding-logger-BBkMSxuJ.js +85 -0
  15. package/dist/forwarding-logger-BBkMSxuJ.js.map +1 -0
  16. package/dist/index.d.ts +991 -75
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +900 -3889
  19. package/dist/index.js.map +1 -1
  20. package/dist/projection-entry.d.ts +1 -0
  21. package/dist/projection-entry.js +406 -0
  22. package/dist/projection-entry.js.map +1 -0
  23. package/dist/projection-shard-manager-_c7orNo5.js +313 -0
  24. package/dist/projection-shard-manager-_c7orNo5.js.map +1 -0
  25. package/dist/projection-worker-wI4PwcV2.js +13 -0
  26. package/dist/projection-worker-wI4PwcV2.js.map +1 -0
  27. package/dist/transport-ByGviWdZ.js +33 -0
  28. package/dist/transport-ByGviWdZ.js.map +1 -0
  29. package/dist/transport-CuogVKN_.js +23 -0
  30. package/dist/transport-CuogVKN_.js.map +1 -0
  31. package/dist/types-CxSpmNGK.js +32 -0
  32. package/dist/types-CxSpmNGK.js.map +1 -0
  33. package/dist/worker-SUoDhurA.js +22 -0
  34. package/dist/worker-SUoDhurA.js.map +1 -0
  35. package/dist/worker-handle-B1w03nRA.js +383 -0
  36. package/dist/worker-handle-B1w03nRA.js.map +1 -0
  37. package/package.json +6 -4
@@ -0,0 +1,917 @@
1
+ import { n as ReactorEventTypes } from "./types-CxSpmNGK.js";
2
+ import { v4 } from "uuid";
3
+ import { childLogger } from "document-model";
4
+ //#region src/read-models/base-read-model.ts
5
+ /**
6
+ * Base class for read models that provides catch-up/rewind functionality.
7
+ * Handles initialization, state tracking via ViewState table, and consistency tracking.
8
+ * Subclasses override commitOperations() with their specific domain logic.
9
+ */
10
+ var BaseReadModel = class {
11
+ lastOrdinal = 0;
12
+ name;
13
+ constructor(db, operationIndex, writeCache, consistencyTracker, config) {
14
+ this.db = db;
15
+ this.operationIndex = operationIndex;
16
+ this.writeCache = writeCache;
17
+ this.consistencyTracker = consistencyTracker;
18
+ this.config = config;
19
+ this.name = config.readModelId;
20
+ }
21
+ /**
22
+ * Initializes the read model by loading state and catching up on missed operations.
23
+ */
24
+ async init() {
25
+ const viewState = await this.loadState();
26
+ if (viewState !== void 0) {
27
+ this.lastOrdinal = viewState;
28
+ const missedOperations = await this.operationIndex.getSinceOrdinal(this.lastOrdinal);
29
+ if (missedOperations.results.length > 0) {
30
+ const ops = this.config.rebuildStateOnInit ? await this.rebuildStateForOperations(missedOperations.results) : missedOperations.results;
31
+ await this.indexOperations(ops);
32
+ }
33
+ } else {
34
+ await this.initializeState();
35
+ const allOperations = await this.operationIndex.getSinceOrdinal(0);
36
+ if (allOperations.results.length > 0) {
37
+ const ops = this.config.rebuildStateOnInit ? await this.rebuildStateForOperations(allOperations.results) : allOperations.results;
38
+ await this.indexOperations(ops);
39
+ }
40
+ }
41
+ }
42
+ /**
43
+ * Template method: runs domain-specific commitOperations, then persists
44
+ * state and updates consistency tracking.
45
+ */
46
+ async indexOperations(items) {
47
+ if (items.length === 0) return;
48
+ await this.commitOperations(items);
49
+ await this.db.transaction().execute(async (trx) => {
50
+ await this.saveState(trx, items);
51
+ });
52
+ this.updateConsistencyTracker(items);
53
+ }
54
+ /**
55
+ * Waits for the read model to reach the specified consistency level.
56
+ */
57
+ async waitForConsistency(token, timeoutMs, signal) {
58
+ if (token.coordinates.length === 0) return;
59
+ await this.consistencyTracker.waitFor(token.coordinates, timeoutMs, signal);
60
+ }
61
+ async commitOperations(items) {}
62
+ /**
63
+ * Rebuilds document state for each operation using the write cache.
64
+ */
65
+ async rebuildStateForOperations(operations) {
66
+ const result = [];
67
+ for (const op of operations) {
68
+ const { documentId, scope, branch } = op.context;
69
+ const targetRevision = op.operation.index;
70
+ const document = await this.writeCache.getState(documentId, scope, branch, targetRevision);
71
+ result.push({
72
+ operation: op.operation,
73
+ context: {
74
+ ...op.context,
75
+ resultingState: JSON.stringify(document)
76
+ }
77
+ });
78
+ }
79
+ return result;
80
+ }
81
+ /**
82
+ * Loads the last processed ordinal from the ViewState table.
83
+ * Returns undefined if no state exists for this read model.
84
+ */
85
+ async loadState() {
86
+ return (await this.db.selectFrom("ViewState").select("lastOrdinal").where("readModelId", "=", this.config.readModelId).executeTakeFirst())?.lastOrdinal;
87
+ }
88
+ /**
89
+ * Initializes the ViewState row for this read model.
90
+ */
91
+ async initializeState() {
92
+ await this.db.insertInto("ViewState").values({
93
+ readModelId: this.config.readModelId,
94
+ lastOrdinal: 0
95
+ }).execute();
96
+ }
97
+ /**
98
+ * Saves the last processed ordinal to the ViewState table.
99
+ */
100
+ async saveState(trx, items) {
101
+ const maxOrdinal = Math.max(...items.map((item) => item.context.ordinal));
102
+ this.lastOrdinal = maxOrdinal;
103
+ await trx.updateTable("ViewState").set({
104
+ lastOrdinal: maxOrdinal,
105
+ lastOperationTimestamp: /* @__PURE__ */ new Date()
106
+ }).where("readModelId", "=", this.config.readModelId).execute();
107
+ }
108
+ /**
109
+ * Updates the consistency tracker with the processed operations.
110
+ */
111
+ updateConsistencyTracker(items) {
112
+ const coordinates = [];
113
+ for (let i = 0; i < items.length; i++) {
114
+ const item = items[i];
115
+ coordinates.push({
116
+ documentId: item.context.documentId,
117
+ scope: item.context.scope,
118
+ branch: item.context.branch,
119
+ operationIndex: item.operation.index
120
+ });
121
+ }
122
+ this.consistencyTracker.update(coordinates);
123
+ }
124
+ };
125
+ //#endregion
126
+ //#region src/read-models/coordinator.ts
127
+ /**
128
+ * Coordinates read model synchronization by listening to operation write events
129
+ * and updating all registered read models on per-`documentId:scope:branch`
130
+ * serial chains. Cross-key projection runs in parallel; same-key projection is
131
+ * serialized so the executor can return to dispatch without holding ordering
132
+ * implicitly.
133
+ */
134
+ var ReadModelCoordinator = class {
135
+ unsubscribe;
136
+ isRunning = false;
137
+ chains = /* @__PURE__ */ new Map();
138
+ logger;
139
+ readModels;
140
+ constructor(eventBus, preReady, postReady) {
141
+ this.eventBus = eventBus;
142
+ this.preReady = preReady;
143
+ this.postReady = postReady;
144
+ this.readModels = [...preReady, ...postReady];
145
+ this.logger = childLogger(["reactor", "read-model-coordinator"]);
146
+ }
147
+ start() {
148
+ if (this.isRunning) return;
149
+ this.unsubscribe = this.eventBus.subscribe(ReactorEventTypes.JOB_WRITE_READY, (type, event) => {
150
+ return this.handleWriteReady(event);
151
+ });
152
+ this.isRunning = true;
153
+ }
154
+ stop() {
155
+ if (!this.isRunning) return;
156
+ if (this.unsubscribe) {
157
+ this.unsubscribe();
158
+ this.unsubscribe = void 0;
159
+ }
160
+ this.isRunning = false;
161
+ }
162
+ /**
163
+ * Resolves when every per-queueKey projection chain has flushed. Intended
164
+ * for test fixtures and explicit shutdown; production callers use
165
+ * consistency tokens instead.
166
+ */
167
+ async drain() {
168
+ while (this.chains.size > 0) {
169
+ const pending = Array.from(this.chains.values());
170
+ await Promise.allSettled(pending);
171
+ }
172
+ }
173
+ getChainDepth() {
174
+ return this.chains.size;
175
+ }
176
+ handleWriteReady(event) {
177
+ if (event.operations.length === 0) {
178
+ this.emitEmptyReadReady(event);
179
+ return;
180
+ }
181
+ const enqueuedAt = performance.now();
182
+ const key = this.queueKeyFor(event);
183
+ const current = (this.chains.get(key) ?? Promise.resolve()).then(() => this.runChain(event, enqueuedAt));
184
+ this.chains.set(key, current);
185
+ current.finally(() => {
186
+ if (this.chains.get(key) === current) this.chains.delete(key);
187
+ });
188
+ }
189
+ async emitEmptyReadReady(event) {
190
+ const readyEvent = {
191
+ jobId: event.jobId,
192
+ operations: event.operations
193
+ };
194
+ try {
195
+ await this.eventBus.emit(ReactorEventTypes.JOB_READ_READY, readyEvent);
196
+ } catch (error) {
197
+ this.logger.error("JOB_READ_READY emit failed for job @jobId: @Error", { jobId: event.jobId }, error);
198
+ }
199
+ this.emitBatchCompleted({
200
+ jobId: event.jobId,
201
+ batchSize: 0,
202
+ chainWaitDurationMs: 0,
203
+ preReadyDurationMs: 0,
204
+ emitDurationMs: 0,
205
+ postReadyDurationMs: 0
206
+ });
207
+ }
208
+ async runChain(event, enqueuedAt) {
209
+ const chainWaitDurationMs = performance.now() - enqueuedAt;
210
+ const preReadyStart = performance.now();
211
+ try {
212
+ await Promise.all(this.preReady.map((readModel) => this.indexWithTiming(readModel, "pre_ready", event)));
213
+ } catch (error) {
214
+ this.logger.error("Pre-ready read model indexing failed for job @jobId: @Error", { jobId: event.jobId }, error);
215
+ }
216
+ const preReadyDurationMs = performance.now() - preReadyStart;
217
+ const readyEvent = {
218
+ jobId: event.jobId,
219
+ operations: event.operations
220
+ };
221
+ const emitStart = performance.now();
222
+ try {
223
+ await this.eventBus.emit(ReactorEventTypes.JOB_READ_READY, readyEvent);
224
+ } catch (error) {
225
+ this.logger.error("JOB_READ_READY emit failed for job @jobId: @Error", { jobId: event.jobId }, error);
226
+ }
227
+ const emitDurationMs = performance.now() - emitStart;
228
+ const postReadyStart = performance.now();
229
+ try {
230
+ await Promise.all(this.postReady.map((readModel) => this.indexWithTiming(readModel, "post_ready", event)));
231
+ } catch (error) {
232
+ this.logger.error("Post-ready read model indexing failed for job @jobId: @Error", { jobId: event.jobId }, error);
233
+ }
234
+ const postReadyDurationMs = performance.now() - postReadyStart;
235
+ this.emitBatchCompleted({
236
+ jobId: event.jobId,
237
+ batchSize: event.operations.length,
238
+ chainWaitDurationMs,
239
+ preReadyDurationMs,
240
+ emitDurationMs,
241
+ postReadyDurationMs
242
+ });
243
+ }
244
+ async indexWithTiming(readModel, stage, event) {
245
+ const start = performance.now();
246
+ let success = false;
247
+ try {
248
+ await readModel.indexOperations(event.operations);
249
+ success = true;
250
+ } finally {
251
+ this.emitReadModelIndexed({
252
+ jobId: event.jobId,
253
+ readModelName: readModel.name,
254
+ stage,
255
+ durationMs: performance.now() - start,
256
+ operationCount: event.operations.length,
257
+ success
258
+ });
259
+ }
260
+ }
261
+ emitReadModelIndexed(payload) {
262
+ this.eventBus.emit(ReactorEventTypes.READMODEL_INDEXED, payload).catch((err) => this.logger.error("READMODEL_INDEXED emit failed for job @jobId: @Error", { jobId: payload.jobId }, err));
263
+ }
264
+ emitBatchCompleted(payload) {
265
+ this.eventBus.emit(ReactorEventTypes.READMODEL_BATCH_COMPLETED, payload).catch((err) => this.logger.error("READMODEL_BATCH_COMPLETED emit failed for job @jobId: @Error", { jobId: payload.jobId }, err));
266
+ }
267
+ queueKeyFor(event) {
268
+ const ctx = event.operations[0].context;
269
+ return `${ctx.documentId}:${ctx.scope}:${ctx.branch}`;
270
+ }
271
+ };
272
+ //#endregion
273
+ //#region src/read-models/document-view.ts
274
+ var KyselyDocumentView = class extends BaseReadModel {
275
+ _db;
276
+ constructor(db, operationStore, operationIndex, writeCache, consistencyTracker) {
277
+ super(db, operationIndex, writeCache, consistencyTracker, {
278
+ readModelId: "document-view",
279
+ rebuildStateOnInit: true
280
+ });
281
+ this.operationStore = operationStore;
282
+ this._db = db;
283
+ }
284
+ async commitOperations(items) {
285
+ await this._db.transaction().execute(async (trx) => {
286
+ for (const item of items) {
287
+ const { operation, context } = item;
288
+ const { documentId, scope, branch, documentType, resultingState } = context;
289
+ const { index, hash } = operation;
290
+ if (!resultingState) throw new Error(`Missing resultingState in context for operation ${operation.id || "unknown"}. IDocumentView requires resultingState from upstream - it does not rebuild documents.`);
291
+ let fullState;
292
+ try {
293
+ fullState = JSON.parse(resultingState);
294
+ } catch (error) {
295
+ throw new Error(`Failed to parse resultingState for operation ${operation.id || "unknown"}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
296
+ }
297
+ const operationType = operation.action.type;
298
+ if (operationType === "DELETE_DOCUMENT") {
299
+ const now = /* @__PURE__ */ new Date();
300
+ await trx.updateTable("DocumentSnapshot").set({
301
+ isDeleted: true,
302
+ deletedAt: now,
303
+ lastOperationIndex: index,
304
+ lastOperationHash: hash,
305
+ lastUpdatedAt: now
306
+ }).where("documentId", "=", documentId).where("branch", "=", branch).execute();
307
+ await trx.deleteFrom("SlugMapping").where("documentId", "=", documentId).where("branch", "=", branch).execute();
308
+ continue;
309
+ }
310
+ let scopesToIndex;
311
+ if (operationType === "CREATE_DOCUMENT") scopesToIndex = Object.entries(fullState).filter(([key]) => key === "header" || key === "document" || key === "auth");
312
+ else if (operationType === "UPGRADE_DOCUMENT") {
313
+ const scopeStatesToIndex = [];
314
+ for (const [scopeName, scopeState] of Object.entries(fullState)) {
315
+ if (scopeName === "header") {
316
+ scopeStatesToIndex.push([scopeName, scopeState]);
317
+ continue;
318
+ }
319
+ if (scopeName === scope) {
320
+ scopeStatesToIndex.push([scopeName, scopeState]);
321
+ continue;
322
+ }
323
+ if (!await trx.selectFrom("DocumentSnapshot").select("scope").where("documentId", "=", documentId).where("scope", "=", scopeName).where("branch", "=", branch).executeTakeFirst()) scopeStatesToIndex.push([scopeName, scopeState]);
324
+ }
325
+ scopesToIndex = scopeStatesToIndex;
326
+ } else {
327
+ scopesToIndex = [];
328
+ if (fullState.header !== void 0) scopesToIndex.push(["header", fullState.header]);
329
+ if (fullState[scope] !== void 0) scopesToIndex.push([scope, fullState[scope]]);
330
+ else scopesToIndex.push([scope, {}]);
331
+ }
332
+ for (const [scopeName, scopeState] of scopesToIndex) {
333
+ const existingSnapshot = await trx.selectFrom("DocumentSnapshot").selectAll().where("documentId", "=", documentId).where("scope", "=", scopeName).where("branch", "=", branch).executeTakeFirst();
334
+ const newState = typeof scopeState === "object" && scopeState !== null ? scopeState : {};
335
+ let slug = existingSnapshot?.slug ?? null;
336
+ let name = existingSnapshot?.name ?? null;
337
+ if (scopeName === "header") {
338
+ const headerSlug = newState.slug;
339
+ const headerName = newState.name;
340
+ if (typeof headerSlug === "string") slug = headerSlug;
341
+ if (typeof headerName === "string") name = headerName;
342
+ if (slug && slug !== documentId) await trx.insertInto("SlugMapping").values({
343
+ slug,
344
+ documentId,
345
+ scope: scopeName,
346
+ branch
347
+ }).onConflict((oc) => oc.column("slug").doUpdateSet({
348
+ documentId,
349
+ scope: scopeName,
350
+ branch
351
+ })).execute();
352
+ }
353
+ if (existingSnapshot) await trx.updateTable("DocumentSnapshot").set({
354
+ lastOperationIndex: index,
355
+ lastOperationHash: hash,
356
+ lastUpdatedAt: /* @__PURE__ */ new Date(),
357
+ snapshotVersion: existingSnapshot.snapshotVersion + 1,
358
+ content: newState,
359
+ slug,
360
+ name
361
+ }).where("documentId", "=", documentId).where("scope", "=", scopeName).where("branch", "=", branch).execute();
362
+ else {
363
+ const snapshot = {
364
+ id: v4(),
365
+ documentId,
366
+ slug,
367
+ name,
368
+ scope: scopeName,
369
+ branch,
370
+ content: newState,
371
+ documentType,
372
+ lastOperationIndex: index,
373
+ lastOperationHash: hash,
374
+ identifiers: null,
375
+ metadata: null,
376
+ deletedAt: null
377
+ };
378
+ await trx.insertInto("DocumentSnapshot").values(snapshot).execute();
379
+ }
380
+ }
381
+ }
382
+ });
383
+ }
384
+ async exists(documentIds, consistencyToken, signal) {
385
+ if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
386
+ if (signal?.aborted) throw new Error("Operation aborted");
387
+ if (documentIds.length === 0) return [];
388
+ const snapshots = await this._db.selectFrom("DocumentSnapshot").select(["documentId"]).where("documentId", "in", documentIds).where("isDeleted", "=", false).distinct().execute();
389
+ const existingIds = new Set(snapshots.map((s) => s.documentId));
390
+ return documentIds.map((id) => existingIds.has(id));
391
+ }
392
+ async get(documentId, view, consistencyToken, signal) {
393
+ if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
394
+ if (signal?.aborted) throw new Error("Operation aborted");
395
+ const branch = view?.branch || "main";
396
+ let scopesToQuery;
397
+ if (view?.scopes && view.scopes.length > 0) scopesToQuery = [...new Set([
398
+ "header",
399
+ "document",
400
+ ...view.scopes
401
+ ])];
402
+ else scopesToQuery = [];
403
+ let query = this._db.selectFrom("DocumentSnapshot").selectAll().where("documentId", "=", documentId).where("branch", "=", branch).where("isDeleted", "=", false);
404
+ if (scopesToQuery.length > 0) query = query.where("scope", "in", scopesToQuery);
405
+ const snapshots = await query.execute();
406
+ if (snapshots.length === 0) throw new Error(`Document not found: ${documentId}`);
407
+ if (signal?.aborted) throw new Error("Operation aborted");
408
+ const headerSnapshot = snapshots.find((s) => s.scope === "header");
409
+ if (!headerSnapshot) throw new Error(`Document header not found: ${documentId}`);
410
+ const header = headerSnapshot.content;
411
+ const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
412
+ header.revision = revisions.revision;
413
+ header.lastModifiedAtUtcIso = revisions.latestTimestamp;
414
+ const state = {};
415
+ for (const snapshot of snapshots) {
416
+ if (snapshot.scope === "header") continue;
417
+ state[snapshot.scope] = snapshot.content;
418
+ }
419
+ return {
420
+ header,
421
+ state,
422
+ operations: {},
423
+ initialState: state,
424
+ clipboard: []
425
+ };
426
+ }
427
+ async getMany(documentIds, view, consistencyToken, signal) {
428
+ if (documentIds.length === 0) return [];
429
+ if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
430
+ if (signal?.aborted) throw new Error("Operation aborted");
431
+ const branch = view?.branch || "main";
432
+ let scopesToQuery;
433
+ if (view?.scopes && view.scopes.length > 0) scopesToQuery = [...new Set([
434
+ "header",
435
+ "document",
436
+ ...view.scopes
437
+ ])];
438
+ else scopesToQuery = [];
439
+ let query = this._db.selectFrom("DocumentSnapshot").selectAll().where("documentId", "in", documentIds).where("branch", "=", branch).where("isDeleted", "=", false);
440
+ if (scopesToQuery.length > 0) query = query.where("scope", "in", scopesToQuery);
441
+ const allSnapshots = await query.execute();
442
+ if (signal?.aborted) throw new Error("Operation aborted");
443
+ const snapshotsByDocId = /* @__PURE__ */ new Map();
444
+ for (const snapshot of allSnapshots) {
445
+ const existing = snapshotsByDocId.get(snapshot.documentId) || [];
446
+ existing.push(snapshot);
447
+ snapshotsByDocId.set(snapshot.documentId, existing);
448
+ }
449
+ const foundDocumentIds = [...snapshotsByDocId.keys()];
450
+ const revisionResults = await Promise.all(foundDocumentIds.map((docId) => this.operationStore.getRevisions(docId, branch, signal)));
451
+ const revisionsByDocId = /* @__PURE__ */ new Map();
452
+ for (let i = 0; i < foundDocumentIds.length; i++) revisionsByDocId.set(foundDocumentIds[i], revisionResults[i]);
453
+ if (signal?.aborted) throw new Error("Operation aborted");
454
+ const documents = [];
455
+ for (const documentId of documentIds) {
456
+ const snapshots = snapshotsByDocId.get(documentId);
457
+ if (!snapshots || snapshots.length === 0) continue;
458
+ const headerSnapshot = snapshots.find((s) => s.scope === "header");
459
+ if (!headerSnapshot) continue;
460
+ const header = headerSnapshot.content;
461
+ const revisions = revisionsByDocId.get(documentId);
462
+ if (revisions) {
463
+ header.revision = revisions.revision;
464
+ header.lastModifiedAtUtcIso = revisions.latestTimestamp;
465
+ }
466
+ const state = {};
467
+ for (const snapshot of snapshots) {
468
+ if (snapshot.scope === "header") continue;
469
+ state[snapshot.scope] = snapshot.content;
470
+ }
471
+ const document = {
472
+ header,
473
+ state,
474
+ operations: {},
475
+ initialState: state,
476
+ clipboard: []
477
+ };
478
+ documents.push(document);
479
+ }
480
+ return documents;
481
+ }
482
+ async getByIdOrSlug(identifier, view, consistencyToken, signal) {
483
+ const documentId = await this.resolveIdOrSlug(identifier, view, consistencyToken, signal);
484
+ return this.get(documentId, view, void 0, signal);
485
+ }
486
+ async findByType(type, view, paging, consistencyToken, signal) {
487
+ if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
488
+ if (signal?.aborted) throw new Error("Operation aborted");
489
+ const branch = view?.branch || "main";
490
+ const startIndex = paging?.cursor ? parseInt(paging.cursor) : 0;
491
+ const limit = paging?.limit || 100;
492
+ const documents = [];
493
+ const processedDocumentIds = /* @__PURE__ */ new Set();
494
+ const allDocumentIds = [];
495
+ const snapshots = await this._db.selectFrom("DocumentSnapshot").selectAll().where("documentType", "=", type).where("branch", "=", branch).where("isDeleted", "=", false).orderBy("lastUpdatedAt", "desc").execute();
496
+ if (signal?.aborted) throw new Error("Operation aborted");
497
+ for (const snapshot of snapshots) {
498
+ if (processedDocumentIds.has(snapshot.documentId)) continue;
499
+ processedDocumentIds.add(snapshot.documentId);
500
+ allDocumentIds.push(snapshot.documentId);
501
+ }
502
+ const docsToFetch = allDocumentIds.slice(startIndex, startIndex + limit);
503
+ for (const documentId of docsToFetch) {
504
+ if (signal?.aborted) throw new Error("Operation aborted");
505
+ try {
506
+ const document = await this.get(documentId, view, void 0, signal);
507
+ documents.push(document);
508
+ } catch {
509
+ continue;
510
+ }
511
+ }
512
+ const hasMore = allDocumentIds.length > startIndex + limit;
513
+ const nextCursor = hasMore ? String(startIndex + limit) : void 0;
514
+ return {
515
+ results: documents,
516
+ options: paging || {
517
+ cursor: "0",
518
+ limit: 100
519
+ },
520
+ nextCursor,
521
+ totalCount: allDocumentIds.length,
522
+ next: hasMore ? () => this.findByType(type, view, {
523
+ cursor: nextCursor,
524
+ limit
525
+ }, consistencyToken, signal) : void 0
526
+ };
527
+ }
528
+ async resolveSlug(slug, view, consistencyToken, signal) {
529
+ if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
530
+ if (signal?.aborted) throw new Error("Operation aborted");
531
+ const branch = view?.branch || "main";
532
+ const mapping = await this._db.selectFrom("SlugMapping").select("documentId").where("slug", "=", slug).where("branch", "=", branch).executeTakeFirst();
533
+ if (!mapping) return;
534
+ if (signal?.aborted) throw new Error("Operation aborted");
535
+ if (view?.scopes && view.scopes.length > 0) {
536
+ if (!await this._db.selectFrom("DocumentSnapshot").select("scope").where("documentId", "=", mapping.documentId).where("branch", "=", branch).where("scope", "in", view.scopes).where("isDeleted", "=", false).executeTakeFirst()) return;
537
+ }
538
+ return mapping.documentId;
539
+ }
540
+ async resolveSlugs(slugs, view, consistencyToken, signal) {
541
+ return (await Promise.all(slugs.map((slug) => this.resolveSlug(slug, view, consistencyToken, signal)))).filter((id) => id !== void 0);
542
+ }
543
+ async resolveIdOrSlug(identifier, view, consistencyToken, signal) {
544
+ if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
545
+ if (signal?.aborted) throw new Error("Operation aborted");
546
+ const branch = view?.branch || "main";
547
+ const idCheckPromise = this._db.selectFrom("DocumentSnapshot").select("documentId").where("documentId", "=", identifier).where("branch", "=", branch).where("isDeleted", "=", false).executeTakeFirst();
548
+ const slugCheckPromise = this._db.selectFrom("SlugMapping").select("documentId").where("slug", "=", identifier).where("branch", "=", branch).executeTakeFirst();
549
+ const [idMatch, slugMatch] = await Promise.all([idCheckPromise, slugCheckPromise]);
550
+ if (signal?.aborted) throw new Error("Operation aborted");
551
+ const idMatchDocId = idMatch?.documentId;
552
+ const slugMatchDocId = slugMatch?.documentId;
553
+ if (idMatchDocId && slugMatchDocId && idMatchDocId !== slugMatchDocId) throw new Error(`Ambiguous identifier "${identifier}": matches both document ID "${idMatchDocId}" and slug for document ID "${slugMatchDocId}". Please use get() for ID or resolveSlug() + get() for slug to be explicit.`);
554
+ const resolvedDocumentId = idMatchDocId || slugMatchDocId;
555
+ if (!resolvedDocumentId) throw new Error(`Document not found: ${identifier}`);
556
+ return resolvedDocumentId;
557
+ }
558
+ };
559
+ //#endregion
560
+ //#region src/shared/consistency-tracker.ts
561
+ /**
562
+ * Creates a consistency key from documentId, scope, and branch.
563
+ */
564
+ function makeConsistencyKey(documentId, scope, branch) {
565
+ return `${documentId}:${scope}:${branch}`;
566
+ }
567
+ /**
568
+ * Tracks operation indexes for documents and provides read-after-write consistency guarantees.
569
+ * Maintains an in-memory map of the latest operation index for each (documentId, scope, branch) tuple.
570
+ */
571
+ var ConsistencyTracker = class {
572
+ state = /* @__PURE__ */ new Map();
573
+ waiters = [];
574
+ update(coordinates) {
575
+ const deduplicated = this.deduplicateCoordinates(coordinates);
576
+ for (let i = 0; i < deduplicated.length; i++) {
577
+ const coord = deduplicated[i];
578
+ const key = makeConsistencyKey(coord.documentId, coord.scope, coord.branch);
579
+ const current = this.state.get(key);
580
+ if (current === void 0 || coord.operationIndex > current) this.state.set(key, coord.operationIndex);
581
+ }
582
+ this.checkWaiters();
583
+ }
584
+ getLatest(key) {
585
+ return this.state.get(key);
586
+ }
587
+ waitFor(coordinates, timeoutMs, signal) {
588
+ if (signal?.aborted) return Promise.reject(/* @__PURE__ */ new Error("Operation aborted"));
589
+ if (this.areCoordinatesSatisfied(coordinates)) return Promise.resolve();
590
+ return new Promise((resolve, reject) => {
591
+ const waiter = {
592
+ coordinates,
593
+ resolve,
594
+ reject,
595
+ signal
596
+ };
597
+ if (timeoutMs !== void 0) waiter.timeoutId = setTimeout(() => {
598
+ this.removeWaiter(waiter);
599
+ reject(/* @__PURE__ */ new Error(`Consistency wait timed out after ${timeoutMs}ms`));
600
+ }, timeoutMs);
601
+ if (signal) {
602
+ const abortHandler = () => {
603
+ this.removeWaiter(waiter);
604
+ reject(/* @__PURE__ */ new Error("Operation aborted"));
605
+ };
606
+ signal.addEventListener("abort", abortHandler, { once: true });
607
+ }
608
+ this.waiters.push(waiter);
609
+ });
610
+ }
611
+ serialize() {
612
+ return Array.from(this.state.entries());
613
+ }
614
+ hydrate(entries) {
615
+ this.state.clear();
616
+ for (const [key, index] of entries) this.state.set(key, index);
617
+ }
618
+ deduplicateCoordinates(coordinates) {
619
+ const map = /* @__PURE__ */ new Map();
620
+ for (let i = 0; i < coordinates.length; i++) {
621
+ const coord = coordinates[i];
622
+ const key = makeConsistencyKey(coord.documentId, coord.scope, coord.branch);
623
+ const existing = map.get(key);
624
+ if (!existing || coord.operationIndex > existing.operationIndex) map.set(key, coord);
625
+ }
626
+ return Array.from(map.values());
627
+ }
628
+ areCoordinatesSatisfied(coordinates) {
629
+ for (let i = 0; i < coordinates.length; i++) {
630
+ const coord = coordinates[i];
631
+ const key = makeConsistencyKey(coord.documentId, coord.scope, coord.branch);
632
+ const latest = this.state.get(key);
633
+ if (latest === void 0 || latest < coord.operationIndex) return false;
634
+ }
635
+ return true;
636
+ }
637
+ checkWaiters() {
638
+ const satisfiedWaiters = [];
639
+ const unsatisfiedWaiters = [];
640
+ for (const waiter of this.waiters) {
641
+ if (waiter.signal?.aborted) continue;
642
+ if (this.areCoordinatesSatisfied(waiter.coordinates)) satisfiedWaiters.push(waiter);
643
+ else unsatisfiedWaiters.push(waiter);
644
+ }
645
+ this.waiters = unsatisfiedWaiters;
646
+ for (const waiter of satisfiedWaiters) {
647
+ if (waiter.timeoutId !== void 0) clearTimeout(waiter.timeoutId);
648
+ waiter.resolve();
649
+ }
650
+ }
651
+ removeWaiter(waiter) {
652
+ const index = this.waiters.indexOf(waiter);
653
+ if (index !== -1) this.waiters.splice(index, 1);
654
+ if (waiter.timeoutId !== void 0) clearTimeout(waiter.timeoutId);
655
+ }
656
+ };
657
+ //#endregion
658
+ //#region src/shared/collect-all-pages.ts
659
+ /**
660
+ * Collects all results from a paged result set by following the next() function
661
+ * until all pages have been fetched.
662
+ */
663
+ async function collectAllPages(firstPage, signal) {
664
+ const allResults = [...firstPage.results];
665
+ let currentPage = firstPage;
666
+ while (currentPage.next) {
667
+ if (signal?.aborted) throw new Error("Operation aborted");
668
+ currentPage = await currentPage.next();
669
+ allResults.push(...currentPage.results);
670
+ }
671
+ return allResults;
672
+ }
673
+ //#endregion
674
+ //#region src/storage/kysely/document-indexer.ts
675
+ var KyselyDocumentIndexer = class extends BaseReadModel {
676
+ _db;
677
+ constructor(db, operationIndex, writeCache, consistencyTracker) {
678
+ super(db, operationIndex, writeCache, consistencyTracker, {
679
+ readModelId: "document-indexer",
680
+ rebuildStateOnInit: false
681
+ });
682
+ this._db = db;
683
+ }
684
+ async commitOperations(items) {
685
+ await this._db.transaction().execute(async (trx) => {
686
+ for (const item of items) {
687
+ const { operation } = item;
688
+ const actionType = operation.action.type;
689
+ if (actionType === "ADD_RELATIONSHIP") await this.handleAddRelationship(trx, operation);
690
+ else if (actionType === "REMOVE_RELATIONSHIP") await this.handleRemoveRelationship(trx, operation);
691
+ else if (actionType === "UPDATE_RELATIONSHIP") await this.handleUpdateRelationship(trx, operation);
692
+ }
693
+ });
694
+ }
695
+ async getOutgoing(documentId, types, paging, consistencyToken, signal) {
696
+ if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
697
+ if (signal?.aborted) throw new Error("Operation aborted");
698
+ const startIndex = paging?.cursor ? parseInt(paging.cursor) : 0;
699
+ const limit = paging?.limit || 100;
700
+ let query = this._db.selectFrom("DocumentRelationship").selectAll().where("sourceId", "=", documentId);
701
+ if (types && types.length > 0) query = query.where("relationshipType", "in", types);
702
+ const rows = await query.orderBy("createdAt", "asc").orderBy("id", "asc").offset(startIndex).limit(limit + 1).execute();
703
+ const hasMore = rows.length > limit;
704
+ const results = hasMore ? rows.slice(0, limit) : rows;
705
+ const nextCursor = hasMore ? String(startIndex + limit) : void 0;
706
+ return {
707
+ results: results.map((row) => ({
708
+ sourceId: row.sourceId,
709
+ targetId: row.targetId,
710
+ relationshipType: row.relationshipType,
711
+ metadata: row.metadata ? row.metadata : void 0,
712
+ createdAt: row.createdAt,
713
+ updatedAt: row.updatedAt
714
+ })),
715
+ options: paging || {
716
+ cursor: "0",
717
+ limit: 100
718
+ },
719
+ nextCursor,
720
+ next: hasMore ? () => this.getOutgoing(documentId, types, {
721
+ cursor: nextCursor,
722
+ limit
723
+ }, consistencyToken, signal) : void 0
724
+ };
725
+ }
726
+ async getIncoming(documentId, types, paging, consistencyToken, signal) {
727
+ if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
728
+ if (signal?.aborted) throw new Error("Operation aborted");
729
+ const startIndex = paging?.cursor ? parseInt(paging.cursor) : 0;
730
+ const limit = paging?.limit || 100;
731
+ let query = this._db.selectFrom("DocumentRelationship").selectAll().where("targetId", "=", documentId);
732
+ if (types && types.length > 0) query = query.where("relationshipType", "in", types);
733
+ const rows = await query.orderBy("createdAt", "asc").orderBy("id", "asc").offset(startIndex).limit(limit + 1).execute();
734
+ const hasMore = rows.length > limit;
735
+ const results = hasMore ? rows.slice(0, limit) : rows;
736
+ const nextCursor = hasMore ? String(startIndex + limit) : void 0;
737
+ return {
738
+ results: results.map((row) => ({
739
+ sourceId: row.sourceId,
740
+ targetId: row.targetId,
741
+ relationshipType: row.relationshipType,
742
+ metadata: row.metadata ? row.metadata : void 0,
743
+ createdAt: row.createdAt,
744
+ updatedAt: row.updatedAt
745
+ })),
746
+ options: paging || {
747
+ cursor: "0",
748
+ limit: 100
749
+ },
750
+ nextCursor,
751
+ next: hasMore ? () => this.getIncoming(documentId, types, {
752
+ cursor: nextCursor,
753
+ limit
754
+ }, consistencyToken, signal) : void 0
755
+ };
756
+ }
757
+ async getOrphanedChildren(parentIds, types, signal) {
758
+ if (parentIds.length === 0) return [];
759
+ if (signal?.aborted) throw new Error("Operation aborted");
760
+ let query = this._db.selectFrom("DocumentRelationship as r").select("r.targetId").distinct().where("r.sourceId", "in", parentIds).where((eb) => eb.not(eb.exists(eb.selectFrom("DocumentRelationship as other").select("other.id").whereRef("other.targetId", "=", "r.targetId").where("other.sourceId", "not in", parentIds))));
761
+ if (types && types.length > 0) query = query.where("r.relationshipType", "in", types);
762
+ return (await query.execute()).map((row) => row.targetId);
763
+ }
764
+ async hasRelationship(sourceId, targetId, types, consistencyToken, signal) {
765
+ if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
766
+ if (signal?.aborted) throw new Error("Operation aborted");
767
+ let query = this._db.selectFrom("DocumentRelationship").select("id").where("sourceId", "=", sourceId).where("targetId", "=", targetId);
768
+ if (types && types.length > 0) query = query.where("relationshipType", "in", types);
769
+ return await query.executeTakeFirst() !== void 0;
770
+ }
771
+ async getUndirectedRelationships(a, b, types, paging, consistencyToken, signal) {
772
+ if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
773
+ if (signal?.aborted) throw new Error("Operation aborted");
774
+ const startIndex = paging?.cursor ? parseInt(paging.cursor) : 0;
775
+ const limit = paging?.limit || 100;
776
+ let query = this._db.selectFrom("DocumentRelationship").selectAll().where((eb) => eb.or([eb.and([eb("sourceId", "=", a), eb("targetId", "=", b)]), eb.and([eb("sourceId", "=", b), eb("targetId", "=", a)])]));
777
+ if (types && types.length > 0) query = query.where("relationshipType", "in", types);
778
+ const rows = await query.orderBy("createdAt", "asc").orderBy("id", "asc").offset(startIndex).limit(limit + 1).execute();
779
+ const hasMore = rows.length > limit;
780
+ const results = hasMore ? rows.slice(0, limit) : rows;
781
+ const nextCursor = hasMore ? String(startIndex + limit) : void 0;
782
+ return {
783
+ results: results.map((row) => ({
784
+ sourceId: row.sourceId,
785
+ targetId: row.targetId,
786
+ relationshipType: row.relationshipType,
787
+ metadata: row.metadata ? row.metadata : void 0,
788
+ createdAt: row.createdAt,
789
+ updatedAt: row.updatedAt
790
+ })),
791
+ options: paging || {
792
+ cursor: "0",
793
+ limit: 100
794
+ },
795
+ nextCursor,
796
+ next: hasMore ? () => this.getUndirectedRelationships(a, b, types, {
797
+ cursor: nextCursor,
798
+ limit
799
+ }, consistencyToken, signal) : void 0
800
+ };
801
+ }
802
+ async getDirectedRelationships(sourceId, targetId, types, paging, consistencyToken, signal) {
803
+ if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
804
+ if (signal?.aborted) throw new Error("Operation aborted");
805
+ const startIndex = paging?.cursor ? parseInt(paging.cursor) : 0;
806
+ const limit = paging?.limit || 100;
807
+ let query = this._db.selectFrom("DocumentRelationship").selectAll().where("sourceId", "=", sourceId).where("targetId", "=", targetId);
808
+ if (types && types.length > 0) query = query.where("relationshipType", "in", types);
809
+ const rows = await query.orderBy("createdAt", "asc").orderBy("id", "asc").offset(startIndex).limit(limit + 1).execute();
810
+ const hasMore = rows.length > limit;
811
+ const results = hasMore ? rows.slice(0, limit) : rows;
812
+ const nextCursor = hasMore ? String(startIndex + limit) : void 0;
813
+ return {
814
+ results: results.map((row) => ({
815
+ sourceId: row.sourceId,
816
+ targetId: row.targetId,
817
+ relationshipType: row.relationshipType,
818
+ metadata: row.metadata ? row.metadata : void 0,
819
+ createdAt: row.createdAt,
820
+ updatedAt: row.updatedAt
821
+ })),
822
+ options: paging || {
823
+ cursor: "0",
824
+ limit: 100
825
+ },
826
+ nextCursor,
827
+ next: hasMore ? () => this.getDirectedRelationships(sourceId, targetId, types, {
828
+ cursor: nextCursor,
829
+ limit
830
+ }, consistencyToken, signal) : void 0
831
+ };
832
+ }
833
+ async findPath(sourceId, targetId, types, consistencyToken, signal) {
834
+ if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
835
+ if (signal?.aborted) throw new Error("Operation aborted");
836
+ if (sourceId === targetId) return [sourceId];
837
+ const visited = /* @__PURE__ */ new Set();
838
+ const queue = [{
839
+ id: sourceId,
840
+ path: [sourceId]
841
+ }];
842
+ while (queue.length > 0) {
843
+ const current = queue.shift();
844
+ if (current.id === targetId) return current.path;
845
+ if (visited.has(current.id)) continue;
846
+ visited.add(current.id);
847
+ const outgoingRelationships = await collectAllPages(await this.getOutgoing(current.id, types, void 0, consistencyToken, signal), signal);
848
+ for (const rel of outgoingRelationships) if (!visited.has(rel.targetId)) queue.push({
849
+ id: rel.targetId,
850
+ path: [...current.path, rel.targetId]
851
+ });
852
+ }
853
+ return null;
854
+ }
855
+ async findAncestors(documentId, types, consistencyToken, signal) {
856
+ if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
857
+ if (signal?.aborted) throw new Error("Operation aborted");
858
+ const nodes = new Set([documentId]);
859
+ const edges = [];
860
+ const queue = [documentId];
861
+ const visited = /* @__PURE__ */ new Set();
862
+ while (queue.length > 0) {
863
+ const currentId = queue.shift();
864
+ if (visited.has(currentId)) continue;
865
+ visited.add(currentId);
866
+ const incomingRelationships = await collectAllPages(await this.getIncoming(currentId, types, void 0, consistencyToken, signal), signal);
867
+ for (const rel of incomingRelationships) {
868
+ nodes.add(rel.sourceId);
869
+ edges.push({
870
+ from: rel.sourceId,
871
+ to: rel.targetId,
872
+ type: rel.relationshipType
873
+ });
874
+ if (!visited.has(rel.sourceId)) queue.push(rel.sourceId);
875
+ }
876
+ }
877
+ return {
878
+ nodes: Array.from(nodes),
879
+ edges
880
+ };
881
+ }
882
+ async getRelationshipTypes(consistencyToken, signal) {
883
+ if (consistencyToken) await this.waitForConsistency(consistencyToken, void 0, signal);
884
+ if (signal?.aborted) throw new Error("Operation aborted");
885
+ return (await this._db.selectFrom("DocumentRelationship").select("relationshipType").distinct().execute()).map((row) => row.relationshipType);
886
+ }
887
+ async handleAddRelationship(trx, operation) {
888
+ const input = operation.action.input;
889
+ if (!await trx.selectFrom("Document").select("id").where("id", "=", input.sourceId).executeTakeFirst()) await trx.insertInto("Document").values({ id: input.sourceId }).execute();
890
+ if (!await trx.selectFrom("Document").select("id").where("id", "=", input.targetId).executeTakeFirst()) await trx.insertInto("Document").values({ id: input.targetId }).execute();
891
+ if (!await trx.selectFrom("DocumentRelationship").select("id").where("sourceId", "=", input.sourceId).where("targetId", "=", input.targetId).where("relationshipType", "=", input.relationshipType).executeTakeFirst()) {
892
+ const relationship = {
893
+ id: v4(),
894
+ sourceId: input.sourceId,
895
+ targetId: input.targetId,
896
+ relationshipType: input.relationshipType,
897
+ metadata: input.metadata || null
898
+ };
899
+ await trx.insertInto("DocumentRelationship").values(relationship).execute();
900
+ }
901
+ }
902
+ async handleRemoveRelationship(trx, operation) {
903
+ const input = operation.action.input;
904
+ await trx.deleteFrom("DocumentRelationship").where("sourceId", "=", input.sourceId).where("targetId", "=", input.targetId).where("relationshipType", "=", input.relationshipType).execute();
905
+ }
906
+ async handleUpdateRelationship(trx, operation) {
907
+ const input = operation.action.input;
908
+ await trx.updateTable("DocumentRelationship").set({
909
+ metadata: input.metadata,
910
+ updatedAt: /* @__PURE__ */ new Date()
911
+ }).where("sourceId", "=", input.sourceId).where("targetId", "=", input.targetId).where("relationshipType", "=", input.relationshipType).execute();
912
+ }
913
+ };
914
+ //#endregion
915
+ export { ReadModelCoordinator as a, KyselyDocumentView as i, ConsistencyTracker as n, BaseReadModel as o, makeConsistencyKey as r, KyselyDocumentIndexer as t };
916
+
917
+ //# sourceMappingURL=document-indexer-B2iLRB0o.js.map