@gobing-ai/ts-infra 0.3.20 → 0.3.22
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/README.md +29 -0
- package/dist/application-cli.d.ts +59 -0
- package/dist/application-cli.d.ts.map +1 -0
- package/dist/application-cli.js +68 -0
- package/dist/event-bus/event-bus.d.ts.map +1 -1
- package/dist/event-bus/event-bus.js +2 -0
- package/package.json +10 -5
- package/src/application-cli.ts +92 -0
- package/src/event-bus/event-bus.ts +2 -0
package/README.md
CHANGED
|
@@ -614,6 +614,35 @@ Validation errors include the config file path and section name for diagnostics.
|
|
|
614
614
|
`@gobing-ai/ts-infra` import stays portable and adapter-light. A future ADR
|
|
615
615
|
may decide whether type-only re-exports are acceptable.
|
|
616
616
|
|
|
617
|
+
#### CLI convenience bootstrap
|
|
618
|
+
|
|
619
|
+
For fire-and-forget CLIs (Commander, Yargs, etc.), `@gobing-ai/ts-infra/application-cli`
|
|
620
|
+
wraps `runNodeApplication` with the one thing every CLI does at the end: map the
|
|
621
|
+
command result to a process exit code, and terminate. Same option shape, same
|
|
622
|
+
defaults, same callback contract — no surprises.
|
|
623
|
+
|
|
624
|
+
```ts
|
|
625
|
+
import { runCliApplication } from '@gobing-ai/ts-infra/application-cli';
|
|
626
|
+
import { Command } from 'commander';
|
|
627
|
+
|
|
628
|
+
await runCliApplication({
|
|
629
|
+
async start(app) {
|
|
630
|
+
const program = new Command().name('mycli');
|
|
631
|
+
// ...register commands...
|
|
632
|
+
program.parse(); // Commander sets exit code on error
|
|
633
|
+
return process.exitCode; // forward to runCliApplication
|
|
634
|
+
},
|
|
635
|
+
});
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
Differences from `runNodeApplication`:
|
|
639
|
+
- `start` returns `number | void` — the number becomes the exit code (void = 0)
|
|
640
|
+
- On success, calls `app.stop('shutdown')` then `process.exit(code)`
|
|
641
|
+
- On error, writes the message to stderr and exits 1 (after graceful teardown)
|
|
642
|
+
|
|
643
|
+
Use this for CLIs. For long-running services (servers, daemons, workers), use
|
|
644
|
+
`runNodeApplication` directly — it returns a runtime handle you control.
|
|
645
|
+
|
|
617
646
|
## Usage
|
|
618
647
|
|
|
619
648
|
### Install
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI convenience bootstrap for `@gobing-ai/ts-infra`.
|
|
3
|
+
*
|
|
4
|
+
* Wraps {@link runNodeApplication} with the one thing every CLI does at the
|
|
5
|
+
* end: map the command result to a process exit code, and terminate. Same
|
|
6
|
+
* option shape, same defaults, same callback contract — no surprises.
|
|
7
|
+
*
|
|
8
|
+
* Differences from `runNodeApplication`:
|
|
9
|
+
* - `start` returns `number | void` instead of `void`. The number becomes the
|
|
10
|
+
* process exit code (void = 0). This is the Commander convention.
|
|
11
|
+
* - After `start` resolves, the runtime is shut down gracefully
|
|
12
|
+
* (`app.stop('shutdown')`) and the process terminates via `exitProcess(code)`.
|
|
13
|
+
* - If `start` throws (or startup fails), the error message is written to
|
|
14
|
+
* stderr and the process exits with code 1. `runNodeApplication`'s built-in
|
|
15
|
+
* reverse-order cleanup (telemetry, owned DB, user `stop` callback) runs
|
|
16
|
+
* before the rethrow, so resources are released.
|
|
17
|
+
*
|
|
18
|
+
* Use this for fire-and-forget CLIs (Commander, Yargs, etc.). For long-running
|
|
19
|
+
* services (servers, daemons, workers), use `runNodeApplication` directly — it
|
|
20
|
+
* returns a runtime handle you control.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* import { runCliApplication } from '@gobing-ai/ts-infra/application-cli';
|
|
25
|
+
*
|
|
26
|
+
* await runCliApplication({
|
|
27
|
+
* async start(app) {
|
|
28
|
+
* app.logger.info('running');
|
|
29
|
+
* return doWork(); // returns exit code
|
|
30
|
+
* },
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
import type { ApplicationRuntime, ApplicationStopReason, EventMap, InfraEvents } from './application/types';
|
|
35
|
+
import { type NodeApplicationOptions } from './application-node';
|
|
36
|
+
/** Options for the CLI convenience {@link runCliApplication}. */
|
|
37
|
+
export interface CliApplicationOptions<TAppConfig = unknown, TEvents extends EventMap = InfraEvents> extends Omit<NodeApplicationOptions<TAppConfig, TEvents>, 'start' | 'stop'> {
|
|
38
|
+
/**
|
|
39
|
+
* User callback: CLI logic. Called after all services are ready.
|
|
40
|
+
* Return a number to set the process exit code; void/undefined means success (0).
|
|
41
|
+
*/
|
|
42
|
+
readonly start: (app: ApplicationRuntime<TAppConfig, TEvents>) => Promise<number | undefined> | number | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* User callback: cleanup before services shut down. Receives the stop
|
|
45
|
+
* reason: `'shutdown'` on normal completion, `'error'` if `start` threw.
|
|
46
|
+
*/
|
|
47
|
+
readonly stop?: (app: ApplicationRuntime<TAppConfig, TEvents>, reason: ApplicationStopReason) => Promise<void> | void;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* CLI convenience application bootstrap.
|
|
51
|
+
*
|
|
52
|
+
* Delegates to {@link runNodeApplication} for all service lifecycle (logger,
|
|
53
|
+
* telemetry, events, DB, scheduler, plugins), then maps the `start` result to
|
|
54
|
+
* a process exit code and terminates. See the module docstring for details.
|
|
55
|
+
*
|
|
56
|
+
* @returns Never resolves — terminates the process via `exitProcess` internally.
|
|
57
|
+
*/
|
|
58
|
+
export declare function runCliApplication<TAppConfig = unknown, TEvents extends EventMap = InfraEvents>(options: CliApplicationOptions<TAppConfig, TEvents>): Promise<never>;
|
|
59
|
+
//# sourceMappingURL=application-cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"application-cli.d.ts","sourceRoot":"","sources":["../src/application-cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAC5G,OAAO,EAAE,KAAK,sBAAsB,EAAsB,MAAM,oBAAoB,CAAC;AAErF,iEAAiE;AACjE,MAAM,WAAW,qBAAqB,CAAC,UAAU,GAAG,OAAO,EAAE,OAAO,SAAS,QAAQ,GAAG,WAAW,CAC/F,SAAQ,IAAI,CAAC,sBAAsB,CAAC,UAAU,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC3E;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,kBAAkB,CAAC,UAAU,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,GAAG,MAAM,GAAG,SAAS,CAAC;IACnH;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,CACZ,GAAG,EAAE,kBAAkB,CAAC,UAAU,EAAE,OAAO,CAAC,EAC5C,MAAM,EAAE,qBAAqB,KAC5B,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CAAC,UAAU,GAAG,OAAO,EAAE,OAAO,SAAS,QAAQ,GAAG,WAAW,EAChG,OAAO,EAAE,qBAAqB,CAAC,UAAU,EAAE,OAAO,CAAC,GACpD,OAAO,CAAC,KAAK,CAAC,CAwBhB"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI convenience bootstrap for `@gobing-ai/ts-infra`.
|
|
3
|
+
*
|
|
4
|
+
* Wraps {@link runNodeApplication} with the one thing every CLI does at the
|
|
5
|
+
* end: map the command result to a process exit code, and terminate. Same
|
|
6
|
+
* option shape, same defaults, same callback contract — no surprises.
|
|
7
|
+
*
|
|
8
|
+
* Differences from `runNodeApplication`:
|
|
9
|
+
* - `start` returns `number | void` instead of `void`. The number becomes the
|
|
10
|
+
* process exit code (void = 0). This is the Commander convention.
|
|
11
|
+
* - After `start` resolves, the runtime is shut down gracefully
|
|
12
|
+
* (`app.stop('shutdown')`) and the process terminates via `exitProcess(code)`.
|
|
13
|
+
* - If `start` throws (or startup fails), the error message is written to
|
|
14
|
+
* stderr and the process exits with code 1. `runNodeApplication`'s built-in
|
|
15
|
+
* reverse-order cleanup (telemetry, owned DB, user `stop` callback) runs
|
|
16
|
+
* before the rethrow, so resources are released.
|
|
17
|
+
*
|
|
18
|
+
* Use this for fire-and-forget CLIs (Commander, Yargs, etc.). For long-running
|
|
19
|
+
* services (servers, daemons, workers), use `runNodeApplication` directly — it
|
|
20
|
+
* returns a runtime handle you control.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* import { runCliApplication } from '@gobing-ai/ts-infra/application-cli';
|
|
25
|
+
*
|
|
26
|
+
* await runCliApplication({
|
|
27
|
+
* async start(app) {
|
|
28
|
+
* app.logger.info('running');
|
|
29
|
+
* return doWork(); // returns exit code
|
|
30
|
+
* },
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
import { echoError, exitProcess } from '@gobing-ai/ts-utils';
|
|
35
|
+
import { runNodeApplication } from './application-node.js';
|
|
36
|
+
/**
|
|
37
|
+
* CLI convenience application bootstrap.
|
|
38
|
+
*
|
|
39
|
+
* Delegates to {@link runNodeApplication} for all service lifecycle (logger,
|
|
40
|
+
* telemetry, events, DB, scheduler, plugins), then maps the `start` result to
|
|
41
|
+
* a process exit code and terminates. See the module docstring for details.
|
|
42
|
+
*
|
|
43
|
+
* @returns Never resolves — terminates the process via `exitProcess` internally.
|
|
44
|
+
*/
|
|
45
|
+
export async function runCliApplication(options) {
|
|
46
|
+
let exitCode = 0;
|
|
47
|
+
try {
|
|
48
|
+
const app = await runNodeApplication({
|
|
49
|
+
...options,
|
|
50
|
+
start: async (runtime) => {
|
|
51
|
+
const code = await options.start(runtime);
|
|
52
|
+
if (typeof code === 'number')
|
|
53
|
+
exitCode = code;
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
// `start` completed — graceful reverse-order shutdown of all services.
|
|
57
|
+
await app.stop('shutdown');
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
// Startup failure OR `start` threw. `runNodeApplication`'s catch block
|
|
61
|
+
// already ran stopAll/unloadAll on whatever started, so resources are
|
|
62
|
+
// released. We just normalize the exit code and surface the message.
|
|
63
|
+
exitCode = 1;
|
|
64
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
65
|
+
echoError(`Error: ${msg}`);
|
|
66
|
+
}
|
|
67
|
+
return exitProcess(exitCode);
|
|
68
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-bus.d.ts","sourceRoot":"","sources":["../../src/event-bus/event-bus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC/D,OAAO,EAAa,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAEnD,OAAO,KAAK,EAER,oBAAoB,EACpB,kBAAkB,EAElB,QAAQ,EAER,gBAAgB,EACnB,MAAM,SAAS,CAAC;AAQjB;;;GAGG;AACH,qBAAa,QAAQ,CAAC,OAAO,SAAS,QAAQ;IAC1C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAyD;IACtF,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAyD;IACvF,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA6C;IAC7E,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA6C;IAC/E,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkB;IAC3C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAsC;IACnE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;IAC5C,OAAO,CAAC,kBAAkB,CAAK;gBACnB,IAAI,CAAC,EAAE;QACf,QAAQ,CAAC,EAAE,QAAQ,CAAC;QACpB,YAAY,CAAC,EAAE,QAAQ,CAAC,kBAAkB,CAAC,CAAC;QAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;KACnB;IAMD,EAAE,CAAC,CAAC,SAAS,MAAM,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,IAAI;IAQzF,IAAI,CAAC,CAAC,SAAS,MAAM,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,IAAI;IAS3F,GAAG,CAAC,CAAC,SAAS,MAAM,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI;IAmBjE,kBAAkB,CAAC,CAAC,SAAS,MAAM,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC,GAAG,IAAI;IAgBtD,IAAI,CAAC,CAAC,SAAS,MAAM,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"event-bus.d.ts","sourceRoot":"","sources":["../../src/event-bus/event-bus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC/D,OAAO,EAAa,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAEnD,OAAO,KAAK,EAER,oBAAoB,EACpB,kBAAkB,EAElB,QAAQ,EAER,gBAAgB,EACnB,MAAM,SAAS,CAAC;AAQjB;;;GAGG;AACH,qBAAa,QAAQ,CAAC,OAAO,SAAS,QAAQ;IAC1C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAyD;IACtF,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAyD;IACvF,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA6C;IAC7E,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA6C;IAC/E,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkB;IAC3C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAsC;IACnE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;IAC5C,OAAO,CAAC,kBAAkB,CAAK;gBACnB,IAAI,CAAC,EAAE;QACf,QAAQ,CAAC,EAAE,QAAQ,CAAC;QACpB,YAAY,CAAC,EAAE,QAAQ,CAAC,kBAAkB,CAAC,CAAC;QAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;KACnB;IAMD,EAAE,CAAC,CAAC,SAAS,MAAM,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,IAAI;IAQzF,IAAI,CAAC,CAAC,SAAS,MAAM,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,IAAI;IAS3F,GAAG,CAAC,CAAC,SAAS,MAAM,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI;IAmBjE,kBAAkB,CAAC,CAAC,SAAS,MAAM,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC,GAAG,IAAI;IAgBtD,IAAI,CAAC,CAAC,SAAS,MAAM,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAoF7F,aAAa,CAAC,CAAC,SAAS,MAAM,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM;IAMjF,UAAU,IAAI,MAAM,EAAE;IAOtB,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,aAAa;IAkBrB,OAAO,CAAC,iBAAiB;IAUzB,6EAA6E;IAC7E,OAAO,CAAC,4BAA4B;IAWpC;;;;;;;;;;;;OAYG;IACH,gBAAgB,IAAI,UAAU,CAAC,oBAAoB,CAAC;IAepD,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,mBAAmB;IAW3B,OAAO,CAAC,oBAAoB;CAU/B"}
|
|
@@ -141,6 +141,8 @@ export class EventBus {
|
|
|
141
141
|
}
|
|
142
142
|
const durationMs = performance.now() - startMs;
|
|
143
143
|
const detail = args.length === 1 ? args[0] : args.length > 1 ? args : undefined;
|
|
144
|
+
// Metrics are emitted inline here; traces (span attributes + events) are attached
|
|
145
|
+
// by attachTelemetryObserver in default-observers.ts — see that file for the trace layer.
|
|
144
146
|
getEventbusEmitsTotal().add(1, { event: eventName });
|
|
145
147
|
if (errors > 0)
|
|
146
148
|
getEventbusErrorsTotal().add(errors, { event: eventName });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/ts-infra",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.22",
|
|
4
4
|
"description": "@gobing-ai/ts-infra — Infrastructure backbone: event bus, job queue, scheduler, telemetry, API client, and logging.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -55,6 +55,10 @@
|
|
|
55
55
|
"./application-node": {
|
|
56
56
|
"types": "./dist/application-node.d.ts",
|
|
57
57
|
"import": "./dist/application-node.js"
|
|
58
|
+
},
|
|
59
|
+
"./application-cli": {
|
|
60
|
+
"types": "./dist/application-cli.d.ts",
|
|
61
|
+
"import": "./dist/application-cli.js"
|
|
58
62
|
}
|
|
59
63
|
},
|
|
60
64
|
"files": [
|
|
@@ -74,11 +78,12 @@
|
|
|
74
78
|
"release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-infra-v<version> && git push --tags' && exit 1"
|
|
75
79
|
},
|
|
76
80
|
"dependencies": {
|
|
81
|
+
"@gobing-ai/ts-utils": "^0.3.22",
|
|
77
82
|
"@logtape/logtape": "^2.0.0"
|
|
78
83
|
},
|
|
79
84
|
"peerDependencies": {
|
|
80
|
-
"@gobing-ai/ts-db": "^0.3.
|
|
81
|
-
"@gobing-ai/ts-runtime": "^0.3.
|
|
85
|
+
"@gobing-ai/ts-db": "^0.3.22",
|
|
86
|
+
"@gobing-ai/ts-runtime": "^0.3.22",
|
|
82
87
|
"@opentelemetry/api": "^1.9.0",
|
|
83
88
|
"@opentelemetry/sdk-trace-node": "^2.0.0",
|
|
84
89
|
"@opentelemetry/sdk-metrics": "^2.0.0",
|
|
@@ -111,8 +116,8 @@
|
|
|
111
116
|
}
|
|
112
117
|
},
|
|
113
118
|
"devDependencies": {
|
|
114
|
-
"@gobing-ai/ts-db": "^0.3.
|
|
115
|
-
"@gobing-ai/ts-runtime": "^0.3.
|
|
119
|
+
"@gobing-ai/ts-db": "^0.3.22",
|
|
120
|
+
"@gobing-ai/ts-runtime": "^0.3.22",
|
|
116
121
|
"@types/bun": "1.3.14",
|
|
117
122
|
"@opentelemetry/api": "^1.9.0",
|
|
118
123
|
"@opentelemetry/sdk-trace-node": "^2.0.0",
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI convenience bootstrap for `@gobing-ai/ts-infra`.
|
|
3
|
+
*
|
|
4
|
+
* Wraps {@link runNodeApplication} with the one thing every CLI does at the
|
|
5
|
+
* end: map the command result to a process exit code, and terminate. Same
|
|
6
|
+
* option shape, same defaults, same callback contract — no surprises.
|
|
7
|
+
*
|
|
8
|
+
* Differences from `runNodeApplication`:
|
|
9
|
+
* - `start` returns `number | void` instead of `void`. The number becomes the
|
|
10
|
+
* process exit code (void = 0). This is the Commander convention.
|
|
11
|
+
* - After `start` resolves, the runtime is shut down gracefully
|
|
12
|
+
* (`app.stop('shutdown')`) and the process terminates via `exitProcess(code)`.
|
|
13
|
+
* - If `start` throws (or startup fails), the error message is written to
|
|
14
|
+
* stderr and the process exits with code 1. `runNodeApplication`'s built-in
|
|
15
|
+
* reverse-order cleanup (telemetry, owned DB, user `stop` callback) runs
|
|
16
|
+
* before the rethrow, so resources are released.
|
|
17
|
+
*
|
|
18
|
+
* Use this for fire-and-forget CLIs (Commander, Yargs, etc.). For long-running
|
|
19
|
+
* services (servers, daemons, workers), use `runNodeApplication` directly — it
|
|
20
|
+
* returns a runtime handle you control.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* import { runCliApplication } from '@gobing-ai/ts-infra/application-cli';
|
|
25
|
+
*
|
|
26
|
+
* await runCliApplication({
|
|
27
|
+
* async start(app) {
|
|
28
|
+
* app.logger.info('running');
|
|
29
|
+
* return doWork(); // returns exit code
|
|
30
|
+
* },
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { echoError, exitProcess } from '@gobing-ai/ts-utils';
|
|
36
|
+
import type { ApplicationRuntime, ApplicationStopReason, EventMap, InfraEvents } from './application/types';
|
|
37
|
+
import { type NodeApplicationOptions, runNodeApplication } from './application-node';
|
|
38
|
+
|
|
39
|
+
/** Options for the CLI convenience {@link runCliApplication}. */
|
|
40
|
+
export interface CliApplicationOptions<TAppConfig = unknown, TEvents extends EventMap = InfraEvents>
|
|
41
|
+
extends Omit<NodeApplicationOptions<TAppConfig, TEvents>, 'start' | 'stop'> {
|
|
42
|
+
/**
|
|
43
|
+
* User callback: CLI logic. Called after all services are ready.
|
|
44
|
+
* Return a number to set the process exit code; void/undefined means success (0).
|
|
45
|
+
*/
|
|
46
|
+
readonly start: (app: ApplicationRuntime<TAppConfig, TEvents>) => Promise<number | undefined> | number | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* User callback: cleanup before services shut down. Receives the stop
|
|
49
|
+
* reason: `'shutdown'` on normal completion, `'error'` if `start` threw.
|
|
50
|
+
*/
|
|
51
|
+
readonly stop?: (
|
|
52
|
+
app: ApplicationRuntime<TAppConfig, TEvents>,
|
|
53
|
+
reason: ApplicationStopReason,
|
|
54
|
+
) => Promise<void> | void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* CLI convenience application bootstrap.
|
|
59
|
+
*
|
|
60
|
+
* Delegates to {@link runNodeApplication} for all service lifecycle (logger,
|
|
61
|
+
* telemetry, events, DB, scheduler, plugins), then maps the `start` result to
|
|
62
|
+
* a process exit code and terminates. See the module docstring for details.
|
|
63
|
+
*
|
|
64
|
+
* @returns Never resolves — terminates the process via `exitProcess` internally.
|
|
65
|
+
*/
|
|
66
|
+
export async function runCliApplication<TAppConfig = unknown, TEvents extends EventMap = InfraEvents>(
|
|
67
|
+
options: CliApplicationOptions<TAppConfig, TEvents>,
|
|
68
|
+
): Promise<never> {
|
|
69
|
+
let exitCode = 0;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const app = await runNodeApplication<TAppConfig, TEvents>({
|
|
73
|
+
...options,
|
|
74
|
+
start: async (runtime) => {
|
|
75
|
+
const code = await options.start(runtime);
|
|
76
|
+
if (typeof code === 'number') exitCode = code;
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// `start` completed — graceful reverse-order shutdown of all services.
|
|
81
|
+
await app.stop('shutdown');
|
|
82
|
+
} catch (error) {
|
|
83
|
+
// Startup failure OR `start` threw. `runNodeApplication`'s catch block
|
|
84
|
+
// already ran stopAll/unloadAll on whatever started, so resources are
|
|
85
|
+
// released. We just normalize the exit code and surface the message.
|
|
86
|
+
exitCode = 1;
|
|
87
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
88
|
+
echoError(`Error: ${msg}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return exitProcess(exitCode);
|
|
92
|
+
}
|
|
@@ -163,6 +163,8 @@ export class EventBus<TEvents extends EventMap> {
|
|
|
163
163
|
const durationMs = performance.now() - startMs;
|
|
164
164
|
const detail = args.length === 1 ? args[0] : args.length > 1 ? args : undefined;
|
|
165
165
|
|
|
166
|
+
// Metrics are emitted inline here; traces (span attributes + events) are attached
|
|
167
|
+
// by attachTelemetryObserver in default-observers.ts — see that file for the trace layer.
|
|
166
168
|
getEventbusEmitsTotal().add(1, { event: eventName });
|
|
167
169
|
if (errors > 0) getEventbusErrorsTotal().add(errors, { event: eventName });
|
|
168
170
|
|