@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.
- package/.env +13 -0
- package/.tsbuild/test/attachments/auth.test.d.ts +2 -0
- package/.tsbuild/test/attachments/auth.test.d.ts.map +1 -0
- package/.tsbuild/test/attachments/index.test.d.ts +2 -0
- package/.tsbuild/test/attachments/index.test.d.ts.map +1 -0
- package/.tsbuild/test/attachments/routes-integration.test.d.ts +2 -0
- package/.tsbuild/test/attachments/routes-integration.test.d.ts.map +1 -0
- package/.tsbuild/test/attachments/routes.test.d.ts +2 -0
- package/.tsbuild/test/attachments/routes.test.d.ts.map +1 -0
- package/.tsbuild/test/attachments/service-config.test.d.ts +2 -0
- package/.tsbuild/test/attachments/service-config.test.d.ts.map +1 -0
- package/.tsbuild/test/metrics.test.d.ts +2 -0
- package/.tsbuild/test/metrics.test.d.ts.map +1 -0
- package/.tsbuild/test/pglite-dialect.test.d.ts +2 -0
- package/.tsbuild/test/pglite-dialect.test.d.ts.map +1 -0
- package/.tsbuild/test/pglite-version.test.d.ts +2 -0
- package/.tsbuild/test/pglite-version.test.d.ts.map +1 -0
- package/.tsbuild/test/worker-pool.test.d.ts +2 -0
- package/.tsbuild/test/worker-pool.test.d.ts.map +1 -0
- package/.tsbuild/tsconfig.tsbuildinfo +1 -0
- package/.tsbuild/tsdown.config.d.ts +3 -0
- package/.tsbuild/tsdown.config.d.ts.map +1 -0
- package/.tsbuild/vitest.config.d.ts +3 -0
- package/.tsbuild/vitest.config.d.ts.map +1 -0
- package/CHANGELOG.md +59 -0
- package/README.md +44 -0
- package/dist/index.mjs +1 -1
- package/dist/{server-DgBz3TJW.mjs → server-Cgud_ONM.mjs} +216 -8
- package/dist/server-Cgud_ONM.mjs.map +1 -0
- package/dist/server.d.mts +13 -9
- package/dist/server.d.mts.map +1 -1
- package/dist/server.mjs +3 -3
- package/package.json +12 -12
- package/test/worker-pool.test.ts +232 -0
- package/dist/server-DgBz3TJW.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tsdown.config.d.ts","sourceRoot":"","sources":["../tsdown.config.ts"],"names":[],"mappings":";AAEA,wBAaG"}
|
|
@@ -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-
|
|
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]="
|
|
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
|
-
|
|
396
|
-
|
|
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.
|
|
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-
|
|
953
|
-
//# debugId=
|
|
1160
|
+
//# sourceMappingURL=server-Cgud_ONM.mjs.map
|
|
1161
|
+
//# debugId=5eb0a978-689a-51dd-978a-cd3d7fe5df4b
|