@loutrejs/loutre 0.8.1 → 0.9.0
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 +3 -3
- package/dist/adapters/bun.d.ts +1 -0
- package/dist/adapters/bun.js +41 -75
- package/dist/application/definition.d.ts +4 -4
- package/dist/application/kernel.js +1 -1
- package/dist/core/extension.d.ts +7 -3
- package/dist/core/generic-layer.d.ts +2 -2
- package/dist/core/generic-layer.js +1 -1
- package/dist/core/index.d.ts +4 -4
- package/dist/core/index.js +1 -1
- package/dist/core/injection.d.ts +11 -1
- package/dist/core/injection.js +7 -3
- package/dist/core/model/context.d.ts +2 -1
- package/dist/core/model/context.js +23 -12
- package/dist/core/model/lifecycle.js +1 -1
- package/dist/core/model/modules.js +6 -6
- package/dist/core/model/providers.js +2 -2
- package/dist/core/model/visibility.d.ts +3 -2
- package/dist/core/model/visibility.js +3 -5
- package/dist/core/model.js +1 -1
- package/dist/core/module.d.ts +6 -4
- package/dist/core/module.js +6 -1
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/http/extension.js +7 -6
- package/dist/message-port/extension.js +3 -3
- package/dist/runtime/di.d.ts +4 -2
- package/dist/runtime/di.js +33 -14
- package/dist/runtime/engine.d.ts +1 -0
- package/dist/runtime/engine.js +15 -1
- package/dist/runtime/index.d.ts +5 -1
- package/dist/runtime/index.js +3 -1
- package/dist/runtime/kernel.d.ts +2 -2
- package/dist/runtime/kernel.js +47 -9
- package/dist/runtime/shutdown-hooks.d.ts +16 -0
- package/dist/runtime/shutdown-hooks.js +69 -0
- package/dist/runtime/shutdown.d.ts +20 -0
- package/dist/runtime/shutdown.js +71 -0
- package/dist/tasks/extension.js +11 -10
- package/dist/websocket/extension.js +5 -5
- package/package.json +1 -1
package/dist/http/extension.js
CHANGED
|
@@ -403,24 +403,25 @@ function assertValidHttpRouteDefinition(route, segments) {
|
|
|
403
403
|
}
|
|
404
404
|
}
|
|
405
405
|
}
|
|
406
|
-
function createHttpExtensionRuntime(
|
|
406
|
+
function createHttpExtensionRuntime(bindings, applicationRuntime) {
|
|
407
407
|
const ingress = new IngressGate();
|
|
408
408
|
const activeResponseStreams = new Set();
|
|
409
409
|
const handlers = new Map();
|
|
410
|
-
for (const execution of
|
|
410
|
+
for (const { execution, resolve } of bindings) {
|
|
411
411
|
handlers.set(execution.id, runInInjectionContext({
|
|
412
412
|
consumer: {
|
|
413
413
|
kind: 'execution',
|
|
414
414
|
id: `http:${execution.id}`,
|
|
415
415
|
name: execution.id,
|
|
416
416
|
},
|
|
417
|
-
resolve: (token) =>
|
|
417
|
+
resolve: (token, consumer) => resolve(token, { consumer }),
|
|
418
418
|
}, () => execution.compiled.factory()));
|
|
419
419
|
}
|
|
420
|
-
const routes =
|
|
421
|
-
.flatMap((execution) => execution.compiled.routes.map((route) => ({
|
|
420
|
+
const routes = bindings
|
|
421
|
+
.flatMap(({ execution, resolve }) => execution.compiled.routes.map((route) => ({
|
|
422
422
|
executionId: execution.id,
|
|
423
423
|
route,
|
|
424
|
+
resolve,
|
|
424
425
|
})))
|
|
425
426
|
.toSorted((left, right) => compareHttpPathSpecificity(left.route.segments, right.route.segments));
|
|
426
427
|
return {
|
|
@@ -600,7 +601,7 @@ function createHttpExtensionRuntime(executions, applicationRuntime) {
|
|
|
600
601
|
const dispatch = () => composeLayers({
|
|
601
602
|
context,
|
|
602
603
|
layers: match.route.middlewares,
|
|
603
|
-
resolve:
|
|
604
|
+
resolve: match.resolve,
|
|
604
605
|
terminal: async (middlewareContext) => {
|
|
605
606
|
const handlerName = `${match.executionId}.${match.route.name}`;
|
|
606
607
|
const handlerOperation = applicationRuntime.beginOperation?.({
|
|
@@ -82,18 +82,18 @@ function snapshotMessagePortRoutes(routes) {
|
|
|
82
82
|
}),
|
|
83
83
|
])));
|
|
84
84
|
}
|
|
85
|
-
function createMessagePortRuntime(
|
|
85
|
+
function createMessagePortRuntime(bindings, applicationRuntime) {
|
|
86
86
|
const routes = new Map();
|
|
87
87
|
const ingress = new IngressGate();
|
|
88
88
|
const activeStreams = new Set();
|
|
89
|
-
for (const execution of
|
|
89
|
+
for (const { execution, resolve } of bindings) {
|
|
90
90
|
const handlers = runInInjectionContext({
|
|
91
91
|
consumer: {
|
|
92
92
|
kind: 'execution',
|
|
93
93
|
id: `message-port:${execution.id}`,
|
|
94
94
|
name: execution.id,
|
|
95
95
|
},
|
|
96
|
-
resolve: (token) =>
|
|
96
|
+
resolve: (token, consumer) => resolve(token, { consumer }),
|
|
97
97
|
}, () => execution.compiled.factory());
|
|
98
98
|
for (const [method, definition] of Object.entries(execution.compiled.routes)) {
|
|
99
99
|
const handler = handlers[method];
|
package/dist/runtime/di.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ArgsClass, type EnvClass, type ProviderDescriptor, type TokenLike } from '../core/index.js';
|
|
1
|
+
import { type ArgsClass, type EnvClass, type DependencyResolutionContext, type ProviderDescriptor, type TokenLike } from '../core/index.js';
|
|
2
2
|
import { Logger } from './logger.js';
|
|
3
3
|
export declare class DependencyResolutionError extends Error {
|
|
4
4
|
constructor(message: string);
|
|
@@ -8,6 +8,7 @@ export interface ContainerHooks {
|
|
|
8
8
|
}
|
|
9
9
|
export interface ContainerOptions {
|
|
10
10
|
readonly logger?: Logger;
|
|
11
|
+
readonly dependencies?: ReadonlyMap<TokenLike, readonly TokenLike[]>;
|
|
11
12
|
readonly hooks?: ContainerHooks;
|
|
12
13
|
readonly environment?: ReadonlyMap<EnvClass, object>;
|
|
13
14
|
readonly arguments?: ReadonlyMap<ArgsClass, object>;
|
|
@@ -17,6 +18,7 @@ export declare class Container {
|
|
|
17
18
|
constructor(providers: readonly ProviderDescriptor[], options?: Logger | ContainerOptions);
|
|
18
19
|
bindEnvironment(environment: EnvClass, value: object): void;
|
|
19
20
|
bindArguments(argumentsContract: ArgsClass, value: object): void;
|
|
20
|
-
resolve<T>(token: TokenLike<T>,
|
|
21
|
+
resolve<T>(token: TokenLike<T>, context?: string | DependencyResolutionContext): T;
|
|
21
22
|
get<T>(token: TokenLike<T>): T;
|
|
22
23
|
}
|
|
24
|
+
export declare function assertDeclaredDependency(consumer: string, token: TokenLike, dependencies: readonly TokenLike[]): void;
|
package/dist/runtime/di.js
CHANGED
|
@@ -11,12 +11,15 @@ export class Container {
|
|
|
11
11
|
#providers = new Map();
|
|
12
12
|
#applicationCache = new Map();
|
|
13
13
|
#logger;
|
|
14
|
+
#dependencies;
|
|
14
15
|
#hooks;
|
|
15
16
|
#environment = new Map();
|
|
16
17
|
#arguments = new Map();
|
|
17
18
|
constructor(providers, options = {}) {
|
|
18
19
|
this.#logger =
|
|
19
20
|
options instanceof Logger ? options : (options.logger ?? new Logger());
|
|
21
|
+
this.#dependencies =
|
|
22
|
+
options instanceof Logger ? undefined : options.dependencies;
|
|
20
23
|
this.#hooks = options instanceof Logger ? {} : (options.hooks ?? {});
|
|
21
24
|
if (!(options instanceof Logger)) {
|
|
22
25
|
for (const [environment, value] of options.environment ?? []) {
|
|
@@ -66,8 +69,9 @@ export class Container {
|
|
|
66
69
|
}
|
|
67
70
|
this.#arguments.set(argumentsContract, value);
|
|
68
71
|
}
|
|
69
|
-
resolve(token,
|
|
70
|
-
|
|
72
|
+
resolve(token, context) {
|
|
73
|
+
const resolution = typeof context === 'string' ? { source: context } : context;
|
|
74
|
+
return this.#resolve(token, resolution?.source, [], resolution?.consumer);
|
|
71
75
|
}
|
|
72
76
|
get(token) {
|
|
73
77
|
const provider = this.#providers.get(token);
|
|
@@ -100,7 +104,7 @@ export class Container {
|
|
|
100
104
|
}
|
|
101
105
|
throw new DependencyResolutionError(`LUTRE_DI_NOT_INITIALIZED: ${tokenName(token)} has not been constructed by Application initialization.`);
|
|
102
106
|
}
|
|
103
|
-
#resolve(token, source, lineage = []) {
|
|
107
|
+
#resolve(token, source, lineage = [], inquirer) {
|
|
104
108
|
const cycleStart = lineage.indexOf(token);
|
|
105
109
|
if (cycleStart >= 0) {
|
|
106
110
|
const cycle = [...lineage.slice(cycleStart), token]
|
|
@@ -123,16 +127,16 @@ export class Container {
|
|
|
123
127
|
}
|
|
124
128
|
const nextLineage = [...lineage, token];
|
|
125
129
|
if (provider.scope !== 'application') {
|
|
126
|
-
return this.#create(provider, nextLineage);
|
|
130
|
+
return this.#create(provider, nextLineage, inquirer);
|
|
127
131
|
}
|
|
128
132
|
if (this.#applicationCache.has(token)) {
|
|
129
133
|
return this.#applicationCache.get(token);
|
|
130
134
|
}
|
|
131
|
-
const instance = this.#create(provider, nextLineage);
|
|
135
|
+
const instance = this.#create(provider, nextLineage, inquirer);
|
|
132
136
|
this.#applicationCache.set(token, instance);
|
|
133
137
|
return instance;
|
|
134
138
|
}
|
|
135
|
-
#create(provider, lineage) {
|
|
139
|
+
#create(provider, lineage, inquirer) {
|
|
136
140
|
try {
|
|
137
141
|
return match(provider)
|
|
138
142
|
.with({ kind: 'value' }, (candidate) => {
|
|
@@ -152,13 +156,16 @@ export class Container {
|
|
|
152
156
|
throw new DependencyResolutionError(`LUTRE_ARGS_005: Arguments ${candidate.provide.name} requires runtime Arguments before Application initialization.`);
|
|
153
157
|
})
|
|
154
158
|
.with({ kind: 'class' }, (candidate) => {
|
|
155
|
-
const value = this.#instantiate(candidate.useClass, lineage);
|
|
159
|
+
const value = this.#instantiate(candidate.useClass, lineage, candidate, inquirer);
|
|
156
160
|
this.#hooks.created?.(candidate, value);
|
|
157
161
|
return value;
|
|
158
162
|
})
|
|
159
163
|
.with({ kind: 'factory' }, (candidate) => {
|
|
160
|
-
const dependencies = candidate.inject.map((token) => this.#
|
|
161
|
-
const value =
|
|
164
|
+
const dependencies = candidate.inject.map((token) => this.#resolveDependency(token, candidate, lineage));
|
|
165
|
+
const value = runInInjectionContext({
|
|
166
|
+
consumer: candidate.provide,
|
|
167
|
+
...(inquirer === undefined ? {} : { inquirer }),
|
|
168
|
+
}, () => candidate.useFactory(...dependencies));
|
|
162
169
|
if (isThenable(value)) {
|
|
163
170
|
throw new DependencyResolutionError('LUTRE_DI_ASYNC_FACTORY: Async factory providers are not supported. Move asynchronous resource initialization to application lifecycle.');
|
|
164
171
|
}
|
|
@@ -166,13 +173,13 @@ export class Container {
|
|
|
166
173
|
return value;
|
|
167
174
|
})
|
|
168
175
|
.with({ kind: 'conditional' }, (candidate) => {
|
|
169
|
-
const input = this.#
|
|
176
|
+
const input = this.#resolveDependency(candidate.select.contract, candidate, lineage);
|
|
170
177
|
const selected = input[candidate.select.key];
|
|
171
178
|
const implementation = candidate.mapping[selected];
|
|
172
179
|
if (!implementation) {
|
|
173
180
|
throw new DependencyResolutionError(`No conditional Provider matches ${candidate.select.key}=${String(selected)}`);
|
|
174
181
|
}
|
|
175
|
-
const value = this.#instantiate(implementation, lineage);
|
|
182
|
+
const value = this.#instantiate(implementation, lineage, candidate, inquirer);
|
|
176
183
|
this.#hooks.created?.(candidate, value);
|
|
177
184
|
return value;
|
|
178
185
|
})
|
|
@@ -185,13 +192,20 @@ export class Container {
|
|
|
185
192
|
throw error;
|
|
186
193
|
}
|
|
187
194
|
}
|
|
188
|
-
#
|
|
195
|
+
#resolveDependency(token, provider, lineage, source = tokenName(provider.provide)) {
|
|
196
|
+
if (this.#dependencies) {
|
|
197
|
+
assertDeclaredDependency(tokenName(provider.provide), token, this.#dependencies.get(provider.provide) ?? []);
|
|
198
|
+
}
|
|
199
|
+
return this.#resolve(token, source, lineage, provider.provide);
|
|
200
|
+
}
|
|
201
|
+
#instantiate(target, lineage, provider, inquirer) {
|
|
189
202
|
if (target.length > 0) {
|
|
190
203
|
throw new DependencyResolutionError(`LUTRE_DI_CONSTRUCTOR: ${target.name} has required constructor parameters. Declare framework dependencies with constructor default parameters using inject().`);
|
|
191
204
|
}
|
|
192
205
|
return runInInjectionContext({
|
|
193
|
-
consumer:
|
|
194
|
-
|
|
206
|
+
consumer: provider.provide,
|
|
207
|
+
...(inquirer === undefined ? {} : { inquirer }),
|
|
208
|
+
resolve: (token) => this.#resolveDependency(token, provider, lineage, target.name),
|
|
195
209
|
}, () => new target());
|
|
196
210
|
}
|
|
197
211
|
}
|
|
@@ -204,3 +218,8 @@ function isThenable(value) {
|
|
|
204
218
|
'then' in value &&
|
|
205
219
|
typeof value.then === 'function');
|
|
206
220
|
}
|
|
221
|
+
export function assertDeclaredDependency(consumer, token, dependencies) {
|
|
222
|
+
if (!dependencies.includes(token)) {
|
|
223
|
+
throw new DependencyResolutionError(`LUTRE_DI_UNDECLARED_DEPENDENCY: ${consumer} requested ${tokenName(token)}, but it is not declared in the canonical Application Model. Declare dependencies unconditionally; construction metadata and runtime values must not add dependencies.`);
|
|
224
|
+
}
|
|
225
|
+
}
|
package/dist/runtime/engine.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export type RuntimeEngine = 'node' | 'bun' | 'cloudflare-workers' | 'aws-lambda' | 'electron' | 'unknown';
|
|
2
2
|
export declare function detectRuntimeEngine(): RuntimeEngine;
|
|
3
3
|
export declare function assertRuntimeEngine(expected: Exclude<RuntimeEngine, 'unknown'>): void;
|
|
4
|
+
export declare function assertSupportedBunVersion(): void;
|
package/dist/runtime/engine.js
CHANGED
|
@@ -31,7 +31,21 @@ export function detectRuntimeEngine() {
|
|
|
31
31
|
}
|
|
32
32
|
export function assertRuntimeEngine(expected) {
|
|
33
33
|
const actual = detectRuntimeEngine();
|
|
34
|
-
if (actual === expected)
|
|
34
|
+
if (actual === expected) {
|
|
35
|
+
assertSupportedBunVersion();
|
|
35
36
|
return;
|
|
37
|
+
}
|
|
36
38
|
throw new Error(`LUTRE_RUNTIME_MISMATCH: Expected ${runtimeLabels[expected]}, detected ${runtimeLabels[actual]}.`);
|
|
37
39
|
}
|
|
40
|
+
export function assertSupportedBunVersion() {
|
|
41
|
+
if (detectRuntimeEngine() !== 'bun')
|
|
42
|
+
return;
|
|
43
|
+
const globals = globalThis;
|
|
44
|
+
const version = globals.Bun?.version ?? globals.process?.versions?.bun;
|
|
45
|
+
const release = version?.match(/^(\d+)\.(\d+)\.(\d+)(?:\+[\w.-]+)?$/);
|
|
46
|
+
if (release &&
|
|
47
|
+
(Number(release[1]) > 1 ||
|
|
48
|
+
(Number(release[1]) === 1 && Number(release[2]) >= 4)))
|
|
49
|
+
return;
|
|
50
|
+
throw new Error(`LUTRE_RUNTIME_VERSION: Bun ${version ?? 'unknown'} is not supported. Bun >=1.4.0 (stable) is required.`);
|
|
51
|
+
}
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { Container, DependencyResolutionError } from './di.js';
|
|
|
4
4
|
export type { ContainerOptions } from './di.js';
|
|
5
5
|
export { normalizeUnknownError } from './error.js';
|
|
6
6
|
export type { NormalizedApplicationError } from './error.js';
|
|
7
|
-
export { assertRuntimeEngine, detectRuntimeEngine } from './engine.js';
|
|
7
|
+
export { assertRuntimeEngine, assertSupportedBunVersion, detectRuntimeEngine, } from './engine.js';
|
|
8
8
|
export type { RuntimeEngine } from './engine.js';
|
|
9
9
|
export { ConsoleLoggerBackend, JsonConsoleLoggerBackend, Logger, SilentLogger, } from './logger.js';
|
|
10
10
|
export type { ConsoleLoggerBackendOptions, LoggerBackend, LogLevel, LogRecord, } from './logger.js';
|
|
@@ -13,3 +13,7 @@ export type { ApplicationKernelRuntimeOptions } from './kernel.js';
|
|
|
13
13
|
export { nodeRuntimeSupport } from './node-capabilities.js';
|
|
14
14
|
export { canRetryOnNextPort, initialServerPort } from './server-port.js';
|
|
15
15
|
export { serverUrl } from './server-url.js';
|
|
16
|
+
export { ProcessShutdownHooks } from './shutdown-hooks.js';
|
|
17
|
+
export type { ProcessShutdownHooksOptions, ProcessSignalSource, ShutdownSignal, } from './shutdown-hooks.js';
|
|
18
|
+
export { ApplicationShutdownError, RuntimeShutdown } from './shutdown.js';
|
|
19
|
+
export type { RuntimeShutdownOptions } from './shutdown.js';
|
package/dist/runtime/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export { checkRuntimeSupport, MissingRuntimeSupportError, } from './capability.js';
|
|
2
2
|
export { Container, DependencyResolutionError } from './di.js';
|
|
3
3
|
export { normalizeUnknownError } from './error.js';
|
|
4
|
-
export { assertRuntimeEngine, detectRuntimeEngine } from './engine.js';
|
|
4
|
+
export { assertRuntimeEngine, assertSupportedBunVersion, detectRuntimeEngine, } from './engine.js';
|
|
5
5
|
export { ConsoleLoggerBackend, JsonConsoleLoggerBackend, Logger, SilentLogger, } from './logger.js';
|
|
6
6
|
export { ApplicationKernelRuntime } from './kernel.js';
|
|
7
7
|
export { nodeRuntimeSupport } from './node-capabilities.js';
|
|
8
8
|
export { canRetryOnNextPort, initialServerPort } from './server-port.js';
|
|
9
9
|
export { serverUrl } from './server-url.js';
|
|
10
|
+
export { ProcessShutdownHooks } from './shutdown-hooks.js';
|
|
11
|
+
export { ApplicationShutdownError, RuntimeShutdown } from './shutdown.js';
|
package/dist/runtime/kernel.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RuntimeCapabilityRegistry, type ApplicationModel, type ExecutionExtension, type ExecutionExtensionRuntime, type ExecutionKernelRuntime, type ExecutionLease, type ExecutionOperationLease, type RuntimeCapabilityBinding, type RuntimeExecutionMetadata, type RuntimeInstrumentation, type RuntimeInvocationRegistration, type RuntimeOperationMetadata, type TokenLike } from '../core/index.js';
|
|
1
|
+
import { RuntimeCapabilityRegistry, type ApplicationModel, type ExecutionExtension, type ExecutionExtensionRuntime, type ExecutionKernelRuntime, type ExecutionRuntimeBinding, type ExecutionLease, type ExecutionOperationLease, type RuntimeCapabilityBinding, type RuntimeExecutionMetadata, type RuntimeInstrumentation, type RuntimeInvocationRegistration, type RuntimeOperationMetadata, type TokenLike } from '../core/index.js';
|
|
2
2
|
import { Container } from './di.js';
|
|
3
3
|
import { Logger } from './logger.js';
|
|
4
4
|
export interface ApplicationKernelRuntimeOptions {
|
|
@@ -16,7 +16,7 @@ export declare class ApplicationKernelRuntime implements ExecutionKernelRuntime
|
|
|
16
16
|
readonly capabilities: RuntimeCapabilityRegistry;
|
|
17
17
|
constructor(model: ApplicationModel, options?: ApplicationKernelRuntimeOptions);
|
|
18
18
|
initialize(): Promise<void>;
|
|
19
|
-
|
|
19
|
+
executionBindings<TCompiled>(extension: ExecutionExtension<any, TCompiled, any, any, any>): readonly ExecutionRuntimeBinding<TCompiled>[];
|
|
20
20
|
get<TValue>(token: TokenLike<TValue>): TValue;
|
|
21
21
|
extensionRuntime<TRuntime extends ExecutionExtensionRuntime>(extension: ExecutionExtension<any, any, any, any, TRuntime>): TRuntime;
|
|
22
22
|
beginExecution(metadata?: RuntimeExecutionMetadata, invocation?: RuntimeInvocationRegistration): ExecutionLease;
|
package/dist/runtime/kernel.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { ApplicationShutdownError } from './shutdown.js';
|
|
1
2
|
import { assertValidApplicationModel, loadArgs, loadEnv, RuntimeCapabilityRegistry, tokenName, } from '../core/index.js';
|
|
2
|
-
import { Container } from './di.js';
|
|
3
|
+
import { Container, assertDeclaredDependency } from './di.js';
|
|
3
4
|
import { ExecutionTracker } from './execution-tracker.js';
|
|
4
5
|
import { Logger } from './logger.js';
|
|
5
6
|
const defaultForceShutdownTimeoutMs = 5_000;
|
|
@@ -10,6 +11,7 @@ export class ApplicationKernelRuntime {
|
|
|
10
11
|
#extensionRuntimes = new Map();
|
|
11
12
|
#providerInstances = new Map();
|
|
12
13
|
#providerNodes = new Map();
|
|
14
|
+
#executionBindings = new Map();
|
|
13
15
|
#initializedModuleIds = new Set();
|
|
14
16
|
#executions = new ExecutionTracker();
|
|
15
17
|
#extensionDrainOperations = new Map();
|
|
@@ -39,10 +41,31 @@ export class ApplicationKernelRuntime {
|
|
|
39
41
|
}
|
|
40
42
|
this.container = new Container(model.providers, {
|
|
41
43
|
logger: options.logger ?? new Logger(),
|
|
44
|
+
dependencies: new Map([...this.#providerNodes.values()].map((node) => [
|
|
45
|
+
node.token,
|
|
46
|
+
node.dependencies,
|
|
47
|
+
])),
|
|
42
48
|
hooks: {
|
|
43
49
|
created: (provider, value) => this.#notifyProviderCreated(provider, value),
|
|
44
50
|
},
|
|
45
51
|
});
|
|
52
|
+
for (const { extension, executions } of this.model.extensions) {
|
|
53
|
+
this.#executionBindings.set(extension.identity, Object.freeze(executions.map((execution) => {
|
|
54
|
+
const resolve = (token, context) => {
|
|
55
|
+
if (this.#state === 'created' || this.#state === 'stopped') {
|
|
56
|
+
throw applicationStateError(this.#state);
|
|
57
|
+
}
|
|
58
|
+
assertDeclaredDependency(execution.id, token, execution.dependencies);
|
|
59
|
+
return this.container.resolve(token, {
|
|
60
|
+
source: execution.id,
|
|
61
|
+
...(typeof context === 'string'
|
|
62
|
+
? { source: context }
|
|
63
|
+
: context),
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
return Object.freeze({ execution, resolve });
|
|
67
|
+
})));
|
|
68
|
+
}
|
|
46
69
|
}
|
|
47
70
|
initialize() {
|
|
48
71
|
if (this.#state === 'running')
|
|
@@ -68,7 +91,7 @@ export class ApplicationKernelRuntime {
|
|
|
68
91
|
await this.#initializeProviders();
|
|
69
92
|
for (const modelExtension of this.model.extensions) {
|
|
70
93
|
const runtime = await modelExtension.extension.createRuntime({
|
|
71
|
-
executions: modelExtension.
|
|
94
|
+
executions: this.executionBindings(modelExtension.extension),
|
|
72
95
|
capabilities: this.capabilities,
|
|
73
96
|
applicationRuntime: this,
|
|
74
97
|
});
|
|
@@ -141,11 +164,12 @@ export class ApplicationKernelRuntime {
|
|
|
141
164
|
await this.#runHook(lifecycleHookOf(this.model, module.id, 'onApplicationBootstrap'));
|
|
142
165
|
}
|
|
143
166
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
167
|
+
executionBindings(extension) {
|
|
168
|
+
const bindings = this.#executionBindings.get(extension.identity);
|
|
169
|
+
if (!bindings) {
|
|
170
|
+
throw new Error(`LUTRE_EXTENSION_RUNTIME_MISSING: ${extension.name} is not declared in the Application Model.`);
|
|
147
171
|
}
|
|
148
|
-
return
|
|
172
|
+
return bindings;
|
|
149
173
|
}
|
|
150
174
|
get(token) {
|
|
151
175
|
if (this.#state !== 'running')
|
|
@@ -259,12 +283,20 @@ export class ApplicationKernelRuntime {
|
|
|
259
283
|
}
|
|
260
284
|
catch (error) {
|
|
261
285
|
errors.push(error);
|
|
262
|
-
throw new
|
|
286
|
+
throw new ApplicationShutdownError(errors, 'Application shutdown did not reach a safe cleanup boundary.', {
|
|
287
|
+
cause: error,
|
|
288
|
+
safeCleanupBoundaryReached: false,
|
|
289
|
+
retryAfter: this.#waitForShutdownProgress(),
|
|
290
|
+
});
|
|
263
291
|
}
|
|
264
292
|
}
|
|
265
293
|
if (timedOutDrains.some(({ isPending }) => isPending())) {
|
|
266
294
|
const cause = errors.find((error) => error instanceof ExtensionDrainTimeoutError);
|
|
267
|
-
throw new
|
|
295
|
+
throw new ApplicationShutdownError(errors, 'Application shutdown did not reach a safe cleanup boundary.', {
|
|
296
|
+
cause,
|
|
297
|
+
safeCleanupBoundaryReached: false,
|
|
298
|
+
retryAfter: this.#waitForShutdownProgress(),
|
|
299
|
+
});
|
|
268
300
|
}
|
|
269
301
|
for (const { extension } of [...this.model.extensions].toReversed()) {
|
|
270
302
|
const runtime = this.#extensionRuntimes.get(extension.identity);
|
|
@@ -276,9 +308,15 @@ export class ApplicationKernelRuntime {
|
|
|
276
308
|
this.#state = 'stopped';
|
|
277
309
|
await closeInstrumentation(this.#instrumentation);
|
|
278
310
|
if (errors.length > 0) {
|
|
279
|
-
throw new
|
|
311
|
+
throw new ApplicationShutdownError(errors, 'Application shutdown failed.', { safeCleanupBoundaryReached: true });
|
|
280
312
|
}
|
|
281
313
|
}
|
|
314
|
+
async #waitForShutdownProgress() {
|
|
315
|
+
await Promise.allSettled([
|
|
316
|
+
this.#executions.waitForIdle(),
|
|
317
|
+
...[...this.#extensionDrainOperations.values()].map(({ result }) => result),
|
|
318
|
+
]);
|
|
319
|
+
}
|
|
282
320
|
#startExtensionDrain(extension, runtime) {
|
|
283
321
|
const existing = this.#extensionDrainOperations.get(extension.identity);
|
|
284
322
|
if (existing)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type ShutdownSignal = 'SIGINT' | 'SIGTERM';
|
|
2
|
+
export interface ProcessSignalSource {
|
|
3
|
+
once(signal: ShutdownSignal, handler: () => void): unknown;
|
|
4
|
+
off(signal: ShutdownSignal, handler: () => void): unknown;
|
|
5
|
+
}
|
|
6
|
+
export interface ProcessShutdownHooksOptions {
|
|
7
|
+
readonly source: ProcessSignalSource;
|
|
8
|
+
readonly close: (signal: ShutdownSignal) => Promise<void>;
|
|
9
|
+
readonly onError: (error: unknown, signal: ShutdownSignal) => void;
|
|
10
|
+
}
|
|
11
|
+
export declare class ProcessShutdownHooks {
|
|
12
|
+
#private;
|
|
13
|
+
constructor(options: ProcessShutdownHooksOptions);
|
|
14
|
+
setEnabled(enabled: boolean): void;
|
|
15
|
+
dispose(): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { ApplicationShutdownError } from './shutdown.js';
|
|
2
|
+
export class ProcessShutdownHooks {
|
|
3
|
+
#handlers = new Map();
|
|
4
|
+
#options;
|
|
5
|
+
#disposed = false;
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.#options = options;
|
|
8
|
+
}
|
|
9
|
+
setEnabled(enabled) {
|
|
10
|
+
if (this.#disposed)
|
|
11
|
+
return;
|
|
12
|
+
if (!enabled) {
|
|
13
|
+
this.#removeHandlers();
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (this.#handlers.size > 0)
|
|
17
|
+
return;
|
|
18
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
19
|
+
const handler = () => {
|
|
20
|
+
if (this.#disposed)
|
|
21
|
+
return;
|
|
22
|
+
this.dispose();
|
|
23
|
+
void this.#close(signal);
|
|
24
|
+
};
|
|
25
|
+
this.#handlers.set(signal, handler);
|
|
26
|
+
this.#options.source.once(signal, handler);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
dispose() {
|
|
30
|
+
this.#disposed = true;
|
|
31
|
+
this.#removeHandlers();
|
|
32
|
+
}
|
|
33
|
+
#removeHandlers() {
|
|
34
|
+
for (const [signal, handler] of this.#handlers) {
|
|
35
|
+
this.#options.source.off(signal, handler);
|
|
36
|
+
}
|
|
37
|
+
this.#handlers.clear();
|
|
38
|
+
}
|
|
39
|
+
async #close(signal) {
|
|
40
|
+
for (;;) {
|
|
41
|
+
try {
|
|
42
|
+
await this.#options.close(signal);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
this.#reportError(error, signal);
|
|
47
|
+
if (!(error instanceof ApplicationShutdownError) ||
|
|
48
|
+
error.safeCleanupBoundaryReached ||
|
|
49
|
+
!error.retryAfter)
|
|
50
|
+
return;
|
|
51
|
+
try {
|
|
52
|
+
await error.retryAfter;
|
|
53
|
+
}
|
|
54
|
+
catch (retryError) {
|
|
55
|
+
this.#reportError(retryError, signal);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
#reportError(error, signal) {
|
|
62
|
+
try {
|
|
63
|
+
this.#options.onError(error, signal);
|
|
64
|
+
}
|
|
65
|
+
catch (reportingError) {
|
|
66
|
+
console.error('Runtime shutdown error reporter failed', error, reportingError);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare class ApplicationShutdownError extends AggregateError {
|
|
2
|
+
readonly safeCleanupBoundaryReached: boolean;
|
|
3
|
+
readonly retryAfter: Promise<void> | undefined;
|
|
4
|
+
constructor(errors: readonly unknown[], message: string, options: ErrorOptions & {
|
|
5
|
+
readonly safeCleanupBoundaryReached: boolean;
|
|
6
|
+
readonly retryAfter?: Promise<void>;
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
export interface RuntimeShutdownOptions {
|
|
10
|
+
readonly message: string;
|
|
11
|
+
readonly beginClose: () => Promise<void>;
|
|
12
|
+
readonly closeApplication: (signal?: string) => Promise<void>;
|
|
13
|
+
readonly closeResources: (safeCleanupBoundaryReached: boolean, resourceClosing: Promise<void>) => Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export declare class RuntimeShutdown {
|
|
16
|
+
#private;
|
|
17
|
+
constructor(options: RuntimeShutdownOptions);
|
|
18
|
+
get started(): boolean;
|
|
19
|
+
close(signal?: string): Promise<void>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export class ApplicationShutdownError extends AggregateError {
|
|
2
|
+
safeCleanupBoundaryReached;
|
|
3
|
+
retryAfter;
|
|
4
|
+
constructor(errors, message, options) {
|
|
5
|
+
super(errors, message, options);
|
|
6
|
+
this.name = 'ApplicationShutdownError';
|
|
7
|
+
this.safeCleanupBoundaryReached = options.safeCleanupBoundaryReached;
|
|
8
|
+
this.retryAfter = options.retryAfter;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class RuntimeShutdown {
|
|
12
|
+
#options;
|
|
13
|
+
#started = false;
|
|
14
|
+
#closed = false;
|
|
15
|
+
#closing;
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.#options = options;
|
|
18
|
+
}
|
|
19
|
+
get started() {
|
|
20
|
+
return this.#started;
|
|
21
|
+
}
|
|
22
|
+
close(signal) {
|
|
23
|
+
if (this.#closed)
|
|
24
|
+
return Promise.resolve();
|
|
25
|
+
if (this.#closing)
|
|
26
|
+
return this.#closing;
|
|
27
|
+
this.#started = true;
|
|
28
|
+
this.#closing = this.#close(signal).catch((error) => {
|
|
29
|
+
this.#closing = undefined;
|
|
30
|
+
throw error;
|
|
31
|
+
});
|
|
32
|
+
return this.#closing;
|
|
33
|
+
}
|
|
34
|
+
async #close(signal) {
|
|
35
|
+
const errors = [];
|
|
36
|
+
let resourceClosing = Promise.resolve();
|
|
37
|
+
try {
|
|
38
|
+
resourceClosing = this.#options.beginClose();
|
|
39
|
+
void resourceClosing.catch(() => undefined);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
errors.push(error);
|
|
43
|
+
}
|
|
44
|
+
let safeCleanupBoundaryReached = false;
|
|
45
|
+
let retryAfter;
|
|
46
|
+
try {
|
|
47
|
+
await this.#options.closeApplication(signal);
|
|
48
|
+
safeCleanupBoundaryReached = true;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
errors.push(error);
|
|
52
|
+
safeCleanupBoundaryReached =
|
|
53
|
+
error instanceof ApplicationShutdownError &&
|
|
54
|
+
error.safeCleanupBoundaryReached;
|
|
55
|
+
if (error instanceof ApplicationShutdownError)
|
|
56
|
+
retryAfter = error.retryAfter;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
await this.#options.closeResources(safeCleanupBoundaryReached, resourceClosing);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
errors.push(error);
|
|
63
|
+
}
|
|
64
|
+
if (errors.length > 0)
|
|
65
|
+
throw new ApplicationShutdownError(errors, this.#options.message, {
|
|
66
|
+
safeCleanupBoundaryReached,
|
|
67
|
+
...(retryAfter === undefined ? {} : { retryAfter }),
|
|
68
|
+
});
|
|
69
|
+
this.#closed = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
package/dist/tasks/extension.js
CHANGED
|
@@ -228,14 +228,14 @@ export function consume(declaration) {
|
|
|
228
228
|
task: declaration.task,
|
|
229
229
|
});
|
|
230
230
|
}
|
|
231
|
-
function createTasksRuntime(
|
|
231
|
+
function createTasksRuntime(bindings, applicationRuntime) {
|
|
232
232
|
const runtimes = new Map();
|
|
233
233
|
const runtimeExecutionIds = new Map();
|
|
234
234
|
const triggers = [];
|
|
235
|
-
for (const execution of
|
|
235
|
+
for (const { execution, resolve } of bindings) {
|
|
236
236
|
const compiled = execution.compiled;
|
|
237
237
|
if (compiled.type !== 'task') {
|
|
238
|
-
triggers.push(
|
|
238
|
+
triggers.push({ execution, resolve });
|
|
239
239
|
continue;
|
|
240
240
|
}
|
|
241
241
|
const runtime = runInInjectionContext({
|
|
@@ -244,7 +244,7 @@ function createTasksRuntime(executions, applicationRuntime) {
|
|
|
244
244
|
id: `task:${compiled.name}`,
|
|
245
245
|
name: compiled.name,
|
|
246
246
|
},
|
|
247
|
-
resolve: (dependency) =>
|
|
247
|
+
resolve: (dependency, consumer) => resolve(dependency, { consumer }),
|
|
248
248
|
}, () => compiled.factory());
|
|
249
249
|
runtimes.set(compiled.taskExecutionId, runtime);
|
|
250
250
|
runtimeExecutionIds.set(compiled.taskExecutionId, execution.id);
|
|
@@ -344,10 +344,11 @@ function createTasksRuntime(executions, applicationRuntime) {
|
|
|
344
344
|
const started = [];
|
|
345
345
|
const startup = (async () => {
|
|
346
346
|
try {
|
|
347
|
-
for (const
|
|
347
|
+
for (const { execution, resolve } of triggers) {
|
|
348
|
+
const trigger = execution.compiled;
|
|
348
349
|
if (trigger.type === 'task')
|
|
349
350
|
continue;
|
|
350
|
-
const handle = await startTrigger(trigger, run,
|
|
351
|
+
const handle = await startTrigger(trigger, run, resolve);
|
|
351
352
|
started.push(handle);
|
|
352
353
|
if (state !== 'running') {
|
|
353
354
|
throw tasksStateError(state);
|
|
@@ -406,11 +407,11 @@ function createTasksRuntime(executions, applicationRuntime) {
|
|
|
406
407
|
},
|
|
407
408
|
};
|
|
408
409
|
}
|
|
409
|
-
async function startTrigger(trigger, run,
|
|
410
|
+
async function startTrigger(trigger, run, resolve) {
|
|
410
411
|
return match(trigger)
|
|
411
412
|
.with({ type: 'cron' }, (candidate) => startCronTrigger(candidate, run))
|
|
412
413
|
.with({ type: 'fixed-delay' }, (candidate) => startFixedDelayTrigger(candidate, run))
|
|
413
|
-
.with({ type: 'queue-consumer' }, (candidate) => startQueueTrigger(candidate, run,
|
|
414
|
+
.with({ type: 'queue-consumer' }, (candidate) => startQueueTrigger(candidate, run, resolve))
|
|
414
415
|
.exhaustive();
|
|
415
416
|
}
|
|
416
417
|
function startCronTrigger(trigger, run) {
|
|
@@ -486,8 +487,8 @@ function startFixedDelayTrigger(trigger, run) {
|
|
|
486
487
|
},
|
|
487
488
|
};
|
|
488
489
|
}
|
|
489
|
-
async function startQueueTrigger(trigger, run,
|
|
490
|
-
const driver =
|
|
490
|
+
async function startQueueTrigger(trigger, run, resolve) {
|
|
491
|
+
const driver = resolve(trigger.queueDriver);
|
|
491
492
|
if (!driver || typeof driver.start !== 'function') {
|
|
492
493
|
throw new Error(`LUTRE_QUEUE_DRIVER_INVALID: Queue ${trigger.queueName} driver is invalid.`);
|
|
493
494
|
}
|