@powerhousedao/reactor 6.2.2-dev.3 → 6.2.2-dev.31

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,10 +1,10 @@
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-DpJp2AmE.js";
1
+ import { A as AuthorizationDeniedError, C as DuplicateOperationError, D as DuplicateModuleError, E as DuplicateManifestError, F as throwIfAborted, M as DocumentNotFoundError, N as matchesScope, O as InvalidModuleError, P as parsePagingOptions, S as AppendConditionFailedError, T as RevisionMismatchError, _ as DocumentMetaCache, a as createForwardingPoolInstrumentation, b as CollectionMembershipCache, c as KyselyKeyframeStore, d as DriveCollectionId, f as buildDecisionModel, g as KyselyOperationIndex, h as KyselyWriteCache, i as runMigrations, j as DocumentDeletedError, k as ModuleNotFoundError, l as DocumentModelRegistry, m as EventBus, n as REACTOR_SCHEMA, o as instrumentPgPool, p as KyselyExecutionScope, r as getMigrationStatus, s as KyselyOperationStore, t as DEFAULT_DRIVE_CONTAINER_TYPES, u as SimpleJobExecutor, v as createConsistencyToken, w as OptimisticLockError, x as APPEND_CONDITION_FAILED_PREFIX, y as createEmptyConsistencyToken } from "./drive-container-types-BNwEHEXP.js";
2
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-FGJmRAdX.js";
4
+ import { a as ReadModelCoordinator, i as KyselyDocumentView, n as ConsistencyTracker, o as BaseReadModel, r as makeConsistencyKey, t as KyselyDocumentIndexer } from "./document-indexer-BY-mPXJF.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-DBJOv8Gp.js";
7
- import { actions, actions as documentActions, createPresignedHeader, generateId, hashDocumentStateForScope, replayDocument } from "@powerhousedao/shared/document-model";
6
+ import { t as workerEntryPath } from "./worker-DNfxv5Ir.js";
7
+ import { actions, actions as documentActions, createPresignedHeader, decide, 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
10
  import { ConsoleLogger, childLogger } from "document-model";
@@ -680,6 +680,39 @@ let DocumentChangeType = /* @__PURE__ */ function(DocumentChangeType) {
680
680
  return DocumentChangeType;
681
681
  }({});
682
682
  //#endregion
683
+ //#region src/client/util.ts
684
+ const ALWAYS_READABLE_SCOPES = new Set(["auth", "document"]);
685
+ function authSubjectFromSigner(signer) {
686
+ return {
687
+ address: signer.user?.address,
688
+ key: signer.app?.key
689
+ };
690
+ }
691
+ function canReadScope(auth, subject, scope) {
692
+ return ALWAYS_READABLE_SCOPES.has(scope) || decide(auth, subject, {
693
+ verb: "read",
694
+ scope
695
+ }) === "allow";
696
+ }
697
+ function withAuthScope(view) {
698
+ if (view?.scopes && view.scopes.length > 0) return {
699
+ ...view,
700
+ scopes: [...new Set([...view.scopes, "auth"])]
701
+ };
702
+ return view;
703
+ }
704
+ function filterReadableScopes(document, subject) {
705
+ const state = document.state;
706
+ if (!state) return document;
707
+ const auth = document.state.auth;
708
+ const filtered = {};
709
+ for (const scope of Object.keys(state)) if (canReadScope(auth, subject, scope)) filtered[scope] = state[scope];
710
+ return {
711
+ ...document,
712
+ state: filtered
713
+ };
714
+ }
715
+ //#endregion
683
716
  //#region src/client/reactor-client.ts
684
717
  /**
685
718
  * ReactorClient implementation that wraps lower-level APIs to provide
@@ -711,6 +744,9 @@ var ReactorClient = class {
711
744
  this.drives = new DriveClient(this, logger, reactor, signer);
712
745
  this.logger.verbose("ReactorClient initialized");
713
746
  }
747
+ readSubject(subject) {
748
+ return subject ?? authSubjectFromSigner(this.signer);
749
+ }
714
750
  /**
715
751
  * Retrieves a list of document model modules.
716
752
  */
@@ -734,7 +770,7 @@ var ReactorClient = class {
734
770
  */
735
771
  async get(identifier, view, signal) {
736
772
  this.logger.verbose("get(@identifier, @view)", identifier, view);
737
- return await this.reactor.getByIdOrSlug(identifier, view, void 0, signal);
773
+ return filterReadableScopes(await this.reactor.getByIdOrSlug(identifier, withAuthScope(view), void 0, signal), this.readSubject(view?.subject));
738
774
  }
739
775
  /**
740
776
  * Resolves an identifier (id or slug) to the canonical document id, using the
@@ -750,8 +786,15 @@ var ReactorClient = class {
750
786
  async getOperations(documentIdentifier, view, filter, paging, signal) {
751
787
  this.logger.verbose("getOperations(@documentIdentifier, @view, @filter, @paging)", documentIdentifier, view, filter, paging);
752
788
  const documentId = await this.documentView.resolveIdOrSlug(documentIdentifier, view, void 0, signal);
753
- if (paging?.cursor && isCompositeCursor(paging.cursor)) return this.getOperationsWithCompositeCursor(documentId, view, filter, paging, signal);
789
+ const authDoc = await this.reactor.getByIdOrSlug(documentId, {
790
+ scopes: ["auth"],
791
+ branch: view?.branch
792
+ }, void 0, signal);
793
+ const subject = this.readSubject(view?.subject);
794
+ const canRead = (scope) => canReadScope(authDoc?.state.auth, subject, scope);
795
+ if (paging?.cursor && isCompositeCursor(paging.cursor)) return this.getOperationsWithCompositeCursor(documentId, view, filter, paging, signal, canRead);
754
796
  const operationsByScope = await this.reactor.getOperations(documentId, view, filter, paging, void 0, signal);
797
+ for (const scope of Object.keys(operationsByScope)) if (!canRead(scope)) delete operationsByScope[scope];
755
798
  const scopeEntries = Object.entries(operationsByScope);
756
799
  const effectivePaging = paging || {
757
800
  cursor: "0",
@@ -779,11 +822,12 @@ var ReactorClient = class {
779
822
  nextCursor: Object.keys(activeCursors).length > 0 ? encodeCompositeCursor(activeCursors) : void 0
780
823
  };
781
824
  }
782
- async getOperationsWithCompositeCursor(documentId, view, filter, paging, signal) {
825
+ async getOperationsWithCompositeCursor(documentId, view, filter, paging, signal, canRead) {
783
826
  const scopeCursors = decodeCompositeCursor(paging.cursor);
784
827
  const allOperations = [];
785
828
  const activeCursors = {};
786
829
  for (const [scopeName, cursor] of Object.entries(scopeCursors)) {
830
+ if (!canRead(scopeName)) continue;
787
831
  const scopeView = {
788
832
  ...view,
789
833
  scopes: [scopeName]
@@ -817,7 +861,7 @@ var ReactorClient = class {
817
861
  limit: 0
818
862
  }
819
863
  };
820
- return this.reactor.find({ ids: targetIds }, view, paging, void 0, signal);
864
+ return this.find({ ids: targetIds }, view, paging, signal);
821
865
  }
822
866
  /**
823
867
  * Retrieves incoming relationships of a given type to a target document.
@@ -833,14 +877,19 @@ var ReactorClient = class {
833
877
  limit: 0
834
878
  }
835
879
  };
836
- return this.reactor.find({ ids: sourceIds }, view, paging, void 0, signal);
880
+ return this.find({ ids: sourceIds }, view, paging, signal);
837
881
  }
838
882
  /**
839
883
  * Filters documents by criteria and returns a list of them
840
884
  */
841
885
  async find(search, view, paging, signal) {
842
886
  this.logger.verbose("find(@search, @view, @paging)", search, view, paging);
843
- return this.reactor.find(search, view, paging, void 0, signal);
887
+ const results = await this.reactor.find(search, withAuthScope(view), paging, void 0, signal);
888
+ const readSubject = this.readSubject(view?.subject);
889
+ return {
890
+ ...results,
891
+ results: results.results.map((doc) => filterReadableScopes(doc, readSubject))
892
+ };
844
893
  }
845
894
  /**
846
895
  * Creates a document and waits for completion
@@ -1144,11 +1193,25 @@ let JobQueueState = /* @__PURE__ */ function(JobQueueState) {
1144
1193
  return JobQueueState;
1145
1194
  }({});
1146
1195
  /**
1196
+ * How a retry is accounted against the job's retry limit.
1197
+ * - `CountAgainstLimit` (default): a fault; the job eventually exhausts its
1198
+ * retries and fails terminally.
1199
+ * - `ExemptFromLimit`: not a fault, so the attempt is not charged to the job.
1200
+ * Used for concurrency conflicts, where the retry does new work.
1201
+ */
1202
+ let RetryAccounting = /* @__PURE__ */ function(RetryAccounting) {
1203
+ RetryAccounting["CountAgainstLimit"] = "count-against-limit";
1204
+ RetryAccounting["ExemptFromLimit"] = "exempt-from-limit";
1205
+ return RetryAccounting;
1206
+ }({});
1207
+ /**
1147
1208
  * Event types for the queue system
1148
1209
  */
1149
1210
  const QueueEventTypes = { JOB_AVAILABLE: 1e4 };
1150
1211
  //#endregion
1151
1212
  //#region src/executor/job-result-handler.ts
1213
+ /** Conflict retries a job may take without charging its retry limit. */
1214
+ const MAX_EXEMPT_CONFLICT_RETRIES = 20;
1152
1215
  function toErrorInfo(error) {
1153
1216
  if (error instanceof Error) return {
1154
1217
  message: error.message,
@@ -1187,12 +1250,19 @@ var JobResultHandler = class {
1187
1250
  } catch {}
1188
1251
  }
1189
1252
  }
1253
+ if (result.error && AppendConditionFailedError.isError(result.error) && this.countConflicts(handle.job) < MAX_EXEMPT_CONFLICT_RETRIES) {
1254
+ const errorInfo = toErrorInfo(result.error);
1255
+ try {
1256
+ await this.queue.retryJob(handle.job.id, errorInfo, RetryAccounting.ExemptFromLimit);
1257
+ return;
1258
+ } catch {}
1259
+ }
1190
1260
  if (result.error && DocumentNotFoundError.isError(result.error)) {
1191
1261
  handle.defer();
1192
1262
  callbacks.deferJob(handle.job.documentId, handle.job);
1193
1263
  return;
1194
1264
  }
1195
- if (result.error && DocumentDeletedError.isError(result.error)) {
1265
+ if (result.error && (DocumentDeletedError.isError(result.error) || AuthorizationDeniedError.isError(result.error))) {
1196
1266
  const errorInfo = toErrorInfo(result.error);
1197
1267
  this.jobTracker.markFailed(handle.job.id, errorInfo, handle.job);
1198
1268
  this.eventBus.emit(ReactorEventTypes.JOB_FAILED, {
@@ -1230,6 +1300,12 @@ var JobResultHandler = class {
1230
1300
  handle.fail(fullErrorInfo);
1231
1301
  }
1232
1302
  }
1303
+ /** How many times this job has already lost an append-condition race. */
1304
+ countConflicts(job) {
1305
+ let conflicts = 0;
1306
+ for (const error of job.errorHistory) if (AppendConditionFailedError.isFailureMessage(error.message)) conflicts++;
1307
+ return conflicts;
1308
+ }
1233
1309
  hasCreateDocumentAction(job) {
1234
1310
  for (const action of job.actions) if (action.type === "CREATE_DOCUMENT") return true;
1235
1311
  for (const operation of job.operations) if (operation.action.type === "CREATE_DOCUMENT") return true;
@@ -2587,7 +2663,7 @@ var InMemoryQueue = class {
2587
2663
  if (documentId) this.markJobComplete(jobId, documentId);
2588
2664
  this.jobIndex.delete(jobId);
2589
2665
  }
2590
- async retryJob(jobId, error) {
2666
+ async retryJob(jobId, error, accounting = RetryAccounting.CountAgainstLimit) {
2591
2667
  const job = this.jobIndex.get(jobId);
2592
2668
  if (!job) return;
2593
2669
  job.lastError = error;
@@ -2596,9 +2672,10 @@ var InMemoryQueue = class {
2596
2672
  this.jobIndex.delete(jobId);
2597
2673
  this.jobIdToQueueKey.delete(jobId);
2598
2674
  if (error) job.errorHistory.push(error);
2675
+ const retryCount = job.retryCount || 0;
2599
2676
  const updatedJob = {
2600
2677
  ...job,
2601
- retryCount: (job.retryCount || 0) + 1,
2678
+ retryCount: accounting === RetryAccounting.CountAgainstLimit ? retryCount + 1 : retryCount,
2602
2679
  lastError: error
2603
2680
  };
2604
2681
  await this.enqueue(updatedJob);
@@ -2686,6 +2763,98 @@ var InMemoryQueue = class {
2686
2763
  }
2687
2764
  };
2688
2765
  //#endregion
2766
+ //#region src/core/model-sources.ts
2767
+ function modelSourceKey(module) {
2768
+ return `${module.documentModel.global.id}@${module.version ?? 1}`;
2769
+ }
2770
+ function isDocumentModelModule(value) {
2771
+ if (typeof value !== "object" || value === null) return false;
2772
+ const candidate = value;
2773
+ return typeof candidate.reducer === "function" && typeof candidate.documentModel?.global?.id === "string";
2774
+ }
2775
+ /**
2776
+ * Resolves every source to live modules by importing file/package sources
2777
+ * host-side and scanning their exports, so the host registry and the worker
2778
+ * manifest are derived from one list and cannot diverge.
2779
+ */
2780
+ async function resolveModelSources(sources) {
2781
+ const resolved = /* @__PURE__ */ new Map();
2782
+ for (const source of sources) {
2783
+ if (isDocumentModelModule(source)) {
2784
+ addEntry(resolved, {
2785
+ module: source,
2786
+ spec: null
2787
+ });
2788
+ continue;
2789
+ }
2790
+ for (const entry of await resolveImportableSource(source)) addEntry(resolved, entry);
2791
+ }
2792
+ const modules = [];
2793
+ const manifest = [];
2794
+ const moduleOnlyKeys = [];
2795
+ for (const [key, entry] of resolved) {
2796
+ modules.push(entry.module);
2797
+ if (entry.spec) manifest.push({
2798
+ documentType: entry.module.documentModel.global.id,
2799
+ version: String(entry.module.version ?? 1),
2800
+ spec: entry.spec
2801
+ });
2802
+ else moduleOnlyKeys.push(key);
2803
+ }
2804
+ return {
2805
+ modules,
2806
+ manifest,
2807
+ moduleOnlyKeys
2808
+ };
2809
+ }
2810
+ function addEntry(map, entry) {
2811
+ const key = modelSourceKey(entry.module);
2812
+ const existing = map.get(key);
2813
+ if (!existing) {
2814
+ map.set(key, entry);
2815
+ return;
2816
+ }
2817
+ if (!existing.spec && entry.spec) existing.spec = entry.spec;
2818
+ }
2819
+ async function resolveImportableSource(source) {
2820
+ const isFile = "filePath" in source;
2821
+ const specifier = isFile ? new URL(`file://${source.filePath}`).href : source.subpath ? `${source.packageName}/${source.subpath}` : source.packageName;
2822
+ const moduleRef = isFile ? { filePath: source.filePath } : { packageName: specifier };
2823
+ let moduleNs;
2824
+ try {
2825
+ moduleNs = await import(
2826
+ /* @vite-ignore */
2827
+ specifier
2828
+ );
2829
+ } catch (error) {
2830
+ throw new Error(`Failed to import document-model source "${describeSource(source)}"`, { cause: error });
2831
+ }
2832
+ if (source.exportName !== void 0) {
2833
+ const value = moduleNs[source.exportName];
2834
+ if (!isDocumentModelModule(value)) throw new Error(`Export "${source.exportName}" of document-model source "${describeSource(source)}" is not a DocumentModelModule`);
2835
+ return [{
2836
+ module: value,
2837
+ spec: { module: {
2838
+ ...moduleRef,
2839
+ exportName: source.exportName
2840
+ } }
2841
+ }];
2842
+ }
2843
+ const entries = [];
2844
+ for (const [exportName, value] of Object.entries(moduleNs)) if (isDocumentModelModule(value)) entries.push({
2845
+ module: value,
2846
+ spec: { module: {
2847
+ ...moduleRef,
2848
+ exportName
2849
+ } }
2850
+ });
2851
+ if (entries.length === 0) throw new Error(`Document-model source "${describeSource(source)}" has no DocumentModelModule exports`);
2852
+ return entries;
2853
+ }
2854
+ function describeSource(source) {
2855
+ return "filePath" in source ? source.filePath : source.subpath ? `${source.packageName}/${source.subpath}` : source.packageName;
2856
+ }
2857
+ //#endregion
2689
2858
  //#region src/registry/document-model-resolver.ts
2690
2859
  /**
2691
2860
  * Encapsulates the logic for resolving document model modules on demand.
@@ -2743,10 +2912,11 @@ var DocumentModelResolver = class {
2743
2912
  return loadPromise;
2744
2913
  }
2745
2914
  async loadRegisterAndBroadcast(documentType) {
2746
- const module = await this.loader.load(documentType);
2747
- const [result] = this.registry.registerModules(module);
2748
- if (result.status === "error" && !DuplicateModuleError.isError(result.error)) throw result.error;
2749
- await this.broadcastIfPossible(documentType);
2915
+ const resolved = await resolveModelSources([await this.loader.load(documentType)]);
2916
+ if (!resolved.modules.some((module) => module.documentModel.global.id === documentType)) throw new Error(`Loader source resolved no module for document type: ${documentType}`);
2917
+ const results = this.registry.registerModules(...resolved.modules);
2918
+ for (const result of results) if (result.status === "error" && !DuplicateModuleError.isError(result.error)) throw result.error;
2919
+ if (this.broadcastHook) for (const entry of resolved.manifest) await this.broadcastHook(entry);
2750
2920
  }
2751
2921
  async notifyPeersModelLoaded(documentType) {
2752
2922
  if (!this.modelLoadedHook) return;
@@ -2756,12 +2926,6 @@ var DocumentModelResolver = class {
2756
2926
  this.logger.warn(`MODEL_LOADED hook failed: ${documentType}`, error);
2757
2927
  }
2758
2928
  }
2759
- async broadcastIfPossible(documentType) {
2760
- if (!this.broadcastHook || !this.loader.resolveSpec) return;
2761
- const entry = await this.loader.resolveSpec(documentType);
2762
- if (!entry) return;
2763
- await this.broadcastHook(entry);
2764
- }
2765
2929
  };
2766
2930
  /**
2767
2931
  * No-op resolver used when no document model loader is configured.
@@ -3559,6 +3723,7 @@ function toOperationWithContext(entry) {
3559
3723
  skip: entry.skip,
3560
3724
  hash: entry.hash,
3561
3725
  timestampUtcMs: entry.timestampUtcMs,
3726
+ deniedReason: entry.deniedReason,
3562
3727
  action: entry.action
3563
3728
  },
3564
3729
  context: {
@@ -3886,6 +4051,7 @@ function serializeEnvelope(envelope) {
3886
4051
  hash: opWithContext.operation.hash,
3887
4052
  skip: opWithContext.operation.skip,
3888
4053
  error: opWithContext.operation.error,
4054
+ deniedReason: opWithContext.operation.deniedReason,
3889
4055
  id: opWithContext.operation.id,
3890
4056
  action: serializeAction(opWithContext.operation.action)
3891
4057
  },
@@ -6478,9 +6644,12 @@ var Reactor = class {
6478
6644
  };
6479
6645
  //#endregion
6480
6646
  //#region src/core/reactor-builder.ts
6647
+ function sameDatabaseTarget(a, b) {
6648
+ return a.host === b.host && a.port === b.port && a.database === b.database;
6649
+ }
6481
6650
  var ReactorBuilder = class {
6482
6651
  logger;
6483
- documentModels = [];
6652
+ documentModelSources = [];
6484
6653
  upgradeManifests = [];
6485
6654
  features = { legacyStorageEnabled: false };
6486
6655
  readModels = [];
@@ -6501,12 +6670,8 @@ var ReactorBuilder = class {
6501
6670
  documentModelLoader;
6502
6671
  shutdownHooks = [];
6503
6672
  driveContainerTypes = DEFAULT_DRIVE_CONTAINER_TYPES;
6504
- documentModelSpecs = [];
6505
- workerPoolConfig;
6673
+ workerPool;
6506
6674
  resolvedModelManifest;
6507
- workerDbConfig;
6508
- workerSignatureVerifierSpec;
6509
- workerFactory;
6510
6675
  projectionShardConfig;
6511
6676
  projectionWorkerFactory;
6512
6677
  instrumentedPools = [];
@@ -6514,8 +6679,15 @@ var ReactorBuilder = class {
6514
6679
  this.logger = logger;
6515
6680
  return this;
6516
6681
  }
6517
- withDocumentModels(models) {
6518
- this.documentModels = models;
6682
+ /**
6683
+ * Register document-model sources: live modules, importable files, or
6684
+ * importable packages. Appends across calls. At `buildModule()` every
6685
+ * source is resolved host-side and registered on the registry; file and
6686
+ * package sources additionally form the worker manifest when the worker
6687
+ * pool is enabled (live modules cannot cross a thread boundary).
6688
+ */
6689
+ withDocumentModelSources(sources) {
6690
+ this.documentModelSources.push(...sources);
6519
6691
  return this;
6520
6692
  }
6521
6693
  withUpgradeManifests(manifests) {
@@ -6630,58 +6802,30 @@ var ReactorBuilder = class {
6630
6802
  this.shutdownHooks.push(hook);
6631
6803
  return this;
6632
6804
  }
6633
- withDocumentModelSpecs(specs) {
6634
- this.documentModelSpecs = specs;
6635
- return this;
6636
- }
6637
- /**
6638
- * Stores the worker-pool configuration. When `config.enabled === true` the
6639
- * builder constructs a {@link WorkerPoolJobExecutorManager} in place of the
6640
- * in-process {@link SimpleJobExecutorManager}.
6641
- */
6642
- withWorkerPool(config) {
6643
- this.workerPoolConfig = config;
6644
- return this;
6645
- }
6646
- /**
6647
- * Postgres connection info forwarded to each worker so it can open its own
6648
- * pool. Required when `workerPool.enabled === true` unless a custom
6649
- * `withWorkerFactory` or `withExecutor` is provided.
6650
- */
6651
- withWorkerDbConfig(db) {
6652
- this.workerDbConfig = db;
6653
- return this;
6654
- }
6655
6805
  /**
6656
- * Factory spec the worker imports to instantiate its signature verifier.
6657
- * Required when `workerPool.enabled === true` unless a custom
6658
- * `withWorkerFactory` or `withExecutor` is provided.
6806
+ * Enable the executor worker pool: N `node:worker_threads` workers with
6807
+ * sticky per-document routing, replacing the in-process executor. Calling
6808
+ * this enables the pool — there is no `enabled` flag. Provide `db`
6809
+ * (each worker opens its own Postgres pool; the parent database is built
6810
+ * from it too unless {@link withKysely} is set) or a custom `factory`
6811
+ * transport. `verifier` is imported by the default transport's workers;
6812
+ * omitted = no executor-side signature verification.
6659
6813
  */
6660
- withWorkerSignatureVerifierSpec(spec) {
6661
- this.workerSignatureVerifierSpec = spec;
6662
- return this;
6663
- }
6664
- /**
6665
- * Inject a custom {@link WorkerFactory}. When set, the builder skips
6666
- * default thread-transport wiring and hands the factory directly to the
6667
- * pool manager. Use this in tests or to plug in a different transport
6668
- * (e.g. a child-process adapter).
6669
- */
6670
- withWorkerFactory(factory) {
6671
- this.workerFactory = factory;
6814
+ withWorkerPool(options) {
6815
+ this.workerPool = options;
6672
6816
  return this;
6673
6817
  }
6674
6818
  /**
6675
6819
  * Configure N sharded projection workers. When set, the builder replaces
6676
6820
  * the default in-process {@link ReadModelCoordinator} with a
6677
- * {@link ProjectionShardManager}. The same `DbConfig` registered via
6678
- * {@link withWorkerDbConfig} is reused for the projection workers'
6679
- * connection info; only the `poolSize` is overridden by
6680
- * {@link ProjectionShardBuilderConfig.poolSize}.
6821
+ * {@link ProjectionShardManager}.
6681
6822
  *
6682
- * Requires {@link withWorkerDbConfig} (the projection workers need
6683
- * connection info to open their own pools). The same model manifest
6684
- * resolved from {@link withDocumentModelSpecs} is forwarded.
6823
+ * Projection workers open their own Postgres pools from `config.db`,
6824
+ * falling back to the executor worker pool's `db` when
6825
+ * {@link withWorkerPool} is configured with one; only the `poolSize` is
6826
+ * overridden by {@link ProjectionShardBuilderConfig.poolSize}. The same
6827
+ * model manifest resolved from {@link withDocumentModelSources} is
6828
+ * forwarded.
6685
6829
  */
6686
6830
  withProjectionShards(config) {
6687
6831
  this.projectionShardConfig = config;
@@ -6704,41 +6848,24 @@ var ReactorBuilder = class {
6704
6848
  }
6705
6849
  async buildModule() {
6706
6850
  if (!this.logger) this.logger = new ConsoleLogger(["reactor"]);
6707
- if (this.workerPoolConfig?.enabled) {
6708
- if (this.documentModels.length > 0) throw new Error("workerPool.enabled requires withDocumentModelSpecs; remove withDocumentModels() in worker-pool mode.");
6709
- if (this.documentModelSpecs.length === 0) throw new Error("workerPool.enabled requires at least one spec registered via withDocumentModelSpecs.");
6710
- const needsDefaultFactory = !this.executorManager && !this.workerFactory;
6711
- if (needsDefaultFactory && !this.workerDbConfig) throw new Error("workerPool.enabled requires withWorkerDbConfig (or a custom withWorkerFactory / withExecutor).");
6712
- if (needsDefaultFactory && !this.workerSignatureVerifierSpec) throw new Error("workerPool.enabled requires withWorkerSignatureVerifierSpec (or a custom withWorkerFactory / withExecutor).");
6713
- }
6714
- if (this.documentModelSpecs.length > 0) this.resolvedModelManifest = this.documentModelSpecs.map((input) => {
6715
- if ("filePath" in input) return {
6716
- documentType: "<unresolved>",
6717
- version: "<unresolved>",
6718
- spec: { module: {
6719
- filePath: input.filePath,
6720
- exportName: "documentModel"
6721
- } }
6722
- };
6723
- return {
6724
- documentType: "<unresolved>",
6725
- version: input.version,
6726
- spec: { module: {
6727
- packageName: input.packageName,
6728
- exportName: "documentModel"
6729
- } }
6730
- };
6731
- });
6851
+ if (this.projectionShardConfig !== void 0 && this.readModelFactories.length > 0) throw new Error("withProjectionShards does not support read models registered through withReadModelFactory; projection workers cannot receive host-only factory dependencies");
6852
+ const resolvedSources = await resolveModelSources(this.documentModelSources);
6853
+ if (this.workerPool) {
6854
+ if (resolvedSources.manifest.length === 0) throw new Error("withWorkerPool requires at least one worker-importable document-model source ({ filePath } or { packageName }).");
6855
+ if (resolvedSources.moduleOnlyKeys.length > 0) throw new Error(`withWorkerPool requires worker-importable sources, but these models were registered only as live modules: ${resolvedSources.moduleOnlyKeys.join(", ")}. Provide a { filePath } or { packageName } source for each.`);
6856
+ }
6857
+ this.resolvedModelManifest = resolvedSources.manifest.length > 0 ? resolvedSources.manifest : void 0;
6732
6858
  const documentModelRegistry = new DocumentModelRegistry();
6733
6859
  if (this.upgradeManifests.length > 0) {
6734
6860
  const results = documentModelRegistry.registerUpgradeManifests(...this.upgradeManifests);
6735
6861
  for (const result of results) if (result.status === "error") this.logger.error("Failed to register upgrade manifest: @error", result.error.message);
6736
6862
  }
6737
- if (this.documentModels.length > 0) {
6738
- const results = documentModelRegistry.registerModules(...this.documentModels);
6863
+ if (resolvedSources.modules.length > 0) {
6864
+ const results = documentModelRegistry.registerModules(...resolvedSources.modules);
6739
6865
  for (const result of results) if (result.status === "error") this.logger.error("Failed to register document model: @error", result.error.message);
6740
6866
  }
6741
- const baseDatabase = this.kyselyInstance ?? (this.workerPoolConfig?.enabled && this.workerDbConfig ? await this.createPostgresDatabase(this.workerDbConfig) : await createDefaultDatabase());
6867
+ const reactorDbConfig = this.resolveReactorDbConfig();
6868
+ const baseDatabase = this.kyselyInstance ?? (reactorDbConfig ? await this.createPostgresDatabase(reactorDbConfig) : await createDefaultDatabase());
6742
6869
  if (this.migrationStrategy === "auto") {
6743
6870
  const result = await runMigrations(baseDatabase, REACTOR_SCHEMA);
6744
6871
  if (!result.success && result.error) throw new Error(`Database migration failed: ${result.error.message}`);
@@ -6764,10 +6891,16 @@ var ReactorBuilder = class {
6764
6891
  const executionScope = new KyselyExecutionScope(database, operationStore, operationIndex, keyframeStore, writeCache, documentMetaCache, collectionMembershipCache);
6765
6892
  let executorManager = this.executorManager;
6766
6893
  let executorStartCount = this.executorConfig.maxConcurrency ?? 1;
6767
- if (!executorManager) if (this.workerPoolConfig?.enabled) {
6768
- const poolManager = new WorkerPoolJobExecutorManager(this.workerFactory ?? await this.createDefaultWorkerFactory(this.workerPoolConfig), eventBus, queue, jobTracker, this.logger, resolver, collectionMembershipCache, this.executorConfig.jobTimeoutMs);
6894
+ if (!executorManager) if (this.workerPool) {
6895
+ const pool = this.workerPool;
6896
+ let factory = pool.factory;
6897
+ if (!factory) {
6898
+ if (pool.db === void 0) throw new Error("unreachable: worker pool configured without db or factory");
6899
+ factory = await this.createDefaultWorkerFactory(pool.numWorkers, pool.db, pool.verifier);
6900
+ }
6901
+ const poolManager = new WorkerPoolJobExecutorManager(factory, eventBus, queue, jobTracker, this.logger, resolver, collectionMembershipCache, this.executorConfig.jobTimeoutMs);
6769
6902
  executorManager = poolManager;
6770
- executorStartCount = this.workerPoolConfig.numWorkers;
6903
+ executorStartCount = pool.numWorkers;
6771
6904
  if (resolver instanceof DocumentModelResolver) resolver.setBroadcastHook((entry) => poolManager.loadModel(entry));
6772
6905
  } else executorManager = new SimpleJobExecutorManager(() => new SimpleJobExecutor(this.logger, documentModelRegistry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, this.driveContainerTypes, this.executorConfig, this.signatureVerifier, executionScope), eventBus, queue, jobTracker, this.logger, resolver, this.executorConfig.jobTimeoutMs);
6773
6906
  await executorManager.start(executorStartCount);
@@ -6799,6 +6932,7 @@ var ReactorBuilder = class {
6799
6932
  }
6800
6933
  for (const factory of this.readModelFactories) {
6801
6934
  const readModel = await factory({
6935
+ documentModelRegistry,
6802
6936
  operationIndex,
6803
6937
  writeCache,
6804
6938
  processorManagerConsistencyTracker
@@ -6843,6 +6977,19 @@ var ReactorBuilder = class {
6843
6977
  return module;
6844
6978
  }
6845
6979
  /**
6980
+ * The single Postgres config for the parent, executor workers, and
6981
+ * projection shards. They must share one physical database (the parent
6982
+ * writes operations; workers and shards read them), so divergent
6983
+ * worker/shard targets throw. `withKysely` overrides the parent and is not
6984
+ * validated against a worker/shard `db`.
6985
+ */
6986
+ resolveReactorDbConfig() {
6987
+ const workerDb = this.workerPool?.db;
6988
+ const projectionDb = this.projectionShardConfig?.db;
6989
+ if (workerDb && projectionDb && !sameDatabaseTarget(workerDb, projectionDb)) throw new Error("withWorkerPool({ db }) and withProjectionShards({ db }) must address the same Postgres database (same host, port, and database); the parent writes operations there and the projection shards read them.");
6990
+ return workerDb ?? projectionDb;
6991
+ }
6992
+ /**
6846
6993
  * Constructs a {@link ProjectionShardManager} bound to the host event
6847
6994
  * bus. Builds the default thread-transport factory unless one was
6848
6995
  * injected via {@link withProjectionWorkerFactory}. Calls
@@ -6850,11 +6997,12 @@ var ReactorBuilder = class {
6850
6997
  * is returned to the caller.
6851
6998
  */
6852
6999
  async createProjectionShardManager(config, eventBus) {
6853
- if (!this.workerDbConfig) throw new Error("withProjectionShards requires withWorkerDbConfig; projection workers need connection info to open their own pools.");
7000
+ const baseDb = this.resolveReactorDbConfig();
7001
+ if (!baseDb) throw new Error("withProjectionShards requires a db (or an executor worker pool configured with one); projection workers need connection info to open their own pools.");
6854
7002
  const models = this.resolvedModelManifest ?? [];
6855
7003
  const db = {
6856
- ...this.workerDbConfig,
6857
- poolSize: config.poolSize ?? this.workerDbConfig.poolSize,
7004
+ ...baseDb,
7005
+ poolSize: config.poolSize ?? baseDb.poolSize,
6858
7006
  applicationName: "reactor-projection-shard"
6859
7007
  };
6860
7008
  const factory = this.projectionWorkerFactory ?? await this.createDefaultProjectionWorkerFactory();
@@ -6889,18 +7037,21 @@ var ReactorBuilder = class {
6889
7037
  return () => createProjectionThreadTransport(projectionWorkerEntryPath);
6890
7038
  }
6891
7039
  /**
6892
- * Default {@link WorkerFactory} used when `workerPool.enabled` is set and
6893
- * the caller did not inject `withWorkerFactory`. Each worker spawns a real
6894
- * `node:worker_threads` Worker pointing at the compiled `worker/entry.js`.
7040
+ * Default {@link WorkerFactory} used when the pool options carry no
7041
+ * custom `factory`. Each worker spawns a real `node:worker_threads`
7042
+ * Worker pointing at the compiled `worker/entry.js`.
6895
7043
  */
6896
- async createDefaultWorkerFactory(poolConfig) {
7044
+ async createDefaultWorkerFactory(numWorkers, db, signatureVerifier) {
6897
7045
  const [{ WorkerHandle }, { createThreadTransport }, { workerEntryPath }] = await Promise.all([
6898
- import("./worker-handle-B1w03nRA.js"),
7046
+ import("./worker-handle-CrERzl8s.js"),
6899
7047
  import("./transport-ByGviWdZ.js"),
6900
- import("./worker-DBJOv8Gp.js").then((n) => n.n)
7048
+ import("./worker-DNfxv5Ir.js").then((n) => n.n)
6901
7049
  ]);
6902
- const db = this.workerDbConfig;
6903
- const signatureVerifier = this.workerSignatureVerifierSpec;
7050
+ const poolConfig = {
7051
+ enabled: true,
7052
+ numWorkers,
7053
+ workerType: "thread"
7054
+ };
6904
7055
  const models = this.resolvedModelManifest ?? [];
6905
7056
  const logger = this.logger;
6906
7057
  return (index) => {
@@ -6915,7 +7066,8 @@ var ReactorBuilder = class {
6915
7066
  poolConfig,
6916
7067
  db,
6917
7068
  signatureVerifier,
6918
- models
7069
+ models,
7070
+ executorConfig: this.executorConfig
6919
7071
  },
6920
7072
  logger,
6921
7073
  poolInstrumentation
@@ -7155,6 +7307,11 @@ function driveIdFromUrl(url) {
7155
7307
  return url.split("/").pop() ?? "";
7156
7308
  }
7157
7309
  //#endregion
7310
+ //#region src/read-models/interfaces.ts
7311
+ function supportsLiveReadModelRegistration(coordinator) {
7312
+ return "addReadModel" in coordinator && typeof coordinator.addReadModel === "function";
7313
+ }
7314
+ //#endregion
7158
7315
  //#region src/admin/passthrough-keyframe-store.ts
7159
7316
  const passthroughKeyframeStore = {
7160
7317
  putKeyframe: () => Promise.resolve(),
@@ -7264,6 +7421,6 @@ var DocumentIntegrityService = class {
7264
7421
  }
7265
7422
  };
7266
7423
  //#endregion
7267
- export { BaseReadModel, ChannelError, ChannelErrorSource, ChannelScheme, ConsistencyTracker, DEFAULT_DRIVE_CONTAINER_TYPES, DRIVE_AUTH_ERROR_MESSAGES, DefaultSubscriptionErrorHandler, DocumentChangeType, DocumentIntegrityService, DocumentModelRegistry, DocumentModelResolver, DriveClient, DriveCollectionId, DuplicateManifestError, DuplicateModuleError, DuplicateOperationError, EventBus, EventBusAggregateError, GqlRequestChannel, GqlRequestChannelFactory, GqlResponseChannel, GqlResponseChannelFactory, SimpleJobExecutor as InMemoryJobExecutor, SimpleJobExecutor, InMemoryJobTracker, InMemoryQueue, IntervalPollTimer, InvalidModuleError, JobAwaiter, JobExecutorEventTypes, JobStatus, KyselyDocumentIndexer, KyselyDocumentView, KyselyKeyframeStore, KyselyOperationStore, KyselySyncCursorStorage, KyselySyncRemoteStorage, KyselyWriteCache, Mailbox, ModuleNotFoundError, NullDocumentModelResolver, OptimisticLockError, PollBehavior, PollingChannelError, ProcessorManager, PropagationMode, QueueEventTypes, REACTOR_SCHEMA, Reactor, ReactorBuilder, ReactorClient, ReactorClientBuilder, ReactorEventTypes, ReactorSubscriptionManager, ReadModelCoordinator, RelationalDbProcessor, RelationshipChangeType, RevisionMismatchError, SimpleJobExecutorManager, SyncBuilder, SyncEventTypes, SyncOperation, SyncOperationAggregateError, SyncOperationStatus, SyncStatus, SyncStatusTracker, addRelationshipAction, batchOperationsByDocument, consolidateSyncOperations, createDocumentAction, createForwardingLogger, createMutableShutdownStatus, createRelationalDb, deleteDocumentAction, documentActions, driveIdFromUrl, envelopesToSyncOperations, errorToInfo, getMigrationStatus, instrumentPgPool, isDriveAuthError, makeConsistencyKey, parseDriveUrl, parsePagingOptions, removeRelationshipAction, runMigrations, sanitizeArg, trimMailboxFromAckOrdinal, updateRelationshipAction, upgradeDocumentAction, workerEntryPath };
7424
+ export { APPEND_CONDITION_FAILED_PREFIX, AppendConditionFailedError, BaseReadModel, ChannelError, ChannelErrorSource, ChannelScheme, ConsistencyTracker, DEFAULT_DRIVE_CONTAINER_TYPES, DRIVE_AUTH_ERROR_MESSAGES, DefaultSubscriptionErrorHandler, DocumentChangeType, DocumentIntegrityService, DocumentModelRegistry, DocumentModelResolver, DriveClient, DriveCollectionId, DuplicateManifestError, DuplicateModuleError, DuplicateOperationError, EventBus, EventBusAggregateError, GqlRequestChannel, GqlRequestChannelFactory, GqlResponseChannel, GqlResponseChannelFactory, SimpleJobExecutor as InMemoryJobExecutor, SimpleJobExecutor, InMemoryJobTracker, InMemoryQueue, IntervalPollTimer, InvalidModuleError, JobAwaiter, JobExecutorEventTypes, JobStatus, KyselyDocumentIndexer, KyselyDocumentView, KyselyKeyframeStore, KyselyOperationStore, KyselySyncCursorStorage, KyselySyncRemoteStorage, KyselyWriteCache, Mailbox, ModuleNotFoundError, NullDocumentModelResolver, OptimisticLockError, PollBehavior, PollingChannelError, ProcessorManager, PropagationMode, QueueEventTypes, REACTOR_SCHEMA, Reactor, ReactorBuilder, ReactorClient, ReactorClientBuilder, ReactorEventTypes, ReactorSubscriptionManager, ReadModelCoordinator, RelationalDbProcessor, RelationshipChangeType, RetryAccounting, RevisionMismatchError, SimpleJobExecutorManager, SyncBuilder, SyncEventTypes, SyncOperation, SyncOperationAggregateError, SyncOperationStatus, SyncStatus, SyncStatusTracker, addRelationshipAction, batchOperationsByDocument, buildDecisionModel, consolidateSyncOperations, createDocumentAction, createForwardingLogger, createMutableShutdownStatus, createRelationalDb, deleteDocumentAction, documentActions, driveIdFromUrl, envelopesToSyncOperations, errorToInfo, getMigrationStatus, instrumentPgPool, isDriveAuthError, makeConsistencyKey, parseDriveUrl, parsePagingOptions, removeRelationshipAction, runMigrations, sanitizeArg, supportsLiveReadModelRegistration, trimMailboxFromAckOrdinal, updateRelationshipAction, upgradeDocumentAction, workerEntryPath };
7268
7425
 
7269
7426
  //# sourceMappingURL=index.js.map