@gobing-ai/ts-infra 0.3.4 → 0.3.6

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 (65) hide show
  1. package/README.md +196 -48
  2. package/dist/application/index.d.ts +55 -0
  3. package/dist/application/index.d.ts.map +1 -0
  4. package/dist/application/index.js +173 -0
  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 +195 -0
  18. package/dist/application/types.d.ts.map +1 -0
  19. package/dist/application/types.js +9 -0
  20. package/dist/application-node.d.ts +68 -0
  21. package/dist/application-node.d.ts.map +1 -0
  22. package/dist/application-node.js +228 -0
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/scheduler/cloudflare.d.ts.map +1 -1
  27. package/dist/scheduler/cloudflare.js +9 -2
  28. package/dist/scheduler/factory.d.ts +4 -8
  29. package/dist/scheduler/factory.d.ts.map +1 -1
  30. package/dist/scheduler/factory.js +12 -22
  31. package/dist/scheduler/index.d.ts +1 -1
  32. package/dist/scheduler/index.d.ts.map +1 -1
  33. package/dist/scheduler/index.js +1 -1
  34. package/dist/scheduler/wrap-handler.d.ts +8 -4
  35. package/dist/scheduler/wrap-handler.d.ts.map +1 -1
  36. package/dist/scheduler/wrap-handler.js +8 -4
  37. package/dist/telemetry/index.d.ts +1 -2
  38. package/dist/telemetry/index.d.ts.map +1 -1
  39. package/dist/telemetry/index.js +1 -2
  40. package/dist/telemetry/metrics.d.ts +9 -1
  41. package/dist/telemetry/metrics.d.ts.map +1 -1
  42. package/dist/telemetry/metrics.js +22 -1
  43. package/dist/telemetry/sdk.d.ts +33 -1
  44. package/dist/telemetry/sdk.d.ts.map +1 -1
  45. package/dist/telemetry/sdk.js +14 -1
  46. package/package.json +16 -3
  47. package/src/application/index.ts +248 -0
  48. package/src/application/plugins/builtins.ts +178 -0
  49. package/src/application/plugins/host.ts +143 -0
  50. package/src/application/plugins/index.ts +3 -0
  51. package/src/application/plugins/types.ts +86 -0
  52. package/src/application/types.ts +210 -0
  53. package/src/application-node.ts +311 -0
  54. package/src/index.ts +0 -2
  55. package/src/scheduler/cloudflare.ts +16 -5
  56. package/src/scheduler/factory.ts +15 -26
  57. package/src/scheduler/index.ts +1 -1
  58. package/src/scheduler/wrap-handler.ts +8 -4
  59. package/src/telemetry/index.ts +9 -2
  60. package/src/telemetry/metrics.ts +22 -1
  61. package/src/telemetry/sdk.ts +51 -2
  62. package/dist/telemetry/config.d.ts +0 -41
  63. package/dist/telemetry/config.d.ts.map +0 -1
  64. package/dist/telemetry/config.js +0 -21
  65. 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
 
@@ -419,6 +421,125 @@ bun add @opentelemetry/sdk-trace-node @opentelemetry/sdk-metrics \
419
421
  @opentelemetry/exporter-metrics-otlp-http
420
422
  ```
421
423
 
424
+
425
+ ### Application Bootstrap
426
+
427
+ The application bootstrap API provides a deterministic lifecycle for wiring
428
+ infrastructure services. Two layers:
429
+
430
+ 1. **Portable DI bootstrap** (`@gobing-ai/ts-infra/application`) — orchestrates
431
+ injected dependencies. Never opens files, creates DB connections, or wires
432
+ runtime-specific exporters.
433
+ 2. **Node/Bun convenience** (`@gobing-ai/ts-infra/application-node`) — composes
434
+ the portable bootstrap with runtime-specific adapters: YAML config loading,
435
+ file log sink, Bun SQLite DB creation, Node OTel export, Node scheduler.
436
+
437
+ #### Portable DI bootstrap
438
+
439
+ ```ts
440
+ import { runApplication } from '@gobing-ai/ts-infra/application';
441
+
442
+ const app = await runApplication({
443
+ config: {
444
+ logging: { level: 'info', console: true },
445
+ telemetry: { enabled: true, serviceName: 'my-api' },
446
+ },
447
+ appConfig: { port: 3000 },
448
+ async start(app) {
449
+ app.logger.info('started', { port: app.appConfig.port });
450
+ },
451
+ });
452
+
453
+ // Graceful shutdown — idempotent
454
+ await app.stop();
455
+ ```
456
+
457
+ The portable `runApplication` accepts pre-built services via `services` and
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:
461
+
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.
472
+
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.)
478
+
479
+ #### Node/Bun convenience bootstrap
480
+
481
+ ```ts
482
+ import { runNodeApplication } from '@gobing-ai/ts-infra/application-node';
483
+
484
+ await runNodeApplication({
485
+ configLoader: {
486
+ configFile: 'config/app.yaml',
487
+ bootstrapSection: 'bootstrap',
488
+ appSection: 'billing',
489
+ appConfig: {
490
+ // Structural safeParse adapter — works with Zod or any validator
491
+ safeParse(raw) {
492
+ return billingSchema.safeParse(raw);
493
+ },
494
+ },
495
+ },
496
+ async start(app) {
497
+ app.logger.info('billing app started', {
498
+ settlementWindowMinutes: app.appConfig.settlementWindowMinutes,
499
+ });
500
+ },
501
+ });
502
+ ```
503
+
504
+ Example YAML config file:
505
+
506
+ ```yaml
507
+ app:
508
+ name: billing-api
509
+ env: production
510
+
511
+ bootstrap:
512
+ logging:
513
+ level: info
514
+ console: true
515
+ filePath: ./logs/app.jsonl
516
+ telemetry:
517
+ enabled: true
518
+ serviceName: billing-api
519
+ endpoint: http://otel-collector:4318
520
+ database:
521
+ enabled: true
522
+ driver: bun-sqlite
523
+ url: ./data/app.db
524
+
525
+ billing:
526
+ settlementWindowMinutes: 15
527
+ riskLimit: 100000
528
+ ```
529
+
530
+ Config validators accept four shapes:
531
+ - `{ safeParse(raw) → { success, data?, errors? } }` — Zod-compatible
532
+ - `(raw) => TAppConfig` — bare function
533
+ - `{ validate(raw) => TAppConfig }` — method form
534
+ - `{ parse(raw) => TAppConfig }` — method form
535
+
536
+ Validation errors include the config file path and section name for diagnostics.
537
+
538
+ **What is intentionally NOT in the main barrel:** `runApplication` and
539
+ `runNodeApplication` live behind explicit subpaths so the main
540
+ `@gobing-ai/ts-infra` import stays portable and adapter-light. A future ADR
541
+ may decide whether type-only re-exports are acceptable.
542
+
422
543
  ## Usage
423
544
 
424
545
  ### Install
@@ -436,68 +557,95 @@ bun add @opentelemetry/sdk-trace-node @opentelemetry/sdk-metrics \
436
557
  @opentelemetry/exporter-metrics-otlp-http
437
558
  ```
438
559
 
439
- ### 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:
440
565
 
441
566
  ```ts
442
- import { createRuntimeContextFromFactory } from '@gobing-ai/ts-runtime';
443
- import { createDbAdapter, applyMigrations } from '@gobing-ai/ts-db';
444
- import {
445
- EventBus,
446
- setSchedulerAdapter,
447
- initTelemetry,
448
- APIClient,
449
- getLogger,
450
- initializeLogger,
451
- } from '@gobing-ai/ts-infra';
452
- import { NodeSchedulerAdapter } from '@gobing-ai/ts-infra/scheduler-node';
567
+ // Portable inject everything yourself:
568
+ import { runApplication } from '@gobing-ai/ts-infra/application';
453
569
 
454
- // 1. Runtime
455
- 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
+ });
456
583
 
457
- // 2. Database
458
- const db = await createDbAdapter({ driver: 'bun-sqlite', url: './data/app.db' });
459
- await applyMigrations(db);
460
- ctx.register('db', db);
584
+ await app.stop('signal');
585
+ ```
461
586
 
462
- // 3. Logging
463
- await initializeLogger({ level: 'info', console: true });
464
- 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';
590
+
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
+ ```
465
603
 
466
- // 4. Telemetry
467
- initTelemetry({ serviceName: 'my-app', environment: 'production' });
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.
468
607
 
469
- // 5. Event bus
470
- const bus = new EventBus<AppEvents>();
608
+ #### Custom plugins
471
609
 
472
- // 6. Scheduler
473
- const scheduler = new NodeSchedulerAdapter();
474
- setSchedulerAdapter(scheduler);
475
- scheduler.register('3600000', async () => {
476
- log.info('Hourly cleanup running');
477
- });
478
- await scheduler.start();
610
+ Plugins run in registration order forward (`loadAll → startAll`) and reverse order
611
+ on teardown (`stopAll unloadAll`):
612
+
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
+ };
479
626
 
480
- // 7. API client
481
- const stripeApi = new APIClient({
482
- baseUrl: 'https://api.stripe.com/v1',
483
- defaultHeaders: { Authorization: `Bearer ${process.env.STRIPE_KEY}` },
627
+ const app = await runApplication({
628
+ plugins: [auditPlugin],
629
+ config: { logging: { console: true } },
484
630
  });
631
+ ```
485
632
 
486
- 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)`.
487
637
  ```
488
638
 
489
639
  ### Graceful shutdown
490
640
 
491
- ```ts
492
- async function shutdown() {
493
- log.info('Shutting down...');
494
- await scheduler.stop();
495
- db.close();
496
- await shutdownTelemetry();
497
- await ctx.dispose();
498
- process.exit(0);
499
- }
641
+ With `runApplication` or `runNodeApplication`, shutdown is a single idempotent call:
500
642
 
501
- process.on('SIGTERM', shutdown);
502
- process.on('SIGINT', shutdown);
643
+ ```ts
644
+ process.on('SIGTERM', () => app.stop('signal'));
645
+ process.on('SIGINT', () => app.stop('signal'));
503
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.
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Portable `runApplication` — DI bootstrap over existing ts-infra primitives.
3
+ *
4
+ * Orchestrates logger, telemetry, event bus, (optional) DB adapter, and
5
+ * (optional) scheduler into a deterministic startup/shutdown lifecycle.
6
+ * The portable subpath never opens files, creates DB connections, or wires
7
+ * runtime-specific exporters — those are injected or handled by the
8
+ * Node/Bun convenience subpath.
9
+ *
10
+ * @module application
11
+ */
12
+ import type { EventMap } from '../event-bus/types';
13
+ import type { InfraEvents } from '../events';
14
+ import type { ApplicationBootstrapOptions, ApplicationRuntime } from './types';
15
+ /**
16
+ * Portable application bootstrap.
17
+ *
18
+ * Orchestrates logger, telemetry, events, optional DB, and optional scheduler.
19
+ * Accepts injected dependencies; never opens files, reads config from disk,
20
+ * or wires runtime-specific exporters.
21
+ *
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.
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * import { runApplication } from '@gobing-ai/ts-infra/application';
40
+ *
41
+ * const app = await runApplication({
42
+ * config: { logging: { level: 'debug' } },
43
+ * async start(app) {
44
+ * app.logger.info('started');
45
+ * },
46
+ * });
47
+ * ```
48
+ */
49
+ export declare function runApplication<TAppConfig = unknown, TEvents extends EventMap = InfraEvents>(options: ApplicationBootstrapOptions<TAppConfig, TEvents>): Promise<ApplicationRuntime<TAppConfig, TEvents>>;
50
+ export type { BusLifecycleEvents, EventMap } from '../event-bus/types';
51
+ export type { InfraEvents } from '../events';
52
+ export type { PluginHost } from './plugins/host';
53
+ export type { Plugin, PluginSummary } from './plugins/types';
54
+ export type { ApplicationBootstrapConfig, ApplicationBootstrapOptions, ApplicationConfigLoader, ApplicationConfigValidator, ApplicationRuntime, ApplicationServices, ApplicationStopReason, ConfigValidationResult, DbAdapterLike, EventsOptions, LoggingOptions, SchedulerOptions, TelemetryOptions, } from './types';
55
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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;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"}
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Portable `runApplication` — DI bootstrap over existing ts-infra primitives.
3
+ *
4
+ * Orchestrates logger, telemetry, event bus, (optional) DB adapter, and
5
+ * (optional) scheduler into a deterministic startup/shutdown lifecycle.
6
+ * The portable subpath never opens files, creates DB connections, or wires
7
+ * runtime-specific exporters — those are injected or handled by the
8
+ * Node/Bun convenience subpath.
9
+ *
10
+ * @module application
11
+ */
12
+ import { attachDefaultObservers, createLifecycleBus } from '../event-bus/default-observers.js';
13
+ import { EventBus } from '../event-bus/event-bus.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';
18
+ // ── Shutdown (deterministic reverse order per R5) ─────────────────────────
19
+ async function performShutdown(state, reason) {
20
+ if (state.stopped)
21
+ return;
22
+ state.stopped = true;
23
+ const app = state.app;
24
+ if (!app)
25
+ return;
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.
33
+ }
34
+ // ── Public API ────────────────────────────────────────────────────────────
35
+ /**
36
+ * Portable application bootstrap.
37
+ *
38
+ * Orchestrates logger, telemetry, events, optional DB, and optional scheduler.
39
+ * Accepts injected dependencies; never opens files, reads config from disk,
40
+ * or wires runtime-specific exporters.
41
+ *
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
48
+ *
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.
53
+ *
54
+ * If startup fails, the host's reverse-order `stopAll('error')`/`unloadAll('error')`
55
+ * tears down whatever started before rethrowing. `stop()` is idempotent.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * import { runApplication } from '@gobing-ai/ts-infra/application';
60
+ *
61
+ * const app = await runApplication({
62
+ * config: { logging: { level: 'debug' } },
63
+ * async start(app) {
64
+ * app.logger.info('started');
65
+ * },
66
+ * });
67
+ * ```
68
+ */
69
+ export async function runApplication(options) {
70
+ // ── Resolve config ─────────────────────────────────────────────────
71
+ const logOpts = options.config?.logging;
72
+ const loggingConfig = {
73
+ enabled: logOpts?.enabled ?? true,
74
+ level: logOpts?.level ?? 'info',
75
+ console: logOpts?.console ?? true,
76
+ json: logOpts?.json ?? true,
77
+ ...(logOpts?.fileSink ? { fileSink: logOpts.fileSink } : {}),
78
+ };
79
+ const telOpts = options.config?.telemetry;
80
+ const telemetryConfig = {
81
+ enabled: telOpts?.enabled ?? true,
82
+ serviceName: telOpts?.serviceName ?? 'ts-libs',
83
+ environment: telOpts?.environment ?? 'development',
84
+ dbStatementDebug: telOpts?.dbStatementDebug ?? false,
85
+ };
86
+ const schedOpts = options.config?.scheduler;
87
+ const schedulerConfig = {
88
+ enabled: schedOpts?.enabled ?? false,
89
+ autoStart: schedOpts?.autoStart ?? true,
90
+ };
91
+ const eventsEnabled = options.config?.events?.enabled ?? true;
92
+ const eventsLifecycle = options.config?.events?.lifecycle ?? true;
93
+ const eventsDefaultObservers = options.config?.events?.defaultObservers ?? true;
94
+ const state = {
95
+ app: undefined,
96
+ stopped: false,
97
+ pluginHost: undefined,
98
+ };
99
+ try {
100
+ // ── 1. Resolve logger (init deferred to loggerPlugin) ───────────
101
+ const logger = options.services?.logger ?? getLogger('bootstrap');
102
+ const loggerInjected = !!options.services?.logger;
103
+ // ── 3. Create lifecycle bus + EventBus ─────────────────────────
104
+ const lifecycleBus = eventsEnabled && eventsLifecycle ? (options.services?.lifecycleBus ?? createLifecycleBus()) : undefined;
105
+ if (lifecycleBus && eventsDefaultObservers) {
106
+ attachDefaultObservers(lifecycleBus);
107
+ }
108
+ const events = options.services?.events
109
+ ? options.services.events
110
+ : new EventBus({ lifecycleBus: lifecycleBus });
111
+ // ── 4. Database (injected only) ────────────────────────────────
112
+ const db = options.services?.db;
113
+ // ── 5. Scheduler ───────────────────────────────────────────────
114
+ let scheduler;
115
+ if (schedulerConfig.enabled) {
116
+ const adapter = options.services?.scheduler ?? schedOpts?.adapter;
117
+ scheduler = initScheduler(adapter, schedOpts?.entries);
118
+ }
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) ─
129
+ const resolvedConfig = {
130
+ logging: loggingConfig,
131
+ events: { enabled: eventsEnabled, lifecycle: eventsLifecycle, defaultObservers: eventsDefaultObservers },
132
+ telemetry: telemetryConfig,
133
+ scheduler: schedulerConfig,
134
+ };
135
+ const app = {
136
+ config: resolvedConfig,
137
+ appConfig: options.appConfig,
138
+ logger,
139
+ events,
140
+ lifecycleBus,
141
+ db,
142
+ scheduler,
143
+ pluginHost,
144
+ stop: (reason) => performShutdown(state, reason ?? 'manual'),
145
+ };
146
+ state.app = app;
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));
158
+ }
159
+ // ── Load + start (built-in failFast=rethrow on critical failure) ─
160
+ await pluginHost.loadAll();
161
+ await pluginHost.startAll();
162
+ return app;
163
+ }
164
+ catch (error) {
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');
171
+ throw error;
172
+ }
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"}