@powerhousedao/reactor 6.0.0-dev.84 → 6.0.0-dev.89

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/src/admin/document-integrity-service.d.ts +17 -0
  2. package/dist/src/admin/document-integrity-service.d.ts.map +1 -0
  3. package/dist/src/admin/types.d.ts +30 -0
  4. package/dist/src/admin/types.d.ts.map +1 -0
  5. package/dist/src/cache/collection-membership-cache.d.ts +1 -0
  6. package/dist/src/cache/collection-membership-cache.d.ts.map +1 -1
  7. package/dist/src/cache/document-meta-cache.d.ts +1 -0
  8. package/dist/src/cache/document-meta-cache.d.ts.map +1 -1
  9. package/dist/src/cache/kysely-operation-index.d.ts +5 -1
  10. package/dist/src/cache/kysely-operation-index.d.ts.map +1 -1
  11. package/dist/src/cache/kysely-write-cache.d.ts +1 -0
  12. package/dist/src/cache/kysely-write-cache.d.ts.map +1 -1
  13. package/dist/src/core/reactor-builder.d.ts.map +1 -1
  14. package/dist/src/core/reactor-client-builder.d.ts +3 -0
  15. package/dist/src/core/reactor-client-builder.d.ts.map +1 -1
  16. package/dist/src/executor/document-action-handler.d.ts +3 -10
  17. package/dist/src/executor/document-action-handler.d.ts.map +1 -1
  18. package/dist/src/executor/execution-scope.d.ts +44 -0
  19. package/dist/src/executor/execution-scope.d.ts.map +1 -0
  20. package/dist/src/executor/simple-job-executor.d.ts +3 -1
  21. package/dist/src/executor/simple-job-executor.d.ts.map +1 -1
  22. package/dist/src/index.d.ts +2 -0
  23. package/dist/src/index.d.ts.map +1 -1
  24. package/dist/src/index.js +498 -205
  25. package/dist/src/processors/index.d.ts +0 -1
  26. package/dist/src/processors/index.d.ts.map +1 -1
  27. package/dist/src/storage/interfaces.d.ts +14 -0
  28. package/dist/src/storage/interfaces.d.ts.map +1 -1
  29. package/dist/src/storage/kysely/keyframe-store.d.ts +10 -1
  30. package/dist/src/storage/kysely/keyframe-store.d.ts.map +1 -1
  31. package/dist/src/storage/kysely/store.d.ts +5 -1
  32. package/dist/src/storage/kysely/store.d.ts.map +1 -1
  33. package/package.json +6 -6
  34. package/dist/src/processors/relational/types.d.ts +0 -2
  35. package/dist/src/processors/relational/types.d.ts.map +0 -1
  36. package/dist/src/processors/relational/utils.d.ts +0 -2
  37. package/dist/src/processors/relational/utils.d.ts.map +0 -1
package/dist/src/index.js CHANGED
@@ -11080,6 +11080,11 @@ class CollectionMembershipCache {
11080
11080
  constructor(operationIndex) {
11081
11081
  this.operationIndex = operationIndex;
11082
11082
  }
11083
+ withScopedIndex(operationIndex) {
11084
+ const scoped = new CollectionMembershipCache(operationIndex);
11085
+ scoped.cache = this.cache;
11086
+ return scoped;
11087
+ }
11083
11088
  async getCollectionsForDocuments(documentIds) {
11084
11089
  const result = {};
11085
11090
  const missing = [];
@@ -11435,6 +11440,12 @@ class DocumentMetaCache {
11435
11440
  this.cache = new Map;
11436
11441
  this.lruTracker = new LRUTracker;
11437
11442
  }
11443
+ withScopedStore(operationStore) {
11444
+ const scoped = new DocumentMetaCache(operationStore, this.config);
11445
+ scoped.cache = this.cache;
11446
+ scoped.lruTracker = this.lruTracker;
11447
+ return scoped;
11448
+ }
11438
11449
  async startup() {
11439
11450
  return Promise.resolve();
11440
11451
  }
@@ -11585,9 +11596,18 @@ class KyselyOperationIndexTxn {
11585
11596
 
11586
11597
  class KyselyOperationIndex {
11587
11598
  db;
11599
+ trx;
11588
11600
  constructor(db) {
11589
11601
  this.db = db;
11590
11602
  }
11603
+ get queryExecutor() {
11604
+ return this.trx ?? this.db;
11605
+ }
11606
+ withTransaction(trx) {
11607
+ const instance = new KyselyOperationIndex(this.db);
11608
+ instance.trx = trx;
11609
+ return instance;
11610
+ }
11591
11611
  start() {
11592
11612
  return new KyselyOperationIndexTxn;
11593
11613
  }
@@ -11596,70 +11616,76 @@ class KyselyOperationIndex {
11596
11616
  throw new Error("Operation aborted");
11597
11617
  }
11598
11618
  const kyselyTxn = txn;
11619
+ if (this.trx) {
11620
+ return this.executeCommit(this.trx, kyselyTxn);
11621
+ }
11622
+ let resultOrdinals = [];
11623
+ await this.db.transaction().execute(async (trx) => {
11624
+ resultOrdinals = await this.executeCommit(trx, kyselyTxn);
11625
+ });
11626
+ return resultOrdinals;
11627
+ }
11628
+ async executeCommit(trx, kyselyTxn) {
11599
11629
  const collections = kyselyTxn.getCollections();
11600
11630
  const memberships = kyselyTxn.getCollectionMembershipRecords();
11601
11631
  const removals = kyselyTxn.getCollectionRemovals();
11602
11632
  const operations = kyselyTxn.getOperations();
11603
- let resultOrdinals = [];
11604
- await this.db.transaction().execute(async (trx) => {
11605
- if (collections.length > 0) {
11606
- const collectionRows = collections.map((collectionId) => ({
11607
- documentId: collectionId,
11608
- collectionId,
11609
- joinedOrdinal: BigInt(0),
11633
+ if (collections.length > 0) {
11634
+ const collectionRows = collections.map((collectionId) => ({
11635
+ documentId: collectionId,
11636
+ collectionId,
11637
+ joinedOrdinal: BigInt(0),
11638
+ leftOrdinal: null
11639
+ }));
11640
+ await trx.insertInto("document_collections").values(collectionRows).onConflict((oc) => oc.doNothing()).execute();
11641
+ }
11642
+ let operationOrdinals = [];
11643
+ if (operations.length > 0) {
11644
+ const operationRows = operations.map((op) => ({
11645
+ opId: op.id || "",
11646
+ documentId: op.documentId,
11647
+ documentType: op.documentType,
11648
+ scope: op.scope,
11649
+ branch: op.branch,
11650
+ timestampUtcMs: op.timestampUtcMs,
11651
+ index: op.index,
11652
+ skip: op.skip,
11653
+ hash: op.hash,
11654
+ action: op.action,
11655
+ sourceRemote: op.sourceRemote
11656
+ }));
11657
+ const insertedOps = await trx.insertInto("operation_index_operations").values(operationRows).returning("ordinal").execute();
11658
+ operationOrdinals = insertedOps.map((row) => row.ordinal);
11659
+ }
11660
+ if (memberships.length > 0) {
11661
+ for (const m of memberships) {
11662
+ const ordinal = operationOrdinals[m.operationIndex];
11663
+ await trx.insertInto("document_collections").values({
11664
+ documentId: m.documentId,
11665
+ collectionId: m.collectionId,
11666
+ joinedOrdinal: BigInt(ordinal),
11610
11667
  leftOrdinal: null
11611
- }));
11612
- await trx.insertInto("document_collections").values(collectionRows).onConflict((oc) => oc.doNothing()).execute();
11613
- }
11614
- let operationOrdinals = [];
11615
- if (operations.length > 0) {
11616
- const operationRows = operations.map((op) => ({
11617
- opId: op.id || "",
11618
- documentId: op.documentId,
11619
- documentType: op.documentType,
11620
- scope: op.scope,
11621
- branch: op.branch,
11622
- timestampUtcMs: op.timestampUtcMs,
11623
- index: op.index,
11624
- skip: op.skip,
11625
- hash: op.hash,
11626
- action: op.action,
11627
- sourceRemote: op.sourceRemote
11628
- }));
11629
- const insertedOps = await trx.insertInto("operation_index_operations").values(operationRows).returning("ordinal").execute();
11630
- operationOrdinals = insertedOps.map((row) => row.ordinal);
11631
- resultOrdinals = operationOrdinals;
11632
- }
11633
- if (memberships.length > 0) {
11634
- for (const m of memberships) {
11635
- const ordinal = operationOrdinals[m.operationIndex];
11636
- await trx.insertInto("document_collections").values({
11637
- documentId: m.documentId,
11638
- collectionId: m.collectionId,
11639
- joinedOrdinal: BigInt(ordinal),
11640
- leftOrdinal: null
11641
- }).onConflict((oc) => oc.columns(["documentId", "collectionId"]).doUpdateSet({
11642
- joinedOrdinal: BigInt(ordinal),
11643
- leftOrdinal: null
11644
- })).execute();
11645
- }
11668
+ }).onConflict((oc) => oc.columns(["documentId", "collectionId"]).doUpdateSet({
11669
+ joinedOrdinal: BigInt(ordinal),
11670
+ leftOrdinal: null
11671
+ })).execute();
11646
11672
  }
11647
- if (removals.length > 0) {
11648
- for (const r of removals) {
11649
- const ordinal = operationOrdinals[r.operationIndex];
11650
- await trx.updateTable("document_collections").set({
11651
- leftOrdinal: BigInt(ordinal)
11652
- }).where("collectionId", "=", r.collectionId).where("documentId", "=", r.documentId).where("leftOrdinal", "is", null).execute();
11653
- }
11673
+ }
11674
+ if (removals.length > 0) {
11675
+ for (const r of removals) {
11676
+ const ordinal = operationOrdinals[r.operationIndex];
11677
+ await trx.updateTable("document_collections").set({
11678
+ leftOrdinal: BigInt(ordinal)
11679
+ }).where("collectionId", "=", r.collectionId).where("documentId", "=", r.documentId).where("leftOrdinal", "is", null).execute();
11654
11680
  }
11655
- });
11656
- return resultOrdinals;
11681
+ }
11682
+ return operationOrdinals;
11657
11683
  }
11658
11684
  async find(collectionId, cursor, view, paging, signal) {
11659
11685
  if (signal?.aborted) {
11660
11686
  throw new Error("Operation aborted");
11661
11687
  }
11662
- let query = this.db.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")`).orderBy("oi.ordinal", "asc");
11688
+ let query = 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")`).orderBy("oi.ordinal", "asc");
11663
11689
  if (cursor !== undefined) {
11664
11690
  query = query.where("oi.ordinal", ">", cursor);
11665
11691
  }
@@ -11701,7 +11727,7 @@ class KyselyOperationIndex {
11701
11727
  if (signal?.aborted) {
11702
11728
  throw new Error("Operation aborted");
11703
11729
  }
11704
- let query = this.db.selectFrom("operation_index_operations").selectAll().where("documentId", "=", documentId).orderBy("ordinal", "asc");
11730
+ let query = this.queryExecutor.selectFrom("operation_index_operations").selectAll().where("documentId", "=", documentId).orderBy("ordinal", "asc");
11705
11731
  if (view?.branch) {
11706
11732
  query = query.where("branch", "=", view.branch);
11707
11733
  }
@@ -11737,7 +11763,7 @@ class KyselyOperationIndex {
11737
11763
  if (signal?.aborted) {
11738
11764
  throw new Error("Operation aborted");
11739
11765
  }
11740
- let query = this.db.selectFrom("operation_index_operations").selectAll().where("ordinal", ">", ordinal).orderBy("ordinal", "asc");
11766
+ let query = this.queryExecutor.selectFrom("operation_index_operations").selectAll().where("ordinal", ">", ordinal).orderBy("ordinal", "asc");
11741
11767
  if (paging?.cursor) {
11742
11768
  const cursorOrdinal = Number.parseInt(paging.cursor, 10);
11743
11769
  query = query.where("ordinal", ">", cursorOrdinal);
@@ -11802,14 +11828,14 @@ class KyselyOperationIndex {
11802
11828
  if (signal?.aborted) {
11803
11829
  throw new Error("Operation aborted");
11804
11830
  }
11805
- const result = await this.db.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();
11831
+ const result = 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();
11806
11832
  return result?.timestampUtcMs ?? null;
11807
11833
  }
11808
11834
  async getCollectionsForDocuments(documentIds) {
11809
11835
  if (documentIds.length === 0) {
11810
11836
  return {};
11811
11837
  }
11812
- const rows = await this.db.selectFrom("document_collections").select(["documentId", "collectionId"]).where("documentId", "in", documentIds).where("leftOrdinal", "is", null).execute();
11838
+ const rows = await this.queryExecutor.selectFrom("document_collections").select(["documentId", "collectionId"]).where("documentId", "in", documentIds).where("leftOrdinal", "is", null).execute();
11813
11839
  const result = {};
11814
11840
  for (const row of rows) {
11815
11841
  if (!(row.documentId in result)) {
@@ -11866,6 +11892,11 @@ class RingBuffer {
11866
11892
  }
11867
11893
 
11868
11894
  // src/cache/kysely-write-cache.ts
11895
+ function extractModuleVersion(doc) {
11896
+ const v = doc.state.document.version;
11897
+ return v === 0 ? undefined : v;
11898
+ }
11899
+
11869
11900
  class KyselyWriteCache {
11870
11901
  streams;
11871
11902
  lruTracker;
@@ -11885,6 +11916,12 @@ class KyselyWriteCache {
11885
11916
  this.streams = new Map;
11886
11917
  this.lruTracker = new LRUTracker;
11887
11918
  }
11919
+ withScopedStores(operationStore, keyframeStore) {
11920
+ const scoped = new KyselyWriteCache(keyframeStore, operationStore, this.registry, this.config);
11921
+ scoped.streams = this.streams;
11922
+ scoped.lruTracker = this.lruTracker;
11923
+ return scoped;
11924
+ }
11888
11925
  async startup() {
11889
11926
  return Promise.resolve();
11890
11927
  }
@@ -12023,7 +12060,7 @@ class KyselyWriteCache {
12023
12060
  throw new Error(`Failed to rebuild document ${documentId}: CREATE_DOCUMENT action missing model in input`);
12024
12061
  }
12025
12062
  document = createDocumentFromAction(documentCreateAction);
12026
- const docModule = this.registry.getModule(documentType);
12063
+ let docModule = this.registry.getModule(documentType, extractModuleVersion(document));
12027
12064
  const docScopeOps = await this.operationStore.getSince(documentId, "document", branch, 0, undefined, undefined, signal);
12028
12065
  for (const operation of docScopeOps.results) {
12029
12066
  if (operation.index === 0) {
@@ -12032,6 +12069,7 @@ class KyselyWriteCache {
12032
12069
  if (operation.action.type === "UPGRADE_DOCUMENT") {
12033
12070
  const upgradeAction = operation.action;
12034
12071
  document = applyUpgradeDocumentAction(document, upgradeAction);
12072
+ docModule = this.registry.getModule(documentType, extractModuleVersion(document));
12035
12073
  } else if (operation.action.type === "DELETE_DOCUMENT") {
12036
12074
  applyDeleteDocumentAction(document, operation.action);
12037
12075
  } else {
@@ -12043,7 +12081,7 @@ class KyselyWriteCache {
12043
12081
  }
12044
12082
  }
12045
12083
  }
12046
- const module = this.registry.getModule(documentType);
12084
+ const module = this.registry.getModule(documentType, extractModuleVersion(document));
12047
12085
  let cursor = undefined;
12048
12086
  const pageSize = 100;
12049
12087
  let hasMorePages;
@@ -12217,6 +12255,64 @@ class EventBus {
12217
12255
  }
12218
12256
  }
12219
12257
 
12258
+ // src/executor/execution-scope.ts
12259
+ class DefaultExecutionScope {
12260
+ operationStore;
12261
+ operationIndex;
12262
+ writeCache;
12263
+ documentMetaCache;
12264
+ collectionMembershipCache;
12265
+ constructor(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache) {
12266
+ this.operationStore = operationStore;
12267
+ this.operationIndex = operationIndex;
12268
+ this.writeCache = writeCache;
12269
+ this.documentMetaCache = documentMetaCache;
12270
+ this.collectionMembershipCache = collectionMembershipCache;
12271
+ }
12272
+ async run(fn) {
12273
+ return fn({
12274
+ operationStore: this.operationStore,
12275
+ operationIndex: this.operationIndex,
12276
+ writeCache: this.writeCache,
12277
+ documentMetaCache: this.documentMetaCache,
12278
+ collectionMembershipCache: this.collectionMembershipCache
12279
+ });
12280
+ }
12281
+ }
12282
+
12283
+ class KyselyExecutionScope {
12284
+ db;
12285
+ operationStore;
12286
+ operationIndex;
12287
+ keyframeStore;
12288
+ writeCache;
12289
+ documentMetaCache;
12290
+ collectionMembershipCache;
12291
+ constructor(db, operationStore, operationIndex, keyframeStore, writeCache, documentMetaCache, collectionMembershipCache) {
12292
+ this.db = db;
12293
+ this.operationStore = operationStore;
12294
+ this.operationIndex = operationIndex;
12295
+ this.keyframeStore = keyframeStore;
12296
+ this.writeCache = writeCache;
12297
+ this.documentMetaCache = documentMetaCache;
12298
+ this.collectionMembershipCache = collectionMembershipCache;
12299
+ }
12300
+ async run(fn) {
12301
+ return this.db.transaction().execute(async (trx) => {
12302
+ const scopedOperationStore = this.operationStore.withTransaction(trx);
12303
+ const scopedOperationIndex = this.operationIndex.withTransaction(trx);
12304
+ const scopedKeyframeStore = this.keyframeStore.withTransaction(trx);
12305
+ return fn({
12306
+ operationStore: scopedOperationStore,
12307
+ operationIndex: scopedOperationIndex,
12308
+ writeCache: this.writeCache.withScopedStores(scopedOperationStore, scopedKeyframeStore),
12309
+ documentMetaCache: this.documentMetaCache.withScopedStore(scopedOperationStore),
12310
+ collectionMembershipCache: this.collectionMembershipCache.withScopedIndex(scopedOperationIndex)
12311
+ });
12312
+ });
12313
+ }
12314
+ }
12315
+
12220
12316
  // src/queue/types.ts
12221
12317
  var QueueEventTypes = {
12222
12318
  JOB_AVAILABLE: 1e4
@@ -12766,37 +12862,29 @@ function driveCollectionId(branch, driveId) {
12766
12862
 
12767
12863
  // src/executor/document-action-handler.ts
12768
12864
  class DocumentActionHandler {
12769
- writeCache;
12770
- operationStore;
12771
- documentMetaCache;
12772
- collectionMembershipCache;
12773
12865
  registry;
12774
12866
  logger;
12775
- constructor(writeCache, operationStore, documentMetaCache, collectionMembershipCache, registry, logger) {
12776
- this.writeCache = writeCache;
12777
- this.operationStore = operationStore;
12778
- this.documentMetaCache = documentMetaCache;
12779
- this.collectionMembershipCache = collectionMembershipCache;
12867
+ constructor(registry, logger) {
12780
12868
  this.registry = registry;
12781
12869
  this.logger = logger;
12782
12870
  }
12783
- async execute(job, action, startTime, indexTxn, skip = 0, sourceRemote = "") {
12871
+ async execute(job, action, startTime, indexTxn, stores, skip = 0, sourceRemote = "") {
12784
12872
  switch (action.type) {
12785
12873
  case "CREATE_DOCUMENT":
12786
- return this.executeCreate(job, action, startTime, indexTxn, skip, sourceRemote);
12874
+ return this.executeCreate(job, action, startTime, indexTxn, stores, skip, sourceRemote);
12787
12875
  case "DELETE_DOCUMENT":
12788
- return this.executeDelete(job, action, startTime, indexTxn, sourceRemote);
12876
+ return this.executeDelete(job, action, startTime, indexTxn, stores, sourceRemote);
12789
12877
  case "UPGRADE_DOCUMENT":
12790
- return this.executeUpgrade(job, action, startTime, indexTxn, skip, sourceRemote);
12878
+ return this.executeUpgrade(job, action, startTime, indexTxn, stores, skip, sourceRemote);
12791
12879
  case "ADD_RELATIONSHIP":
12792
- return this.executeAddRelationship(job, action, startTime, indexTxn, sourceRemote);
12880
+ return this.executeAddRelationship(job, action, startTime, indexTxn, stores, sourceRemote);
12793
12881
  case "REMOVE_RELATIONSHIP":
12794
- return this.executeRemoveRelationship(job, action, startTime, indexTxn, sourceRemote);
12882
+ return this.executeRemoveRelationship(job, action, startTime, indexTxn, stores, sourceRemote);
12795
12883
  default:
12796
12884
  return buildErrorResult(job, new Error(`Unknown document action type: ${action.type}`), startTime);
12797
12885
  }
12798
12886
  }
12799
- async executeCreate(job, action, startTime, indexTxn, skip = 0, sourceRemote = "") {
12887
+ async executeCreate(job, action, startTime, indexTxn, stores, skip = 0, sourceRemote = "") {
12800
12888
  if (job.scope !== "document") {
12801
12889
  return {
12802
12890
  job,
@@ -12816,12 +12904,12 @@ class DocumentActionHandler {
12816
12904
  ...document.state
12817
12905
  };
12818
12906
  const resultingState = JSON.stringify(resultingStateObj);
12819
- const writeError = await this.writeOperationToStore(document.header.id, document.header.documentType, job.scope, job.branch, operation, job, startTime);
12907
+ const writeError = await this.writeOperationToStore(document.header.id, document.header.documentType, job.scope, job.branch, operation, job, startTime, stores);
12820
12908
  if (writeError !== null) {
12821
12909
  return writeError;
12822
12910
  }
12823
12911
  updateDocumentRevision(document, job.scope, operation.index);
12824
- this.writeCache.putState(document.header.id, job.scope, job.branch, operation.index, document);
12912
+ stores.writeCache.putState(document.header.id, job.scope, job.branch, operation.index, document);
12825
12913
  indexTxn.write([
12826
12914
  {
12827
12915
  ...operation,
@@ -12837,14 +12925,14 @@ class DocumentActionHandler {
12837
12925
  indexTxn.createCollection(collectionId);
12838
12926
  indexTxn.addToCollection(collectionId, document.header.id);
12839
12927
  }
12840
- this.documentMetaCache.putDocumentMeta(document.header.id, job.branch, {
12928
+ stores.documentMetaCache.putDocumentMeta(document.header.id, job.branch, {
12841
12929
  state: document.state.document,
12842
12930
  documentType: document.header.documentType,
12843
12931
  documentScopeRevision: 1
12844
12932
  });
12845
12933
  return buildSuccessResult(job, operation, document.header.id, document.header.documentType, resultingState, startTime);
12846
12934
  }
12847
- async executeDelete(job, action, startTime, indexTxn, sourceRemote = "") {
12935
+ async executeDelete(job, action, startTime, indexTxn, stores, sourceRemote = "") {
12848
12936
  const input = action.input;
12849
12937
  if (!input.documentId) {
12850
12938
  return buildErrorResult(job, new Error("DELETE_DOCUMENT action requires a documentId in input"), startTime);
@@ -12852,7 +12940,7 @@ class DocumentActionHandler {
12852
12940
  const documentId = input.documentId;
12853
12941
  let document;
12854
12942
  try {
12855
- document = await this.writeCache.getState(documentId, job.scope, job.branch);
12943
+ document = await stores.writeCache.getState(documentId, job.scope, job.branch);
12856
12944
  } catch (error) {
12857
12945
  return buildErrorResult(job, new Error(`Failed to fetch document before deletion: ${error instanceof Error ? error.message : String(error)}`), startTime);
12858
12946
  }
@@ -12872,12 +12960,12 @@ class DocumentActionHandler {
12872
12960
  document: document.state.document
12873
12961
  };
12874
12962
  const resultingState = JSON.stringify(resultingStateObj);
12875
- const writeError = await this.writeOperationToStore(documentId, document.header.documentType, job.scope, job.branch, operation, job, startTime);
12963
+ const writeError = await this.writeOperationToStore(documentId, document.header.documentType, job.scope, job.branch, operation, job, startTime, stores);
12876
12964
  if (writeError !== null) {
12877
12965
  return writeError;
12878
12966
  }
12879
12967
  updateDocumentRevision(document, job.scope, operation.index);
12880
- this.writeCache.putState(documentId, job.scope, job.branch, operation.index, document);
12968
+ stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document);
12881
12969
  indexTxn.write([
12882
12970
  {
12883
12971
  ...operation,
@@ -12888,14 +12976,14 @@ class DocumentActionHandler {
12888
12976
  sourceRemote
12889
12977
  }
12890
12978
  ]);
12891
- this.documentMetaCache.putDocumentMeta(documentId, job.branch, {
12979
+ stores.documentMetaCache.putDocumentMeta(documentId, job.branch, {
12892
12980
  state: document.state.document,
12893
12981
  documentType: document.header.documentType,
12894
12982
  documentScopeRevision: operation.index + 1
12895
12983
  });
12896
12984
  return buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
12897
12985
  }
12898
- async executeUpgrade(job, action, startTime, indexTxn, skip = 0, sourceRemote = "") {
12986
+ async executeUpgrade(job, action, startTime, indexTxn, stores, skip = 0, sourceRemote = "") {
12899
12987
  const input = action.input;
12900
12988
  if (!input.documentId) {
12901
12989
  return buildErrorResult(job, new Error("UPGRADE_DOCUMENT action requires a documentId in input"), startTime);
@@ -12905,7 +12993,7 @@ class DocumentActionHandler {
12905
12993
  const toVersion = input.toVersion;
12906
12994
  let document;
12907
12995
  try {
12908
- document = await this.writeCache.getState(documentId, job.scope, job.branch);
12996
+ document = await stores.writeCache.getState(documentId, job.scope, job.branch);
12909
12997
  } catch (error) {
12910
12998
  return buildErrorResult(job, new Error(`Failed to fetch document for upgrade: ${error instanceof Error ? error.message : String(error)}`), startTime);
12911
12999
  }
@@ -12946,12 +13034,12 @@ class DocumentActionHandler {
12946
13034
  ...document.state
12947
13035
  };
12948
13036
  const resultingState = JSON.stringify(resultingStateObj);
12949
- const writeError = await this.writeOperationToStore(documentId, document.header.documentType, job.scope, job.branch, operation, job, startTime);
13037
+ const writeError = await this.writeOperationToStore(documentId, document.header.documentType, job.scope, job.branch, operation, job, startTime, stores);
12950
13038
  if (writeError !== null) {
12951
13039
  return writeError;
12952
13040
  }
12953
13041
  updateDocumentRevision(document, job.scope, operation.index);
12954
- this.writeCache.putState(documentId, job.scope, job.branch, operation.index, document);
13042
+ stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document);
12955
13043
  indexTxn.write([
12956
13044
  {
12957
13045
  ...operation,
@@ -12962,14 +13050,14 @@ class DocumentActionHandler {
12962
13050
  sourceRemote
12963
13051
  }
12964
13052
  ]);
12965
- this.documentMetaCache.putDocumentMeta(documentId, job.branch, {
13053
+ stores.documentMetaCache.putDocumentMeta(documentId, job.branch, {
12966
13054
  state: document.state.document,
12967
13055
  documentType: document.header.documentType,
12968
13056
  documentScopeRevision: operation.index + 1
12969
13057
  });
12970
13058
  return buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
12971
13059
  }
12972
- async executeAddRelationship(job, action, startTime, indexTxn, sourceRemote = "") {
13060
+ async executeAddRelationship(job, action, startTime, indexTxn, stores, sourceRemote = "") {
12973
13061
  if (job.scope !== "document") {
12974
13062
  return buildErrorResult(job, new Error(`ADD_RELATIONSHIP must be in "document" scope, got "${job.scope}"`), startTime);
12975
13063
  }
@@ -12982,7 +13070,7 @@ class DocumentActionHandler {
12982
13070
  }
12983
13071
  let sourceDoc;
12984
13072
  try {
12985
- sourceDoc = await this.writeCache.getState(input.sourceId, "document", job.branch);
13073
+ sourceDoc = await stores.writeCache.getState(input.sourceId, "document", job.branch);
12986
13074
  } catch (error) {
12987
13075
  return buildErrorResult(job, new Error(`ADD_RELATIONSHIP: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
12988
13076
  }
@@ -12992,7 +13080,7 @@ class DocumentActionHandler {
12992
13080
  scope: job.scope,
12993
13081
  branch: job.branch
12994
13082
  });
12995
- const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime);
13083
+ const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime, stores);
12996
13084
  if (writeError !== null) {
12997
13085
  return writeError;
12998
13086
  }
@@ -13008,7 +13096,7 @@ class DocumentActionHandler {
13008
13096
  [job.scope]: scopeState === undefined ? {} : structuredClone(scopeState)
13009
13097
  };
13010
13098
  const resultingState = JSON.stringify(resultingStateObj);
13011
- this.writeCache.putState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc);
13099
+ stores.writeCache.putState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc);
13012
13100
  indexTxn.write([
13013
13101
  {
13014
13102
  ...operation,
@@ -13022,16 +13110,16 @@ class DocumentActionHandler {
13022
13110
  if (sourceDoc.header.documentType === "powerhouse/document-drive") {
13023
13111
  const collectionId = driveCollectionId(job.branch, input.sourceId);
13024
13112
  indexTxn.addToCollection(collectionId, input.targetId);
13025
- this.collectionMembershipCache.invalidate(input.targetId);
13113
+ stores.collectionMembershipCache.invalidate(input.targetId);
13026
13114
  }
13027
- this.documentMetaCache.putDocumentMeta(input.sourceId, job.branch, {
13115
+ stores.documentMetaCache.putDocumentMeta(input.sourceId, job.branch, {
13028
13116
  state: sourceDoc.state.document,
13029
13117
  documentType: sourceDoc.header.documentType,
13030
13118
  documentScopeRevision: operation.index + 1
13031
13119
  });
13032
13120
  return buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
13033
13121
  }
13034
- async executeRemoveRelationship(job, action, startTime, indexTxn, sourceRemote = "") {
13122
+ async executeRemoveRelationship(job, action, startTime, indexTxn, stores, sourceRemote = "") {
13035
13123
  if (job.scope !== "document") {
13036
13124
  return buildErrorResult(job, new Error(`REMOVE_RELATIONSHIP must be in "document" scope, got "${job.scope}"`), startTime);
13037
13125
  }
@@ -13041,7 +13129,7 @@ class DocumentActionHandler {
13041
13129
  }
13042
13130
  let sourceDoc;
13043
13131
  try {
13044
- sourceDoc = await this.writeCache.getState(input.sourceId, "document", job.branch);
13132
+ sourceDoc = await stores.writeCache.getState(input.sourceId, "document", job.branch);
13045
13133
  } catch (error) {
13046
13134
  return buildErrorResult(job, new Error(`REMOVE_RELATIONSHIP: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
13047
13135
  }
@@ -13051,7 +13139,7 @@ class DocumentActionHandler {
13051
13139
  scope: job.scope,
13052
13140
  branch: job.branch
13053
13141
  });
13054
- const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime);
13142
+ const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime, stores);
13055
13143
  if (writeError !== null) {
13056
13144
  return writeError;
13057
13145
  }
@@ -13067,7 +13155,7 @@ class DocumentActionHandler {
13067
13155
  [job.scope]: scopeState === undefined ? {} : structuredClone(scopeState)
13068
13156
  };
13069
13157
  const resultingState = JSON.stringify(resultingStateObj);
13070
- this.writeCache.putState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc);
13158
+ stores.writeCache.putState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc);
13071
13159
  indexTxn.write([
13072
13160
  {
13073
13161
  ...operation,
@@ -13081,24 +13169,24 @@ class DocumentActionHandler {
13081
13169
  if (sourceDoc.header.documentType === "powerhouse/document-drive") {
13082
13170
  const collectionId = driveCollectionId(job.branch, input.sourceId);
13083
13171
  indexTxn.removeFromCollection(collectionId, input.targetId);
13084
- this.collectionMembershipCache.invalidate(input.targetId);
13172
+ stores.collectionMembershipCache.invalidate(input.targetId);
13085
13173
  }
13086
- this.documentMetaCache.putDocumentMeta(input.sourceId, job.branch, {
13174
+ stores.documentMetaCache.putDocumentMeta(input.sourceId, job.branch, {
13087
13175
  state: sourceDoc.state.document,
13088
13176
  documentType: sourceDoc.header.documentType,
13089
13177
  documentScopeRevision: operation.index + 1
13090
13178
  });
13091
13179
  return buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
13092
13180
  }
13093
- async writeOperationToStore(documentId, documentType, scope, branch, operation, job, startTime) {
13181
+ async writeOperationToStore(documentId, documentType, scope, branch, operation, job, startTime, stores) {
13094
13182
  try {
13095
- await this.operationStore.apply(documentId, documentType, scope, branch, operation.index, (txn) => {
13183
+ await stores.operationStore.apply(documentId, documentType, scope, branch, operation.index, (txn) => {
13096
13184
  txn.addOperations(operation);
13097
13185
  });
13098
13186
  return null;
13099
13187
  } catch (error) {
13100
13188
  this.logger.error("Error writing @Operation to IOperationStore: @Error", operation, error);
13101
- this.writeCache.invalidate(documentId, scope, branch);
13189
+ stores.writeCache.invalidate(documentId, scope, branch);
13102
13190
  return {
13103
13191
  job,
13104
13192
  success: false,
@@ -13199,7 +13287,8 @@ class SimpleJobExecutor {
13199
13287
  config;
13200
13288
  signatureVerifierModule;
13201
13289
  documentActionHandler;
13202
- constructor(logger, registry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, config, signatureVerifier) {
13290
+ executionScope;
13291
+ constructor(logger, registry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, config, signatureVerifier, executionScope) {
13203
13292
  this.logger = logger;
13204
13293
  this.registry = registry;
13205
13294
  this.operationStore = operationStore;
@@ -13216,71 +13305,101 @@ class SimpleJobExecutor {
13216
13305
  retryMaxDelayMs: config.retryMaxDelayMs ?? 5000
13217
13306
  };
13218
13307
  this.signatureVerifierModule = new SignatureVerifier(signatureVerifier);
13219
- this.documentActionHandler = new DocumentActionHandler(writeCache, operationStore, documentMetaCache, collectionMembershipCache, registry, logger);
13308
+ this.documentActionHandler = new DocumentActionHandler(registry, logger);
13309
+ this.executionScope = executionScope ?? new DefaultExecutionScope(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache);
13220
13310
  }
13221
13311
  async executeJob(job) {
13222
13312
  const startTime = Date.now();
13223
- const indexTxn = this.operationIndex.start();
13224
- if (job.kind === "load") {
13225
- const result2 = await this.executeLoadJob(job, startTime, indexTxn);
13226
- if (result2.success && result2.operationsWithContext) {
13227
- const ordinals2 = await this.operationIndex.commit(indexTxn);
13228
- for (let i = 0;i < result2.operationsWithContext.length; i++) {
13229
- result2.operationsWithContext[i].context.ordinal = ordinals2[i];
13313
+ const touchedCacheEntries = [];
13314
+ let pendingEvent;
13315
+ let result;
13316
+ try {
13317
+ result = await this.executionScope.run(async (stores) => {
13318
+ const indexTxn = stores.operationIndex.start();
13319
+ if (job.kind === "load") {
13320
+ const loadResult = await this.executeLoadJob(job, startTime, indexTxn, stores);
13321
+ if (loadResult.success && loadResult.operationsWithContext) {
13322
+ for (const owc of loadResult.operationsWithContext) {
13323
+ touchedCacheEntries.push({
13324
+ documentId: owc.context.documentId,
13325
+ scope: owc.context.scope,
13326
+ branch: owc.context.branch
13327
+ });
13328
+ }
13329
+ const ordinals2 = await stores.operationIndex.commit(indexTxn);
13330
+ for (let i = 0;i < loadResult.operationsWithContext.length; i++) {
13331
+ loadResult.operationsWithContext[i].context.ordinal = ordinals2[i];
13332
+ }
13333
+ const collectionMemberships = loadResult.operationsWithContext.length > 0 ? await this.getCollectionMembershipsForOperations(loadResult.operationsWithContext, stores) : {};
13334
+ pendingEvent = {
13335
+ jobId: job.id,
13336
+ operations: loadResult.operationsWithContext,
13337
+ jobMeta: job.meta,
13338
+ collectionMemberships
13339
+ };
13340
+ }
13341
+ return loadResult;
13230
13342
  }
13231
- const collectionMemberships = result2.operationsWithContext.length > 0 ? await this.getCollectionMembershipsForOperations(result2.operationsWithContext) : {};
13232
- const event = {
13233
- jobId: job.id,
13234
- operations: result2.operationsWithContext,
13235
- jobMeta: job.meta,
13236
- collectionMemberships
13343
+ const actionResult = await this.processActions(job, job.actions, startTime, indexTxn, stores);
13344
+ if (!actionResult.success) {
13345
+ return {
13346
+ job,
13347
+ success: false,
13348
+ error: actionResult.error,
13349
+ duration: Date.now() - startTime
13350
+ };
13351
+ }
13352
+ if (actionResult.operationsWithContext.length > 0) {
13353
+ for (const owc of actionResult.operationsWithContext) {
13354
+ touchedCacheEntries.push({
13355
+ documentId: owc.context.documentId,
13356
+ scope: owc.context.scope,
13357
+ branch: owc.context.branch
13358
+ });
13359
+ }
13360
+ }
13361
+ const ordinals = await stores.operationIndex.commit(indexTxn);
13362
+ if (actionResult.operationsWithContext.length > 0) {
13363
+ for (let i = 0;i < actionResult.operationsWithContext.length; i++) {
13364
+ actionResult.operationsWithContext[i].context.ordinal = ordinals[i];
13365
+ }
13366
+ const collectionMemberships = await this.getCollectionMembershipsForOperations(actionResult.operationsWithContext, stores);
13367
+ pendingEvent = {
13368
+ jobId: job.id,
13369
+ operations: actionResult.operationsWithContext,
13370
+ jobMeta: job.meta,
13371
+ collectionMemberships
13372
+ };
13373
+ }
13374
+ return {
13375
+ job,
13376
+ success: true,
13377
+ operations: actionResult.generatedOperations,
13378
+ operationsWithContext: actionResult.operationsWithContext,
13379
+ duration: Date.now() - startTime
13237
13380
  };
13238
- this.eventBus.emit(ReactorEventTypes.JOB_WRITE_READY, event).catch((error) => {
13239
- this.logger.error("Failed to emit JOB_WRITE_READY event: @Event : @Error", event, error);
13240
- });
13381
+ });
13382
+ } catch (error) {
13383
+ for (const entry of touchedCacheEntries) {
13384
+ this.writeCache.invalidate(entry.documentId, entry.scope, entry.branch);
13385
+ this.documentMetaCache.invalidate(entry.documentId, entry.branch);
13241
13386
  }
13242
- return result2;
13243
- }
13244
- const result = await this.processActions(job, job.actions, startTime, indexTxn);
13245
- if (!result.success) {
13246
- return {
13247
- job,
13248
- success: false,
13249
- error: result.error,
13250
- duration: Date.now() - startTime
13251
- };
13387
+ throw error;
13252
13388
  }
13253
- const ordinals = await this.operationIndex.commit(indexTxn);
13254
- if (result.operationsWithContext.length > 0) {
13255
- for (let i = 0;i < result.operationsWithContext.length; i++) {
13256
- result.operationsWithContext[i].context.ordinal = ordinals[i];
13257
- }
13258
- const collectionMemberships = await this.getCollectionMembershipsForOperations(result.operationsWithContext);
13259
- const event = {
13260
- jobId: job.id,
13261
- operations: result.operationsWithContext,
13262
- jobMeta: job.meta,
13263
- collectionMemberships
13264
- };
13265
- this.eventBus.emit(ReactorEventTypes.JOB_WRITE_READY, event).catch((error) => {
13266
- this.logger.error("Failed to emit JOB_WRITE_READY event: @Event : @Error", event, error);
13389
+ if (pendingEvent) {
13390
+ this.eventBus.emit(ReactorEventTypes.JOB_WRITE_READY, pendingEvent).catch((error) => {
13391
+ this.logger.error("Failed to emit JOB_WRITE_READY event: @Event : @Error", pendingEvent, error);
13267
13392
  });
13268
13393
  }
13269
- return {
13270
- job,
13271
- success: true,
13272
- operations: result.generatedOperations,
13273
- operationsWithContext: result.operationsWithContext,
13274
- duration: Date.now() - startTime
13275
- };
13394
+ return result;
13276
13395
  }
13277
- async getCollectionMembershipsForOperations(operations) {
13396
+ async getCollectionMembershipsForOperations(operations, stores) {
13278
13397
  const documentIds = [
13279
13398
  ...new Set(operations.map((op) => op.context.documentId))
13280
13399
  ];
13281
- return this.collectionMembershipCache.getCollectionsForDocuments(documentIds);
13400
+ return stores.collectionMembershipCache.getCollectionsForDocuments(documentIds);
13282
13401
  }
13283
- async processActions(job, actions2, startTime, indexTxn, skipValues, sourceOperations, sourceRemote = "") {
13402
+ async processActions(job, actions2, startTime, indexTxn, stores, skipValues, sourceOperations, sourceRemote = "") {
13284
13403
  const generatedOperations = [];
13285
13404
  const operationsWithContext = [];
13286
13405
  try {
@@ -13298,7 +13417,7 @@ class SimpleJobExecutor {
13298
13417
  const skip = skipValues?.[actionIndex] ?? 0;
13299
13418
  const sourceOperation = sourceOperations?.[actionIndex];
13300
13419
  const isDocumentAction = documentScopeActions.includes(action.type);
13301
- const result = isDocumentAction ? await this.documentActionHandler.execute(job, action, startTime, indexTxn, skip, sourceRemote) : await this.executeRegularAction(job, action, startTime, indexTxn, skip, sourceOperation, sourceRemote);
13420
+ const result = isDocumentAction ? await this.documentActionHandler.execute(job, action, startTime, indexTxn, stores, skip, sourceRemote) : await this.executeRegularAction(job, action, startTime, indexTxn, stores, skip, sourceOperation, sourceRemote);
13302
13421
  const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
13303
13422
  if (error !== null) {
13304
13423
  return {
@@ -13315,10 +13434,10 @@ class SimpleJobExecutor {
13315
13434
  operationsWithContext
13316
13435
  };
13317
13436
  }
13318
- async executeRegularAction(job, action, startTime, indexTxn, skip = 0, sourceOperation, sourceRemote = "") {
13437
+ async executeRegularAction(job, action, startTime, indexTxn, stores, skip = 0, sourceOperation, sourceRemote = "") {
13319
13438
  let docMeta;
13320
13439
  try {
13321
- docMeta = await this.documentMetaCache.getDocumentMeta(job.documentId, job.branch);
13440
+ docMeta = await stores.documentMetaCache.getDocumentMeta(job.documentId, job.branch);
13322
13441
  } catch (error) {
13323
13442
  return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
13324
13443
  }
@@ -13326,11 +13445,11 @@ class SimpleJobExecutor {
13326
13445
  return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
13327
13446
  }
13328
13447
  if (isUndoRedo(action) || action.type === "PRUNE" || action.type === "NOOP" && skip > 0) {
13329
- this.writeCache.invalidate(job.documentId, job.scope, job.branch);
13448
+ stores.writeCache.invalidate(job.documentId, job.scope, job.branch);
13330
13449
  }
13331
13450
  let document;
13332
13451
  try {
13333
- document = await this.writeCache.getState(job.documentId, job.scope, job.branch);
13452
+ document = await stores.writeCache.getState(job.documentId, job.scope, job.branch);
13334
13453
  } catch (error) {
13335
13454
  return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
13336
13455
  }
@@ -13381,12 +13500,12 @@ ${error.stack}`;
13381
13500
  header: updatedDocument.header
13382
13501
  });
13383
13502
  try {
13384
- await this.operationStore.apply(job.documentId, document.header.documentType, scope, job.branch, newOperation.index, (txn) => {
13503
+ await stores.operationStore.apply(job.documentId, document.header.documentType, scope, job.branch, newOperation.index, (txn) => {
13385
13504
  txn.addOperations(newOperation);
13386
13505
  });
13387
13506
  } catch (error) {
13388
13507
  this.logger.error("Error writing @Operation to IOperationStore: @Error", newOperation, error);
13389
- this.writeCache.invalidate(job.documentId, scope, job.branch);
13508
+ stores.writeCache.invalidate(job.documentId, scope, job.branch);
13390
13509
  return {
13391
13510
  job,
13392
13511
  success: false,
@@ -13398,7 +13517,7 @@ ${error.stack}`;
13398
13517
  ...updatedDocument.header.revision,
13399
13518
  [scope]: newOperation.index + 1
13400
13519
  };
13401
- this.writeCache.putState(job.documentId, scope, job.branch, newOperation.index, updatedDocument);
13520
+ stores.writeCache.putState(job.documentId, scope, job.branch, newOperation.index, updatedDocument);
13402
13521
  indexTxn.write([
13403
13522
  {
13404
13523
  ...newOperation,
@@ -13429,13 +13548,13 @@ ${error.stack}`;
13429
13548
  duration: Date.now() - startTime
13430
13549
  };
13431
13550
  }
13432
- async executeLoadJob(job, startTime, indexTxn) {
13551
+ async executeLoadJob(job, startTime, indexTxn, stores) {
13433
13552
  if (job.operations.length === 0) {
13434
13553
  return buildErrorResult(job, new Error("Load job must include at least one operation"), startTime);
13435
13554
  }
13436
13555
  let docMeta;
13437
13556
  try {
13438
- docMeta = await this.documentMetaCache.getDocumentMeta(job.documentId, job.branch);
13557
+ docMeta = await stores.documentMetaCache.getDocumentMeta(job.documentId, job.branch);
13439
13558
  } catch {}
13440
13559
  if (docMeta?.state.isDeleted) {
13441
13560
  return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
@@ -13443,7 +13562,7 @@ ${error.stack}`;
13443
13562
  const scope = job.scope;
13444
13563
  let latestRevision = 0;
13445
13564
  try {
13446
- const revisions = await this.operationStore.getRevisions(job.documentId, job.branch);
13565
+ const revisions = await stores.operationStore.getRevisions(job.documentId, job.branch);
13447
13566
  latestRevision = revisions.revision[scope] ?? 0;
13448
13567
  } catch {
13449
13568
  latestRevision = 0;
@@ -13459,7 +13578,7 @@ ${error.stack}`;
13459
13578
  }
13460
13579
  let conflictingOps = [];
13461
13580
  try {
13462
- const conflictingResult = await this.operationStore.getConflicting(job.documentId, scope, job.branch, minIncomingTimestamp);
13581
+ const conflictingResult = await stores.operationStore.getConflicting(job.documentId, scope, job.branch, minIncomingTimestamp);
13463
13582
  conflictingOps = conflictingResult.results;
13464
13583
  } catch {
13465
13584
  conflictingOps = [];
@@ -13468,7 +13587,7 @@ ${error.stack}`;
13468
13587
  if (conflictingOps.length > 0) {
13469
13588
  const minConflictingIndex = Math.min(...conflictingOps.map((op) => op.index));
13470
13589
  try {
13471
- const allOpsResult = await this.operationStore.getSince(job.documentId, scope, job.branch, minConflictingIndex - 1);
13590
+ const allOpsResult = await stores.operationStore.getSince(job.documentId, scope, job.branch, minConflictingIndex - 1);
13472
13591
  allOpsFromMinConflictingIndex = allOpsResult.results;
13473
13592
  } catch {
13474
13593
  allOpsFromMinConflictingIndex = conflictingOps;
@@ -13529,7 +13648,7 @@ ${error.stack}`;
13529
13648
  const actions2 = reshuffledOperations.map((operation) => operation.action);
13530
13649
  const skipValues = reshuffledOperations.map((operation) => operation.skip);
13531
13650
  const effectiveSourceRemote = skipCount > 0 ? "" : job.meta.sourceRemote || "";
13532
- const result = await this.processActions(job, actions2, startTime, indexTxn, skipValues, reshuffledOperations, effectiveSourceRemote);
13651
+ const result = await this.processActions(job, actions2, startTime, indexTxn, stores, skipValues, reshuffledOperations, effectiveSourceRemote);
13533
13652
  if (!result.success) {
13534
13653
  return {
13535
13654
  job,
@@ -13538,9 +13657,9 @@ ${error.stack}`;
13538
13657
  duration: Date.now() - startTime
13539
13658
  };
13540
13659
  }
13541
- this.writeCache.invalidate(job.documentId, scope, job.branch);
13660
+ stores.writeCache.invalidate(job.documentId, scope, job.branch);
13542
13661
  if (scope === "document") {
13543
- this.documentMetaCache.invalidate(job.documentId, job.branch);
13662
+ stores.documentMetaCache.invalidate(job.documentId, job.branch);
13544
13663
  }
13545
13664
  return {
13546
13665
  job,
@@ -15456,14 +15575,23 @@ class KyselyDocumentIndexer extends BaseReadModel {
15456
15575
  // src/storage/kysely/keyframe-store.ts
15457
15576
  class KyselyKeyframeStore {
15458
15577
  db;
15578
+ trx;
15459
15579
  constructor(db) {
15460
15580
  this.db = db;
15461
15581
  }
15582
+ get queryExecutor() {
15583
+ return this.trx ?? this.db;
15584
+ }
15585
+ withTransaction(trx) {
15586
+ const instance = new KyselyKeyframeStore(this.db);
15587
+ instance.trx = trx;
15588
+ return instance;
15589
+ }
15462
15590
  async putKeyframe(documentId, scope, branch, revision, document, signal) {
15463
15591
  if (signal?.aborted) {
15464
15592
  throw new Error("Operation aborted");
15465
15593
  }
15466
- await this.db.insertInto("Keyframe").values({
15594
+ await this.queryExecutor.insertInto("Keyframe").values({
15467
15595
  documentId,
15468
15596
  documentType: document.header.documentType,
15469
15597
  scope,
@@ -15476,7 +15604,7 @@ class KyselyKeyframeStore {
15476
15604
  if (signal?.aborted) {
15477
15605
  throw new Error("Operation aborted");
15478
15606
  }
15479
- const row = await this.db.selectFrom("Keyframe").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("revision", "<=", targetRevision).orderBy("revision", "desc").limit(1).executeTakeFirst();
15607
+ 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();
15480
15608
  if (!row) {
15481
15609
  return;
15482
15610
  }
@@ -15485,11 +15613,30 @@ class KyselyKeyframeStore {
15485
15613
  document: row.document
15486
15614
  };
15487
15615
  }
15616
+ async listKeyframes(documentId, scope, branch, signal) {
15617
+ if (signal?.aborted) {
15618
+ throw new Error("Operation aborted");
15619
+ }
15620
+ let query = this.queryExecutor.selectFrom("Keyframe").selectAll().where("documentId", "=", documentId).orderBy("revision", "asc");
15621
+ if (scope !== undefined) {
15622
+ query = query.where("scope", "=", scope);
15623
+ }
15624
+ if (branch !== undefined) {
15625
+ query = query.where("branch", "=", branch);
15626
+ }
15627
+ const rows = await query.execute();
15628
+ return rows.map((row) => ({
15629
+ scope: row.scope,
15630
+ branch: row.branch,
15631
+ revision: row.revision,
15632
+ document: row.document
15633
+ }));
15634
+ }
15488
15635
  async deleteKeyframes(documentId, scope, branch, signal) {
15489
15636
  if (signal?.aborted) {
15490
15637
  throw new Error("Operation aborted");
15491
15638
  }
15492
- let query = this.db.deleteFrom("Keyframe").where("documentId", "=", documentId);
15639
+ let query = this.queryExecutor.deleteFrom("Keyframe").where("documentId", "=", documentId);
15493
15640
  if (scope !== undefined && branch !== undefined) {
15494
15641
  query = query.where("scope", "=", scope).where("branch", "=", branch);
15495
15642
  } else if (scope !== undefined) {
@@ -15564,48 +15711,64 @@ class AtomicTransaction {
15564
15711
  // src/storage/kysely/store.ts
15565
15712
  class KyselyOperationStore {
15566
15713
  db;
15714
+ trx;
15567
15715
  constructor(db) {
15568
15716
  this.db = db;
15569
15717
  }
15718
+ get queryExecutor() {
15719
+ return this.trx ?? this.db;
15720
+ }
15721
+ withTransaction(trx) {
15722
+ const instance = new KyselyOperationStore(this.db);
15723
+ instance.trx = trx;
15724
+ return instance;
15725
+ }
15570
15726
  async apply(documentId, documentType, scope, branch, revision, fn, signal) {
15571
- await this.db.transaction().execute(async (trx) => {
15572
- if (signal?.aborted) {
15573
- throw new Error("Operation aborted");
15727
+ if (this.trx) {
15728
+ await this.executeApply(this.trx, documentId, documentType, scope, branch, revision, fn, signal);
15729
+ } else {
15730
+ await this.db.transaction().execute(async (trx) => {
15731
+ await this.executeApply(trx, documentId, documentType, scope, branch, revision, fn, signal);
15732
+ });
15733
+ }
15734
+ }
15735
+ async executeApply(trx, documentId, documentType, scope, branch, revision, fn, signal) {
15736
+ if (signal?.aborted) {
15737
+ throw new Error("Operation aborted");
15738
+ }
15739
+ const latestOp = await trx.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).orderBy("index", "desc").limit(1).executeTakeFirst();
15740
+ const currentRevision = latestOp ? latestOp.index : -1;
15741
+ if (currentRevision !== revision - 1) {
15742
+ throw new RevisionMismatchError(currentRevision + 1, revision);
15743
+ }
15744
+ const atomicTxn = new AtomicTransaction(documentId, documentType, scope, branch, revision);
15745
+ await fn(atomicTxn);
15746
+ const operations = atomicTxn.getOperations();
15747
+ if (operations.length > 0) {
15748
+ let prevOpId = latestOp?.opId || "";
15749
+ for (const op of operations) {
15750
+ op.prevOpId = prevOpId;
15751
+ prevOpId = op.opId;
15574
15752
  }
15575
- const latestOp = await trx.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).orderBy("index", "desc").limit(1).executeTakeFirst();
15576
- const currentRevision = latestOp ? latestOp.index : -1;
15577
- if (currentRevision !== revision - 1) {
15578
- throw new RevisionMismatchError(currentRevision + 1, revision);
15579
- }
15580
- const atomicTxn = new AtomicTransaction(documentId, documentType, scope, branch, revision);
15581
- await fn(atomicTxn);
15582
- const operations = atomicTxn.getOperations();
15583
- if (operations.length > 0) {
15584
- let prevOpId = latestOp?.opId || "";
15585
- for (const op of operations) {
15586
- op.prevOpId = prevOpId;
15587
- prevOpId = op.opId;
15588
- }
15589
- try {
15590
- await trx.insertInto("Operation").values(operations).execute();
15591
- } catch (error) {
15592
- if (error instanceof Error) {
15593
- if (error.message.includes("unique constraint")) {
15594
- const op = operations[0];
15595
- throw new DuplicateOperationError(`${op.opId} at index ${op.index} with skip ${op.skip}`);
15596
- }
15597
- throw error;
15753
+ try {
15754
+ await trx.insertInto("Operation").values(operations).execute();
15755
+ } catch (error) {
15756
+ if (error instanceof Error) {
15757
+ if (error.message.includes("unique constraint")) {
15758
+ const op = operations[0];
15759
+ throw new DuplicateOperationError(`${op.opId} at index ${op.index} with skip ${op.skip}`);
15598
15760
  }
15599
15761
  throw error;
15600
15762
  }
15763
+ throw error;
15601
15764
  }
15602
- });
15765
+ }
15603
15766
  }
15604
15767
  async getSince(documentId, scope, branch, revision, filter, paging, signal) {
15605
15768
  if (signal?.aborted) {
15606
15769
  throw new Error("Operation aborted");
15607
15770
  }
15608
- let query = this.db.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("index", ">", revision).orderBy("index", "asc");
15771
+ let query = this.queryExecutor.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("index", ">", revision).orderBy("index", "asc");
15609
15772
  if (filter) {
15610
15773
  if (filter.actionTypes && filter.actionTypes.length > 0) {
15611
15774
  const actionTypesArray = filter.actionTypes.map((t) => `'${t.replace(/'/g, "''")}'`).join(",");
@@ -15652,7 +15815,7 @@ class KyselyOperationStore {
15652
15815
  if (signal?.aborted) {
15653
15816
  throw new Error("Operation aborted");
15654
15817
  }
15655
- let query = this.db.selectFrom("Operation").selectAll().where("id", ">", id).orderBy("id", "asc");
15818
+ let query = this.queryExecutor.selectFrom("Operation").selectAll().where("id", ">", id).orderBy("id", "asc");
15656
15819
  if (paging) {
15657
15820
  const cursorValue = Number.parseInt(paging.cursor, 10);
15658
15821
  if (cursorValue > 0) {
@@ -15684,7 +15847,7 @@ class KyselyOperationStore {
15684
15847
  if (signal?.aborted) {
15685
15848
  throw new Error("Operation aborted");
15686
15849
  }
15687
- let query = this.db.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("timestampUtcMs", ">=", new Date(minTimestamp)).orderBy("index", "asc");
15850
+ let query = this.queryExecutor.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("timestampUtcMs", ">=", new Date(minTimestamp)).orderBy("index", "asc");
15688
15851
  if (paging) {
15689
15852
  const cursorValue = Number.parseInt(paging.cursor, 10);
15690
15853
  if (cursorValue > 0) {
@@ -15716,7 +15879,7 @@ class KyselyOperationStore {
15716
15879
  if (signal?.aborted) {
15717
15880
  throw new Error("Operation aborted");
15718
15881
  }
15719
- const scopeRevisions = await this.db.selectFrom("Operation as o1").select(["o1.scope", "o1.index", "o1.timestampUtcMs"]).where("o1.documentId", "=", documentId).where("o1.branch", "=", branch).where((eb) => eb("o1.index", "=", eb.selectFrom("Operation as o2").select((eb2) => eb2.fn.max("o2.index").as("maxIndex")).where("o2.documentId", "=", eb.ref("o1.documentId")).where("o2.branch", "=", eb.ref("o1.branch")).where("o2.scope", "=", eb.ref("o1.scope")))).execute();
15882
+ const scopeRevisions = await this.queryExecutor.selectFrom("Operation as o1").select(["o1.scope", "o1.index", "o1.timestampUtcMs"]).where("o1.documentId", "=", documentId).where("o1.branch", "=", branch).where((eb) => eb("o1.index", "=", eb.selectFrom("Operation as o2").select((eb2) => eb2.fn.max("o2.index").as("maxIndex")).where("o2.documentId", "=", eb.ref("o1.documentId")).where("o2.branch", "=", eb.ref("o1.branch")).where("o2.scope", "=", eb.ref("o1.scope")))).execute();
15720
15883
  const revision = {};
15721
15884
  let latestTimestamp = new Date(0).toISOString();
15722
15885
  for (const row of scopeRevisions) {
@@ -19755,9 +19918,10 @@ class ReactorBuilder {
19755
19918
  });
19756
19919
  await documentMetaCache.startup();
19757
19920
  const collectionMembershipCache = new CollectionMembershipCache(operationIndex);
19921
+ const executionScope = new KyselyExecutionScope(database, operationStore, operationIndex, keyframeStore, writeCache, documentMetaCache, collectionMembershipCache);
19758
19922
  let executorManager = this.executorManager;
19759
19923
  if (!executorManager) {
19760
- executorManager = new SimpleJobExecutorManager(() => new SimpleJobExecutor(this.logger, documentModelRegistry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, this.executorConfig, this.signatureVerifier), eventBus, queue, jobTracker, this.logger, resolver);
19924
+ executorManager = new SimpleJobExecutorManager(() => new SimpleJobExecutor(this.logger, documentModelRegistry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, this.executorConfig, this.signatureVerifier, executionScope), eventBus, queue, jobTracker, this.logger, resolver);
19761
19925
  }
19762
19926
  await executorManager.start(this.executorConfig.maxConcurrency ?? 1);
19763
19927
  const readModelInstances = Array.from(new Set([...this.readModels]));
@@ -19889,6 +20053,7 @@ class ReactorClientBuilder {
19889
20053
  signatureVerifier;
19890
20054
  subscriptionManager;
19891
20055
  jobAwaiter;
20056
+ documentModelLoader;
19892
20057
  withLogger(logger) {
19893
20058
  this.logger = logger;
19894
20059
  return this;
@@ -19927,6 +20092,10 @@ class ReactorClientBuilder {
19927
20092
  this.jobAwaiter = jobAwaiter;
19928
20093
  return this;
19929
20094
  }
20095
+ withDocumentModelLoader(loader) {
20096
+ this.documentModelLoader = loader;
20097
+ return this;
20098
+ }
19930
20099
  async build() {
19931
20100
  const module = await this.buildModule();
19932
20101
  return module.client;
@@ -19944,6 +20113,9 @@ class ReactorClientBuilder {
19944
20113
  if (this.signatureVerifier) {
19945
20114
  this.reactorBuilder.withSignatureVerifier(this.signatureVerifier);
19946
20115
  }
20116
+ if (this.documentModelLoader) {
20117
+ this.reactorBuilder.withDocumentModelLoader(this.documentModelLoader);
20118
+ }
19947
20119
  reactorModule = await this.reactorBuilder.buildModule();
19948
20120
  reactor = reactorModule.reactor;
19949
20121
  eventBus = reactorModule.eventBus;
@@ -20126,6 +20298,126 @@ class RelationalDbProcessor {
20126
20298
  return relationalDbToQueryBuilder(this.relationalDb);
20127
20299
  }
20128
20300
  }
20301
+ // src/admin/document-integrity-service.ts
20302
+ import { hashDocumentStateForScope } from "document-model/core";
20303
+ var nullKeyframeStore = {
20304
+ putKeyframe: () => Promise.resolve(),
20305
+ findNearestKeyframe: () => Promise.resolve(undefined),
20306
+ listKeyframes: () => Promise.resolve([]),
20307
+ deleteKeyframes: () => Promise.resolve(0)
20308
+ };
20309
+
20310
+ class DocumentIntegrityService {
20311
+ keyframeStore;
20312
+ operationStore;
20313
+ writeCache;
20314
+ documentView;
20315
+ documentModelRegistry;
20316
+ constructor(keyframeStore, operationStore, writeCache, documentView, documentModelRegistry) {
20317
+ this.keyframeStore = keyframeStore;
20318
+ this.operationStore = operationStore;
20319
+ this.writeCache = writeCache;
20320
+ this.documentView = documentView;
20321
+ this.documentModelRegistry = documentModelRegistry;
20322
+ }
20323
+ async validateDocument(documentId, branch = "main", signal) {
20324
+ const keyframeIssues = [];
20325
+ const snapshotIssues = [];
20326
+ const replayCache = new KyselyWriteCache(nullKeyframeStore, this.operationStore, this.documentModelRegistry, {
20327
+ maxDocuments: 1,
20328
+ ringBufferSize: 1,
20329
+ keyframeInterval: Number.MAX_SAFE_INTEGER
20330
+ });
20331
+ const keyframes = await this.keyframeStore.listKeyframes(documentId, undefined, branch, signal);
20332
+ for (const keyframe of keyframes) {
20333
+ if (signal?.aborted) {
20334
+ throw new Error("Operation aborted");
20335
+ }
20336
+ replayCache.invalidate(documentId, keyframe.scope, branch);
20337
+ const replayedDoc = await replayCache.getState(documentId, keyframe.scope, branch, keyframe.revision, signal);
20338
+ const kfHash = hashDocumentStateForScope(keyframe.document, keyframe.scope);
20339
+ const replayHash = hashDocumentStateForScope(replayedDoc, keyframe.scope);
20340
+ if (kfHash !== replayHash) {
20341
+ keyframeIssues.push({
20342
+ scope: keyframe.scope,
20343
+ branch,
20344
+ revision: keyframe.revision,
20345
+ keyframeHash: kfHash,
20346
+ replayedHash: replayHash
20347
+ });
20348
+ }
20349
+ }
20350
+ let currentDoc;
20351
+ try {
20352
+ currentDoc = await this.documentView.get(documentId);
20353
+ } catch {
20354
+ return {
20355
+ documentId,
20356
+ isConsistent: keyframeIssues.length === 0,
20357
+ keyframeIssues,
20358
+ snapshotIssues
20359
+ };
20360
+ }
20361
+ const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
20362
+ const allScopes = Object.keys(revisions.revision);
20363
+ for (const scope of allScopes) {
20364
+ if (scope === "document")
20365
+ continue;
20366
+ replayCache.invalidate(documentId, scope, branch);
20367
+ let replayedDoc;
20368
+ try {
20369
+ replayedDoc = await replayCache.getState(documentId, scope, branch, undefined, signal);
20370
+ } catch {
20371
+ if (signal?.aborted) {
20372
+ throw new Error("Operation aborted");
20373
+ }
20374
+ continue;
20375
+ }
20376
+ const snapshotHash = hashDocumentStateForScope(currentDoc, scope);
20377
+ const replayHash = hashDocumentStateForScope(replayedDoc, scope);
20378
+ if (snapshotHash !== replayHash) {
20379
+ snapshotIssues.push({
20380
+ scope,
20381
+ branch,
20382
+ snapshotHash,
20383
+ replayedHash: replayHash
20384
+ });
20385
+ }
20386
+ }
20387
+ return {
20388
+ documentId,
20389
+ isConsistent: keyframeIssues.length === 0 && snapshotIssues.length === 0,
20390
+ keyframeIssues,
20391
+ snapshotIssues
20392
+ };
20393
+ }
20394
+ async rebuildKeyframes(documentId, branch = "main", signal) {
20395
+ const deleted = await this.keyframeStore.deleteKeyframes(documentId, undefined, branch, signal);
20396
+ return {
20397
+ documentId,
20398
+ keyframesDeleted: deleted,
20399
+ scopesInvalidated: 0
20400
+ };
20401
+ }
20402
+ async rebuildSnapshots(documentId, branch = "main", signal) {
20403
+ const scopes = await this.discoverScopes(documentId, branch, signal);
20404
+ for (const scope of scopes) {
20405
+ if (signal?.aborted) {
20406
+ throw new Error("Operation aborted");
20407
+ }
20408
+ this.writeCache.invalidate(documentId, scope, branch);
20409
+ }
20410
+ return {
20411
+ documentId,
20412
+ keyframesDeleted: 0,
20413
+ scopesInvalidated: scopes.length
20414
+ };
20415
+ }
20416
+ async discoverScopes(documentId, branch, signal) {
20417
+ const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
20418
+ return Object.keys(revisions.revision);
20419
+ }
20420
+ }
20129
20421
  export {
20130
20422
  upgradeDocumentAction,
20131
20423
  trimMailboxFromAckOrdinal,
@@ -20197,6 +20489,7 @@ export {
20197
20489
  DuplicateOperationError,
20198
20490
  DuplicateModuleError,
20199
20491
  DocumentModelRegistry,
20492
+ DocumentIntegrityService,
20200
20493
  DocumentChangeType,
20201
20494
  DefaultSubscriptionErrorHandler,
20202
20495
  ConsoleLogger,