@powerhousedao/reactor 6.2.0-rc.8 → 6.2.1-dev.0

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
@@ -1,13 +1,13 @@
1
- import { A as parsePagingOptions, C as DuplicateManifestError, D as DocumentDeletedError, E as ModuleNotFoundError, O as DocumentNotFoundError, S as CollectionMembershipCache, T as InvalidModuleError, _ as KyselyWriteCache, a as createForwardingPoolInstrumentation, b as createConsistencyToken, c as DuplicateOperationError, d as KyselyKeyframeStore, f as DocumentModelRegistry, g as EventBus, h as KyselyExecutionScope, i as runMigrations, j as throwIfAborted, k as matchesScope, l as OptimisticLockError, m as DriveCollectionId, n as REACTOR_SCHEMA, o as instrumentPgPool, p as SimpleJobExecutor, r as getMigrationStatus, s as KyselyOperationStore, t as DEFAULT_DRIVE_CONTAINER_TYPES, u as RevisionMismatchError, v as KyselyOperationIndex, w as DuplicateModuleError, x as createEmptyConsistencyToken, y as DocumentMetaCache } from "./drive-container-types-BxnXaOAp.js";
2
- import { n as ReactorEventTypes, t as EventBusAggregateError } from "./types-CxSpmNGK.js";
1
+ import { A as parsePagingOptions, C as DuplicateManifestError, D as DocumentDeletedError, E as ModuleNotFoundError, O as DocumentNotFoundError, S as CollectionMembershipCache, T as InvalidModuleError, _ as KyselyWriteCache, a as createForwardingPoolInstrumentation, b as createConsistencyToken, c as DuplicateOperationError, d as KyselyKeyframeStore, f as DocumentModelRegistry, g as EventBus, h as KyselyExecutionScope, i as runMigrations, j as throwIfAborted, k as matchesScope, l as OptimisticLockError, m as DriveCollectionId, n as REACTOR_SCHEMA, o as instrumentPgPool, p as SimpleJobExecutor, r as getMigrationStatus, s as KyselyOperationStore, t as DEFAULT_DRIVE_CONTAINER_TYPES, u as RevisionMismatchError, v as KyselyOperationIndex, w as DuplicateModuleError, x as createEmptyConsistencyToken, y as DocumentMetaCache } from "./drive-container-types-BuFNr0j0.js";
2
+ import { n as ReactorEventTypes, t as EventBusAggregateError } from "./types-DMKLa0Ok.js";
3
3
  import { i as WorkerInitFailedError, r as WorkerExitedError, t as WorkerAbortTimeoutError } from "./errors-D3S6Eysd.js";
4
- import { a as ReadModelCoordinator, i as KyselyDocumentView, n as ConsistencyTracker, o as BaseReadModel, r as makeConsistencyKey, t as KyselyDocumentIndexer } from "./document-indexer-DLEyUQxU.js";
4
+ import { a as ReadModelCoordinator, i as KyselyDocumentView, n as ConsistencyTracker, o as BaseReadModel, r as makeConsistencyKey, t as KyselyDocumentIndexer } from "./document-indexer-B6iippzG.js";
5
5
  import { n as errorToInfo, r as sanitizeArg, t as createForwardingLogger } from "./forwarding-logger-BBkMSxuJ.js";
6
- import { t as workerEntryPath } from "./worker-XYrQaEmt.js";
6
+ import { t as workerEntryPath } from "./worker-Dzrw1YMV.js";
7
7
  import { actions, actions as documentActions, createPresignedHeader, generateId, hashDocumentStateForScope, replayDocument } from "@powerhousedao/shared/document-model";
8
8
  import { addFile, addFolder, copyNode, deleteNode, driveCreateDocument, generateNodesCopy, getDescendants, handleTargetNameCollisions, isFileNode, isFolderNode, moveNode, updateNode } from "@powerhousedao/shared/document-drive";
9
9
  import { v4 } from "uuid";
10
- import { ConsoleLogger } from "document-model";
10
+ import { ConsoleLogger, childLogger } from "document-model";
11
11
  import { sql } from "kysely";
12
12
  import { RelationalDbProcessor, createRelationalDb } from "@powerhousedao/shared/processors";
13
13
  //#region src/actions/index.ts
@@ -2696,6 +2696,8 @@ var DocumentModelResolver = class {
2696
2696
  loadingModels = /* @__PURE__ */ new Map();
2697
2697
  failedModelTypes = /* @__PURE__ */ new Set();
2698
2698
  broadcastHook = null;
2699
+ modelLoadedHook = null;
2700
+ logger = childLogger(["reactor", "document-model-resolver"]);
2699
2701
  constructor(registry, loader) {
2700
2702
  this.registry = registry;
2701
2703
  this.loader = loader;
@@ -2708,6 +2710,13 @@ var DocumentModelResolver = class {
2708
2710
  setBroadcastHook(hook) {
2709
2711
  this.broadcastHook = hook;
2710
2712
  }
2713
+ /**
2714
+ * Post-success hook called with the document type after a model loads, so
2715
+ * peers (tabs) can load the same type via the event bus.
2716
+ */
2717
+ setModelLoadedHook(hook) {
2718
+ this.modelLoadedHook = hook;
2719
+ }
2711
2720
  async ensureModelLoaded(documentType) {
2712
2721
  try {
2713
2722
  this.registry.getModule(documentType);
@@ -2720,20 +2729,32 @@ var DocumentModelResolver = class {
2720
2729
  if (existing) return existing;
2721
2730
  const loadPromise = (async () => {
2722
2731
  try {
2723
- const module = await this.loader.load(documentType);
2724
- const [result] = this.registry.registerModules(module);
2725
- if (result.status === "error" && !DuplicateModuleError.isError(result.error)) throw result.error;
2726
- await this.broadcastIfPossible(documentType);
2732
+ await this.loadRegisterAndBroadcast(documentType);
2727
2733
  } catch (error) {
2728
2734
  this.failedModelTypes.add(documentType);
2729
2735
  throw error;
2730
2736
  } finally {
2731
2737
  this.loadingModels.delete(documentType);
2732
2738
  }
2739
+ await this.notifyPeersModelLoaded(documentType);
2733
2740
  })();
2734
2741
  this.loadingModels.set(documentType, loadPromise);
2735
2742
  return loadPromise;
2736
2743
  }
2744
+ async loadRegisterAndBroadcast(documentType) {
2745
+ const module = await this.loader.load(documentType);
2746
+ const [result] = this.registry.registerModules(module);
2747
+ if (result.status === "error" && !DuplicateModuleError.isError(result.error)) throw result.error;
2748
+ await this.broadcastIfPossible(documentType);
2749
+ }
2750
+ async notifyPeersModelLoaded(documentType) {
2751
+ if (!this.modelLoadedHook) return;
2752
+ try {
2753
+ await this.modelLoadedHook(documentType);
2754
+ } catch (error) {
2755
+ this.logger.warn(`MODEL_LOADED hook failed: ${documentType}`, error);
2756
+ }
2757
+ }
2737
2758
  async broadcastIfPossible(documentType) {
2738
2759
  if (!this.broadcastHook || !this.loader.resolveSpec) return;
2739
2760
  const entry = await this.loader.resolveSpec(documentType);
@@ -4909,6 +4930,11 @@ function rowToRemoteRecord(row) {
4909
4930
  }
4910
4931
  };
4911
4932
  }
4933
+ /**
4934
+ * Maps a RemoteRecord to a row. jsonb array columns are JSON-encoded because the
4935
+ * pg driver would otherwise emit a Postgres array literal ("{a,b}"), which jsonb
4936
+ * rejects (matches sync-dead-letter-storage).
4937
+ */
4912
4938
  function remoteRecordToRow(remote) {
4913
4939
  return {
4914
4940
  name: remote.name,
@@ -5355,11 +5381,14 @@ var SyncManager = class {
5355
5381
  for (const record of remoteRecords) {
5356
5382
  const channel = this.channelFactory.instance(record.id, record.name, record.channelConfig, this.cursorStorage, record.collectionId, record.filter, this.operationIndex, record.options);
5357
5383
  const remote = {
5358
- id: record.id,
5359
- name: record.name,
5360
- collectionId: record.collectionId,
5361
- filter: record.filter,
5362
- options: record.options,
5384
+ meta: {
5385
+ id: record.id,
5386
+ name: record.name,
5387
+ collectionId: record.collectionId,
5388
+ channelConfig: record.channelConfig,
5389
+ filter: record.filter,
5390
+ options: record.options
5391
+ },
5363
5392
  channel
5364
5393
  };
5365
5394
  this.remotes.set(record.name, remote);
@@ -5378,7 +5407,7 @@ var SyncManager = class {
5378
5407
  this.backfillAbortControllers.set(record.name, backfillController);
5379
5408
  this.updateOutbox(remote, outboxAckOrdinal, OutboxMode.Backfill, backfillController.signal).catch((error) => {
5380
5409
  if (backfillController.signal.aborted) return;
5381
- this.logger.error("Backfill failed for remote @RemoteName: @Error", remote.name, error instanceof Error ? error : new Error(String(error)));
5410
+ this.logger.error("Backfill failed for remote @RemoteName: @Error", remote.meta.name, error instanceof Error ? error : new Error(String(error)));
5382
5411
  }).finally(() => {
5383
5412
  this.backfillAbortControllers.delete(record.name);
5384
5413
  });
@@ -5422,7 +5451,7 @@ var SyncManager = class {
5422
5451
  return remote;
5423
5452
  }
5424
5453
  getById(id) {
5425
- for (const remote of this.remotes.values()) if (remote.id === id) return remote;
5454
+ for (const remote of this.remotes.values()) if (remote.meta.id === id) return remote;
5426
5455
  throw new Error(`Remote with id '${id}' does not exist`);
5427
5456
  }
5428
5457
  async add(name, collectionId, channelConfig, filter = {
@@ -5449,11 +5478,14 @@ var SyncManager = class {
5449
5478
  await this.remoteStorage.upsert(remoteRecord);
5450
5479
  const channel = this.channelFactory.instance(remoteId, name, channelConfig, this.cursorStorage, collectionId, filter, this.operationIndex, options);
5451
5480
  const remote = {
5452
- id: remoteId,
5453
- name,
5454
- collectionId,
5455
- filter,
5456
- options,
5481
+ meta: {
5482
+ id: remoteId,
5483
+ name,
5484
+ collectionId,
5485
+ channelConfig,
5486
+ filter,
5487
+ options
5488
+ },
5457
5489
  channel
5458
5490
  };
5459
5491
  this.remotes.set(name, remote);
@@ -5470,7 +5502,7 @@ var SyncManager = class {
5470
5502
  this.backfillAbortControllers.set(name, backfillController);
5471
5503
  this.updateOutbox(remote, 0, OutboxMode.Backfill, backfillController.signal).catch((error) => {
5472
5504
  if (backfillController.signal.aborted) return;
5473
- this.logger.error("Backfill failed for remote @RemoteName: @Error", remote.name, error instanceof Error ? error : new Error(String(error)));
5505
+ this.logger.error("Backfill failed for remote @RemoteName: @Error", remote.meta.name, error instanceof Error ? error : new Error(String(error)));
5474
5506
  }).finally(() => {
5475
5507
  this.backfillAbortControllers.delete(name);
5476
5508
  });
@@ -5521,20 +5553,20 @@ var SyncManager = class {
5521
5553
  }
5522
5554
  wireChannelCallbacks(remote) {
5523
5555
  remote.channel.inbox.onAdded((syncOps) => this.handleInboxAdded(remote, syncOps));
5524
- this.syncStatusTracker.trackRemote(remote.name, remote.channel);
5556
+ this.syncStatusTracker.trackRemote(remote.meta.name, remote.channel);
5525
5557
  const unsubscribe = remote.channel.onConnectionStateChange((snapshot) => {
5526
5558
  this.eventBus.emit(SyncEventTypes.CONNECTION_STATE_CHANGED, {
5527
- remoteName: remote.name,
5528
- remoteId: remote.id,
5559
+ remoteName: remote.meta.name,
5560
+ remoteId: remote.meta.id,
5529
5561
  previous: snapshot.state,
5530
5562
  current: snapshot.state,
5531
5563
  snapshot
5532
5564
  }).catch(() => {});
5533
5565
  });
5534
- this.connectionStateUnsubscribes.set(remote.name, unsubscribe);
5566
+ this.connectionStateUnsubscribes.set(remote.meta.name, unsubscribe);
5535
5567
  remote.channel.deadLetter.onAdded((syncOps) => {
5536
5568
  for (const syncOp of syncOps) {
5537
- this.logger.error("Dead letter (@remote, @documentId, @jobId, @error, @dependencies)", remote.name, syncOp.documentId, syncOp.jobId, syncOp.error?.message ?? "unknown", syncOp.jobDependencies);
5569
+ this.logger.error("Dead letter (@remote, @documentId, @jobId, @error, @dependencies)", remote.meta.name, syncOp.documentId, syncOp.jobId, syncOp.error?.message ?? "unknown", syncOp.jobDependencies);
5538
5570
  this.quarantinedDocumentIds.add(syncOp.documentId);
5539
5571
  const record = {
5540
5572
  id: syncOp.id,
@@ -5570,12 +5602,12 @@ var SyncManager = class {
5570
5602
  async loadDeadLetters(remote) {
5571
5603
  let records;
5572
5604
  try {
5573
- records = (await this.deadLetterStorage.list(remote.name, {
5605
+ records = (await this.deadLetterStorage.list(remote.meta.name, {
5574
5606
  cursor: "0",
5575
5607
  limit: this.config.maxDeadLettersPerRemote
5576
5608
  })).results;
5577
5609
  } catch (error) {
5578
- this.logger.error("Failed to load dead letters for remote (@name, @error)", remote.name, error instanceof Error ? error.message : String(error));
5610
+ this.logger.error("Failed to load dead letters for remote (@name, @error)", remote.meta.name, error instanceof Error ? error.message : String(error));
5579
5611
  return;
5580
5612
  }
5581
5613
  if (records.length === 0) return;
@@ -5587,10 +5619,10 @@ var SyncManager = class {
5587
5619
  syncOps.push(syncOp);
5588
5620
  }
5589
5621
  remote.channel.deadLetter.add(...syncOps);
5590
- this.logger.debug("Loaded @count persisted dead letters for remote @name", records.length, remote.name);
5622
+ this.logger.debug("Loaded @count persisted dead letters for remote @name", records.length, remote.meta.name);
5591
5623
  }
5592
5624
  getRemotesForCollection(collectionId) {
5593
- return Array.from(this.remotes.values()).filter((remote) => remote.collectionId.key === collectionId);
5625
+ return Array.from(this.remotes.values()).filter((remote) => remote.meta.collectionId.key === collectionId);
5594
5626
  }
5595
5627
  async processCompleteBatch(batch) {
5596
5628
  if (this.isShutdown) return;
@@ -5636,11 +5668,11 @@ var SyncManager = class {
5636
5668
  const operations = syncOp.operations.map((op) => op.operation);
5637
5669
  let jobInfo;
5638
5670
  try {
5639
- jobInfo = await this.reactor.load(syncOp.documentId, syncOp.branch, operations, this.abortController.signal, { sourceRemote: remote.name });
5671
+ jobInfo = await this.reactor.load(syncOp.documentId, syncOp.branch, operations, this.abortController.signal, { sourceRemote: remote.meta.name });
5640
5672
  } catch (error) {
5641
5673
  if (this.isShutdown) return;
5642
5674
  const err = error instanceof Error ? error : new Error(String(error));
5643
- this.logger.error("Failed to load operations from inbox (@remote, @documentId, @error)", remote.name, syncOp.documentId, err.message);
5675
+ this.logger.error("Failed to load operations from inbox (@remote, @documentId, @error)", remote.meta.name, syncOp.documentId, err.message);
5644
5676
  const channelError = new ChannelError(ChannelErrorSource.Inbox, err);
5645
5677
  syncOp.failed(channelError);
5646
5678
  remote.channel.deadLetter.add(syncOp);
@@ -5653,7 +5685,7 @@ var SyncManager = class {
5653
5685
  } catch (error) {
5654
5686
  if (this.isShutdown) return;
5655
5687
  const err = error instanceof Error ? error : new Error(String(error));
5656
- this.logger.error("Failed to wait for job completion (@remote, @documentId, @jobId, @error)", remote.name, syncOp.documentId, jobInfo.id, err.message);
5688
+ this.logger.error("Failed to wait for job completion (@remote, @documentId, @jobId, @error)", remote.meta.name, syncOp.documentId, jobInfo.id, err.message);
5657
5689
  const channelError = new ChannelError(ChannelErrorSource.Inbox, err);
5658
5690
  syncOp.failed(channelError);
5659
5691
  remote.channel.deadLetter.add(syncOp);
@@ -5663,7 +5695,7 @@ var SyncManager = class {
5663
5695
  if (this.isShutdown) return;
5664
5696
  if (completedJobInfo.status === JobStatus.FAILED) {
5665
5697
  const errorMessage = completedJobInfo.error?.message || "Unknown error";
5666
- this.logger.error("Failed to apply operations from inbox (@remote, @documentId, @jobId, @error)", remote.name, syncOp.documentId, completedJobInfo.id, errorMessage);
5698
+ this.logger.error("Failed to apply operations from inbox (@remote, @documentId, @jobId, @error)", remote.meta.name, syncOp.documentId, completedJobInfo.id, errorMessage);
5667
5699
  const error = new ChannelError(ChannelErrorSource.Inbox, /* @__PURE__ */ new Error(`Failed to apply operations: ${errorMessage}`));
5668
5700
  syncOp.failed(error);
5669
5701
  remote.channel.deadLetter.add(syncOp);
@@ -5671,7 +5703,7 @@ var SyncManager = class {
5671
5703
  remote.channel.inbox.remove(syncOp);
5672
5704
  }
5673
5705
  async applyInboxBatch(items) {
5674
- const sourceRemote = items[0].remote.name;
5706
+ const sourceRemote = items[0].remote.meta.name;
5675
5707
  const chunkKeys = new Set(items.map(({ syncOp }) => syncOp.jobId));
5676
5708
  const jobs = items.map(({ syncOp }) => {
5677
5709
  const dependsOn = [];
@@ -5721,7 +5753,7 @@ var SyncManager = class {
5721
5753
  }
5722
5754
  for (const { remote, syncOp } of items) {
5723
5755
  if (!(syncOp.jobId in result.jobs)) {
5724
- this.logger.error("Job key missing from batch load result (@remote, @documentId, @jobId)", remote.name, syncOp.documentId, syncOp.jobId);
5756
+ this.logger.error("Job key missing from batch load result (@remote, @documentId, @jobId)", remote.meta.name, syncOp.documentId, syncOp.jobId);
5725
5757
  const error = new ChannelError(ChannelErrorSource.Inbox, /* @__PURE__ */ new Error(`Job key '${syncOp.jobId}' missing from batch load result`));
5726
5758
  syncOp.failed(error);
5727
5759
  remote.channel.deadLetter.add(syncOp);
@@ -5755,7 +5787,7 @@ var SyncManager = class {
5755
5787
  let maxOrdinal = ackOrdinal;
5756
5788
  const lastJobByDoc = /* @__PURE__ */ new Map();
5757
5789
  let prevChainJobId;
5758
- const sinceTimestamp = remote.options.sinceTimestampUtcMs;
5790
+ const sinceTimestamp = remote.meta.options.sinceTimestampUtcMs;
5759
5791
  const emitBatches = (operations) => {
5760
5792
  if (operations.length === 0) return;
5761
5793
  const batches = batchOperationsByDocument(operations);
@@ -5766,14 +5798,14 @@ var SyncManager = class {
5766
5798
  const deps = [];
5767
5799
  if (prevJobId) deps.push(prevJobId);
5768
5800
  if (mode === OutboxMode.BatchTriggered && prevChainJobId && prevChainJobId !== prevJobId) deps.push(prevChainJobId);
5769
- const syncOp = new SyncOperation(crypto.randomUUID(), jobId, deps, remote.name, batch.documentId, [batch.scope], batch.branch, batch.operations);
5801
+ const syncOp = new SyncOperation(crypto.randomUUID(), jobId, deps, remote.meta.name, batch.documentId, [batch.scope], batch.branch, batch.operations);
5770
5802
  syncOps.push(syncOp);
5771
5803
  lastJobByDoc.set(batch.documentId, jobId);
5772
5804
  if (mode === OutboxMode.BatchTriggered) prevChainJobId = jobId;
5773
5805
  }
5774
5806
  remote.channel.outbox.add(...syncOps);
5775
5807
  };
5776
- let page = await this.operationIndex.find(remote.collectionId.key, ackOrdinal, { excludeSourceRemote: remote.name }, void 0, composedSignal);
5808
+ let page = await this.operationIndex.find(remote.meta.collectionId.key, ackOrdinal, { excludeSourceRemote: remote.meta.name }, void 0, composedSignal);
5777
5809
  let carry = [];
5778
5810
  let hasMore;
5779
5811
  do {
@@ -5785,7 +5817,7 @@ var SyncManager = class {
5785
5817
  carry = [];
5786
5818
  }
5787
5819
  if (sinceTimestamp && sinceTimestamp !== "0") operations = operations.filter((op) => op.operation.timestampUtcMs >= sinceTimestamp);
5788
- operations = filterOperations(operations, remote.filter);
5820
+ operations = filterOperations(operations, remote.meta.filter);
5789
5821
  operations = operations.filter((op) => !this.quarantinedDocumentIds.has(op.context.documentId));
5790
5822
  hasMore = !!page.next;
5791
5823
  if (operations.length > 0) {
@@ -6715,6 +6747,7 @@ var ReactorBuilder = class {
6715
6747
  const keyframeStore = new KyselyKeyframeStore(database);
6716
6748
  const eventBus = this.eventBus || new EventBus();
6717
6749
  const resolver = this.documentModelLoader ? new DocumentModelResolver(documentModelRegistry, this.documentModelLoader) : new NullDocumentModelResolver(documentModelRegistry);
6750
+ if (resolver instanceof DocumentModelResolver) resolver.setModelLoadedHook((documentType) => eventBus.emit(ReactorEventTypes.MODEL_LOADED, { documentType }));
6718
6751
  const queue = this.queueInstance ?? new InMemoryQueue(eventBus, resolver);
6719
6752
  const jobTracker = new InMemoryJobTracker(eventBus);
6720
6753
  const writeCache = new KyselyWriteCache(keyframeStore, operationStore, documentModelRegistry, {
@@ -6830,7 +6863,7 @@ var ReactorBuilder = class {
6830
6863
  poolInstrumentations.push(forwarder);
6831
6864
  this.instrumentedPools.push(forwarder);
6832
6865
  }
6833
- const { ProjectionShardManager } = await import("./projection-shard-manager-_c7orNo5.js");
6866
+ const { ProjectionShardManager } = await import("./projection-shard-manager-BnQKFqQd.js");
6834
6867
  const manager = new ProjectionShardManager({
6835
6868
  shardCount: config.shardCount,
6836
6869
  db,
@@ -6863,7 +6896,7 @@ var ReactorBuilder = class {
6863
6896
  const [{ WorkerHandle }, { createThreadTransport }, { workerEntryPath }] = await Promise.all([
6864
6897
  import("./worker-handle-B1w03nRA.js"),
6865
6898
  import("./transport-ByGviWdZ.js"),
6866
- import("./worker-XYrQaEmt.js").then((n) => n.n)
6899
+ import("./worker-Dzrw1YMV.js").then((n) => n.n)
6867
6900
  ]);
6868
6901
  const db = this.workerDbConfig;
6869
6902
  const signatureVerifier = this.workerSignatureVerifierSpec;