@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,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portable application bootstrap types.
|
|
3
|
+
*
|
|
4
|
+
* These types define the DI contract for `runApplication` — a thin orchestration
|
|
5
|
+
* layer over existing ts-infra primitives. The portable subpath does not import
|
|
6
|
+
* any runtime-specific adapters; everything injectable comes through the options.
|
|
7
|
+
*
|
|
8
|
+
* @module application/types
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { EventBus } from '../event-bus/event-bus';
|
|
12
|
+
import type { BusLifecycleEvents, EventMap } from '../event-bus/types';
|
|
13
|
+
import type { InfraEvents } from '../events';
|
|
14
|
+
import type { Logger, LogLevel } from '../logger';
|
|
15
|
+
import type { SchedulerAdapter } from '../scheduler/types';
|
|
16
|
+
|
|
17
|
+
// ── Feature flag option groups ────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/** Logging feature flags. */
|
|
20
|
+
export interface LoggingOptions {
|
|
21
|
+
/** Enable logging. Default `true`. */
|
|
22
|
+
enabled?: boolean;
|
|
23
|
+
/** Minimum log level. Default `'info'`. */
|
|
24
|
+
level?: LogLevel;
|
|
25
|
+
/** Enable console output. Default `true`. */
|
|
26
|
+
console?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* File sink writer. The portable bootstrap never opens files — the caller
|
|
29
|
+
* (or Node convenience subpath) provides a writer.
|
|
30
|
+
*/
|
|
31
|
+
fileSink?: (line: string) => void;
|
|
32
|
+
/** JSON Lines format. Default `true`. */
|
|
33
|
+
json?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Event bus feature flags. */
|
|
37
|
+
export interface EventsOptions<TEvents extends EventMap = InfraEvents> {
|
|
38
|
+
/** Enable event bus. Default `true`. */
|
|
39
|
+
enabled?: boolean;
|
|
40
|
+
/** Create and attach a lifecycle bus. Default `true`. */
|
|
41
|
+
lifecycle?: boolean;
|
|
42
|
+
/** Attach default observers (log + telemetry). Default `true`. */
|
|
43
|
+
defaultObservers?: boolean;
|
|
44
|
+
/** Pre-built event bus (skips creation when provided). */
|
|
45
|
+
bus?: EventBus<TEvents>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Telemetry feature flags. */
|
|
49
|
+
export interface TelemetryOptions {
|
|
50
|
+
/** Enable telemetry instrumentation. Default `true`. */
|
|
51
|
+
enabled?: boolean;
|
|
52
|
+
/** Service name for spans. Default `'ts-libs'`. */
|
|
53
|
+
serviceName?: string;
|
|
54
|
+
/** Deployment environment. Default `'development'`. */
|
|
55
|
+
environment?: string;
|
|
56
|
+
/** Capture sanitized SQL in DB spans. Default `false`. */
|
|
57
|
+
dbStatementDebug?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Scheduler feature flags. */
|
|
61
|
+
export interface SchedulerOptions {
|
|
62
|
+
/** Enable scheduler. Default `false`. */
|
|
63
|
+
enabled?: boolean;
|
|
64
|
+
/** Injected adapter (skips noop default when provided). */
|
|
65
|
+
adapter?: SchedulerAdapter;
|
|
66
|
+
/** Cron entries to register: `[cron, action][]`. */
|
|
67
|
+
entries?: Array<[string, () => Promise<void>]>;
|
|
68
|
+
/** Start scheduler immediately after registration. Default `true` when enabled. */
|
|
69
|
+
autoStart?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Resolved bootstrap config ─────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Fully-resolved bootstrap config (all optionals filled with defaults).
|
|
76
|
+
* Constructed internally by `resolveBootstrapConfig`.
|
|
77
|
+
*/
|
|
78
|
+
export interface ApplicationBootstrapConfig {
|
|
79
|
+
readonly logging: Readonly<
|
|
80
|
+
Required<Pick<LoggingOptions, 'enabled' | 'level' | 'console' | 'json'>> & { fileSink?: (line: string) => void }
|
|
81
|
+
>;
|
|
82
|
+
readonly events: { enabled: boolean; lifecycle: boolean; defaultObservers: boolean };
|
|
83
|
+
readonly telemetry: { enabled: boolean; serviceName: string; environment: string; dbStatementDebug: boolean };
|
|
84
|
+
readonly scheduler: { enabled: boolean; autoStart: boolean };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Injected services ─────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
/** Services that may be pre-injected instead of created by the bootstrap. */
|
|
90
|
+
export interface ApplicationServices<TEvents extends EventMap = InfraEvents> {
|
|
91
|
+
logger?: Logger;
|
|
92
|
+
events?: EventBus<TEvents>;
|
|
93
|
+
lifecycleBus?: EventBus<BusLifecycleEvents>;
|
|
94
|
+
db?: DbAdapterLike;
|
|
95
|
+
scheduler?: SchedulerAdapter;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Minimal DB adapter shape the bootstrap cares about: `close()` for lifecycle.
|
|
100
|
+
* This avoids importing `ts-db` (an optional peer) from the portable subpath.
|
|
101
|
+
*/
|
|
102
|
+
export interface DbAdapterLike {
|
|
103
|
+
close(): void;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Stop reason ───────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
/** Why the application is stopping. */
|
|
109
|
+
export type ApplicationStopReason = 'manual' | 'signal' | 'error' | 'shutdown';
|
|
110
|
+
|
|
111
|
+
// ── Runtime handle ────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Runtime handle returned by `runApplication`.
|
|
115
|
+
*
|
|
116
|
+
* Exposes all resolved services and a `stop()` method for graceful shutdown.
|
|
117
|
+
*/
|
|
118
|
+
export interface ApplicationRuntime<TAppConfig = unknown, TEvents extends EventMap = InfraEvents> {
|
|
119
|
+
/** Fully-resolved bootstrap config (feature flags + defaults). */
|
|
120
|
+
readonly config: ApplicationBootstrapConfig;
|
|
121
|
+
/** Caller-provided application config, or `undefined`. */
|
|
122
|
+
readonly appConfig: TAppConfig;
|
|
123
|
+
/** Structured logger. */
|
|
124
|
+
readonly logger: Logger;
|
|
125
|
+
/** Application event bus. */
|
|
126
|
+
readonly events: EventBus<TEvents>;
|
|
127
|
+
/** Lifecycle bus (when enabled). */
|
|
128
|
+
readonly lifecycleBus?: EventBus<BusLifecycleEvents>;
|
|
129
|
+
/** DB adapter (when enabled and injected). */
|
|
130
|
+
readonly db?: DbAdapterLike;
|
|
131
|
+
/** Scheduler adapter (when enabled). */
|
|
132
|
+
readonly scheduler?: SchedulerAdapter;
|
|
133
|
+
/** Graceful shutdown. Idempotent — safe to call multiple times. */
|
|
134
|
+
stop(reason?: ApplicationStopReason): Promise<void>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── Options ───────────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Options for the portable `runApplication`.
|
|
141
|
+
*/
|
|
142
|
+
export interface ApplicationBootstrapOptions<TAppConfig = unknown, TEvents extends EventMap = InfraEvents> {
|
|
143
|
+
/** Bootstrap feature flags (partial — defaults applied internally). */
|
|
144
|
+
readonly config?: {
|
|
145
|
+
logging?: LoggingOptions;
|
|
146
|
+
events?: EventsOptions<TEvents>;
|
|
147
|
+
telemetry?: TelemetryOptions;
|
|
148
|
+
scheduler?: SchedulerOptions;
|
|
149
|
+
};
|
|
150
|
+
/** Already-resolved application config. Portable — no file reads. */
|
|
151
|
+
readonly appConfig?: TAppConfig;
|
|
152
|
+
/** Pre-built services to inject instead of creating defaults. */
|
|
153
|
+
readonly services?: Partial<ApplicationServices<TEvents>>;
|
|
154
|
+
/** User callback: application logic. Called after all services are ready. */
|
|
155
|
+
readonly start: (app: ApplicationRuntime<TAppConfig, TEvents>) => Promise<void> | void;
|
|
156
|
+
/** User callback: cleanup before services shut down. */
|
|
157
|
+
readonly stop?: (
|
|
158
|
+
app: ApplicationRuntime<TAppConfig, TEvents>,
|
|
159
|
+
reason: ApplicationStopReason,
|
|
160
|
+
) => Promise<void> | void;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── Config validation (Node/Bun convenience subpath) ──────────────────────
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Validation result matching the structural `safeParse` pattern.
|
|
167
|
+
* Avoids hard-coupling to a specific validation library.
|
|
168
|
+
*/
|
|
169
|
+
export interface ConfigValidationResult<T> {
|
|
170
|
+
readonly success: boolean;
|
|
171
|
+
readonly data?: T;
|
|
172
|
+
readonly errors?: ReadonlyArray<{ path: string; message: string }>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Config validator: structural `safeParse`-compatible adapter.
|
|
177
|
+
* Accepts any validation library that produces `{ success, data?, errors? }`.
|
|
178
|
+
*/
|
|
179
|
+
export type ApplicationConfigValidator<TAppConfig> =
|
|
180
|
+
| { safeParse(raw: unknown): ConfigValidationResult<TAppConfig> }
|
|
181
|
+
| ((raw: unknown) => TAppConfig)
|
|
182
|
+
| { validate(raw: unknown): TAppConfig }
|
|
183
|
+
| { parse(raw: unknown): TAppConfig };
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Config loader options for the Node/Bun convenience subpath.
|
|
187
|
+
*/
|
|
188
|
+
export interface ApplicationConfigLoader<TAppConfig = unknown> {
|
|
189
|
+
/** Path to the YAML config file. */
|
|
190
|
+
readonly configFile?: string;
|
|
191
|
+
/** YAML section name for bootstrap config. Default `'bootstrap'`. */
|
|
192
|
+
readonly bootstrapSection?: string;
|
|
193
|
+
/** YAML section name for app-specific config. Default: remaining object. */
|
|
194
|
+
readonly appSection?: string;
|
|
195
|
+
/** Caller-provided validator for the app config section. */
|
|
196
|
+
readonly appConfig?: ApplicationConfigValidator<TAppConfig>;
|
|
197
|
+
/** Override values merged after loading. */
|
|
198
|
+
readonly overrides?: Record<string, unknown>;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Re-export event types needed by consumers
|
|
202
|
+
export type { BusLifecycleEvents, EventMap } from '../event-bus/types';
|
|
203
|
+
export type { InfraEvents } from '../events';
|
|
@@ -0,0 +1,329 @@
|
|
|
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
|
+
|
|
18
|
+
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
19
|
+
import { dirname } from 'node:path';
|
|
20
|
+
|
|
21
|
+
import { createDbAdapter, type DbAdapter } from '@gobing-ai/ts-db';
|
|
22
|
+
import { interpolateTree, parseYamlObject } from '@gobing-ai/ts-runtime';
|
|
23
|
+
|
|
24
|
+
import { runApplication } from './application/index';
|
|
25
|
+
import type {
|
|
26
|
+
ApplicationBootstrapOptions,
|
|
27
|
+
ApplicationConfigLoader,
|
|
28
|
+
ApplicationConfigValidator,
|
|
29
|
+
ApplicationRuntime,
|
|
30
|
+
ApplicationStopReason,
|
|
31
|
+
ConfigValidationResult,
|
|
32
|
+
DbAdapterLike,
|
|
33
|
+
EventMap,
|
|
34
|
+
InfraEvents,
|
|
35
|
+
LoggingOptions,
|
|
36
|
+
SchedulerOptions,
|
|
37
|
+
TelemetryOptions,
|
|
38
|
+
} from './application/types';
|
|
39
|
+
import { NodeSchedulerAdapter } from './scheduler-node';
|
|
40
|
+
import { initNodeTelemetry, shutdownNodeTelemetry } from './telemetry/otel-node';
|
|
41
|
+
|
|
42
|
+
// ── Errors ────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
/** Error thrown when application config validation fails. Includes file path and section name. */
|
|
45
|
+
export class ConfigValidationError extends Error {
|
|
46
|
+
constructor(message: string) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = 'ConfigValidationError';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── Config validation helper ──────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validate a raw config section using the caller-provided validator.
|
|
56
|
+
* Supports structural `safeParse`, `validate()`, `parse()`, and bare function forms.
|
|
57
|
+
*/
|
|
58
|
+
function validateAppConfig<TAppConfig>(
|
|
59
|
+
validator: ApplicationConfigValidator<TAppConfig>,
|
|
60
|
+
raw: unknown,
|
|
61
|
+
section: string,
|
|
62
|
+
filePath: string | undefined,
|
|
63
|
+
): TAppConfig {
|
|
64
|
+
if (typeof validator === 'object' && 'safeParse' in validator) {
|
|
65
|
+
const result: ConfigValidationResult<TAppConfig> = validator.safeParse(raw);
|
|
66
|
+
if (!result.success) {
|
|
67
|
+
const details = result.errors?.map((e) => `${e.path}: ${e.message}`).join('; ') ?? 'unknown error';
|
|
68
|
+
throw new ConfigValidationError(
|
|
69
|
+
`Application config validation failed in section "${section}"` +
|
|
70
|
+
(filePath ? ` (file: ${filePath})` : '') +
|
|
71
|
+
`: ${details}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return result.data as TAppConfig;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (typeof validator === 'function') {
|
|
78
|
+
return (validator as (raw: unknown) => TAppConfig)(raw);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (typeof validator === 'object' && 'validate' in validator) {
|
|
82
|
+
return validator.validate(raw);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (typeof validator === 'object' && 'parse' in validator) {
|
|
86
|
+
return validator.parse(raw);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
throw new ConfigValidationError(`Unsupported validator shape for section "${section}"`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── File sink helper ──────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
function createFileSink(filePath: string): (line: string) => void {
|
|
95
|
+
return (line: string) => {
|
|
96
|
+
try {
|
|
97
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
98
|
+
appendFileSync(filePath, line);
|
|
99
|
+
} catch {
|
|
100
|
+
// Best-effort — don't crash on log write failure
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Config loading ────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
interface LoadedConfig<TAppConfig> {
|
|
108
|
+
bootstrapConfig: Record<string, unknown>;
|
|
109
|
+
appConfig: TAppConfig | undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function loadYamlConfig<TAppConfig>(
|
|
113
|
+
loader: ApplicationConfigLoader<TAppConfig>,
|
|
114
|
+
fileContent: string | undefined,
|
|
115
|
+
): LoadedConfig<TAppConfig> {
|
|
116
|
+
const bootstrapSection = loader.bootstrapSection ?? 'bootstrap';
|
|
117
|
+
|
|
118
|
+
let raw: Record<string, unknown>;
|
|
119
|
+
if (fileContent !== undefined) {
|
|
120
|
+
raw = parseYamlObject(fileContent);
|
|
121
|
+
} else if (loader.overrides) {
|
|
122
|
+
raw = { ...loader.overrides };
|
|
123
|
+
} else {
|
|
124
|
+
raw = {};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Interpolate environment variables (Node/Bun only)
|
|
128
|
+
raw = interpolateTree(raw) as Record<string, unknown>;
|
|
129
|
+
|
|
130
|
+
// Extract bootstrap section
|
|
131
|
+
const bootstrapSection_ = raw[bootstrapSection];
|
|
132
|
+
const bootstrapConfig: Record<string, unknown> =
|
|
133
|
+
typeof bootstrapSection_ === 'object' && bootstrapSection_ !== null
|
|
134
|
+
? (bootstrapSection_ as Record<string, unknown>)
|
|
135
|
+
: {};
|
|
136
|
+
|
|
137
|
+
// Extract app config section
|
|
138
|
+
let appConfig: TAppConfig | undefined;
|
|
139
|
+
if (loader.appConfig) {
|
|
140
|
+
const appSection = loader.appSection;
|
|
141
|
+
const appRaw = appSection
|
|
142
|
+
? raw[appSection]
|
|
143
|
+
: (() => {
|
|
144
|
+
// Default: full remaining object minus bootstrap section
|
|
145
|
+
const remaining: Record<string, unknown> = {};
|
|
146
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
147
|
+
if (key !== bootstrapSection) {
|
|
148
|
+
remaining[key] = value;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return remaining;
|
|
152
|
+
})();
|
|
153
|
+
|
|
154
|
+
appConfig = validateAppConfig(loader.appConfig, appRaw, appSection ?? '*', loader.configFile);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { bootstrapConfig, appConfig };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── Node/Bun options ──────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
/** Options for the Node/Bun convenience {@link runNodeApplication}. */
|
|
163
|
+
export interface NodeApplicationOptions<TAppConfig = unknown, TEvents extends EventMap = InfraEvents> {
|
|
164
|
+
/** YAML config loading options. When omitted, uses defaults. */
|
|
165
|
+
readonly configLoader?: ApplicationConfigLoader<TAppConfig>;
|
|
166
|
+
/** Inline bootstrap config (overrides YAML-loaded config). */
|
|
167
|
+
readonly config?: ApplicationBootstrapOptions<TAppConfig, TEvents>['config'];
|
|
168
|
+
/** Pre-built services to inject. */
|
|
169
|
+
readonly services?: ApplicationBootstrapOptions<TAppConfig, TEvents>['services'];
|
|
170
|
+
/** User callback: application logic. */
|
|
171
|
+
readonly start: (app: ApplicationRuntime<TAppConfig, TEvents>) => Promise<void> | void;
|
|
172
|
+
/** User callback: cleanup before services shut down. */
|
|
173
|
+
readonly stop?: (
|
|
174
|
+
app: ApplicationRuntime<TAppConfig, TEvents>,
|
|
175
|
+
reason: ApplicationStopReason,
|
|
176
|
+
) => Promise<void> | void;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Public API ────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Node/Bun convenience application bootstrap.
|
|
183
|
+
*
|
|
184
|
+
* Extends the portable `runApplication` with:
|
|
185
|
+
* - YAML config file loading with section splitting
|
|
186
|
+
* - Application-specific config validation
|
|
187
|
+
* - File log sink creation from `logging.filePath`
|
|
188
|
+
* - Bun SQLite DB adapter creation when `database.enabled` + `database.driver`
|
|
189
|
+
* - Optional Node OTel telemetry exporter
|
|
190
|
+
* - Optional Node scheduler adapter
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```ts
|
|
194
|
+
* import { runNodeApplication } from '@gobing-ai/ts-infra/application-node';
|
|
195
|
+
*
|
|
196
|
+
* await runNodeApplication({
|
|
197
|
+
* configLoader: {
|
|
198
|
+
* configFile: 'config/app.yaml',
|
|
199
|
+
* bootstrapSection: 'bootstrap',
|
|
200
|
+
* appSection: 'billing',
|
|
201
|
+
* appConfig: {
|
|
202
|
+
* safeParse(raw) {
|
|
203
|
+
* return billingSchema.safeParse(raw);
|
|
204
|
+
* },
|
|
205
|
+
* },
|
|
206
|
+
* },
|
|
207
|
+
* async start(app) {
|
|
208
|
+
* app.logger.info('started');
|
|
209
|
+
* },
|
|
210
|
+
* });
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
export async function runNodeApplication<TAppConfig = unknown, TEvents extends EventMap = InfraEvents>(
|
|
214
|
+
options: NodeApplicationOptions<TAppConfig, TEvents>,
|
|
215
|
+
): Promise<ApplicationRuntime<TAppConfig, TEvents>> {
|
|
216
|
+
let nodeTelemetryInitialized = false;
|
|
217
|
+
|
|
218
|
+
// ── Load config ─────────────────────────────────────────────────────
|
|
219
|
+
let loadedAppConfig: TAppConfig | undefined;
|
|
220
|
+
let yamlBootstrap: Record<string, unknown> = {};
|
|
221
|
+
|
|
222
|
+
if (options.configLoader?.configFile) {
|
|
223
|
+
const fileContent = readFileSync(options.configLoader.configFile, 'utf-8');
|
|
224
|
+
const loaded = loadYamlConfig(options.configLoader, fileContent);
|
|
225
|
+
yamlBootstrap = loaded.bootstrapConfig;
|
|
226
|
+
loadedAppConfig = loaded.appConfig;
|
|
227
|
+
} else if (options.configLoader) {
|
|
228
|
+
const loaded = loadYamlConfig(options.configLoader, undefined);
|
|
229
|
+
yamlBootstrap = loaded.bootstrapConfig;
|
|
230
|
+
loadedAppConfig = loaded.appConfig;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── Resolve bootstrap config from YAML + inline options ────────────
|
|
234
|
+
// YAML loads as Record<string, unknown>; bridge into typed options.
|
|
235
|
+
// Inline options (options.config) take precedence over YAML sections.
|
|
236
|
+
const yamlLog = yamlBootstrap.logging as Partial<LoggingOptions> | undefined;
|
|
237
|
+
const yamlTel = yamlBootstrap.telemetry as Partial<TelemetryOptions> | undefined;
|
|
238
|
+
const yamlSched = yamlBootstrap.scheduler as Partial<SchedulerOptions> | undefined;
|
|
239
|
+
const databaseOpts = (yamlBootstrap.database ?? {}) as Record<string, unknown>;
|
|
240
|
+
|
|
241
|
+
const loggingOpts: Partial<LoggingOptions> = {
|
|
242
|
+
...yamlLog,
|
|
243
|
+
...options.config?.logging,
|
|
244
|
+
};
|
|
245
|
+
const telemetryOpts: Partial<TelemetryOptions> = {
|
|
246
|
+
...yamlTel,
|
|
247
|
+
...options.config?.telemetry,
|
|
248
|
+
};
|
|
249
|
+
const schedulerOpts: Partial<SchedulerOptions> = {
|
|
250
|
+
...yamlSched,
|
|
251
|
+
...options.config?.scheduler,
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// File sink from logging.filePath
|
|
255
|
+
const logFilePath = (yamlBootstrap.logging as Record<string, unknown> | undefined)?.filePath as string | undefined;
|
|
256
|
+
const loggingConfig: Partial<LoggingOptions> =
|
|
257
|
+
typeof logFilePath === 'string' ? { ...loggingOpts, fileSink: createFileSink(logFilePath) } : loggingOpts;
|
|
258
|
+
|
|
259
|
+
// ── Node OTel telemetry ─────────────────────────────────────────────
|
|
260
|
+
const rawTel = { ...telemetryOpts } as Record<string, unknown>;
|
|
261
|
+
if (rawTel.enabled !== false && rawTel.endpoint) {
|
|
262
|
+
initNodeTelemetry({
|
|
263
|
+
serviceName: (rawTel.serviceName as string | undefined) ?? 'ts-libs',
|
|
264
|
+
endpoint: rawTel.endpoint as string,
|
|
265
|
+
headers: rawTel.headers as Record<string, string> | undefined,
|
|
266
|
+
});
|
|
267
|
+
nodeTelemetryInitialized = true;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── DB adapter ──────────────────────────────────────────────────────
|
|
271
|
+
let dbAdapter: DbAdapterLike | undefined = options.services?.db;
|
|
272
|
+
if (!dbAdapter && databaseOpts.enabled === true) {
|
|
273
|
+
const driver = databaseOpts.driver as string | undefined;
|
|
274
|
+
if (driver === 'bun-sqlite') {
|
|
275
|
+
const adapter = await createDbAdapter({
|
|
276
|
+
driver: 'bun-sqlite',
|
|
277
|
+
url: databaseOpts.url as string | undefined,
|
|
278
|
+
});
|
|
279
|
+
dbAdapter = adapter as DbAdapter;
|
|
280
|
+
} else {
|
|
281
|
+
throw new ConfigValidationError(
|
|
282
|
+
`database.enabled is true but driver ${driver ? `"${driver}"` : 'is missing'} is not supported ` +
|
|
283
|
+
`(expected "bun-sqlite"). Provide a supported driver or inject a DbAdapter via services.db.`,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── Scheduler adapter ───────────────────────────────────────────────
|
|
289
|
+
const schedulerConfig: SchedulerOptions = {};
|
|
290
|
+
const rawSched = { ...schedulerOpts } as Record<string, unknown>;
|
|
291
|
+
if (rawSched.enabled === true) {
|
|
292
|
+
schedulerConfig.enabled = true;
|
|
293
|
+
schedulerConfig.autoStart = schedulerOpts.autoStart;
|
|
294
|
+
// Use Node scheduler adapter by default in this subpath
|
|
295
|
+
schedulerConfig.adapter = new NodeSchedulerAdapter();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ── Delegate to portable runApplication ─────────────────────────────
|
|
299
|
+
const app = await runApplication<TAppConfig, TEvents>({
|
|
300
|
+
config: {
|
|
301
|
+
...options.config,
|
|
302
|
+
logging: loggingConfig,
|
|
303
|
+
telemetry: telemetryOpts,
|
|
304
|
+
scheduler: schedulerConfig,
|
|
305
|
+
},
|
|
306
|
+
appConfig: loadedAppConfig,
|
|
307
|
+
services: {
|
|
308
|
+
...options.services,
|
|
309
|
+
...(dbAdapter ? { db: dbAdapter } : {}),
|
|
310
|
+
},
|
|
311
|
+
start: options.start,
|
|
312
|
+
stop: options.stop,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// ── Compose a handle with Node-specific cleanup on stop ─────────────
|
|
316
|
+
const originalStop = app.stop.bind(app);
|
|
317
|
+
return {
|
|
318
|
+
...app,
|
|
319
|
+
stop: async (reason?: ApplicationStopReason) => {
|
|
320
|
+
await originalStop(reason);
|
|
321
|
+
|
|
322
|
+
// Node-specific cleanup (after portable shutdown):
|
|
323
|
+
// 5. Shut down Node telemetry exporter
|
|
324
|
+
if (nodeTelemetryInitialized) {
|
|
325
|
+
await shutdownNodeTelemetry();
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
}
|