@powerhousedao/switchboard 6.2.3-dev.43 → 6.2.3-dev.45

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 (37) hide show
  1. package/.tsbuild/test/durable-node-fs-pg16.test.d.ts +2 -0
  2. package/.tsbuild/test/durable-node-fs-pg16.test.d.ts.map +1 -0
  3. package/.tsbuild/test/observability-signals.test.d.ts +2 -0
  4. package/.tsbuild/test/observability-signals.test.d.ts.map +1 -0
  5. package/.tsbuild/test/pglite-preflight.test.d.ts +2 -0
  6. package/.tsbuild/test/pglite-preflight.test.d.ts.map +1 -0
  7. package/.tsbuild/test/snapshot-writer.d.ts +18 -0
  8. package/.tsbuild/test/snapshot-writer.d.ts.map +1 -0
  9. package/.tsbuild/test/workflow-reactor-access.test.d.ts +2 -0
  10. package/.tsbuild/test/workflow-reactor-access.test.d.ts.map +1 -0
  11. package/.tsbuild/test/workflow-reactor-enforcement.test.d.ts +2 -0
  12. package/.tsbuild/test/workflow-reactor-enforcement.test.d.ts.map +1 -0
  13. package/.tsbuild/tsconfig.tsbuildinfo +1 -1
  14. package/CHANGELOG.md +43 -0
  15. package/dist/index.mjs +42 -9
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/{server-CMAxYG6D.mjs → server-Dvw8_NeM.mjs} +271 -79
  18. package/dist/server-Dvw8_NeM.mjs.map +1 -0
  19. package/dist/server.d.mts +3 -1
  20. package/dist/server.d.mts.map +1 -1
  21. package/dist/server.mjs +3 -3
  22. package/package.json +15 -16
  23. package/test/boot-unsupported-stored-documents.test.ts +5 -5
  24. package/test/durable-node-fs-pg16.test.ts +80 -0
  25. package/test/fatal-shutdown.test.ts +33 -0
  26. package/test/observability-signals.test.ts +131 -0
  27. package/test/pglite-dialect.test.ts +28 -0
  28. package/test/pglite-preflight.test.ts +313 -0
  29. package/test/pglite-version.test.ts +9 -1
  30. package/test/snapshot-writer.ts +94 -0
  31. package/test/worker-pool.test.ts +27 -1
  32. package/test/workflow/resolvers.test.ts +26 -0
  33. package/test/workflow-reactor-access.test.ts +384 -0
  34. package/test/workflow-reactor-enforcement.test.ts +805 -0
  35. package/test/workflow-runtime.test.ts +53 -0
  36. package/tsconfig.json +0 -3
  37. package/dist/server-CMAxYG6D.mjs.map +0 -1
@@ -1,5 +1,5 @@
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]="e04a905d-8b84-5d81-aff7-5e3f6387cda0")}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]="b9c02424-1406-5d63-abbd-6c32caa53205")}catch(e){}}();
3
3
  import { n as addDefaultReactorDrive, r as isPostgresUrl, t as addDefaultDrive } from "./utils-uaBlzyjS.mjs";
4
4
  import { createRequire, register } from "node:module";
5
5
  import { ReactorInstrumentation } from "@powerhousedao/opentelemetry-instrumentation-reactor";
@@ -9,7 +9,7 @@ import dotenv from "dotenv";
9
9
  import { getConfig } from "@powerhousedao/config/node";
10
10
  import { existsSync, promises, realpathSync } from "node:fs";
11
11
  import path, { resolve } from "node:path";
12
- import { AtomicNodeFs } from "@powerhousedao/pglite-fs";
12
+ import { createDurableNodeFs, preparePgliteDataDir, removeStalePgliteFiles, resolvePgliteFsync, syncTree } from "@powerhousedao/reactor-api/pglite-node";
13
13
  import { ChannelScheme, DriveCollectionId, EventBus, REACTOR_SCHEMA, ReactorBuilder, ReactorClientBuilder, UnsupportedStoredProtocolError, createHybridProjectionCoordinatorFactory, instrumentPgPool, parseDriveUrl, supportsLiveReadModelRegistration } from "@powerhousedao/reactor";
14
14
  import { AuthorizationPolicy, BaseSubgraph, ForbiddenError, HttpPackageLoader, ImportPackageLoader, PGLITE_UTC_PARSERS, PackageManagementService, PackagesSubgraph, PrivacyPermissionAdapter, callerSubject, createCanonicalDocumentIdResolver, getUniqueDocumentModels, initializeAndStartAPI, resolveRenownConfig } from "@powerhousedao/reactor-api";
15
15
  import { httpsHooksPath } from "@powerhousedao/reactor-api/https-hooks";
@@ -29,7 +29,7 @@ import { driveDocumentModelModule } from "@powerhousedao/shared/document-drive";
29
29
  import { DEFAULT_SIGNATURE_POLICY, isSignaturePolicy } from "@powerhousedao/shared/document-model";
30
30
  import { readFile } from "node:fs/promises";
31
31
  import os from "node:os";
32
- import { fileURLToPath } from "node:url";
32
+ import { fileURLToPath, pathToFileURL } from "node:url";
33
33
  import { EnvVarProvider } from "@openfeature/env-var-provider";
34
34
  import { OpenFeature } from "@openfeature/server-sdk";
35
35
  import { GraphQLError, Kind } from "graphql";
@@ -65,6 +65,19 @@ async function loadPGliteModule(major) {
65
65
  if (major === 16) return await import("pglite-legacy-02");
66
66
  return import("@electric-sql/pglite");
67
67
  }
68
+ /** The stock NodeFS class of the PGlite module that opens `major` dirs. */
69
+ async function loadNodeFsClass(major) {
70
+ if (major === 16) return (await import("pglite-legacy-02/nodefs")).NodeFS;
71
+ return (await import("@electric-sql/pglite/nodefs")).NodeFS;
72
+ }
73
+ /** Opens a converted dir over stock NodeFS so the conversion can check it. */
74
+ async function openForVerify(major, dataDir) {
75
+ if (!isSupportedMajor(major)) throw new Error(`Cannot verify PGlite data dir ${dataDir}: PG_VERSION=${major} is not supported (expected one of ${SUPPORTED_PG_MAJORS.join(", ")})`);
76
+ const [{ PGlite }, NodeFS] = await Promise.all([loadPGliteModule(major), loadNodeFsClass(major)]);
77
+ const pg = new PGlite({ fs: new NodeFS(dataDir) });
78
+ await pg.waitReady;
79
+ return pg;
80
+ }
68
81
  async function loadPgDump(major) {
69
82
  if (major === 16) return (await import("pglite-tools-legacy-02/pg_dump")).pgDump;
70
83
  return (await import("@electric-sql/pglite-tools/pg_dump")).pgDump;
@@ -798,9 +811,11 @@ const fatalHandlers = /* @__PURE__ */ new WeakMap();
798
811
  /**
799
812
  * Sends an uncaught exception or unhandled rejection through the SIGTERM
800
813
  * shutdown that `withSignalHandlers()` installs, so the reactor and read-model
801
- * PGlite stores write their snapshots before the process exits with code 1.
802
- * Node's default is to exit at once, which drops every write AtomicNodeFs has
803
- * not flushed yet. A shutdown that hangs is cut off after FORCED_EXIT_MS.
814
+ * PGlite stores close with a shutdown checkpoint before the process exits
815
+ * with code 1. Node's default is to exit at once, which leaves both stores
816
+ * to WAL recovery on the next open. A store whose wasm runtime aborted
817
+ * (ENOSPC) rejects its close; the builder logs that and still exits. A
818
+ * shutdown that hangs is cut off after FORCED_EXIT_MS.
804
819
  *
805
820
  * A rejection is fatal only when this is its sole listener: Node exits on an
806
821
  * unhandled rejection only when nothing listens for it, and Sentry's
@@ -810,13 +825,26 @@ function installFatalErrorShutdown(logger, proc = process) {
810
825
  if (fatalHandlers.has(proc)) return;
811
826
  const realExit = proc.exit.bind(proc);
812
827
  let shuttingDown = false;
828
+ /**
829
+ * Logs without ever throwing. onFatal runs inside the uncaughtException
830
+ * handler, so a logger whose transport is gone (EPIPE on a closed stdout)
831
+ * would otherwise throw from the handler that reports throws, killing the
832
+ * process before the shutdown it is supposed to run. A logging failure is
833
+ * swallowed: there is nowhere left to report it.
834
+ */
835
+ const logFatal = (message, err) => {
836
+ try {
837
+ if (err === void 0) logger.error(message);
838
+ else logger.error(message, err);
839
+ } catch {}
840
+ };
813
841
  const onFatal = (kind, err) => {
814
- logger.error(`${kind}: @error`, err);
842
+ logFatal(`${kind}: @error`, err);
815
843
  if (shuttingDown) return;
816
844
  shuttingDown = true;
817
845
  proc.exitCode = 1;
818
846
  setTimeout(() => {
819
- logger.error("Shutdown after fatal error timed out; exiting");
847
+ logFatal("Shutdown after fatal error timed out; exiting");
820
848
  realExit(1);
821
849
  }, FORCED_EXIT_MS).unref();
822
850
  proc.kill(proc.pid, "SIGTERM");
@@ -1035,6 +1063,20 @@ const BASE_MODEL_SPECIFIERS = [
1035
1063
  "@powerhousedao/reactor-drive",
1036
1064
  "@powerhousedao/reactor-group/document-models"
1037
1065
  ];
1066
+ function isModelModule(value) {
1067
+ const candidate = value;
1068
+ return typeof candidate?.reducer === "function" && typeof candidate.documentModel?.global?.id === "string";
1069
+ }
1070
+ async function keepImportableSources(sources, logger) {
1071
+ const kept = [];
1072
+ for (const source of sources) try {
1073
+ const exports = await import(pathToFileURL(source.filePath).href);
1074
+ if (Object.values(exports).some(isModelModule)) kept.push(source);
1075
+ } catch (error) {
1076
+ logger.warn(`Worker model sources: "${source.filePath}" does not import, so workflow pieces will not see its models: ${String(error)}`);
1077
+ }
1078
+ return kept;
1079
+ }
1038
1080
  /**
1039
1081
  * Resolves base models and configured package identifiers to `{ filePath }`
1040
1082
  * sources for the reactor builder. Resolution to absolute paths happens here
@@ -1349,6 +1391,7 @@ function toStepRecord(row) {
1349
1391
  output: parseJson(row.output),
1350
1392
  port: row.port,
1351
1393
  error: row.error,
1394
+ errorName: row.error_name,
1352
1395
  startedAt: row.started_at,
1353
1396
  endedAt: row.ended_at,
1354
1397
  pieceVersion: row.piece_version,
@@ -1367,6 +1410,7 @@ function toRunRecord(row, steps) {
1367
1410
  triggerPayload: parseJson(row.trigger_payload),
1368
1411
  status: row.status,
1369
1412
  error: row.error,
1413
+ errorName: row.error_name,
1370
1414
  startedAt: row.started_at,
1371
1415
  endedAt: row.ended_at,
1372
1416
  rerunOf: row.rerun_of,
@@ -1375,6 +1419,14 @@ function toRunRecord(row, steps) {
1375
1419
  steps: steps.map(toStepRecord)
1376
1420
  };
1377
1421
  }
1422
+ const UNKNOWN_ACCESS = {
1423
+ authEnforcement: true,
1424
+ authConditions: false,
1425
+ identity: null
1426
+ };
1427
+ function signedIn(ctx) {
1428
+ return Boolean(ctx.user?.address);
1429
+ }
1378
1430
  function isAbsolute(url) {
1379
1431
  return /^https?:\/\//i.test(url);
1380
1432
  }
@@ -1403,13 +1455,21 @@ function returnUrlFor(requested, ctx) {
1403
1455
  if (!url || !origin || url.origin !== origin) throw new GraphQLError("returnUrl must be on the requesting page's origin");
1404
1456
  return url.href;
1405
1457
  }
1406
- const getResolvers = (runtime, authorizationService, oauth) => {
1458
+ const getResolvers = (runtime, authorizationService, oauth, access = UNKNOWN_ACCESS, assertCanRead) => {
1407
1459
  return {
1408
1460
  Query: { workflowRuntime: () => ({}) },
1409
1461
  WorkflowRuntimeQueries: {
1410
1462
  health: () => "ok",
1463
+ authEnforcement: () => access.authEnforcement,
1464
+ reactorIdentity: (_parent, _args, ctx) => signedIn(ctx) ? access.identity : null,
1465
+ authConditions: (_parent, _args, ctx) => signedIn(ctx) ? access.authConditions : null,
1466
+ reactorAccessDenial: async (_parent, args, ctx) => {
1467
+ if (!assertCanRead) throw new GraphQLError("No read check configured");
1468
+ await assertCanRead(args.workflowId, ctx);
1469
+ return runtime.reactorAccessDenial(args.workflowId) ?? null;
1470
+ },
1411
1471
  blockDescriptor: (_parent, args) => runtime.blockDescriptor(blockRef(args.block)),
1412
- blockOptions: (_parent, args, ctx) => runtime.blockOptions(blockRef(args.block), args.propName, args.input, args.connectionId ?? void 0, ctx, args.searchValue ?? void 0),
1472
+ blockOptions: (_parent, args, ctx) => runtime.blockOptions(blockRef(args.block), args.propName, args.input, args.connectionId ?? void 0, ctx, args.searchValue ?? void 0, args.reactorConnectionId ?? void 0),
1413
1473
  pieceCatalog: () => runtime.pieceCatalog(),
1414
1474
  pieceActions: (_parent, args) => runtime.pieceActions(args.packageName, args.version ?? void 0),
1415
1475
  pieceTriggers: (_parent, args) => runtime.pieceTriggers(args.packageName, args.version ?? void 0),
@@ -1520,6 +1580,28 @@ const schema = gql`
1520
1580
  type WorkflowRuntimeQueries {
1521
1581
  health: String!
1522
1582
  """
1583
+ Whether this reactor enforces document auth (REACTOR_AUTH_ENFORCEMENT).
1584
+ On, a workflow needs a signed publish, and the host grants, to reach
1585
+ documents. Answered to any caller.
1586
+ """
1587
+ authEnforcement: Boolean!
1588
+ """
1589
+ Who this host signs as: grant it on each document a step writes. Null for
1590
+ a caller who is not signed in, or a host with no signer.
1591
+ """
1592
+ reactorIdentity: ReactorIdentity
1593
+ """
1594
+ REACTOR_AUTH_CONDITIONS. True: grant the host by key, with
1595
+ { match: subject.key == key }; false: by address. Null for a caller who
1596
+ is not signed in.
1597
+ """
1598
+ authConditions: Boolean
1599
+ """
1600
+ Why the workflow's last publish or enable got no reactor access, naming
1601
+ the publisher and the connection; null when the check passed.
1602
+ """
1603
+ reactorAccessDenial(workflowId: String!): String
1604
+ """
1523
1605
  Persisted runs, newest first. Scope them to one workflow, or to every
1524
1606
  workflow a drive holds; workflowId wins when both are given.
1525
1607
  """
@@ -1560,6 +1642,8 @@ const schema = gql`
1560
1642
  connectionId: String
1561
1643
  "What the author typed, for a DROPDOWN declared with refreshOnSearch."
1562
1644
  searchValue: String
1645
+ "The step's reactor connection; its filter narrows what the resolver reads."
1646
+ reactorConnectionId: String
1563
1647
  ): Unknown
1564
1648
  """
1565
1649
  Every piece this reactor offers: the core piece, its package pieces, and
@@ -1737,6 +1821,13 @@ const schema = gql`
1737
1821
  accountLabel: String
1738
1822
  }
1739
1823
 
1824
+ type ReactorIdentity {
1825
+ "The address the host signs as; null without a signed-in identity."
1826
+ address: String
1827
+ "The did:key that signs the host's operations."
1828
+ key: String!
1829
+ }
1830
+
1740
1831
  type ConnectionCheckResult {
1741
1832
  ok: Boolean!
1742
1833
  detail: String
@@ -1798,6 +1889,8 @@ const schema = gql`
1798
1889
  output: Unknown
1799
1890
  port: String
1800
1891
  error: String
1892
+ "The thrown error's name, e.g. ReactorAccessDeniedError."
1893
+ errorName: String
1801
1894
  startedAt: String
1802
1895
  endedAt: String
1803
1896
  "The piece version that ran; null for an unresolved block."
@@ -1819,6 +1912,7 @@ const schema = gql`
1819
1912
  triggerPayload: Unknown
1820
1913
  status: String!
1821
1914
  error: String
1915
+ errorName: String
1822
1916
  "When the run began executing; a PENDING run's is when it was journaled."
1823
1917
  startedAt: String!
1824
1918
  endedAt: String
@@ -1856,12 +1950,14 @@ const schema = gql`
1856
1950
  output: Unknown
1857
1951
  port: String
1858
1952
  error: String
1953
+ errorName: String
1859
1954
  }
1860
1955
 
1861
1956
  type WorkflowRunPayload {
1862
1957
  runId: String
1863
1958
  status: String!
1864
1959
  error: String
1960
+ errorName: String
1865
1961
  steps: [WorkflowStepRun!]!
1866
1962
  }
1867
1963
 
@@ -1872,6 +1968,7 @@ const schema = gql`
1872
1968
  status: String!
1873
1969
  output: Unknown
1874
1970
  error: String
1971
+ errorName: String
1875
1972
  durationMs: Int!
1876
1973
  }
1877
1974
 
@@ -1956,11 +2053,11 @@ const schema = gql`
1956
2053
  //#region src/workflow/subgraph.ts
1957
2054
  /** The runtime's read/write surface. The runtime itself is composed by the
1958
2055
  * host, so the subgraph only serves what it is handed. */
1959
- function createWorkflowRuntimeSubgraph(runtime, oauth) {
2056
+ function createWorkflowRuntimeSubgraph(runtime, oauth, access) {
1960
2057
  return class WorkflowRuntimeSubgraph extends BaseSubgraph {
1961
2058
  name = "workflow-runtime";
1962
2059
  typeDefs = schema;
1963
- resolvers = getResolvers(runtime, this.authorizationService, oauth);
2060
+ resolvers = getResolvers(runtime, this.authorizationService, oauth, access, (documentId, ctx) => this.assertCanRead(documentId, ctx));
1964
2061
  additionalContextFields = {};
1965
2062
  };
1966
2063
  }
@@ -1986,6 +2083,12 @@ async function assertWorkflowPackageLoadable(load = () => import("@powerhousedao
1986
2083
  throw new Error(`Workflows are enabled but ${WORKFLOW_PACKAGE_NAME} could not be loaded`, { cause: error });
1987
2084
  }
1988
2085
  }
2086
+ function modelManifestSource(builder) {
2087
+ return {
2088
+ modelManifest: () => builder.getResolvedModelManifest() ?? [],
2089
+ modelEntries: (documentType) => builder.getImportableEntries(documentType)
2090
+ };
2091
+ }
1989
2092
  function readAssertion(authorizationService, reactorClient) {
1990
2093
  const resolveCanonical = createCanonicalDocumentIdResolver(reactorClient);
1991
2094
  const hostCanRead = async (identifier, ctx) => {
@@ -2027,6 +2130,37 @@ function writeAssertion(authorizationService, reactorClient) {
2027
2130
  if (!await authorizationService.canWrite(documentId, ctx.user?.address)) throw new ForbiddenError("to write this document");
2028
2131
  };
2029
2132
  }
2133
+ /** What the reactor enforces and who it signs as; unknown counts as enforced. */
2134
+ function reactorAccessOf(clientModule) {
2135
+ const flags = clientModule?.reactorModule?.featureFlags;
2136
+ const signer = clientModule?.signer;
2137
+ const key = signer?.app?.key;
2138
+ const identity = key ? {
2139
+ address: signer.user?.address ?? null,
2140
+ key
2141
+ } : null;
2142
+ return {
2143
+ authEnforcement: flags ? flags.authEnforcement : true,
2144
+ authConditions: flags?.authConditions === true,
2145
+ identity
2146
+ };
2147
+ }
2148
+ /** Whom a run's created documents also grant: the host, as §8 grants it. */
2149
+ function hostPrincipalOf(access) {
2150
+ const identity = access.identity;
2151
+ if (!identity) return void 0;
2152
+ if (access.authConditions) return { match: { eq: [{ attr: "subject.key" }, { lit: identity.key }] } };
2153
+ return identity.address ? { address: identity.address } : void 0;
2154
+ }
2155
+ /** Who the host signs as, so a publish it signs makes no run user. */
2156
+ function hostIdentityOf(access) {
2157
+ const identity = access.identity;
2158
+ if (!identity) return void 0;
2159
+ return identity.address ? {
2160
+ address: identity.address,
2161
+ key: identity.key
2162
+ } : { key: identity.key };
2163
+ }
2030
2164
  /** A step reads any well-formed attachment, as it reads any document: a run
2031
2165
  * carries no caller to check against. */
2032
2166
  function canReadAttachmentRef(_documentId, ref) {
@@ -2069,6 +2203,8 @@ async function composeWorkflowRuntime(deps) {
2069
2203
  }
2070
2204
  engine.setPieceRegistryUrl(deps.pieceRegistryUrl);
2071
2205
  if (deps.pieces) bindPackagePieces(engine.packagePieces, deps.pieces);
2206
+ const access = reactorAccessOf(deps.clientModule);
2207
+ const models = deps.models;
2072
2208
  const runtime = engine.createWorkflowRuntime({
2073
2209
  relationalDb: deps.relationalDb,
2074
2210
  secretsKeyFile: deps.secretsKeyFile,
@@ -2076,6 +2212,13 @@ async function composeWorkflowRuntime(deps) {
2076
2212
  assertCanRead: readAssertion(deps.authorizationService, deps.reactorClient),
2077
2213
  subjectOf: (caller) => callerSubject(caller.user),
2078
2214
  assertCanWrite: writeAssertion(deps.authorizationService, deps.reactorClient),
2215
+ ...deps.clientModule?.reactorModule ? { authEnforcement: access.authEnforcement } : {},
2216
+ hostPrincipal: hostPrincipalOf(access),
2217
+ hostIdentity: hostIdentityOf(access),
2218
+ ...models ? {
2219
+ modelManifest: () => models.modelManifest(),
2220
+ modelEntries: (documentType) => models.modelEntries(documentType)
2221
+ } : {},
2079
2222
  webhooks: deps.webhooks,
2080
2223
  attachments: deps.attachments,
2081
2224
  canReadAttachmentRef,
@@ -2087,7 +2230,7 @@ async function composeWorkflowRuntime(deps) {
2087
2230
  let stopped = false;
2088
2231
  const oauthCallback = deps.http ? registerOAuthCallback(deps.http, runtime) : void 0;
2089
2232
  return {
2090
- subgraph: createWorkflowRuntimeSubgraph(runtime, deps.http ? { callbackUrl: callbackUrlOf(deps.http) } : void 0),
2233
+ subgraph: createWorkflowRuntimeSubgraph(runtime, deps.http ? { callbackUrl: callbackUrlOf(deps.http) } : void 0, access),
2091
2234
  triggers,
2092
2235
  async start() {
2093
2236
  await runtime.registerWebhookEndpoint();
@@ -2116,7 +2259,12 @@ var ClosablePGliteDialect = class extends PGliteDialect {
2116
2259
  const innerDestroy = driver.destroy.bind(driver);
2117
2260
  driver.destroy = async () => {
2118
2261
  await innerDestroy();
2119
- if (!pglite.closed) await pglite.close();
2262
+ if (pglite.closed) return;
2263
+ try {
2264
+ await pglite.close();
2265
+ } catch (err) {
2266
+ if (!/PGlite aborted/.test(String(err))) throw err;
2267
+ }
2120
2268
  };
2121
2269
  return driver;
2122
2270
  }
@@ -2171,7 +2319,15 @@ async function migratePgliteDir(dataDir, logger) {
2171
2319
  try {
2172
2320
  const [legacyMod, pgDump] = await Promise.all([loadPGliteModule(major), loadPgDump(major)]);
2173
2321
  const LegacyPGlite = legacyMod.PGlite;
2174
- const pg = new LegacyPGlite(backupDir);
2322
+ const onDisk = new LegacyPGlite(backupDir);
2323
+ let tar;
2324
+ try {
2325
+ await onDisk.waitReady;
2326
+ tar = await onDisk.dumpDataDir("none");
2327
+ } finally {
2328
+ await onDisk.close();
2329
+ }
2330
+ const pg = new LegacyPGlite({ loadDataDir: tar });
2175
2331
  try {
2176
2332
  await pg.waitReady;
2177
2333
  sql = await (await pgDump({ pg })).text();
@@ -2201,6 +2357,7 @@ async function migratePgliteDir(dataDir, logger) {
2201
2357
  } finally {
2202
2358
  await pg.close();
2203
2359
  }
2360
+ syncTree(dataDir);
2204
2361
  } catch (err) {
2205
2362
  await rollback(dataDir, backupDir, err, logger);
2206
2363
  throw err;
@@ -2221,6 +2378,59 @@ async function rollback(dataDir, backupDir, originalError, logger) {
2221
2378
  logger.error(`[pglite-migration] Migration failed for ${dataDir}; rolled back from ${backupDir}. Original error: ${String(originalError)}`);
2222
2379
  }
2223
2380
  //#endregion
2381
+ //#region src/pglite-preflight.ts
2382
+ const RM_OPTIONS = {
2383
+ recursive: true,
2384
+ force: true,
2385
+ maxRetries: 10,
2386
+ retryDelay: 100
2387
+ };
2388
+ /**
2389
+ * Prepares every local PGlite dir before the first open and returns the
2390
+ * PG major detected on disk per dir. Conversion of an AtomicNodeFs snapshot
2391
+ * runs for every dir, before the major is read, so a pure-snapshot PG16 dir
2392
+ * is converted and then migrated.
2393
+ */
2394
+ async function runPglitePreflight(options) {
2395
+ const { dirs, logger } = options;
2396
+ const detectedMajors = /* @__PURE__ */ new Map();
2397
+ if (options.inMemory || dirs.length === 0) return detectedMajors;
2398
+ if (options.forcePgVersion !== void 0) {
2399
+ if (options.migratePglite) logger.warn("PH_FORCE_PG_VERSION is set; ignoring --migrate-pglite/PH_MIGRATE_PGLITE because the data dirs will be wiped.");
2400
+ logger.warn(`PH_FORCE_PG_VERSION=${options.forcePgVersion} set; wiping PGLite data dirs and re-initializing at PG${options.forcePgVersion}.`);
2401
+ for (const dir of dirs) {
2402
+ for (const p of [
2403
+ dir,
2404
+ `${dir}.converting`,
2405
+ `${dir}.old`
2406
+ ]) await promises.rm(p, RM_OPTIONS);
2407
+ logger.info(`Wiped PGLite data dir ${dir}`);
2408
+ }
2409
+ return detectedMajors;
2410
+ }
2411
+ const deps = {
2412
+ openForVerify: options.openForVerify ?? openForVerify,
2413
+ logger
2414
+ };
2415
+ for (const dir of dirs) {
2416
+ await preparePgliteDataDir(dir, deps);
2417
+ const major = await readPgVersionFile(dir);
2418
+ if (major !== null) detectedMajors.set(dir, major);
2419
+ }
2420
+ if (options.migratePglite) for (const [dir, major] of detectedMajors) {
2421
+ if (major === 17) continue;
2422
+ await migratePgliteDir(dir, logger);
2423
+ await removeStalePgliteFiles(dir, logger);
2424
+ const after = await readPgVersionFile(dir);
2425
+ if (after !== null) detectedMajors.set(dir, after);
2426
+ }
2427
+ else for (const [dir, major] of detectedMajors) {
2428
+ if (major === 17) continue;
2429
+ logger.warn(`PGLite data dir at ${dir} was created with PG${major} but Switchboard ships PG17. Running on legacy PGLite. Re-start with --migrate-pglite (or PH_MIGRATE_PGLITE=true) to upgrade.`);
2430
+ }
2431
+ return detectedMajors;
2432
+ }
2433
+ //#endregion
2224
2434
  //#region src/reactor-feature-flags.mts
2225
2435
  /**
2226
2436
  * Enforcement flags from the REACTOR_* env vars. Each flag requires the ones
@@ -2409,14 +2619,29 @@ const DOCUMENT_MODEL_SUBGRAPHS_ENABLED = "DOCUMENT_MODEL_SUBGRAPHS_ENABLED";
2409
2619
  const DOCUMENT_MODEL_SUBGRAPHS_ENABLED_DEFAULT = true;
2410
2620
  const DEFAULT_PORT = process.env.PORT ? Number(process.env.PORT) : 4001;
2411
2621
  const PORT_FALLBACK_ATTEMPTS = 20;
2412
- const PGLITE_FLUSH_INTERVAL_MS = (() => {
2413
- const raw = process.env.PGLITE_FLUSH_INTERVAL_MS;
2414
- if (raw === void 0) return 100;
2415
- const parsed = Number(raw);
2416
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : 100;
2417
- })();
2418
- function onPgliteFlushError(err) {
2419
- triggerFatalShutdown("PGlite snapshot flush failed", err);
2622
+ let flushIntervalWarned = false;
2623
+ function warnIgnoredFlushInterval(logger) {
2624
+ if (flushIntervalWarned) return;
2625
+ if (process.env.PGLITE_FLUSH_INTERVAL_MS === void 0) return;
2626
+ flushIntervalWarned = true;
2627
+ logger.warn("PGLITE_FLUSH_INTERVAL_MS is set but ignored: PGlite stores no longer snapshot on a timer.");
2628
+ }
2629
+ function onPgliteAbort(logger, what) {
2630
+ if (!triggerFatalShutdown("PGlite aborted", what)) logger.error("PGlite aborted: @error", what);
2631
+ }
2632
+ function pgliteFsyncFor(major, dir, logger) {
2633
+ const wanted = resolvePgliteFsync(process.env);
2634
+ if (wanted && major === 16) logger.warn(`PGlite data dir ${dir} is PG16 on pglite-legacy-02, which does not sync commits to disk; only checkpoints are synced until --migrate-pglite upgrades it.`);
2635
+ return wanted;
2636
+ }
2637
+ /** Preloads the module's NodeFS so the returned factory can be synchronous. */
2638
+ async function loadDurableFsFactory(major, logger) {
2639
+ const NodeFS = await loadNodeFsClass(major);
2640
+ return (dir) => createDurableNodeFs(NodeFS, dir, {
2641
+ fsync: pgliteFsyncFor(major, dir, logger),
2642
+ logger,
2643
+ onAbort: (what) => onPgliteAbort(logger, what)
2644
+ });
2420
2645
  }
2421
2646
  const PGLITE_IN_MEMORY = process.env.PH_PGLITE_IN_MEMORY === "1";
2422
2647
  /**
@@ -2463,7 +2688,7 @@ async function resolveServerPort(requested, strictPort, logger) {
2463
2688
  return requested;
2464
2689
  }
2465
2690
  async function createReactorKysely(opts) {
2466
- const { reactorDbUrl, reactorPgliteDir, reactorPgliteMajor, inMemory, flushIntervalMs, hostPoolSize, logger } = opts;
2691
+ const { reactorDbUrl, reactorPgliteDir, reactorPgliteMajor, inMemory, hostPoolSize, logger } = opts;
2467
2692
  if (reactorDbUrl && isPostgresUrl(reactorDbUrl)) {
2468
2693
  const connectionString = reactorDbUrl.includes("?") ? reactorDbUrl : `${reactorDbUrl}?sslmode=disable`;
2469
2694
  const poolSize = hostPoolSize();
@@ -2480,11 +2705,7 @@ async function createReactorKysely(opts) {
2480
2705
  }
2481
2706
  if (!reactorPgliteDir || reactorPgliteMajor === null) throw new Error("Reactor PGLite directory not resolved");
2482
2707
  const { PGlite } = await loadPGliteModule(reactorPgliteMajor);
2483
- const pglite = inMemory ? new PGlite() : new PGlite({ fs: new AtomicNodeFs(reactorPgliteDir, {
2484
- logger,
2485
- flushIntervalMs,
2486
- onFlushError: onPgliteFlushError
2487
- }) });
2708
+ const pglite = inMemory ? new PGlite() : new PGlite({ fs: (await loadDurableFsFactory(reactorPgliteMajor, logger))(reactorPgliteDir) });
2488
2709
  logger.info(inMemory ? `Using in-memory PGlite (PG${reactorPgliteMajor}) for reactor storage [PH_PGLITE_IN_MEMORY=1]` : `Using PGlite (PG${reactorPgliteMajor}) for reactor storage at ${reactorPgliteDir}`);
2489
2710
  return {
2490
2711
  kysely: new Kysely({ dialect: new ClosablePGliteDialect(pglite) }),
@@ -2512,42 +2733,14 @@ async function initServer(serverPort, options, renown, renownConfig) {
2512
2733
  const reactorPgliteDir = options.reactor ? null : !reactorDbUrl || !isPostgresUrl(reactorDbUrl) ? reactorPath : null;
2513
2734
  const readModelPgliteDir = !dbPath || !isPostgresUrl(dbPath) ? readModelPath : null;
2514
2735
  const pgliteDirs = [reactorPgliteDir, readModelPgliteDir].filter((d) => d !== null);
2515
- const detectedMajors = /* @__PURE__ */ new Map();
2516
- for (const dir of pgliteDirs) {
2517
- const lockPath = path$1.join(dir, "postmaster.pid");
2518
- try {
2519
- await promises.unlink(lockPath);
2520
- logger.warn(`Removed stale PGLite lockfile ${lockPath}`);
2521
- } catch (err) {
2522
- if (err.code !== "ENOENT") throw err;
2523
- }
2524
- }
2525
- if (options.forcePgVersion !== void 0 && pgliteDirs.length > 0) {
2526
- if (options.migratePglite) logger.warn("PH_FORCE_PG_VERSION is set; ignoring --migrate-pglite/PH_MIGRATE_PGLITE because the data dirs will be wiped.");
2527
- logger.warn(`PH_FORCE_PG_VERSION=${options.forcePgVersion} set; wiping PGLite data dirs and re-initializing at PG${options.forcePgVersion}.`);
2528
- for (const dir of pgliteDirs) {
2529
- await promises.rm(dir, {
2530
- recursive: true,
2531
- force: true
2532
- });
2533
- logger.info(`Wiped PGLite data dir ${dir}`);
2534
- }
2535
- } else if (options.forcePgVersion === void 0) {
2536
- for (const dir of pgliteDirs) {
2537
- const major = await readPgVersionFile(dir);
2538
- if (major !== null) detectedMajors.set(dir, major);
2539
- }
2540
- if (options.migratePglite) for (const [dir, major] of detectedMajors) {
2541
- if (major === 17) continue;
2542
- await migratePgliteDir(dir, logger);
2543
- const after = await readPgVersionFile(dir);
2544
- if (after !== null) detectedMajors.set(dir, after);
2545
- }
2546
- else for (const [dir, major] of detectedMajors) {
2547
- if (major === 17) continue;
2548
- logger.warn(`PGLite data dir at ${dir} was created with PG${major} but Switchboard ships PG17. Running on legacy PGLite. Re-start with --migrate-pglite (or PH_MIGRATE_PGLITE=true) to upgrade.`);
2549
- }
2550
- }
2736
+ warnIgnoredFlushInterval(logger);
2737
+ const detectedMajors = await runPglitePreflight({
2738
+ dirs: pgliteDirs,
2739
+ forcePgVersion: options.forcePgVersion,
2740
+ migratePglite: options.migratePglite,
2741
+ inMemory: PGLITE_IN_MEMORY,
2742
+ logger
2743
+ });
2551
2744
  function resolvePgliteMajorForDir(dir) {
2552
2745
  if (options.forcePgVersion !== void 0) return options.forcePgVersion;
2553
2746
  const detected = detectedMajors.get(dir);
@@ -2614,6 +2807,7 @@ async function initServer(serverPort, options, renown, renownConfig) {
2614
2807
  if (!packages.includes("@powerhousedao/workflow")) packages.push(WORKFLOW_PACKAGE_NAME);
2615
2808
  }
2616
2809
  let ownedReactorModule;
2810
+ let workerModels;
2617
2811
  let localRenownRequest;
2618
2812
  const initializeClient = async (documentModels, { attachmentReferenceWriter, upgradeManifests }) => {
2619
2813
  if (options.reactor) {
@@ -2632,7 +2826,6 @@ async function initServer(serverPort, options, renown, renownConfig) {
2632
2826
  reactorPgliteDir,
2633
2827
  reactorPgliteMajor,
2634
2828
  inMemory: PGLITE_IN_MEMORY,
2635
- flushIntervalMs: PGLITE_FLUSH_INTERVAL_MS,
2636
2829
  hostPoolSize: () => resolveHostPoolSize(process.env),
2637
2830
  logger
2638
2831
  });
@@ -2668,10 +2861,14 @@ async function initServer(serverPort, options, renown, renownConfig) {
2668
2861
  switchboardUrl: renownConfig.switchboardUrl
2669
2862
  }, renown) : void 0
2670
2863
  });
2864
+ if (workerPool || projectionWorker || workflowsEnabled) {
2865
+ const sources = await resolveWorkerModelSources(packages, reactorLogger, { resolveRemote: resolveRegistryModels });
2866
+ reactorBuilder.withDocumentModelSources(workerPool || projectionWorker ? sources : await keepImportableSources(sources, reactorLogger));
2867
+ }
2868
+ workerModels = modelManifestSource(reactorBuilder);
2671
2869
  if (workerPool) {
2672
2870
  if (!reactorDbUrl) throw new Error("unreachable: worker pool enabled without a reactor database URL");
2673
- const workerSources = await resolveWorkerModelSources(packages, reactorLogger, { resolveRemote: resolveRegistryModels });
2674
- reactorBuilder.withDocumentModelSources(workerSources).withWorkerPool({
2871
+ reactorBuilder.withWorkerPool({
2675
2872
  numWorkers: workerPool.numWorkers,
2676
2873
  db: buildWorkerDbConfig(reactorDbUrl, workerPool)
2677
2874
  });
@@ -2688,10 +2885,6 @@ async function initServer(serverPort, options, renown, renownConfig) {
2688
2885
  });
2689
2886
  if (projectionWorker) {
2690
2887
  if (!reactorDbUrl) throw new Error("unreachable: projection worker enabled without a reactor database URL");
2691
- if (!workerPool) {
2692
- const workerSources = await resolveWorkerModelSources(packages, reactorLogger, { resolveRemote: resolveRegistryModels });
2693
- reactorBuilder.withDocumentModelSources(workerSources);
2694
- }
2695
2888
  const db = {
2696
2889
  ...buildWorkerDbConfig(reactorDbUrl, {
2697
2890
  dbPoolSizePerWorker: projectionWorker.dbPoolSize,
@@ -2785,12 +2978,9 @@ async function initServer(serverPort, options, renown, renownConfig) {
2785
2978
  let pgliteFactory;
2786
2979
  if (readModelPgliteDir && readModelPgliteMajor !== null) {
2787
2980
  const { PGlite: ReadModelPGlite } = await loadPGliteModule(readModelPgliteMajor);
2981
+ const durableFs = await loadDurableFsFactory(readModelPgliteMajor, logger);
2788
2982
  pgliteFactory = PGLITE_IN_MEMORY ? () => new ReadModelPGlite({ parsers: PGLITE_UTC_PARSERS }) : (connectionString) => new ReadModelPGlite({
2789
- fs: new AtomicNodeFs(connectionString ?? readModelPgliteDir, {
2790
- logger,
2791
- flushIntervalMs: PGLITE_FLUSH_INTERVAL_MS,
2792
- onFlushError: onPgliteFlushError
2793
- }),
2983
+ fs: durableFs(connectionString ?? readModelPgliteDir),
2794
2984
  parsers: PGLITE_UTC_PARSERS
2795
2985
  });
2796
2986
  }
@@ -2839,6 +3029,7 @@ async function initServer(serverPort, options, renown, renownConfig) {
2839
3029
  authorizationService: api.authorizationService,
2840
3030
  pieces: api.packageManager,
2841
3031
  pieceRegistryUrl: registryUrl,
3032
+ models: workerModels,
2842
3033
  logger: logger.child(["workflow-runtime"])
2843
3034
  });
2844
3035
  const WorkflowRuntimeSubgraph = workflows.subgraph;
@@ -2976,6 +3167,7 @@ async function initServer(serverPort, options, renown, renownConfig) {
2976
3167
  attachmentReferenceProjection: api.attachmentReferenceProjection,
2977
3168
  workflowTriggers: workflows?.triggers,
2978
3169
  workflowsEnabled,
3170
+ modelManifest: () => workerModels?.modelManifest() ?? [],
2979
3171
  privacy: privacy ? { erasure: privacy.erasure } : void 0,
2980
3172
  mcpEnabled: options.mcp !== false,
2981
3173
  renown,
@@ -3048,5 +3240,5 @@ if (import.meta.main) await startSwitchboard({ fatalErrorShutdown: true });
3048
3240
  //#endregion
3049
3241
  export { resolveUnsupportedStoredDocuments as a, StoredDocumentsRefusedError as i, isPortAvailable as n, applySwitchboardReactorDefaults as o, startSwitchboard as r, parseForcePgVersion as s, deriveAttachmentServiceConfig as t };
3050
3242
 
3051
- //# sourceMappingURL=server-CMAxYG6D.mjs.map
3052
- //# debugId=e04a905d-8b84-5d81-aff7-5e3f6387cda0
3243
+ //# sourceMappingURL=server-Dvw8_NeM.mjs.map
3244
+ //# debugId=b9c02424-1406-5d63-abbd-6c32caa53205