@andrew_l/app 0.3.7

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.mjs ADDED
@@ -0,0 +1,2624 @@
1
+ import { env, getLoggerLevel, noop, isFunction, typeOf, EJSON, captureStackTrace, def, isSkip, defer, toError, SimpleEventEmitter, camelCase, capitalize, asyncForEach, pickPrefixed, isError, Scheduler, isObject, assert, catchError, isString, getFileExtension, uniq, isNumber, kebabCase, arrayable, AsyncIterableQueue, ResourcePool, CancellablePromise } from '@andrew_l/toolkit';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { createConsola, LogLevels } from 'consola';
5
+ import { isShuttingDown, onShutdownError, onShutdown, processGraceful } from '@andrew_l/graceful';
6
+ import * as cp from 'node:child_process';
7
+ import { EventEmitter } from 'node:events';
8
+ import { Option, InvalidArgumentError, Command } from 'commander';
9
+ import fs from 'node:fs';
10
+ import dotenv from 'dotenv';
11
+ import { getColor } from 'consola/utils';
12
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
13
+ import { Box, Text, useStdout, useInput, render } from 'ink';
14
+ import { createContext, useState, useEffect, useContext } from 'react';
15
+
16
+ const CONFIG = Object.freeze({
17
+ IS_VRUN: env.bool("VRUN", false),
18
+ WATCH_MODE: env.bool("VRUN_WATCH", false),
19
+ /**
20
+ * Root path of application
21
+ */
22
+ APP_ROOT: env.string("APP_ROOT") || null,
23
+ /**
24
+ * Typescript resolver mode
25
+ */
26
+ TS_MODE: env.string(
27
+ "VRUN_TS_MODE",
28
+ process.execArgv.some((v) => v.includes("tsx/dist/loader.mjs")) ? "tsx" : ""
29
+ ) || null,
30
+ /**
31
+ * List of disabled workers
32
+ */
33
+ WORKER_DISABLED: new Set(env.list("WORKER_DISABLE", "string"))
34
+ });
35
+
36
+ const isHelpArgument = (argv) => {
37
+ if (!Array.isArray(argv)) {
38
+ argv = [argv];
39
+ }
40
+ return argv.includes("--help") || argv.includes("-h");
41
+ };
42
+ function extractOptionsArgs(argv) {
43
+ const result = [];
44
+ let flag = false;
45
+ for (const value of argv) {
46
+ if (value.startsWith("--")) {
47
+ flag = true;
48
+ result.push(value);
49
+ continue;
50
+ }
51
+ if (flag) {
52
+ result.push(value);
53
+ flag = false;
54
+ }
55
+ }
56
+ return result;
57
+ }
58
+
59
+ function filePathFromStack(stack) {
60
+ const stackLines = stack.trim().split("\n");
61
+ const callerLine = stackLines[0];
62
+ let filePath = null;
63
+ const patterns = [
64
+ /\((.+):(\d+):(\d+)\)$/,
65
+ // (file:line:col)
66
+ /at\s+(.+):(\d+):(\d+)$/,
67
+ // at file:line:col
68
+ /\s+at\s+(.+):(\d+):(\d+)$/,
69
+ // at file:line:col with spaces
70
+ /\(file:\/\/(.+):(\d+):(\d+)\)$/,
71
+ // (file://path:line:col)
72
+ /file:\/\/(.+):(\d+):(\d+)$/
73
+ // file://path:line:col
74
+ ];
75
+ for (const pattern of patterns) {
76
+ const match = callerLine.match(pattern);
77
+ if (match) {
78
+ filePath = match[1];
79
+ break;
80
+ }
81
+ }
82
+ if (!filePath) {
83
+ return null;
84
+ }
85
+ if (filePath.startsWith("file://")) {
86
+ filePath = filePath.replace("file://", "");
87
+ }
88
+ if (process.platform === "win32") {
89
+ filePath = filePath.replace(/\//g, "\\");
90
+ if (filePath.match(/^[A-Za-z]:\\/)) ; else if (filePath.startsWith("\\")) ; else {
91
+ filePath = path.resolve(filePath);
92
+ }
93
+ }
94
+ return path.normalize(filePath);
95
+ }
96
+
97
+ function isMainFile(filepath) {
98
+ const cwd = process.cwd();
99
+ return process.argv.some((arg) => filepath.endsWith(path.resolve(cwd, arg)));
100
+ }
101
+
102
+ const createLogger = (tagName) => {
103
+ const log2 = createConsola({
104
+ formatOptions: { date: false },
105
+ level: LogLevels[getLoggerLevel()],
106
+ fancy: true,
107
+ defaults: {
108
+ tag: tagName
109
+ }
110
+ });
111
+ log2.extend = (tagName2) => createLogger(tagName2);
112
+ return log2;
113
+ };
114
+ const log = createLogger();
115
+ const noopLogger = {
116
+ debug: noop,
117
+ error: noop,
118
+ extend: () => noopLogger,
119
+ info: noop,
120
+ log: noop,
121
+ warn: noop
122
+ };
123
+
124
+ function formatLogEvent(event, fields) {
125
+ if (!fields) return event;
126
+ const parts = [];
127
+ for (const key in fields) {
128
+ if (key === "vrun_app_thread_message") continue;
129
+ const v = fields[key];
130
+ if (v === void 0) continue;
131
+ parts.push(`${key}=${formatField(v)}`);
132
+ }
133
+ return parts.length ? `${event} ${parts.join(" ")}` : event;
134
+ }
135
+ function formatField(value) {
136
+ switch (typeOf(value)) {
137
+ case "array":
138
+ case "object":
139
+ return EJSON.stringify(value);
140
+ default: {
141
+ return String(value);
142
+ }
143
+ }
144
+ }
145
+ function createAppLogger(definition) {
146
+ return definition.logger === false ? noopLogger : isFunction(definition.logger) ? definition.logger(definition) : definition.logger || createLogger(definition.name);
147
+ }
148
+
149
+ const APP_INSTANCE_STATE = {
150
+ INIT: "init",
151
+ IN_SETUP: "in-setup",
152
+ SETUP: "setup",
153
+ IN_STOP: "in-stop",
154
+ IN_RUN: "in-run",
155
+ RUN: "run",
156
+ STOP: "stop",
157
+ IN_SHUTDOWN: "in-shutdown",
158
+ SHUTDOWN: "shutdown",
159
+ ERROR: "error"
160
+ };
161
+ const APP_DEF = Symbol("app-definition");
162
+ function defineApp(definition) {
163
+ const result = {
164
+ filePath: filePathFromStack(captureStackTrace(defineApp)),
165
+ ...definition
166
+ };
167
+ def(result, APP_DEF, true);
168
+ if (!CONFIG.IS_VRUN) {
169
+ if (result.filePath) {
170
+ if (isMainFile(result.filePath)) {
171
+ Promise.resolve().then(function () { return index; }).then((m) => {
172
+ m.cli.runApp({
173
+ cliName: "app",
174
+ scriptFile: result.filePath,
175
+ argv: extractOptionsArgs(process.argv.slice(1))
176
+ });
177
+ });
178
+ }
179
+ }
180
+ }
181
+ return result;
182
+ }
183
+ function createAppInstance(definition) {
184
+ const setupState = createSetupState(definition);
185
+ return {
186
+ definition,
187
+ props: null,
188
+ setupState,
189
+ eventBus: new SimpleEventEmitter(),
190
+ mutexName: null,
191
+ mutexQueue: Promise.resolve(),
192
+ state: APP_INSTANCE_STATE.INIT,
193
+ logger: setupState.log
194
+ };
195
+ }
196
+ function isAppDefinition(value) {
197
+ return value?.[APP_DEF] === true;
198
+ }
199
+ async function startApp(app, props) {
200
+ const instance = isAppDefinition(app) ? createAppInstance(app) : app;
201
+ const setupResult = await setupApp(instance, props);
202
+ if (isSkip(setupResult)) {
203
+ return setupResult;
204
+ }
205
+ const runResult = await runApp$1(instance);
206
+ if (isSkip(runResult)) {
207
+ await shutdownApp(instance);
208
+ return runResult;
209
+ }
210
+ return {
211
+ success: true,
212
+ code: "start_app",
213
+ app: instance
214
+ };
215
+ }
216
+ async function setupApp(instance, props) {
217
+ const mutexResolve = await mutexAcquire(instance, "setup");
218
+ if (instance.state !== APP_INSTANCE_STATE.INIT) {
219
+ mutexResolve();
220
+ return {
221
+ skip: true,
222
+ code: "setup_app",
223
+ reason: `application in ${instance.state} state`
224
+ };
225
+ }
226
+ setState$1(instance, APP_INSTANCE_STATE.IN_SETUP);
227
+ const { setup } = instance.definition;
228
+ if (isFunction(setup)) {
229
+ try {
230
+ const setupResult = await setup.call(instance.setupState, props);
231
+ Object.assign(instance.setupState, setupResult);
232
+ } catch (error) {
233
+ setState$1(instance, APP_INSTANCE_STATE.ERROR);
234
+ mutexResolve();
235
+ return {
236
+ code: "setup_app",
237
+ skip: true,
238
+ reason: "setup function throw error",
239
+ error: toError(error)
240
+ };
241
+ }
242
+ }
243
+ instance.props = props;
244
+ setState$1(instance, APP_INSTANCE_STATE.SETUP);
245
+ mutexResolve();
246
+ return {
247
+ success: true,
248
+ code: "setup_app"
249
+ };
250
+ }
251
+ async function runApp$1(instance) {
252
+ const mutexResolve = await mutexAcquire(instance, "run");
253
+ if (instance.state !== APP_INSTANCE_STATE.SETUP) {
254
+ mutexResolve();
255
+ return {
256
+ skip: true,
257
+ code: "execute_app",
258
+ reason: `application in ${instance.state} state`
259
+ };
260
+ }
261
+ const { entry } = instance.definition;
262
+ setState$1(instance, APP_INSTANCE_STATE.IN_RUN);
263
+ instance.logger.info("Starting...");
264
+ if (isFunction(entry)) {
265
+ try {
266
+ await entry.call(instance.setupState, instance.props);
267
+ } catch (error) {
268
+ setState$1(instance, APP_INSTANCE_STATE.ERROR);
269
+ mutexResolve();
270
+ return {
271
+ skip: true,
272
+ code: "execute_app",
273
+ reason: "entry function throw error",
274
+ error: toError(error)
275
+ };
276
+ }
277
+ }
278
+ setState$1(instance, APP_INSTANCE_STATE.RUN);
279
+ mutexResolve();
280
+ instance.logger.info("Started");
281
+ return {
282
+ success: true,
283
+ code: "execute_app"
284
+ };
285
+ }
286
+ async function stopApp(instance) {
287
+ const mutexResolve = await mutexAcquire(instance, "stop");
288
+ if (instance.state !== APP_INSTANCE_STATE.RUN) {
289
+ mutexResolve();
290
+ return {
291
+ skip: true,
292
+ code: "stop_app",
293
+ reason: `application in ${instance.state} state`
294
+ };
295
+ }
296
+ const { stop } = instance.definition;
297
+ setState$1(instance, APP_INSTANCE_STATE.IN_STOP);
298
+ instance.logger.info("Stopping...");
299
+ let stopErr;
300
+ if (isFunction(stop)) {
301
+ try {
302
+ await stop.call(instance.setupState, instance.props);
303
+ } catch (err) {
304
+ setState$1(instance, APP_INSTANCE_STATE.ERROR);
305
+ stopErr = toError(err);
306
+ }
307
+ }
308
+ mutexResolve();
309
+ if (stopErr) {
310
+ return {
311
+ skip: true,
312
+ code: "stop_app",
313
+ reason: "stop function throw error",
314
+ error: stopErr
315
+ };
316
+ }
317
+ setState$1(instance, APP_INSTANCE_STATE.STOP);
318
+ instance.logger.info("Stopped");
319
+ return {
320
+ success: true,
321
+ code: "stop_app"
322
+ };
323
+ }
324
+ async function shutdownApp(instance) {
325
+ await stopApp(instance);
326
+ const mutexResolve = await mutexAcquire(instance, "shutdown");
327
+ if (instance.state !== APP_INSTANCE_STATE.STOP && instance.state !== APP_INSTANCE_STATE.SETUP && instance.state !== APP_INSTANCE_STATE.ERROR) {
328
+ mutexResolve();
329
+ return {
330
+ skip: true,
331
+ code: "shutdown_app",
332
+ reason: `application in ${instance.state} state`
333
+ };
334
+ }
335
+ const { shutdown } = instance.definition;
336
+ setState$1(instance, APP_INSTANCE_STATE.IN_SHUTDOWN);
337
+ instance.logger.info("Shutdown...");
338
+ let shutdownErr;
339
+ if (isFunction(shutdown)) {
340
+ try {
341
+ await shutdown.call(instance.setupState, instance.props);
342
+ } catch (err) {
343
+ shutdownErr = toError(err);
344
+ }
345
+ }
346
+ instance.props = null;
347
+ instance.setupState = createSetupState(instance.definition);
348
+ mutexResolve();
349
+ if (shutdownErr) {
350
+ setState$1(instance, APP_INSTANCE_STATE.ERROR);
351
+ return {
352
+ skip: true,
353
+ code: "shutdown_app",
354
+ reason: "shutdown function throw error",
355
+ error: shutdownErr
356
+ };
357
+ }
358
+ setState$1(instance, APP_INSTANCE_STATE.SHUTDOWN);
359
+ return {
360
+ success: true,
361
+ code: "shutdown_app"
362
+ };
363
+ }
364
+ function appWaitShutdown(instance) {
365
+ if (instance.state === APP_INSTANCE_STATE.SHUTDOWN) {
366
+ return Promise.resolve();
367
+ }
368
+ const q = defer();
369
+ instance.eventBus.once("state:shutdown", q.resolve);
370
+ instance.eventBus.once("error", q.reject);
371
+ return q.promise.finally(() => {
372
+ instance.eventBus.off("state:shutdown", q.resolve);
373
+ instance.eventBus.off("error", q.reject);
374
+ });
375
+ }
376
+ function createSetupState(definition) {
377
+ const setupState = {
378
+ log: createAppLogger(definition)
379
+ };
380
+ if (definition.methods) {
381
+ for (const [key, fn] of Object.entries(definition.methods)) {
382
+ setupState[key] = fn.bind(setupState);
383
+ }
384
+ }
385
+ return setupState;
386
+ }
387
+ function mutexAcquire(instance, name) {
388
+ let outerResolve;
389
+ const acquirePromise = new Promise((res) => {
390
+ outerResolve = res;
391
+ });
392
+ instance.mutexQueue = instance.mutexQueue.then(
393
+ () => new Promise((innerResolve) => {
394
+ instance.mutexName = name;
395
+ outerResolve(() => {
396
+ instance.mutexName = null;
397
+ innerResolve();
398
+ });
399
+ })
400
+ );
401
+ return acquirePromise;
402
+ }
403
+ function setState$1(app, newState) {
404
+ const oldState = app.state;
405
+ if (newState !== oldState) {
406
+ app.state = newState;
407
+ app.eventBus.emit("state", newState, oldState);
408
+ app.eventBus.emit(`state:${newState}`);
409
+ }
410
+ }
411
+
412
+ function createAppHub(definitions) {
413
+ const app = defineApp({
414
+ name: "app-hub",
415
+ description: `Application wrapper around: ${definitions.map((v) => v.name).join(", ")}`,
416
+ logger: false,
417
+ props: prefixifyProps(definitions),
418
+ setup(props) {
419
+ const instances = [];
420
+ const setupSkips = [];
421
+ return asyncForEach(definitions, (definition) => {
422
+ const appProps = getPrefixedProps(definition, props);
423
+ const instance = createAppInstance(definition);
424
+ instances.push(instance);
425
+ return setupApp(instance, appProps).then((setupResult) => {
426
+ if (isSkip(setupResult)) {
427
+ setupSkips.push({ ...setupResult, app: instance });
428
+ }
429
+ });
430
+ }).then(() => {
431
+ if (setupSkips.length) {
432
+ throw skipExecsToError("setup", setupSkips);
433
+ }
434
+ return { instances };
435
+ });
436
+ },
437
+ entry() {
438
+ const runSkips = [];
439
+ return asyncForEach(this.instances, (instance) => {
440
+ return runApp$1(instance).then((runResult) => {
441
+ if (isSkip(runResult)) {
442
+ runSkips.push({ ...runResult, app: instance });
443
+ }
444
+ });
445
+ }).then(() => {
446
+ if (runSkips.length) {
447
+ throw skipExecsToError("start", runSkips);
448
+ }
449
+ });
450
+ },
451
+ stop() {
452
+ const stopSkips = [];
453
+ return asyncForEach(this.instances, (instance) => {
454
+ return stopApp(instance).then((stopResult) => {
455
+ if (isSkip(stopResult)) {
456
+ stopSkips.push({ ...stopResult, app: instance });
457
+ }
458
+ });
459
+ }).then(() => {
460
+ if (stopSkips.length) {
461
+ throw skipExecsToError("stop", stopSkips);
462
+ }
463
+ });
464
+ },
465
+ shutdown() {
466
+ const shutdownSkips = [];
467
+ return asyncForEach(this.instances, (instance) => {
468
+ return shutdownApp(instance).then((shutdownResult) => {
469
+ if (isSkip(shutdownResult)) {
470
+ shutdownSkips.push({ ...shutdownResult, app: instance });
471
+ }
472
+ });
473
+ }).then(() => {
474
+ if (shutdownSkips.length) {
475
+ throw skipExecsToError("shutdown", shutdownSkips);
476
+ }
477
+ });
478
+ }
479
+ });
480
+ return app;
481
+ }
482
+ function skipExecsToError(verb, skips) {
483
+ const getError = (r) => {
484
+ if ("error" in r && isError(r.error)) {
485
+ return r.error.stack ? `
486
+ ${r.error.message}
487
+ ${r.error.stack}` : `
488
+ ${r.error.message}`;
489
+ }
490
+ return "";
491
+ };
492
+ const lines = skips.map(
493
+ (s) => s.reason ? ` ${s.app.definition.name} [${s.code}]: ${s.reason}${getError(s).replaceAll("\n", "\n ")}` : ` ${s.app.definition.name} [${s.code}]${getError(s).replaceAll("\n", "\n ")}`
494
+ );
495
+ return new Error(
496
+ `Failed to ${verb} ${skips.length} app(s):
497
+ ${lines.join("\n")}`,
498
+ { cause: "error" in skips[0] ? skips[0].error : void 0 }
499
+ );
500
+ }
501
+ function getPrefixedProps(definition, props) {
502
+ const result = pickPrefixed(props, {
503
+ prefix: camelCase(definition.name),
504
+ prefixTrim: true
505
+ });
506
+ for (const [key, value] of Object.entries(result)) {
507
+ const propName = key.charAt(0).toLowerCase() + key.slice(1).toLowerCase();
508
+ result[propName] = value;
509
+ delete result[key];
510
+ }
511
+ return result;
512
+ }
513
+ function prefixifyProps(definitions) {
514
+ const props = {};
515
+ for (const definition of definitions) {
516
+ if (definition.props) {
517
+ for (const [propName, prop] of Object.entries(definition.props)) {
518
+ props[camelCase(`${definition.name}${capitalize(propName)}`)] = prop;
519
+ }
520
+ }
521
+ }
522
+ return props;
523
+ }
524
+
525
+ const MAX_RESTARTS = 3;
526
+ const HEARTBEAT_INTERVAL_MS = 5e3;
527
+ const HEARTBEAT_TIMEOUT_MS = 1e4;
528
+ const SHUTDOWN_EXIT_TIMEOUT_MS = 5e3;
529
+ const SHUTDOWN_REPLY_TIMEOUT_MS = 3e4;
530
+ const appModulePath = import.meta.url.endsWith(".ts") ? new URL("./index.js", import.meta.url).href : import.meta.url;
531
+ const bootstrapCode = `
532
+ import { delay } from '@andrew_l/toolkit'
533
+ import { processGraceful, onShutdown, onShutdownError } from '@andrew_l/graceful'
534
+ import process from 'node:process';
535
+
536
+ const scriptFile = process.env.VRUN_PROCESS_SCRIPT_FILE;
537
+ const threadId = parseInt(process.env.VRUN_PROCESS_ID, 10);
538
+ const appUrl = ${JSON.stringify(appModulePath)};
539
+
540
+ if (
541
+ scriptFile.endsWith('.ts') ||
542
+ appUrl.endsWith('.ts') ||
543
+ appUrl.includes('/src/')
544
+ ) {
545
+ const { register } = await import('tsx/esm/api');
546
+ register();
547
+ }
548
+
549
+ const { isAppDefinition, createAppThreadInstance, shutdownApp } = await import(appUrl);
550
+
551
+ const definition = await import(scriptFile).then(r => r.default);
552
+
553
+ if (!isAppDefinition(definition)) {
554
+ throw new Error('Default export must be an app definition: ' + scriptFile);
555
+ }
556
+
557
+ const listeners = new Set();
558
+ const port = {
559
+ on(event, cb) {
560
+ if (event === 'message') {
561
+ process.on('message', cb);
562
+ listeners.add(cb);
563
+ }
564
+ },
565
+ postMessage(msg, cb) {
566
+ process.send(msg, cb);
567
+ },
568
+ close() {
569
+ for (const cb of listeners) process.off('message', cb);
570
+ },
571
+ };
572
+
573
+ const app = createAppThreadInstance({
574
+ threadId,
575
+ definition,
576
+ parentPort: port,
577
+ // onShutdown: () => processGraceful()
578
+ });
579
+
580
+ const log = app.logger;
581
+
582
+ onShutdownError((error) => {
583
+ log.error('Shutdown error:', error);
584
+ });
585
+
586
+ onShutdown('app', async () => {
587
+ log.info('Shutdown signal received');
588
+ await shutdownApp(app);
589
+ });
590
+ `;
591
+ function setState(w, next) {
592
+ const prev = w.state;
593
+ if (prev === next) return;
594
+ w.state = next;
595
+ w.writeLog("debug", "state", { from: prev, to: next });
596
+ w.eventBus.emit("state", next, prev);
597
+ }
598
+ function spawnChild(w) {
599
+ w.writeLog("debug", "thread.spawn.start", {
600
+ script: path.basename(w.scriptFile),
601
+ thread: w.threadId
602
+ });
603
+ setState(w, "init");
604
+ const child = cp.spawn(
605
+ process.execPath,
606
+ ["--input-type=module", "-e", bootstrapCode],
607
+ {
608
+ stdio: w.threadProps.__inheritIO !== false ? ["pipe", "inherit", "inherit", "ipc"] : ["pipe", "pipe", "pipe", "ipc"],
609
+ env: {
610
+ ...process.env,
611
+ VRUN_PROCESS_SCRIPT_FILE: w.scriptFile,
612
+ VRUN_PROCESS_ID: String(w.threadId)
613
+ }
614
+ }
615
+ );
616
+ w.child = child;
617
+ const onData = (chunk) => {
618
+ const text = chunk.toString("utf8");
619
+ for (const line of text.split(/\r?\n/)) {
620
+ if (line.length === 0) continue;
621
+ w.eventBus.emit("log", parseRawLogLine(line));
622
+ }
623
+ };
624
+ child.stdout?.on("data", onData);
625
+ child.stderr?.on("data", onData);
626
+ child.on("message", (msg) => {
627
+ if (isThreadMessage(msg)) {
628
+ w.eventBus.emit("message", msg);
629
+ w.eventBus.emit(`msg:${msg.type}`, msg);
630
+ }
631
+ });
632
+ child.once("exit", (code, signal) => {
633
+ w.eventBus.emit("exit", code, signal);
634
+ });
635
+ child.on("error", (err) => {
636
+ w.writeLog("error", "thread.spawn.error", { message: err.message });
637
+ w.eventBus.emit("error", err);
638
+ });
639
+ }
640
+ function startHeartbeat(w) {
641
+ w.lastPong = Date.now();
642
+ w.heartbeatTimer = setInterval(() => {
643
+ if (w.state !== APP_INSTANCE_STATE.RUN) {
644
+ clearInterval(w.heartbeatTimer);
645
+ return;
646
+ }
647
+ const since = Date.now() - w.lastPong;
648
+ if (since > HEARTBEAT_TIMEOUT_MS) {
649
+ clearInterval(w.heartbeatTimer);
650
+ w.writeLog("warn", "heartbeat.miss", { since_ms: since });
651
+ restartThreadApp(w).catch((err) => w.eventBus.emit("error", err));
652
+ return;
653
+ }
654
+ if (w.child.connected) {
655
+ w.child.send(createThreadMessage("ping", {}));
656
+ }
657
+ }, HEARTBEAT_INTERVAL_MS);
658
+ }
659
+ function waitForExit(w, timeoutMs) {
660
+ if (isThreadExit(w)) {
661
+ return Promise.resolve();
662
+ }
663
+ return new Promise((resolve) => {
664
+ w.writeLog("info", "thread.exit.waiting", { timeout_ms: timeoutMs });
665
+ const cleanup = () => {
666
+ clearTimeout(slowTimer);
667
+ clearTimeout(killTimer);
668
+ w.child.off("exit", onExit);
669
+ };
670
+ const onExit = () => {
671
+ cleanup();
672
+ resolve();
673
+ };
674
+ const halfway = Math.floor(timeoutMs / 2);
675
+ const slowTimer = setTimeout(() => {
676
+ w.writeLog("warn", "thread.exit.slow", { elapsed_ms: halfway });
677
+ }, halfway);
678
+ const killTimer = setTimeout(() => {
679
+ cleanup();
680
+ try {
681
+ w.writeLog("warn", "thread.exit.force-kill", { timeout_ms: timeoutMs });
682
+ w.child.kill("SIGKILL");
683
+ } catch {
684
+ }
685
+ resolve();
686
+ }, timeoutMs);
687
+ w.child.once("exit", onExit);
688
+ });
689
+ }
690
+ function waitForThreadReady(w) {
691
+ if (w.state === "ready") {
692
+ return Promise.resolve();
693
+ }
694
+ return new Promise((resolve, reject) => {
695
+ const cleanup = () => {
696
+ w.eventBus.off("ready", onReady);
697
+ w.eventBus.off("error", onError);
698
+ w.eventBus.off("exit", onExit);
699
+ };
700
+ const onReady = () => {
701
+ resolve();
702
+ cleanup();
703
+ };
704
+ const onExit = (code) => {
705
+ reject(new Error(`Child exited before ready (code=${code ?? "null"})`));
706
+ cleanup();
707
+ };
708
+ const onError = (error) => {
709
+ reject(error);
710
+ cleanup();
711
+ };
712
+ w.eventBus.once("ready", onReady);
713
+ w.eventBus.once("error", onError);
714
+ w.eventBus.once("exit", onExit);
715
+ });
716
+ }
717
+ function sendAndWait(w, message, replyType, timeout = 3e4) {
718
+ return new Promise((resolve) => {
719
+ const eventName = `msg:${replyType}`;
720
+ const cleanup = () => {
721
+ w.eventBus.off(eventName, onReply);
722
+ w.eventBus.off("exit", onExit);
723
+ clearTimeout(timer);
724
+ };
725
+ const timer = setTimeout(() => {
726
+ cleanup();
727
+ w.writeLog("error", "parent.ipc.timeout", {
728
+ waiting: replyType,
729
+ ms: timeout
730
+ });
731
+ resolve({
732
+ skip: true,
733
+ code: "parent.ipc.timeout",
734
+ reason: `Thread ${path.basename(w.scriptFile)} timed out waiting for "${replyType}"`
735
+ });
736
+ }, timeout);
737
+ const onReply = (reply) => {
738
+ cleanup();
739
+ resolve({
740
+ success: true,
741
+ code: "parent.ipc.recv",
742
+ reply
743
+ });
744
+ };
745
+ const onExit = () => {
746
+ const reason = `thread.exit (code=${w.child.exitCode} signal=${w.child.signalCode ?? "null"})`;
747
+ w.writeLog("debug", "parent.ipc.skipped", {
748
+ waiting: replyType,
749
+ reason
750
+ });
751
+ resolve({ skip: true, code: "thread.exit", reason });
752
+ return;
753
+ };
754
+ if (isThreadExit(w)) {
755
+ return void onExit();
756
+ }
757
+ w.eventBus.once("exit", onExit);
758
+ w.eventBus.once(eventName, onReply);
759
+ w.writeLog("debug", "parent.ipc.send", message);
760
+ try {
761
+ w.child.send(message);
762
+ } catch (err) {
763
+ const error = toError(err);
764
+ w.writeLog("error", "parent.ipc.fail", {
765
+ message,
766
+ error: error.message
767
+ });
768
+ cleanup();
769
+ resolve({
770
+ skip: true,
771
+ code: "parent.ipc.fail",
772
+ reason: error.message
773
+ });
774
+ }
775
+ });
776
+ }
777
+ function initThread(threadId, scriptFile, threadProps) {
778
+ const w = {
779
+ threadId,
780
+ scriptFile,
781
+ threadProps,
782
+ pid: 0,
783
+ state: APP_INSTANCE_STATE.INIT,
784
+ restartCount: 0,
785
+ lastPong: 0,
786
+ child: null,
787
+ scheduler: new Scheduler(),
788
+ eventBus: new EventEmitter(),
789
+ writeLog(level, event, fields) {
790
+ w.eventBus.emit("log", {
791
+ level,
792
+ text: formatLogEvent(event, fields),
793
+ ts: Date.now()
794
+ });
795
+ }
796
+ };
797
+ w.eventBus.setMaxListeners(0);
798
+ const onJobPhase = (phase) => (job) => {
799
+ w.writeLog("debug", `job.${job.jobName ?? "untitled"}.${phase}`);
800
+ };
801
+ w.scheduler.onJob(onJobPhase("queue"));
802
+ w.scheduler.onJobStart(onJobPhase("start"));
803
+ w.scheduler.onJobComplete(onJobPhase("done"));
804
+ w.eventBus.on("exit", (code, signal) => {
805
+ const prevState = w.state;
806
+ if (signal) {
807
+ w.writeLog("warn", "thread.exit.signal", { signal, code });
808
+ } else if (code === 0 || code === null) {
809
+ w.writeLog("info", "thread.exit.clean", { code });
810
+ } else {
811
+ w.writeLog("error", "thread.exit.crash", { code });
812
+ }
813
+ setState(w, "shutdown");
814
+ clearInterval(w.heartbeatTimer);
815
+ w.pid = 0;
816
+ if (prevState === APP_INSTANCE_STATE.RUN && !isShuttingDown()) {
817
+ restartThreadApp(w).catch((err) => w.eventBus.emit("error", err));
818
+ }
819
+ });
820
+ w.eventBus.on("message", (msg) => {
821
+ w.writeLog("debug", "parent.ipc.recv", msg);
822
+ switch (msg.type) {
823
+ case "app-state": {
824
+ setState(w, msg.state);
825
+ break;
826
+ }
827
+ case "ready": {
828
+ w.pid = msg.pid;
829
+ w.eventBus.emit("pid", msg.pid);
830
+ setState(w, "ready");
831
+ w.writeLog("debug", "thread.spawn.ready");
832
+ w.eventBus.emit("ready");
833
+ break;
834
+ }
835
+ case "pong": {
836
+ w.lastPong = Date.now();
837
+ break;
838
+ }
839
+ }
840
+ });
841
+ spawnChild(w);
842
+ return w;
843
+ }
844
+ function withLifecycle(w, phase, fn) {
845
+ w.writeLog("debug", `lifecycle.${phase}.begin`);
846
+ return fn().then(
847
+ (result) => {
848
+ w.writeLog("debug", `lifecycle.${phase}.done`);
849
+ return result;
850
+ },
851
+ (error) => {
852
+ w.writeLog("error", `lifecycle.${phase}.fail`, {
853
+ message: String(error && error.message || error)
854
+ });
855
+ throw error;
856
+ }
857
+ );
858
+ }
859
+ function setupThreadApp(w) {
860
+ return w.scheduler.queueJobWait(
861
+ () => withLifecycle(w, "setupThreadApp", () => doSetupThread(w)),
862
+ { jobName: "setupThreadApp" }
863
+ );
864
+ }
865
+ function doSetupThread(w) {
866
+ setState(w, APP_INSTANCE_STATE.IN_SETUP);
867
+ return sendAndWait(
868
+ w,
869
+ createThreadMessage("setup", {
870
+ props: w.threadProps
871
+ }),
872
+ "setup_done"
873
+ ).then(noop);
874
+ }
875
+ function startThreadApp(w) {
876
+ return w.scheduler.queueJobWait(
877
+ () => withLifecycle(w, "startThreadApp", () => doStartThreadApp(w)),
878
+ { jobName: "startThreadApp" }
879
+ );
880
+ }
881
+ function doStartThreadApp(w) {
882
+ setState(w, APP_INSTANCE_STATE.IN_RUN);
883
+ return sendAndWait(w, createThreadMessage("start", {}), "start_done").then(
884
+ () => {
885
+ if (w.state === APP_INSTANCE_STATE.RUN) {
886
+ startHeartbeat(w);
887
+ }
888
+ }
889
+ );
890
+ }
891
+ function stopThreadApp(w) {
892
+ return w.scheduler.queueJobWait(
893
+ () => withLifecycle(w, "stopThreadApp", () => doStopThreadApp(w)),
894
+ { jobName: "stopThreadApp" }
895
+ );
896
+ }
897
+ function doStopThreadApp(w) {
898
+ setState(w, APP_INSTANCE_STATE.IN_STOP);
899
+ clearInterval(w.heartbeatTimer);
900
+ return sendAndWait(w, createThreadMessage("stop", {}), "stop_done").then(
901
+ noop
902
+ );
903
+ }
904
+ function shutdownThreadApp(w) {
905
+ return w.scheduler.queueJobWait(
906
+ () => withLifecycle(w, "shutdownThreadApp", () => doShutdownThreadApp(w)),
907
+ { jobName: "shutdownThreadApp" }
908
+ );
909
+ }
910
+ function doShutdownThreadApp(w) {
911
+ setState(w, "shutdown");
912
+ clearInterval(w.heartbeatTimer);
913
+ return (w.child.connected ? sendAndWait(
914
+ w,
915
+ createThreadMessage("shutdown", {}),
916
+ "shutdown_done",
917
+ SHUTDOWN_REPLY_TIMEOUT_MS
918
+ ) : Promise.resolve()).then(() => waitForExit(w, SHUTDOWN_EXIT_TIMEOUT_MS));
919
+ }
920
+ function restartThreadApp(w) {
921
+ if (w.restartCount >= MAX_RESTARTS) {
922
+ return Promise.reject(
923
+ new Error(`Thread ${w.threadId} exceeded max restarts (${MAX_RESTARTS})`)
924
+ );
925
+ }
926
+ return w.scheduler.queueJobWait(
927
+ () => withLifecycle(
928
+ w,
929
+ "restartThreadApp",
930
+ () => doShutdownThreadApp(w).then(() => {
931
+ w.restartCount++;
932
+ spawnChild(w);
933
+ return waitForThreadReady(w);
934
+ }).then(() => doSetupThread(w)).then(() => doStartThreadApp(w))
935
+ ),
936
+ { jobName: "restartThreadApp" }
937
+ );
938
+ }
939
+ function isThreadMessage(value) {
940
+ return isObject(value) && !!value.vrun_app_thread_message;
941
+ }
942
+ function createThreadMessage(type, msg) {
943
+ return {
944
+ type,
945
+ ...msg,
946
+ vrun_app_thread_message: true
947
+ };
948
+ }
949
+ function parseRawLogLine(line) {
950
+ const trimmed = line.trimEnd();
951
+ const lower = trimmed.toLowerCase();
952
+ let level = "info";
953
+ if (lower.includes("error") || lower.includes("\u2716") || lower.includes("[error]")) {
954
+ level = "error";
955
+ } else if (lower.includes("warn") || lower.includes("\u26A0")) {
956
+ level = "warn";
957
+ } else if (lower.includes("debug") || lower.includes("\u2699")) {
958
+ level = "debug";
959
+ }
960
+ return { ts: Date.now(), level, text: trimmed };
961
+ }
962
+ function isThreadExit(w) {
963
+ return w.child.exitCode !== null || w.child.signalCode !== null || !w.child.connected;
964
+ }
965
+
966
+ function createAppThread(definition) {
967
+ const scriptFile = definition.filePath;
968
+ assert.ok(
969
+ scriptFile,
970
+ `Failed to detect definition file path for: ${definition.name}`
971
+ );
972
+ const app = defineApp({
973
+ name: definition.name,
974
+ description: definition.description,
975
+ props: {
976
+ ...definition.props || {},
977
+ threads: {
978
+ type: Number,
979
+ description: "Amount of threads",
980
+ default: () => 1,
981
+ parser: (v) => parseInt(v)
982
+ }
983
+ },
984
+ logger: false,
985
+ setup(props) {
986
+ const { threads = 1, ...threadProps } = props;
987
+ assert.greaterThan(threads, 0, "threads expected to be greater than 0");
988
+ const managedThreads = Array.from(
989
+ { length: threads },
990
+ (_, i) => initThread(i + 1, scriptFile, threadProps)
991
+ );
992
+ if (props.__inheritIO !== false) {
993
+ for (const w of managedThreads) {
994
+ const childLogger = createAppLogger({
995
+ ...definition,
996
+ name: `${definition.name}.${w.threadId}`
997
+ });
998
+ w.eventBus.on("log", (entry) => {
999
+ childLogger[entry.level](entry.text);
1000
+ });
1001
+ }
1002
+ }
1003
+ return Promise.all(managedThreads.map((w) => waitForThreadReady(w))).then(() => Promise.all(managedThreads.map((w) => setupThreadApp(w)))).then(() => ({ managedThreads }));
1004
+ },
1005
+ entry() {
1006
+ return Promise.all(this.managedThreads.map((w) => startThreadApp(w))).then(
1007
+ noop
1008
+ );
1009
+ },
1010
+ stop() {
1011
+ return Promise.all(this.managedThreads.map((w) => stopThreadApp(w))).then(
1012
+ noop
1013
+ );
1014
+ },
1015
+ shutdown() {
1016
+ return Promise.all(
1017
+ this.managedThreads.map((w) => shutdownThreadApp(w))
1018
+ ).then(() => {
1019
+ this.managedThreads = [];
1020
+ });
1021
+ }
1022
+ });
1023
+ return app;
1024
+ }
1025
+ function createAppThreadInstance({
1026
+ definition,
1027
+ parentPort,
1028
+ threadId,
1029
+ onShutdown = noop
1030
+ }) {
1031
+ definition = { ...definition };
1032
+ definition.name += "." + threadId;
1033
+ const instance = createAppInstance(definition);
1034
+ const log = instance.logger;
1035
+ const postMessage = (message, cb) => {
1036
+ log.debug(formatLogEvent("child.ipc.send", message));
1037
+ parentPort.postMessage(message, cb);
1038
+ };
1039
+ instance.eventBus.on("state", (newState) => {
1040
+ postMessage(createThreadMessage("app-state", { state: newState }));
1041
+ });
1042
+ parentPort.on("message", async (message) => {
1043
+ if (!isThreadMessage(message)) return;
1044
+ log.debug(formatLogEvent("child.ipc.recv", message));
1045
+ switch (message.type) {
1046
+ case "setup": {
1047
+ const result = await setupApp(instance, message.props);
1048
+ postMessage(createThreadMessage("setup_done", { result }));
1049
+ break;
1050
+ }
1051
+ case "start": {
1052
+ const result = await runApp$1(instance);
1053
+ postMessage(createThreadMessage("start_done", { result }));
1054
+ break;
1055
+ }
1056
+ case "stop": {
1057
+ const result = await stopApp(instance);
1058
+ postMessage(createThreadMessage("stop_done", { result }));
1059
+ break;
1060
+ }
1061
+ case "shutdown": {
1062
+ const result = await shutdownApp(instance);
1063
+ postMessage(createThreadMessage("shutdown_done", { result }), () => {
1064
+ setImmediate(() => {
1065
+ catchError(onShutdown);
1066
+ if (isFunction(parentPort.close)) {
1067
+ parentPort.close();
1068
+ }
1069
+ });
1070
+ });
1071
+ break;
1072
+ }
1073
+ case "ping": {
1074
+ postMessage(createThreadMessage("pong", {}));
1075
+ break;
1076
+ }
1077
+ }
1078
+ });
1079
+ postMessage(
1080
+ createThreadMessage("ready", {
1081
+ pid: process.pid
1082
+ })
1083
+ );
1084
+ return instance;
1085
+ }
1086
+
1087
+ const TYPESCRIPT_EXTENSIONS = ["mts", "cts", "ts"];
1088
+ const JAVASCRIPT_EXTENSIONS = ["js", "mjs", "cjs"];
1089
+ const ALL_EXTENSIONS = [
1090
+ ...TYPESCRIPT_EXTENSIONS,
1091
+ ...JAVASCRIPT_EXTENSIONS,
1092
+ "json"
1093
+ ];
1094
+ function createProject(projectPath) {
1095
+ const packageJsonPath = locateNearestFile(projectPath, "package.json");
1096
+ assert.ok(
1097
+ packageJsonPath,
1098
+ "package.json not found in path: " + path.dirname(projectPath)
1099
+ );
1100
+ const packageJson = importJson(packageJsonPath);
1101
+ const projectLocation = path.dirname(packageJsonPath);
1102
+ const project = {
1103
+ path: projectLocation,
1104
+ name: packageJson.name || path.basename(projectPath),
1105
+ description: packageJson.description || "",
1106
+ version: packageJson.version || "0.0.0",
1107
+ tsconfigPath: locateNearestFile(projectLocation, "tsconfig.json"),
1108
+ dotenvPath: locateNearestFile(projectLocation, ".env"),
1109
+ autoImports: ["inversify.config"],
1110
+ importLookupPaths: createImportLookupPaths(projectLocation),
1111
+ allowTs: false,
1112
+ packageJson
1113
+ };
1114
+ project.autoImports = project.autoImports.map((v) => projectResolveFile(project, v)).filter(isString);
1115
+ return project;
1116
+ }
1117
+ function projectImportFile(project, filePath) {
1118
+ const existedFile = projectResolveFile(project, filePath);
1119
+ if (!existedFile) {
1120
+ throw new Error(
1121
+ `Cannot resolve import for path.
1122
+ Path:
1123
+ ${filePath}
1124
+ Available extensions: ${ALL_EXTENSIONS.join(", ")}`
1125
+ );
1126
+ }
1127
+ const ext = getFileExtension(existedFile, false);
1128
+ if (ext === "json") {
1129
+ const data = importJson(existedFile);
1130
+ return Promise.resolve({ default: data });
1131
+ }
1132
+ return import(existedFile);
1133
+ }
1134
+ function projectResolveFile(project, filePath) {
1135
+ const pathVariants = project.importLookupPaths.map(
1136
+ (dir) => path.resolve(dir, filePath)
1137
+ );
1138
+ for (const filePath2 of pathVariants) {
1139
+ const pathVariants2 = ALL_EXTENSIONS.includes(
1140
+ getFileExtension(filePath2, false)
1141
+ ) ? [filePath2] : [...ALL_EXTENSIONS.map((ext) => `${filePath2}.${ext}`)];
1142
+ const existedPath = pathVariants2.find((v) => fs.existsSync(v));
1143
+ if (existedPath) {
1144
+ return existedPath;
1145
+ }
1146
+ }
1147
+ return null;
1148
+ }
1149
+ async function projectLoadEnv(project) {
1150
+ if (!project.dotenvPath) {
1151
+ return false;
1152
+ }
1153
+ dotenv.config({
1154
+ path: project.dotenvPath,
1155
+ quiet: true
1156
+ });
1157
+ return true;
1158
+ }
1159
+ async function projectAutoImport(project) {
1160
+ const result = [];
1161
+ const filePaths = project.autoImports.map(
1162
+ (filePath) => projectResolveFile(project, filePath)
1163
+ );
1164
+ for (const filePath of filePaths) {
1165
+ if (filePath) {
1166
+ await projectImportFile(project, filePath);
1167
+ result.push(filePath.replace(project.path, ""));
1168
+ }
1169
+ }
1170
+ return result;
1171
+ }
1172
+ function projectPrintInfo(project) {
1173
+ const tags = [];
1174
+ if (project.allowTs) {
1175
+ tags.push(getColor("bgBlue")(" TS "));
1176
+ }
1177
+ if (CONFIG.WATCH_MODE) {
1178
+ tags.push(getColor("bgMagenta")(" WATCH "));
1179
+ }
1180
+ if (project.dotenvPath) {
1181
+ tags.push(getColor("bgYellow")(` ${path.basename(project.dotenvPath)} `));
1182
+ }
1183
+ if (project.autoImports.length) {
1184
+ tags.push(
1185
+ getColor("bgGreen")(
1186
+ ` IMPORT: ${project.autoImports.map((v) => path.relative(project.path, v)).join(" | ")} `
1187
+ )
1188
+ );
1189
+ }
1190
+ log.info(
1191
+ "%s (%s)%s",
1192
+ project.name,
1193
+ project.version,
1194
+ tags.length ? ` ${tags.join(" ")}` : ""
1195
+ );
1196
+ }
1197
+ function createImportLookupPaths(projectPath, appRoot = CONFIG.APP_ROOT) {
1198
+ const cwd = process.cwd();
1199
+ const result = [];
1200
+ if (appRoot) {
1201
+ result.push(path.resolve(cwd, appRoot));
1202
+ }
1203
+ result.push(path.resolve(cwd));
1204
+ result.push(path.resolve(projectPath));
1205
+ return uniq(result);
1206
+ }
1207
+ function locateNearestFile(location, fileName) {
1208
+ let currentDir = path.extname(location) ? path.dirname(path.resolve(location)) : path.resolve(location);
1209
+ while (currentDir) {
1210
+ const filePath = path.join(currentDir, fileName);
1211
+ if (fs.existsSync(filePath)) {
1212
+ return filePath;
1213
+ }
1214
+ const parentDir = path.dirname(currentDir);
1215
+ if (parentDir === currentDir) break;
1216
+ currentDir = parentDir;
1217
+ }
1218
+ return null;
1219
+ }
1220
+ function importJson(filePath) {
1221
+ if (!filePath.endsWith(".json")) {
1222
+ filePath += ".json";
1223
+ }
1224
+ if (!fs.existsSync(filePath)) {
1225
+ throw new Error(`JSON file not found.
1226
+ Path:
1227
+ ${filePath}`);
1228
+ }
1229
+ const data = fs.readFileSync(filePath, { encoding: "utf-8" });
1230
+ return JSON.parse(data);
1231
+ }
1232
+
1233
+ const LOG_BUFFER_CAP = 2e3;
1234
+ class RingBuffer {
1235
+ constructor(cap) {
1236
+ this.cap = cap;
1237
+ }
1238
+ buf = [];
1239
+ start = 0;
1240
+ push(item) {
1241
+ if (this.buf.length < this.cap) {
1242
+ this.buf.push(item);
1243
+ } else {
1244
+ this.buf[this.start] = item;
1245
+ this.start = (this.start + 1) % this.cap;
1246
+ }
1247
+ }
1248
+ size() {
1249
+ return this.buf.length;
1250
+ }
1251
+ toArray() {
1252
+ if (this.start === 0) return this.buf.slice();
1253
+ return this.buf.slice(this.start).concat(this.buf.slice(0, this.start));
1254
+ }
1255
+ clear() {
1256
+ this.buf = [];
1257
+ this.start = 0;
1258
+ }
1259
+ }
1260
+ const noopHandlers = {
1261
+ stop: () => Promise.resolve(),
1262
+ start: () => Promise.resolve(),
1263
+ restart: () => Promise.resolve()
1264
+ };
1265
+ function createTuiStore() {
1266
+ const emitter = new EventEmitter();
1267
+ emitter.setMaxListeners(0);
1268
+ const logs = /* @__PURE__ */ new Map();
1269
+ const apps = [];
1270
+ const bufferOf = (id) => {
1271
+ let buf = logs.get(id);
1272
+ if (!buf) {
1273
+ buf = new RingBuffer(LOG_BUFFER_CAP);
1274
+ logs.set(id, buf);
1275
+ }
1276
+ return buf;
1277
+ };
1278
+ const notify = () => {
1279
+ emitter.emit("change");
1280
+ };
1281
+ const isSelectedFeed = (nodeId) => {
1282
+ if (!store.selectedId) return false;
1283
+ if (store.selectedId === nodeId) return true;
1284
+ for (const app of apps) {
1285
+ if (app.id !== store.selectedId) continue;
1286
+ return !!app.threads?.some((t) => t.id === nodeId);
1287
+ }
1288
+ return false;
1289
+ };
1290
+ const store = {
1291
+ apps,
1292
+ logs,
1293
+ system: null,
1294
+ selectedId: null,
1295
+ filter: "info",
1296
+ logScroll: 0,
1297
+ handlers: noopHandlers,
1298
+ pushLog(nodeId, entry) {
1299
+ bufferOf(nodeId).push(entry);
1300
+ if (store.logScroll > 0 && isSelectedFeed(nodeId) && passesFilter(entry, store.filter)) {
1301
+ store.logScroll += 1;
1302
+ }
1303
+ notify();
1304
+ },
1305
+ setState(nodeId, state) {
1306
+ for (const app of apps) {
1307
+ if (app.id === nodeId) {
1308
+ app.state = state;
1309
+ notify();
1310
+ return;
1311
+ }
1312
+ if (app.threads) {
1313
+ for (const t of app.threads) {
1314
+ if (t.id === nodeId) {
1315
+ t.state = state;
1316
+ notify();
1317
+ return;
1318
+ }
1319
+ }
1320
+ }
1321
+ }
1322
+ },
1323
+ setSelected(nodeId) {
1324
+ store.selectedId = nodeId;
1325
+ store.logScroll = 0;
1326
+ notify();
1327
+ },
1328
+ setFilter(filter) {
1329
+ store.filter = filter;
1330
+ store.logScroll = 0;
1331
+ notify();
1332
+ },
1333
+ setLogScroll(offset) {
1334
+ store.logScroll = Math.max(0, offset);
1335
+ notify();
1336
+ },
1337
+ setSystem(entry) {
1338
+ store.system = entry;
1339
+ notify();
1340
+ },
1341
+ toggleExpand(appId) {
1342
+ for (const app of apps) {
1343
+ if (app.id === appId) {
1344
+ app.expanded = !app.expanded;
1345
+ notify();
1346
+ return;
1347
+ }
1348
+ }
1349
+ },
1350
+ addApp(node) {
1351
+ apps.push(node);
1352
+ if (!store.selectedId) store.selectedId = node.id;
1353
+ notify();
1354
+ },
1355
+ setThreads(appId, threads) {
1356
+ for (const app of apps) {
1357
+ if (app.id === appId) {
1358
+ app.threads = threads;
1359
+ notify();
1360
+ return;
1361
+ }
1362
+ }
1363
+ },
1364
+ setPid(nodeId, pid) {
1365
+ for (const app of apps) {
1366
+ if (app.threads) {
1367
+ for (const t of app.threads) {
1368
+ if (t.id === nodeId) {
1369
+ t.pid = pid;
1370
+ notify();
1371
+ return;
1372
+ }
1373
+ }
1374
+ }
1375
+ }
1376
+ },
1377
+ getLogs(nodeId) {
1378
+ const buf = logs.get(nodeId);
1379
+ return buf ? buf.toArray() : [];
1380
+ },
1381
+ clearLogs(nodeId) {
1382
+ logs.get(nodeId)?.clear();
1383
+ notify();
1384
+ },
1385
+ subscribe(fn) {
1386
+ emitter.on("change", fn);
1387
+ return () => emitter.off("change", fn);
1388
+ }
1389
+ };
1390
+ return store;
1391
+ }
1392
+ const LEVEL_RANK = {
1393
+ debug: 0,
1394
+ log: 1,
1395
+ info: 2,
1396
+ warn: 3,
1397
+ error: 4
1398
+ };
1399
+ const FILTER_THRESHOLD = {
1400
+ all: 0,
1401
+ info: LEVEL_RANK.info,
1402
+ warn: LEVEL_RANK.warn,
1403
+ error: LEVEL_RANK.error
1404
+ };
1405
+ function passesFilter(entry, filter) {
1406
+ return LEVEL_RANK[entry.level] >= FILTER_THRESHOLD[filter];
1407
+ }
1408
+ function getSelectedFilteredCount(store) {
1409
+ if (!store.selectedId) return 0;
1410
+ const passes = (e) => passesFilter(e, store.filter);
1411
+ for (const app of store.apps) {
1412
+ if (app.id !== store.selectedId) continue;
1413
+ if (!app.threads || app.threads.length === 0) {
1414
+ return store.getLogs(app.id).filter(passes).length;
1415
+ }
1416
+ let count = 0;
1417
+ for (const t of app.threads) {
1418
+ for (const e of store.getLogs(t.id)) if (passes(e)) count += 1;
1419
+ }
1420
+ return count;
1421
+ }
1422
+ return store.getLogs(store.selectedId).filter(passes).length;
1423
+ }
1424
+ function nextFilter(filter) {
1425
+ switch (filter) {
1426
+ case "all":
1427
+ return "info";
1428
+ case "info":
1429
+ return "warn";
1430
+ case "warn":
1431
+ return "error";
1432
+ case "error":
1433
+ return "all";
1434
+ }
1435
+ }
1436
+ function flattenVisibleTree(apps) {
1437
+ const out = [];
1438
+ for (const app of apps) {
1439
+ out.push(app);
1440
+ if (app.expanded && app.threads) {
1441
+ for (const t of app.threads) out.push(t);
1442
+ }
1443
+ }
1444
+ return out;
1445
+ }
1446
+
1447
+ const TuiStoreContext = createContext(null);
1448
+ function useTuiStore() {
1449
+ const store = useContext(TuiStoreContext);
1450
+ if (!store) throw new Error("TuiStoreContext is not provided");
1451
+ return store;
1452
+ }
1453
+ function useStoreSnapshot() {
1454
+ const store = useTuiStore();
1455
+ const [version, setVersion] = useState(0);
1456
+ useEffect(() => {
1457
+ return store.subscribe(() => setVersion((v) => v + 1));
1458
+ }, [store]);
1459
+ return version;
1460
+ }
1461
+
1462
+ const STATE_COLORS = {
1463
+ "in-run": "yellow",
1464
+ run: "green",
1465
+ "in-stop": "yellow",
1466
+ "in-setup": "yellow",
1467
+ setup: "yellow",
1468
+ ready: "gray",
1469
+ "in-shutdown": "yellow",
1470
+ init: "gray",
1471
+ shutdown: "gray",
1472
+ stop: "gray",
1473
+ error: "red"
1474
+ };
1475
+ function isApp(n) {
1476
+ return n.kind === "app";
1477
+ }
1478
+ function rowLength(n) {
1479
+ if (n.kind === "app") return 1 + 1 + 1 + 1 + 1 + (n.name?.length ?? 0);
1480
+ return 1 + 2 + 1 + 1 + `PID ${n.pid}`.length;
1481
+ }
1482
+ function AppList() {
1483
+ useStoreSnapshot();
1484
+ const store = useTuiStore();
1485
+ const nodes = flattenVisibleTree(store.apps);
1486
+ const maxRow = nodes.reduce(
1487
+ (m, n) => Math.max(m, rowLength(n)),
1488
+ "Apps".length
1489
+ );
1490
+ const width = maxRow + 4;
1491
+ return /* @__PURE__ */ jsxs(
1492
+ Box,
1493
+ {
1494
+ flexDirection: "column",
1495
+ width,
1496
+ flexShrink: 0,
1497
+ borderStyle: "single",
1498
+ paddingX: 1,
1499
+ overflow: "hidden",
1500
+ children: [
1501
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Apps" }),
1502
+ nodes.map((node) => {
1503
+ const selected = node.id === store.selectedId;
1504
+ const color = STATE_COLORS[node.state];
1505
+ const marker = selected ? "\u258C" : " ";
1506
+ if (isApp(node)) {
1507
+ const caret = node.threads && node.threads.length ? node.expanded ? "\u25BE" : "\u25B8" : " ";
1508
+ return /* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", bold: selected, children: [
1509
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: marker }),
1510
+ caret,
1511
+ " ",
1512
+ /* @__PURE__ */ jsx(Text, { color, children: "\u25CF" }),
1513
+ " ",
1514
+ node.name
1515
+ ] }, node.id);
1516
+ }
1517
+ return /* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", bold: selected, children: [
1518
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: marker }),
1519
+ " ",
1520
+ /* @__PURE__ */ jsx(Text, { color, children: "\u25CF" }),
1521
+ " ",
1522
+ node.name
1523
+ ] }, node.id);
1524
+ })
1525
+ ]
1526
+ }
1527
+ );
1528
+ }
1529
+
1530
+ const LEVEL_COLORS = {
1531
+ debug: "gray",
1532
+ log: "white",
1533
+ info: "cyan",
1534
+ warn: "yellow",
1535
+ error: "red"
1536
+ };
1537
+ function formatTime(ts) {
1538
+ const d = new Date(ts);
1539
+ const pad = (n) => n.toString().padStart(2, "0");
1540
+ return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
1541
+ }
1542
+ function describeSelection(store) {
1543
+ if (!store.selectedId) return null;
1544
+ for (const app of store.apps) {
1545
+ if (app.id === store.selectedId) {
1546
+ const threads = app.threads ?? [];
1547
+ const pids = Array.from(
1548
+ new Set(threads.map((t) => t.pid).filter((p) => p > 0))
1549
+ );
1550
+ return {
1551
+ appName: app.name,
1552
+ processCount: threads.length,
1553
+ pids,
1554
+ states: threads.map((v) => v.state),
1555
+ showProcessTag: threads.length > 1
1556
+ };
1557
+ }
1558
+ if (app.threads) {
1559
+ for (const t of app.threads) {
1560
+ if (t.id === store.selectedId) {
1561
+ return {
1562
+ appName: app.name,
1563
+ pid: t.pid,
1564
+ pids: t.pid > 0 ? [t.pid] : [],
1565
+ state: t.state,
1566
+ states: [t.state],
1567
+ processCount: app.threads.length,
1568
+ showProcessTag: false
1569
+ };
1570
+ }
1571
+ }
1572
+ }
1573
+ }
1574
+ return null;
1575
+ }
1576
+ function formatPids(pids) {
1577
+ if (pids.length === 0) return "?";
1578
+ if (pids.length === 1) return String(pids[0]);
1579
+ return pids.join(", ");
1580
+ }
1581
+ function gatherEntries(store, info) {
1582
+ if (!store.selectedId) return [];
1583
+ for (const app of store.apps) {
1584
+ if (app.id !== store.selectedId) continue;
1585
+ if (!app.threads || app.threads.length === 0) {
1586
+ return store.getLogs(app.id);
1587
+ }
1588
+ const merged = [];
1589
+ for (const t of app.threads) {
1590
+ for (const entry of store.getLogs(t.id)) {
1591
+ merged.push(
1592
+ info.showProcessTag ? { ...entry, text: `[${t.pid}] ${entry.text}` } : entry
1593
+ );
1594
+ }
1595
+ }
1596
+ merged.sort((a, b) => a.ts - b.ts);
1597
+ return merged;
1598
+ }
1599
+ return store.getLogs(store.selectedId);
1600
+ }
1601
+ function LogView() {
1602
+ useStoreSnapshot();
1603
+ const store = useTuiStore();
1604
+ const { stdout } = useStdout();
1605
+ const rows = stdout?.rows ?? 24;
1606
+ const visibleRows = Math.max(1, rows - 5);
1607
+ const info = describeSelection(store);
1608
+ let entries = [];
1609
+ let scrollLabel = "";
1610
+ if (info) {
1611
+ const all = gatherEntries(store, info);
1612
+ const filtered = all.filter((e) => passesFilter(e, store.filter));
1613
+ const total = filtered.length;
1614
+ const maxOffset = Math.max(0, total - visibleRows);
1615
+ const offset = Math.min(store.logScroll, maxOffset);
1616
+ const end = total - offset;
1617
+ const start = Math.max(0, end - visibleRows);
1618
+ entries = filtered.slice(start, end);
1619
+ if (offset > 0) scrollLabel = ` \xB7 scrolled +${offset}`;
1620
+ }
1621
+ return /* @__PURE__ */ jsxs(
1622
+ Box,
1623
+ {
1624
+ flexDirection: "column",
1625
+ flexGrow: 1,
1626
+ flexShrink: 1,
1627
+ borderStyle: "single",
1628
+ paddingX: 1,
1629
+ overflow: "hidden",
1630
+ children: [
1631
+ /* @__PURE__ */ jsx(Box, { flexShrink: 0, flexDirection: "column", children: info ? /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
1632
+ /* @__PURE__ */ jsxs(Text, { bold: true, wrap: "truncate-end", children: [
1633
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: info.appName }),
1634
+ info.processCount > 1 && info.pid == null ? /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
1635
+ " (",
1636
+ info.processCount,
1637
+ " processes)"
1638
+ ] }) : null,
1639
+ /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
1640
+ " ",
1641
+ "\xB7 filter: ",
1642
+ store.filter,
1643
+ scrollLabel
1644
+ ] })
1645
+ ] }),
1646
+ /* @__PURE__ */ jsxs(Text, { bold: true, wrap: "truncate-end", children: [
1647
+ /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
1648
+ "STATE ",
1649
+ info.states.join(", ")
1650
+ ] }),
1651
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: " \xB7 " }),
1652
+ /* @__PURE__ */ jsxs(Text, { color: "magenta", children: [
1653
+ "PID ",
1654
+ formatPids(info.pids)
1655
+ ] })
1656
+ ] })
1657
+ ] }) : /* @__PURE__ */ jsx(Text, { bold: true, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: " (no selection)" }) }) }),
1658
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: entries.map((entry, idx) => /* @__PURE__ */ jsxs(Text, { wrap: "wrap", children: [
1659
+ /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
1660
+ formatTime(entry.ts),
1661
+ " "
1662
+ ] }),
1663
+ /* @__PURE__ */ jsx(Text, { color: LEVEL_COLORS[entry.level], children: entry.level.toUpperCase().padEnd(5) }),
1664
+ " ",
1665
+ entry.text
1666
+ ] }, idx)) })
1667
+ ]
1668
+ }
1669
+ );
1670
+ }
1671
+
1672
+ function StatusBar() {
1673
+ useStoreSnapshot();
1674
+ const store = useTuiStore();
1675
+ const sys = store.system;
1676
+ return /* @__PURE__ */ jsxs(
1677
+ Box,
1678
+ {
1679
+ flexDirection: "row",
1680
+ justifyContent: "space-between",
1681
+ paddingX: 1,
1682
+ flexShrink: 0,
1683
+ height: 1,
1684
+ children: [
1685
+ /* @__PURE__ */ jsx(Box, { flexShrink: 1, overflow: "hidden", children: /* @__PURE__ */ jsx(Text, { wrap: "truncate-end", children: sys ? /* @__PURE__ */ jsxs(Fragment, { children: [
1686
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "[system] " }),
1687
+ /* @__PURE__ */ jsx(Text, { color: sys.level === "error" ? "red" : "white", children: sys.text })
1688
+ ] }) : /* @__PURE__ */ jsx(Text, { color: "gray", children: "[system] ready" }) }) }),
1689
+ /* @__PURE__ */ jsx(Box, { flexShrink: 0, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: "\u2191/\u2193 \xB7 \u2192/\u2190 \xB7 PgUp/PgDn \xB7 s \xB7 \u21E7S \xB7 r \xB7 f \xB7 q" }) })
1690
+ ]
1691
+ }
1692
+ );
1693
+ }
1694
+
1695
+ function findNodeAndApp(store, id) {
1696
+ for (const app of store.apps) {
1697
+ if (app.id === id) return { node: app, app };
1698
+ if (app.threads) {
1699
+ for (const t of app.threads) {
1700
+ if (t.id === id) return { node: t, app };
1701
+ }
1702
+ }
1703
+ }
1704
+ return null;
1705
+ }
1706
+ function InnerTui({ store, onExit }) {
1707
+ useStoreSnapshot();
1708
+ const { stdout } = useStdout();
1709
+ const rows = stdout?.rows ?? 24;
1710
+ useInput((input, key) => {
1711
+ const visible = flattenVisibleTree(store.apps);
1712
+ if (visible.length === 0) return;
1713
+ const currentIdx = visible.findIndex((n) => n.id === store.selectedId);
1714
+ const idx = currentIdx < 0 ? 0 : currentIdx;
1715
+ const selected = visible[idx];
1716
+ const visibleLogRows = Math.max(1, rows - 5);
1717
+ const halfPage = Math.max(1, Math.floor(visibleLogRows / 2));
1718
+ if (key.upArrow && key.shift) {
1719
+ const maxOffset = Math.max(
1720
+ 0,
1721
+ getSelectedFilteredCount(store) - visibleLogRows
1722
+ );
1723
+ store.setLogScroll(Math.min(maxOffset, store.logScroll + halfPage));
1724
+ return;
1725
+ }
1726
+ if (key.downArrow && key.shift) {
1727
+ store.setLogScroll(Math.max(0, store.logScroll - halfPage));
1728
+ return;
1729
+ }
1730
+ if (key.home) {
1731
+ const total = getSelectedFilteredCount(store);
1732
+ store.setLogScroll(Math.max(0, total - visibleLogRows));
1733
+ return;
1734
+ }
1735
+ if (key.end) {
1736
+ store.setLogScroll(0);
1737
+ return;
1738
+ }
1739
+ if (key.upArrow) {
1740
+ const next = visible[Math.max(0, idx - 1)];
1741
+ if (next) store.setSelected(next.id);
1742
+ return;
1743
+ }
1744
+ if (key.downArrow) {
1745
+ const next = visible[Math.min(visible.length - 1, idx + 1)];
1746
+ if (next) store.setSelected(next.id);
1747
+ return;
1748
+ }
1749
+ if (key.rightArrow) {
1750
+ if (selected.kind === "app" && selected.threads && selected.threads.length) {
1751
+ if (!selected.expanded) store.toggleExpand(selected.id);
1752
+ }
1753
+ return;
1754
+ }
1755
+ if (key.leftArrow) {
1756
+ if (selected.kind === "app" && selected.expanded) {
1757
+ store.toggleExpand(selected.id);
1758
+ } else if (selected.kind === "thread") {
1759
+ store.setSelected(selected.parentId);
1760
+ }
1761
+ return;
1762
+ }
1763
+ if (input === "f") {
1764
+ store.setFilter(nextFilter(store.filter));
1765
+ return;
1766
+ }
1767
+ if (input === "s") {
1768
+ const target = findNodeAndApp(store, selected.id);
1769
+ if (target) void store.handlers.stop(target.node.id);
1770
+ return;
1771
+ }
1772
+ if (input === "S") {
1773
+ const target = findNodeAndApp(store, selected.id);
1774
+ if (target) void store.handlers.start(target.node.id);
1775
+ return;
1776
+ }
1777
+ if (input === "r") {
1778
+ const target = findNodeAndApp(store, selected.id);
1779
+ if (target) void store.handlers.restart(target.node.id);
1780
+ return;
1781
+ }
1782
+ if (input === "q" || key.ctrl && input === "c") {
1783
+ void onExit();
1784
+ return;
1785
+ }
1786
+ });
1787
+ const columns = stdout?.columns ?? 80;
1788
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: columns, height: rows, children: [
1789
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", flexGrow: 1, width: columns, children: [
1790
+ /* @__PURE__ */ jsx(AppList, {}),
1791
+ /* @__PURE__ */ jsx(LogView, {})
1792
+ ] }),
1793
+ /* @__PURE__ */ jsx(StatusBar, {})
1794
+ ] });
1795
+ }
1796
+ function TuiRoot(props) {
1797
+ return /* @__PURE__ */ jsx(TuiStoreContext.Provider, { value: props.store, children: /* @__PURE__ */ jsx(InnerTui, { ...props }) });
1798
+ }
1799
+
1800
+ function getAppProps(definition, props, isSingle) {
1801
+ if (isSingle) return { ...props };
1802
+ return getPrefixedProps(definition, props);
1803
+ }
1804
+ function appIdFor(definition, idx) {
1805
+ return `${definition.name}#${idx}`;
1806
+ }
1807
+ function resolveThreadCount(appProps) {
1808
+ if (isNumber(appProps.threads) && appProps.threads > 0) {
1809
+ return appProps.threads;
1810
+ }
1811
+ return 1;
1812
+ }
1813
+ const APP_STATE_RANK = {
1814
+ error: 10,
1815
+ run: 9,
1816
+ "in-run": 8,
1817
+ setup: 7,
1818
+ "in-setup": 6,
1819
+ stop: 5,
1820
+ "in-stop": 4,
1821
+ shutdown: 3,
1822
+ "in-shutdown": 2,
1823
+ ready: 1,
1824
+ init: 0
1825
+ };
1826
+ function aggregateAppState(threads) {
1827
+ let best = "init";
1828
+ for (const w of threads) {
1829
+ const s = APP_STATE_RANK[w.state];
1830
+ if (s > APP_STATE_RANK[best]) best = w.state;
1831
+ }
1832
+ return best;
1833
+ }
1834
+ function launchAppsTui(definitions, props) {
1835
+ const store = createTuiStore();
1836
+ const records = [];
1837
+ const isSingle = definitions.length === 1;
1838
+ if (props.watch) {
1839
+ store.setSystem({
1840
+ ts: Date.now(),
1841
+ level: "warn",
1842
+ text: "TUI does not yet support --watch mode;"
1843
+ });
1844
+ }
1845
+ for (let i = 0; i < definitions.length; i++) {
1846
+ const def = definitions[i];
1847
+ const appId = appIdFor(def, i);
1848
+ const appProps = getAppProps(def, props, isSingle);
1849
+ const threadCount = resolveThreadCount(appProps);
1850
+ appProps.__inheritIO = false;
1851
+ delete appProps.threads;
1852
+ const appNode = {
1853
+ kind: "app",
1854
+ id: appId,
1855
+ name: def.name,
1856
+ state: APP_INSTANCE_STATE.INIT,
1857
+ expanded: false,
1858
+ threads: []
1859
+ };
1860
+ store.addApp(appNode);
1861
+ const threads = [];
1862
+ const threadNodes = [];
1863
+ for (let threadId = 1; threadId <= threadCount; threadId++) {
1864
+ const nodeId = `${appId}:${threadId}`;
1865
+ const threadNode = {
1866
+ id: `${appId}:${threadId}`,
1867
+ kind: "thread",
1868
+ name: `${appNode.name}.${threadId}`,
1869
+ parentId: appId,
1870
+ threadId,
1871
+ pid: 0,
1872
+ state: APP_INSTANCE_STATE.INIT
1873
+ };
1874
+ threadNodes.push(threadNode);
1875
+ const w = initThread(threadId, def.filePath, appProps);
1876
+ w.eventBus.on("log", (entry) => {
1877
+ store.pushLog(nodeId, {
1878
+ ts: entry.ts,
1879
+ level: entry.level,
1880
+ text: entry.text
1881
+ });
1882
+ });
1883
+ w.eventBus.on("state", (newState) => {
1884
+ store.setState(nodeId, newState);
1885
+ store.setState(appId, aggregateAppState(threads));
1886
+ });
1887
+ w.eventBus.on("pid", (pid) => {
1888
+ store.setPid(nodeId, pid);
1889
+ });
1890
+ w.eventBus.on("error", (err) => {
1891
+ store.pushLog(nodeId, {
1892
+ ts: Date.now(),
1893
+ level: "error",
1894
+ text: `Thread error: ${err.message}`
1895
+ });
1896
+ });
1897
+ threads.push(w);
1898
+ }
1899
+ store.setThreads(appId, threadNodes);
1900
+ records.push({ appId, node: appNode, threads });
1901
+ }
1902
+ const findRecordByAppId = (id) => records.find((r) => r.appId === id);
1903
+ const findThread = (id) => {
1904
+ for (const r of records) {
1905
+ for (const w of r.threads) {
1906
+ if (`${r.appId}:${w.threadId}` === id) return { record: r, thread: w };
1907
+ }
1908
+ }
1909
+ return void 0;
1910
+ };
1911
+ const startSingleThread = (w) => {
1912
+ if (w.state === APP_INSTANCE_STATE.IN_RUN || APP_INSTANCE_STATE.RUN) {
1913
+ return Promise.resolve();
1914
+ }
1915
+ if (w.state === APP_INSTANCE_STATE.STOP) return startThreadApp(w);
1916
+ return setupThreadApp(w).then(() => startThreadApp(w));
1917
+ };
1918
+ store.handlers = {
1919
+ stop(id) {
1920
+ const r = findRecordByAppId(id);
1921
+ if (r)
1922
+ return Promise.all(r.threads.map((w) => stopThreadApp(w))).then(() => {
1923
+ });
1924
+ const found = findThread(id);
1925
+ if (found) return stopThreadApp(found.thread);
1926
+ return Promise.resolve();
1927
+ },
1928
+ start(id) {
1929
+ const r = findRecordByAppId(id);
1930
+ if (r) {
1931
+ return Promise.all(r.threads.map(startSingleThread)).then(() => {
1932
+ });
1933
+ }
1934
+ const found = findThread(id);
1935
+ if (found) return startSingleThread(found.thread);
1936
+ return Promise.resolve();
1937
+ },
1938
+ restart(id) {
1939
+ const r = findRecordByAppId(id);
1940
+ if (r) {
1941
+ return Promise.all(
1942
+ r.threads.map((w) => {
1943
+ w.restartCount = 0;
1944
+ return restartThreadApp(w);
1945
+ })
1946
+ ).then(() => {
1947
+ });
1948
+ }
1949
+ const found = findThread(id);
1950
+ if (found) {
1951
+ found.thread.restartCount = 0;
1952
+ return restartThreadApp(found.thread);
1953
+ }
1954
+ return Promise.resolve();
1955
+ }
1956
+ };
1957
+ const startRecord = (r) => Promise.all(
1958
+ r.threads.map(
1959
+ (w) => waitForThreadReady(w).then(() => setupThreadApp(w)).then(() => startThreadApp(w))
1960
+ )
1961
+ ).then(noop);
1962
+ onShutdownError((error) => {
1963
+ console.error("[Graceful Shutdown] error:", error);
1964
+ });
1965
+ const q = defer();
1966
+ onShutdown("app", () => {
1967
+ store.setSystem({
1968
+ ts: Date.now(),
1969
+ level: "warn",
1970
+ text: "shutdown initiated"
1971
+ });
1972
+ return Promise.all(
1973
+ records.map((r) => Promise.all(r.threads.map((w) => shutdownThreadApp(w))))
1974
+ ).then(() => {
1975
+ store.setSystem({
1976
+ ts: Date.now(),
1977
+ level: "info",
1978
+ text: "shutdown complete"
1979
+ });
1980
+ }).then(() => inkInstance.waitUntilRenderFlush()).then(() => inkInstance.unmount()).then(() => q.resolve()).catch(console.error);
1981
+ });
1982
+ const onExit = () => {
1983
+ return Promise.resolve().then(() => processGraceful());
1984
+ };
1985
+ const inkInstance = render(/* @__PURE__ */ jsx(TuiRoot, { store, onExit }), {
1986
+ exitOnCtrlC: false
1987
+ });
1988
+ return Promise.all(records.map(startRecord)).then(() => q.promise);
1989
+ }
1990
+
1991
+ const DB_PATH = path.join(os.tmpdir(), "vrun.json");
1992
+ class TempDB {
1993
+ constructor(filepath) {
1994
+ this.filepath = filepath;
1995
+ }
1996
+ _storage = /* @__PURE__ */ new Map();
1997
+ _dirty = false;
1998
+ get isDirty() {
1999
+ return this._dirty;
2000
+ }
2001
+ get(key) {
2002
+ return this._storage.get(key);
2003
+ }
2004
+ set(key, value) {
2005
+ this._storage.set(key, value);
2006
+ this._dirty = true;
2007
+ }
2008
+ save() {
2009
+ if (!this._dirty) return;
2010
+ const data = EJSON.stringify(Array.from(this._storage.entries()));
2011
+ fs.writeFileSync(this.filepath, data);
2012
+ this._dirty = false;
2013
+ }
2014
+ load() {
2015
+ try {
2016
+ const data = EJSON.parse(fs.readFileSync(this.filepath, "utf-8"));
2017
+ for (const [key, value] of data) {
2018
+ this._storage.set(key, value);
2019
+ }
2020
+ return true;
2021
+ } catch (_) {
2022
+ return false;
2023
+ }
2024
+ }
2025
+ }
2026
+ const db = new TempDB(DB_PATH);
2027
+ db.load();
2028
+
2029
+ const TYPE_TO_PLACEHOLDER = /* @__PURE__ */ new Map([
2030
+ [String, "<string>"],
2031
+ [Boolean, "<boolean>"],
2032
+ [Number, "<number>"],
2033
+ [Date, "<date>"],
2034
+ [Array, "<array>"]
2035
+ ]);
2036
+ function propsToOptions(props) {
2037
+ const result = [];
2038
+ for (const [
2039
+ name,
2040
+ {
2041
+ description,
2042
+ type,
2043
+ default: defaultValue,
2044
+ required,
2045
+ enum: variants,
2046
+ alias,
2047
+ env,
2048
+ parser
2049
+ }
2050
+ ] of Object.entries(props)) {
2051
+ const flagName = kebabCase(name);
2052
+ const typePlaceholder = TYPE_TO_PLACEHOLDER.get(type);
2053
+ const envVariables = arrayable(env);
2054
+ const option = new Option(
2055
+ [alias ? `-${alias}, ` : "", `--${flagName}`, typePlaceholder].filter(Boolean).join(" "),
2056
+ description
2057
+ );
2058
+ if (variants?.length) {
2059
+ option.choices(variants);
2060
+ }
2061
+ if (envVariables.length) {
2062
+ option.env(envVariables.join(", "));
2063
+ option.default(getEnv(envVariables) || defaultValue?.());
2064
+ } else if (defaultValue) {
2065
+ option.default(defaultValue());
2066
+ }
2067
+ if (required) {
2068
+ option.mandatory = true;
2069
+ }
2070
+ option.attributeName();
2071
+ switch (type) {
2072
+ case Boolean: {
2073
+ option.isBoolean();
2074
+ break;
2075
+ }
2076
+ case Number: {
2077
+ option.argParser(parseNumberArgument);
2078
+ }
2079
+ }
2080
+ if (parser) {
2081
+ option.argParser(parser);
2082
+ }
2083
+ result.push(option);
2084
+ }
2085
+ return result;
2086
+ }
2087
+ function getEnv(variables) {
2088
+ for (const env of variables) {
2089
+ const value = process.env[env];
2090
+ if (value) {
2091
+ return value;
2092
+ }
2093
+ }
2094
+ }
2095
+ function parseNumberArgument(value) {
2096
+ const parsedValue = parseInt(value, 10);
2097
+ if (!isNumber(parsedValue)) {
2098
+ throw new InvalidArgumentError("Not a number.");
2099
+ }
2100
+ return parsedValue;
2101
+ }
2102
+
2103
+ function runApp({
2104
+ scriptFile,
2105
+ cliName = "app",
2106
+ cliDescription = "Application cli",
2107
+ argv = process.argv
2108
+ }) {
2109
+ const promise = scriptFile ? loadScriptCommand(scriptFile) : Promise.resolve(buildRootCommand({ cliName, cliDescription }));
2110
+ return promise.then((program) => program.parseAsync(["node", cliName, ...argv])).then(noop).catch((error) => {
2111
+ log.error("Failed to run app:", error);
2112
+ process.exit(1);
2113
+ });
2114
+ }
2115
+ function buildRootCommand({
2116
+ cliDescription,
2117
+ cliName
2118
+ }) {
2119
+ const program = new Command().name(cliName).description(cliDescription).enablePositionalOptions().passThroughOptions().helpOption(false).helpCommand(false).action(function() {
2120
+ const cmd = this;
2121
+ if (isHelpArgument(cmd.args)) {
2122
+ return cmd.help();
2123
+ }
2124
+ });
2125
+ program.command("start", { isDefault: true }).argument("<string...>", "Path to script file").description("Start application.").addOption(new Option("--watch", "Watch file changes")).addOption(new Option("--dev", "Allow to import ts files")).passThroughOptions(true).allowUnknownOption(true).allowExcessArguments(true).action(function(scriptFiles, _opts) {
2126
+ assert.arrayStrings(scriptFiles, "scriptFiles expected string[]");
2127
+ const cmd = this;
2128
+ const remainingArgs = cmd.args;
2129
+ const flagIdx = scriptFiles.findIndex((v) => v.startsWith("-"));
2130
+ scriptFiles = (flagIdx > -1 ? scriptFiles.slice(0, flagIdx) : scriptFiles).filter((v) => !v.startsWith("-"));
2131
+ if (!scriptFiles.length) {
2132
+ return cmd.parent?.args?.includes("start") ? cmd.help() : program.help();
2133
+ }
2134
+ const project = createProject(path.resolve(scriptFiles[0]));
2135
+ return Promise.resolve().then(() => registerTsx(project)).then(() => loadProject(project)).then(
2136
+ () => Promise.all(scriptFiles.map((f) => loadAppDefinition(project, f)))
2137
+ ).then((apps) => buildStartCommand(apps)).then((appCmd) => {
2138
+ if (isHelpArgument(remainingArgs)) {
2139
+ return appCmd.help();
2140
+ }
2141
+ return appCmd.parseAsync(remainingArgs, { from: "user" });
2142
+ }).then(noop);
2143
+ });
2144
+ program.command("json").description(
2145
+ "Start apps from a JSON file containing an array of script paths."
2146
+ ).argument("[string]", "Path to JSON file").allowUnknownOption(true).allowExcessArguments(true).action(function(jsonFile) {
2147
+ const cmd = this;
2148
+ if (!jsonFile || isHelpArgument(jsonFile)) {
2149
+ return cmd.help();
2150
+ }
2151
+ const project = createProject(path.dirname(path.resolve(jsonFile)));
2152
+ const remainingArgs = cmd.args;
2153
+ return Promise.resolve().then(() => registerTsx(project)).then(() => loadProject(project)).then(
2154
+ () => projectImportFile(
2155
+ project,
2156
+ path.resolve(jsonFile)
2157
+ )
2158
+ ).then((m) => {
2159
+ const paths = m.default;
2160
+ assert.arrayStrings(paths, "JSON file must contain a string[].");
2161
+ return paths.map(
2162
+ (p) => path.resolve(path.dirname(path.resolve(jsonFile)), p)
2163
+ );
2164
+ }).then(
2165
+ (scriptFiles) => Promise.all(scriptFiles.map((f) => loadAppDefinition(project, f)))
2166
+ ).then((apps) => buildStartCommand(apps)).then((appCmd) => {
2167
+ if (isHelpArgument(remainingArgs)) {
2168
+ return appCmd.help();
2169
+ }
2170
+ return appCmd.parseAsync(remainingArgs, { from: "user" });
2171
+ }).then(noop);
2172
+ });
2173
+ program.command("list").description("Interactively pick apps to run from a folder.").argument("[string]", "Path to folder", ".").addOption(
2174
+ new Option("--pattern <glob>", "Glob pattern for app files").default(
2175
+ "**/*.{app,worker}.{ts,js}"
2176
+ )
2177
+ ).allowUnknownOption(true).allowExcessArguments(true).action(function(folderPath, opts) {
2178
+ const cmd = this;
2179
+ if (isHelpArgument(cmd.args)) {
2180
+ return cmd.help();
2181
+ }
2182
+ const remainingArgs = cmd.args;
2183
+ const project = createProject(path.resolve(folderPath));
2184
+ return Promise.resolve().then(() => registerTsx(project)).then(() => loadProject(project)).then(() => findAppFiles(path.resolve(folderPath), opts.pattern)).then((scriptFiles) => {
2185
+ if (!scriptFiles.length) {
2186
+ log.error("No files found. Pattern: %s", opts.pattern);
2187
+ process.exit(1);
2188
+ }
2189
+ return log.prompt("Pick apps to run.", {
2190
+ type: "multiselect",
2191
+ options: scriptFiles.map((f) => ({
2192
+ label: path.basename(f),
2193
+ value: f
2194
+ })),
2195
+ initial: db.get("app_last_run"),
2196
+ cancel: "null"
2197
+ });
2198
+ }).then((selected) => {
2199
+ const selectedFiles = selected || [];
2200
+ if (!selectedFiles.length) process.exit(0);
2201
+ db.set("app_last_run", selectedFiles);
2202
+ db.save();
2203
+ return Promise.all(
2204
+ selectedFiles.map((f) => loadAppDefinition(project, f))
2205
+ );
2206
+ }).then((apps) => buildStartCommand(apps)).then((appCmd) => {
2207
+ if (isHelpArgument(remainingArgs)) {
2208
+ return appCmd.help();
2209
+ }
2210
+ return appCmd.parseAsync(remainingArgs, { from: "user" });
2211
+ }).then(noop);
2212
+ });
2213
+ program.command("folder").description("Start all apps found in a folder.").argument("<string>", "Path to folder").addOption(
2214
+ new Option("--pattern <glob>", "Glob pattern for app files").default(
2215
+ "**/*.{ts,js}"
2216
+ )
2217
+ ).allowUnknownOption(true).allowExcessArguments(true).action(function(folderPath, opts) {
2218
+ const cmd = this;
2219
+ if (!folderPath || isHelpArgument(folderPath)) {
2220
+ return cmd.help();
2221
+ }
2222
+ const remainingArgs = cmd.args;
2223
+ const project = createProject(path.resolve(folderPath));
2224
+ return Promise.resolve().then(() => registerTsx(project)).then(() => loadProject(project)).then(() => findAppFiles(path.resolve(folderPath), opts.pattern)).then((scriptFiles) => {
2225
+ if (!scriptFiles.length) {
2226
+ log.error("No files found. Pattern: %s", opts.pattern);
2227
+ process.exit(1);
2228
+ }
2229
+ return Promise.all(
2230
+ scriptFiles.map((f) => loadAppDefinition(project, f))
2231
+ );
2232
+ }).then((apps) => {
2233
+ if (!apps.length) {
2234
+ log.error("No valid AppDefinitions found in folder.");
2235
+ process.exit(1);
2236
+ }
2237
+ return apps;
2238
+ }).then((apps) => buildStartCommand(apps)).then((appCmd) => {
2239
+ if (isHelpArgument(remainingArgs)) {
2240
+ return appCmd.help();
2241
+ }
2242
+ return appCmd.parseAsync(remainingArgs, { from: "user" });
2243
+ }).then(noop);
2244
+ });
2245
+ return program;
2246
+ }
2247
+ function findAppFiles(folderPath, pattern) {
2248
+ return fs.globSync(pattern, {
2249
+ cwd: folderPath,
2250
+ exclude: (f) => f.includes("node_modules")
2251
+ }).map((f) => path.resolve(folderPath, f)).sort();
2252
+ }
2253
+ function loadScriptCommand(scriptFile) {
2254
+ const project = createProject(path.dirname(scriptFile));
2255
+ return Promise.resolve().then(() => registerTsx(project)).then(() => loadProject(project)).then(() => loadAppDefinition(project, scriptFile)).then((app) => buildStartCommand([app]));
2256
+ }
2257
+ function registerTsx(project) {
2258
+ if (CONFIG.TS_MODE === "tsx") {
2259
+ project.allowTs = true;
2260
+ return Promise.resolve();
2261
+ }
2262
+ if (CONFIG.TS_MODE === "tsx-register") {
2263
+ return Promise.resolve().then(() => import('tsx/esm/api')).then((m) => m.register({ tsconfig: project.tsconfigPath || false })).then(() => {
2264
+ project.allowTs = true;
2265
+ });
2266
+ }
2267
+ return Promise.resolve();
2268
+ }
2269
+ function loadProject(project) {
2270
+ projectPrintInfo(project);
2271
+ return Promise.resolve().then(() => projectLoadEnv(project)).then(() => projectAutoImport(project)).then(noop);
2272
+ }
2273
+ function loadAppDefinition(project, scriptFile) {
2274
+ return projectImportFile(project, scriptFile).then((appModule) => {
2275
+ const app = appModule.default;
2276
+ if (!isAppDefinition(app)) {
2277
+ log.warn(`Default export must be a app definition: ${scriptFile}`);
2278
+ process.exit(1);
2279
+ }
2280
+ return app;
2281
+ });
2282
+ }
2283
+ function buildStartCommand(definitions) {
2284
+ const rootDefinition = definitions.length > 1 ? createAppHub(definitions.map((v) => createAppThread(v))) : createAppThread(definitions[0]);
2285
+ const program = new Command().name(rootDefinition.name).description(rootDefinition.description || "An application");
2286
+ if (rootDefinition.props) {
2287
+ for (const option of propsToOptions(rootDefinition.props)) {
2288
+ program.addOption(option);
2289
+ }
2290
+ }
2291
+ program.addOption(new Option("--watch", "Watch file changes")).addOption(new Option("--dev", "Allow to import ts files")).addOption(new Option("--tui", "Launch interactive terminal UI")).helpCommand(false).command("start", { isDefault: true, hidden: true }).allowUnknownOption(true).allowExcessArguments(true).action((...args) => {
2292
+ const props = program.optsWithGlobals();
2293
+ if (props.tui) {
2294
+ return launchAppsTui(definitions, props);
2295
+ }
2296
+ return launchApp(
2297
+ definitions.length > 1 || props.threads > 1 ? rootDefinition : definitions[0],
2298
+ props
2299
+ );
2300
+ });
2301
+ return program;
2302
+ }
2303
+ function launchApp(definition, props) {
2304
+ const startDefer = defer();
2305
+ let app;
2306
+ onShutdown("app", () => {
2307
+ log.warn("Graceful shutdown initiated, waiting for process to complete");
2308
+ if (CONFIG.TS_MODE === "tsx") {
2309
+ log.warn("Graceful in tsx + watch mode is not supported.");
2310
+ }
2311
+ return startDefer.promise.then(() => {
2312
+ if (app) {
2313
+ return shutdownApp(app);
2314
+ }
2315
+ });
2316
+ });
2317
+ return startApp(definition, props).then((startResult) => {
2318
+ if (isSkip(startResult)) {
2319
+ startDefer.resolve();
2320
+ log.warn("Failed to start %s", startResult, startResult.error);
2321
+ return;
2322
+ }
2323
+ app = startResult.app;
2324
+ startDefer.resolve();
2325
+ return appWaitShutdown(app);
2326
+ }).then(() => processGraceful());
2327
+ }
2328
+
2329
+ const cli = Object.freeze({
2330
+ runApp
2331
+ });
2332
+
2333
+ const index = {
2334
+ __proto__: null,
2335
+ cli: cli
2336
+ };
2337
+
2338
+ class IntervalStrategy {
2339
+ constructor(options) {
2340
+ this.options = options;
2341
+ }
2342
+ timerSequence = 0;
2343
+ timer = null;
2344
+ worker;
2345
+ doSetup({ worker }) {
2346
+ this.worker = worker;
2347
+ }
2348
+ startSignal() {
2349
+ this.timer = setInterval(() => {
2350
+ if (this.worker.isIdle) {
2351
+ this.worker.addTask(this.createTask());
2352
+ } else {
2353
+ log.warn(
2354
+ "[%s] Worker busy, skipping tick",
2355
+ this.worker.definition.name
2356
+ );
2357
+ }
2358
+ }, this.options.intervalSeconds * 1e3);
2359
+ }
2360
+ stopSignal(done) {
2361
+ clearInterval(this.timer);
2362
+ this.timer = null;
2363
+ done();
2364
+ }
2365
+ doShutdown() {
2366
+ }
2367
+ createTask() {
2368
+ return { timerSequence: ++this.timerSequence };
2369
+ }
2370
+ }
2371
+
2372
+ const WORKER_DEF = Symbol("worker-definition");
2373
+ function createWorkerPool(size) {
2374
+ return new ResourcePool({
2375
+ poolSize: size,
2376
+ auto: true,
2377
+ createResource: () => Symbol("worker-slot")
2378
+ });
2379
+ }
2380
+ function createWorkerInstance(definition, logger) {
2381
+ const instance = {
2382
+ definition,
2383
+ log: logger,
2384
+ queue: new AsyncIterableQueue(),
2385
+ pool: createWorkerPool(definition.taskParallel ?? 2),
2386
+ runTask: null,
2387
+ queueMaxSize: definition.taskLimit ?? 50,
2388
+ taskParallel: definition.taskParallel ?? 2,
2389
+ overloadedSignaled: false,
2390
+ addTask(ctx) {
2391
+ addWorkerTask(this, ctx);
2392
+ },
2393
+ get isOverloaded() {
2394
+ return this.queueSize > this.queueMaxSize * 0.8;
2395
+ },
2396
+ get isIdle() {
2397
+ return this.pool.isIdle && this.queueSize === 0;
2398
+ },
2399
+ get queueSize() {
2400
+ return this.queue._queue.items.length;
2401
+ }
2402
+ };
2403
+ return instance;
2404
+ }
2405
+ function addWorkerTask(instance, ctx) {
2406
+ instance.queue.put(ctx);
2407
+ checkWorkerPressure(instance);
2408
+ }
2409
+ async function executeWorkerTask(instance, entryContext, props, abortSignal) {
2410
+ const { executeStrategy, entry } = instance.definition;
2411
+ if (isFunction(executeStrategy.executeSignal)) {
2412
+ const [signalError, signalResult] = await catchError(
2413
+ () => executeStrategy.executeSignal(entryContext)
2414
+ );
2415
+ if (signalError) {
2416
+ instance.log.error("Strategy executeSignal error, dropping task", {
2417
+ error: signalError
2418
+ });
2419
+ return;
2420
+ }
2421
+ if (isSkip(signalResult)) {
2422
+ instance.log.warn(
2423
+ "Strategy executeSignal returned skip, dropping task",
2424
+ signalResult
2425
+ );
2426
+ return;
2427
+ }
2428
+ }
2429
+ let [executeError, executeResult] = await catchError(
2430
+ () => entry?.call(entryContext, props)
2431
+ );
2432
+ if (executeError) {
2433
+ if (executeStrategy.handleEntryError) {
2434
+ executeResult = executeStrategy.handleEntryError(executeError);
2435
+ }
2436
+ if (!executeResult) {
2437
+ executeResult = {
2438
+ skip: true,
2439
+ error: true,
2440
+ code: "critical_error",
2441
+ reason: executeError.message,
2442
+ stack: executeError.stack
2443
+ };
2444
+ }
2445
+ } else if (!executeResult || Array.isArray(executeResult) && !executeResult.length) {
2446
+ executeResult = { success: true, code: "void" };
2447
+ }
2448
+ const results = Array.isArray(executeResult) ? executeResult : [executeResult];
2449
+ let hasError = false;
2450
+ let hasSkip = false;
2451
+ for (const result of results) {
2452
+ hasError = hasError || isSkip(result) && !!result.error;
2453
+ hasSkip = isSkip(result);
2454
+ }
2455
+ if (hasError) {
2456
+ instance.log.error("Task error", results);
2457
+ } else if (hasSkip) {
2458
+ instance.log.warn("Task skipped", results);
2459
+ } else {
2460
+ instance.log.debug("Task completed", results);
2461
+ }
2462
+ if (executeStrategy.completeSignal) {
2463
+ const [completeError] = await catchError(
2464
+ () => executeStrategy.completeSignal(entryContext, executeResult)
2465
+ );
2466
+ if (completeError) {
2467
+ instance.log.error("Strategy completeSignal error", {
2468
+ error: completeError
2469
+ });
2470
+ }
2471
+ }
2472
+ }
2473
+ function defineWorker(definition) {
2474
+ const result = defineApp({
2475
+ filePath: filePathFromStack(captureStackTrace(defineWorker)),
2476
+ ...definition,
2477
+ setup(props) {
2478
+ return setupWorker(this, definition, props);
2479
+ },
2480
+ entry(props) {
2481
+ return runWorkerLoop(this, definition, props);
2482
+ },
2483
+ stop(props) {
2484
+ return stopWorker(this, definition, props);
2485
+ },
2486
+ shutdown(props) {
2487
+ return shutdownWorker(this, definition, props);
2488
+ }
2489
+ });
2490
+ result.disabled = result.disabled || CONFIG.WORKER_DISABLED.has(result.name);
2491
+ def(result, WORKER_DEF, true);
2492
+ return result;
2493
+ }
2494
+ function setupWorker(setupContext, definition, props) {
2495
+ const setupFn = definition.setup;
2496
+ const ctx = {
2497
+ ...setupContext,
2498
+ worker: createWorkerInstance(definition, setupContext.log)
2499
+ };
2500
+ return Promise.resolve().then(() => setupFn ? setupFn.call(ctx, props) : void 0).then((setupResult) => {
2501
+ return {
2502
+ ...ctx,
2503
+ ...setupResult || {}
2504
+ };
2505
+ });
2506
+ }
2507
+ async function runWorkerLoop(runtimeContext, definition, props) {
2508
+ const worker = runtimeContext.worker;
2509
+ const { executeStrategy } = definition;
2510
+ await executeStrategy.doSetup({ worker });
2511
+ const abortController = new AbortController();
2512
+ abortController.signal;
2513
+ const onStopSignal = () => {
2514
+ return Promise.resolve().then(() => abortController.abort()).then(
2515
+ () => catchError(
2516
+ () => executeStrategy.stopSignal(() => worker.queue.close())
2517
+ )
2518
+ ).then((stopResult) => {
2519
+ const stopError = stopResult[0];
2520
+ if (stopError) {
2521
+ worker.queue.close();
2522
+ worker.log.error("Strategy stopSignal error", { error: stopError });
2523
+ }
2524
+ });
2525
+ };
2526
+ worker.runTask = new CancellablePromise(
2527
+ async (resolve, _reject, onCancel) => {
2528
+ onCancel(onStopSignal);
2529
+ const [startError] = await catchError(
2530
+ () => executeStrategy.startSignal()
2531
+ );
2532
+ if (startError) {
2533
+ worker.log.error("Strategy startSignal error", { error: startError });
2534
+ worker.runTask = null;
2535
+ return resolve();
2536
+ }
2537
+ worker.log.info("Worker started (parallel=%d)", worker.taskParallel);
2538
+ for await (const taskContext of worker.queue) {
2539
+ const jobTicket = await worker.pool.acquire();
2540
+ const entryContext = {
2541
+ ...runtimeContext,
2542
+ ...taskContext
2543
+ };
2544
+ executeWorkerTask(worker, entryContext, props).finally(
2545
+ () => {
2546
+ worker.pool.release(jobTicket);
2547
+ checkWorkerPressure(worker);
2548
+ }
2549
+ );
2550
+ }
2551
+ if (worker.pool.usedCount > 0) {
2552
+ worker.log.info(
2553
+ "Draining task pool (active=%d)",
2554
+ worker.pool.usedCount
2555
+ );
2556
+ }
2557
+ await worker.pool.destroy();
2558
+ worker.queue = new AsyncIterableQueue();
2559
+ worker.runTask = null;
2560
+ resolve();
2561
+ }
2562
+ );
2563
+ worker.runTask.catch((err) => {
2564
+ worker.log.error("Worker crash: %s", { error: toError(err) });
2565
+ });
2566
+ }
2567
+ function checkWorkerPressure(worker) {
2568
+ try {
2569
+ if (worker.isOverloaded) {
2570
+ if (!worker.definition.executeStrategy.overloadedSignal)
2571
+ return void worker.log.warn(
2572
+ "Worker under pressure (queue=%d, limit=%d)",
2573
+ worker.queueSize,
2574
+ worker.queueMaxSize
2575
+ );
2576
+ if (worker.overloadedSignaled) return;
2577
+ worker.overloadedSignaled = true;
2578
+ worker.definition.executeStrategy.overloadedSignal();
2579
+ } else {
2580
+ if (!worker.overloadedSignaled) return;
2581
+ if (!worker.definition.executeStrategy.availableSignal) {
2582
+ worker.log.warn("Worker capacity restored");
2583
+ } else {
2584
+ worker.definition.executeStrategy.availableSignal();
2585
+ }
2586
+ worker.overloadedSignaled = false;
2587
+ }
2588
+ } catch (err) {
2589
+ worker.log.error("Backpressure signal error", { error: err });
2590
+ }
2591
+ }
2592
+ async function stopWorker(runtimeContext, definition, props) {
2593
+ const worker = runtimeContext.worker;
2594
+ const userStop = definition.stop;
2595
+ if (worker.runTask) {
2596
+ if (isFunction(userStop)) {
2597
+ await userStop.call(runtimeContext, props);
2598
+ }
2599
+ if (!worker.runTask) return;
2600
+ worker.runTask.cancel();
2601
+ await worker.runTask.catch(() => {
2602
+ });
2603
+ worker.runTask = null;
2604
+ }
2605
+ }
2606
+ async function shutdownWorker(runtimeContext, definition, props) {
2607
+ const worker = runtimeContext.worker;
2608
+ const userShutdown = definition.shutdown;
2609
+ if (isFunction(userShutdown)) {
2610
+ await userShutdown.call(runtimeContext, props);
2611
+ }
2612
+ await definition.executeStrategy.doShutdown();
2613
+ worker.queue = new AsyncIterableQueue();
2614
+ worker.pool = createWorkerPool(definition.taskParallel ?? 2);
2615
+ }
2616
+ function isWorkerDefinition(value) {
2617
+ return value?.[WORKER_DEF] === true;
2618
+ }
2619
+ function isWorkerInstance(value) {
2620
+ return isWorkerDefinition(value?.definition);
2621
+ }
2622
+
2623
+ export { APP_INSTANCE_STATE, IntervalStrategy, addWorkerTask, appWaitShutdown, cli, createAppHub, createAppInstance, createAppThread, createAppThreadInstance, createThreadMessage, createWorkerInstance, defineApp, defineWorker, getPrefixedProps, initThread, isAppDefinition, isThreadExit, isThreadMessage, isWorkerDefinition, isWorkerInstance, prefixifyProps, restartThreadApp, runApp$1 as runApp, setupApp, setupThreadApp, shutdownApp, shutdownThreadApp, startApp, startThreadApp, stopApp, stopThreadApp, waitForThreadReady };
2624
+ //# sourceMappingURL=index.mjs.map