@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 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;IAkF7F,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"}
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.20",
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.20",
81
- "@gobing-ai/ts-runtime": "^0.3.20",
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.20",
115
- "@gobing-ai/ts-runtime": "^0.3.20",
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