@gobing-ai/ts-infra 0.3.3 → 0.3.5
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 +113 -0
- package/dist/application/index.d.ts +57 -0
- package/dist/application/index.d.ts.map +1 -0
- package/dist/application/index.js +210 -0
- package/dist/application/types.d.ts +188 -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 +245 -0
- package/package.json +16 -3
- package/src/application/index.ts +278 -0
- package/src/application/types.ts +203 -0
- package/src/application-node.ts +329 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node/Bun convenience bootstrap for `@gobing-ai/ts-infra`.
|
|
3
|
+
*
|
|
4
|
+
* Composes the portable `runApplication` with runtime-specific wiring:
|
|
5
|
+
* - YAML config loading via `@gobing-ai/ts-runtime`
|
|
6
|
+
* - Application-specific config validation with caller-provided validator
|
|
7
|
+
* - File log sink via `node:fs`
|
|
8
|
+
* - Bun SQLite DB adapter creation from config
|
|
9
|
+
* - Optional Node OTel exporter initialization
|
|
10
|
+
* - Optional Node scheduler adapter
|
|
11
|
+
*
|
|
12
|
+
* This subpath may import runtime-specific adapters. The portable bootstrap
|
|
13
|
+
* subpath (`@gobing-ai/ts-infra/application`) must not.
|
|
14
|
+
*
|
|
15
|
+
* @module application-node
|
|
16
|
+
*/
|
|
17
|
+
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
18
|
+
import { dirname } from 'node:path';
|
|
19
|
+
import { createDbAdapter } from '@gobing-ai/ts-db';
|
|
20
|
+
import { interpolateTree, parseYamlObject } from '@gobing-ai/ts-runtime';
|
|
21
|
+
import { runApplication } from './application/index.js';
|
|
22
|
+
import { NodeSchedulerAdapter } from './scheduler-node.js';
|
|
23
|
+
import { initNodeTelemetry, shutdownNodeTelemetry } from './telemetry/otel-node.js';
|
|
24
|
+
// ── Errors ────────────────────────────────────────────────────────────────
|
|
25
|
+
/** Error thrown when application config validation fails. Includes file path and section name. */
|
|
26
|
+
export class ConfigValidationError extends Error {
|
|
27
|
+
constructor(message) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = 'ConfigValidationError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// ── Config validation helper ──────────────────────────────────────────────
|
|
33
|
+
/**
|
|
34
|
+
* Validate a raw config section using the caller-provided validator.
|
|
35
|
+
* Supports structural `safeParse`, `validate()`, `parse()`, and bare function forms.
|
|
36
|
+
*/
|
|
37
|
+
function validateAppConfig(validator, raw, section, filePath) {
|
|
38
|
+
if (typeof validator === 'object' && 'safeParse' in validator) {
|
|
39
|
+
const result = validator.safeParse(raw);
|
|
40
|
+
if (!result.success) {
|
|
41
|
+
const details = result.errors?.map((e) => `${e.path}: ${e.message}`).join('; ') ?? 'unknown error';
|
|
42
|
+
throw new ConfigValidationError(`Application config validation failed in section "${section}"` +
|
|
43
|
+
(filePath ? ` (file: ${filePath})` : '') +
|
|
44
|
+
`: ${details}`);
|
|
45
|
+
}
|
|
46
|
+
return result.data;
|
|
47
|
+
}
|
|
48
|
+
if (typeof validator === 'function') {
|
|
49
|
+
return validator(raw);
|
|
50
|
+
}
|
|
51
|
+
if (typeof validator === 'object' && 'validate' in validator) {
|
|
52
|
+
return validator.validate(raw);
|
|
53
|
+
}
|
|
54
|
+
if (typeof validator === 'object' && 'parse' in validator) {
|
|
55
|
+
return validator.parse(raw);
|
|
56
|
+
}
|
|
57
|
+
throw new ConfigValidationError(`Unsupported validator shape for section "${section}"`);
|
|
58
|
+
}
|
|
59
|
+
// ── File sink helper ──────────────────────────────────────────────────────
|
|
60
|
+
function createFileSink(filePath) {
|
|
61
|
+
return (line) => {
|
|
62
|
+
try {
|
|
63
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
64
|
+
appendFileSync(filePath, line);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Best-effort — don't crash on log write failure
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function loadYamlConfig(loader, fileContent) {
|
|
72
|
+
const bootstrapSection = loader.bootstrapSection ?? 'bootstrap';
|
|
73
|
+
let raw;
|
|
74
|
+
if (fileContent !== undefined) {
|
|
75
|
+
raw = parseYamlObject(fileContent);
|
|
76
|
+
}
|
|
77
|
+
else if (loader.overrides) {
|
|
78
|
+
raw = { ...loader.overrides };
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
raw = {};
|
|
82
|
+
}
|
|
83
|
+
// Interpolate environment variables (Node/Bun only)
|
|
84
|
+
raw = interpolateTree(raw);
|
|
85
|
+
// Extract bootstrap section
|
|
86
|
+
const bootstrapSection_ = raw[bootstrapSection];
|
|
87
|
+
const bootstrapConfig = typeof bootstrapSection_ === 'object' && bootstrapSection_ !== null
|
|
88
|
+
? bootstrapSection_
|
|
89
|
+
: {};
|
|
90
|
+
// Extract app config section
|
|
91
|
+
let appConfig;
|
|
92
|
+
if (loader.appConfig) {
|
|
93
|
+
const appSection = loader.appSection;
|
|
94
|
+
const appRaw = appSection
|
|
95
|
+
? raw[appSection]
|
|
96
|
+
: (() => {
|
|
97
|
+
// Default: full remaining object minus bootstrap section
|
|
98
|
+
const remaining = {};
|
|
99
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
100
|
+
if (key !== bootstrapSection) {
|
|
101
|
+
remaining[key] = value;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return remaining;
|
|
105
|
+
})();
|
|
106
|
+
appConfig = validateAppConfig(loader.appConfig, appRaw, appSection ?? '*', loader.configFile);
|
|
107
|
+
}
|
|
108
|
+
return { bootstrapConfig, appConfig };
|
|
109
|
+
}
|
|
110
|
+
// ── Public API ────────────────────────────────────────────────────────────
|
|
111
|
+
/**
|
|
112
|
+
* Node/Bun convenience application bootstrap.
|
|
113
|
+
*
|
|
114
|
+
* Extends the portable `runApplication` with:
|
|
115
|
+
* - YAML config file loading with section splitting
|
|
116
|
+
* - Application-specific config validation
|
|
117
|
+
* - File log sink creation from `logging.filePath`
|
|
118
|
+
* - Bun SQLite DB adapter creation when `database.enabled` + `database.driver`
|
|
119
|
+
* - Optional Node OTel telemetry exporter
|
|
120
|
+
* - Optional Node scheduler adapter
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```ts
|
|
124
|
+
* import { runNodeApplication } from '@gobing-ai/ts-infra/application-node';
|
|
125
|
+
*
|
|
126
|
+
* await runNodeApplication({
|
|
127
|
+
* configLoader: {
|
|
128
|
+
* configFile: 'config/app.yaml',
|
|
129
|
+
* bootstrapSection: 'bootstrap',
|
|
130
|
+
* appSection: 'billing',
|
|
131
|
+
* appConfig: {
|
|
132
|
+
* safeParse(raw) {
|
|
133
|
+
* return billingSchema.safeParse(raw);
|
|
134
|
+
* },
|
|
135
|
+
* },
|
|
136
|
+
* },
|
|
137
|
+
* async start(app) {
|
|
138
|
+
* app.logger.info('started');
|
|
139
|
+
* },
|
|
140
|
+
* });
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
export async function runNodeApplication(options) {
|
|
144
|
+
let nodeTelemetryInitialized = false;
|
|
145
|
+
// ── Load config ─────────────────────────────────────────────────────
|
|
146
|
+
let loadedAppConfig;
|
|
147
|
+
let yamlBootstrap = {};
|
|
148
|
+
if (options.configLoader?.configFile) {
|
|
149
|
+
const fileContent = readFileSync(options.configLoader.configFile, 'utf-8');
|
|
150
|
+
const loaded = loadYamlConfig(options.configLoader, fileContent);
|
|
151
|
+
yamlBootstrap = loaded.bootstrapConfig;
|
|
152
|
+
loadedAppConfig = loaded.appConfig;
|
|
153
|
+
}
|
|
154
|
+
else if (options.configLoader) {
|
|
155
|
+
const loaded = loadYamlConfig(options.configLoader, undefined);
|
|
156
|
+
yamlBootstrap = loaded.bootstrapConfig;
|
|
157
|
+
loadedAppConfig = loaded.appConfig;
|
|
158
|
+
}
|
|
159
|
+
// ── Resolve bootstrap config from YAML + inline options ────────────
|
|
160
|
+
// YAML loads as Record<string, unknown>; bridge into typed options.
|
|
161
|
+
// Inline options (options.config) take precedence over YAML sections.
|
|
162
|
+
const yamlLog = yamlBootstrap.logging;
|
|
163
|
+
const yamlTel = yamlBootstrap.telemetry;
|
|
164
|
+
const yamlSched = yamlBootstrap.scheduler;
|
|
165
|
+
const databaseOpts = (yamlBootstrap.database ?? {});
|
|
166
|
+
const loggingOpts = {
|
|
167
|
+
...yamlLog,
|
|
168
|
+
...options.config?.logging,
|
|
169
|
+
};
|
|
170
|
+
const telemetryOpts = {
|
|
171
|
+
...yamlTel,
|
|
172
|
+
...options.config?.telemetry,
|
|
173
|
+
};
|
|
174
|
+
const schedulerOpts = {
|
|
175
|
+
...yamlSched,
|
|
176
|
+
...options.config?.scheduler,
|
|
177
|
+
};
|
|
178
|
+
// File sink from logging.filePath
|
|
179
|
+
const logFilePath = yamlBootstrap.logging?.filePath;
|
|
180
|
+
const loggingConfig = typeof logFilePath === 'string' ? { ...loggingOpts, fileSink: createFileSink(logFilePath) } : loggingOpts;
|
|
181
|
+
// ── Node OTel telemetry ─────────────────────────────────────────────
|
|
182
|
+
const rawTel = { ...telemetryOpts };
|
|
183
|
+
if (rawTel.enabled !== false && rawTel.endpoint) {
|
|
184
|
+
initNodeTelemetry({
|
|
185
|
+
serviceName: rawTel.serviceName ?? 'ts-libs',
|
|
186
|
+
endpoint: rawTel.endpoint,
|
|
187
|
+
headers: rawTel.headers,
|
|
188
|
+
});
|
|
189
|
+
nodeTelemetryInitialized = true;
|
|
190
|
+
}
|
|
191
|
+
// ── DB adapter ──────────────────────────────────────────────────────
|
|
192
|
+
let dbAdapter = options.services?.db;
|
|
193
|
+
if (!dbAdapter && databaseOpts.enabled === true) {
|
|
194
|
+
const driver = databaseOpts.driver;
|
|
195
|
+
if (driver === 'bun-sqlite') {
|
|
196
|
+
const adapter = await createDbAdapter({
|
|
197
|
+
driver: 'bun-sqlite',
|
|
198
|
+
url: databaseOpts.url,
|
|
199
|
+
});
|
|
200
|
+
dbAdapter = adapter;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
throw new ConfigValidationError(`database.enabled is true but driver ${driver ? `"${driver}"` : 'is missing'} is not supported ` +
|
|
204
|
+
`(expected "bun-sqlite"). Provide a supported driver or inject a DbAdapter via services.db.`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// ── Scheduler adapter ───────────────────────────────────────────────
|
|
208
|
+
const schedulerConfig = {};
|
|
209
|
+
const rawSched = { ...schedulerOpts };
|
|
210
|
+
if (rawSched.enabled === true) {
|
|
211
|
+
schedulerConfig.enabled = true;
|
|
212
|
+
schedulerConfig.autoStart = schedulerOpts.autoStart;
|
|
213
|
+
// Use Node scheduler adapter by default in this subpath
|
|
214
|
+
schedulerConfig.adapter = new NodeSchedulerAdapter();
|
|
215
|
+
}
|
|
216
|
+
// ── Delegate to portable runApplication ─────────────────────────────
|
|
217
|
+
const app = await runApplication({
|
|
218
|
+
config: {
|
|
219
|
+
...options.config,
|
|
220
|
+
logging: loggingConfig,
|
|
221
|
+
telemetry: telemetryOpts,
|
|
222
|
+
scheduler: schedulerConfig,
|
|
223
|
+
},
|
|
224
|
+
appConfig: loadedAppConfig,
|
|
225
|
+
services: {
|
|
226
|
+
...options.services,
|
|
227
|
+
...(dbAdapter ? { db: dbAdapter } : {}),
|
|
228
|
+
},
|
|
229
|
+
start: options.start,
|
|
230
|
+
stop: options.stop,
|
|
231
|
+
});
|
|
232
|
+
// ── Compose a handle with Node-specific cleanup on stop ─────────────
|
|
233
|
+
const originalStop = app.stop.bind(app);
|
|
234
|
+
return {
|
|
235
|
+
...app,
|
|
236
|
+
stop: async (reason) => {
|
|
237
|
+
await originalStop(reason);
|
|
238
|
+
// Node-specific cleanup (after portable shutdown):
|
|
239
|
+
// 5. Shut down Node telemetry exporter
|
|
240
|
+
if (nodeTelemetryInitialized) {
|
|
241
|
+
await shutdownNodeTelemetry();
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/ts-infra",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "@gobing-ai/ts-infra — Infrastructure backbone: event bus, job queue, scheduler, telemetry, API client, and logging.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -47,6 +47,14 @@
|
|
|
47
47
|
"./scheduler-node": {
|
|
48
48
|
"types": "./dist/scheduler-node.d.ts",
|
|
49
49
|
"import": "./dist/scheduler-node.js"
|
|
50
|
+
},
|
|
51
|
+
"./application": {
|
|
52
|
+
"types": "./dist/application/index.d.ts",
|
|
53
|
+
"import": "./dist/application/index.js"
|
|
54
|
+
},
|
|
55
|
+
"./application-node": {
|
|
56
|
+
"types": "./dist/application-node.d.ts",
|
|
57
|
+
"import": "./dist/application-node.js"
|
|
50
58
|
}
|
|
51
59
|
},
|
|
52
60
|
"files": [
|
|
@@ -69,7 +77,8 @@
|
|
|
69
77
|
"@logtape/logtape": "^2.0.0"
|
|
70
78
|
},
|
|
71
79
|
"peerDependencies": {
|
|
72
|
-
"@gobing-ai/ts-db": "^0.3.
|
|
80
|
+
"@gobing-ai/ts-db": "^0.3.5",
|
|
81
|
+
"@gobing-ai/ts-runtime": "^0.3.5",
|
|
73
82
|
"@opentelemetry/api": "^1.9.0",
|
|
74
83
|
"@opentelemetry/sdk-trace-node": "^2.0.0",
|
|
75
84
|
"@opentelemetry/sdk-metrics": "^2.0.0",
|
|
@@ -82,6 +91,9 @@
|
|
|
82
91
|
"@gobing-ai/ts-db": {
|
|
83
92
|
"optional": true
|
|
84
93
|
},
|
|
94
|
+
"@gobing-ai/ts-runtime": {
|
|
95
|
+
"optional": true
|
|
96
|
+
},
|
|
85
97
|
"@opentelemetry/sdk-trace-node": {
|
|
86
98
|
"optional": true
|
|
87
99
|
},
|
|
@@ -99,7 +111,8 @@
|
|
|
99
111
|
}
|
|
100
112
|
},
|
|
101
113
|
"devDependencies": {
|
|
102
|
-
"@gobing-ai/ts-db": "^0.3.
|
|
114
|
+
"@gobing-ai/ts-db": "^0.3.5",
|
|
115
|
+
"@gobing-ai/ts-runtime": "^0.3.5",
|
|
103
116
|
"@types/bun": "1.3.14",
|
|
104
117
|
"@opentelemetry/api": "^1.9.0",
|
|
105
118
|
"@opentelemetry/sdk-trace-node": "^2.0.0",
|
|
@@ -0,0 +1,278 @@
|
|
|
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
|
+
|
|
13
|
+
import { attachDefaultObservers, createLifecycleBus } from '../event-bus/default-observers';
|
|
14
|
+
import { EventBus } from '../event-bus/event-bus';
|
|
15
|
+
import type { BusLifecycleEvents, EventMap } from '../event-bus/types';
|
|
16
|
+
import type { InfraEvents } from '../events';
|
|
17
|
+
import { getLogger, initializeLogger, type Logger } from '../logger';
|
|
18
|
+
import { initScheduler, setSchedulerAdapter } from '../scheduler/factory';
|
|
19
|
+
import type { SchedulerAdapter } from '../scheduler/types';
|
|
20
|
+
import { initTelemetry, shutdownTelemetry } from '../telemetry/sdk';
|
|
21
|
+
import type {
|
|
22
|
+
ApplicationBootstrapConfig,
|
|
23
|
+
ApplicationBootstrapOptions,
|
|
24
|
+
ApplicationRuntime,
|
|
25
|
+
ApplicationStopReason,
|
|
26
|
+
DbAdapterLike,
|
|
27
|
+
} from './types';
|
|
28
|
+
|
|
29
|
+
// ── Internal runtime state ────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
interface RuntimeState<TAppConfig, TEvents extends EventMap> {
|
|
32
|
+
app: ApplicationRuntime<TAppConfig, TEvents> | undefined;
|
|
33
|
+
userStop?: (app: ApplicationRuntime<TAppConfig, TEvents>, reason: ApplicationStopReason) => Promise<void> | void;
|
|
34
|
+
schedulerAdapter?: SchedulerAdapter;
|
|
35
|
+
schedulerStarted: boolean;
|
|
36
|
+
loggerInitialized: boolean;
|
|
37
|
+
telemetryInitialized: boolean;
|
|
38
|
+
stopped: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Shutdown (deterministic reverse order per R5) ─────────────────────────
|
|
42
|
+
|
|
43
|
+
async function performShutdown<TAppConfig, TEvents extends EventMap>(
|
|
44
|
+
state: RuntimeState<TAppConfig, TEvents>,
|
|
45
|
+
reason: ApplicationStopReason,
|
|
46
|
+
): Promise<void> {
|
|
47
|
+
if (state.stopped) return;
|
|
48
|
+
state.stopped = true;
|
|
49
|
+
|
|
50
|
+
const app = state.app;
|
|
51
|
+
if (!app) return;
|
|
52
|
+
|
|
53
|
+
// 1. User stop callback
|
|
54
|
+
if (state.userStop) {
|
|
55
|
+
await state.userStop(app, reason);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 2. Stop scheduler
|
|
59
|
+
if (state.schedulerStarted && state.schedulerAdapter) {
|
|
60
|
+
await state.schedulerAdapter.stop().catch(() => {});
|
|
61
|
+
state.schedulerStarted = false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 3. Close DB adapter
|
|
65
|
+
if (app.db) {
|
|
66
|
+
try {
|
|
67
|
+
app.db.close();
|
|
68
|
+
} catch {
|
|
69
|
+
/* best-effort */
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 4. Shutdown telemetry
|
|
74
|
+
if (state.telemetryInitialized) {
|
|
75
|
+
await shutdownTelemetry();
|
|
76
|
+
state.telemetryInitialized = false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Public API ────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Portable application bootstrap.
|
|
84
|
+
*
|
|
85
|
+
* Orchestrates logger, telemetry, events, optional DB, and optional scheduler.
|
|
86
|
+
* Accepts injected dependencies; never opens files, reads config from disk,
|
|
87
|
+
* or wires runtime-specific exporters.
|
|
88
|
+
*
|
|
89
|
+
* Startup order (deterministic, per R5):
|
|
90
|
+
* 1. Resolve bootstrap config + app config
|
|
91
|
+
* 2. Initialize logger
|
|
92
|
+
* 3. Initialize telemetry
|
|
93
|
+
* 4. Create lifecycle bus + application EventBus
|
|
94
|
+
* 5. Register DB adapter (injected only)
|
|
95
|
+
* 6. Initialize scheduler + register entries
|
|
96
|
+
* 7. Call user `start(app)` callback
|
|
97
|
+
* 8. Start scheduler if `autoStart`
|
|
98
|
+
*
|
|
99
|
+
* Shutdown order (reverse, per R5):
|
|
100
|
+
* 1. User `stop(app, reason)` callback
|
|
101
|
+
* 2. Stop scheduler
|
|
102
|
+
* 3. Close DB adapter
|
|
103
|
+
* 4. Shut down telemetry
|
|
104
|
+
*
|
|
105
|
+
* If any startup step fails, already-started services are cleaned up in reverse
|
|
106
|
+
* order before rethrowing. `stop()` is idempotent.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```ts
|
|
110
|
+
* import { runApplication } from '@gobing-ai/ts-infra/application';
|
|
111
|
+
*
|
|
112
|
+
* const app = await runApplication({
|
|
113
|
+
* config: { logging: { level: 'debug' } },
|
|
114
|
+
* async start(app) {
|
|
115
|
+
* app.logger.info('started');
|
|
116
|
+
* },
|
|
117
|
+
* });
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export async function runApplication<TAppConfig = unknown, TEvents extends EventMap = InfraEvents>(
|
|
121
|
+
options: ApplicationBootstrapOptions<TAppConfig, TEvents>,
|
|
122
|
+
): Promise<ApplicationRuntime<TAppConfig, TEvents>> {
|
|
123
|
+
// ── Resolve config ─────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
const logOpts = options.config?.logging;
|
|
126
|
+
const loggingConfig: ApplicationBootstrapConfig['logging'] = {
|
|
127
|
+
enabled: logOpts?.enabled ?? true,
|
|
128
|
+
level: logOpts?.level ?? 'info',
|
|
129
|
+
console: logOpts?.console ?? true,
|
|
130
|
+
json: logOpts?.json ?? true,
|
|
131
|
+
...(logOpts?.fileSink ? { fileSink: logOpts.fileSink } : {}),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const telOpts = options.config?.telemetry;
|
|
135
|
+
const telemetryConfig: ApplicationBootstrapConfig['telemetry'] = {
|
|
136
|
+
enabled: telOpts?.enabled ?? true,
|
|
137
|
+
serviceName: telOpts?.serviceName ?? 'ts-libs',
|
|
138
|
+
environment: telOpts?.environment ?? 'development',
|
|
139
|
+
dbStatementDebug: telOpts?.dbStatementDebug ?? false,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const schedOpts = options.config?.scheduler;
|
|
143
|
+
const schedulerConfig: ApplicationBootstrapConfig['scheduler'] = {
|
|
144
|
+
enabled: schedOpts?.enabled ?? false,
|
|
145
|
+
autoStart: schedOpts?.autoStart ?? true,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const eventsEnabled = options.config?.events?.enabled ?? true;
|
|
149
|
+
const eventsLifecycle = options.config?.events?.lifecycle ?? true;
|
|
150
|
+
const eventsDefaultObservers = options.config?.events?.defaultObservers ?? true;
|
|
151
|
+
|
|
152
|
+
const state: RuntimeState<TAppConfig, TEvents> = {
|
|
153
|
+
app: undefined,
|
|
154
|
+
userStop: options.stop,
|
|
155
|
+
schedulerAdapter: undefined,
|
|
156
|
+
schedulerStarted: false,
|
|
157
|
+
loggerInitialized: false,
|
|
158
|
+
telemetryInitialized: false,
|
|
159
|
+
stopped: false,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
// ── 1. Initialize logger ────────────────────────────────────────
|
|
164
|
+
let logger: Logger;
|
|
165
|
+
if (options.services?.logger) {
|
|
166
|
+
logger = options.services.logger;
|
|
167
|
+
} else if (loggingConfig.enabled) {
|
|
168
|
+
await initializeLogger({
|
|
169
|
+
level: loggingConfig.level,
|
|
170
|
+
console: loggingConfig.console,
|
|
171
|
+
fileSink: loggingConfig.fileSink,
|
|
172
|
+
json: loggingConfig.json,
|
|
173
|
+
});
|
|
174
|
+
logger = getLogger('bootstrap');
|
|
175
|
+
} else {
|
|
176
|
+
logger = getLogger('bootstrap');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── 2. Initialize telemetry ────────────────────────────────────
|
|
180
|
+
if (telemetryConfig.enabled) {
|
|
181
|
+
initTelemetry({
|
|
182
|
+
enabled: telemetryConfig.enabled,
|
|
183
|
+
serviceName: telemetryConfig.serviceName,
|
|
184
|
+
environment: telemetryConfig.environment,
|
|
185
|
+
dbStatementDebug: telemetryConfig.dbStatementDebug,
|
|
186
|
+
});
|
|
187
|
+
state.telemetryInitialized = true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── 3. Create lifecycle bus + EventBus ─────────────────────────
|
|
191
|
+
const lifecycleBus =
|
|
192
|
+
eventsEnabled && eventsLifecycle ? (options.services?.lifecycleBus ?? createLifecycleBus()) : undefined;
|
|
193
|
+
|
|
194
|
+
if (lifecycleBus && eventsDefaultObservers) {
|
|
195
|
+
attachDefaultObservers(lifecycleBus);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const events = options.services?.events
|
|
199
|
+
? options.services.events
|
|
200
|
+
: new EventBus<TEvents>({ lifecycleBus: lifecycleBus as EventBus<BusLifecycleEvents> | undefined });
|
|
201
|
+
|
|
202
|
+
// ── 4. Database (injected only) ────────────────────────────────
|
|
203
|
+
const db: DbAdapterLike | undefined = options.services?.db;
|
|
204
|
+
|
|
205
|
+
// ── 5. Scheduler ───────────────────────────────────────────────
|
|
206
|
+
let scheduler: SchedulerAdapter | undefined;
|
|
207
|
+
if (schedulerConfig.enabled) {
|
|
208
|
+
const adapter = options.services?.scheduler ?? schedOpts?.adapter;
|
|
209
|
+
if (adapter) {
|
|
210
|
+
setSchedulerAdapter(adapter);
|
|
211
|
+
}
|
|
212
|
+
scheduler = initScheduler(schedOpts?.entries);
|
|
213
|
+
state.schedulerAdapter = scheduler;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── Build resolved config ──────────────────────────────────────
|
|
217
|
+
const resolvedConfig: ApplicationBootstrapConfig = {
|
|
218
|
+
logging: loggingConfig,
|
|
219
|
+
events: { enabled: eventsEnabled, lifecycle: eventsLifecycle, defaultObservers: eventsDefaultObservers },
|
|
220
|
+
telemetry: telemetryConfig,
|
|
221
|
+
scheduler: schedulerConfig,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// ── Build runtime handle ───────────────────────────────────────
|
|
225
|
+
const app: ApplicationRuntime<TAppConfig, TEvents> = {
|
|
226
|
+
config: resolvedConfig,
|
|
227
|
+
appConfig: options.appConfig as TAppConfig,
|
|
228
|
+
logger,
|
|
229
|
+
events,
|
|
230
|
+
lifecycleBus,
|
|
231
|
+
db,
|
|
232
|
+
scheduler,
|
|
233
|
+
stop: (reason?: ApplicationStopReason) => performShutdown(state, reason ?? 'manual'),
|
|
234
|
+
};
|
|
235
|
+
state.app = app;
|
|
236
|
+
|
|
237
|
+
// ── 6. User start callback ─────────────────────────────────────
|
|
238
|
+
await options.start(app);
|
|
239
|
+
|
|
240
|
+
// ── 7. Start scheduler ─────────────────────────────────────────
|
|
241
|
+
if (schedulerConfig.enabled && schedulerConfig.autoStart && scheduler) {
|
|
242
|
+
await scheduler.start();
|
|
243
|
+
state.schedulerStarted = true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return app;
|
|
247
|
+
} catch (error) {
|
|
248
|
+
// Reverse-order cleanup of services this bootstrap owns.
|
|
249
|
+
// Scheduler: start() is the last async op before return; if it throws,
|
|
250
|
+
// schedulerStarted is still false, so there is nothing started to stop.
|
|
251
|
+
// DB: injected by the caller (portable bootstrap never creates one), so
|
|
252
|
+
// its lifecycle is caller-owned and not closed here.
|
|
253
|
+
// Telemetry: owned by the bootstrap — shut it down if it was initialized.
|
|
254
|
+
if (state.telemetryInitialized) {
|
|
255
|
+
await shutdownTelemetry();
|
|
256
|
+
}
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export type { BusLifecycleEvents, EventMap } from '../event-bus/types';
|
|
262
|
+
export type { InfraEvents } from '../events';
|
|
263
|
+
// Re-export types
|
|
264
|
+
export type {
|
|
265
|
+
ApplicationBootstrapConfig,
|
|
266
|
+
ApplicationBootstrapOptions,
|
|
267
|
+
ApplicationConfigLoader,
|
|
268
|
+
ApplicationConfigValidator,
|
|
269
|
+
ApplicationRuntime,
|
|
270
|
+
ApplicationServices,
|
|
271
|
+
ApplicationStopReason,
|
|
272
|
+
ConfigValidationResult,
|
|
273
|
+
DbAdapterLike,
|
|
274
|
+
EventsOptions,
|
|
275
|
+
LoggingOptions,
|
|
276
|
+
SchedulerOptions,
|
|
277
|
+
TelemetryOptions,
|
|
278
|
+
} from './types';
|