@open-mercato/cli 0.6.1-develop.3167.1.7aedd7b2ba → 0.6.1-develop.3174.1.c3822bcdf0

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.
@@ -1,4 +1,4 @@
1
- [build:cli] found 66 entry points
1
+ [build:cli] found 68 entry points
2
2
  Copied create-app/agentic/ → dist/agentic/
3
3
  Discovered 16 standalone guides → dist/agentic/guides/
4
4
  [build:cli] built successfully
@@ -0,0 +1,37 @@
1
+ import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
2
+ const DEFAULT_LAZY_POLL_MS = 1e3;
3
+ const MIN_LAZY_POLL_MS = 250;
4
+ function resolveAutoSpawnWorkersEnabled(env = process.env) {
5
+ const legacy = parseBooleanToken(env.AUTO_SPAWN_WORKERS);
6
+ if (legacy !== null) return legacy;
7
+ const aliased = parseBooleanToken(env.OM_AUTO_SPAWN_WORKERS);
8
+ if (aliased !== null) return aliased;
9
+ return true;
10
+ }
11
+ function resolveAutoSpawnWorkersLazy(env = process.env) {
12
+ return parseBooleanToken(env.OM_AUTO_SPAWN_WORKERS_LAZY) === true;
13
+ }
14
+ function resolveAutoSpawnWorkersMode(env = process.env) {
15
+ if (!resolveAutoSpawnWorkersEnabled(env)) return "off";
16
+ if (resolveAutoSpawnWorkersLazy(env)) return "lazy";
17
+ return "eager";
18
+ }
19
+ function resolveLazyPollMs(env = process.env) {
20
+ const raw = env.OM_AUTO_SPAWN_WORKERS_LAZY_POLL_MS;
21
+ if (typeof raw !== "string" || raw.trim() === "") return DEFAULT_LAZY_POLL_MS;
22
+ const parsed = Number(raw);
23
+ if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_LAZY_POLL_MS;
24
+ return Math.max(MIN_LAZY_POLL_MS, Math.floor(parsed));
25
+ }
26
+ function resolveLazyRestart(env = process.env) {
27
+ const parsed = parseBooleanToken(env.OM_AUTO_SPAWN_WORKERS_LAZY_RESTART);
28
+ return parsed === null ? true : parsed;
29
+ }
30
+ export {
31
+ resolveAutoSpawnWorkersEnabled,
32
+ resolveAutoSpawnWorkersLazy,
33
+ resolveAutoSpawnWorkersMode,
34
+ resolveLazyPollMs,
35
+ resolveLazyRestart
36
+ };
37
+ //# sourceMappingURL=auto-spawn-workers.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/lib/auto-spawn-workers.ts"],
4
+ "sourcesContent": ["import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\n\nexport type AutoSpawnWorkersMode = 'off' | 'eager' | 'lazy'\n\nexport type AutoSpawnEnvSource = Pick<NodeJS.ProcessEnv, string> | Record<string, string | undefined>\n\nconst DEFAULT_LAZY_POLL_MS = 1000\nconst MIN_LAZY_POLL_MS = 250\n\nexport function resolveAutoSpawnWorkersEnabled(env: AutoSpawnEnvSource = process.env): boolean {\n const legacy = parseBooleanToken(env.AUTO_SPAWN_WORKERS)\n if (legacy !== null) return legacy\n const aliased = parseBooleanToken(env.OM_AUTO_SPAWN_WORKERS)\n if (aliased !== null) return aliased\n return true\n}\n\nexport function resolveAutoSpawnWorkersLazy(env: AutoSpawnEnvSource = process.env): boolean {\n return parseBooleanToken(env.OM_AUTO_SPAWN_WORKERS_LAZY) === true\n}\n\nexport function resolveAutoSpawnWorkersMode(env: AutoSpawnEnvSource = process.env): AutoSpawnWorkersMode {\n if (!resolveAutoSpawnWorkersEnabled(env)) return 'off'\n if (resolveAutoSpawnWorkersLazy(env)) return 'lazy'\n return 'eager'\n}\n\nexport function resolveLazyPollMs(env: AutoSpawnEnvSource = process.env): number {\n const raw = env.OM_AUTO_SPAWN_WORKERS_LAZY_POLL_MS\n if (typeof raw !== 'string' || raw.trim() === '') return DEFAULT_LAZY_POLL_MS\n const parsed = Number(raw)\n if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_LAZY_POLL_MS\n return Math.max(MIN_LAZY_POLL_MS, Math.floor(parsed))\n}\n\nexport function resolveLazyRestart(env: AutoSpawnEnvSource = process.env): boolean {\n const parsed = parseBooleanToken(env.OM_AUTO_SPAWN_WORKERS_LAZY_RESTART)\n return parsed === null ? true : parsed\n}\n"],
5
+ "mappings": "AAAA,SAAS,yBAAyB;AAMlC,MAAM,uBAAuB;AAC7B,MAAM,mBAAmB;AAElB,SAAS,+BAA+B,MAA0B,QAAQ,KAAc;AAC7F,QAAM,SAAS,kBAAkB,IAAI,kBAAkB;AACvD,MAAI,WAAW,KAAM,QAAO;AAC5B,QAAM,UAAU,kBAAkB,IAAI,qBAAqB;AAC3D,MAAI,YAAY,KAAM,QAAO;AAC7B,SAAO;AACT;AAEO,SAAS,4BAA4B,MAA0B,QAAQ,KAAc;AAC1F,SAAO,kBAAkB,IAAI,0BAA0B,MAAM;AAC/D;AAEO,SAAS,4BAA4B,MAA0B,QAAQ,KAA2B;AACvG,MAAI,CAAC,+BAA+B,GAAG,EAAG,QAAO;AACjD,MAAI,4BAA4B,GAAG,EAAG,QAAO;AAC7C,SAAO;AACT;AAEO,SAAS,kBAAkB,MAA0B,QAAQ,KAAa;AAC/E,QAAM,MAAM,IAAI;AAChB,MAAI,OAAO,QAAQ,YAAY,IAAI,KAAK,MAAM,GAAI,QAAO;AACzD,QAAM,SAAS,OAAO,GAAG;AACzB,MAAI,CAAC,OAAO,SAAS,MAAM,KAAK,UAAU,EAAG,QAAO;AACpD,SAAO,KAAK,IAAI,kBAAkB,KAAK,MAAM,MAAM,CAAC;AACtD;AAEO,SAAS,mBAAmB,MAA0B,QAAQ,KAAc;AACjF,QAAM,SAAS,kBAAkB,IAAI,kCAAkC;AACvE,SAAO,WAAW,OAAO,OAAO;AAClC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,222 @@
1
+ import { spawn as nodeSpawn } from "node:child_process";
2
+ import {
3
+ getQueuePendingProbe
4
+ } from "@open-mercato/queue";
5
+ function groupWorkersByQueue(workers) {
6
+ const groups = /* @__PURE__ */ new Map();
7
+ for (const worker of workers) {
8
+ const existing = groups.get(worker.queue);
9
+ if (existing) {
10
+ existing.workerCount += 1;
11
+ if (worker.concurrency > existing.concurrency) {
12
+ existing.concurrency = worker.concurrency;
13
+ }
14
+ } else {
15
+ groups.set(worker.queue, {
16
+ queueName: worker.queue,
17
+ concurrency: Math.max(worker.concurrency, 1),
18
+ workerCount: 1
19
+ });
20
+ }
21
+ }
22
+ return [...groups.values()].sort((a, b) => a.queueName.localeCompare(b.queueName));
23
+ }
24
+ function resolveStrategyFromEnv(env) {
25
+ return env.QUEUE_STRATEGY === "async" ? "async" : "local";
26
+ }
27
+ const PROBE_ERROR_LOG_THROTTLE_MS = 6e4;
28
+ function startLazyWorkerSupervisor(options) {
29
+ const logger = options.logger ?? console;
30
+ const strategy = options.strategy ?? resolveStrategyFromEnv(options.runtimeEnv);
31
+ const probe = options.probeFn ?? ((queueName) => getQueuePendingProbe(queueName, strategy));
32
+ const groups = groupWorkersByQueue(options.workers);
33
+ const startedQueues = /* @__PURE__ */ new Set();
34
+ const activeChildren = /* @__PURE__ */ new Map();
35
+ const startingQueues = /* @__PURE__ */ new Set();
36
+ const probeErrorLastLoggedAt = /* @__PURE__ */ new Map();
37
+ let stopping = false;
38
+ let pollTimer = null;
39
+ let watchPromiseResolve = null;
40
+ const done = new Promise((resolve) => {
41
+ watchPromiseResolve = resolve;
42
+ });
43
+ if (groups.length === 0) {
44
+ logger.warn(
45
+ "[lazy-supervisor] No queues discovered. Idle until queues are registered or run `yarn generate`."
46
+ );
47
+ } else {
48
+ logger.log(
49
+ `[lazy-supervisor] Watching ${groups.length} queue(s): ${groups.map((g) => g.queueName).join(", ")}`
50
+ );
51
+ }
52
+ const spawnFn = options.spawnFn ?? ((command, args, opts) => nodeSpawn(command, args, opts));
53
+ function logProbeError(queueName, message) {
54
+ const now = Date.now();
55
+ const lastAt = probeErrorLastLoggedAt.get(queueName) ?? 0;
56
+ if (now - lastAt < PROBE_ERROR_LOG_THROTTLE_MS) return;
57
+ probeErrorLastLoggedAt.set(queueName, now);
58
+ logger.warn(`[lazy-supervisor] Probe failed for queue "${queueName}": ${message}`);
59
+ }
60
+ function spawnQueueWorker(queueName) {
61
+ if (stopping) return;
62
+ if (activeChildren.has(queueName)) return;
63
+ if (startingQueues.has(queueName)) return;
64
+ startingQueues.add(queueName);
65
+ logger.log(`[lazy-supervisor] Pending job detected \u2014 starting worker for queue "${queueName}"`);
66
+ let child;
67
+ try {
68
+ child = spawnFn(
69
+ "node",
70
+ [options.mercatoBin, "queue", "worker", queueName],
71
+ {
72
+ stdio: "inherit",
73
+ env: options.runtimeEnv,
74
+ cwd: options.appDir
75
+ }
76
+ );
77
+ } catch (err) {
78
+ startingQueues.delete(queueName);
79
+ const message = err instanceof Error ? err.message : String(err);
80
+ logger.error(`[lazy-supervisor] Failed to spawn worker for "${queueName}": ${message}`);
81
+ return;
82
+ }
83
+ activeChildren.set(queueName, child);
84
+ startedQueues.add(queueName);
85
+ startingQueues.delete(queueName);
86
+ if (options.onSpawn) {
87
+ try {
88
+ options.onSpawn(queueName, child);
89
+ } catch (err) {
90
+ const message = err instanceof Error ? err.message : String(err);
91
+ logger.warn(`[lazy-supervisor] onSpawn callback threw for "${queueName}": ${message}`);
92
+ }
93
+ }
94
+ child.on("exit", (code, signal) => {
95
+ activeChildren.delete(queueName);
96
+ if (options.onChildExit) {
97
+ try {
98
+ options.onChildExit(queueName, code, signal);
99
+ } catch (err) {
100
+ const message = err instanceof Error ? err.message : String(err);
101
+ logger.warn(`[lazy-supervisor] onChildExit callback threw for "${queueName}": ${message}`);
102
+ }
103
+ }
104
+ if (stopping) return;
105
+ const expected = signal === "SIGTERM" || signal === "SIGINT";
106
+ if (expected) return;
107
+ const reason = code !== null ? `code ${code}` : `signal ${signal ?? "unknown"}`;
108
+ logger.warn(`[lazy-supervisor] Worker for "${queueName}" exited (${reason}).`);
109
+ if (!options.restartOnUnexpectedExit) return;
110
+ void (async () => {
111
+ const result = await safeProbe(queueName);
112
+ if (!result || result.error) return;
113
+ if (result.ready > 0) {
114
+ logger.warn(`[lazy-supervisor] Restarting worker for "${queueName}" because jobs remain pending.`);
115
+ spawnQueueWorker(queueName);
116
+ }
117
+ })();
118
+ });
119
+ }
120
+ async function safeProbe(queueName) {
121
+ try {
122
+ return await probe(queueName, strategy);
123
+ } catch (err) {
124
+ const message = err instanceof Error ? err.message : String(err);
125
+ logProbeError(queueName, message);
126
+ return null;
127
+ }
128
+ }
129
+ async function tickOnce() {
130
+ if (stopping) return;
131
+ if (groups.length === 0) return;
132
+ const toCheck = groups.filter((g) => !activeChildren.has(g.queueName) && !startingQueues.has(g.queueName));
133
+ if (toCheck.length === 0) return;
134
+ const probes = await Promise.all(
135
+ toCheck.map(async (group) => ({
136
+ group,
137
+ result: await safeProbe(group.queueName)
138
+ }))
139
+ );
140
+ if (stopping) return;
141
+ for (const { group, result } of probes) {
142
+ if (!result) continue;
143
+ if (result.error) {
144
+ if (result.errorMessage) {
145
+ logProbeError(group.queueName, result.errorMessage);
146
+ }
147
+ continue;
148
+ }
149
+ if (result.ready > 0) {
150
+ spawnQueueWorker(group.queueName);
151
+ }
152
+ }
153
+ }
154
+ function scheduleNext() {
155
+ if (stopping) return;
156
+ pollTimer = setTimeout(() => {
157
+ void (async () => {
158
+ try {
159
+ await tickOnce();
160
+ } catch (err) {
161
+ const message = err instanceof Error ? err.message : String(err);
162
+ logger.warn(`[lazy-supervisor] Poll cycle failed: ${message}`);
163
+ } finally {
164
+ if (!stopping) scheduleNext();
165
+ }
166
+ })();
167
+ }, options.pollMs);
168
+ pollTimer.unref?.();
169
+ }
170
+ void (async () => {
171
+ try {
172
+ await tickOnce();
173
+ } catch (err) {
174
+ const message = err instanceof Error ? err.message : String(err);
175
+ logger.warn(`[lazy-supervisor] Initial poll failed: ${message}`);
176
+ } finally {
177
+ scheduleNext();
178
+ }
179
+ })();
180
+ async function close() {
181
+ if (stopping) {
182
+ await done;
183
+ return;
184
+ }
185
+ stopping = true;
186
+ if (pollTimer) {
187
+ clearTimeout(pollTimer);
188
+ pollTimer = null;
189
+ }
190
+ const exitWaiters = [];
191
+ for (const [queueName, child] of activeChildren) {
192
+ exitWaiters.push(
193
+ new Promise((resolve) => {
194
+ if (child.exitCode !== null || child.signalCode !== null) return resolve();
195
+ child.once("exit", () => resolve());
196
+ })
197
+ );
198
+ try {
199
+ if (!child.killed) child.kill("SIGTERM");
200
+ } catch (err) {
201
+ const message = err instanceof Error ? err.message : String(err);
202
+ logger.warn(`[lazy-supervisor] Failed to send SIGTERM to "${queueName}": ${message}`);
203
+ }
204
+ }
205
+ await Promise.all(exitWaiters);
206
+ activeChildren.clear();
207
+ if (watchPromiseResolve) {
208
+ watchPromiseResolve();
209
+ watchPromiseResolve = null;
210
+ }
211
+ }
212
+ return {
213
+ startedQueues,
214
+ getActiveChild: (queueName) => activeChildren.get(queueName),
215
+ close,
216
+ done
217
+ };
218
+ }
219
+ export {
220
+ startLazyWorkerSupervisor
221
+ };
222
+ //# sourceMappingURL=queue-worker-supervisor.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/lib/queue-worker-supervisor.ts"],
4
+ "sourcesContent": ["/**\n * Lazy queue worker supervisor.\n *\n * Replaces `mercato queue worker --all` with a lightweight watcher that\n * starts a per-queue worker process only after the first ready job appears\n * on that queue. Discovered queues with no jobs stay completely idle.\n *\n * Design:\n * - Probes every queue on a configurable interval using\n * `getQueuePendingProbe()` from `@open-mercato/queue`.\n * - The first non-error probe with `ready > 0` triggers `spawn('node', [\n * mercatoBin, 'queue', 'worker', queueName ])`.\n * - Each spawned child inherits stdio so its logs flow through the\n * surrounding dev runtime.\n * - On unexpected exit while jobs are still pending, the supervisor\n * restarts the worker if `restartOnUnexpectedExit` is enabled.\n * - On `close()`, all active children receive SIGTERM and the supervisor\n * waits for them to exit before resolving.\n *\n * Probes never invoke handlers, never create BullMQ Workers, and never\n * import generated worker handler modules. The contract is preserved by\n * `getQueuePendingProbe()`.\n */\n\nimport { spawn as nodeSpawn } from 'node:child_process'\nimport type { ChildProcess, SpawnOptions } from 'node:child_process'\nimport {\n getQueuePendingProbe,\n type QueuePendingProbeResult,\n type QueueStrategyType,\n} from '@open-mercato/queue'\nimport type { ModuleWorker } from '@open-mercato/shared/modules/registry'\n\nexport type LazySupervisorSpawnFn = (\n command: string,\n args: readonly string[],\n options: SpawnOptions,\n) => ChildProcess\n\nexport type LazySupervisorProbeFn = (\n queueName: string,\n strategy: QueueStrategyType,\n) => Promise<QueuePendingProbeResult>\n\nexport type LazyWorkerSupervisorOptions = {\n mercatoBin: string\n appDir: string\n runtimeEnv: NodeJS.ProcessEnv\n workers: ModuleWorker[]\n pollMs: number\n restartOnUnexpectedExit: boolean\n strategy?: QueueStrategyType\n /** Override for tests. Defaults to `child_process.spawn`. */\n spawnFn?: LazySupervisorSpawnFn\n /** Override for tests. Defaults to `getQueuePendingProbe`. */\n probeFn?: LazySupervisorProbeFn\n /** Logger override. Defaults to `console`. */\n logger?: Pick<Console, 'log' | 'warn' | 'error'>\n onSpawn?: (queueName: string, child: ChildProcess) => void\n onChildExit?: (\n queueName: string,\n code: number | null,\n signal: NodeJS.Signals | null,\n ) => void\n}\n\nexport type LazyWorkerSupervisorHandle = {\n /** Set of queue names whose worker has started at least once. */\n readonly startedQueues: ReadonlySet<string>\n /** Currently running child process per queue (if any). */\n getActiveChild(queueName: string): ChildProcess | undefined\n /** Stop polling, kill children, wait for exit. Idempotent. */\n close(): Promise<void>\n /** Resolves when the watcher loop has stopped (after `close()`). */\n done: Promise<void>\n}\n\ntype QueueGroup = {\n queueName: string\n concurrency: number\n workerCount: number\n}\n\nfunction groupWorkersByQueue(workers: ModuleWorker[]): QueueGroup[] {\n const groups = new Map<string, QueueGroup>()\n for (const worker of workers) {\n const existing = groups.get(worker.queue)\n if (existing) {\n existing.workerCount += 1\n if (worker.concurrency > existing.concurrency) {\n existing.concurrency = worker.concurrency\n }\n } else {\n groups.set(worker.queue, {\n queueName: worker.queue,\n concurrency: Math.max(worker.concurrency, 1),\n workerCount: 1,\n })\n }\n }\n return [...groups.values()].sort((a, b) => a.queueName.localeCompare(b.queueName))\n}\n\nfunction resolveStrategyFromEnv(env: NodeJS.ProcessEnv): QueueStrategyType {\n return env.QUEUE_STRATEGY === 'async' ? 'async' : 'local'\n}\n\nconst PROBE_ERROR_LOG_THROTTLE_MS = 60_000\n\nexport function startLazyWorkerSupervisor(\n options: LazyWorkerSupervisorOptions,\n): LazyWorkerSupervisorHandle {\n const logger = options.logger ?? console\n const strategy: QueueStrategyType = options.strategy ?? resolveStrategyFromEnv(options.runtimeEnv)\n const probe: LazySupervisorProbeFn = options.probeFn\n ?? ((queueName) => getQueuePendingProbe(queueName, strategy))\n const groups = groupWorkersByQueue(options.workers)\n\n const startedQueues = new Set<string>()\n const activeChildren = new Map<string, ChildProcess>()\n const startingQueues = new Set<string>()\n const probeErrorLastLoggedAt = new Map<string, number>()\n\n let stopping = false\n let pollTimer: ReturnType<typeof setTimeout> | null = null\n let watchPromiseResolve: (() => void) | null = null\n const done = new Promise<void>((resolve) => {\n watchPromiseResolve = resolve\n })\n\n if (groups.length === 0) {\n logger.warn(\n '[lazy-supervisor] No queues discovered. Idle until queues are registered or run `yarn generate`.',\n )\n } else {\n logger.log(\n `[lazy-supervisor] Watching ${groups.length} queue(s): ${groups\n .map((g) => g.queueName)\n .join(', ')}`,\n )\n }\n\n const spawnFn: LazySupervisorSpawnFn =\n options.spawnFn ?? ((command, args, opts) => nodeSpawn(command, args as string[], opts))\n\n function logProbeError(queueName: string, message: string): void {\n const now = Date.now()\n const lastAt = probeErrorLastLoggedAt.get(queueName) ?? 0\n if (now - lastAt < PROBE_ERROR_LOG_THROTTLE_MS) return\n probeErrorLastLoggedAt.set(queueName, now)\n logger.warn(`[lazy-supervisor] Probe failed for queue \"${queueName}\": ${message}`)\n }\n\n function spawnQueueWorker(queueName: string): void {\n if (stopping) return\n if (activeChildren.has(queueName)) return\n if (startingQueues.has(queueName)) return\n startingQueues.add(queueName)\n\n logger.log(`[lazy-supervisor] Pending job detected \u2014 starting worker for queue \"${queueName}\"`)\n let child: ChildProcess\n try {\n child = spawnFn(\n 'node',\n [options.mercatoBin, 'queue', 'worker', queueName],\n {\n stdio: 'inherit',\n env: options.runtimeEnv,\n cwd: options.appDir,\n },\n )\n } catch (err) {\n startingQueues.delete(queueName)\n const message = err instanceof Error ? err.message : String(err)\n logger.error(`[lazy-supervisor] Failed to spawn worker for \"${queueName}\": ${message}`)\n return\n }\n\n activeChildren.set(queueName, child)\n startedQueues.add(queueName)\n startingQueues.delete(queueName)\n if (options.onSpawn) {\n try {\n options.onSpawn(queueName, child)\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n logger.warn(`[lazy-supervisor] onSpawn callback threw for \"${queueName}\": ${message}`)\n }\n }\n\n child.on('exit', (code, signal) => {\n activeChildren.delete(queueName)\n if (options.onChildExit) {\n try {\n options.onChildExit(queueName, code, signal)\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n logger.warn(`[lazy-supervisor] onChildExit callback threw for \"${queueName}\": ${message}`)\n }\n }\n if (stopping) return\n const expected = signal === 'SIGTERM' || signal === 'SIGINT'\n if (expected) return\n const reason = code !== null ? `code ${code}` : `signal ${signal ?? 'unknown'}`\n logger.warn(`[lazy-supervisor] Worker for \"${queueName}\" exited (${reason}).`)\n if (!options.restartOnUnexpectedExit) return\n void (async () => {\n const result = await safeProbe(queueName)\n if (!result || result.error) return\n if (result.ready > 0) {\n logger.warn(`[lazy-supervisor] Restarting worker for \"${queueName}\" because jobs remain pending.`)\n spawnQueueWorker(queueName)\n }\n })()\n })\n }\n\n async function safeProbe(queueName: string): Promise<QueuePendingProbeResult | null> {\n try {\n return await probe(queueName, strategy)\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n logProbeError(queueName, message)\n return null\n }\n }\n\n async function tickOnce(): Promise<void> {\n if (stopping) return\n if (groups.length === 0) return\n\n const toCheck = groups.filter((g) => !activeChildren.has(g.queueName) && !startingQueues.has(g.queueName))\n if (toCheck.length === 0) return\n\n const probes = await Promise.all(\n toCheck.map(async (group) => ({\n group,\n result: await safeProbe(group.queueName),\n })),\n )\n\n if (stopping) return\n\n for (const { group, result } of probes) {\n if (!result) continue\n if (result.error) {\n if (result.errorMessage) {\n logProbeError(group.queueName, result.errorMessage)\n }\n continue\n }\n if (result.ready > 0) {\n spawnQueueWorker(group.queueName)\n }\n }\n }\n\n function scheduleNext(): void {\n if (stopping) return\n pollTimer = setTimeout(() => {\n void (async () => {\n try {\n await tickOnce()\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n logger.warn(`[lazy-supervisor] Poll cycle failed: ${message}`)\n } finally {\n if (!stopping) scheduleNext()\n }\n })()\n }, options.pollMs)\n pollTimer.unref?.()\n }\n\n // Kick off immediately so the first probe doesn't wait one full poll interval.\n void (async () => {\n try {\n await tickOnce()\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n logger.warn(`[lazy-supervisor] Initial poll failed: ${message}`)\n } finally {\n scheduleNext()\n }\n })()\n\n async function close(): Promise<void> {\n if (stopping) {\n await done\n return\n }\n stopping = true\n if (pollTimer) {\n clearTimeout(pollTimer)\n pollTimer = null\n }\n\n const exitWaiters: Promise<void>[] = []\n for (const [queueName, child] of activeChildren) {\n exitWaiters.push(\n new Promise<void>((resolve) => {\n if (child.exitCode !== null || child.signalCode !== null) return resolve()\n child.once('exit', () => resolve())\n }),\n )\n try {\n if (!child.killed) child.kill('SIGTERM')\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n logger.warn(`[lazy-supervisor] Failed to send SIGTERM to \"${queueName}\": ${message}`)\n }\n }\n await Promise.all(exitWaiters)\n activeChildren.clear()\n if (watchPromiseResolve) {\n watchPromiseResolve()\n watchPromiseResolve = null\n }\n }\n\n return {\n startedQueues,\n getActiveChild: (queueName) => activeChildren.get(queueName),\n close,\n done,\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
package/dist/mercato.js CHANGED
@@ -4,6 +4,12 @@ import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
4
4
  import { getSslConfig } from "@open-mercato/shared/lib/db/ssl";
5
5
  import { getRedisUrl, getRedisUrlOrThrow } from "@open-mercato/shared/lib/redis/connection";
6
6
  import { resolveInitDerivedSecrets } from "./lib/init-secrets.js";
7
+ import {
8
+ resolveAutoSpawnWorkersMode,
9
+ resolveLazyPollMs,
10
+ resolveLazyRestart
11
+ } from "./lib/auto-spawn-workers.js";
12
+ import { startLazyWorkerSupervisor } from "./lib/queue-worker-supervisor.js";
7
13
  import { parseModuleInstallArgs } from "./lib/module-install-args.js";
8
14
  import { resolveNextBuildIdCandidate } from "./lib/next-build-id.js";
9
15
  import { acquireServerStartLock } from "./lib/server-start-lock.js";
@@ -1382,6 +1388,7 @@ async function run(argv = process.argv) {
1382
1388
  let didRetryCorruptedTurbopackCache = false;
1383
1389
  let stopping = false;
1384
1390
  let envChangePromiseResolve = null;
1391
+ let activeLazySupervisor = null;
1385
1392
  const envReloader = createDevEnvReloader(appDir, process.env, initialProcessEnvironmentEntries);
1386
1393
  function cleanup() {
1387
1394
  console.log("[server] Shutting down...");
@@ -1390,6 +1397,9 @@ async function run(argv = process.argv) {
1390
1397
  proc.kill("SIGTERM");
1391
1398
  }
1392
1399
  }
1400
+ if (activeLazySupervisor) {
1401
+ void activeLazySupervisor.close().catch(() => void 0);
1402
+ }
1393
1403
  }
1394
1404
  async function cleanupAndWait() {
1395
1405
  cleanup();
@@ -1401,6 +1411,13 @@ async function run(argv = process.argv) {
1401
1411
  })
1402
1412
  )
1403
1413
  );
1414
+ if (activeLazySupervisor) {
1415
+ try {
1416
+ await activeLazySupervisor.close();
1417
+ } catch {
1418
+ }
1419
+ activeLazySupervisor = null;
1420
+ }
1404
1421
  const lockFile = path.join(appDir, ".mercato", "next", "dev", "lock");
1405
1422
  try {
1406
1423
  fs.unlinkSync(lockFile);
@@ -1474,7 +1491,7 @@ async function run(argv = process.argv) {
1474
1491
  while (!stopping) {
1475
1492
  envReloader.reload();
1476
1493
  const runtimeEnv = buildServerProcessEnvironment(process.env);
1477
- const autoSpawnWorkers = process.env.AUTO_SPAWN_WORKERS !== "false";
1494
+ const autoSpawnWorkersMode = resolveAutoSpawnWorkersMode(process.env);
1478
1495
  const autoSpawnScheduler = process.env.AUTO_SPAWN_SCHEDULER !== "false";
1479
1496
  const queueStrategy = process.env.QUEUE_STRATEGY || "local";
1480
1497
  const schedulerCommand = lookupModuleCommand(getCliModules(), "scheduler", "start");
@@ -1482,10 +1499,21 @@ async function run(argv = process.argv) {
1482
1499
  startNextDev(runtimeEnv),
1483
1500
  waitForEnvChange()
1484
1501
  ];
1485
- if (autoSpawnWorkers) {
1486
- const discoveredWorkerQueues = [...new Set(getRegisteredCliWorkers().map((worker) => worker.queue))];
1502
+ if (autoSpawnWorkersMode !== "off") {
1503
+ const discoveredWorkers = getRegisteredCliWorkers();
1504
+ const discoveredWorkerQueues = [...new Set(discoveredWorkers.map((worker) => worker.queue))];
1487
1505
  if (discoveredWorkerQueues.length === 0) {
1488
1506
  console.error("[server] AUTO_SPAWN_WORKERS is enabled, but no queues were discovered from CLI modules. Run `yarn generate` and verify `.mercato/generated/modules.cli.generated.ts` contains worker entries. Continuing without auto-spawned workers.");
1507
+ } else if (autoSpawnWorkersMode === "lazy") {
1508
+ console.log(`[server] Lazy worker auto-spawn enabled \u2014 workers will start on first job (${discoveredWorkerQueues.length} queue(s) watched).`);
1509
+ activeLazySupervisor = startLazyWorkerSupervisor({
1510
+ mercatoBin,
1511
+ appDir,
1512
+ runtimeEnv,
1513
+ workers: discoveredWorkers,
1514
+ pollMs: resolveLazyPollMs(process.env),
1515
+ restartOnUnexpectedExit: resolveLazyRestart(process.env)
1516
+ });
1489
1517
  } else {
1490
1518
  console.log("[server] Starting workers for all queues...");
1491
1519
  const workerProcess = spawn("node", [mercatoBin, "queue", "worker", "--all"], {
@@ -1537,7 +1565,7 @@ async function run(argv = process.argv) {
1537
1565
  const appDir = env.appDir;
1538
1566
  const nodeModulesBases = Array.from(/* @__PURE__ */ new Set([env.rootDir, appDir]));
1539
1567
  const processes = [];
1540
- const autoSpawnWorkers = process.env.AUTO_SPAWN_WORKERS !== "false";
1568
+ const autoSpawnWorkersMode = resolveAutoSpawnWorkersMode(process.env);
1541
1569
  const autoSpawnScheduler = process.env.AUTO_SPAWN_SCHEDULER !== "false";
1542
1570
  const queueStrategy = process.env.QUEUE_STRATEGY || "local";
1543
1571
  const runtimeEnv = buildServerProcessEnvironment(process.env);
@@ -1545,6 +1573,7 @@ async function run(argv = process.argv) {
1545
1573
  const serverStartLock = acquireServerStartLock(appDir, {
1546
1574
  port: runtimeEnv.PORT ?? process.env.PORT ?? null
1547
1575
  });
1576
+ let activeLazySupervisor = null;
1548
1577
  function cleanup() {
1549
1578
  console.log("[server] Shutting down...");
1550
1579
  for (const proc of processes) {
@@ -1552,6 +1581,9 @@ async function run(argv = process.argv) {
1552
1581
  proc.kill("SIGTERM");
1553
1582
  }
1554
1583
  }
1584
+ if (activeLazySupervisor) {
1585
+ void activeLazySupervisor.close().catch(() => void 0);
1586
+ }
1555
1587
  }
1556
1588
  async function cleanupAndWait() {
1557
1589
  cleanup();
@@ -1563,6 +1595,13 @@ async function run(argv = process.argv) {
1563
1595
  })
1564
1596
  )
1565
1597
  );
1598
+ if (activeLazySupervisor) {
1599
+ try {
1600
+ await activeLazySupervisor.close();
1601
+ } catch {
1602
+ }
1603
+ activeLazySupervisor = null;
1604
+ }
1566
1605
  }
1567
1606
  process.on("SIGTERM", cleanup);
1568
1607
  process.on("SIGINT", cleanup);
@@ -1580,10 +1619,21 @@ async function run(argv = process.argv) {
1580
1619
  const managedExitPromises = [
1581
1620
  waitForManagedProcessExit(nextProcess, "Next.js production server")
1582
1621
  ];
1583
- if (autoSpawnWorkers) {
1584
- const discoveredWorkerQueues = [...new Set(getRegisteredCliWorkers().map((worker) => worker.queue))];
1622
+ if (autoSpawnWorkersMode !== "off") {
1623
+ const discoveredWorkers = getRegisteredCliWorkers();
1624
+ const discoveredWorkerQueues = [...new Set(discoveredWorkers.map((worker) => worker.queue))];
1585
1625
  if (discoveredWorkerQueues.length === 0) {
1586
1626
  console.error("[server] AUTO_SPAWN_WORKERS is enabled, but no queues were discovered from CLI modules. Run `yarn generate` and verify `.mercato/generated/modules.cli.generated.ts` contains worker entries. Continuing without auto-spawned workers.");
1627
+ } else if (autoSpawnWorkersMode === "lazy") {
1628
+ console.log(`[server] Lazy worker auto-spawn enabled \u2014 workers will start on first job (${discoveredWorkerQueues.length} queue(s) watched).`);
1629
+ activeLazySupervisor = startLazyWorkerSupervisor({
1630
+ mercatoBin,
1631
+ appDir,
1632
+ runtimeEnv,
1633
+ workers: discoveredWorkers,
1634
+ pollMs: resolveLazyPollMs(process.env),
1635
+ restartOnUnexpectedExit: resolveLazyRestart(process.env)
1636
+ });
1587
1637
  } else {
1588
1638
  console.log("[server] Starting workers for all queues...");
1589
1639
  const workerProcess = spawn("node", [mercatoBin, "queue", "worker", "--all"], {