@powerhousedao/switchboard 6.2.2-dev.1 → 6.2.2-dev.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/.env +13 -0
  2. package/.tsbuild/test/attachments/auth.test.d.ts +2 -0
  3. package/.tsbuild/test/attachments/auth.test.d.ts.map +1 -0
  4. package/.tsbuild/test/attachments/index.test.d.ts +2 -0
  5. package/.tsbuild/test/attachments/index.test.d.ts.map +1 -0
  6. package/.tsbuild/test/attachments/routes-integration.test.d.ts +2 -0
  7. package/.tsbuild/test/attachments/routes-integration.test.d.ts.map +1 -0
  8. package/.tsbuild/test/attachments/routes.test.d.ts +2 -0
  9. package/.tsbuild/test/attachments/routes.test.d.ts.map +1 -0
  10. package/.tsbuild/test/attachments/service-config.test.d.ts +2 -0
  11. package/.tsbuild/test/attachments/service-config.test.d.ts.map +1 -0
  12. package/.tsbuild/test/metrics.test.d.ts +2 -0
  13. package/.tsbuild/test/metrics.test.d.ts.map +1 -0
  14. package/.tsbuild/test/pglite-dialect.test.d.ts +2 -0
  15. package/.tsbuild/test/pglite-dialect.test.d.ts.map +1 -0
  16. package/.tsbuild/test/pglite-version.test.d.ts +2 -0
  17. package/.tsbuild/test/pglite-version.test.d.ts.map +1 -0
  18. package/.tsbuild/test/worker-pool.test.d.ts +2 -0
  19. package/.tsbuild/test/worker-pool.test.d.ts.map +1 -0
  20. package/.tsbuild/tsconfig.tsbuildinfo +1 -0
  21. package/.tsbuild/tsdown.config.d.ts +3 -0
  22. package/.tsbuild/tsdown.config.d.ts.map +1 -0
  23. package/.tsbuild/vitest.config.d.ts +3 -0
  24. package/.tsbuild/vitest.config.d.ts.map +1 -0
  25. package/CHANGELOG.md +59 -0
  26. package/README.md +44 -0
  27. package/dist/index.mjs +1 -1
  28. package/dist/{server-DgBz3TJW.mjs → server-Cgud_ONM.mjs} +216 -8
  29. package/dist/server-Cgud_ONM.mjs.map +1 -0
  30. package/dist/server.d.mts +13 -9
  31. package/dist/server.d.mts.map +1 -1
  32. package/dist/server.mjs +3 -3
  33. package/package.json +12 -12
  34. package/test/worker-pool.test.ts +232 -0
  35. package/dist/server-DgBz3TJW.mjs.map +0 -1
@@ -0,0 +1,3 @@
1
+ declare const _default: import("tsdown").UserConfig;
2
+ export default _default;
3
+ //# sourceMappingURL=tsdown.config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tsdown.config.d.ts","sourceRoot":"","sources":["../tsdown.config.ts"],"names":[],"mappings":";AAEA,wBAaG"}
@@ -0,0 +1,3 @@
1
+ declare const _default: import("vite").UserConfig;
2
+ export default _default;
3
+ //# sourceMappingURL=vitest.config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vitest.config.d.ts","sourceRoot":"","sources":["../vitest.config.ts"],"names":[],"mappings":";AAEA,wBAQG"}
package/CHANGELOG.md CHANGED
@@ -1,3 +1,62 @@
1
+ ## 6.2.2-dev.11 (2026-07-22)
2
+
3
+ ### 🚀 Features
4
+
5
+ - **connect:** dynamic packages can contribute to the PWA configuration ([da867e21d](https://github.com/powerhouse-inc/powerhouse/commit/da867e21d))
6
+
7
+ ### ❤️ Thank You
8
+
9
+ - Yasiel Cabrera @YasielCabrera
10
+
11
+ ## 6.2.2-dev.10 (2026-07-21)
12
+
13
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
14
+
15
+ ## 6.2.2-dev.9 (2026-07-20)
16
+
17
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
18
+
19
+ ## 6.2.2-dev.8 (2026-07-19)
20
+
21
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
22
+
23
+ ## 6.2.2-dev.7 (2026-07-18)
24
+
25
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
26
+
27
+ ## 6.2.2-dev.6 (2026-07-17)
28
+
29
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
30
+
31
+ ## 6.2.2-dev.5 (2026-07-17)
32
+
33
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
34
+
35
+ ## 6.2.2-dev.4 (2026-07-17)
36
+
37
+ ### 🚀 Features
38
+
39
+ - **switchboard, reactor:** worker config integration on switchboard, including auto mode ([38c3b3de6](https://github.com/powerhouse-inc/powerhouse/commit/38c3b3de6))
40
+ - **vetra-e2e:** full e2e spec for shared worker ([ed1b3fbd2](https://github.com/powerhouse-inc/powerhouse/commit/ed1b3fbd2))
41
+
42
+ ### ❤️ Thank You
43
+
44
+ - Benjamin Jordan
45
+
46
+ ## 6.2.2-dev.3 (2026-07-16)
47
+
48
+ ### 🚀 Features
49
+
50
+ - **registry:** postgres-backed auth plugin with persistent accounts and package ownership ([0c1ffbeb4](https://github.com/powerhouse-inc/powerhouse/commit/0c1ffbeb4))
51
+
52
+ ### ❤️ Thank You
53
+
54
+ - Guillermo Puente @gpuente
55
+
56
+ ## 6.2.2-dev.2 (2026-07-16)
57
+
58
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
59
+
1
60
  ## 6.2.2-dev.1 (2026-07-15)
2
61
 
3
62
  This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
package/README.md CHANGED
@@ -112,6 +112,50 @@ pnpm add -g @powerhousedao/switchboard
112
112
 
113
113
  See [Observability](#observability) below for Sentry and OpenTelemetry variables.
114
114
 
115
+ ### Executor Worker Pool
116
+
117
+ By default the reactor executes jobs on a single in-process executor. Setting
118
+ `REACTOR_WORKERS` to a value greater than 0 moves job execution into N
119
+ `node:worker_threads` workers; jobs route to a worker stickily by document id,
120
+ and each worker opens its own Postgres pool.
121
+
122
+ | Variable | Description | Default |
123
+ | ------------------------------- | -------------------------------------------------------------- | ------- |
124
+ | `REACTOR_WORKERS` | Executor worker threads: a number (0 = in-process) or `auto` | `0` |
125
+ | `REACTOR_DB_POOL_SIZE_WORKER` | Per-worker Postgres pool size | `2` |
126
+ | `REACTOR_DB_ACQUIRE_TIMEOUT_MS` | Pool connection acquire timeout (ms) | `5000` |
127
+
128
+ `REACTOR_WORKERS=auto` lets the runtime size the pool from the machine's
129
+ available cores (`os.availableParallelism()`, which respects container CPU
130
+ limits): `max(1, min(8, cores - 2))`. Two cores stay reserved for the host
131
+ event loop and the cap keeps the worker connection budget bounded; `auto`
132
+ always enables worker mode with at least one worker.
133
+
134
+ Requirements when `REACTOR_WORKERS > 0`:
135
+
136
+ - **Postgres only.** The reactor database must be a `postgres://` URL with
137
+ explicit credentials (`PH_REACTOR_DATABASE_URL` or
138
+ `PH_SWITCHBOARD_DATABASE_URL`). PGlite cannot be shared across threads,
139
+ so switchboard refuses to start on local PGlite storage.
140
+ - **Built packages only.** Worker threads import document models by file path,
141
+ so every configured package needs a built `document-models` entry. `dev`
142
+ mode (Vite-loaded models, e.g. `ph vetra`) is rejected.
143
+ - **Connection budget.** Total Postgres connections are roughly
144
+ `host pool + REACTOR_WORKERS x REACTOR_DB_POOL_SIZE_WORKER`; keep this
145
+ under the server's `max_connections`.
146
+
147
+ Example:
148
+
149
+ ```bash
150
+ PH_SWITCHBOARD_DATABASE_URL=postgres://reactor:reactor@localhost:5433/switchboard \
151
+ REACTOR_WORKERS=4 \
152
+ ph switchboard
153
+ ```
154
+
155
+ The same settings are available programmatically via
156
+ `startSwitchboard({ workerPool: { numWorkers, dbPoolSizePerWorker, acquireTimeoutMs } })`,
157
+ which takes precedence over the environment variables.
158
+
115
159
  ### Authentication Configuration
116
160
 
117
161
  ```typescript
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="5a65c537-fadb-5705-95c0-4cec44d7b4d2")}catch(e){}}();
4
- import { a as parseForcePgVersion, r as startSwitchboard } from "./server-DgBz3TJW.mjs";
4
+ import { a as parseForcePgVersion, r as startSwitchboard } from "./server-Cgud_ONM.mjs";
5
5
  import "./utils-Baw7rThP.mjs";
6
6
  import { metrics } from "@opentelemetry/api";
7
7
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
@@ -1,12 +1,12 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="8a8055ab-2ceb-59fe-9007-f1d1fde8a019")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="5eb0a978-689a-51dd-978a-cd3d7fe5df4b")}catch(e){}}();
3
3
  import { n as addDefaultReactorDrive, r as isPostgresUrl, t as addDefaultDrive } from "./utils-Baw7rThP.mjs";
4
4
  import { register } from "node:module";
5
5
  import * as Sentry from "@sentry/node";
6
6
  import { childLogger, documentModelDocumentModelModule, setLogLevel } from "document-model";
7
7
  import dotenv from "dotenv";
8
8
  import { getConfig } from "@powerhousedao/config/node";
9
- import { promises } from "node:fs";
9
+ import { existsSync, promises, realpathSync } from "node:fs";
10
10
  import path from "node:path";
11
11
  import { ReactorInstrumentation } from "@powerhousedao/opentelemetry-instrumentation-reactor";
12
12
  import { AtomicNodeFs } from "@powerhousedao/pglite-fs";
@@ -21,6 +21,9 @@ import path$1 from "path";
21
21
  import { Pool } from "pg";
22
22
  import { Readable } from "node:stream";
23
23
  import { driveDocumentModelModule } from "@powerhousedao/shared/document-drive";
24
+ import { readFile } from "node:fs/promises";
25
+ import os from "node:os";
26
+ import { fileURLToPath } from "node:url";
24
27
  import { EnvVarProvider } from "@openfeature/env-var-provider";
25
28
  import { OpenFeature } from "@openfeature/server-sdk";
26
29
  import { PGliteDialect } from "kysely-pglite-dialect";
@@ -392,14 +395,18 @@ function registerAttachmentRoutes(api) {
392
395
  * Does NOT touch kysely or read models — callers wire those themselves
393
396
  * (see `withKysely` / `withReadModelFactory` on the reactor builder).
394
397
  */
395
- function applySwitchboardReactorDefaults(reactorBuilder, clientBuilder, options = {}) {
396
- const baseModels = options.includeBaseModels !== false ? [
398
+ /** The models switchboard always registers alongside package models. */
399
+ function switchboardBaseDocumentModels() {
400
+ return [
397
401
  documentModelDocumentModelModule,
398
402
  driveDocumentModelModule,
399
403
  reactorDriveDocumentModelModule
400
- ] : [];
404
+ ];
405
+ }
406
+ function applySwitchboardReactorDefaults(reactorBuilder, clientBuilder, options = {}) {
407
+ const baseModels = options.includeBaseModels !== false ? switchboardBaseDocumentModels() : [];
401
408
  const extra = options.documentModels ?? [];
402
- if (baseModels.length || extra.length) reactorBuilder.withDocumentModels(getUniqueDocumentModels(baseModels, extra));
409
+ if (baseModels.length || extra.length) reactorBuilder.withDocumentModelSources(getUniqueDocumentModels(baseModels, extra));
403
410
  const scheme = options.channelScheme === void 0 ? ChannelScheme.SWITCHBOARD : options.channelScheme;
404
411
  if (scheme !== false) reactorBuilder.withChannelScheme(scheme);
405
412
  if (options.signalHandlers !== false) reactorBuilder.withSignalHandlers();
@@ -409,6 +416,188 @@ function applySwitchboardReactorDefaults(reactorBuilder, clientBuilder, options
409
416
  if (options.signer) clientBuilder.withSigner(options.signer);
410
417
  }
411
418
  //#endregion
419
+ //#region src/worker-pool.mts
420
+ const DEFAULT_DB_POOL_SIZE_PER_WORKER = 2;
421
+ const DEFAULT_ACQUIRE_TIMEOUT_MS = 5e3;
422
+ const AUTO_RESERVED_CORES = 2;
423
+ const AUTO_WORKER_CAP = 8;
424
+ /** Worker count for "auto": always at least 1 — auto sizes the pool, it does not turn worker mode off. */
425
+ function autoWorkerCount(availableCores) {
426
+ return Math.max(1, Math.min(AUTO_WORKER_CAP, availableCores - AUTO_RESERVED_CORES));
427
+ }
428
+ /**
429
+ * Effective worker-pool config from programmatic options (which win) and the
430
+ * REACTOR_* env vars; null when disabled (numWorkers 0, the default).
431
+ * `"auto"` sizes the pool from the machine's available cores.
432
+ */
433
+ function resolveWorkerPoolOptions(input, env, availableCores = os.availableParallelism()) {
434
+ const requested = input?.numWorkers ?? parseWorkerCount(env.REACTOR_WORKERS) ?? 0;
435
+ const mode = requested === "auto" ? "auto" : "explicit";
436
+ const numWorkers = requested === "auto" ? autoWorkerCount(availableCores) : requested;
437
+ if (!Number.isInteger(numWorkers) || numWorkers < 0) throw new Error(`workerPool.numWorkers must be "auto" or a non-negative integer, got ${numWorkers}`);
438
+ if (numWorkers === 0) return null;
439
+ return {
440
+ numWorkers,
441
+ mode,
442
+ dbPoolSizePerWorker: input?.dbPoolSizePerWorker ?? parseNonNegativeInt(env.REACTOR_DB_POOL_SIZE_WORKER, "REACTOR_DB_POOL_SIZE_WORKER") ?? DEFAULT_DB_POOL_SIZE_PER_WORKER,
443
+ acquireTimeoutMs: input?.acquireTimeoutMs ?? parseNonNegativeInt(env.REACTOR_DB_ACQUIRE_TIMEOUT_MS, "REACTOR_DB_ACQUIRE_TIMEOUT_MS") ?? DEFAULT_ACQUIRE_TIMEOUT_MS
444
+ };
445
+ }
446
+ /**
447
+ * DbConfig for each worker's own pool, parsed from a postgres:// URL.
448
+ * Explicit host, database, and user are required — no pg-side defaults.
449
+ */
450
+ function buildWorkerDbConfig(postgresUrl, options) {
451
+ let parsed;
452
+ try {
453
+ parsed = new URL(postgresUrl);
454
+ } catch {
455
+ throw new Error(`Worker pool requires a valid postgres:// URL for the reactor database, got "${postgresUrl}"`);
456
+ }
457
+ const database = decodeURIComponent(parsed.pathname.replace(/^\//, ""));
458
+ if (!parsed.hostname || !database) throw new Error(`Worker pool requires a postgres URL with a host and database name, got "${postgresUrl}"`);
459
+ if (!parsed.username) throw new Error(`Worker pool requires explicit credentials in the postgres URL (workers open their own connections), got "${postgresUrl}"`);
460
+ const sslmode = parsed.searchParams.get("sslmode");
461
+ const ssl = parsed.searchParams.get("ssl") === "true" || sslmode !== null && sslmode !== "disable";
462
+ return {
463
+ host: parsed.hostname,
464
+ port: parsed.port ? Number(parsed.port) : 5432,
465
+ database,
466
+ user: decodeURIComponent(parsed.username),
467
+ password: decodeURIComponent(parsed.password),
468
+ ssl,
469
+ applicationName: "switchboard-worker",
470
+ poolSize: options.dbPoolSizePerWorker,
471
+ connectionTimeoutMillis: options.acquireTimeoutMs
472
+ };
473
+ }
474
+ /** Specifiers of the base models switchboard always registers. */
475
+ const BASE_MODEL_SPECIFIERS = [
476
+ "document-model",
477
+ "@powerhousedao/shared/document-drive",
478
+ "@powerhousedao/reactor-drive"
479
+ ];
480
+ /**
481
+ * Resolves base models and configured package identifiers to `{ filePath }`
482
+ * sources for the reactor builder. Resolution to absolute paths happens here
483
+ * because workers (and the builder) resolve bare specifiers from the reactor
484
+ * package's own dependency context, which cannot see switchboard or project
485
+ * packages. Unresolvable identifiers are skipped with a warning; the builder
486
+ * fails the boot if a registered model ends up without an importable source.
487
+ */
488
+ async function resolveWorkerModelSources(packages, logger) {
489
+ const sources = [];
490
+ for (const identifier of [...BASE_MODEL_SPECIFIERS, ...packages]) {
491
+ const filePath = await resolveModelModuleFile(identifier);
492
+ if (!filePath) {
493
+ logger.warn(`Worker model sources: no importable document-models entry for "${identifier}", skipping`);
494
+ continue;
495
+ }
496
+ sources.push({ filePath });
497
+ }
498
+ return sources;
499
+ }
500
+ /**
501
+ * Absolute file path of a source's models barrel. Mirrors reactor-api's
502
+ * import-resolver: import.meta.resolve, then the project's node_modules via
503
+ * the package.json exports map / dist layout.
504
+ */
505
+ async function resolveModelModuleFile(source) {
506
+ if (isFsPath(source)) return resolveFromPackageDir(source, "./document-models");
507
+ const specifier = BASE_MODEL_SPECIFIERS.includes(source) ? source : `${source}/document-models`;
508
+ try {
509
+ const resolved = import.meta.resolve(specifier);
510
+ if (resolved.startsWith("file:")) return fileURLToPath(resolved);
511
+ } catch {}
512
+ const { packageName, subpath } = splitSpecifier(specifier);
513
+ const packageDir = path.join(process.cwd(), "node_modules", packageName);
514
+ if (!existsSync(packageDir)) return null;
515
+ let realDir;
516
+ try {
517
+ realDir = realpathSync(packageDir);
518
+ } catch {
519
+ return null;
520
+ }
521
+ return resolveFromPackageDir(realDir, subpath);
522
+ }
523
+ function splitSpecifier(specifier) {
524
+ const parts = specifier.split("/");
525
+ const packageSegments = specifier.startsWith("@") ? 2 : 1;
526
+ const packageName = parts.slice(0, packageSegments).join("/");
527
+ const rest = parts.slice(packageSegments).join("/");
528
+ return {
529
+ packageName,
530
+ subpath: rest ? `./${rest}` : "."
531
+ };
532
+ }
533
+ /** Exports-map lookup (import condition), then conventional dist paths. */
534
+ async function resolveFromPackageDir(packageDir, subpath) {
535
+ const fromManifest = await resolveViaManifest(packageDir, subpath);
536
+ if (fromManifest) return fromManifest;
537
+ const candidates = subpath === "." ? [path.join(packageDir, "dist", "index.js")] : [
538
+ path.join(packageDir, "dist", subpath.slice(2), "index.js"),
539
+ path.join(packageDir, "dist", `${subpath.slice(2)}.js`),
540
+ path.join(packageDir, subpath.slice(2), "index.js")
541
+ ];
542
+ for (const candidate of candidates) if (existsSync(candidate)) return candidate;
543
+ return null;
544
+ }
545
+ async function resolveViaManifest(packageDir, subpath) {
546
+ let raw;
547
+ try {
548
+ raw = await readFile(path.join(packageDir, "package.json"), "utf8");
549
+ } catch {
550
+ return null;
551
+ }
552
+ let manifest;
553
+ try {
554
+ manifest = JSON.parse(raw);
555
+ } catch {
556
+ return null;
557
+ }
558
+ let entry;
559
+ const exportsField = manifest.exports;
560
+ if (exportsField !== null && typeof exportsField === "object" && !Array.isArray(exportsField)) {
561
+ const map = exportsField;
562
+ entry = Object.keys(map).some((key) => key.startsWith(".")) ? map[subpath] : subpath === "." ? map : void 0;
563
+ } else if (typeof exportsField === "string" && subpath === ".") entry = exportsField;
564
+ let target = pickImportTarget(entry);
565
+ if (!target && subpath === "." && typeof manifest.main === "string") target = manifest.main;
566
+ if (!target) return null;
567
+ const resolved = path.join(packageDir, target);
568
+ return existsSync(resolved) ? resolved : null;
569
+ }
570
+ function pickImportTarget(entry) {
571
+ if (typeof entry === "string") return entry;
572
+ if (typeof entry !== "object" || entry === null) return null;
573
+ const conditions = entry;
574
+ for (const condition of [
575
+ "import",
576
+ "node",
577
+ "default"
578
+ ]) {
579
+ const value = conditions[condition];
580
+ if (typeof value === "string") return value;
581
+ }
582
+ return null;
583
+ }
584
+ function isFsPath(identifier) {
585
+ return path.isAbsolute(identifier) || identifier.startsWith("./") || identifier.startsWith("../");
586
+ }
587
+ function parseWorkerCount(raw) {
588
+ if (raw === void 0 || raw.trim() === "") return;
589
+ if (raw.trim().toLowerCase() === "auto") return "auto";
590
+ const value = Number.parseInt(raw, 10);
591
+ if (!Number.isInteger(value) || value < 0 || String(value) !== raw.trim()) throw new Error(`REACTOR_WORKERS must be "auto" or a non-negative integer, got "${raw}"`);
592
+ return value;
593
+ }
594
+ function parseNonNegativeInt(raw, name) {
595
+ if (raw === void 0 || raw.trim() === "") return;
596
+ const value = Number.parseInt(raw, 10);
597
+ if (!Number.isInteger(value) || value < 0 || String(value) !== raw.trim()) throw new Error(`${name} must be a non-negative integer, got "${raw}"`);
598
+ return value;
599
+ }
600
+ //#endregion
412
601
  //#region src/feature-flags.ts
413
602
  async function initFeatureFlags() {
414
603
  const provider = new EnvVarProvider();
@@ -697,6 +886,15 @@ async function initServer(serverPort, options, renown) {
697
886
  }
698
887
  const reactorPgliteMajor = reactorPgliteDir ? resolvePgliteMajorForDir(reactorPgliteDir) : null;
699
888
  const readModelPgliteMajor = readModelPgliteDir ? resolvePgliteMajorForDir(readModelPgliteDir) : null;
889
+ let workerPool = resolveWorkerPoolOptions(options.workerPool, process.env);
890
+ if (workerPool && options.reactor) {
891
+ logger.warn("Worker pool configuration ignored: the caller-provided reactor owns its own executor");
892
+ workerPool = null;
893
+ }
894
+ if (workerPool) {
895
+ if (dev) throw new Error("The executor worker pool (REACTOR_WORKERS) is not supported in dev mode: Vite-loaded document models cannot cross a worker-thread boundary");
896
+ if (!reactorDbUrl || !isPostgresUrl(reactorDbUrl)) throw new Error("The executor worker pool (REACTOR_WORKERS) requires a Postgres reactor database — set PH_REACTOR_DATABASE_URL or PH_SWITCHBOARD_DATABASE_URL. PGlite cannot be shared across worker threads.");
897
+ }
700
898
  const apiRef = { current: void 0 };
701
899
  let driveNodeView;
702
900
  const config = getConfig(options.configFile ?? path$1.join(process.cwd(), "powerhouse.config.json"));
@@ -732,6 +930,7 @@ async function initServer(serverPort, options, renown) {
732
930
  const maxSkipThreshold = parseInt(process.env.MAX_SKIP_THRESHOLD ?? "", 10);
733
931
  const hasSkipThreshold = !isNaN(maxSkipThreshold) && maxSkipThreshold > 0;
734
932
  if (hasSkipThreshold) logger.info(`Reactor maxSkipThreshold set to ${maxSkipThreshold}`);
933
+ if (hasSkipThreshold && workerPool) logger.warn("MAX_SKIP_THRESHOLD is not forwarded to executor workers and has no effect in worker-pool mode");
735
934
  const reactorBuilder = new ReactorBuilder().withEventBus(new EventBus()).withKysely(baseKysely);
736
935
  const clientBuilder = new ReactorClientBuilder().withReactorBuilder(reactorBuilder);
737
936
  const vetraDocumentModels = dev ? Object.values(await import("@powerhousedao/vetra/document-models")).filter((m) => typeof m === "object" && m !== null && "documentModel" in m && "reducer" in m) : [];
@@ -742,6 +941,15 @@ async function initServer(serverPort, options, renown) {
742
941
  logger: reactorLogger,
743
942
  signer: renown ? getRenownSignerConfig(renown, options.identity?.requireSignatures) : void 0
744
943
  });
944
+ if (workerPool) {
945
+ if (!reactorDbUrl) throw new Error("unreachable: worker pool enabled without a reactor database URL");
946
+ const workerSources = await resolveWorkerModelSources(packages, reactorLogger);
947
+ reactorBuilder.withDocumentModelSources(workerSources).withWorkerPool({
948
+ numWorkers: workerPool.numWorkers,
949
+ db: buildWorkerDbConfig(reactorDbUrl, workerPool)
950
+ });
951
+ reactorLogger.info(`Executor worker pool enabled: ${workerPool.numWorkers} worker threads${workerPool.mode === "auto" ? " (auto-sized from cores)" : ""}`);
952
+ }
745
953
  reactorBuilder.withReadModelFactory(async ({ operationIndex, writeCache, processorManagerConsistencyTracker }) => {
746
954
  const nodeProcessor = new NodeProcessor(baseKysely, REACTOR_SCHEMA, operationIndex, writeCache, processorManagerConsistencyTracker);
747
955
  await nodeProcessor.init();
@@ -949,5 +1157,5 @@ if (import.meta.main) await startSwitchboard();
949
1157
  //#endregion
950
1158
  export { parseForcePgVersion as a, applySwitchboardReactorDefaults as i, isPortAvailable as n, startSwitchboard as r, deriveAttachmentServiceConfig as t };
951
1159
 
952
- //# sourceMappingURL=server-DgBz3TJW.mjs.map
953
- //# debugId=8a8055ab-2ceb-59fe-9007-f1d1fde8a019
1160
+ //# sourceMappingURL=server-Cgud_ONM.mjs.map
1161
+ //# debugId=5eb0a978-689a-51dd-978a-cd3d7fe5df4b