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

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
@@ -2686,6 +2686,98 @@ var InMemoryQueue = class {
2686
2686
  }
2687
2687
  };
2688
2688
  //#endregion
2689
+ //#region src/core/model-sources.ts
2690
+ function modelSourceKey(module) {
2691
+ return `${module.documentModel.global.id}@${module.version ?? 1}`;
2692
+ }
2693
+ function isDocumentModelModule(value) {
2694
+ if (typeof value !== "object" || value === null) return false;
2695
+ const candidate = value;
2696
+ return typeof candidate.reducer === "function" && typeof candidate.documentModel?.global?.id === "string";
2697
+ }
2698
+ /**
2699
+ * Resolves every source to live modules by importing file/package sources
2700
+ * host-side and scanning their exports, so the host registry and the worker
2701
+ * manifest are derived from one list and cannot diverge.
2702
+ */
2703
+ async function resolveModelSources(sources) {
2704
+ const resolved = /* @__PURE__ */ new Map();
2705
+ for (const source of sources) {
2706
+ if (isDocumentModelModule(source)) {
2707
+ addEntry(resolved, {
2708
+ module: source,
2709
+ spec: null
2710
+ });
2711
+ continue;
2712
+ }
2713
+ for (const entry of await resolveImportableSource(source)) addEntry(resolved, entry);
2714
+ }
2715
+ const modules = [];
2716
+ const manifest = [];
2717
+ const moduleOnlyKeys = [];
2718
+ for (const [key, entry] of resolved) {
2719
+ modules.push(entry.module);
2720
+ if (entry.spec) manifest.push({
2721
+ documentType: entry.module.documentModel.global.id,
2722
+ version: String(entry.module.version ?? 1),
2723
+ spec: entry.spec
2724
+ });
2725
+ else moduleOnlyKeys.push(key);
2726
+ }
2727
+ return {
2728
+ modules,
2729
+ manifest,
2730
+ moduleOnlyKeys
2731
+ };
2732
+ }
2733
+ function addEntry(map, entry) {
2734
+ const key = modelSourceKey(entry.module);
2735
+ const existing = map.get(key);
2736
+ if (!existing) {
2737
+ map.set(key, entry);
2738
+ return;
2739
+ }
2740
+ if (!existing.spec && entry.spec) existing.spec = entry.spec;
2741
+ }
2742
+ async function resolveImportableSource(source) {
2743
+ const isFile = "filePath" in source;
2744
+ const specifier = isFile ? new URL(`file://${source.filePath}`).href : source.subpath ? `${source.packageName}/${source.subpath}` : source.packageName;
2745
+ const moduleRef = isFile ? { filePath: source.filePath } : { packageName: specifier };
2746
+ let moduleNs;
2747
+ try {
2748
+ moduleNs = await import(
2749
+ /* @vite-ignore */
2750
+ specifier
2751
+ );
2752
+ } catch (error) {
2753
+ throw new Error(`Failed to import document-model source "${describeSource(source)}"`, { cause: error });
2754
+ }
2755
+ if (source.exportName !== void 0) {
2756
+ const value = moduleNs[source.exportName];
2757
+ if (!isDocumentModelModule(value)) throw new Error(`Export "${source.exportName}" of document-model source "${describeSource(source)}" is not a DocumentModelModule`);
2758
+ return [{
2759
+ module: value,
2760
+ spec: { module: {
2761
+ ...moduleRef,
2762
+ exportName: source.exportName
2763
+ } }
2764
+ }];
2765
+ }
2766
+ const entries = [];
2767
+ for (const [exportName, value] of Object.entries(moduleNs)) if (isDocumentModelModule(value)) entries.push({
2768
+ module: value,
2769
+ spec: { module: {
2770
+ ...moduleRef,
2771
+ exportName
2772
+ } }
2773
+ });
2774
+ if (entries.length === 0) throw new Error(`Document-model source "${describeSource(source)}" has no DocumentModelModule exports`);
2775
+ return entries;
2776
+ }
2777
+ function describeSource(source) {
2778
+ return "filePath" in source ? source.filePath : source.subpath ? `${source.packageName}/${source.subpath}` : source.packageName;
2779
+ }
2780
+ //#endregion
2689
2781
  //#region src/registry/document-model-resolver.ts
2690
2782
  /**
2691
2783
  * Encapsulates the logic for resolving document model modules on demand.
@@ -2743,10 +2835,11 @@ var DocumentModelResolver = class {
2743
2835
  return loadPromise;
2744
2836
  }
2745
2837
  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);
2838
+ const resolved = await resolveModelSources([await this.loader.load(documentType)]);
2839
+ if (!resolved.modules.some((module) => module.documentModel.global.id === documentType)) throw new Error(`Loader source resolved no module for document type: ${documentType}`);
2840
+ const results = this.registry.registerModules(...resolved.modules);
2841
+ for (const result of results) if (result.status === "error" && !DuplicateModuleError.isError(result.error)) throw result.error;
2842
+ if (this.broadcastHook) for (const entry of resolved.manifest) await this.broadcastHook(entry);
2750
2843
  }
2751
2844
  async notifyPeersModelLoaded(documentType) {
2752
2845
  if (!this.modelLoadedHook) return;
@@ -2756,12 +2849,6 @@ var DocumentModelResolver = class {
2756
2849
  this.logger.warn(`MODEL_LOADED hook failed: ${documentType}`, error);
2757
2850
  }
2758
2851
  }
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
2852
  };
2766
2853
  /**
2767
2854
  * No-op resolver used when no document model loader is configured.
@@ -6478,9 +6565,12 @@ var Reactor = class {
6478
6565
  };
6479
6566
  //#endregion
6480
6567
  //#region src/core/reactor-builder.ts
6568
+ function sameDatabaseTarget(a, b) {
6569
+ return a.host === b.host && a.port === b.port && a.database === b.database;
6570
+ }
6481
6571
  var ReactorBuilder = class {
6482
6572
  logger;
6483
- documentModels = [];
6573
+ documentModelSources = [];
6484
6574
  upgradeManifests = [];
6485
6575
  features = { legacyStorageEnabled: false };
6486
6576
  readModels = [];
@@ -6501,12 +6591,8 @@ var ReactorBuilder = class {
6501
6591
  documentModelLoader;
6502
6592
  shutdownHooks = [];
6503
6593
  driveContainerTypes = DEFAULT_DRIVE_CONTAINER_TYPES;
6504
- documentModelSpecs = [];
6505
- workerPoolConfig;
6594
+ workerPool;
6506
6595
  resolvedModelManifest;
6507
- workerDbConfig;
6508
- workerSignatureVerifierSpec;
6509
- workerFactory;
6510
6596
  projectionShardConfig;
6511
6597
  projectionWorkerFactory;
6512
6598
  instrumentedPools = [];
@@ -6514,8 +6600,15 @@ var ReactorBuilder = class {
6514
6600
  this.logger = logger;
6515
6601
  return this;
6516
6602
  }
6517
- withDocumentModels(models) {
6518
- this.documentModels = models;
6603
+ /**
6604
+ * Register document-model sources: live modules, importable files, or
6605
+ * importable packages. Appends across calls. At `buildModule()` every
6606
+ * source is resolved host-side and registered on the registry; file and
6607
+ * package sources additionally form the worker manifest when the worker
6608
+ * pool is enabled (live modules cannot cross a thread boundary).
6609
+ */
6610
+ withDocumentModelSources(sources) {
6611
+ this.documentModelSources.push(...sources);
6519
6612
  return this;
6520
6613
  }
6521
6614
  withUpgradeManifests(manifests) {
@@ -6630,58 +6723,30 @@ var ReactorBuilder = class {
6630
6723
  this.shutdownHooks.push(hook);
6631
6724
  return this;
6632
6725
  }
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
- /**
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.
6659
- */
6660
- withWorkerSignatureVerifierSpec(spec) {
6661
- this.workerSignatureVerifierSpec = spec;
6662
- return this;
6663
- }
6664
6726
  /**
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).
6727
+ * Enable the executor worker pool: N `node:worker_threads` workers with
6728
+ * sticky per-document routing, replacing the in-process executor. Calling
6729
+ * this enables the pool there is no `enabled` flag. Provide `db`
6730
+ * (each worker opens its own Postgres pool; the parent database is built
6731
+ * from it too unless {@link withKysely} is set) or a custom `factory`
6732
+ * transport. `verifier` is imported by the default transport's workers;
6733
+ * omitted = no executor-side signature verification.
6669
6734
  */
6670
- withWorkerFactory(factory) {
6671
- this.workerFactory = factory;
6735
+ withWorkerPool(options) {
6736
+ this.workerPool = options;
6672
6737
  return this;
6673
6738
  }
6674
6739
  /**
6675
6740
  * Configure N sharded projection workers. When set, the builder replaces
6676
6741
  * 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}.
6742
+ * {@link ProjectionShardManager}.
6681
6743
  *
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.
6744
+ * Projection workers open their own Postgres pools from `config.db`,
6745
+ * falling back to the executor worker pool's `db` when
6746
+ * {@link withWorkerPool} is configured with one; only the `poolSize` is
6747
+ * overridden by {@link ProjectionShardBuilderConfig.poolSize}. The same
6748
+ * model manifest resolved from {@link withDocumentModelSources} is
6749
+ * forwarded.
6685
6750
  */
6686
6751
  withProjectionShards(config) {
6687
6752
  this.projectionShardConfig = config;
@@ -6704,41 +6769,23 @@ var ReactorBuilder = class {
6704
6769
  }
6705
6770
  async buildModule() {
6706
6771
  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
- });
6772
+ const resolvedSources = await resolveModelSources(this.documentModelSources);
6773
+ if (this.workerPool) {
6774
+ if (resolvedSources.manifest.length === 0) throw new Error("withWorkerPool requires at least one worker-importable document-model source ({ filePath } or { packageName }).");
6775
+ 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.`);
6776
+ }
6777
+ this.resolvedModelManifest = resolvedSources.manifest.length > 0 ? resolvedSources.manifest : void 0;
6732
6778
  const documentModelRegistry = new DocumentModelRegistry();
6733
6779
  if (this.upgradeManifests.length > 0) {
6734
6780
  const results = documentModelRegistry.registerUpgradeManifests(...this.upgradeManifests);
6735
6781
  for (const result of results) if (result.status === "error") this.logger.error("Failed to register upgrade manifest: @error", result.error.message);
6736
6782
  }
6737
- if (this.documentModels.length > 0) {
6738
- const results = documentModelRegistry.registerModules(...this.documentModels);
6783
+ if (resolvedSources.modules.length > 0) {
6784
+ const results = documentModelRegistry.registerModules(...resolvedSources.modules);
6739
6785
  for (const result of results) if (result.status === "error") this.logger.error("Failed to register document model: @error", result.error.message);
6740
6786
  }
6741
- const baseDatabase = this.kyselyInstance ?? (this.workerPoolConfig?.enabled && this.workerDbConfig ? await this.createPostgresDatabase(this.workerDbConfig) : await createDefaultDatabase());
6787
+ const reactorDbConfig = this.resolveReactorDbConfig();
6788
+ const baseDatabase = this.kyselyInstance ?? (reactorDbConfig ? await this.createPostgresDatabase(reactorDbConfig) : await createDefaultDatabase());
6742
6789
  if (this.migrationStrategy === "auto") {
6743
6790
  const result = await runMigrations(baseDatabase, REACTOR_SCHEMA);
6744
6791
  if (!result.success && result.error) throw new Error(`Database migration failed: ${result.error.message}`);
@@ -6764,10 +6811,16 @@ var ReactorBuilder = class {
6764
6811
  const executionScope = new KyselyExecutionScope(database, operationStore, operationIndex, keyframeStore, writeCache, documentMetaCache, collectionMembershipCache);
6765
6812
  let executorManager = this.executorManager;
6766
6813
  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);
6814
+ if (!executorManager) if (this.workerPool) {
6815
+ const pool = this.workerPool;
6816
+ let factory = pool.factory;
6817
+ if (!factory) {
6818
+ if (pool.db === void 0) throw new Error("unreachable: worker pool configured without db or factory");
6819
+ factory = await this.createDefaultWorkerFactory(pool.numWorkers, pool.db, pool.verifier);
6820
+ }
6821
+ const poolManager = new WorkerPoolJobExecutorManager(factory, eventBus, queue, jobTracker, this.logger, resolver, collectionMembershipCache, this.executorConfig.jobTimeoutMs);
6769
6822
  executorManager = poolManager;
6770
- executorStartCount = this.workerPoolConfig.numWorkers;
6823
+ executorStartCount = pool.numWorkers;
6771
6824
  if (resolver instanceof DocumentModelResolver) resolver.setBroadcastHook((entry) => poolManager.loadModel(entry));
6772
6825
  } 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
6826
  await executorManager.start(executorStartCount);
@@ -6843,6 +6896,19 @@ var ReactorBuilder = class {
6843
6896
  return module;
6844
6897
  }
6845
6898
  /**
6899
+ * The single Postgres config for the parent, executor workers, and
6900
+ * projection shards. They must share one physical database (the parent
6901
+ * writes operations; workers and shards read them), so divergent
6902
+ * worker/shard targets throw. `withKysely` overrides the parent and is not
6903
+ * validated against a worker/shard `db`.
6904
+ */
6905
+ resolveReactorDbConfig() {
6906
+ const workerDb = this.workerPool?.db;
6907
+ const projectionDb = this.projectionShardConfig?.db;
6908
+ 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.");
6909
+ return workerDb ?? projectionDb;
6910
+ }
6911
+ /**
6846
6912
  * Constructs a {@link ProjectionShardManager} bound to the host event
6847
6913
  * bus. Builds the default thread-transport factory unless one was
6848
6914
  * injected via {@link withProjectionWorkerFactory}. Calls
@@ -6850,11 +6916,12 @@ var ReactorBuilder = class {
6850
6916
  * is returned to the caller.
6851
6917
  */
6852
6918
  async createProjectionShardManager(config, eventBus) {
6853
- if (!this.workerDbConfig) throw new Error("withProjectionShards requires withWorkerDbConfig; projection workers need connection info to open their own pools.");
6919
+ const baseDb = this.resolveReactorDbConfig();
6920
+ 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
6921
  const models = this.resolvedModelManifest ?? [];
6855
6922
  const db = {
6856
- ...this.workerDbConfig,
6857
- poolSize: config.poolSize ?? this.workerDbConfig.poolSize,
6923
+ ...baseDb,
6924
+ poolSize: config.poolSize ?? baseDb.poolSize,
6858
6925
  applicationName: "reactor-projection-shard"
6859
6926
  };
6860
6927
  const factory = this.projectionWorkerFactory ?? await this.createDefaultProjectionWorkerFactory();
@@ -6889,18 +6956,21 @@ var ReactorBuilder = class {
6889
6956
  return () => createProjectionThreadTransport(projectionWorkerEntryPath);
6890
6957
  }
6891
6958
  /**
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`.
6959
+ * Default {@link WorkerFactory} used when the pool options carry no
6960
+ * custom `factory`. Each worker spawns a real `node:worker_threads`
6961
+ * Worker pointing at the compiled `worker/entry.js`.
6895
6962
  */
6896
- async createDefaultWorkerFactory(poolConfig) {
6963
+ async createDefaultWorkerFactory(numWorkers, db, signatureVerifier) {
6897
6964
  const [{ WorkerHandle }, { createThreadTransport }, { workerEntryPath }] = await Promise.all([
6898
6965
  import("./worker-handle-B1w03nRA.js"),
6899
6966
  import("./transport-ByGviWdZ.js"),
6900
6967
  import("./worker-DBJOv8Gp.js").then((n) => n.n)
6901
6968
  ]);
6902
- const db = this.workerDbConfig;
6903
- const signatureVerifier = this.workerSignatureVerifierSpec;
6969
+ const poolConfig = {
6970
+ enabled: true,
6971
+ numWorkers,
6972
+ workerType: "thread"
6973
+ };
6904
6974
  const models = this.resolvedModelManifest ?? [];
6905
6975
  const logger = this.logger;
6906
6976
  return (index) => {