@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.
- package/README.md +196 -48
- package/dist/application/index.d.ts +55 -0
- package/dist/application/index.d.ts.map +1 -0
- package/dist/application/index.js +173 -0
- package/dist/application/plugins/builtins.d.ts +64 -0
- package/dist/application/plugins/builtins.d.ts.map +1 -0
- package/dist/application/plugins/builtins.js +146 -0
- package/dist/application/plugins/host.d.ts +61 -0
- package/dist/application/plugins/host.d.ts.map +1 -0
- package/dist/application/plugins/host.js +131 -0
- package/dist/application/plugins/index.d.ts +3 -0
- package/dist/application/plugins/index.d.ts.map +1 -0
- package/dist/application/plugins/index.js +2 -0
- package/dist/application/plugins/types.d.ts +69 -0
- package/dist/application/plugins/types.d.ts.map +1 -0
- package/dist/application/plugins/types.js +10 -0
- package/dist/application/types.d.ts +195 -0
- package/dist/application/types.d.ts.map +1 -0
- package/dist/application/types.js +9 -0
- package/dist/application-node.d.ts +68 -0
- package/dist/application-node.d.ts.map +1 -0
- package/dist/application-node.js +228 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/scheduler/cloudflare.d.ts.map +1 -1
- package/dist/scheduler/cloudflare.js +9 -2
- package/dist/scheduler/factory.d.ts +4 -8
- package/dist/scheduler/factory.d.ts.map +1 -1
- package/dist/scheduler/factory.js +12 -22
- package/dist/scheduler/index.d.ts +1 -1
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +1 -1
- package/dist/scheduler/wrap-handler.d.ts +8 -4
- package/dist/scheduler/wrap-handler.d.ts.map +1 -1
- package/dist/scheduler/wrap-handler.js +8 -4
- package/dist/telemetry/index.d.ts +1 -2
- package/dist/telemetry/index.d.ts.map +1 -1
- package/dist/telemetry/index.js +1 -2
- package/dist/telemetry/metrics.d.ts +9 -1
- package/dist/telemetry/metrics.d.ts.map +1 -1
- package/dist/telemetry/metrics.js +22 -1
- package/dist/telemetry/sdk.d.ts +33 -1
- package/dist/telemetry/sdk.d.ts.map +1 -1
- package/dist/telemetry/sdk.js +14 -1
- package/package.json +16 -3
- package/src/application/index.ts +248 -0
- package/src/application/plugins/builtins.ts +178 -0
- package/src/application/plugins/host.ts +143 -0
- package/src/application/plugins/index.ts +3 -0
- package/src/application/plugins/types.ts +86 -0
- package/src/application/types.ts +210 -0
- package/src/application-node.ts +311 -0
- package/src/index.ts +0 -2
- package/src/scheduler/cloudflare.ts +16 -5
- package/src/scheduler/factory.ts +15 -26
- package/src/scheduler/index.ts +1 -1
- package/src/scheduler/wrap-handler.ts +8 -4
- package/src/telemetry/index.ts +9 -2
- package/src/telemetry/metrics.ts +22 -1
- package/src/telemetry/sdk.ts +51 -2
- package/dist/telemetry/config.d.ts +0 -41
- package/dist/telemetry/config.d.ts.map +0 -1
- package/dist/telemetry/config.js +0 -21
- 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
|
-
|
|
443
|
-
import {
|
|
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
|
-
|
|
455
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
await applyMigrations(db);
|
|
460
|
-
ctx.register('db', db);
|
|
584
|
+
await app.stop('signal');
|
|
585
|
+
```
|
|
461
586
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
467
|
-
|
|
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
|
-
|
|
470
|
-
const bus = new EventBus<AppEvents>();
|
|
608
|
+
#### Custom plugins
|
|
471
609
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
478
|
-
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
502
|
-
process.on('
|
|
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"}
|