@gobing-ai/ts-infra 0.3.5 → 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.
Files changed (63) hide show
  1. package/README.md +95 -60
  2. package/dist/application/index.d.ts +16 -18
  3. package/dist/application/index.d.ts.map +1 -1
  4. package/dist/application/index.js +59 -96
  5. package/dist/application/plugins/builtins.d.ts +64 -0
  6. package/dist/application/plugins/builtins.d.ts.map +1 -0
  7. package/dist/application/plugins/builtins.js +146 -0
  8. package/dist/application/plugins/host.d.ts +61 -0
  9. package/dist/application/plugins/host.d.ts.map +1 -0
  10. package/dist/application/plugins/host.js +131 -0
  11. package/dist/application/plugins/index.d.ts +3 -0
  12. package/dist/application/plugins/index.d.ts.map +1 -0
  13. package/dist/application/plugins/index.js +2 -0
  14. package/dist/application/plugins/types.d.ts +69 -0
  15. package/dist/application/plugins/types.d.ts.map +1 -0
  16. package/dist/application/plugins/types.js +10 -0
  17. package/dist/application/types.d.ts +7 -0
  18. package/dist/application/types.d.ts.map +1 -1
  19. package/dist/application-node.d.ts.map +1 -1
  20. package/dist/application-node.js +40 -57
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -1
  24. package/dist/scheduler/cloudflare.d.ts.map +1 -1
  25. package/dist/scheduler/cloudflare.js +9 -2
  26. package/dist/scheduler/factory.d.ts +4 -8
  27. package/dist/scheduler/factory.d.ts.map +1 -1
  28. package/dist/scheduler/factory.js +12 -22
  29. package/dist/scheduler/index.d.ts +1 -1
  30. package/dist/scheduler/index.d.ts.map +1 -1
  31. package/dist/scheduler/index.js +1 -1
  32. package/dist/scheduler/wrap-handler.d.ts +8 -4
  33. package/dist/scheduler/wrap-handler.d.ts.map +1 -1
  34. package/dist/scheduler/wrap-handler.js +8 -4
  35. package/dist/telemetry/index.d.ts +1 -2
  36. package/dist/telemetry/index.d.ts.map +1 -1
  37. package/dist/telemetry/index.js +1 -2
  38. package/dist/telemetry/metrics.d.ts +9 -1
  39. package/dist/telemetry/metrics.d.ts.map +1 -1
  40. package/dist/telemetry/metrics.js +22 -1
  41. package/dist/telemetry/sdk.d.ts +33 -1
  42. package/dist/telemetry/sdk.d.ts.map +1 -1
  43. package/dist/telemetry/sdk.js +14 -1
  44. package/package.json +5 -5
  45. package/src/application/index.ts +72 -102
  46. package/src/application/plugins/builtins.ts +178 -0
  47. package/src/application/plugins/host.ts +143 -0
  48. package/src/application/plugins/index.ts +3 -0
  49. package/src/application/plugins/types.ts +86 -0
  50. package/src/application/types.ts +7 -0
  51. package/src/application-node.ts +43 -61
  52. package/src/index.ts +0 -2
  53. package/src/scheduler/cloudflare.ts +16 -5
  54. package/src/scheduler/factory.ts +15 -26
  55. package/src/scheduler/index.ts +1 -1
  56. package/src/scheduler/wrap-handler.ts +8 -4
  57. package/src/telemetry/index.ts +9 -2
  58. package/src/telemetry/metrics.ts +22 -1
  59. package/src/telemetry/sdk.ts +51 -2
  60. package/dist/telemetry/config.d.ts +0 -41
  61. package/dist/telemetry/config.d.ts.map +0 -1
  62. package/dist/telemetry/config.js +0 -21
  63. package/src/telemetry/config.ts +0 -59
package/README.md CHANGED
@@ -14,6 +14,8 @@ Infrastructure backbone — typed event bus, queue/scheduler contracts, adapter
14
14
  | **Telemetry** | `telemetry/` | OTel instrumentation — tracing (`traceAsync`), metrics (12 instruments), SQL sanitizer; opt-in OTLP export via the `/otel-node` subpath |
15
15
  | **API Client** | `api-client.ts` | Typed HTTP client with OTel tracing, timeout, and error handling |
16
16
  | **Logger** | `logger.ts` | LogTape-backed structured logger with levels, child loggers, injectable sinks, and mute toggle |
17
+ | **Application Bootstrap** | `application/` | Plugin-driven lifecycle: deterministic startup → shutdown, DI orchestration, services as plugins |
18
+ | **Plugin Host** | `application/plugins/` | Insertion-ordered plugin registry, fail-fast/fail-soft lifecycle fan-out, reason-carrying teardown |
17
19
 
18
20
  ## Architecture
19
21
 
@@ -453,20 +455,26 @@ await app.stop();
453
455
  ```
454
456
 
455
457
  The portable `runApplication` accepts pre-built services via `services` and
456
- never reads files. All services are created with defaults; feature flags
457
- control what gets initialized. Startup order is deterministic:
458
+ never reads files. The lifecycle is **plugin-driven**: infra services (logger,
459
+ telemetry, scheduler) and the user `start`/`stop` callbacks are registered as
460
+ built-in plugins on a `PluginHost`, in dependency order:
458
461
 
459
- 1. Resolve bootstrap config + app config
460
- 2. Initialize logger
461
- 3. Initialize telemetry
462
- 4. Create lifecycle bus + EventBus
463
- 5. Register injected DB adapter
464
- 6. Initialize scheduler + register entries
465
- 7. Call user `start(app)` callback
466
- 8. Start scheduler if `autoStart`
462
+ ```
463
+ logger telemetry → [your plugins] → user-callback → scheduler
464
+ ```
465
+
466
+ `loadAll()` then `startAll()` run them forward (A→Z). Plugins marked `failFast`
467
+ (the built-in services) abort the bootstrap if their `onStart` throws; other
468
+ plugins are fail-soft. Shutdown is the reverse fan-out — `stop(reason)` calls
469
+ each plugin's `onStop(host, reason)` in reverse registration order (scheduler
470
+ stop, your `stop(app, reason)`, telemetry shutdown). `stop()` is idempotent —
471
+ safe to call from multiple signal handlers.
467
472
 
468
- Shutdown runs in reverse order. `stop()` is idempotent — safe to call from
469
- multiple signal handlers.
473
+ **DB ownership.** The portable layer **never closes a caller-injected
474
+ `services.db`** you own its lifecycle and must close it yourself. Only an
475
+ adapter the bootstrap *creates* (e.g. the Node subpath's Bun SQLite adapter) is
476
+ closed automatically, via a built-in DB plugin. (Changed in 0.x — previously the
477
+ portable layer closed injected adapters unconditionally.)
470
478
 
471
479
  #### Node/Bun convenience bootstrap
472
480
 
@@ -549,68 +557,95 @@ bun add @opentelemetry/sdk-trace-node @opentelemetry/sdk-metrics \
549
557
  @opentelemetry/exporter-metrics-otlp-http
550
558
  ```
551
559
 
552
- ### Full bootstrap example
560
+ ### Full bootstrap example (recommended: `runApplication` or `runNodeApplication`)
561
+
562
+ Prefer the plugin-driven bootstrap over manual wiring — the host lifecycle handles
563
+ startup ordering, reverse-order teardown, fail-fast/fail-soft policies, and idempotent
564
+ stop. Two entry points:
553
565
 
554
566
  ```ts
555
- import { createRuntimeContextFromFactory } from '@gobing-ai/ts-runtime';
556
- import { createDbAdapter, applyMigrations } from '@gobing-ai/ts-db';
557
- import {
558
- EventBus,
559
- setSchedulerAdapter,
560
- initTelemetry,
561
- APIClient,
562
- getLogger,
563
- initializeLogger,
564
- } from '@gobing-ai/ts-infra';
565
- import { NodeSchedulerAdapter } from '@gobing-ai/ts-infra/scheduler-node';
567
+ // Portable inject everything yourself:
568
+ import { runApplication } from '@gobing-ai/ts-infra/application';
566
569
 
567
- // 1. Runtime
568
- const ctx = await createRuntimeContextFromFactory();
570
+ const app = await runApplication({
571
+ config: {
572
+ logging: { level: 'info', console: true },
573
+ telemetry: { enabled: true, serviceName: 'my-app' },
574
+ },
575
+ services: { db }, // caller-owned — you close it
576
+ async start(app) {
577
+ app.logger.info('started');
578
+ },
579
+ async stop(app, reason) {
580
+ app.logger.info('shutting down', { reason });
581
+ },
582
+ });
569
583
 
570
- // 2. Database
571
- const db = await createDbAdapter({ driver: 'bun-sqlite', url: './data/app.db' });
572
- await applyMigrations(db);
573
- ctx.register('db', db);
584
+ await app.stop('signal');
585
+ ```
574
586
 
575
- // 3. Logging
576
- await initializeLogger({ level: 'info', console: true });
577
- const log = getLogger('app');
587
+ ```ts
588
+ // Node / Bun — YAML config, file logs, OTel export, Bun SQLite, Node scheduler:
589
+ import { runNodeApplication } from '@gobing-ai/ts-infra/application-node';
578
590
 
579
- // 4. Telemetry
580
- initTelemetry({ serviceName: 'my-app', environment: 'production' });
591
+ const app = await runNodeApplication({
592
+ configLoader: {
593
+ configFile: 'config/app.yaml',
594
+ bootstrapSection: 'bootstrap',
595
+ appSection: 'billing',
596
+ appConfig: { safeParse: (raw) => mySchema.safeParse(raw) },
597
+ },
598
+ async start(app) {
599
+ app.logger.info('started', { port: app.appConfig.port });
600
+ },
601
+ });
602
+ ```
581
603
 
582
- // 5. Event bus
583
- const bus = new EventBus<AppEvents>();
604
+ If you prefer manual wiring (e.g. you already have an init sequence), the individual
605
+ subsystems still work standalone — `EventBus`, `APIClient`, `getLogger`, etc. are
606
+ all importable directly from the main barrel and do not require the bootstrap.
584
607
 
585
- // 6. Scheduler
586
- const scheduler = new NodeSchedulerAdapter();
587
- setSchedulerAdapter(scheduler);
588
- scheduler.register('3600000', async () => {
589
- log.info('Hourly cleanup running');
590
- });
591
- await scheduler.start();
608
+ #### Custom plugins
609
+
610
+ Plugins run in registration order forward (`loadAll → startAll`) and reverse order
611
+ on teardown (`stopAll unloadAll`):
592
612
 
593
- // 7. API client
594
- const stripeApi = new APIClient({
595
- baseUrl: 'https://api.stripe.com/v1',
596
- defaultHeaders: { Authorization: `Bearer ${process.env.STRIPE_KEY}` },
613
+ ```ts
614
+ import { runApplication } from '@gobing-ai/ts-infra/application';
615
+ import type { Plugin } from '@gobing-ai/ts-infra';
616
+
617
+ const auditPlugin: Plugin = {
618
+ name: 'audit-logger',
619
+ version: '1.0.0',
620
+ failFast: false, // fail-soft: a throwing onStart is logged, boot continues
621
+ onLoad: async (host) => { /* validate preconditions — throws abort boot */ },
622
+ onStart: async (host) => { host.events.on('order.placed', auditHandler); },
623
+ onStop: async (host, reason) => { host.logger.info('persisting audit buffer', { reason }); },
624
+ onUnload: async () => { /* release resources */ },
625
+ };
626
+
627
+ const app = await runApplication({
628
+ plugins: [auditPlugin],
629
+ config: { logging: { console: true } },
597
630
  });
631
+ ```
598
632
 
599
- log.info('Application started');
633
+ The host forwards the optional teardown `reason` (a plain `string` on the core contract,
634
+ `ApplicationStopReason` — `'manual' | 'signal' | 'error' | 'shutdown'` — at the app level)
635
+ to every plugin's `onStop`/`onUnload`. Built-in service plugins use this for log context;
636
+ the user-callback plugin delivers it to your `stop(app, reason)`.
600
637
  ```
601
638
 
602
639
  ### Graceful shutdown
603
640
 
604
- ```ts
605
- async function shutdown() {
606
- log.info('Shutting down...');
607
- await scheduler.stop();
608
- db.close();
609
- await shutdownTelemetry();
610
- await ctx.dispose();
611
- process.exit(0);
612
- }
641
+ With `runApplication` or `runNodeApplication`, shutdown is a single idempotent call:
613
642
 
614
- process.on('SIGTERM', shutdown);
615
- process.on('SIGINT', shutdown);
643
+ ```ts
644
+ process.on('SIGTERM', () => app.stop('signal'));
645
+ process.on('SIGINT', () => app.stop('signal'));
616
646
  ```
647
+
648
+ The host runs `stopAll(reason) → unloadAll(reason)` in reverse registration order:
649
+ scheduler stops, your `stop` callback fires, telemetry flushes, and the optional
650
+ DB adapter closes (if the bootstrap created it — caller-injected adapters are
651
+ your responsibility). Idempotent: safe from overlapping signal handlers.
@@ -19,24 +19,20 @@ import type { ApplicationBootstrapOptions, ApplicationRuntime } from './types';
19
19
  * Accepts injected dependencies; never opens files, reads config from disk,
20
20
  * or wires runtime-specific exporters.
21
21
  *
22
- * Startup order (deterministic, per R5):
23
- * 1. Resolve bootstrap config + app config
24
- * 2. Initialize logger
25
- * 3. Initialize telemetry
26
- * 4. Create lifecycle bus + application EventBus
27
- * 5. Register DB adapter (injected only)
28
- * 6. Initialize scheduler + register entries
29
- * 7. Call user `start(app)` callback
30
- * 8. Start scheduler if `autoStart`
31
- *
32
- * Shutdown order (reverse, per R5):
33
- * 1. User `stop(app, reason)` callback
34
- * 2. Stop scheduler
35
- * 3. Close DB adapter
36
- * 4. Shut down telemetry
37
- *
38
- * If any startup step fails, already-started services are cleaned up in reverse
39
- * order before rethrowing. `stop()` is idempotent.
22
+ * Startup is plugin-driven. Built-in service plugins are registered in dependency
23
+ * order, then `loadAll()` + `startAll()` run them forward:
24
+ * 1. Resolve bootstrap config; build EventBus + PluginHost
25
+ * 2. Register built-ins in order: logger → telemetry → [caller plugins] →
26
+ * user-callback scheduler (scheduler last so autoStart runs after user start)
27
+ * 3. `loadAll()` then `startAll()` — `failFast` plugins abort boot on failure
28
+ *
29
+ * Shutdown is the reverse fan-out: `stopAll(reason)` → `unloadAll(reason)` calls
30
+ * every plugin's `onStop`/`onUnload` in reverse registration order (scheduler stop,
31
+ * user `stop(app, reason)`, telemetry shutdown, owned-DB close). Caller-injected
32
+ * `services.db` is caller-owned and never closed here.
33
+ *
34
+ * If startup fails, the host's reverse-order `stopAll('error')`/`unloadAll('error')`
35
+ * tears down whatever started before rethrowing. `stop()` is idempotent.
40
36
  *
41
37
  * @example
42
38
  * ```ts
@@ -53,5 +49,7 @@ import type { ApplicationBootstrapOptions, ApplicationRuntime } from './types';
53
49
  export declare function runApplication<TAppConfig = unknown, TEvents extends EventMap = InfraEvents>(options: ApplicationBootstrapOptions<TAppConfig, TEvents>): Promise<ApplicationRuntime<TAppConfig, TEvents>>;
54
50
  export type { BusLifecycleEvents, EventMap } from '../event-bus/types';
55
51
  export type { InfraEvents } from '../events';
52
+ export type { PluginHost } from './plugins/host';
53
+ export type { Plugin, PluginSummary } from './plugins/types';
56
54
  export type { ApplicationBootstrapConfig, ApplicationBootstrapOptions, ApplicationConfigLoader, ApplicationConfigValidator, ApplicationRuntime, ApplicationServices, ApplicationStopReason, ConfigValidationResult, DbAdapterLike, EventsOptions, LoggingOptions, SchedulerOptions, TelemetryOptions, } from './types';
57
55
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/application/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAsB,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAK7C,OAAO,KAAK,EAER,2BAA2B,EAC3B,kBAAkB,EAGrB,MAAM,SAAS,CAAC;AAuDjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAsB,cAAc,CAAC,UAAU,GAAG,OAAO,EAAE,OAAO,SAAS,QAAQ,GAAG,WAAW,EAC7F,OAAO,EAAE,2BAA2B,CAAC,UAAU,EAAE,OAAO,CAAC,GAC1D,OAAO,CAAC,kBAAkB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAyIlD;AAED,YAAY,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACvE,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C,YAAY,EACR,0BAA0B,EAC1B,2BAA2B,EAC3B,uBAAuB,EACvB,0BAA0B,EAC1B,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,EACrB,sBAAsB,EACtB,aAAa,EACb,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,gBAAgB,GACnB,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/application/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAsB,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAM7C,OAAO,KAAK,EAER,2BAA2B,EAC3B,kBAAkB,EAGrB,MAAM,SAAS,CAAC;AAiCjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAsB,cAAc,CAAC,UAAU,GAAG,OAAO,EAAE,OAAO,SAAS,QAAQ,GAAG,WAAW,EAC7F,OAAO,EAAE,2BAA2B,CAAC,UAAU,EAAE,OAAO,CAAC,GAC1D,OAAO,CAAC,kBAAkB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAkIlD;AAED,YAAY,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACvE,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,YAAY,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAE7D,YAAY,EACR,0BAA0B,EAC1B,2BAA2B,EAC3B,uBAAuB,EACvB,0BAA0B,EAC1B,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,EACrB,sBAAsB,EACtB,aAAa,EACb,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,gBAAgB,GACnB,MAAM,SAAS,CAAC"}
@@ -11,9 +11,10 @@
11
11
  */
12
12
  import { attachDefaultObservers, createLifecycleBus } from '../event-bus/default-observers.js';
13
13
  import { EventBus } from '../event-bus/event-bus.js';
14
- import { getLogger, initializeLogger } from '../logger.js';
15
- import { initScheduler, setSchedulerAdapter } from '../scheduler/factory.js';
16
- import { initTelemetry, shutdownTelemetry } from '../telemetry/sdk.js';
14
+ import { getLogger } from '../logger.js';
15
+ import { initScheduler } from '../scheduler/factory.js';
16
+ import { loggerPlugin, schedulerPlugin, telemetryPlugin, userCallbackPlugin } from './plugins/builtins.js';
17
+ import { PluginHost } from './plugins/host.js';
17
18
  // ── Shutdown (deterministic reverse order per R5) ─────────────────────────
18
19
  async function performShutdown(state, reason) {
19
20
  if (state.stopped)
@@ -22,29 +23,13 @@ async function performShutdown(state, reason) {
22
23
  const app = state.app;
23
24
  if (!app)
24
25
  return;
25
- // 1. User stop callback
26
- if (state.userStop) {
27
- await state.userStop(app, reason);
28
- }
29
- // 2. Stop scheduler
30
- if (state.schedulerStarted && state.schedulerAdapter) {
31
- await state.schedulerAdapter.stop().catch(() => { });
32
- state.schedulerStarted = false;
33
- }
34
- // 3. Close DB adapter
35
- if (app.db) {
36
- try {
37
- app.db.close();
38
- }
39
- catch {
40
- /* best-effort */
41
- }
42
- }
43
- // 4. Shutdown telemetry
44
- if (state.telemetryInitialized) {
45
- await shutdownTelemetry();
46
- state.telemetryInitialized = false;
47
- }
26
+ // 1. Stop + unload plugins in reverse registration order (fail-soft).
27
+ // Scheduler stop and telemetry shutdown run here via their onStop hooks.
28
+ await state.pluginHost.stopAll(reason);
29
+ await state.pluginHost.unloadAll(reason);
30
+ // Shutdown is complete — the host's stopAll/unloadAll calls every plugin's
31
+ // onStop/onUnload in reverse registration order, including user callback,
32
+ // scheduler, and service teardown. No inline steps.
48
33
  }
49
34
  // ── Public API ────────────────────────────────────────────────────────────
50
35
  /**
@@ -54,24 +39,20 @@ async function performShutdown(state, reason) {
54
39
  * Accepts injected dependencies; never opens files, reads config from disk,
55
40
  * or wires runtime-specific exporters.
56
41
  *
57
- * Startup order (deterministic, per R5):
58
- * 1. Resolve bootstrap config + app config
59
- * 2. Initialize logger
60
- * 3. Initialize telemetry
61
- * 4. Create lifecycle bus + application EventBus
62
- * 5. Register DB adapter (injected only)
63
- * 6. Initialize scheduler + register entries
64
- * 7. Call user `start(app)` callback
65
- * 8. Start scheduler if `autoStart`
42
+ * Startup is plugin-driven. Built-in service plugins are registered in dependency
43
+ * order, then `loadAll()` + `startAll()` run them forward:
44
+ * 1. Resolve bootstrap config; build EventBus + PluginHost
45
+ * 2. Register built-ins in order: logger → telemetry → [caller plugins] →
46
+ * user-callback scheduler (scheduler last so autoStart runs after user start)
47
+ * 3. `loadAll()` then `startAll()` — `failFast` plugins abort boot on failure
66
48
  *
67
- * Shutdown order (reverse, per R5):
68
- * 1. User `stop(app, reason)` callback
69
- * 2. Stop scheduler
70
- * 3. Close DB adapter
71
- * 4. Shut down telemetry
49
+ * Shutdown is the reverse fan-out: `stopAll(reason)` → `unloadAll(reason)` calls
50
+ * every plugin's `onStop`/`onUnload` in reverse registration order (scheduler stop,
51
+ * user `stop(app, reason)`, telemetry shutdown, owned-DB close). Caller-injected
52
+ * `services.db` is caller-owned and never closed here.
72
53
  *
73
- * If any startup step fails, already-started services are cleaned up in reverse
74
- * order before rethrowing. `stop()` is idempotent.
54
+ * If startup fails, the host's reverse-order `stopAll('error')`/`unloadAll('error')`
55
+ * tears down whatever started before rethrowing. `stop()` is idempotent.
75
56
  *
76
57
  * @example
77
58
  * ```ts
@@ -112,41 +93,13 @@ export async function runApplication(options) {
112
93
  const eventsDefaultObservers = options.config?.events?.defaultObservers ?? true;
113
94
  const state = {
114
95
  app: undefined,
115
- userStop: options.stop,
116
- schedulerAdapter: undefined,
117
- schedulerStarted: false,
118
- loggerInitialized: false,
119
- telemetryInitialized: false,
120
96
  stopped: false,
97
+ pluginHost: undefined,
121
98
  };
122
99
  try {
123
- // ── 1. Initialize logger ────────────────────────────────────────
124
- let logger;
125
- if (options.services?.logger) {
126
- logger = options.services.logger;
127
- }
128
- else if (loggingConfig.enabled) {
129
- await initializeLogger({
130
- level: loggingConfig.level,
131
- console: loggingConfig.console,
132
- fileSink: loggingConfig.fileSink,
133
- json: loggingConfig.json,
134
- });
135
- logger = getLogger('bootstrap');
136
- }
137
- else {
138
- logger = getLogger('bootstrap');
139
- }
140
- // ── 2. Initialize telemetry ────────────────────────────────────
141
- if (telemetryConfig.enabled) {
142
- initTelemetry({
143
- enabled: telemetryConfig.enabled,
144
- serviceName: telemetryConfig.serviceName,
145
- environment: telemetryConfig.environment,
146
- dbStatementDebug: telemetryConfig.dbStatementDebug,
147
- });
148
- state.telemetryInitialized = true;
149
- }
100
+ // ── 1. Resolve logger (init deferred to loggerPlugin) ───────────
101
+ const logger = options.services?.logger ?? getLogger('bootstrap');
102
+ const loggerInjected = !!options.services?.logger;
150
103
  // ── 3. Create lifecycle bus + EventBus ─────────────────────────
151
104
  const lifecycleBus = eventsEnabled && eventsLifecycle ? (options.services?.lifecycleBus ?? createLifecycleBus()) : undefined;
152
105
  if (lifecycleBus && eventsDefaultObservers) {
@@ -161,20 +114,24 @@ export async function runApplication(options) {
161
114
  let scheduler;
162
115
  if (schedulerConfig.enabled) {
163
116
  const adapter = options.services?.scheduler ?? schedOpts?.adapter;
164
- if (adapter) {
165
- setSchedulerAdapter(adapter);
166
- }
167
- scheduler = initScheduler(schedOpts?.entries);
168
- state.schedulerAdapter = scheduler;
117
+ scheduler = initScheduler(adapter, schedOpts?.entries);
169
118
  }
170
- // ── Build resolved config ──────────────────────────────────────
119
+ // ── 5.5 Plugin host + built-in service plugins ─────────────────
120
+ const pluginHost = options.services?.pluginHost ?? new PluginHost(events);
121
+ state.pluginHost = pluginHost;
122
+ // Register built-in service plugins in dependency order: logger -> telemetry.
123
+ pluginHost.register(loggerPlugin(loggingConfig, loggerInjected));
124
+ pluginHost.register(telemetryPlugin(telemetryConfig));
125
+ // Caller-injected `services.db` is NOT closed by the portable layer — it is
126
+ // caller-owned (task 0028). Only adapters the bootstrap CREATES (the Node
127
+ // subpath) are wrapped in a `dbPlugin` whose onStop closes them.
128
+ // ── Build runtime handle (before startAll so plugins can capture it) ─
171
129
  const resolvedConfig = {
172
130
  logging: loggingConfig,
173
131
  events: { enabled: eventsEnabled, lifecycle: eventsLifecycle, defaultObservers: eventsDefaultObservers },
174
132
  telemetry: telemetryConfig,
175
133
  scheduler: schedulerConfig,
176
134
  };
177
- // ── Build runtime handle ───────────────────────────────────────
178
135
  const app = {
179
136
  config: resolvedConfig,
180
137
  appConfig: options.appConfig,
@@ -183,28 +140,34 @@ export async function runApplication(options) {
183
140
  lifecycleBus,
184
141
  db,
185
142
  scheduler,
143
+ pluginHost,
186
144
  stop: (reason) => performShutdown(state, reason ?? 'manual'),
187
145
  };
188
146
  state.app = app;
189
- // ── 6. User start callback ─────────────────────────────────────
190
- await options.start(app);
191
- // ── 7. Start scheduler ─────────────────────────────────────────
192
- if (schedulerConfig.enabled && schedulerConfig.autoStart && scheduler) {
193
- await scheduler.start();
194
- state.schedulerStarted = true;
147
+ // ── Register caller-provided plugins (before user callback) ─────────
148
+ if (options.plugins) {
149
+ for (const p of options.plugins) {
150
+ pluginHost.register(p);
151
+ }
152
+ }
153
+ // ── Register user-callback plugin (after services, before scheduler) ─
154
+ pluginHost.register(userCallbackPlugin(options.start, options.stop, app));
155
+ // ── Register scheduler plugin (LAST — autoStart after user callback) ─
156
+ if (schedulerConfig.enabled && scheduler) {
157
+ pluginHost.register(schedulerPlugin(scheduler, schedulerConfig.autoStart));
195
158
  }
159
+ // ── Load + start (built-in failFast=rethrow on critical failure) ─
160
+ await pluginHost.loadAll();
161
+ await pluginHost.startAll();
196
162
  return app;
197
163
  }
198
164
  catch (error) {
199
- // Reverse-order cleanup of services this bootstrap owns.
200
- // Scheduler: start() is the last async op before return; if it throws,
201
- // schedulerStarted is still false, so there is nothing started to stop.
202
- // DB: injected by the caller (portable bootstrap never creates one), so
203
- // its lifecycle is caller-owned and not closed here.
204
- // Telemetry: owned by the bootstrap — shut it down if it was initialized.
205
- if (state.telemetryInitialized) {
206
- await shutdownTelemetry();
207
- }
165
+ // Startup failed: tear down whatever started, in reverse registration
166
+ // order, via the host. Each plugin's onStop/onUnload is best-effort, so a
167
+ // partially-started ring still releases its resources (telemetry, scheduler,
168
+ // owned DB). Caller-injected services.db is caller-owned and not touched.
169
+ await state.pluginHost.stopAll('error');
170
+ await state.pluginHost.unloadAll('error');
208
171
  throw error;
209
172
  }
210
173
  }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Built-in service plugins for the application bootstrap.
3
+ *
4
+ * Each factory returns a `Plugin` that maps an existing init/shutdown pair onto
5
+ * the PluginHost lifecycle: `onStart` = init, `onStop` = teardown. Absent
6
+ * hooks are omitted (no-op by absence, not by stub).
7
+ *
8
+ * @module application/plugins
9
+ */
10
+ import type { LogLevel } from '../../logger';
11
+ import type { ApplicationBootstrapConfig, DbAdapterLike } from '../types';
12
+ import type { Plugin } from './types';
13
+ /**
14
+ * Built-in plugin for telemetry + metrics.
15
+ *
16
+ * `onStart`: `initTelemetry` + `initMetrics` (pre-warm instruments).
17
+ * `onStop`: `shutdownMetrics` + `shutdownTelemetry` (reverse of init).
18
+ * `failFast: true` — a failing telemetry init aborts the bootstrap.
19
+ */
20
+ export declare function telemetryPlugin(config: ApplicationBootstrapConfig['telemetry']): Plugin;
21
+ /**
22
+ * Built-in plugin for the structured logger.
23
+ *
24
+ * `onStart`: `initializeLogger` (skipped when an injected logger is present).
25
+ * No `onStop` — the logger has no teardown.
26
+ * `failFast: true` — a failing logger init aborts the bootstrap.
27
+ */
28
+ export declare function loggerPlugin(config: {
29
+ enabled: boolean;
30
+ level: LogLevel;
31
+ console: boolean;
32
+ json: boolean;
33
+ fileSink?: ((line: string) => void) | undefined;
34
+ }, injected?: boolean): Plugin;
35
+ import type { EventMap } from '../../event-bus/types';
36
+ import type { ApplicationRuntime } from '../types';
37
+ /**
38
+ * Built-in plugin that wraps the user's `start`/`stop` callbacks.
39
+ *
40
+ * `onStart`: calls `options.start(app)`. `failFast: true`.
41
+ * `onStop`: calls `options.stop(app, reason)`. Registered after services,
42
+ * before scheduler, so stopAll/reverse-order places it after
43
+ * scheduler.stop and before service teardown.
44
+ */
45
+ export declare function userCallbackPlugin<TAppConfig, TEvents extends EventMap>(start: (app: ApplicationRuntime<TAppConfig, TEvents>) => Promise<void> | void, stop: ((app: ApplicationRuntime<TAppConfig, TEvents>, reason: string) => Promise<void> | void) | undefined, app?: ApplicationRuntime<TAppConfig, TEvents>): Plugin;
46
+ import type { SchedulerAdapter } from '../../scheduler/types';
47
+ /**
48
+ * Built-in plugin for the scheduler.
49
+ *
50
+ * `onStart`: `adapter.start()` when `autoStart` is true.
51
+ * `onStop`: `adapter.stop()` — fail-soft.
52
+ *
53
+ * Registered LAST so autoStart always runs after the user callback.
54
+ */
55
+ export declare function schedulerPlugin(adapter: SchedulerAdapter, autoStart: boolean): Plugin;
56
+ /**
57
+ * Built-in plugin for a DB adapter the bootstrap OWNS.
58
+ *
59
+ * `onStop(reason)`: `db.close()` — best-effort.
60
+ * Register ONLY when the bootstrap creates the adapter (Node subpath).
61
+ * Do NOT register for caller-injected `services.db` — those are caller-owned.
62
+ */
63
+ export declare function dbPlugin(db: DbAdapterLike): Plugin;
64
+ //# sourceMappingURL=builtins.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../../../src/application/plugins/builtins.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAI7C,OAAO,KAAK,EAAE,0BAA0B,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC1E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAItC;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,0BAA0B,CAAC,WAAW,CAAC,GAAG,MAAM,CAwBvF;AAID;;;;;;GAMG;AACH,wBAAgB,YAAY,CACxB,MAAM,EAAE;IACJ,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,QAAQ,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;CACnD,EACD,QAAQ,CAAC,EAAE,OAAO,GACnB,MAAM,CAiBR;AAID,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAEnD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,OAAO,SAAS,QAAQ,EACnE,KAAK,EAAE,CAAC,GAAG,EAAE,kBAAkB,CAAC,UAAU,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,EAC7E,IAAI,EAAE,CAAC,CAAC,GAAG,EAAE,kBAAkB,CAAC,UAAU,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,SAAS,EAC1G,GAAG,CAAC,EAAE,kBAAkB,CAAC,UAAU,EAAE,OAAO,CAAC,GAC9C,MAAM,CAgBR;AAGD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAE9D;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,OAAO,GAAG,MAAM,CAgBrF;AAID;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,EAAE,EAAE,aAAa,GAAG,MAAM,CAelD"}