@velum-labs/routekit-daemon 0.18.0 → 0.18.2

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
@@ -6,11 +6,12 @@
6
6
  * ports behind that front door; config/account reload builds a complete new
7
7
  * generation before atomically switching new traffic and draining the old.
8
8
  */
9
+ import { createHash } from "node:crypto";
9
10
  import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, rmSync } from "node:fs";
10
11
  import { basename, dirname, join } from "node:path";
11
12
  import { AccountActivityCoordinator, AccountAuthCoordinator, accountStoreEntries, CLIPROXY_API_KEY_ENV, CLIPROXY_BASE_URL_ENV, cliproxyAccountEntries, cliproxyAccountMatchesKind, cliproxyApiKey, cliproxyAuthDirectory, cliproxyBaseUrl, cliproxyCredentialValid, defaultSubscriptionAccountDirectory, RateLimitTracker, removeCliproxyAccount, removeSubscriptionAccount, renameSubscriptionAccount, sanitizeSubscriptionLabel, subscriptionAccountIdentity, subscriptionCredentialFingerprint } from "@velum-labs/routekit-accounts";
12
13
  import { configuredProviderIds, globalRouterConfigPath, parseRouterConfigDocument, routekitHome, writeRouterConfig } from "@velum-labs/routekit-config";
13
- import { createRouteKitControlHandler, ROUTEKIT_CONTROL_CAPABILITY } from "@velum-labs/routekit-control";
14
+ import { createRouteKitControlHandler, MUTATING_ROUTEKIT_METHODS, ROUTEKIT_CONTROL_CAPABILITY, ROUTEKIT_DAEMON_ROLL_CAPABILITY } from "@velum-labs/routekit-control";
14
15
  import { resolveCodexStartupModel, resolveLeaderboardConfig, startSwitchingGatewayProxy } from "@velum-labs/routekit-gateway";
15
16
  import { accountKindForCliproxyAuthType, PROVIDERS, resolveAccountConnector } from "@velum-labs/routekit-registry";
16
17
  import { startRouter } from "@velum-labs/routekit-router";
@@ -20,6 +21,7 @@ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
20
21
  import { cleanupAccountTransaction, markAccountTransactionCommitted, prepareAccountTransaction, recoverAccountTransactions, rollbackAccountTransaction } from "./account-transaction.js";
21
22
  import { CallAttributionStore, callInspection } from "./call-attribution-store.js";
22
23
  import { createCliproxySidecar } from "./cliproxy-sidecar.js";
24
+ import { DAEMON_HOST_PROTOCOL_VERSION } from "./host-protocol.js";
23
25
  import { aggregateInspections, buildLeaderboardResult, defaultLeaderboardWindow, LeaderboardRollupStore } from "./leaderboard.js";
24
26
  import { DaemonTelemetry, DEFAULT_TELEMETRY_HOST, GatewayTelemetryAggregator, resolveTelemetryProjectKey } from "./telemetry.js";
25
27
  export const ROUTEKIT_DAEMON_KIND = "daemon";
@@ -54,11 +56,11 @@ function resolveDataToken(home, input, tokens) {
54
56
  throw new Error("RouteKit data-plane token is empty");
55
57
  return { token: ensured.token, path };
56
58
  }
57
- function daemonPublicRecordPath(home) {
59
+ export function daemonPublicRecordPath(home) {
58
60
  return join(home, "services", "daemon.public.json");
59
61
  }
60
62
  /** Publish a secret-free discovery file peers can read across OS accounts. */
61
- function writeDaemonPublicRecord(home, record) {
63
+ export function writeDaemonPublicRecord(home, record) {
62
64
  const servicesDir = join(home, "services");
63
65
  mkdirSync(home, { recursive: true, mode: SERVICE_HOME_MODE });
64
66
  chmodSync(home, SERVICE_HOME_MODE);
@@ -68,7 +70,7 @@ function writeDaemonPublicRecord(home, record) {
68
70
  writeFileAtomic(path, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o644 });
69
71
  chmodSync(path, 0o644);
70
72
  }
71
- function removeDaemonPublicRecord(home) {
73
+ export function removeDaemonPublicRecord(home) {
72
74
  rmSync(daemonPublicRecordPath(home), { force: true });
73
75
  }
74
76
  /**
@@ -101,7 +103,7 @@ function dataTokenForPrincipal(tokens, cache, ownerToken, principal) {
101
103
  function revisionPath(home) {
102
104
  return join(home, "daemon-revisions.json");
103
105
  }
104
- function readRevisions(home) {
106
+ export function readDaemonRevisions(home) {
105
107
  try {
106
108
  const parsed = JSON.parse(readFileSync(revisionPath(home), "utf8"));
107
109
  return {
@@ -118,7 +120,7 @@ function readRevisions(home) {
118
120
  return { config: 0, accounts: 0, daemon: 0 };
119
121
  }
120
122
  }
121
- function writeRevisions(home, revisions) {
123
+ export function writeDaemonRevisions(home, revisions) {
122
124
  mkdirSync(home, { recursive: true, mode: 0o700 });
123
125
  writeFileAtomic(revisionPath(home), `${JSON.stringify(revisions, null, 2)}\n`, {
124
126
  mode: 0o600
@@ -248,26 +250,29 @@ export async function startRouteKitDaemon(options) {
248
250
  const dataAuth = resolveDataToken(home, options, tokens);
249
251
  dataTokenCache.set("default", dataAuth.token);
250
252
  const store = createServiceRecordStore({ home, product: ROUTEKIT_PRODUCT });
253
+ const hosted = options.hosted;
251
254
  // Held for the daemon's whole lifetime. Lifecycle clients use daemon.lock
252
255
  // while this authority lock prevents any second daemon from becoming live.
253
- const authority = await acquireLifecycleLock(join(store.directory, "daemon-authority.lock"), {
254
- timeoutMs: 30_000,
255
- onWait: async () => {
256
- const existing = store.read(ROUTEKIT_DAEMON_KIND);
257
- return existing !== undefined && (await healthyControl(existing))
258
- ? new ControlError({
259
- code: "conflict",
260
- message: `RouteKit daemon is already running (pid ${existing.pid})`
261
- })
262
- : undefined;
263
- }
264
- });
256
+ const authority = hosted === undefined
257
+ ? await acquireLifecycleLock(join(store.directory, "daemon-authority.lock"), {
258
+ timeoutMs: 30_000,
259
+ onWait: async () => {
260
+ const existing = store.read(ROUTEKIT_DAEMON_KIND);
261
+ return existing !== undefined && (await healthyControl(existing))
262
+ ? new ControlError({
263
+ code: "conflict",
264
+ message: `RouteKit daemon is already running (pid ${existing.pid})`
265
+ })
266
+ : undefined;
267
+ }
268
+ })
269
+ : undefined;
265
270
  let accountRecovery;
266
271
  try {
267
272
  accountRecovery = recoverAccountTransactions(home);
268
273
  }
269
274
  catch (error) {
270
- authority.release();
275
+ authority?.release();
271
276
  throw error;
272
277
  }
273
278
  let control;
@@ -282,8 +287,8 @@ export async function startRouteKitDaemon(options) {
282
287
  let record;
283
288
  let closed = false;
284
289
  let draining = false;
285
- let lifecycle = "running";
286
- let revisions = readRevisions(home);
290
+ let lifecycle = hosted?.initiallyPaused === true ? "paused" : "running";
291
+ let revisions = readDaemonRevisions(home);
287
292
  let currentDocument = canonicalConfigDocument(configPath);
288
293
  let currentConfig = parseConfigDocument(currentDocument);
289
294
  let mutationTail = Promise.resolve();
@@ -300,26 +305,27 @@ export async function startRouteKitDaemon(options) {
300
305
  };
301
306
  const startedAt = new Date().toISOString();
302
307
  try {
303
- const previous = store.read(ROUTEKIT_DAEMON_KIND);
304
- if (previous !== undefined &&
305
- previous.pid !== process.pid &&
306
- (await healthyControl(previous))) {
308
+ const previous = hosted === undefined ? store.read(ROUTEKIT_DAEMON_KIND) : undefined;
309
+ if (hosted === undefined && previous !== undefined && previous.pid !== process.pid && (await healthyControl(previous))) {
307
310
  throw new ControlError({
308
311
  code: "conflict",
309
312
  message: `RouteKit daemon is already running (pid ${previous.pid})`
310
313
  });
311
314
  }
312
- if (previous !== undefined && previous.pid !== process.pid) {
315
+ if (hosted === undefined && previous !== undefined && previous.pid !== process.pid) {
313
316
  // A live-but-unhealthy daemon is not safe to replace under its feet.
314
317
  throw new ControlError({
315
318
  code: "unavailable",
316
319
  message: `RouteKit daemon pid ${previous.pid} is alive but its control plane is unhealthy; stop it before recovery`
317
320
  });
318
321
  }
319
- const generation = nextServiceGeneration(Math.max(previous?.generation ?? 0, revisions.daemon));
320
- revisions.daemon = generation;
321
- writeRevisions(home, revisions);
322
- const sidecar = createCliproxySidecar({ env });
322
+ const generation = hosted?.generation ??
323
+ nextServiceGeneration(Math.max(previous?.generation ?? 0, revisions.daemon));
324
+ if (hosted === undefined) {
325
+ revisions.daemon = generation;
326
+ writeDaemonRevisions(home, revisions);
327
+ }
328
+ const sidecar = hosted?.sidecar ?? createCliproxySidecar({ env });
323
329
  sidecarRef = sidecar;
324
330
  let leaderboardConfig = resolveLeaderboardConfig(currentConfig);
325
331
  const callAttributions = new CallAttributionStore({
@@ -430,12 +436,16 @@ export async function startRouteKitDaemon(options) {
430
436
  };
431
437
  }
432
438
  });
433
- portless = await createPortlessSession(options.portless ?? env.ROUTEKIT_PORTLESS !== "0", {
434
- project: "routekit",
435
- ownerLabel: "routekit-daemon",
436
- bareNames: []
437
- });
438
- const dataUrl = portless.enabled ? portless.register("gateway", proxy.port()) : proxy.url();
439
+ portless =
440
+ hosted === undefined
441
+ ? await createPortlessSession(options.portless ?? env.ROUTEKIT_PORTLESS !== "0", {
442
+ project: "routekit",
443
+ ownerLabel: "routekit-daemon",
444
+ bareNames: []
445
+ })
446
+ : undefined;
447
+ const dataUrl = hosted?.dataUrl() ??
448
+ (portless?.enabled === true ? portless.register("gateway", proxy.port()) : proxy.url());
439
449
  const replaceRouter = async (nextConfig, nextDocument, input) => {
440
450
  // Sidecar reconcile runs before the generation commits; any failure
441
451
  // below must put the sidecar back to the still-live currentConfig.
@@ -463,7 +473,7 @@ export async function startRouteKitDaemon(options) {
463
473
  try {
464
474
  if (input.write)
465
475
  writeRouterConfig(configPath, nextConfig);
466
- writeRevisions(home, nextRevisions);
476
+ writeDaemonRevisions(home, nextRevisions);
467
477
  await input.beforeSwap?.();
468
478
  }
469
479
  catch (error) {
@@ -472,7 +482,7 @@ export async function startRouteKitDaemon(options) {
472
482
  chmodSync(configPath, 0o600);
473
483
  }
474
484
  revisions = previousRevisions;
475
- writeRevisions(home, previousRevisions);
485
+ writeDaemonRevisions(home, previousRevisions);
476
486
  await candidate.close();
477
487
  await sidecar.reconcile(wantsCliproxySidecar(currentConfig));
478
488
  throw error;
@@ -515,17 +525,22 @@ export async function startRouteKitDaemon(options) {
515
525
  handlers = {
516
526
  "daemon.status": async () => ({
517
527
  pid: process.pid,
528
+ workerPid: process.pid,
529
+ hostPid: hosted?.hostPid ?? process.pid,
530
+ hostStartedAt: hosted?.hostStartedAt ?? startedAt,
518
531
  startedAt,
519
532
  packageVersion: options.packageVersion,
520
533
  protocolVersion: CONTROL_PROTOCOL_VERSION,
534
+ hostProtocolVersion: hosted === undefined ? 0 : DAEMON_HOST_PROTOCOL_VERSION,
521
535
  generation,
522
536
  configRevision: revisions.config,
523
537
  accountRevision: revisions.accounts,
524
538
  controlUrl: control?.url ?? "",
525
- dataUrl,
539
+ dataUrl: hosted?.dataUrl() ?? dataUrl,
526
540
  dataPort: proxy?.port() ?? 0,
527
541
  supervisor: supervisorFromEnv(env),
528
- draining
542
+ draining,
543
+ rolling: hosted?.rolling() ?? false
529
544
  }),
530
545
  "daemon.reload": async (params) => {
531
546
  await serializeMutation(async () => {
@@ -545,9 +560,66 @@ export async function startRouteKitDaemon(options) {
545
560
  accountRevision: revisions.accounts
546
561
  };
547
562
  },
563
+ "daemon.roll": async (params, context) => {
564
+ if (context.principal?.role !== "ephemeral") {
565
+ throw new ControlError({
566
+ code: "unauthorized",
567
+ message: "daemon roll requires the local service credential"
568
+ });
569
+ }
570
+ if (options.onRollRequested === undefined) {
571
+ throw new ControlError({
572
+ code: "upgrade_required",
573
+ message: "this daemon does not support rolling process replacement"
574
+ });
575
+ }
576
+ const startedAt = Date.now();
577
+ const supervisor = ["systemd", "launchd", "detached"].includes(supervisorFromEnv(env))
578
+ ? supervisorFromEnv(env)
579
+ : "unknown";
580
+ const toVersion = params.candidate?.expectedVersion ?? options.packageVersion;
581
+ daemonTelemetry?.capture("routekit.daemon_lifecycle", {
582
+ action: "roll_started",
583
+ outcome: "success",
584
+ supervisor,
585
+ version: options.packageVersion,
586
+ reason: params.reason,
587
+ from_version: options.packageVersion,
588
+ to_version: toVersion
589
+ });
590
+ try {
591
+ const result = await options.onRollRequested(params);
592
+ daemonTelemetry?.capture("routekit.daemon_lifecycle", {
593
+ action: "roll_committed",
594
+ outcome: "success",
595
+ supervisor,
596
+ version: result.packageVersion,
597
+ reason: params.reason,
598
+ from_version: options.packageVersion,
599
+ to_version: result.packageVersion,
600
+ duration_bucket: durationBucket(Date.now() - startedAt)
601
+ });
602
+ return result;
603
+ }
604
+ catch (error) {
605
+ daemonTelemetry?.capture("routekit.daemon_lifecycle", {
606
+ action: "roll_failed",
607
+ outcome: "error",
608
+ supervisor,
609
+ version: options.packageVersion,
610
+ reason: params.reason,
611
+ from_version: options.packageVersion,
612
+ to_version: toVersion,
613
+ rollback_stage: "candidate",
614
+ duration_bucket: durationBucket(Date.now() - startedAt)
615
+ });
616
+ throw error;
617
+ }
618
+ },
548
619
  "daemon.prepareShutdown": async (params) => {
549
- if (lifecycle !== "running")
620
+ if (lifecycle === "quiescing" || lifecycle === "draining" || lifecycle === "closed") {
550
621
  return { accepted: true };
622
+ }
551
623
  lifecycle = "quiescing";
552
624
  draining = true;
553
625
  await mutationTail;
@@ -1653,16 +1725,29 @@ export async function startRouteKitDaemon(options) {
1653
1725
  version: options.packageVersion
1654
1726
  });
1655
1727
  };
1656
- const dispatch = createRouteKitControlHandler(handlers, {
1728
+ const routeKitDispatch = createRouteKitControlHandler(handlers, {
1657
1729
  onCommitted: (method, params, durationMs) => captureOperation(method, params, "success", durationMs),
1658
1730
  onControlError: (method, params, _code, durationMs) => captureOperation(method, params, "error", durationMs)
1659
1731
  });
1732
+ const dispatch = async (method, params, context) => {
1733
+ if (lifecycle === "paused" && MUTATING_ROUTEKIT_METHODS.has(method)) {
1734
+ throw new ControlError({
1735
+ code: "unavailable",
1736
+ message: "RouteKit daemon is synchronizing a replacement worker"
1737
+ });
1738
+ }
1739
+ return await routeKitDispatch(method, params, context);
1740
+ };
1660
1741
  control = await startControlServer({
1661
1742
  handler: dispatch,
1662
- token: generateControlToken(),
1743
+ token: hosted?.controlToken ?? generateControlToken(),
1663
1744
  product: ROUTEKIT_PRODUCT,
1664
1745
  packageVersion: options.packageVersion,
1665
- capabilities: [ROUTEKIT_CONTROL_CAPABILITY],
1746
+ port: options.controlPort,
1747
+ capabilities: [
1748
+ ROUTEKIT_CONTROL_CAPABILITY,
1749
+ ...(hosted === undefined ? [] : [ROUTEKIT_DAEMON_ROLL_CAPABILITY])
1750
+ ],
1666
1751
  authorize: (presented) => {
1667
1752
  const principal = tokens.resolve(presented, "control");
1668
1753
  if (principal === undefined)
@@ -1678,7 +1763,7 @@ export async function startRouteKitDaemon(options) {
1678
1763
  console.error(`RouteKit ${operation} failed (request ${context.requestId}):`, error);
1679
1764
  }
1680
1765
  });
1681
- record = store.write({
1766
+ const workerRecordInput = {
1682
1767
  kind: ROUTEKIT_DAEMON_KIND,
1683
1768
  pid: process.pid,
1684
1769
  ...(processIdentity(process.pid) !== undefined
@@ -1693,7 +1778,7 @@ export async function startRouteKitDaemon(options) {
1693
1778
  dataUrl,
1694
1779
  dataPort: proxy.port(),
1695
1780
  host: options.host ?? "127.0.0.1",
1696
- portless: portless.enabled,
1781
+ portless: portless?.enabled ?? false,
1697
1782
  drainGraceMs,
1698
1783
  authTokenFile: dataAuth.path,
1699
1784
  generation,
@@ -1701,18 +1786,24 @@ export async function startRouteKitDaemon(options) {
1701
1786
  ...(process.argv[1] !== undefined ? { binPath: process.argv[1] } : {}),
1702
1787
  args: redactedProcessArgs(process.argv.slice(2)),
1703
1788
  cwd: process.cwd()
1704
- });
1705
- writeDaemonPublicRecord(home, {
1706
- product: ROUTEKIT_PRODUCT,
1707
- kind: ROUTEKIT_DAEMON_KIND,
1708
- url: control.url,
1709
- port: control.port,
1710
- generation,
1711
- protocolVersion: CONTROL_PROTOCOL_VERSION,
1712
- dataUrl,
1713
- dataPort: proxy.port(),
1714
- startedAt
1715
- });
1789
+ };
1790
+ record =
1791
+ hosted === undefined
1792
+ ? store.write(workerRecordInput)
1793
+ : { product: ROUTEKIT_PRODUCT, owner: ROUTEKIT_PRODUCT, ...workerRecordInput };
1794
+ if (hosted === undefined) {
1795
+ writeDaemonPublicRecord(home, {
1796
+ product: ROUTEKIT_PRODUCT,
1797
+ kind: ROUTEKIT_DAEMON_KIND,
1798
+ url: control.url,
1799
+ port: control.port,
1800
+ generation,
1801
+ protocolVersion: CONTROL_PROTOCOL_VERSION,
1802
+ dataUrl,
1803
+ dataPort: proxy.port(),
1804
+ startedAt
1805
+ });
1806
+ }
1716
1807
  daemonTelemetry.capture("routekit.daemon_lifecycle", {
1717
1808
  action: "started",
1718
1809
  outcome: "success",
@@ -1745,13 +1836,16 @@ export async function startRouteKitDaemon(options) {
1745
1836
  await activeRouter?.close();
1746
1837
  accountActivity?.close();
1747
1838
  accountAuth?.close();
1748
- await sidecar.close();
1839
+ if (hosted === undefined)
1840
+ await sidecar.close();
1749
1841
  await control?.close();
1750
- if (portless?.enabled)
1751
- portless.unregister("gateway");
1752
- store.remove(ROUTEKIT_DAEMON_KIND, { ifPid: process.pid });
1753
- removeDaemonPublicRecord(home);
1754
- authority.release();
1842
+ if (hosted === undefined) {
1843
+ if (portless?.enabled)
1844
+ portless.unregister("gateway");
1845
+ store.remove(ROUTEKIT_DAEMON_KIND, { ifPid: process.pid });
1846
+ removeDaemonPublicRecord(home);
1847
+ authority?.release();
1848
+ }
1755
1849
  lifecycle = "closed";
1756
1850
  })();
1757
1851
  return closeRun;
@@ -1770,6 +1864,42 @@ export async function startRouteKitDaemon(options) {
1770
1864
  dataUrl,
1771
1865
  controlUrl: control.url,
1772
1866
  close,
1867
+ retire: async (graceMs = drainGraceMs) => {
1868
+ if (lifecycle === "closed" || lifecycle === "draining")
1869
+ return;
1870
+ lifecycle = "quiescing";
1871
+ draining = true;
1872
+ await mutationTail;
1873
+ lifecycle = "draining";
1874
+ await Promise.all([proxy?.retire(graceMs), control?.retire(Math.min(graceMs, 2_000))]);
1875
+ await activeRouter?.close();
1876
+ accountActivity?.close();
1877
+ accountAuth?.close();
1878
+ gatewayTelemetry?.close();
1879
+ await daemonTelemetry?.shutdown();
1880
+ lifecycle = "closed";
1881
+ },
1882
+ pauseMutations: async () => {
1883
+ if (lifecycle !== "running") {
1884
+ throw new ControlError({ code: "unavailable", message: "RouteKit daemon is not mutable" });
1885
+ }
1886
+ lifecycle = "paused";
1887
+ await mutationTail;
1888
+ return {
1889
+ configRevision: revisions.config,
1890
+ accountRevision: revisions.accounts,
1891
+ configHash: createHash("sha256").update(currentDocument).digest("hex")
1892
+ };
1893
+ },
1894
+ resumeMutations: () => {
1895
+ if (lifecycle === "paused")
1896
+ lifecycle = "running";
1897
+ },
1898
+ snapshot: () => ({
1899
+ configRevision: revisions.config,
1900
+ accountRevision: revisions.accounts,
1901
+ configHash: createHash("sha256").update(currentDocument).digest("hex")
1902
+ }),
1773
1903
  reload: async () => {
1774
1904
  await handlers["daemon.reload"]({}, {
1775
1905
  signal: new AbortController().signal,
@@ -1785,14 +1915,26 @@ export async function startRouteKitDaemon(options) {
1785
1915
  await activeRouter?.close();
1786
1916
  accountActivity?.close();
1787
1917
  accountAuth?.close();
1788
- await sidecarRef?.close();
1918
+ if (hosted === undefined)
1919
+ await sidecarRef?.close();
1789
1920
  await control?.close();
1790
- if (portless?.enabled)
1791
- portless.unregister("gateway");
1792
- if (record !== undefined)
1793
- store.remove(ROUTEKIT_DAEMON_KIND, { ifPid: process.pid });
1794
- removeDaemonPublicRecord(home);
1795
- authority.release();
1921
+ if (hosted === undefined) {
1922
+ if (portless?.enabled)
1923
+ portless.unregister("gateway");
1924
+ if (record !== undefined)
1925
+ store.remove(ROUTEKIT_DAEMON_KIND, { ifPid: process.pid });
1926
+ removeDaemonPublicRecord(home);
1927
+ authority?.release();
1928
+ }
1796
1929
  throw error;
1797
1930
  }
1798
1931
  }
1932
+ export { ROUTEKIT_DAEMON_WORKER_ENV } from "./host-protocol.js";
1933
+ export async function startRouteKitDaemonHost(options) {
1934
+ const daemonHost = await import("./host.js");
1935
+ return await daemonHost.startRouteKitDaemonHost(options);
1936
+ }
1937
+ export async function runRouteKitDaemonWorker(options) {
1938
+ const daemonWorker = await import("./worker.js");
1939
+ return await daemonWorker.runRouteKitDaemonWorker(options);
1940
+ }
@@ -0,0 +1,2 @@
1
+ import type { RouteKitDaemonOptions } from "./index.js";
2
+ export declare function runRouteKitDaemonWorker(options: RouteKitDaemonOptions): Promise<never>;
package/dist/worker.js ADDED
@@ -0,0 +1,168 @@
1
+ import cluster from "node:cluster";
2
+ import { DAEMON_HOST_PROTOCOL_VERSION, ROUTEKIT_DAEMON_CONTROL_PORT_ENV, ROUTEKIT_DAEMON_CONTROL_TOKEN_ENV, ROUTEKIT_DAEMON_DATA_PORT_ENV, ROUTEKIT_DAEMON_DATA_URL_ENV, ROUTEKIT_DAEMON_GENERATION_ENV, ROUTEKIT_DAEMON_HOST_PID_ENV, ROUTEKIT_DAEMON_HOST_STARTED_AT_ENV, ROUTEKIT_DAEMON_INITIAL_PAUSED_ENV } from "./host-protocol.js";
3
+ import { startRouteKitDaemon } from "./index.js";
4
+ function requiredEnv(env, name) {
5
+ const value = env[name];
6
+ if (value === undefined || value.length === 0)
7
+ throw new Error(`missing hosted worker environment ${name}`);
8
+ return value;
9
+ }
10
+ function positiveInteger(value, name) {
11
+ const parsed = Number.parseInt(value, 10);
12
+ if (!Number.isSafeInteger(parsed) || parsed <= 0)
13
+ throw new Error(`invalid hosted worker ${name}`);
14
+ return parsed;
15
+ }
16
+ function send(message) {
17
+ if (typeof process.send !== "function")
18
+ throw new Error("hosted daemon worker has no IPC channel");
19
+ process.send(message);
20
+ }
21
+ export async function runRouteKitDaemonWorker(options) {
22
+ if (!cluster.isWorker)
23
+ throw new Error("daemon worker must run as a cluster worker");
24
+ const env = options.env ?? process.env;
25
+ const generation = positiveInteger(requiredEnv(env, ROUTEKIT_DAEMON_GENERATION_ENV), "generation");
26
+ const hostPid = positiveInteger(requiredEnv(env, ROUTEKIT_DAEMON_HOST_PID_ENV), "host pid");
27
+ const hostStartedAt = requiredEnv(env, ROUTEKIT_DAEMON_HOST_STARTED_AT_ENV);
28
+ const controlToken = requiredEnv(env, ROUTEKIT_DAEMON_CONTROL_TOKEN_ENV);
29
+ const dataPort = positiveInteger(requiredEnv(env, ROUTEKIT_DAEMON_DATA_PORT_ENV), "data port");
30
+ const controlPort = positiveInteger(requiredEnv(env, ROUTEKIT_DAEMON_CONTROL_PORT_ENV), "control port");
31
+ let dataUrl = requiredEnv(env, ROUTEKIT_DAEMON_DATA_URL_ENV);
32
+ let rolling = env[ROUTEKIT_DAEMON_INITIAL_PAUSED_ENV] === "1";
33
+ let sidecarState = { managed: true, running: false };
34
+ let nextRequestId = 0;
35
+ const pending = new Map();
36
+ let running;
37
+ const requestHost = async (request) => {
38
+ const requestId = `${process.pid}-${++nextRequestId}`;
39
+ const response = new Promise((resolve, reject) => {
40
+ pending.set(requestId, { resolve: (value) => resolve(value), reject });
41
+ });
42
+ send({ ...request, requestId });
43
+ return await response;
44
+ };
45
+ const sidecar = {
46
+ reconcile: async (wanted) => {
47
+ sidecarState = await requestHost({ type: "host.sidecar", operation: "reconcile", wanted });
48
+ },
49
+ refresh: async () => {
50
+ sidecarState = await requestHost({ type: "host.sidecar", operation: "refresh" });
51
+ },
52
+ running: () => sidecarState.running,
53
+ managed: () => sidecarState.managed,
54
+ reachable: async (timeoutMs) => {
55
+ const reachable = await requestHost({
56
+ type: "host.sidecar",
57
+ operation: "reachable",
58
+ timeoutMs
59
+ });
60
+ sidecarState = { ...sidecarState, running: reachable };
61
+ return reachable;
62
+ },
63
+ close: async () => { }
64
+ };
65
+ const onRollRequested = async (params) => await requestHost({ type: "host.roll", params });
66
+ process.on("message", (message) => {
67
+ if (message.type === "host.response") {
68
+ const waiter = pending.get(message.requestId);
69
+ if (waiter === undefined)
70
+ return;
71
+ pending.delete(message.requestId);
72
+ if (message.ok)
73
+ waiter.resolve(message.result);
74
+ else
75
+ waiter.reject(new Error(message.error));
76
+ return;
77
+ }
78
+ if (!message.type.startsWith("worker."))
79
+ return;
80
+ const request = message;
81
+ void (async () => {
82
+ try {
83
+ let result;
84
+ switch (request.type) {
85
+ case "worker.pause":
86
+ result = await running?.pauseMutations();
87
+ break;
88
+ case "worker.resume":
89
+ running?.resumeMutations();
90
+ rolling = false;
91
+ break;
92
+ case "worker.hostState":
93
+ dataUrl = request.dataUrl;
94
+ rolling = request.rolling;
95
+ break;
96
+ case "worker.retire":
97
+ await running?.retire(request.graceMs);
98
+ result = { retired: true };
99
+ break;
100
+ case "worker.shutdown":
101
+ await running?.close();
102
+ result = { closed: true };
103
+ break;
104
+ }
105
+ send({ type: "worker.response", requestId: request.requestId, ok: true, result });
106
+ if (request.type === "worker.retire" || request.type === "worker.shutdown") {
107
+ setImmediate(() => process.exit(0));
108
+ }
109
+ }
110
+ catch (error) {
111
+ send({
112
+ type: "worker.response",
113
+ requestId: request.requestId,
114
+ ok: false,
115
+ error: error instanceof Error ? error.message : String(error)
116
+ });
117
+ }
118
+ })();
119
+ });
120
+ process.once("disconnect", () => {
121
+ void running?.close().finally(() => process.exit(1));
122
+ });
123
+ try {
124
+ running = await startRouteKitDaemon({
125
+ ...options,
126
+ port: dataPort,
127
+ controlPort,
128
+ hosted: {
129
+ generation,
130
+ controlToken,
131
+ dataUrl: () => dataUrl,
132
+ hostPid,
133
+ hostStartedAt,
134
+ rolling: () => rolling,
135
+ sidecar,
136
+ initiallyPaused: env[ROUTEKIT_DAEMON_INITIAL_PAUSED_ENV] === "1"
137
+ },
138
+ onRollRequested,
139
+ onShutdownRequested: (reason) => send({ type: "host.shutdown", reason })
140
+ });
141
+ }
142
+ catch (error) {
143
+ if (process.connected)
144
+ process.disconnect();
145
+ throw error;
146
+ }
147
+ const snapshot = running.snapshot();
148
+ send({
149
+ type: "worker.ready",
150
+ protocolVersion: DAEMON_HOST_PROTOCOL_VERSION,
151
+ workerPid: process.pid,
152
+ ...(running.record.processIdentity !== undefined
153
+ ? { workerProcessIdentity: running.record.processIdentity }
154
+ : {}),
155
+ workerStartedAt: running.record.startedAt,
156
+ packageVersion: options.packageVersion,
157
+ generation,
158
+ configRevision: snapshot.configRevision,
159
+ accountRevision: snapshot.accountRevision,
160
+ configHash: snapshot.configHash,
161
+ controlUrl: running.controlUrl,
162
+ controlPort: running.record.port,
163
+ dataUrl: running.dataUrl,
164
+ dataPort: running.record.dataPort ?? 0,
165
+ ...(process.argv[1] !== undefined ? { binPath: process.argv[1] } : {})
166
+ });
167
+ return await new Promise(() => undefined);
168
+ }