@loutrejs/loutre 0.5.0 → 0.7.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 +63 -32
- package/dist/adapters/aws-lambda.d.ts +5 -3
- package/dist/adapters/aws-lambda.js +53 -23
- package/dist/adapters/bun.d.ts +7 -4
- package/dist/adapters/bun.js +52 -42
- package/dist/adapters/cloudflare-workers.d.ts +5 -3
- package/dist/adapters/cloudflare-workers.js +23 -28
- package/dist/adapters/deno.d.ts +7 -4
- package/dist/adapters/deno.js +94 -57
- package/dist/adapters/electron.d.ts +13 -5
- package/dist/adapters/electron.js +76 -28
- package/dist/application/definition.d.ts +31 -0
- package/dist/application/definition.js +16 -0
- package/dist/application/index.d.ts +4 -63
- package/dist/application/index.js +2 -11
- package/dist/application/kernel-internal.d.ts +2 -0
- package/dist/application/kernel-internal.js +10 -0
- package/dist/application/kernel.d.ts +22 -0
- package/dist/application/kernel.js +65 -0
- package/dist/core/diagnostic.d.ts +9 -0
- package/dist/core/diagnostic.js +9 -0
- package/dist/core/extension.d.ts +118 -0
- package/dist/core/extension.js +73 -0
- package/dist/core/generic-layer.d.ts +26 -0
- package/dist/core/generic-layer.js +116 -0
- package/dist/core/index.d.ts +32 -15
- package/dist/core/index.js +15 -15
- package/dist/core/injection.d.ts +4 -13
- package/dist/core/injection.js +63 -0
- package/dist/core/instrumentation.d.ts +36 -0
- package/dist/core/model/context.d.ts +18 -0
- package/dist/core/model/context.js +34 -0
- package/dist/core/model/lifecycle.d.ts +5 -0
- package/dist/core/model/lifecycle.js +44 -0
- package/dist/core/model/modules.d.ts +2 -0
- package/dist/core/model/modules.js +101 -0
- package/dist/core/model/providers.d.ts +4 -0
- package/dist/core/model/providers.js +74 -0
- package/dist/core/model/snapshot.d.ts +6 -0
- package/dist/core/model/snapshot.js +31 -0
- package/dist/core/model/types.d.ts +78 -0
- package/dist/core/model/types.js +1 -0
- package/dist/core/model/validation.d.ts +4 -0
- package/dist/core/model/validation.js +40 -0
- package/dist/core/model/visibility.d.ts +4 -0
- package/dist/core/model/visibility.js +20 -0
- package/dist/core/model.d.ts +10 -0
- package/dist/core/model.js +346 -0
- package/dist/core/module.d.ts +8 -13
- package/dist/core/source-location.d.ts +8 -0
- package/dist/core/source-location.js +31 -0
- package/dist/core/token-internal.d.ts +3 -0
- package/dist/core/token-internal.js +4 -0
- package/dist/core/type.d.ts +7 -0
- package/dist/core/type.js +4 -0
- package/dist/devtools/capsule.d.ts +51 -0
- package/dist/devtools/capsule.js +245 -0
- package/dist/devtools/index.d.ts +11 -0
- package/dist/devtools/index.js +6 -0
- package/dist/devtools/inspector.d.ts +57 -0
- package/dist/devtools/inspector.js +1 -0
- package/dist/devtools/module.d.ts +18 -0
- package/dist/devtools/module.js +26 -0
- package/dist/devtools/observer.d.ts +77 -0
- package/dist/devtools/observer.js +10 -0
- package/dist/devtools/protocol.d.ts +1 -0
- package/dist/devtools/protocol.js +1 -0
- package/dist/devtools/provider-instrumentation.d.ts +14 -0
- package/dist/devtools/provider-instrumentation.js +117 -0
- package/dist/devtools/runtime.d.ts +20 -0
- package/dist/devtools/runtime.js +519 -0
- package/dist/devtools/values.d.ts +35 -0
- package/dist/devtools/values.js +175 -0
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/graph/index.d.ts +2 -3
- package/dist/graph/index.js +1 -3
- package/dist/graph/model.d.ts +34 -0
- package/dist/graph/model.js +127 -0
- package/dist/http/auth.d.ts +46 -0
- package/dist/http/auth.js +96 -0
- package/dist/http/client.d.ts +28 -31
- package/dist/http/client.js +58 -44
- package/dist/http/cors-internal.d.ts +29 -0
- package/dist/http/cors-internal.js +160 -0
- package/dist/http/cors.d.ts +5 -8
- package/dist/http/cors.js +7 -165
- package/dist/http/extension.d.ts +348 -0
- package/dist/http/extension.js +1061 -0
- package/dist/http/index.d.ts +9 -7
- package/dist/http/index.js +5 -7
- package/dist/{application → http}/openapi.d.ts +12 -3
- package/dist/{application → http}/openapi.js +164 -102
- package/dist/http/path.d.ts +1 -42
- package/dist/http/path.js +1 -2
- package/dist/http/runtime/request.d.ts +17 -0
- package/dist/http/runtime/request.js +109 -0
- package/dist/http/runtime/response.d.ts +18 -0
- package/dist/http/runtime/response.js +108 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/message-port/extension.d.ts +66 -0
- package/dist/message-port/extension.js +411 -0
- package/dist/message-port/index.d.ts +4 -73
- package/dist/message-port/index.js +2 -189
- package/dist/message-port/transport.d.ts +11 -0
- package/dist/message-port/transport.js +45 -0
- package/dist/runtime/async-iterator-lifecycle.d.ts +12 -0
- package/dist/runtime/async-iterator-lifecycle.js +95 -0
- package/dist/runtime/capability.d.ts +11 -11
- package/dist/runtime/capability.js +3 -3
- package/dist/runtime/di.d.ts +5 -21
- package/dist/runtime/di.js +22 -289
- package/dist/runtime/execution-tracker.d.ts +8 -0
- package/dist/runtime/execution-tracker.js +54 -0
- package/dist/runtime/index.d.ts +15 -9
- package/dist/runtime/index.js +9 -9
- package/dist/runtime/ingress-gate.d.ts +7 -0
- package/dist/runtime/ingress-gate.js +33 -0
- package/dist/runtime/kernel.d.ts +25 -0
- package/dist/runtime/kernel.js +447 -0
- package/dist/runtime/logger.d.ts +2 -0
- package/dist/runtime/logger.js +2 -0
- package/dist/runtime/node-capabilities.d.ts +1 -1
- package/dist/runtime/node-capabilities.js +1 -1
- package/dist/runtime/server-port.d.ts +2 -0
- package/dist/runtime/server-port.js +11 -0
- package/dist/tasks/extension.d.ts +144 -0
- package/dist/tasks/extension.js +525 -0
- package/dist/tasks/index.d.ts +2 -0
- package/dist/tasks/index.js +1 -0
- package/dist/tasks/runtime/cron.d.ts +6 -0
- package/dist/tasks/runtime/cron.js +89 -0
- package/dist/websocket/errors.d.ts +9 -0
- package/dist/websocket/errors.js +18 -0
- package/dist/websocket/extension.d.ts +154 -0
- package/dist/websocket/extension.js +533 -0
- package/dist/websocket/index.d.ts +3 -0
- package/dist/websocket/index.js +2 -0
- package/package.json +16 -16
- package/dist/application/binding.d.ts +0 -33
- package/dist/application/binding.js +0 -295
- package/dist/application/host.d.ts +0 -8
- package/dist/application/host.js +0 -14
- package/dist/application/queue.d.ts +0 -3
- package/dist/application/queue.js +0 -4
- package/dist/application/scheduler.d.ts +0 -2
- package/dist/application/scheduler.js +0 -45
- package/dist/core/context-key.d.ts +0 -16
- package/dist/core/context-key.js +0 -17
- package/dist/core/contract-internal.d.ts +0 -22
- package/dist/core/contract-internal.js +0 -61
- package/dist/core/contract.d.ts +0 -96
- package/dist/core/contract.js +0 -88
- package/dist/core/execution.d.ts +0 -78
- package/dist/core/execution.js +0 -61
- package/dist/core/implementation.d.ts +0 -44
- package/dist/core/implementation.js +0 -35
- package/dist/core/layer.d.ts +0 -137
- package/dist/core/layer.js +0 -81
- package/dist/graph/compiler.d.ts +0 -4
- package/dist/graph/compiler.js +0 -228
- package/dist/graph/graph.d.ts +0 -18
- package/dist/graph/graph.js +0 -1118
- package/dist/graph/ir.d.ts +0 -175
- package/dist/http/application.d.ts +0 -18
- package/dist/http/application.js +0 -515
- package/dist/http/basic-auth.d.ts +0 -37
- package/dist/http/basic-auth.js +0 -67
- package/dist/http/definitions.d.ts +0 -363
- package/dist/http/definitions.js +0 -310
- package/dist/http/hosted-protocol.d.ts +0 -4
- package/dist/http/hosted-protocol.js +0 -4
- package/dist/http/internal.d.ts +0 -8
- package/dist/http/internal.js +0 -1
- package/dist/http/params.d.ts +0 -2
- package/dist/http/params.js +0 -19
- package/dist/http/request-body.d.ts +0 -4
- package/dist/http/request-body.js +0 -6
- package/dist/http/validate.d.ts +0 -20
- package/dist/http/validate.js +0 -6
- package/dist/message-port/environment.d.ts +0 -2
- package/dist/message-port/environment.js +0 -4
- package/dist/runtime/application.d.ts +0 -38
- package/dist/runtime/application.js +0 -339
- package/dist/runtime/pipeline.d.ts +0 -11
- package/dist/runtime/pipeline.js +0 -161
- /package/dist/{graph/ir.js → core/instrumentation.js} +0 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import { assertValidApplicationModel, loadArgs, loadEnv, RuntimeCapabilityRegistry, tokenName, } from '../core/index.js';
|
|
2
|
+
import { Container } from './di.js';
|
|
3
|
+
import { ExecutionTracker } from './execution-tracker.js';
|
|
4
|
+
import { Logger } from './logger.js';
|
|
5
|
+
const defaultForceShutdownTimeoutMs = 5_000;
|
|
6
|
+
export class ApplicationKernelRuntime {
|
|
7
|
+
model;
|
|
8
|
+
container;
|
|
9
|
+
capabilities;
|
|
10
|
+
#extensionRuntimes = new Map();
|
|
11
|
+
#providerInstances = new Map();
|
|
12
|
+
#providerNodes = new Map();
|
|
13
|
+
#initializedModuleIds = new Set();
|
|
14
|
+
#executions = new ExecutionTracker();
|
|
15
|
+
#extensionDrainOperations = new Map();
|
|
16
|
+
#environmentSource;
|
|
17
|
+
#argumentsSource;
|
|
18
|
+
#forceShutdownTimeoutMs;
|
|
19
|
+
#instrumentation;
|
|
20
|
+
#state = 'created';
|
|
21
|
+
#initialization;
|
|
22
|
+
#shutdown;
|
|
23
|
+
constructor(model, options = {}) {
|
|
24
|
+
this.model = assertValidApplicationModel(model);
|
|
25
|
+
for (const node of this.model.nodes) {
|
|
26
|
+
if (node.kind === 'provider')
|
|
27
|
+
this.#providerNodes.set(node.provider, node);
|
|
28
|
+
}
|
|
29
|
+
this.capabilities = new RuntimeCapabilityRegistry(options.capabilities ?? []);
|
|
30
|
+
this.#environmentSource = options.environmentSource;
|
|
31
|
+
this.#argumentsSource =
|
|
32
|
+
'argumentsSource' in options ? options.argumentsSource : Object.freeze({});
|
|
33
|
+
this.#forceShutdownTimeoutMs =
|
|
34
|
+
options.forceShutdownTimeoutMs ?? defaultForceShutdownTimeoutMs;
|
|
35
|
+
this.#instrumentation = options.instrumentation;
|
|
36
|
+
if (!Number.isFinite(this.#forceShutdownTimeoutMs) ||
|
|
37
|
+
this.#forceShutdownTimeoutMs < 0) {
|
|
38
|
+
throw new TypeError('LUTRE_FORCE_SHUTDOWN_TIMEOUT: forceShutdownTimeoutMs must be a non-negative finite number.');
|
|
39
|
+
}
|
|
40
|
+
this.container = new Container(model.providers, {
|
|
41
|
+
logger: options.logger ?? new Logger(),
|
|
42
|
+
hooks: {
|
|
43
|
+
created: (provider, value) => this.#notifyProviderCreated(provider, value),
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
initialize() {
|
|
48
|
+
if (this.#state === 'running')
|
|
49
|
+
return Promise.resolve();
|
|
50
|
+
if (this.#initialization)
|
|
51
|
+
return this.#initialization;
|
|
52
|
+
if (this.#state === 'draining' || this.#state === 'stopped') {
|
|
53
|
+
return Promise.reject(applicationStateError(this.#state));
|
|
54
|
+
}
|
|
55
|
+
const initialization = this.#initialize().finally(() => {
|
|
56
|
+
if (this.#initialization === initialization) {
|
|
57
|
+
this.#initialization = undefined;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
this.#initialization = initialization;
|
|
61
|
+
return initialization;
|
|
62
|
+
}
|
|
63
|
+
async #initialize() {
|
|
64
|
+
this.#state = 'initializing';
|
|
65
|
+
try {
|
|
66
|
+
this.#validateCapabilities();
|
|
67
|
+
await this.#bindRuntimeInputs();
|
|
68
|
+
await this.#initializeProviders();
|
|
69
|
+
for (const modelExtension of this.model.extensions) {
|
|
70
|
+
const runtime = await modelExtension.extension.createRuntime({
|
|
71
|
+
executions: modelExtension.executions,
|
|
72
|
+
capabilities: this.capabilities,
|
|
73
|
+
applicationRuntime: this,
|
|
74
|
+
});
|
|
75
|
+
this.#extensionRuntimes.set(modelExtension.extension.identity, runtime);
|
|
76
|
+
}
|
|
77
|
+
this.#state = 'running';
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
const cleanupErrors = await this.#rollbackInitialization();
|
|
81
|
+
this.#state = 'stopped';
|
|
82
|
+
if (cleanupErrors.length > 0) {
|
|
83
|
+
throw new AggregateError([error, ...cleanupErrors], 'Application initialization failed and cleanup also failed.', { cause: error });
|
|
84
|
+
}
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
#validateCapabilities() {
|
|
89
|
+
const missing = new Map();
|
|
90
|
+
for (const execution of this.model.executions) {
|
|
91
|
+
for (const capability of execution.capabilities) {
|
|
92
|
+
if (this.capabilities.has(capability))
|
|
93
|
+
continue;
|
|
94
|
+
const consumers = missing.get(capability.id) ?? [];
|
|
95
|
+
consumers.push(execution.id);
|
|
96
|
+
missing.set(capability.id, consumers);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (missing.size === 0)
|
|
100
|
+
return;
|
|
101
|
+
const details = [...missing]
|
|
102
|
+
.map(([id, consumers]) => `${id} (${consumers.join(', ')})`)
|
|
103
|
+
.join(', ');
|
|
104
|
+
throw new Error(`LUTRE_CAPABILITY_MISSING: ${details}`);
|
|
105
|
+
}
|
|
106
|
+
async #bindRuntimeInputs() {
|
|
107
|
+
for (const provider of this.model.providers) {
|
|
108
|
+
if (provider.kind !== 'environment')
|
|
109
|
+
continue;
|
|
110
|
+
if (this.#environmentSource === undefined) {
|
|
111
|
+
throw new Error(`LUTRE_ENV_SOURCE_MISSING: ${provider.provide.name} requires an Environment source.`);
|
|
112
|
+
}
|
|
113
|
+
this.container.bindEnvironment(provider.provide, await loadEnv(provider.provide, this.#environmentSource));
|
|
114
|
+
}
|
|
115
|
+
if (this.model.arguments) {
|
|
116
|
+
this.container.bindArguments(this.model.arguments, await loadArgs(this.model.arguments, this.#argumentsSource));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async #initializeProviders() {
|
|
120
|
+
for (const module of moduleNodes(this.model)) {
|
|
121
|
+
const instances = [];
|
|
122
|
+
this.#providerInstances.set(module.id, instances);
|
|
123
|
+
for (const providerNode of providerNodesOfModule(this.model, module.id)) {
|
|
124
|
+
const provider = providerNode.provider;
|
|
125
|
+
if (provider.scope !== 'application' ||
|
|
126
|
+
provider.kind === 'environment' ||
|
|
127
|
+
provider.kind === 'arguments') {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const instance = this.container.resolve(provider.provide);
|
|
131
|
+
instances.push(instance);
|
|
132
|
+
await callLifecycle(instance, 'onModuleInit');
|
|
133
|
+
}
|
|
134
|
+
this.#initializedModuleIds.add(module.id);
|
|
135
|
+
await this.#runHook(lifecycleHookOf(this.model, module.id, 'onModuleInit'));
|
|
136
|
+
}
|
|
137
|
+
for (const module of moduleNodes(this.model)) {
|
|
138
|
+
for (const instance of this.#providerInstances.get(module.id) ?? []) {
|
|
139
|
+
await callLifecycle(instance, 'onApplicationBootstrap');
|
|
140
|
+
}
|
|
141
|
+
await this.#runHook(lifecycleHookOf(this.model, module.id, 'onApplicationBootstrap'));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
resolve(token, source) {
|
|
145
|
+
if (this.#state === 'created' || this.#state === 'stopped') {
|
|
146
|
+
throw applicationStateError(this.#state);
|
|
147
|
+
}
|
|
148
|
+
return this.container.resolve(token, source);
|
|
149
|
+
}
|
|
150
|
+
get(token) {
|
|
151
|
+
if (this.#state !== 'running')
|
|
152
|
+
throw applicationStateError(this.#state);
|
|
153
|
+
return this.container.get(token);
|
|
154
|
+
}
|
|
155
|
+
extensionRuntime(extension) {
|
|
156
|
+
const runtime = this.#extensionRuntimes.get(extension.identity);
|
|
157
|
+
if (!runtime) {
|
|
158
|
+
throw new Error(`LUTRE_EXTENSION_RUNTIME_MISSING: ${extension.name} is not initialized.`);
|
|
159
|
+
}
|
|
160
|
+
return runtime;
|
|
161
|
+
}
|
|
162
|
+
beginExecution(metadata, invocation) {
|
|
163
|
+
if (this.#state !== 'running')
|
|
164
|
+
throw applicationStateError(this.#state);
|
|
165
|
+
const tracked = this.#executions.begin();
|
|
166
|
+
if (metadata === undefined)
|
|
167
|
+
return tracked;
|
|
168
|
+
const scope = safely(() => this.#instrumentation?.beginExecution?.(metadata, invocation));
|
|
169
|
+
if (!scope)
|
|
170
|
+
return tracked;
|
|
171
|
+
const instrumented = instrumentationLease(scope);
|
|
172
|
+
return {
|
|
173
|
+
signal: tracked.signal,
|
|
174
|
+
run: instrumented.run,
|
|
175
|
+
abort(reason) {
|
|
176
|
+
callInstrumentation(() => scope.abort?.(reason));
|
|
177
|
+
tracked.abort(reason);
|
|
178
|
+
},
|
|
179
|
+
fail: instrumented.fail,
|
|
180
|
+
annotate: instrumented.annotate,
|
|
181
|
+
complete(...result) {
|
|
182
|
+
instrumented.complete(...result);
|
|
183
|
+
tracked.complete();
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
beginOperation(metadata, invocation) {
|
|
188
|
+
return instrumentationLease(safely(() => this.#instrumentation?.beginOperation?.(metadata, invocation)));
|
|
189
|
+
}
|
|
190
|
+
#notifyProviderCreated(provider, value) {
|
|
191
|
+
const instrumentation = this.#instrumentation;
|
|
192
|
+
if (!instrumentation?.providerCreated)
|
|
193
|
+
return;
|
|
194
|
+
const providerNode = this.#providerNodes.get(provider);
|
|
195
|
+
if (!providerNode)
|
|
196
|
+
return;
|
|
197
|
+
callInstrumentation(() => instrumentation.providerCreated({
|
|
198
|
+
providerId: providerNode.id,
|
|
199
|
+
graphNodeId: providerNode.id,
|
|
200
|
+
name: tokenName(providerNode.token),
|
|
201
|
+
scope: provider.scope,
|
|
202
|
+
providerKind: provider.kind,
|
|
203
|
+
}, value));
|
|
204
|
+
}
|
|
205
|
+
shutdown(signal) {
|
|
206
|
+
if (this.#state === 'stopped')
|
|
207
|
+
return Promise.resolve();
|
|
208
|
+
if (this.#shutdown)
|
|
209
|
+
return this.#shutdown;
|
|
210
|
+
const shutdown = this.#shutdownApplication(signal).finally(() => {
|
|
211
|
+
if (this.#shutdown === shutdown)
|
|
212
|
+
this.#shutdown = undefined;
|
|
213
|
+
});
|
|
214
|
+
this.#shutdown = shutdown;
|
|
215
|
+
return shutdown;
|
|
216
|
+
}
|
|
217
|
+
async #shutdownApplication(signal) {
|
|
218
|
+
if (this.#initialization)
|
|
219
|
+
await this.#initialization.catch(() => undefined);
|
|
220
|
+
if (this.#state === 'created') {
|
|
221
|
+
this.#state = 'stopped';
|
|
222
|
+
await closeInstrumentation(this.#instrumentation);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (this.#state === 'stopped')
|
|
226
|
+
return;
|
|
227
|
+
this.#state = 'draining';
|
|
228
|
+
const errors = [];
|
|
229
|
+
const drainOperations = [];
|
|
230
|
+
for (const { extension } of this.model.extensions) {
|
|
231
|
+
const runtime = this.#extensionRuntimes.get(extension.identity);
|
|
232
|
+
if (!runtime?.drain)
|
|
233
|
+
continue;
|
|
234
|
+
const operation = this.#startExtensionDrain(extension, runtime);
|
|
235
|
+
drainOperations.push({
|
|
236
|
+
result: withTimeout(() => operation.result, this.#forceShutdownTimeoutMs, () => new ExtensionDrainTimeoutError(`LUTRE_EXTENSION_DRAIN_TIMEOUT: Extension ${extension.name} did not drain within ${this.#forceShutdownTimeoutMs}ms.`)),
|
|
237
|
+
isPending: () => operation.pending,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
const drainResults = await Promise.allSettled(drainOperations.map(({ result }) => result));
|
|
241
|
+
let drainFailed = false;
|
|
242
|
+
const timedOutDrains = [];
|
|
243
|
+
for (const [index, result] of drainResults.entries()) {
|
|
244
|
+
if (result.status === 'fulfilled')
|
|
245
|
+
continue;
|
|
246
|
+
drainFailed = true;
|
|
247
|
+
if (result.reason instanceof ExtensionDrainTimeoutError) {
|
|
248
|
+
timedOutDrains.push(drainOperations[index]);
|
|
249
|
+
}
|
|
250
|
+
errors.push(result.reason);
|
|
251
|
+
}
|
|
252
|
+
if (this.#executions.size > 0) {
|
|
253
|
+
if (drainFailed) {
|
|
254
|
+
const reason = new Error('LUTRE_APPLICATION_FORCE_SHUTDOWN: Extension drain failed.');
|
|
255
|
+
this.#executions.abortAll(reason);
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
await this.#executions.waitForIdle(this.#forceShutdownTimeoutMs);
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
errors.push(error);
|
|
262
|
+
throw new AggregateError(errors, 'Application shutdown did not reach a safe cleanup boundary.', { cause: error });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (timedOutDrains.some(({ isPending }) => isPending())) {
|
|
266
|
+
const cause = errors.find((error) => error instanceof ExtensionDrainTimeoutError);
|
|
267
|
+
throw new AggregateError(errors, 'Application shutdown did not reach a safe cleanup boundary.', { cause });
|
|
268
|
+
}
|
|
269
|
+
for (const { extension } of [...this.model.extensions].toReversed()) {
|
|
270
|
+
const runtime = this.#extensionRuntimes.get(extension.identity);
|
|
271
|
+
if (runtime?.close)
|
|
272
|
+
await collectError(() => runtime.close(), errors);
|
|
273
|
+
}
|
|
274
|
+
this.#extensionDrainOperations.clear();
|
|
275
|
+
await this.#cleanupProviders(signal, errors);
|
|
276
|
+
this.#state = 'stopped';
|
|
277
|
+
await closeInstrumentation(this.#instrumentation);
|
|
278
|
+
if (errors.length > 0) {
|
|
279
|
+
throw new AggregateError(errors, 'Application shutdown failed.');
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
#startExtensionDrain(extension, runtime) {
|
|
283
|
+
const existing = this.#extensionDrainOperations.get(extension.identity);
|
|
284
|
+
if (existing)
|
|
285
|
+
return existing;
|
|
286
|
+
const operation = {
|
|
287
|
+
result: startOperation(() => runtime.drain({ timeoutMs: this.#forceShutdownTimeoutMs })),
|
|
288
|
+
pending: true,
|
|
289
|
+
};
|
|
290
|
+
this.#extensionDrainOperations.set(extension.identity, operation);
|
|
291
|
+
void operation.result.then(() => {
|
|
292
|
+
operation.pending = false;
|
|
293
|
+
}, () => {
|
|
294
|
+
operation.pending = false;
|
|
295
|
+
if (this.#extensionDrainOperations.get(extension.identity) === operation) {
|
|
296
|
+
this.#extensionDrainOperations.delete(extension.identity);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
return operation;
|
|
300
|
+
}
|
|
301
|
+
async #cleanupProviders(signal, errors) {
|
|
302
|
+
const modules = moduleNodes(this.model).toReversed();
|
|
303
|
+
for (const module of modules) {
|
|
304
|
+
for (const instance of (this.#providerInstances.get(module.id) ?? []).toReversed()) {
|
|
305
|
+
await collectError(() => callLifecycle(instance, 'onModuleDestroy'), errors);
|
|
306
|
+
}
|
|
307
|
+
await collectError(() => this.#runCleanupHook(module.id, 'onModuleDestroy'), errors);
|
|
308
|
+
}
|
|
309
|
+
for (const module of modules) {
|
|
310
|
+
for (const instance of (this.#providerInstances.get(module.id) ?? []).toReversed()) {
|
|
311
|
+
await collectError(() => callLifecycle(instance, 'beforeApplicationShutdown', signal), errors);
|
|
312
|
+
}
|
|
313
|
+
await collectError(() => this.#runCleanupHook(module.id, 'beforeApplicationShutdown'), errors);
|
|
314
|
+
}
|
|
315
|
+
for (const module of modules) {
|
|
316
|
+
for (const instance of (this.#providerInstances.get(module.id) ?? []).toReversed()) {
|
|
317
|
+
await collectError(() => callLifecycle(instance, 'onApplicationShutdown', signal), errors);
|
|
318
|
+
}
|
|
319
|
+
await collectError(() => this.#runCleanupHook(module.id, 'onApplicationShutdown'), errors);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async #rollbackInitialization() {
|
|
323
|
+
const errors = [];
|
|
324
|
+
for (const runtime of [...this.#extensionRuntimes.values()].toReversed()) {
|
|
325
|
+
if (runtime.close)
|
|
326
|
+
await collectError(() => runtime.close(), errors);
|
|
327
|
+
}
|
|
328
|
+
for (const module of moduleNodes(this.model).toReversed()) {
|
|
329
|
+
await collectError(() => this.#runCleanupHook(module.id, 'onModuleDestroy'), errors);
|
|
330
|
+
for (const instance of (this.#providerInstances.get(module.id) ?? []).toReversed()) {
|
|
331
|
+
await collectError(() => callLifecycle(instance, 'onModuleDestroy'), errors);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
await closeInstrumentation(this.#instrumentation);
|
|
335
|
+
return errors;
|
|
336
|
+
}
|
|
337
|
+
async #runHook(hook) {
|
|
338
|
+
if (!hook)
|
|
339
|
+
return;
|
|
340
|
+
const dependencies = hook.inject.map((token) => this.container.resolve(token));
|
|
341
|
+
await hook.run(...dependencies);
|
|
342
|
+
}
|
|
343
|
+
async #runCleanupHook(moduleId, phase) {
|
|
344
|
+
if (!this.#initializedModuleIds.has(moduleId))
|
|
345
|
+
return;
|
|
346
|
+
await this.#runHook(lifecycleHookOf(this.model, moduleId, phase));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function moduleNodes(model) {
|
|
350
|
+
return model.nodes.filter((node) => node.kind === 'module');
|
|
351
|
+
}
|
|
352
|
+
function providerNodesOfModule(model, moduleId) {
|
|
353
|
+
return model.nodes.filter((node) => node.kind === 'provider' && node.moduleId === moduleId);
|
|
354
|
+
}
|
|
355
|
+
function lifecycleHookOf(model, moduleId, phase) {
|
|
356
|
+
return model.nodes.find((node) => node.kind === 'lifecycle' &&
|
|
357
|
+
node.moduleId === moduleId &&
|
|
358
|
+
node.phase === phase)?.hook;
|
|
359
|
+
}
|
|
360
|
+
function instrumentationLease(scope) {
|
|
361
|
+
return {
|
|
362
|
+
run(operation) {
|
|
363
|
+
if (!scope?.run)
|
|
364
|
+
return operation();
|
|
365
|
+
return scope.run(operation);
|
|
366
|
+
},
|
|
367
|
+
fail(reason) {
|
|
368
|
+
callInstrumentation(() => scope?.fail?.(reason));
|
|
369
|
+
},
|
|
370
|
+
annotate(attributes) {
|
|
371
|
+
callInstrumentation(() => scope?.annotate?.(attributes));
|
|
372
|
+
},
|
|
373
|
+
complete(...result) {
|
|
374
|
+
callInstrumentation(() => scope?.complete(...result));
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function safely(operation) {
|
|
379
|
+
try {
|
|
380
|
+
return operation();
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function callInstrumentation(operation) {
|
|
387
|
+
try {
|
|
388
|
+
operation();
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
// instrumentation障害をApplication semanticsへ伝播させてはならない。
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
async function closeInstrumentation(instrumentation) {
|
|
395
|
+
try {
|
|
396
|
+
await instrumentation?.close?.();
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
// instrumentationのcleanup失敗をshutdown失敗として扱わない。
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
async function withTimeout(operation, timeoutMs, createTimeoutError) {
|
|
403
|
+
let timer;
|
|
404
|
+
try {
|
|
405
|
+
return await Promise.race([
|
|
406
|
+
Promise.resolve(operation()),
|
|
407
|
+
new Promise((_, reject) => {
|
|
408
|
+
timer = setTimeout(() => reject(createTimeoutError()), timeoutMs);
|
|
409
|
+
}),
|
|
410
|
+
]);
|
|
411
|
+
}
|
|
412
|
+
finally {
|
|
413
|
+
if (timer !== undefined)
|
|
414
|
+
clearTimeout(timer);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
function startOperation(operation) {
|
|
418
|
+
try {
|
|
419
|
+
return Promise.resolve(operation());
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
return Promise.reject(error);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
class ExtensionDrainTimeoutError extends Error {
|
|
426
|
+
}
|
|
427
|
+
async function collectError(operation, errors) {
|
|
428
|
+
try {
|
|
429
|
+
await operation();
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
errors.push(error);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
async function callLifecycle(instance, method, signal) {
|
|
436
|
+
if (!instance ||
|
|
437
|
+
(typeof instance !== 'object' && typeof instance !== 'function')) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const candidate = instance[method];
|
|
441
|
+
if (typeof candidate !== 'function')
|
|
442
|
+
return;
|
|
443
|
+
await Reflect.apply(candidate, instance, signal === undefined ? [] : [signal]);
|
|
444
|
+
}
|
|
445
|
+
function applicationStateError(state) {
|
|
446
|
+
return new Error(`LUTRE_APPLICATION_STATE: Application is ${state} and cannot perform this operation.`);
|
|
447
|
+
}
|
package/dist/runtime/logger.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { frameworkProvidedTokenBrand } from '../core/token-internal.js';
|
|
1
2
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
2
3
|
export interface LogRecord {
|
|
3
4
|
readonly timestamp: string;
|
|
@@ -24,6 +25,7 @@ export declare class Logger {
|
|
|
24
25
|
#private;
|
|
25
26
|
readonly backend: LoggerBackend;
|
|
26
27
|
readonly context: Readonly<Record<string, unknown>>;
|
|
28
|
+
static readonly [frameworkProvidedTokenBrand] = true;
|
|
27
29
|
constructor(backend?: LoggerBackend, context?: Readonly<Record<string, unknown>>);
|
|
28
30
|
child(context: Readonly<Record<string, unknown>>): Logger;
|
|
29
31
|
debug(message: string, context?: Readonly<Record<string, unknown>>): void;
|
package/dist/runtime/logger.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { frameworkProvidedTokenBrand } from '../core/token-internal.js';
|
|
1
2
|
export class ConsoleLoggerBackend {
|
|
2
3
|
#colors;
|
|
3
4
|
#prefix;
|
|
@@ -20,6 +21,7 @@ class SilentLoggerBackend {
|
|
|
20
21
|
export class Logger {
|
|
21
22
|
backend;
|
|
22
23
|
context;
|
|
24
|
+
static [frameworkProvidedTokenBrand] = true;
|
|
23
25
|
constructor(backend = new ConsoleLoggerBackend(), context = {}) {
|
|
24
26
|
this.backend = backend;
|
|
25
27
|
this.context = context;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** CLIが`@loutrejs/node`へ依存するとBun/DenoでもNode.js専用moduleを評価するため、Capability metadataだけをruntime-neutral側へ分離する。 */
|
|
2
|
-
export declare const
|
|
2
|
+
export declare const nodeRuntimeSupport: {
|
|
3
3
|
readonly runtime: 'node';
|
|
4
4
|
readonly capabilities: Set<string>;
|
|
5
5
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function initialServerPort(requestedPort) {
|
|
2
|
+
return requestedPort ?? 3000;
|
|
3
|
+
}
|
|
4
|
+
export function canRetryOnNextPort(error, port) {
|
|
5
|
+
return port < 65_535 && isAddressInUseError(error);
|
|
6
|
+
}
|
|
7
|
+
function isAddressInUseError(error) {
|
|
8
|
+
return (error instanceof Error &&
|
|
9
|
+
'code' in error &&
|
|
10
|
+
error.code === 'EADDRINUSE');
|
|
11
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { type ExecutionDefinition, type ProviderDescriptor, type SchemaOutput, type StandardSchemaV1, type Token } from '../core/index.js';
|
|
2
|
+
export type TaskRuntime<TInput, TOutput> = [TInput] extends [void] ? () => TOutput | Promise<TOutput> : (input: TInput) => TOutput | Promise<TOutput>;
|
|
3
|
+
export interface TaskDefinitionData<TInput = unknown, TOutput = unknown> {
|
|
4
|
+
readonly type: 'task';
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly factory: () => TaskRuntime<TInput, TOutput>;
|
|
7
|
+
readonly '~input'?: TInput;
|
|
8
|
+
readonly '~output'?: TOutput;
|
|
9
|
+
}
|
|
10
|
+
export type TaskInput<TTask> = TTask extends TaskDefinitionData<infer TInput, any> ? TInput : never;
|
|
11
|
+
export type TaskOutput<TTask> = TTask extends TaskDefinitionData<any, infer TOutput> ? TOutput : never;
|
|
12
|
+
export type TaskArguments<TTask> = [TaskInput<TTask>] extends [void] ? readonly [] : readonly [input: TaskInput<TTask>];
|
|
13
|
+
declare const taskExecutionIdentityKey: unique symbol;
|
|
14
|
+
declare const queueDriverToken: unique symbol;
|
|
15
|
+
export interface TaskExecutionIdentity {
|
|
16
|
+
readonly [taskExecutionIdentityKey]: symbol;
|
|
17
|
+
}
|
|
18
|
+
type AnyTaskDefinition = ExecutionDefinition & TaskExecutionIdentity & {
|
|
19
|
+
readonly type: 'task';
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly factory: () => Function;
|
|
22
|
+
};
|
|
23
|
+
type VoidTaskConstraint<TTask> = [TaskInput<TTask>] extends [void] ? unknown : never;
|
|
24
|
+
export type CronOverlap = 'allow' | 'skip';
|
|
25
|
+
export interface CronTriggerDefinitionData<TTask extends AnyTaskDefinition = AnyTaskDefinition> {
|
|
26
|
+
readonly type: 'cron';
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly expression: string;
|
|
29
|
+
readonly timezone: string;
|
|
30
|
+
readonly overlap: CronOverlap;
|
|
31
|
+
readonly task: TTask;
|
|
32
|
+
}
|
|
33
|
+
export interface FixedDelayTriggerDefinitionData<TTask extends AnyTaskDefinition = AnyTaskDefinition> {
|
|
34
|
+
readonly type: 'fixed-delay';
|
|
35
|
+
readonly name: string;
|
|
36
|
+
readonly delay: number;
|
|
37
|
+
readonly immediate: boolean;
|
|
38
|
+
readonly task: TTask;
|
|
39
|
+
}
|
|
40
|
+
export interface QueueDescriptor<TSchema extends StandardSchemaV1 = StandardSchemaV1, TName extends string = string> {
|
|
41
|
+
readonly kind: 'queue';
|
|
42
|
+
readonly name: TName;
|
|
43
|
+
readonly payload: TSchema;
|
|
44
|
+
readonly [queueDriverToken]: Token<QueueConsumerDriver>;
|
|
45
|
+
}
|
|
46
|
+
export type QueuePayload<TQueue> = TQueue extends QueueDescriptor<infer TSchema, any> ? SchemaOutput<TSchema> : never;
|
|
47
|
+
export interface QueueConsumerHandle {
|
|
48
|
+
stop(): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
export interface QueueConsumerDriver {
|
|
51
|
+
start(options: {
|
|
52
|
+
consume(payload: unknown): Promise<void>;
|
|
53
|
+
}): Promise<QueueConsumerHandle>;
|
|
54
|
+
}
|
|
55
|
+
export interface QueueConsumerTriggerDefinitionData<TQueue extends QueueDescriptor<any> = QueueDescriptor<any>, TTask extends AnyTaskDefinition = AnyTaskDefinition> {
|
|
56
|
+
readonly type: 'queue-consumer';
|
|
57
|
+
readonly name: string;
|
|
58
|
+
readonly queue: TQueue;
|
|
59
|
+
readonly task: TTask;
|
|
60
|
+
}
|
|
61
|
+
export type TriggerDefinitionData = CronTriggerDefinitionData | FixedDelayTriggerDefinitionData | QueueConsumerTriggerDefinitionData;
|
|
62
|
+
type TasksDefinitionData = (TaskDefinitionData & TaskExecutionIdentity) | TriggerDefinitionData;
|
|
63
|
+
export interface TasksCompiledTask {
|
|
64
|
+
readonly type: 'task';
|
|
65
|
+
readonly taskExecutionId: symbol;
|
|
66
|
+
readonly name: string;
|
|
67
|
+
readonly factory: () => (...arguments_: any[]) => any;
|
|
68
|
+
}
|
|
69
|
+
export interface TasksCompiledCronTrigger {
|
|
70
|
+
readonly type: 'cron';
|
|
71
|
+
readonly name: string;
|
|
72
|
+
readonly expression: string;
|
|
73
|
+
readonly timezone: string;
|
|
74
|
+
readonly overlap: CronOverlap;
|
|
75
|
+
readonly taskExecutionId: symbol;
|
|
76
|
+
readonly taskName: string;
|
|
77
|
+
}
|
|
78
|
+
export interface TasksCompiledFixedDelayTrigger {
|
|
79
|
+
readonly type: 'fixed-delay';
|
|
80
|
+
readonly name: string;
|
|
81
|
+
readonly delay: number;
|
|
82
|
+
readonly immediate: boolean;
|
|
83
|
+
readonly taskExecutionId: symbol;
|
|
84
|
+
readonly taskName: string;
|
|
85
|
+
}
|
|
86
|
+
export interface TasksCompiledQueueConsumerTrigger {
|
|
87
|
+
readonly type: 'queue-consumer';
|
|
88
|
+
readonly name: string;
|
|
89
|
+
readonly queueName: string;
|
|
90
|
+
readonly queuePayload: StandardSchemaV1;
|
|
91
|
+
readonly queueDriver: Token<QueueConsumerDriver>;
|
|
92
|
+
readonly taskExecutionId: symbol;
|
|
93
|
+
readonly taskName: string;
|
|
94
|
+
}
|
|
95
|
+
export type TasksCompiledExecution = TasksCompiledTask | TasksCompiledCronTrigger | TasksCompiledFixedDelayTrigger | TasksCompiledQueueConsumerTrigger;
|
|
96
|
+
export interface TasksExtensionRuntime {
|
|
97
|
+
run<TInput, TOutput>(taskExecutionId: symbol, taskName: string, ...arguments_: [TInput] extends [void] ? readonly [] : readonly [input: TInput]): Promise<TOutput>;
|
|
98
|
+
startTriggers(): Promise<void>;
|
|
99
|
+
stopTriggers(): Promise<void>;
|
|
100
|
+
drain(): Promise<void>;
|
|
101
|
+
close(): Promise<void>;
|
|
102
|
+
}
|
|
103
|
+
export interface TasksHostApi {
|
|
104
|
+
run<TInput, TOutput>(task: TaskDefinitionData<TInput, TOutput> & ExecutionDefinition & TaskExecutionIdentity, ...arguments_: [TInput] extends [void] ? readonly [] : readonly [input: TInput]): Promise<TOutput>;
|
|
105
|
+
start(): Promise<void>;
|
|
106
|
+
stop(): Promise<void>;
|
|
107
|
+
}
|
|
108
|
+
export declare const tasksExtension: import("../core/extension.js").ExecutionExtension<TasksDefinitionData & ExecutionDefinition<import("../core/extension.js").AnyExecutionExtension>, TasksCompiledExecution, "tasks", TasksHostApi, TasksExtensionRuntime>;
|
|
109
|
+
export type TaskDefinition<TInput = unknown, TOutput = unknown> = TaskDefinitionData<TInput, TOutput> & ExecutionDefinition<typeof tasksExtension> & TaskExecutionIdentity;
|
|
110
|
+
export type CronTriggerDefinition<TTask extends TaskDefinition<void, any> = TaskDefinition<void, any>> = CronTriggerDefinitionData<TTask> & ExecutionDefinition<typeof tasksExtension>;
|
|
111
|
+
export type FixedDelayTriggerDefinition<TTask extends TaskDefinition<void, any> = TaskDefinition<void, any>> = FixedDelayTriggerDefinitionData<TTask> & ExecutionDefinition<typeof tasksExtension>;
|
|
112
|
+
export type QueueConsumerTriggerDefinition<TQueue extends QueueDescriptor<any> = QueueDescriptor<any>, TTask extends AnyTaskDefinition = TaskDefinition<any, any>> = QueueConsumerTriggerDefinitionData<TQueue, TTask> & ExecutionDefinition<typeof tasksExtension>;
|
|
113
|
+
export type TriggerDefinition = CronTriggerDefinition | FixedDelayTriggerDefinition | QueueConsumerTriggerDefinition;
|
|
114
|
+
export declare function task<TInput = void, TOutput = void>(definition: {
|
|
115
|
+
readonly name: string;
|
|
116
|
+
readonly factory: () => TaskRuntime<TInput, TOutput>;
|
|
117
|
+
}): TaskDefinition<TInput, TOutput>;
|
|
118
|
+
export declare function cron<const TTask extends AnyTaskDefinition>(declaration: {
|
|
119
|
+
readonly name: string;
|
|
120
|
+
readonly expression: string;
|
|
121
|
+
readonly timezone: string;
|
|
122
|
+
readonly overlap?: CronOverlap;
|
|
123
|
+
readonly task: TTask;
|
|
124
|
+
} & VoidTaskConstraint<TTask>): CronTriggerDefinition<TTask & TaskDefinition<void, any>>;
|
|
125
|
+
export declare function fixedDelay<const TTask extends AnyTaskDefinition>(declaration: {
|
|
126
|
+
readonly name: string;
|
|
127
|
+
readonly delay: number;
|
|
128
|
+
readonly immediate?: boolean;
|
|
129
|
+
readonly task: TTask;
|
|
130
|
+
} & VoidTaskConstraint<TTask>): FixedDelayTriggerDefinition<TTask & TaskDefinition<void, any>>;
|
|
131
|
+
export declare function queue<const TSchema extends StandardSchemaV1, const TName extends string = string>(declaration: {
|
|
132
|
+
readonly name: TName;
|
|
133
|
+
readonly payload: TSchema;
|
|
134
|
+
}): QueueDescriptor<TSchema, TName>;
|
|
135
|
+
export declare function bindQueueDriver(descriptor: QueueDescriptor, driver: QueueConsumerDriver): ProviderDescriptor;
|
|
136
|
+
type ConsumerTaskConstraint<TQueue, TTask> = [TaskInput<TTask>] extends [
|
|
137
|
+
QueuePayload<TQueue>
|
|
138
|
+
] ? [QueuePayload<TQueue>] extends [TaskInput<TTask>] ? unknown : never : never;
|
|
139
|
+
export declare function consume<const TQueue extends QueueDescriptor<any>, const TTask extends AnyTaskDefinition>(declaration: {
|
|
140
|
+
readonly name: string;
|
|
141
|
+
readonly queue: TQueue;
|
|
142
|
+
readonly task: TTask;
|
|
143
|
+
} & ConsumerTaskConstraint<TQueue, TTask>): QueueConsumerTriggerDefinition<TQueue, TTask & TaskDefinition<QueuePayload<TQueue>, any>>;
|
|
144
|
+
export {};
|