@powerhousedao/reactor 6.0.0-dev.243 → 6.0.0-dev.245

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.
package/dist/index.js CHANGED
@@ -409,6 +409,10 @@ var DriveClient = class {
409
409
  if (!node) throw new Error("Node missing from drive after rename");
410
410
  return node;
411
411
  }
412
+ async setPreferredEditorOnNode(nodeId, preferredEditor, signal) {
413
+ this.logger.verbose("drives.setPreferredEditorOnNode(@nodeId, @preferredEditor)", nodeId, preferredEditor);
414
+ return this.client.setPreferredEditor(nodeId, preferredEditor, "main", signal);
415
+ }
412
416
  async moveNode(driveIdentifier, srcNodeId, targetParentFolderId, signal) {
413
417
  this.logger.verbose("drives.moveNode(@driveIdentifier, @srcNodeId, @targetParentFolderId)", driveIdentifier, srcNodeId, targetParentFolderId);
414
418
  return this.client.execute(driveIdentifier, "main", [moveNode({
@@ -928,6 +932,14 @@ var ReactorClient = class {
928
932
  return this.execute(documentIdentifier, branch, [actions.setName(name)], signal);
929
933
  }
930
934
  /**
935
+ * Updates the preferred editor recorded in the document header meta.
936
+ * Pass `null` to clear it.
937
+ */
938
+ async setPreferredEditor(documentIdentifier, preferredEditor, branch = "main", signal) {
939
+ this.logger.verbose("setPreferredEditor(@documentIdentifier, @preferredEditor, @branch)", documentIdentifier, preferredEditor, branch);
940
+ return this.execute(documentIdentifier, branch, [actions.setPreferredEditor(preferredEditor)], signal);
941
+ }
942
+ /**
931
943
  * Adds multiple documents as children to another and waits for completion
932
944
  */
933
945
  async addRelationship(sourceIdentifier, targetIdentifier, relationshipType, branch = "main", signal) {
@@ -2368,7 +2380,7 @@ let JobQueueState = /* @__PURE__ */ function(JobQueueState) {
2368
2380
  */
2369
2381
  const QueueEventTypes = { JOB_AVAILABLE: 1e4 };
2370
2382
  //#endregion
2371
- //#region src/registry/implementation.ts
2383
+ //#region src/registry/errors.ts
2372
2384
  /**
2373
2385
  * Error thrown when a document model module is not found in the registry.
2374
2386
  */
@@ -2456,140 +2468,6 @@ var InvalidUpgradeStepError = class extends Error {
2456
2468
  this.name = "InvalidUpgradeStepError";
2457
2469
  }
2458
2470
  };
2459
- /**
2460
- * In-memory implementation of the IDocumentModelRegistry interface.
2461
- * Manages document model modules with version-aware storage and upgrade manifest support.
2462
- */
2463
- var DocumentModelRegistry = class {
2464
- modules = [];
2465
- manifests = [];
2466
- registerModules(...modules) {
2467
- return modules.map((module) => {
2468
- try {
2469
- const documentType = module.documentModel.global.id;
2470
- const version = module.version ?? 1;
2471
- for (let i = 0; i < this.modules.length; i++) {
2472
- const existing = this.modules[i];
2473
- const existingType = existing.documentModel.global.id;
2474
- const existingVersion = existing.version ?? 1;
2475
- if (existingType === documentType && existingVersion === version) throw new DuplicateModuleError(documentType, version);
2476
- }
2477
- this.modules.push(module);
2478
- return {
2479
- status: "success",
2480
- item: module
2481
- };
2482
- } catch (error) {
2483
- return {
2484
- status: "error",
2485
- item: module,
2486
- error: error instanceof Error ? error : new Error(String(error))
2487
- };
2488
- }
2489
- });
2490
- }
2491
- unregisterModules(...documentTypes) {
2492
- let allFound = true;
2493
- for (const documentType of documentTypes) {
2494
- if (!this.modules.some((m) => m.documentModel.global.id === documentType)) allFound = false;
2495
- this.modules = this.modules.filter((m) => m.documentModel.global.id !== documentType);
2496
- }
2497
- return allFound;
2498
- }
2499
- getModule(documentType, version) {
2500
- let latestModule;
2501
- let latestVersion = -1;
2502
- for (let i = 0; i < this.modules.length; i++) {
2503
- const module = this.modules[i];
2504
- const moduleType = module.documentModel.global.id;
2505
- const moduleVersion = module.version ?? 1;
2506
- if (moduleType === documentType) {
2507
- if (version !== void 0 && moduleVersion === version) return module;
2508
- if (moduleVersion > latestVersion) {
2509
- latestModule = module;
2510
- latestVersion = moduleVersion;
2511
- }
2512
- }
2513
- }
2514
- if (version === void 0 && latestModule !== void 0) return latestModule;
2515
- throw new ModuleNotFoundError(documentType, version);
2516
- }
2517
- getAllModules() {
2518
- return [...this.modules];
2519
- }
2520
- clear() {
2521
- this.modules = [];
2522
- this.manifests = [];
2523
- }
2524
- getSupportedVersions(documentType) {
2525
- const versions = [];
2526
- for (const module of this.modules) if (module.documentModel.global.id === documentType) versions.push(module.version ?? 1);
2527
- if (versions.length === 0) throw new ModuleNotFoundError(documentType);
2528
- return versions.sort((a, b) => a - b);
2529
- }
2530
- getLatestVersion(documentType) {
2531
- let latest = -1;
2532
- let found = false;
2533
- for (const module of this.modules) if (module.documentModel.global.id === documentType) {
2534
- found = true;
2535
- const version = module.version ?? 1;
2536
- if (version > latest) latest = version;
2537
- }
2538
- if (!found) throw new ModuleNotFoundError(documentType);
2539
- return latest;
2540
- }
2541
- registerUpgradeManifests(...manifestsToRegister) {
2542
- return manifestsToRegister.map((manifestToRegister) => {
2543
- try {
2544
- if (!manifestToRegister.documentType) throw new Error("Upgrade manifest is missing a documentType");
2545
- for (const registeredManifest of this.manifests) if (registeredManifest.documentType === manifestToRegister.documentType) throw new DuplicateManifestError(manifestToRegister.documentType);
2546
- this.manifests.push(manifestToRegister);
2547
- return {
2548
- status: "success",
2549
- item: manifestToRegister
2550
- };
2551
- } catch (error) {
2552
- return {
2553
- status: "error",
2554
- item: manifestToRegister,
2555
- error: error instanceof Error ? error : new Error(String(error))
2556
- };
2557
- }
2558
- });
2559
- }
2560
- unregisterUpgradeManifests(...documentTypes) {
2561
- let allFound = true;
2562
- for (const documentType of documentTypes) {
2563
- if (!this.manifests.some((m) => m.documentType === documentType)) allFound = false;
2564
- this.manifests = this.manifests.filter((m) => m.documentType !== documentType);
2565
- }
2566
- return allFound;
2567
- }
2568
- getUpgradeManifest(documentType) {
2569
- for (let i = 0; i < this.manifests.length; i++) if (this.manifests[i].documentType === documentType) return this.manifests[i];
2570
- throw new ManifestNotFoundError(documentType);
2571
- }
2572
- computeUpgradePath(documentType, fromVersion, toVersion) {
2573
- if (fromVersion === toVersion) return [];
2574
- if (toVersion < fromVersion) throw new DowngradeNotSupportedError(documentType, fromVersion, toVersion);
2575
- const manifest = this.getUpgradeManifest(documentType);
2576
- const path = [];
2577
- for (let v = fromVersion + 1; v <= toVersion; v++) {
2578
- const key = `v${v}`;
2579
- if (!(key in manifest.upgrades)) throw new MissingUpgradeTransitionError(documentType, v - 1, v);
2580
- const transition = manifest.upgrades[key];
2581
- path.push(transition);
2582
- }
2583
- return path;
2584
- }
2585
- getUpgradeReducer(documentType, fromVersion, toVersion) {
2586
- if (toVersion !== fromVersion + 1) throw new InvalidUpgradeStepError(documentType, fromVersion, toVersion);
2587
- const manifest = this.getUpgradeManifest(documentType);
2588
- const key = `v${toVersion}`;
2589
- if (!(key in manifest.upgrades)) throw new MissingUpgradeTransitionError(documentType, fromVersion, toVersion);
2590
- return manifest.upgrades[key].upgradeReducer;
2591
- }
2592
- };
2593
2471
  //#endregion
2594
2472
  //#region src/executor/simple-job-executor-manager.ts
2595
2473
  /**
@@ -2854,6 +2732,10 @@ function yieldToMain() {
2854
2732
  if (s?.yield) return s.yield();
2855
2733
  return new Promise((resolve) => setTimeout(resolve, 0));
2856
2734
  }
2735
+ const defaultAbortError = () => /* @__PURE__ */ new Error("Operation aborted");
2736
+ function throwIfAborted(signal, makeError = defaultAbortError) {
2737
+ if (signal?.aborted) throw makeError();
2738
+ }
2857
2739
  //#endregion
2858
2740
  //#region src/utils/reshuffle.ts
2859
2741
  const STRICT_ORDER_ACTION_TYPES = new Set([
@@ -4995,6 +4877,142 @@ var NullDocumentModelResolver = class {
4995
4877
  }
4996
4878
  };
4997
4879
  //#endregion
4880
+ //#region src/registry/implementation.ts
4881
+ /**
4882
+ * In-memory implementation of the IDocumentModelRegistry interface.
4883
+ * Manages document model modules with version-aware storage and upgrade manifest support.
4884
+ */
4885
+ var DocumentModelRegistry = class {
4886
+ modules = [];
4887
+ manifests = [];
4888
+ registerModules(...modules) {
4889
+ return modules.map((module) => {
4890
+ try {
4891
+ const documentType = module.documentModel.global.id;
4892
+ const version = module.version ?? 1;
4893
+ for (let i = 0; i < this.modules.length; i++) {
4894
+ const existing = this.modules[i];
4895
+ const existingType = existing.documentModel.global.id;
4896
+ const existingVersion = existing.version ?? 1;
4897
+ if (existingType === documentType && existingVersion === version) throw new DuplicateModuleError(documentType, version);
4898
+ }
4899
+ this.modules.push(module);
4900
+ return {
4901
+ status: "success",
4902
+ item: module
4903
+ };
4904
+ } catch (error) {
4905
+ return {
4906
+ status: "error",
4907
+ item: module,
4908
+ error: error instanceof Error ? error : new Error(String(error))
4909
+ };
4910
+ }
4911
+ });
4912
+ }
4913
+ unregisterModules(...documentTypes) {
4914
+ let allFound = true;
4915
+ for (const documentType of documentTypes) {
4916
+ if (!this.modules.some((m) => m.documentModel.global.id === documentType)) allFound = false;
4917
+ this.modules = this.modules.filter((m) => m.documentModel.global.id !== documentType);
4918
+ }
4919
+ return allFound;
4920
+ }
4921
+ getModule(documentType, version) {
4922
+ let latestModule;
4923
+ let latestVersion = -1;
4924
+ for (let i = 0; i < this.modules.length; i++) {
4925
+ const module = this.modules[i];
4926
+ const moduleType = module.documentModel.global.id;
4927
+ const moduleVersion = module.version ?? 1;
4928
+ if (moduleType === documentType) {
4929
+ if (version !== void 0 && moduleVersion === version) return module;
4930
+ if (moduleVersion > latestVersion) {
4931
+ latestModule = module;
4932
+ latestVersion = moduleVersion;
4933
+ }
4934
+ }
4935
+ }
4936
+ if (version === void 0 && latestModule !== void 0) return latestModule;
4937
+ throw new ModuleNotFoundError(documentType, version);
4938
+ }
4939
+ getAllModules() {
4940
+ return [...this.modules];
4941
+ }
4942
+ clear() {
4943
+ this.modules = [];
4944
+ this.manifests = [];
4945
+ }
4946
+ getSupportedVersions(documentType) {
4947
+ const versions = [];
4948
+ for (const module of this.modules) if (module.documentModel.global.id === documentType) versions.push(module.version ?? 1);
4949
+ if (versions.length === 0) throw new ModuleNotFoundError(documentType);
4950
+ return versions.sort((a, b) => a - b);
4951
+ }
4952
+ getLatestVersion(documentType) {
4953
+ let latest = -1;
4954
+ let found = false;
4955
+ for (const module of this.modules) if (module.documentModel.global.id === documentType) {
4956
+ found = true;
4957
+ const version = module.version ?? 1;
4958
+ if (version > latest) latest = version;
4959
+ }
4960
+ if (!found) throw new ModuleNotFoundError(documentType);
4961
+ return latest;
4962
+ }
4963
+ registerUpgradeManifests(...manifestsToRegister) {
4964
+ return manifestsToRegister.map((manifestToRegister) => {
4965
+ try {
4966
+ if (!manifestToRegister.documentType) throw new Error("Upgrade manifest is missing a documentType");
4967
+ for (const registeredManifest of this.manifests) if (registeredManifest.documentType === manifestToRegister.documentType) throw new DuplicateManifestError(manifestToRegister.documentType);
4968
+ this.manifests.push(manifestToRegister);
4969
+ return {
4970
+ status: "success",
4971
+ item: manifestToRegister
4972
+ };
4973
+ } catch (error) {
4974
+ return {
4975
+ status: "error",
4976
+ item: manifestToRegister,
4977
+ error: error instanceof Error ? error : new Error(String(error))
4978
+ };
4979
+ }
4980
+ });
4981
+ }
4982
+ unregisterUpgradeManifests(...documentTypes) {
4983
+ let allFound = true;
4984
+ for (const documentType of documentTypes) {
4985
+ if (!this.manifests.some((m) => m.documentType === documentType)) allFound = false;
4986
+ this.manifests = this.manifests.filter((m) => m.documentType !== documentType);
4987
+ }
4988
+ return allFound;
4989
+ }
4990
+ getUpgradeManifest(documentType) {
4991
+ for (let i = 0; i < this.manifests.length; i++) if (this.manifests[i].documentType === documentType) return this.manifests[i];
4992
+ throw new ManifestNotFoundError(documentType);
4993
+ }
4994
+ computeUpgradePath(documentType, fromVersion, toVersion) {
4995
+ if (fromVersion === toVersion) return [];
4996
+ if (toVersion < fromVersion) throw new DowngradeNotSupportedError(documentType, fromVersion, toVersion);
4997
+ const manifest = this.getUpgradeManifest(documentType);
4998
+ const path = [];
4999
+ for (let v = fromVersion + 1; v <= toVersion; v++) {
5000
+ const key = `v${v}`;
5001
+ if (!(key in manifest.upgrades)) throw new MissingUpgradeTransitionError(documentType, v - 1, v);
5002
+ const transition = manifest.upgrades[key];
5003
+ path.push(transition);
5004
+ }
5005
+ return path;
5006
+ }
5007
+ getUpgradeReducer(documentType, fromVersion, toVersion) {
5008
+ if (toVersion !== fromVersion + 1) throw new InvalidUpgradeStepError(documentType, fromVersion, toVersion);
5009
+ const manifest = this.getUpgradeManifest(documentType);
5010
+ const key = `v${toVersion}`;
5011
+ if (!(key in manifest.upgrades)) throw new MissingUpgradeTransitionError(documentType, fromVersion, toVersion);
5012
+ return manifest.upgrades[key].upgradeReducer;
5013
+ }
5014
+ };
5015
+ //#endregion
4998
5016
  //#region src/shared/consistency-tracker.ts
4999
5017
  /**
5000
5018
  * Creates a consistency key from documentId, scope, and branch.
@@ -5403,6 +5421,29 @@ var KyselyKeyframeStore = class KyselyKeyframeStore {
5403
5421
  }
5404
5422
  };
5405
5423
  //#endregion
5424
+ //#region src/storage/kysely/pagination.ts
5425
+ const DEFAULT_LIMIT = 100;
5426
+ function paginateRows(rows, paging, cursorOf, toItem, refetch) {
5427
+ let hasMore = false;
5428
+ let items = rows;
5429
+ if (paging?.limit && rows.length > paging.limit) {
5430
+ hasMore = true;
5431
+ items = rows.slice(0, paging.limit);
5432
+ }
5433
+ const nextCursor = hasMore && items.length > 0 ? cursorOf(items[items.length - 1]).toString() : void 0;
5434
+ const cursor = paging?.cursor || "0";
5435
+ const limit = paging?.limit || DEFAULT_LIMIT;
5436
+ return {
5437
+ results: items.map(toItem),
5438
+ options: {
5439
+ cursor,
5440
+ limit
5441
+ },
5442
+ nextCursor,
5443
+ next: hasMore ? () => refetch(nextCursor, limit) : void 0
5444
+ };
5445
+ }
5446
+ //#endregion
5406
5447
  //#region src/storage/interfaces.ts
5407
5448
  /**
5408
5449
  * Thrown when an operation with the same identity already exists in the store.
@@ -5486,7 +5527,7 @@ var KyselyOperationStore = class KyselyOperationStore {
5486
5527
  });
5487
5528
  }
5488
5529
  async executeApply(trx, documentId, documentType, scope, branch, revision, fn, signal) {
5489
- if (signal?.aborted) throw new Error("Operation aborted");
5530
+ throwIfAborted(signal);
5490
5531
  const latestOp = await trx.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).orderBy("index", "desc").limit(1).executeTakeFirst();
5491
5532
  const currentRevision = latestOp ? latestOp.index : -1;
5492
5533
  if (currentRevision !== revision - 1) throw new RevisionMismatchError(currentRevision + 1, revision);
@@ -5514,7 +5555,7 @@ var KyselyOperationStore = class KyselyOperationStore {
5514
5555
  }
5515
5556
  }
5516
5557
  async getSince(documentId, scope, branch, revision, filter, paging, signal) {
5517
- if (signal?.aborted) throw new Error("Operation aborted");
5558
+ throwIfAborted(signal);
5518
5559
  let query = this.queryExecutor.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("index", ">", revision).orderBy("index", "asc");
5519
5560
  if (filter) {
5520
5561
  if (filter.actionTypes && filter.actionTypes.length > 0) {
@@ -5530,93 +5571,39 @@ var KyselyOperationStore = class KyselyOperationStore {
5530
5571
  if (cursorValue > 0) query = query.where("index", ">", cursorValue);
5531
5572
  if (paging.limit) query = query.limit(paging.limit + 1);
5532
5573
  }
5533
- const rows = await query.execute();
5534
- let hasMore = false;
5535
- let items = rows;
5536
- if (paging?.limit && rows.length > paging.limit) {
5537
- hasMore = true;
5538
- items = rows.slice(0, paging.limit);
5539
- }
5540
- const nextCursor = hasMore && items.length > 0 ? items[items.length - 1].index.toString() : void 0;
5541
- const cursor = paging?.cursor || "0";
5542
- const limit = paging?.limit || 100;
5543
- return {
5544
- results: items.map((row) => this.rowToOperation(row)),
5545
- options: {
5546
- cursor,
5547
- limit
5548
- },
5549
- nextCursor,
5550
- next: hasMore ? () => this.getSince(documentId, scope, branch, revision, filter, {
5551
- cursor: nextCursor,
5552
- limit
5553
- }, signal) : void 0
5554
- };
5574
+ return paginateRows(await query.execute(), paging, (row) => row.index, (row) => this.rowToOperation(row), (cursor, limit) => this.getSince(documentId, scope, branch, revision, filter, {
5575
+ cursor,
5576
+ limit
5577
+ }, signal));
5555
5578
  }
5556
5579
  async getSinceId(id, paging, signal) {
5557
- if (signal?.aborted) throw new Error("Operation aborted");
5580
+ throwIfAborted(signal);
5558
5581
  let query = this.queryExecutor.selectFrom("Operation").selectAll().where("id", ">", id).orderBy("id", "asc");
5559
5582
  if (paging) {
5560
5583
  const cursorValue = Number.parseInt(paging.cursor, 10);
5561
5584
  if (cursorValue > 0) query = query.where("id", ">", cursorValue);
5562
5585
  if (paging.limit) query = query.limit(paging.limit + 1);
5563
5586
  }
5564
- const rows = await query.execute();
5565
- let hasMore = false;
5566
- let items = rows;
5567
- if (paging?.limit && rows.length > paging.limit) {
5568
- hasMore = true;
5569
- items = rows.slice(0, paging.limit);
5570
- }
5571
- const nextCursor = hasMore && items.length > 0 ? items[items.length - 1].id.toString() : void 0;
5572
- const cursor = paging?.cursor || "0";
5573
- const limit = paging?.limit || 100;
5574
- return {
5575
- results: items.map((row) => this.rowToOperationWithContext(row)),
5576
- options: {
5577
- cursor,
5578
- limit
5579
- },
5580
- nextCursor,
5581
- next: hasMore ? () => this.getSinceId(id, {
5582
- cursor: nextCursor,
5583
- limit
5584
- }, signal) : void 0
5585
- };
5587
+ return paginateRows(await query.execute(), paging, (row) => row.id, (row) => this.rowToOperationWithContext(row), (cursor, limit) => this.getSinceId(id, {
5588
+ cursor,
5589
+ limit
5590
+ }, signal));
5586
5591
  }
5587
5592
  async getConflicting(documentId, scope, branch, minTimestamp, paging, signal) {
5588
- if (signal?.aborted) throw new Error("Operation aborted");
5593
+ throwIfAborted(signal);
5589
5594
  let query = this.queryExecutor.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("timestampUtcMs", ">=", new Date(minTimestamp)).orderBy("index", "asc");
5590
5595
  if (paging) {
5591
5596
  const cursorValue = Number.parseInt(paging.cursor, 10);
5592
5597
  if (cursorValue > 0) query = query.where("index", ">", cursorValue);
5593
5598
  if (paging.limit) query = query.limit(paging.limit + 1);
5594
5599
  }
5595
- const rows = await query.execute();
5596
- let hasMore = false;
5597
- let items = rows;
5598
- if (paging?.limit && rows.length > paging.limit) {
5599
- hasMore = true;
5600
- items = rows.slice(0, paging.limit);
5601
- }
5602
- const nextCursor = hasMore && items.length > 0 ? items[items.length - 1].index.toString() : void 0;
5603
- const cursor = paging?.cursor || "0";
5604
- const limit = paging?.limit || 100;
5605
- return {
5606
- results: items.map((row) => this.rowToOperation(row)),
5607
- options: {
5608
- cursor,
5609
- limit
5610
- },
5611
- nextCursor,
5612
- next: hasMore ? () => this.getConflicting(documentId, scope, branch, minTimestamp, {
5613
- cursor: nextCursor,
5614
- limit
5615
- }, signal) : void 0
5616
- };
5600
+ return paginateRows(await query.execute(), paging, (row) => row.index, (row) => this.rowToOperation(row), (cursor, limit) => this.getConflicting(documentId, scope, branch, minTimestamp, {
5601
+ cursor,
5602
+ limit
5603
+ }, signal));
5617
5604
  }
5618
5605
  async getRevisions(documentId, branch, signal) {
5619
- if (signal?.aborted) throw new Error("Operation aborted");
5606
+ throwIfAborted(signal);
5620
5607
  const scopeRevisions = await this.queryExecutor.selectFrom("Operation as o1").select([
5621
5608
  "o1.scope",
5622
5609
  "o1.index",
@@ -6610,6 +6597,38 @@ function batchOperationsByDocument(operations) {
6610
6597
  flushBatch();
6611
6598
  return batches;
6612
6599
  }
6600
+ /**
6601
+ * Splits a sorted page of operations into a safe-to-emit prefix and a
6602
+ * deferred tail containing the trailing run that shares the same
6603
+ * (documentId, branch, scope, timestampUtcMs) as the last operation.
6604
+ *
6605
+ * The page is assumed to be sorted by (documentId, scope, ordinal), so a
6606
+ * same-(docId, scope, ts) run is contiguous and lives at the end of any
6607
+ * page that contains its last member. Holding that tail back lets callers
6608
+ * prepend it to the next page so a single producer-side execute() call
6609
+ * never gets split across two outbound envelopes.
6610
+ */
6611
+ function splitTrailingSameTimestampRun(operations) {
6612
+ if (operations.length === 0) return {
6613
+ emit: [],
6614
+ carry: []
6615
+ };
6616
+ const last = operations[operations.length - 1];
6617
+ const lastDocId = last.context.documentId;
6618
+ const lastBranch = last.context.branch;
6619
+ const lastScope = last.context.scope;
6620
+ const lastTs = last.operation.timestampUtcMs;
6621
+ let carryStart = operations.length;
6622
+ for (let i = operations.length - 1; i >= 0; i--) {
6623
+ const op = operations[i];
6624
+ if (op.context.documentId === lastDocId && op.context.branch === lastBranch && op.context.scope === lastScope && op.operation.timestampUtcMs === lastTs) carryStart = i;
6625
+ else break;
6626
+ }
6627
+ return {
6628
+ emit: operations.slice(0, carryStart),
6629
+ carry: operations.slice(carryStart)
6630
+ };
6631
+ }
6613
6632
  function toOperationWithContext(entry) {
6614
6633
  return {
6615
6634
  operation: {
@@ -8744,8 +8763,8 @@ var SyncManager = class {
8744
8763
  }
8745
8764
  if (this.isShutdown) return;
8746
8765
  for (const plan of jobs) {
8766
+ if (!(plan.key in result.jobs)) continue;
8747
8767
  const info = result.jobs[plan.key];
8748
- if (!info) continue;
8749
8768
  this.recordPlanKeyMapping(plan.key, info.id);
8750
8769
  const fifoKey = `${plan.documentId}:${plan.scope}:${plan.branch}`;
8751
8770
  this.lastEnqueuedJobIdByKey.set(fifoKey, info.id);
@@ -8785,41 +8804,55 @@ var SyncManager = class {
8785
8804
  const composedSignal = signal ? AbortSignal.any([signal, this.abortController.signal]) : this.abortController.signal;
8786
8805
  let maxOrdinal = ackOrdinal;
8787
8806
  const lastJobByDoc = /* @__PURE__ */ new Map();
8807
+ let prevChainJobId;
8788
8808
  const sinceTimestamp = remote.options.sinceTimestampUtcMs;
8809
+ const emitBatches = (operations) => {
8810
+ if (operations.length === 0) return;
8811
+ const batches = batchOperationsByDocument(operations);
8812
+ const syncOps = [];
8813
+ for (const batch of batches) {
8814
+ const jobId = crypto.randomUUID();
8815
+ const prevJobId = lastJobByDoc.get(batch.documentId);
8816
+ const deps = [];
8817
+ if (prevJobId) deps.push(prevJobId);
8818
+ if (mode === OutboxMode.BatchTriggered && prevChainJobId && prevChainJobId !== prevJobId) deps.push(prevChainJobId);
8819
+ const syncOp = new SyncOperation(crypto.randomUUID(), jobId, deps, remote.name, batch.documentId, [batch.scope], batch.branch, batch.operations);
8820
+ syncOps.push(syncOp);
8821
+ lastJobByDoc.set(batch.documentId, jobId);
8822
+ if (mode === OutboxMode.BatchTriggered) prevChainJobId = jobId;
8823
+ }
8824
+ remote.channel.outbox.add(...syncOps);
8825
+ };
8789
8826
  let page = await this.operationIndex.find(remote.collectionId, ackOrdinal, { excludeSourceRemote: remote.name }, void 0, composedSignal);
8827
+ let carry = [];
8790
8828
  let hasMore;
8791
8829
  do {
8792
8830
  if (composedSignal.aborted) return;
8793
8831
  for (const entry of page.results) maxOrdinal = Math.max(maxOrdinal, entry.ordinal ?? 0);
8794
8832
  let operations = page.results.map((entry) => toOperationWithContext(entry));
8833
+ if (carry.length > 0) {
8834
+ operations = [...carry, ...operations];
8835
+ carry = [];
8836
+ }
8795
8837
  if (sinceTimestamp && sinceTimestamp !== "0") operations = operations.filter((op) => op.operation.timestampUtcMs >= sinceTimestamp);
8796
8838
  operations = filterOperations(operations, remote.filter);
8797
8839
  operations = operations.filter((op) => !this.quarantinedDocumentIds.has(op.context.documentId));
8840
+ hasMore = !!page.next;
8798
8841
  if (operations.length > 0) {
8799
8842
  operations.sort((a, b) => {
8800
8843
  if (a.context.documentId !== b.context.documentId) return a.context.documentId < b.context.documentId ? -1 : 1;
8801
8844
  if (a.context.scope !== b.context.scope) return a.context.scope < b.context.scope ? -1 : 1;
8802
8845
  return a.context.ordinal - b.context.ordinal;
8803
8846
  });
8804
- const batches = batchOperationsByDocument(operations);
8805
- const syncOps = [];
8806
- let prevChainJobId;
8807
- for (const batch of batches) {
8808
- const jobId = crypto.randomUUID();
8809
- const prevJobId = lastJobByDoc.get(batch.documentId);
8810
- const deps = [];
8811
- if (prevJobId) deps.push(prevJobId);
8812
- if (mode === OutboxMode.BatchTriggered && prevChainJobId && prevChainJobId !== prevJobId) deps.push(prevChainJobId);
8813
- const syncOp = new SyncOperation(crypto.randomUUID(), jobId, deps, remote.name, batch.documentId, [batch.scope], batch.branch, batch.operations);
8814
- syncOps.push(syncOp);
8815
- lastJobByDoc.set(batch.documentId, jobId);
8816
- if (mode === OutboxMode.BatchTriggered) prevChainJobId = jobId;
8817
- }
8818
- remote.channel.outbox.add(...syncOps);
8847
+ if (hasMore) {
8848
+ const split = splitTrailingSameTimestampRun(operations);
8849
+ carry = split.carry;
8850
+ emitBatches(split.emit);
8851
+ } else emitBatches(operations);
8819
8852
  }
8820
- hasMore = !!page.next;
8821
8853
  if (hasMore) page = await page.next();
8822
8854
  } while (hasMore);
8855
+ if (carry.length > 0) emitBatches(carry);
8823
8856
  remote.channel.outbox.advanceOrdinal(maxOrdinal);
8824
8857
  }
8825
8858
  };
@@ -8976,7 +9009,7 @@ var Reactor = class {
8976
9009
  }
8977
9010
  getDocumentModels(namespace, paging, signal) {
8978
9011
  this.logger.verbose("getDocumentModels(@namespace, @paging)", namespace, paging);
8979
- if (signal?.aborted) throw new AbortError();
9012
+ throwIfAborted(signal, () => new AbortError());
8980
9013
  const filteredModels = this.documentModelRegistry.getAllModules().filter((module) => !namespace || module.documentModel.global.id.startsWith(namespace));
8981
9014
  const startIndex = paging ? parseInt(paging.cursor) || 0 : 0;
8982
9015
  const limit = paging?.limit || filteredModels.length;
@@ -9012,24 +9045,24 @@ var Reactor = class {
9012
9045
  }
9013
9046
  async getOutgoingRelationships(sourceId, relationshipType, consistencyToken, signal) {
9014
9047
  const relationships = await this.documentIndexer.getOutgoing(sourceId, [relationshipType], void 0, consistencyToken, signal);
9015
- if (signal?.aborted) throw new AbortError();
9048
+ throwIfAborted(signal, () => new AbortError());
9016
9049
  return relationships.results.map((rel) => rel.targetId);
9017
9050
  }
9018
9051
  async getIncomingRelationships(targetId, relationshipType, consistencyToken, signal) {
9019
9052
  const relationships = await this.documentIndexer.getIncoming(targetId, [relationshipType], void 0, consistencyToken, signal);
9020
- if (signal?.aborted) throw new AbortError();
9053
+ throwIfAborted(signal, () => new AbortError());
9021
9054
  return relationships.results.map((rel) => rel.sourceId);
9022
9055
  }
9023
9056
  async getOperations(documentId, view, filter, paging, consistencyToken, signal) {
9024
9057
  this.logger.verbose("getOperations(@documentId, @view, @filter, @paging)", documentId, view, filter, paging);
9025
9058
  const branch = view?.branch || "main";
9026
9059
  const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
9027
- if (signal?.aborted) throw new AbortError();
9060
+ throwIfAborted(signal, () => new AbortError());
9028
9061
  const allScopes = Object.keys(revisions.revision);
9029
9062
  const result = {};
9030
9063
  for (const scope of allScopes) {
9031
9064
  if (!matchesScope(view, scope)) continue;
9032
- if (signal?.aborted) throw new AbortError();
9065
+ throwIfAborted(signal, () => new AbortError());
9033
9066
  const scopeResult = await this.operationStore.getSince(documentId, scope, branch, -1, filter, paging, signal);
9034
9067
  result[scope] = {
9035
9068
  results: scopeResult.results,
@@ -9060,13 +9093,13 @@ var Reactor = class {
9060
9093
  if (search.type) results = filterByType(results, search.type);
9061
9094
  } else if (search.type) results = await this.findByType(search.type, view, paging, consistencyToken, signal);
9062
9095
  else throw new Error("No search criteria provided");
9063
- if (signal?.aborted) throw new AbortError();
9096
+ throwIfAborted(signal, () => new AbortError());
9064
9097
  return results;
9065
9098
  }
9066
9099
  async create(document, signer, signal, meta) {
9067
9100
  this.logger.verbose("create(@id, @type, @slug)", document.header.id, document.header.documentType, document.header.slug);
9068
9101
  const createdAtUtcIso = (/* @__PURE__ */ new Date()).toISOString();
9069
- if (signal?.aborted) throw new AbortError();
9102
+ throwIfAborted(signal, () => new AbortError());
9070
9103
  let actions = [createDocumentAction({
9071
9104
  model: document.header.documentType,
9072
9105
  version: 0,
@@ -9126,7 +9159,7 @@ var Reactor = class {
9126
9159
  async deleteDocument(id, signer, signal, meta) {
9127
9160
  this.logger.verbose("deleteDocument(@id)", id);
9128
9161
  const createdAtUtcIso = (/* @__PURE__ */ new Date()).toISOString();
9129
- if (signal?.aborted) throw new AbortError();
9162
+ throwIfAborted(signal, () => new AbortError());
9130
9163
  let action = deleteDocumentAction(id);
9131
9164
  if (signer) action = await signAction(action, signer, signal);
9132
9165
  const jobId = v4();
@@ -9163,7 +9196,7 @@ var Reactor = class {
9163
9196
  }
9164
9197
  async execute(docId, branch, actions, signal, meta) {
9165
9198
  this.logger.verbose("execute(@docId, @branch, @actions)", docId, branch, actions);
9166
- if (signal?.aborted) throw new AbortError();
9199
+ throwIfAborted(signal, () => new AbortError());
9167
9200
  const createdAtUtcIso = (/* @__PURE__ */ new Date()).toISOString();
9168
9201
  const scope = getSharedActionScope(actions);
9169
9202
  const jobId = v4();
@@ -9196,12 +9229,12 @@ var Reactor = class {
9196
9229
  this.jobTracker.registerJob(jobInfo);
9197
9230
  this.emitJobPending(jobInfo.id, jobMeta);
9198
9231
  await this.queue.enqueue(job);
9199
- if (signal?.aborted) throw new AbortError();
9232
+ throwIfAborted(signal, () => new AbortError());
9200
9233
  return jobInfo;
9201
9234
  }
9202
9235
  async load(docId, branch, operations, signal, meta) {
9203
9236
  this.logger.verbose("load(@docId, @branch, @count, @operations)", docId, branch, operations.length, operations);
9204
- if (signal?.aborted) throw new AbortError();
9237
+ throwIfAborted(signal, () => new AbortError());
9205
9238
  if (operations.length === 0) throw new Error("load requires at least one operation");
9206
9239
  const scope = getSharedOperationScope(operations);
9207
9240
  const createdAtUtcIso = (/* @__PURE__ */ new Date()).toISOString();
@@ -9235,12 +9268,12 @@ var Reactor = class {
9235
9268
  this.jobTracker.registerJob(jobInfo);
9236
9269
  this.emitJobPending(jobInfo.id, jobMeta);
9237
9270
  await this.queue.enqueue(job);
9238
- if (signal?.aborted) throw new AbortError();
9271
+ throwIfAborted(signal, () => new AbortError());
9239
9272
  return jobInfo;
9240
9273
  }
9241
9274
  async executeBatch(request, signal, meta) {
9242
9275
  this.logger.verbose("executeBatch(@count jobs)", request.jobs.length);
9243
- if (signal?.aborted) throw new AbortError();
9276
+ throwIfAborted(signal, () => new AbortError());
9244
9277
  validateBatchRequest(request.jobs);
9245
9278
  for (const jobPlan of request.jobs) validateActionScopes(jobPlan);
9246
9279
  const createdAtUtcIso = (/* @__PURE__ */ new Date()).toISOString();
@@ -9274,7 +9307,7 @@ var Reactor = class {
9274
9307
  const enqueuedKeys = [];
9275
9308
  try {
9276
9309
  for (const key of sortedKeys) {
9277
- if (signal?.aborted) throw new AbortError();
9310
+ throwIfAborted(signal, () => new AbortError());
9278
9311
  const jobPlan = request.jobs.find((j) => j.key === key);
9279
9312
  const jobId = planKeyToJobId.get(key);
9280
9313
  const queueHint = jobPlan.dependsOn.map((depKey) => planKeyToJobId.get(depKey));
@@ -9309,7 +9342,7 @@ var Reactor = class {
9309
9342
  }
9310
9343
  async loadBatch(request, signal, meta) {
9311
9344
  this.logger.verbose("loadBatch(@count jobs)", request.jobs.length);
9312
- if (signal?.aborted) throw new AbortError();
9345
+ throwIfAborted(signal, () => new AbortError());
9313
9346
  validateBatchLoadRequest(request.jobs);
9314
9347
  for (const jobPlan of request.jobs) validateOperationScopes(jobPlan);
9315
9348
  const createdAtUtcIso = (/* @__PURE__ */ new Date()).toISOString();
@@ -9343,7 +9376,7 @@ var Reactor = class {
9343
9376
  const enqueuedKeys = [];
9344
9377
  try {
9345
9378
  for (const key of sortedKeys) {
9346
- if (signal?.aborted) throw new AbortError();
9379
+ throwIfAborted(signal, () => new AbortError());
9347
9380
  const jobPlan = request.jobs.find((j) => j.key === key);
9348
9381
  const jobId = planKeyToJobId.get(key);
9349
9382
  const queueHint = [...jobPlan.dependsOn.map((depKey) => planKeyToJobId.get(depKey)), ...jobPlan.externalDeps];
@@ -9378,21 +9411,21 @@ var Reactor = class {
9378
9411
  }
9379
9412
  async addRelationship(sourceId, targetId, relationshipType, branch = "main", signer, signal) {
9380
9413
  this.logger.verbose("addRelationship(@sourceId, @targetId, @relationshipType, @branch)", sourceId, targetId, relationshipType, branch);
9381
- if (signal?.aborted) throw new AbortError();
9414
+ throwIfAborted(signal, () => new AbortError());
9382
9415
  let actions = [addRelationshipAction(sourceId, targetId, relationshipType)];
9383
9416
  if (signer) actions = await signActions(actions, signer, signal);
9384
9417
  return await this.execute(sourceId, branch, actions, signal);
9385
9418
  }
9386
9419
  async removeRelationship(sourceId, targetId, relationshipType, branch = "main", signer, signal) {
9387
9420
  this.logger.verbose("removeRelationship(@sourceId, @targetId, @relationshipType, @branch)", sourceId, targetId, relationshipType, branch);
9388
- if (signal?.aborted) throw new AbortError();
9421
+ throwIfAborted(signal, () => new AbortError());
9389
9422
  let actions = [removeRelationshipAction(sourceId, targetId, relationshipType)];
9390
9423
  if (signer) actions = await signActions(actions, signer, signal);
9391
9424
  return await this.execute(sourceId, branch, actions, signal);
9392
9425
  }
9393
9426
  getJobStatus(jobId, signal) {
9394
9427
  this.logger.verbose("getJobStatus(@jobId)", jobId);
9395
- if (signal?.aborted) throw new AbortError();
9428
+ throwIfAborted(signal, () => new AbortError());
9396
9429
  const jobInfo = this.jobTracker.getJobStatus(jobId);
9397
9430
  if (!jobInfo) {
9398
9431
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -9890,13 +9923,15 @@ const JobExecutorEventTypes = {
9890
9923
  EXECUTOR_STOPPED: 20004
9891
9924
  };
9892
9925
  //#endregion
9893
- //#region src/admin/document-integrity-service.ts
9894
- const nullKeyframeStore = {
9926
+ //#region src/admin/passthrough-keyframe-store.ts
9927
+ const passthroughKeyframeStore = {
9895
9928
  putKeyframe: () => Promise.resolve(),
9896
9929
  findNearestKeyframe: () => Promise.resolve(void 0),
9897
9930
  listKeyframes: () => Promise.resolve([]),
9898
9931
  deleteKeyframes: () => Promise.resolve(0)
9899
9932
  };
9933
+ //#endregion
9934
+ //#region src/admin/document-integrity-service.ts
9900
9935
  var DocumentIntegrityService = class {
9901
9936
  keyframeStore;
9902
9937
  operationStore;
@@ -9913,14 +9948,14 @@ var DocumentIntegrityService = class {
9913
9948
  async validateDocument(documentId, branch = "main", signal) {
9914
9949
  const keyframeIssues = [];
9915
9950
  const snapshotIssues = [];
9916
- const replayCache = new KyselyWriteCache(nullKeyframeStore, this.operationStore, this.documentModelRegistry, {
9951
+ const replayCache = new KyselyWriteCache(passthroughKeyframeStore, this.operationStore, this.documentModelRegistry, {
9917
9952
  maxDocuments: 1,
9918
9953
  ringBufferSize: 1,
9919
9954
  keyframeInterval: Number.MAX_SAFE_INTEGER
9920
9955
  });
9921
9956
  const keyframes = await this.keyframeStore.listKeyframes(documentId, void 0, branch, signal);
9922
9957
  for (const keyframe of keyframes) {
9923
- if (signal?.aborted) throw new Error("Operation aborted");
9958
+ throwIfAborted(signal);
9924
9959
  replayCache.invalidate(documentId, keyframe.scope, branch);
9925
9960
  const replayedDoc = await replayCache.getState(documentId, keyframe.scope, branch, keyframe.revision, signal);
9926
9961
  const kfHash = hashDocumentStateForScope(keyframe.document, keyframe.scope);
@@ -9953,7 +9988,7 @@ var DocumentIntegrityService = class {
9953
9988
  try {
9954
9989
  replayedDoc = await replayCache.getState(documentId, scope, branch, void 0, signal);
9955
9990
  } catch {
9956
- if (signal?.aborted) throw new Error("Operation aborted");
9991
+ throwIfAborted(signal);
9957
9992
  continue;
9958
9993
  }
9959
9994
  const snapshotHash = hashDocumentStateForScope(currentDoc, scope);
@@ -9982,7 +10017,7 @@ var DocumentIntegrityService = class {
9982
10017
  async rebuildSnapshots(documentId, branch = "main", signal) {
9983
10018
  const scopes = await this.discoverScopes(documentId, branch, signal);
9984
10019
  for (const scope of scopes) {
9985
- if (signal?.aborted) throw new Error("Operation aborted");
10020
+ throwIfAborted(signal);
9986
10021
  this.writeCache.invalidate(documentId, scope, branch);
9987
10022
  }
9988
10023
  return {