@arki/dot 0.3.0 → 0.4.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 +86 -86
- package/dist/cli/discover.js +2 -2
- package/dist/cli/discover.js.map +1 -1
- package/dist/cli/error-codes.d.ts +8 -0
- package/dist/cli/error-codes.d.ts.map +1 -1
- package/dist/cli/error-codes.js +12 -0
- package/dist/cli/error-codes.js.map +1 -1
- package/dist/cli/index.d.ts +9 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +54 -23
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/new.d.ts +1 -1
- package/dist/cli/new.js +1 -1
- package/dist/cli/render-doctor.d.ts +1 -1
- package/dist/cli/render-doctor.js +23 -23
- package/dist/cli/render-doctor.js.map +1 -1
- package/dist/cli/render-explain.d.ts.map +1 -1
- package/dist/cli/render-explain.js +33 -19
- package/dist/cli/render-explain.js.map +1 -1
- package/dist/cli/render-graph.d.ts +3 -3
- package/dist/cli/render-graph.js +12 -12
- package/dist/cli/render-graph.js.map +1 -1
- package/dist/cli/render-projection.d.ts +29 -0
- package/dist/cli/render-projection.d.ts.map +1 -0
- package/dist/cli/render-projection.js +202 -0
- package/dist/cli/render-projection.js.map +1 -0
- package/dist/define-app.d.ts +49 -17
- package/dist/define-app.d.ts.map +1 -1
- package/dist/define-app.js +16 -9
- package/dist/define-app.js.map +1 -1
- package/dist/diagnostics.d.ts +19 -15
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +2 -2
- package/dist/index.d.ts +15 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/init-pips.d.ts +45 -0
- package/dist/init-pips.d.ts.map +1 -0
- package/dist/init-pips.js +49 -0
- package/dist/init-pips.js.map +1 -0
- package/dist/init-plugins.d.ts +45 -0
- package/dist/init-plugins.d.ts.map +1 -0
- package/dist/init-plugins.js +49 -0
- package/dist/init-plugins.js.map +1 -0
- package/dist/kernel/app-instance.d.ts +5 -5
- package/dist/kernel/app-instance.d.ts.map +1 -1
- package/dist/kernel/app-instance.js +375 -244
- package/dist/kernel/app-instance.js.map +1 -1
- package/dist/kernel/otel.d.ts +19 -19
- package/dist/kernel/otel.d.ts.map +1 -1
- package/dist/kernel/otel.js +22 -22
- package/dist/kernel/otel.js.map +1 -1
- package/dist/lifecycle-observer.d.ts +10 -10
- package/dist/lifecycle-observer.d.ts.map +1 -1
- package/dist/lifecycle-observer.js +1 -1
- package/dist/lifecycle.d.ts +18 -14
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +13 -9
- package/dist/lifecycle.js.map +1 -1
- package/dist/manifest.d.ts +70 -48
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +74 -5
- package/dist/manifest.js.map +1 -1
- package/dist/pip-contract.d.ts +29 -21
- package/dist/pip-contract.d.ts.map +1 -1
- package/dist/pip-contract.js +1 -0
- package/dist/pip-contract.js.map +1 -1
- package/dist/pip.d.ts +7 -4
- package/dist/pip.d.ts.map +1 -1
- package/dist/pip.js +2 -0
- package/dist/pip.js.map +1 -1
- package/dist/plugin-contract.d.ts +335 -143
- package/dist/plugin-contract.d.ts.map +1 -1
- package/dist/plugin-contract.js +201 -36
- package/dist/plugin-contract.js.map +1 -1
- package/dist/plugin.d.ts +18 -15
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +11 -11
- package/dist/plugin.js.map +1 -1
- package/dist/test-harness.d.ts +31 -31
- package/dist/test-harness.d.ts.map +1 -1
- package/dist/test-harness.js +29 -28
- package/dist/test-harness.js.map +1 -1
- package/dist/timeline.d.ts +2 -2
- package/dist/timeline.js +5 -5
- package/dist/timeline.js.map +1 -1
- package/package.json +9 -9
- package/src/cli/discover.ts +2 -2
- package/src/cli/error-codes.ts +12 -0
- package/src/cli/index.ts +76 -29
- package/src/cli/new.ts +1 -1
- package/src/cli/render-doctor.ts +21 -21
- package/src/cli/render-explain.ts +37 -19
- package/src/cli/render-graph.ts +12 -12
- package/src/cli/render-projection.ts +239 -0
- package/src/define-app.ts +85 -22
- package/src/diagnostics.ts +19 -15
- package/src/index.ts +37 -20
- package/src/init-plugins.ts +142 -0
- package/src/kernel/app-instance.ts +428 -282
- package/src/kernel/otel.ts +27 -27
- package/src/lifecycle-observer.ts +10 -10
- package/src/lifecycle.ts +19 -15
- package/src/manifest.ts +128 -50
- package/src/{pip-contract.ts → plugin-contract.ts} +73 -62
- package/src/{pip.ts → plugin.ts} +37 -18
- package/src/test-harness.ts +52 -51
- package/src/timeline.ts +6 -6
- package/templates/app-minimal/AGENTS.md.tmpl +9 -9
- package/templates/app-minimal/README.md.tmpl +1 -1
- package/templates/app-minimal/tests/boot.test.ts.tmpl +1 -1
- package/src/cli/render-openapi.ts +0 -147
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
import { Logger } from '@arki/log';
|
|
8
8
|
import { createDebugLogger } from '@arki/log/debug';
|
|
9
9
|
import { DotLifecycleError, DotLifecycleErrorCode } from '../lifecycle.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { toJsonObject } from '../manifest.js';
|
|
11
|
+
import { isLazy, isLazyWitness, lazyOf } from '../plugin-contract.js';
|
|
12
|
+
import { withPhaseSpan, withPluginHookSpan } from './otel.js';
|
|
12
13
|
const debugKernel = createDebugLogger('arki:dot:kernel');
|
|
13
14
|
const DOCS_BASE = 'https://docs.arki.dev/dot/diagnostics';
|
|
14
15
|
/** Resolve a needs-shape witness to its wire key. */
|
|
@@ -22,7 +23,7 @@ function makeIssue(args) {
|
|
|
22
23
|
return {
|
|
23
24
|
code: args.code,
|
|
24
25
|
severity: args.severity ?? 'error',
|
|
25
|
-
|
|
26
|
+
plugin: args.plugin,
|
|
26
27
|
message: args.message,
|
|
27
28
|
remediation: args.remediation,
|
|
28
29
|
docsUrl: `${DOCS_BASE}#${args.docsAnchor}`,
|
|
@@ -37,7 +38,7 @@ export class DotAppImpl {
|
|
|
37
38
|
#appName;
|
|
38
39
|
#appVersion;
|
|
39
40
|
#config;
|
|
40
|
-
/**
|
|
41
|
+
/** Plugins in declaration order — v2 boot order IS declaration order. */
|
|
41
42
|
#ordered;
|
|
42
43
|
#records;
|
|
43
44
|
/** Macro-state of the app. */
|
|
@@ -46,9 +47,9 @@ export class DotAppImpl {
|
|
|
46
47
|
#hookTimeoutMs;
|
|
47
48
|
/** Manifest finalised after `configure`. */
|
|
48
49
|
#manifest;
|
|
49
|
-
/** Wire-keyed services map — populated as
|
|
50
|
+
/** Wire-keyed services map — populated as plugins boot. */
|
|
50
51
|
#serviceMap = new Map();
|
|
51
|
-
/** Which
|
|
52
|
+
/** Which plugin provided each wire key — feeds dependency edges + collisions. */
|
|
52
53
|
#providerByWireKey = new Map();
|
|
53
54
|
/** Dependency edges observed during boot (consumer → provider). */
|
|
54
55
|
#wiringEdges = [];
|
|
@@ -77,7 +78,7 @@ export class DotAppImpl {
|
|
|
77
78
|
* Structured lifecycle logger. One per app instance; named so consumers
|
|
78
79
|
* can elevate it to DEBUG via `DEBUG=arki:dot:lifecycle` without
|
|
79
80
|
* touching unrelated namespaces. Every line carries the app name plus
|
|
80
|
-
* any phase/
|
|
81
|
+
* any phase/plugin attributes from the call site. The span helpers
|
|
81
82
|
* (see `./otel.ts`) thread the active span's `traceId`+`spanId` onto
|
|
82
83
|
* forked instances of this logger, so every log record is groupable
|
|
83
84
|
* with its trace in any OTel-compatible backend.
|
|
@@ -99,47 +100,48 @@ export class DotAppImpl {
|
|
|
99
100
|
this.#observers = new Set(config.observers);
|
|
100
101
|
// Declaration order is boot order in v2. Duplicate names are surfaced at
|
|
101
102
|
// construction (the `configure` phase pseudo-time).
|
|
102
|
-
debugKernel('[%s] registering %d
|
|
103
|
-
this.#ordered = [...config.
|
|
103
|
+
debugKernel('[%s] registering %d plugin(s) in declaration order', this.#appName, config.plugins.length);
|
|
104
|
+
this.#ordered = [...config.plugins];
|
|
104
105
|
this.#records = new Map();
|
|
105
|
-
for (const [order,
|
|
106
|
-
if (this.#records.has(
|
|
106
|
+
for (const [order, plugin] of this.#ordered.entries()) {
|
|
107
|
+
if (this.#records.has(plugin.name)) {
|
|
107
108
|
throw new DotLifecycleError({
|
|
108
|
-
code: DotLifecycleErrorCode.
|
|
109
|
+
code: DotLifecycleErrorCode.DuplicatePlugin,
|
|
109
110
|
phase: 'configure',
|
|
110
|
-
|
|
111
|
-
message: `
|
|
111
|
+
plugin: plugin.name,
|
|
112
|
+
message: `Plugin "${plugin.name}" is registered twice`,
|
|
112
113
|
});
|
|
113
114
|
}
|
|
114
|
-
// `$` is the kernel context namespace ($app/$
|
|
115
|
+
// `$` is the kernel context namespace ($app/$plugin/$config). The plugin()
|
|
115
116
|
// constraint bans these at compile time; this is the runtime backstop
|
|
116
|
-
// for erased
|
|
117
|
-
for (const alias of Object.keys(
|
|
117
|
+
// for erased plugins and rename() targets, which types cannot see.
|
|
118
|
+
for (const alias of Object.keys(plugin.needs)) {
|
|
118
119
|
if (alias.startsWith('$')) {
|
|
119
120
|
throw new DotLifecycleError({
|
|
120
121
|
code: DotLifecycleErrorCode.ReservedServiceKey,
|
|
121
122
|
phase: 'configure',
|
|
122
|
-
|
|
123
|
-
message: `
|
|
124
|
-
`for kernel context keys ($app, $
|
|
123
|
+
plugin: plugin.name,
|
|
124
|
+
message: `Plugin "${plugin.name}" declares needs alias "${alias}" — the "$" prefix is reserved ` +
|
|
125
|
+
`for kernel context keys ($app, $plugin, $config). Rename the alias.`,
|
|
125
126
|
});
|
|
126
127
|
}
|
|
127
128
|
}
|
|
128
|
-
for (const [localKey, wireKey] of Object.entries(
|
|
129
|
+
for (const [localKey, wireKey] of Object.entries(plugin.renames)) {
|
|
129
130
|
if (wireKey.startsWith('$')) {
|
|
130
131
|
throw new DotLifecycleError({
|
|
131
132
|
code: DotLifecycleErrorCode.ReservedServiceKey,
|
|
132
133
|
phase: 'configure',
|
|
133
|
-
|
|
134
|
-
message: `
|
|
135
|
-
`reserved for kernel context keys ($app, $
|
|
134
|
+
plugin: plugin.name,
|
|
135
|
+
message: `Plugin "${plugin.name}" renames "${localKey}" to "${wireKey}" — the "$" prefix is ` +
|
|
136
|
+
`reserved for kernel context keys ($app, $plugin, $config). Pick a different wire key.`,
|
|
136
137
|
});
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
|
-
this.#records.set(
|
|
140
|
-
|
|
140
|
+
this.#records.set(plugin.name, {
|
|
141
|
+
plugin,
|
|
141
142
|
order,
|
|
142
|
-
|
|
143
|
+
actions: [],
|
|
144
|
+
projections: [],
|
|
143
145
|
services: [],
|
|
144
146
|
hooks: new Set(),
|
|
145
147
|
provides: new Set(),
|
|
@@ -199,13 +201,13 @@ export class DotAppImpl {
|
|
|
199
201
|
* Emit a single hook-level event. Skipped (zero allocation) when no
|
|
200
202
|
* observers are registered.
|
|
201
203
|
*/
|
|
202
|
-
#emitHook(phase,
|
|
204
|
+
#emitHook(phase, plugin, order, status, opts) {
|
|
203
205
|
if (this.#observers.size === 0)
|
|
204
206
|
return;
|
|
205
207
|
const event = {
|
|
206
|
-
kind: '
|
|
208
|
+
kind: 'plugin-hook',
|
|
207
209
|
phase,
|
|
208
|
-
|
|
210
|
+
plugin,
|
|
209
211
|
order,
|
|
210
212
|
status,
|
|
211
213
|
appName: this.#appName,
|
|
@@ -215,6 +217,89 @@ export class DotAppImpl {
|
|
|
215
217
|
};
|
|
216
218
|
this.#emit(event);
|
|
217
219
|
}
|
|
220
|
+
#registerAction(record, action) {
|
|
221
|
+
let meta;
|
|
222
|
+
if (action.meta !== undefined) {
|
|
223
|
+
try {
|
|
224
|
+
meta = toJsonObject(action.meta);
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
const issue = makeIssue({
|
|
228
|
+
code: DotLifecycleErrorCode.ActionMetaNotJson,
|
|
229
|
+
plugin: record.plugin.name,
|
|
230
|
+
message: `action "${action.id}" for binding "${action.binding}" has metadata that is not JSON-round-trip safe: ` +
|
|
231
|
+
stringifyError(error),
|
|
232
|
+
remediation: 'Only put JSON-serializable data in action meta. Convert Dates to strings, remove functions/undefined fields, and avoid NaN/Infinity.',
|
|
233
|
+
docsAnchor: 'action-meta-not-json',
|
|
234
|
+
metadata: { actionId: action.id, binding: action.binding },
|
|
235
|
+
});
|
|
236
|
+
record.issues.push(issue);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const stamped = {
|
|
240
|
+
id: action.id,
|
|
241
|
+
plugin: record.plugin.name,
|
|
242
|
+
binding: action.binding,
|
|
243
|
+
direction: action.direction,
|
|
244
|
+
...(action.address === undefined ? {} : { address: action.address }),
|
|
245
|
+
...(action.summary === undefined ? {} : { summary: action.summary }),
|
|
246
|
+
...(meta === undefined ? {} : { meta }),
|
|
247
|
+
...(action.metaSchema === undefined ? {} : { metaSchema: action.metaSchema }),
|
|
248
|
+
};
|
|
249
|
+
for (const existingRecord of this.#records.values()) {
|
|
250
|
+
const duplicate = existingRecord.actions.find(existing => existing.binding === stamped.binding && existing.id === stamped.id);
|
|
251
|
+
if (duplicate === undefined)
|
|
252
|
+
continue;
|
|
253
|
+
const issue = makeIssue({
|
|
254
|
+
code: DotLifecycleErrorCode.DuplicateAction,
|
|
255
|
+
plugin: record.plugin.name,
|
|
256
|
+
message: `action "${stamped.id}" is registered more than once for binding "${stamped.binding}".`,
|
|
257
|
+
remediation: 'Action ids must be unique within a binding. Rename one action or move one of them to a different binding.',
|
|
258
|
+
docsAnchor: 'duplicate-action',
|
|
259
|
+
metadata: { binding: stamped.binding, id: stamped.id, firstPlugin: duplicate.plugin },
|
|
260
|
+
});
|
|
261
|
+
record.issues.push(issue);
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
record.actions.push(stamped);
|
|
265
|
+
}
|
|
266
|
+
#registerProjection(record, projection) {
|
|
267
|
+
record.projections.push({ ...projection, plugin: record.plugin.name });
|
|
268
|
+
}
|
|
269
|
+
#registerRouteShim(record, route) {
|
|
270
|
+
const meta = {};
|
|
271
|
+
if (route.method !== undefined)
|
|
272
|
+
meta['method'] = route.method;
|
|
273
|
+
if (route.path !== undefined)
|
|
274
|
+
meta['path'] = route.path;
|
|
275
|
+
if (route.input !== undefined) {
|
|
276
|
+
const input = {};
|
|
277
|
+
if (route.input.query !== undefined)
|
|
278
|
+
input['query'] = route.input.query;
|
|
279
|
+
if (route.input.body !== undefined)
|
|
280
|
+
input['body'] = route.input.body;
|
|
281
|
+
if (Object.keys(input).length > 0)
|
|
282
|
+
meta['input'] = input;
|
|
283
|
+
}
|
|
284
|
+
if (route.output !== undefined)
|
|
285
|
+
meta['output'] = route.output;
|
|
286
|
+
if (route.streaming !== undefined)
|
|
287
|
+
meta['streaming'] = route.streaming;
|
|
288
|
+
this.#registerAction(record, {
|
|
289
|
+
id: route.id,
|
|
290
|
+
binding: route.transport,
|
|
291
|
+
direction: 'in',
|
|
292
|
+
...(route.path === undefined ? {} : { address: `${route.method ?? 'GET'} ${route.path}` }),
|
|
293
|
+
...(route.description === undefined ? {} : { summary: route.description }),
|
|
294
|
+
...(Object.keys(meta).length === 0 ? {} : { meta }),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
#actionFromSource(source) {
|
|
298
|
+
if (hasToDotAction(source)) {
|
|
299
|
+
return source.toDotAction();
|
|
300
|
+
}
|
|
301
|
+
return source;
|
|
302
|
+
}
|
|
218
303
|
/**
|
|
219
304
|
* Wrap a sync phase body with starting/completed/failed observer events.
|
|
220
305
|
* `configure` is the only sync phase in the kernel today — keep the
|
|
@@ -314,12 +399,12 @@ export class DotAppImpl {
|
|
|
314
399
|
throw this.#reuseError('configure');
|
|
315
400
|
}
|
|
316
401
|
const phaseLogger = this.#logger.withAttribute('dot.app.phase', 'configure');
|
|
317
|
-
phaseLogger.debug('configure: starting', { 'dot.app.
|
|
402
|
+
phaseLogger.debug('configure: starting', { 'dot.app.plugin.count': this.#ordered.length });
|
|
318
403
|
this.#withPhaseEmit('configure', () => {
|
|
319
404
|
withPhaseSpan({
|
|
320
405
|
appName: this.#appName,
|
|
321
406
|
phase: 'configure',
|
|
322
|
-
|
|
407
|
+
pluginCount: this.#ordered.length,
|
|
323
408
|
logger: phaseLogger,
|
|
324
409
|
}, () => this.#runConfigureInner(phaseLogger));
|
|
325
410
|
});
|
|
@@ -330,23 +415,57 @@ export class DotAppImpl {
|
|
|
330
415
|
* intricate error-handling readable.
|
|
331
416
|
*/
|
|
332
417
|
#runConfigureInner(phaseLogger) {
|
|
333
|
-
for (const
|
|
334
|
-
const record = this.#records.get(
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
418
|
+
for (const plugin of this.#ordered) {
|
|
419
|
+
const record = this.#records.get(plugin.name);
|
|
420
|
+
const pluginLogger = phaseLogger.withAttribute('dot.plugin.name', plugin.name);
|
|
421
|
+
const declaredStarted = performance.now();
|
|
422
|
+
try {
|
|
423
|
+
for (const source of plugin.actions)
|
|
424
|
+
this.#registerAction(record, this.#actionFromSource(source));
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
const durationMs = performance.now() - declaredStarted;
|
|
428
|
+
const issue = makeIssue({
|
|
429
|
+
code: DotLifecycleErrorCode.ConfigureFailed,
|
|
430
|
+
plugin: plugin.name,
|
|
431
|
+
message: `action declaration threw for plugin "${plugin.name}": ${stringifyError(error)}`,
|
|
432
|
+
remediation: `Fix the action declarations of "${plugin.name}". Declarative actions must be static and synchronous.`,
|
|
433
|
+
docsAnchor: 'configure-failed',
|
|
434
|
+
});
|
|
435
|
+
record.issues.push(issue);
|
|
436
|
+
record.lifecycleDiagnostics.push({
|
|
437
|
+
plugin: plugin.name,
|
|
438
|
+
hook: 'configure',
|
|
439
|
+
state: 'failed',
|
|
440
|
+
order: record.order,
|
|
441
|
+
durationMs,
|
|
442
|
+
issues: [issue],
|
|
443
|
+
});
|
|
444
|
+
this.#emitHook('configure', plugin.name, record.order, 'failed', { durationMs, error });
|
|
445
|
+
this.#state = 'failed';
|
|
446
|
+
this.#manifest = this.#buildManifest();
|
|
447
|
+
throw new DotLifecycleError({
|
|
448
|
+
code: DotLifecycleErrorCode.ConfigureFailed,
|
|
449
|
+
phase: 'configure',
|
|
450
|
+
plugin: plugin.name,
|
|
451
|
+
message: issue.message,
|
|
452
|
+
cause: error,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
if (!plugin.hooks.configure) {
|
|
456
|
+
pluginLogger.debug('configure: skipped (no hook)');
|
|
338
457
|
continue;
|
|
339
458
|
}
|
|
340
459
|
record.hooks.add('configure');
|
|
341
460
|
const ctx = {
|
|
342
|
-
|
|
461
|
+
pluginName: plugin.name,
|
|
343
462
|
appName: this.#appName,
|
|
344
463
|
registerService: (name, kind) => {
|
|
345
|
-
record.services.push({ name,
|
|
346
|
-
},
|
|
347
|
-
registerRoute: route => {
|
|
348
|
-
record.routes.push({ ...route, pip: pip.name });
|
|
464
|
+
record.services.push({ name, plugin: plugin.name, kind });
|
|
349
465
|
},
|
|
466
|
+
registerAction: action => this.#registerAction(record, action),
|
|
467
|
+
registerProjection: projection => this.#registerProjection(record, projection),
|
|
468
|
+
registerRoute: route => this.#registerRouteShim(record, route),
|
|
350
469
|
registerLifecycleHook: hook => {
|
|
351
470
|
record.hooks.add(hook);
|
|
352
471
|
},
|
|
@@ -356,43 +475,43 @@ export class DotAppImpl {
|
|
|
356
475
|
},
|
|
357
476
|
};
|
|
358
477
|
const started = performance.now();
|
|
359
|
-
this.#emitHook('configure',
|
|
478
|
+
this.#emitHook('configure', plugin.name, record.order, 'starting');
|
|
360
479
|
let returned;
|
|
361
480
|
try {
|
|
362
|
-
returned =
|
|
481
|
+
returned = withPluginHookSpan({
|
|
363
482
|
appName: this.#appName,
|
|
364
|
-
|
|
365
|
-
|
|
483
|
+
pluginName: plugin.name,
|
|
484
|
+
pluginVersion: plugin.version,
|
|
366
485
|
hook: 'configure',
|
|
367
486
|
order: record.order,
|
|
368
|
-
logger:
|
|
369
|
-
}, () =>
|
|
487
|
+
logger: pluginLogger,
|
|
488
|
+
}, () => plugin.hooks.configure(ctx));
|
|
370
489
|
}
|
|
371
490
|
catch (error) {
|
|
372
491
|
const durationMs = performance.now() - started;
|
|
373
492
|
const issue = makeIssue({
|
|
374
493
|
code: DotLifecycleErrorCode.ConfigureFailed,
|
|
375
|
-
|
|
376
|
-
message: `configure hook threw for
|
|
377
|
-
remediation: `Fix the error in the configure() hook of "${
|
|
494
|
+
plugin: plugin.name,
|
|
495
|
+
message: `configure hook threw for plugin "${plugin.name}": ${stringifyError(error)}`,
|
|
496
|
+
remediation: `Fix the error in the configure() hook of "${plugin.name}". configure() is for synchronous registration only — avoid throwing on declarative work.`,
|
|
378
497
|
docsAnchor: 'configure-failed',
|
|
379
498
|
});
|
|
380
499
|
record.issues.push(issue);
|
|
381
500
|
record.lifecycleDiagnostics.push({
|
|
382
|
-
|
|
501
|
+
plugin: plugin.name,
|
|
383
502
|
hook: 'configure',
|
|
384
503
|
state: 'failed',
|
|
385
504
|
order: record.order,
|
|
386
505
|
durationMs,
|
|
387
506
|
issues: [issue],
|
|
388
507
|
});
|
|
389
|
-
this.#emitHook('configure',
|
|
508
|
+
this.#emitHook('configure', plugin.name, record.order, 'failed', { durationMs, error });
|
|
390
509
|
this.#state = 'failed';
|
|
391
510
|
this.#manifest = this.#buildManifest();
|
|
392
511
|
throw new DotLifecycleError({
|
|
393
512
|
code: DotLifecycleErrorCode.ConfigureFailed,
|
|
394
513
|
phase: 'configure',
|
|
395
|
-
|
|
514
|
+
plugin: plugin.name,
|
|
396
515
|
message: issue.message,
|
|
397
516
|
cause: error,
|
|
398
517
|
});
|
|
@@ -401,21 +520,21 @@ export class DotAppImpl {
|
|
|
401
520
|
const durationMs = performance.now() - started;
|
|
402
521
|
const issue = makeIssue({
|
|
403
522
|
code: DotLifecycleErrorCode.ConfigureAsync,
|
|
404
|
-
|
|
405
|
-
message: `configure hook of
|
|
523
|
+
plugin: plugin.name,
|
|
524
|
+
message: `configure hook of plugin "${plugin.name}" returned a Promise. configure must be synchronous.`,
|
|
406
525
|
remediation: 'Move async work to the boot() hook. configure() is for synchronous registration only.',
|
|
407
526
|
docsAnchor: 'configure-async',
|
|
408
527
|
});
|
|
409
528
|
record.issues.push(issue);
|
|
410
529
|
record.lifecycleDiagnostics.push({
|
|
411
|
-
|
|
530
|
+
plugin: plugin.name,
|
|
412
531
|
hook: 'configure',
|
|
413
532
|
state: 'failed',
|
|
414
533
|
order: record.order,
|
|
415
534
|
durationMs,
|
|
416
535
|
issues: [issue],
|
|
417
536
|
});
|
|
418
|
-
this.#emitHook('configure',
|
|
537
|
+
this.#emitHook('configure', plugin.name, record.order, 'failed', {
|
|
419
538
|
durationMs,
|
|
420
539
|
error: new Error(issue.message),
|
|
421
540
|
});
|
|
@@ -424,39 +543,39 @@ export class DotAppImpl {
|
|
|
424
543
|
throw new DotLifecycleError({
|
|
425
544
|
code: DotLifecycleErrorCode.ConfigureAsync,
|
|
426
545
|
phase: 'configure',
|
|
427
|
-
|
|
546
|
+
plugin: plugin.name,
|
|
428
547
|
message: issue.message,
|
|
429
548
|
});
|
|
430
549
|
}
|
|
431
550
|
const durationMs = performance.now() - started;
|
|
432
551
|
record.lifecycleDiagnostics.push({
|
|
433
|
-
|
|
552
|
+
plugin: plugin.name,
|
|
434
553
|
hook: 'configure',
|
|
435
554
|
state: 'configured',
|
|
436
555
|
order: record.order,
|
|
437
556
|
durationMs,
|
|
438
557
|
issues: [],
|
|
439
558
|
});
|
|
440
|
-
this.#emitHook('configure',
|
|
441
|
-
|
|
559
|
+
this.#emitHook('configure', plugin.name, record.order, 'completed', { durationMs });
|
|
560
|
+
pluginLogger.debug('configure: done', { 'dot.plugin.duration.ms': durationMs });
|
|
442
561
|
}
|
|
443
562
|
// Also declare lifecycle-hook participation for non-configure hooks present
|
|
444
|
-
// on the
|
|
445
|
-
for (const
|
|
446
|
-
const record = this.#records.get(
|
|
447
|
-
if (
|
|
563
|
+
// on the plugin object, so the manifest reflects them.
|
|
564
|
+
for (const plugin of this.#ordered) {
|
|
565
|
+
const record = this.#records.get(plugin.name);
|
|
566
|
+
if (plugin.hooks.boot)
|
|
448
567
|
record.hooks.add('boot');
|
|
449
|
-
if (
|
|
568
|
+
if (plugin.hooks.start)
|
|
450
569
|
record.hooks.add('start');
|
|
451
|
-
if (
|
|
570
|
+
if (plugin.hooks.stop)
|
|
452
571
|
record.hooks.add('stop');
|
|
453
|
-
if (
|
|
572
|
+
if (plugin.hooks.dispose)
|
|
454
573
|
record.hooks.add('dispose');
|
|
455
574
|
}
|
|
456
575
|
this.#configured = true;
|
|
457
576
|
this.#state = 'configured';
|
|
458
577
|
this.#manifest = this.#buildManifest();
|
|
459
|
-
phaseLogger.debug('configure: complete', { 'dot.app.
|
|
578
|
+
phaseLogger.debug('configure: complete', { 'dot.app.plugin.count': this.#ordered.length });
|
|
460
579
|
}
|
|
461
580
|
/**
|
|
462
581
|
* Run one lifecycle transition after all previously enqueued ones.
|
|
@@ -495,42 +614,42 @@ export class DotAppImpl {
|
|
|
495
614
|
this.runConfigure();
|
|
496
615
|
}
|
|
497
616
|
const phaseLogger = this.#logger.withAttribute('dot.app.phase', 'boot');
|
|
498
|
-
phaseLogger.debug('boot: starting', { 'dot.app.
|
|
617
|
+
phaseLogger.debug('boot: starting', { 'dot.app.plugin.count': this.#ordered.length });
|
|
499
618
|
return this.#withPhaseEmitAsync('boot', () => withPhaseSpan({
|
|
500
619
|
appName: this.#appName,
|
|
501
620
|
phase: 'boot',
|
|
502
|
-
|
|
621
|
+
pluginCount: this.#ordered.length,
|
|
503
622
|
logger: phaseLogger,
|
|
504
623
|
}, () => this.#runBootInner(phaseLogger)));
|
|
505
624
|
}
|
|
506
625
|
/**
|
|
507
|
-
* Resolve a
|
|
508
|
-
* the `$`-prefixed kernel keys and — for post-boot hooks — the
|
|
626
|
+
* Resolve a plugin's needs into a hook context (alias-keyed), joined with
|
|
627
|
+
* the `$`-prefixed kernel keys and — for post-boot hooks — the plugin's own
|
|
509
628
|
* published services (local keys).
|
|
510
629
|
*
|
|
511
630
|
* @throws {DotLifecycleError} `DOT_LIFECYCLE_E012` when a need has no
|
|
512
|
-
* provider among earlier-booted
|
|
631
|
+
* provider among earlier-booted plugins. Teardown hooks can never hit
|
|
513
632
|
* this: the service map only grows, and reverse-order teardown keeps
|
|
514
633
|
* providers alive until their consumers are done.
|
|
515
634
|
*/
|
|
516
635
|
#buildHookCtx(record, phase, includeOwnProvides) {
|
|
517
636
|
const ctx = {
|
|
518
637
|
$app: this.#appName,
|
|
519
|
-
$
|
|
638
|
+
$plugin: record.plugin.name,
|
|
520
639
|
$config: this.#config,
|
|
521
640
|
};
|
|
522
|
-
for (const [alias, witness] of Object.entries(record.
|
|
641
|
+
for (const [alias, witness] of Object.entries(record.plugin.needs)) {
|
|
523
642
|
const wireKey = wireKeyOf(witness, alias);
|
|
524
643
|
if (!this.#serviceMap.has(wireKey)) {
|
|
525
644
|
throw new DotLifecycleError({
|
|
526
645
|
code: DotLifecycleErrorCode.UnsatisfiedNeed,
|
|
527
646
|
phase,
|
|
528
|
-
|
|
529
|
-
message: `
|
|
530
|
-
`Register a provider with .use() before this
|
|
531
|
-
`if a later
|
|
647
|
+
plugin: record.plugin.name,
|
|
648
|
+
message: `Plugin "${record.plugin.name}" needs service "${wireKey}" but no earlier plugin provides it. ` +
|
|
649
|
+
`Register a provider with .use() before this plugin. Services flow strictly forward — ` +
|
|
650
|
+
`if a later plugin provides "${wireKey}", move that provider earlier; if two plugins need ` +
|
|
532
651
|
`each other's services, that is a cycle: merge them or extract the shared piece into ` +
|
|
533
|
-
`a third
|
|
652
|
+
`a third plugin both consume.`,
|
|
534
653
|
});
|
|
535
654
|
}
|
|
536
655
|
const value = this.#serviceMap.get(wireKey);
|
|
@@ -541,8 +660,8 @@ export class DotAppImpl {
|
|
|
541
660
|
// the underlying value's lifecycle stays with its provider.
|
|
542
661
|
ctx[alias] = isLazyWitness(witness) && !isLazy(value) ? lazyOf(value) : value;
|
|
543
662
|
const provider = this.#providerByWireKey.get(wireKey);
|
|
544
|
-
if (provider !== undefined && !this.#wiringEdges.some(e => e.from === record.
|
|
545
|
-
this.#wiringEdges.push({ from: record.
|
|
663
|
+
if (provider !== undefined && !this.#wiringEdges.some(e => e.from === record.plugin.name && e.to === provider)) {
|
|
664
|
+
this.#wiringEdges.push({ from: record.plugin.name, to: provider, kind: 'requires' });
|
|
546
665
|
}
|
|
547
666
|
}
|
|
548
667
|
if (includeOwnProvides) {
|
|
@@ -553,13 +672,13 @@ export class DotAppImpl {
|
|
|
553
672
|
/**
|
|
554
673
|
* Race one hook invocation against the app's `hookTimeoutMs` watchdog.
|
|
555
674
|
* No budget configured → plain pass-through. On timeout the returned
|
|
556
|
-
* promise rejects with `DOT_LIFECYCLE_E015` naming the
|
|
675
|
+
* promise rejects with `DOT_LIFECYCLE_E015` naming the plugin and hook,
|
|
557
676
|
* and the call site's existing catch path applies the phase's normal
|
|
558
677
|
* failure rules (boot rollback, start cascade, teardown aggregation).
|
|
559
678
|
* The hook's own promise cannot be cancelled — the watchdog makes a
|
|
560
679
|
* hang visible and bounded; it does not kill the hung work.
|
|
561
680
|
*/
|
|
562
|
-
async #withHookBudget(
|
|
681
|
+
async #withHookBudget(pluginName, hook, run) {
|
|
563
682
|
const budget = this.#hookTimeoutMs;
|
|
564
683
|
if (budget === undefined)
|
|
565
684
|
return run();
|
|
@@ -569,8 +688,8 @@ export class DotAppImpl {
|
|
|
569
688
|
reject(new DotLifecycleError({
|
|
570
689
|
code: DotLifecycleErrorCode.HookTimeout,
|
|
571
690
|
phase: hook,
|
|
572
|
-
|
|
573
|
-
message: `${hook} hook of
|
|
691
|
+
plugin: pluginName,
|
|
692
|
+
message: `${hook} hook of plugin "${pluginName}" exceeded the ${budget.toString()}ms hookTimeoutMs watchdog. ` +
|
|
574
693
|
`The kernel treats the hook as failed and applies its normal rollback/aggregation rules, but ` +
|
|
575
694
|
`cannot cancel the hook's promise — find the hang (a missing await? a connection that never ` +
|
|
576
695
|
`settles?) or raise the budget in defineApp(name, { hookTimeoutMs }).`,
|
|
@@ -598,26 +717,26 @@ export class DotAppImpl {
|
|
|
598
717
|
const fail = async (args) => {
|
|
599
718
|
const issue = makeIssue({
|
|
600
719
|
code: args.code,
|
|
601
|
-
|
|
720
|
+
plugin: args.record.plugin.name,
|
|
602
721
|
message: args.message,
|
|
603
722
|
remediation: args.remediation,
|
|
604
723
|
docsAnchor: args.docsAnchor,
|
|
605
724
|
});
|
|
606
725
|
args.record.issues.push(issue);
|
|
607
726
|
args.record.lifecycleDiagnostics.push({
|
|
608
|
-
|
|
727
|
+
plugin: args.record.plugin.name,
|
|
609
728
|
hook: 'boot',
|
|
610
729
|
state: 'failed',
|
|
611
730
|
order: args.record.order,
|
|
612
731
|
durationMs: args.durationMs,
|
|
613
732
|
issues: [issue],
|
|
614
733
|
});
|
|
615
|
-
this.#emitHook('boot', args.record.
|
|
734
|
+
this.#emitHook('boot', args.record.plugin.name, args.record.order, 'failed', {
|
|
616
735
|
durationMs: args.durationMs,
|
|
617
736
|
error: args.cause ?? new Error(args.message),
|
|
618
737
|
});
|
|
619
|
-
phaseLogger.error('boot: FAILED — rolling back already-booted
|
|
620
|
-
'dot.
|
|
738
|
+
phaseLogger.error('boot: FAILED — rolling back already-booted plugins', {
|
|
739
|
+
'dot.plugin.name': args.record.plugin.name,
|
|
621
740
|
'dot.app.rollback.count': args.rollback.length,
|
|
622
741
|
});
|
|
623
742
|
const disposeFailures = await this.#runDisposeForRecords(reverseRecords(args.rollback), phaseLogger);
|
|
@@ -626,17 +745,17 @@ export class DotAppImpl {
|
|
|
626
745
|
throw new DotLifecycleError({
|
|
627
746
|
code: args.code,
|
|
628
747
|
phase: 'boot',
|
|
629
|
-
|
|
748
|
+
plugin: args.record.plugin.name,
|
|
630
749
|
message: args.message,
|
|
631
750
|
cause: args.cause,
|
|
632
751
|
failures: disposeFailures.length > 0 ? disposeFailures : undefined,
|
|
633
752
|
});
|
|
634
753
|
};
|
|
635
|
-
for (const
|
|
636
|
-
const record = this.#records.get(
|
|
637
|
-
const
|
|
754
|
+
for (const plugin of this.#ordered) {
|
|
755
|
+
const record = this.#records.get(plugin.name);
|
|
756
|
+
const pluginLogger = phaseLogger.withAttribute('dot.plugin.name', plugin.name);
|
|
638
757
|
const started = performance.now();
|
|
639
|
-
// Resolve needs BEFORE invoking the hook — a
|
|
758
|
+
// Resolve needs BEFORE invoking the hook — a plugin with needs but no
|
|
640
759
|
// boot hook still fails fast when its wiring is unsatisfied.
|
|
641
760
|
let ctx;
|
|
642
761
|
try {
|
|
@@ -648,36 +767,36 @@ export class DotAppImpl {
|
|
|
648
767
|
record,
|
|
649
768
|
code: DotLifecycleErrorCode.UnsatisfiedNeed,
|
|
650
769
|
message,
|
|
651
|
-
remediation: `Add a provider for the missing service with .use() before "${
|
|
652
|
-
`existing provider's keys to match. If a later
|
|
653
|
-
`strictly forward. Mutual needs between two
|
|
654
|
-
`shared piece into a third
|
|
770
|
+
remediation: `Add a provider for the missing service with .use() before "${plugin.name}", or rename an ` +
|
|
771
|
+
`existing provider's keys to match. If a later plugin provides it, reorder — services flow ` +
|
|
772
|
+
`strictly forward. Mutual needs between two plugins are a cycle: merge them or extract the ` +
|
|
773
|
+
`shared piece into a third plugin both consume.`,
|
|
655
774
|
docsAnchor: 'unsatisfied-need',
|
|
656
775
|
durationMs: performance.now() - started,
|
|
657
776
|
rollback: bootedRecords,
|
|
658
777
|
});
|
|
659
778
|
}
|
|
660
|
-
if (!
|
|
779
|
+
if (!plugin.hooks.boot) {
|
|
661
780
|
record.booted = true;
|
|
662
781
|
bootedRecords.push(record);
|
|
663
|
-
|
|
782
|
+
pluginLogger.debug('boot: skipped (no hook)');
|
|
664
783
|
continue;
|
|
665
784
|
}
|
|
666
785
|
record.hooks.add('boot');
|
|
667
|
-
this.#emitHook('boot',
|
|
786
|
+
this.#emitHook('boot', plugin.name, record.order, 'starting');
|
|
668
787
|
let result;
|
|
669
788
|
try {
|
|
670
|
-
result = await this.#withHookBudget(
|
|
789
|
+
result = await this.#withHookBudget(plugin.name, 'boot', () => withPluginHookSpan({
|
|
671
790
|
appName: this.#appName,
|
|
672
|
-
|
|
673
|
-
|
|
791
|
+
pluginName: plugin.name,
|
|
792
|
+
pluginVersion: plugin.version,
|
|
674
793
|
hook: 'boot',
|
|
675
794
|
order: record.order,
|
|
676
|
-
logger:
|
|
795
|
+
logger: pluginLogger,
|
|
677
796
|
},
|
|
678
797
|
// Erasure boundary: hooks are stored as `(ctx: never) => ...`;
|
|
679
798
|
// the kernel is the one caller allowed to cross it.
|
|
680
|
-
() =>
|
|
799
|
+
() => plugin.hooks.boot(ctx)));
|
|
681
800
|
}
|
|
682
801
|
catch (error) {
|
|
683
802
|
const timedOut = error instanceof DotLifecycleError && error.code === DotLifecycleErrorCode.HookTimeout;
|
|
@@ -686,10 +805,10 @@ export class DotAppImpl {
|
|
|
686
805
|
code: timedOut ? DotLifecycleErrorCode.HookTimeout : DotLifecycleErrorCode.BootFailed,
|
|
687
806
|
message: timedOut
|
|
688
807
|
? error.message
|
|
689
|
-
: `boot hook threw for
|
|
808
|
+
: `boot hook threw for plugin "${plugin.name}": ${stringifyError(error)}`,
|
|
690
809
|
remediation: timedOut
|
|
691
|
-
? `Find the hang in the boot() hook of "${
|
|
692
|
-
: `Fix the error in the boot() hook of "${
|
|
810
|
+
? `Find the hang in the boot() hook of "${plugin.name}" — a missing await or a connection that never settles — or raise defineApp's hookTimeoutMs.`
|
|
811
|
+
: `Fix the error in the boot() hook of "${plugin.name}". If boot opens partial resources before throwing, clean them up locally — DOT only disposes plugins whose boot completed.`,
|
|
693
812
|
docsAnchor: timedOut ? 'hook-timeout' : 'boot-failed',
|
|
694
813
|
durationMs: performance.now() - started,
|
|
695
814
|
cause: error,
|
|
@@ -700,17 +819,17 @@ export class DotAppImpl {
|
|
|
700
819
|
record.publishedServices = publishedServices;
|
|
701
820
|
record.booted = true;
|
|
702
821
|
for (const [localKey, value] of Object.entries(publishedServices)) {
|
|
703
|
-
const wireKey =
|
|
822
|
+
const wireKey = plugin.renames[localKey] ?? localKey;
|
|
704
823
|
if (localKey.startsWith('$') || wireKey.startsWith('$')) {
|
|
705
|
-
// The
|
|
706
|
-
// still reach here. A `$` publish would shadow $app/$
|
|
707
|
-
// in the
|
|
824
|
+
// The plugin() constraint bans this at compile time; erased plugins can
|
|
825
|
+
// still reach here. A `$` publish would shadow $app/$plugin/$config
|
|
826
|
+
// in the plugin's own post-boot hook contexts.
|
|
708
827
|
return fail({
|
|
709
828
|
record,
|
|
710
829
|
code: DotLifecycleErrorCode.ReservedServiceKey,
|
|
711
|
-
message: `
|
|
712
|
-
`for kernel context keys ($app, $
|
|
713
|
-
remediation: `Rename the "${wireKey}" key returned from the boot() hook of "${
|
|
830
|
+
message: `Plugin "${plugin.name}" publishes service "${wireKey}" — the "$" prefix is reserved ` +
|
|
831
|
+
`for kernel context keys ($app, $plugin, $config).`,
|
|
832
|
+
remediation: `Rename the "${wireKey}" key returned from the boot() hook of "${plugin.name}" — "$"-prefixed keys would shadow the kernel context.`,
|
|
714
833
|
docsAnchor: 'reserved-service-key',
|
|
715
834
|
durationMs: performance.now() - started,
|
|
716
835
|
rollback: [...bootedRecords, record],
|
|
@@ -718,41 +837,41 @@ export class DotAppImpl {
|
|
|
718
837
|
}
|
|
719
838
|
if (this.#serviceMap.has(wireKey)) {
|
|
720
839
|
const owner = this.#providerByWireKey.get(wireKey) ?? 'unknown';
|
|
721
|
-
// The current
|
|
840
|
+
// The current plugin HAS booted — include it in the rollback.
|
|
722
841
|
return fail({
|
|
723
842
|
record,
|
|
724
843
|
code: DotLifecycleErrorCode.ServiceCollision,
|
|
725
|
-
message: `
|
|
726
|
-
remediation: `Mount one of the two with rename(
|
|
844
|
+
message: `Plugin "${plugin.name}" publishes service "${wireKey}" which plugin "${owner}" already provides.`,
|
|
845
|
+
remediation: `Mount one of the two with rename(plugin, { '${wireKey}': '<newKey>' }) to keep both instances, or remove the duplicate provider.`,
|
|
727
846
|
docsAnchor: 'service-collision',
|
|
728
847
|
durationMs: performance.now() - started,
|
|
729
848
|
rollback: [...bootedRecords, record],
|
|
730
849
|
});
|
|
731
850
|
}
|
|
732
851
|
this.#serviceMap.set(wireKey, value);
|
|
733
|
-
this.#providerByWireKey.set(wireKey,
|
|
852
|
+
this.#providerByWireKey.set(wireKey, plugin.name);
|
|
734
853
|
this.#aggregatedServices[wireKey] = value;
|
|
735
854
|
record.provides.add(wireKey);
|
|
736
855
|
}
|
|
737
856
|
bootedRecords.push(record);
|
|
738
857
|
const durationMs = performance.now() - started;
|
|
739
858
|
record.lifecycleDiagnostics.push({
|
|
740
|
-
|
|
859
|
+
plugin: plugin.name,
|
|
741
860
|
hook: 'boot',
|
|
742
861
|
state: 'booted',
|
|
743
862
|
order: record.order,
|
|
744
863
|
durationMs,
|
|
745
864
|
issues: [],
|
|
746
865
|
});
|
|
747
|
-
this.#emitHook('boot',
|
|
748
|
-
|
|
749
|
-
'dot.
|
|
750
|
-
'dot.
|
|
866
|
+
this.#emitHook('boot', plugin.name, record.order, 'completed', { durationMs });
|
|
867
|
+
pluginLogger.debug('boot: done', {
|
|
868
|
+
'dot.plugin.duration.ms': durationMs,
|
|
869
|
+
'dot.plugin.services.published': Object.keys(publishedServices).length,
|
|
751
870
|
});
|
|
752
871
|
}
|
|
753
872
|
this.#state = 'booted';
|
|
754
873
|
this.#manifest = this.#buildManifest();
|
|
755
|
-
phaseLogger.debug('boot: complete', { 'dot.app.
|
|
874
|
+
phaseLogger.debug('boot: complete', { 'dot.app.plugin.count': this.#ordered.length });
|
|
756
875
|
}
|
|
757
876
|
/** Public start(). Boots first if needed. Idempotent + concurrent-safe. */
|
|
758
877
|
async start() {
|
|
@@ -778,11 +897,11 @@ export class DotAppImpl {
|
|
|
778
897
|
}
|
|
779
898
|
// From booted or stopped, run start hooks.
|
|
780
899
|
const phaseLogger = this.#logger.withAttribute('dot.app.phase', 'start');
|
|
781
|
-
phaseLogger.debug('start: starting', { 'dot.app.
|
|
900
|
+
phaseLogger.debug('start: starting', { 'dot.app.plugin.count': this.#ordered.length });
|
|
782
901
|
return this.#withPhaseEmitAsync('start', () => withPhaseSpan({
|
|
783
902
|
appName: this.#appName,
|
|
784
903
|
phase: 'start',
|
|
785
|
-
|
|
904
|
+
pluginCount: this.#ordered.length,
|
|
786
905
|
logger: phaseLogger,
|
|
787
906
|
}, () => this.#runStartInner(phaseLogger)));
|
|
788
907
|
}
|
|
@@ -792,52 +911,52 @@ export class DotAppImpl {
|
|
|
792
911
|
*/
|
|
793
912
|
async #runStartInner(phaseLogger) {
|
|
794
913
|
const startedRecords = [];
|
|
795
|
-
for (const
|
|
796
|
-
const record = this.#records.get(
|
|
797
|
-
const
|
|
798
|
-
if (!
|
|
799
|
-
|
|
914
|
+
for (const plugin of this.#ordered) {
|
|
915
|
+
const record = this.#records.get(plugin.name);
|
|
916
|
+
const pluginLogger = phaseLogger.withAttribute('dot.plugin.name', plugin.name);
|
|
917
|
+
if (!plugin.hooks.start) {
|
|
918
|
+
pluginLogger.debug('start: skipped (no hook)');
|
|
800
919
|
continue;
|
|
801
920
|
}
|
|
802
921
|
record.hooks.add('start');
|
|
803
922
|
const ctx = this.#buildHookCtx(record, 'start', true);
|
|
804
923
|
const startedAt = performance.now();
|
|
805
|
-
this.#emitHook('start',
|
|
924
|
+
this.#emitHook('start', plugin.name, record.order, 'starting');
|
|
806
925
|
try {
|
|
807
|
-
await this.#withHookBudget(
|
|
926
|
+
await this.#withHookBudget(plugin.name, 'start', () => withPluginHookSpan({
|
|
808
927
|
appName: this.#appName,
|
|
809
|
-
|
|
810
|
-
|
|
928
|
+
pluginName: plugin.name,
|
|
929
|
+
pluginVersion: plugin.version,
|
|
811
930
|
hook: 'start',
|
|
812
931
|
order: record.order,
|
|
813
|
-
logger:
|
|
814
|
-
}, () =>
|
|
932
|
+
logger: pluginLogger,
|
|
933
|
+
}, () => plugin.hooks.start(ctx)));
|
|
815
934
|
}
|
|
816
935
|
catch (error) {
|
|
817
936
|
const durationMs = performance.now() - startedAt;
|
|
818
937
|
const timedOut = error instanceof DotLifecycleError && error.code === DotLifecycleErrorCode.HookTimeout;
|
|
819
938
|
const issue = makeIssue({
|
|
820
939
|
code: timedOut ? DotLifecycleErrorCode.HookTimeout : DotLifecycleErrorCode.StartFailed,
|
|
821
|
-
|
|
940
|
+
plugin: plugin.name,
|
|
822
941
|
message: timedOut
|
|
823
942
|
? error.message
|
|
824
|
-
: `start hook threw for
|
|
943
|
+
: `start hook threw for plugin "${plugin.name}": ${stringifyError(error)}`,
|
|
825
944
|
remediation: timedOut
|
|
826
|
-
? `Find the hang in the start() hook of "${
|
|
827
|
-
: `Fix the error in the start() hook of "${
|
|
945
|
+
? `Find the hang in the start() hook of "${plugin.name}" — or raise defineApp's hookTimeoutMs. DOT rolls back as for any start failure.`
|
|
946
|
+
: `Fix the error in the start() hook of "${plugin.name}". DOT will stop all already-started plugins and dispose all booted plugins in reverse order.`,
|
|
828
947
|
docsAnchor: timedOut ? 'hook-timeout' : 'start-failed',
|
|
829
948
|
});
|
|
830
949
|
record.issues.push(issue);
|
|
831
950
|
record.lifecycleDiagnostics.push({
|
|
832
|
-
|
|
951
|
+
plugin: plugin.name,
|
|
833
952
|
hook: 'start',
|
|
834
953
|
state: 'failed',
|
|
835
954
|
order: record.order,
|
|
836
955
|
durationMs,
|
|
837
956
|
issues: [issue],
|
|
838
957
|
});
|
|
839
|
-
this.#emitHook('start',
|
|
840
|
-
|
|
958
|
+
this.#emitHook('start', plugin.name, record.order, 'failed', { durationMs, error });
|
|
959
|
+
pluginLogger.error('start: FAILED — rolling back', {
|
|
841
960
|
'dot.app.rollback.started.count': startedRecords.length,
|
|
842
961
|
});
|
|
843
962
|
const stopFailures = await this.#runStopForRecords(reverseRecords(startedRecords), phaseLogger);
|
|
@@ -849,7 +968,7 @@ export class DotAppImpl {
|
|
|
849
968
|
throw new DotLifecycleError({
|
|
850
969
|
code: timedOut ? DotLifecycleErrorCode.HookTimeout : DotLifecycleErrorCode.StartFailed,
|
|
851
970
|
phase: 'start',
|
|
852
|
-
|
|
971
|
+
plugin: plugin.name,
|
|
853
972
|
message: issue.message,
|
|
854
973
|
cause: error,
|
|
855
974
|
failures: failures.length > 0 ? failures : undefined,
|
|
@@ -859,19 +978,19 @@ export class DotAppImpl {
|
|
|
859
978
|
startedRecords.push(record);
|
|
860
979
|
const durationMs = performance.now() - startedAt;
|
|
861
980
|
record.lifecycleDiagnostics.push({
|
|
862
|
-
|
|
981
|
+
plugin: plugin.name,
|
|
863
982
|
hook: 'start',
|
|
864
983
|
state: 'started',
|
|
865
984
|
order: record.order,
|
|
866
985
|
durationMs,
|
|
867
986
|
issues: [],
|
|
868
987
|
});
|
|
869
|
-
this.#emitHook('start',
|
|
870
|
-
|
|
988
|
+
this.#emitHook('start', plugin.name, record.order, 'completed', { durationMs });
|
|
989
|
+
pluginLogger.debug('start: done', { 'dot.plugin.duration.ms': durationMs });
|
|
871
990
|
}
|
|
872
991
|
this.#state = 'started';
|
|
873
992
|
this.#manifest = this.#buildManifest();
|
|
874
|
-
phaseLogger.debug('start: complete', { 'dot.app.
|
|
993
|
+
phaseLogger.debug('start: complete', { 'dot.app.plugin.count': this.#ordered.length });
|
|
875
994
|
}
|
|
876
995
|
/** Public stop(). Idempotent + concurrent-safe. */
|
|
877
996
|
async stop() {
|
|
@@ -901,11 +1020,11 @@ export class DotAppImpl {
|
|
|
901
1020
|
}
|
|
902
1021
|
async #runStop() {
|
|
903
1022
|
const phaseLogger = this.#logger.withAttribute('dot.app.phase', 'stop');
|
|
904
|
-
phaseLogger.debug('stop: starting', { 'dot.app.
|
|
1023
|
+
phaseLogger.debug('stop: starting', { 'dot.app.plugin.count': this.#ordered.length });
|
|
905
1024
|
return this.#withPhaseEmitAsync('stop', () => withPhaseSpan({
|
|
906
1025
|
appName: this.#appName,
|
|
907
1026
|
phase: 'stop',
|
|
908
|
-
|
|
1027
|
+
pluginCount: this.#ordered.length,
|
|
909
1028
|
logger: phaseLogger,
|
|
910
1029
|
}, async () => {
|
|
911
1030
|
const startedRecords = this.#ordered.map(p => this.#records.get(p.name)).filter(r => r.started);
|
|
@@ -917,67 +1036,67 @@ export class DotAppImpl {
|
|
|
917
1036
|
throw new DotLifecycleError({
|
|
918
1037
|
code: DotLifecycleErrorCode.StopFailed,
|
|
919
1038
|
phase: 'stop',
|
|
920
|
-
message: `${failures.length}
|
|
1039
|
+
message: `${failures.length} plugin(s) failed during stop`,
|
|
921
1040
|
failures,
|
|
922
1041
|
});
|
|
923
1042
|
}
|
|
924
|
-
phaseLogger.debug('stop: complete', { 'dot.app.
|
|
1043
|
+
phaseLogger.debug('stop: complete', { 'dot.app.plugin.count': this.#ordered.length });
|
|
925
1044
|
}));
|
|
926
1045
|
}
|
|
927
1046
|
async #runStopForRecords(records, phaseLogger) {
|
|
928
1047
|
const failures = [];
|
|
929
1048
|
for (const record of records) {
|
|
930
|
-
if (!record.
|
|
1049
|
+
if (!record.plugin.hooks.stop)
|
|
931
1050
|
continue;
|
|
932
|
-
const
|
|
1051
|
+
const pluginLogger = phaseLogger.withAttribute('dot.plugin.name', record.plugin.name);
|
|
933
1052
|
record.hooks.add('stop');
|
|
934
1053
|
const ctx = this.#buildHookCtx(record, 'stop', true);
|
|
935
1054
|
const startedAt = performance.now();
|
|
936
|
-
this.#emitHook('stop', record.
|
|
1055
|
+
this.#emitHook('stop', record.plugin.name, record.order, 'starting');
|
|
937
1056
|
try {
|
|
938
|
-
await this.#withHookBudget(record.
|
|
1057
|
+
await this.#withHookBudget(record.plugin.name, 'stop', () => withPluginHookSpan({
|
|
939
1058
|
appName: this.#appName,
|
|
940
|
-
|
|
941
|
-
|
|
1059
|
+
pluginName: record.plugin.name,
|
|
1060
|
+
pluginVersion: record.plugin.version,
|
|
942
1061
|
hook: 'stop',
|
|
943
1062
|
order: record.order,
|
|
944
|
-
logger:
|
|
945
|
-
}, () => record.
|
|
1063
|
+
logger: pluginLogger,
|
|
1064
|
+
}, () => record.plugin.hooks.stop(ctx)));
|
|
946
1065
|
record.started = false;
|
|
947
1066
|
const durationMs = performance.now() - startedAt;
|
|
948
1067
|
record.lifecycleDiagnostics.push({
|
|
949
|
-
|
|
1068
|
+
plugin: record.plugin.name,
|
|
950
1069
|
hook: 'stop',
|
|
951
1070
|
state: 'stopped',
|
|
952
1071
|
order: record.order,
|
|
953
1072
|
durationMs,
|
|
954
1073
|
issues: [],
|
|
955
1074
|
});
|
|
956
|
-
this.#emitHook('stop', record.
|
|
957
|
-
|
|
1075
|
+
this.#emitHook('stop', record.plugin.name, record.order, 'completed', { durationMs });
|
|
1076
|
+
pluginLogger.debug('stop: done', { 'dot.plugin.duration.ms': durationMs });
|
|
958
1077
|
}
|
|
959
1078
|
catch (error) {
|
|
960
1079
|
const durationMs = performance.now() - startedAt;
|
|
961
1080
|
const issue = makeIssue({
|
|
962
1081
|
code: DotLifecycleErrorCode.StopFailed,
|
|
963
|
-
|
|
964
|
-
message: `stop hook threw for
|
|
965
|
-
remediation: `Fix the error in the stop() hook of "${record.
|
|
1082
|
+
plugin: record.plugin.name,
|
|
1083
|
+
message: `stop hook threw for plugin "${record.plugin.name}": ${stringifyError(error)}`,
|
|
1084
|
+
remediation: `Fix the error in the stop() hook of "${record.plugin.name}". Stop continues through individual failures and reports an aggregate error.`,
|
|
966
1085
|
docsAnchor: 'stop-failed',
|
|
967
1086
|
});
|
|
968
1087
|
record.issues.push(issue);
|
|
969
1088
|
record.lifecycleDiagnostics.push({
|
|
970
|
-
|
|
1089
|
+
plugin: record.plugin.name,
|
|
971
1090
|
hook: 'stop',
|
|
972
1091
|
state: 'failed',
|
|
973
1092
|
order: record.order,
|
|
974
1093
|
durationMs,
|
|
975
1094
|
issues: [issue],
|
|
976
1095
|
});
|
|
977
|
-
this.#emitHook('stop', record.
|
|
978
|
-
failures.push({
|
|
979
|
-
|
|
980
|
-
'dot.
|
|
1096
|
+
this.#emitHook('stop', record.plugin.name, record.order, 'failed', { durationMs, error });
|
|
1097
|
+
failures.push({ plugin: record.plugin.name, phase: 'stop', error });
|
|
1098
|
+
pluginLogger.error('stop: failed (continuing)', {
|
|
1099
|
+
'dot.plugin.error.message': stringifyError(error),
|
|
981
1100
|
});
|
|
982
1101
|
}
|
|
983
1102
|
}
|
|
@@ -1000,11 +1119,11 @@ export class DotAppImpl {
|
|
|
1000
1119
|
}
|
|
1001
1120
|
async #runDispose() {
|
|
1002
1121
|
const phaseLogger = this.#logger.withAttribute('dot.app.phase', 'dispose');
|
|
1003
|
-
phaseLogger.debug('dispose: starting', { 'dot.app.
|
|
1122
|
+
phaseLogger.debug('dispose: starting', { 'dot.app.plugin.count': this.#ordered.length });
|
|
1004
1123
|
return this.#withPhaseEmitAsync('dispose', () => withPhaseSpan({
|
|
1005
1124
|
appName: this.#appName,
|
|
1006
1125
|
phase: 'dispose',
|
|
1007
|
-
|
|
1126
|
+
pluginCount: this.#ordered.length,
|
|
1008
1127
|
logger: phaseLogger,
|
|
1009
1128
|
}, () => this.#runDisposeInner(phaseLogger)));
|
|
1010
1129
|
}
|
|
@@ -1032,14 +1151,14 @@ export class DotAppImpl {
|
|
|
1032
1151
|
throw new DotLifecycleError({
|
|
1033
1152
|
code: DotLifecycleErrorCode.DisposeFailed,
|
|
1034
1153
|
phase: 'dispose',
|
|
1035
|
-
message: `${failures.length}
|
|
1154
|
+
message: `${failures.length} plugin(s) failed during stop+dispose cascade`,
|
|
1036
1155
|
failures,
|
|
1037
1156
|
});
|
|
1038
1157
|
}
|
|
1039
1158
|
phaseLogger.debug('dispose: complete (cascaded from started)');
|
|
1040
1159
|
return;
|
|
1041
1160
|
}
|
|
1042
|
-
// From booted/stopped/configured/defined: only dispose booted
|
|
1161
|
+
// From booted/stopped/configured/defined: only dispose booted plugins.
|
|
1043
1162
|
if (this.#state === 'failed') {
|
|
1044
1163
|
// Already cleaned up at the failure site — mark disposed.
|
|
1045
1164
|
this.#state = 'disposed';
|
|
@@ -1056,11 +1175,11 @@ export class DotAppImpl {
|
|
|
1056
1175
|
throw new DotLifecycleError({
|
|
1057
1176
|
code: DotLifecycleErrorCode.DisposeFailed,
|
|
1058
1177
|
phase: 'dispose',
|
|
1059
|
-
message: `${failures.length}
|
|
1178
|
+
message: `${failures.length} plugin(s) failed during dispose`,
|
|
1060
1179
|
failures,
|
|
1061
1180
|
});
|
|
1062
1181
|
}
|
|
1063
|
-
phaseLogger.debug('dispose: complete', { 'dot.app.
|
|
1182
|
+
phaseLogger.debug('dispose: complete', { 'dot.app.plugin.count': this.#ordered.length });
|
|
1064
1183
|
}
|
|
1065
1184
|
async #runDisposeForRecords(records, phaseLogger) {
|
|
1066
1185
|
const failures = [];
|
|
@@ -1070,7 +1189,7 @@ export class DotAppImpl {
|
|
|
1070
1189
|
// (declaration order): dispose runs in reverse, so cleaning the handle
|
|
1071
1190
|
// with a republisher would kill it before the original provider's own
|
|
1072
1191
|
// dispose hook gets to use it. `records` arrives reversed, so plain
|
|
1073
|
-
// overwrite leaves the earliest-declared
|
|
1192
|
+
// overwrite leaves the earliest-declared plugin as each handle's owner.
|
|
1074
1193
|
const ownerOf = new Map();
|
|
1075
1194
|
for (const record of records) {
|
|
1076
1195
|
for (const value of Object.values(record.publishedServices)) {
|
|
@@ -1087,16 +1206,16 @@ export class DotAppImpl {
|
|
|
1087
1206
|
seen.add(value);
|
|
1088
1207
|
return true;
|
|
1089
1208
|
});
|
|
1090
|
-
const hasHook = record.
|
|
1209
|
+
const hasHook = record.plugin.hooks.dispose !== undefined;
|
|
1091
1210
|
if (!hasHook && lazyPublishes.length === 0) {
|
|
1092
1211
|
record.booted = false;
|
|
1093
1212
|
continue;
|
|
1094
1213
|
}
|
|
1095
|
-
const
|
|
1214
|
+
const pluginLogger = phaseLogger.withAttribute('dot.plugin.name', record.plugin.name);
|
|
1096
1215
|
if (hasHook) {
|
|
1097
|
-
await this.#runDisposeHook(record,
|
|
1216
|
+
await this.#runDisposeHook(record, pluginLogger, failures);
|
|
1098
1217
|
}
|
|
1099
|
-
// Auto-dispose lazy service handles AFTER the
|
|
1218
|
+
// Auto-dispose lazy service handles AFTER the plugin's own dispose hook
|
|
1100
1219
|
// (the hook may still use them). Never-initialized handles no-op.
|
|
1101
1220
|
for (const [serviceKey, handle] of lazyPublishes) {
|
|
1102
1221
|
try {
|
|
@@ -1105,16 +1224,16 @@ export class DotAppImpl {
|
|
|
1105
1224
|
catch (error) {
|
|
1106
1225
|
const issue = makeIssue({
|
|
1107
1226
|
code: DotLifecycleErrorCode.DisposeFailed,
|
|
1108
|
-
|
|
1109
|
-
message: `lazy service "${serviceKey}" cleanup threw for
|
|
1227
|
+
plugin: record.plugin.name,
|
|
1228
|
+
message: `lazy service "${serviceKey}" cleanup threw for plugin "${record.plugin.name}": ${stringifyError(error)}`,
|
|
1110
1229
|
remediation: `Fix the error in the dispose callback passed to lazy(...) for service "${serviceKey}". Dispose continues through individual failures and reports an aggregate error.`,
|
|
1111
1230
|
docsAnchor: 'dispose-failed',
|
|
1112
1231
|
});
|
|
1113
1232
|
record.issues.push(issue);
|
|
1114
|
-
failures.push({
|
|
1115
|
-
|
|
1116
|
-
'dot.
|
|
1117
|
-
'dot.
|
|
1233
|
+
failures.push({ plugin: record.plugin.name, phase: 'dispose', error });
|
|
1234
|
+
pluginLogger.error('dispose: lazy service cleanup failed (continuing)', {
|
|
1235
|
+
'dot.plugin.service.key': serviceKey,
|
|
1236
|
+
'dot.plugin.error.message': stringifyError(error),
|
|
1118
1237
|
});
|
|
1119
1238
|
}
|
|
1120
1239
|
}
|
|
@@ -1124,56 +1243,56 @@ export class DotAppImpl {
|
|
|
1124
1243
|
}
|
|
1125
1244
|
return failures;
|
|
1126
1245
|
}
|
|
1127
|
-
/** Run a single
|
|
1128
|
-
async #runDisposeHook(record,
|
|
1246
|
+
/** Run a single plugin's dispose hook with spans/events/diagnostics. */
|
|
1247
|
+
async #runDisposeHook(record, pluginLogger, failures) {
|
|
1129
1248
|
record.hooks.add('dispose');
|
|
1130
1249
|
const ctx = this.#buildHookCtx(record, 'dispose', true);
|
|
1131
1250
|
const startedAt = performance.now();
|
|
1132
|
-
this.#emitHook('dispose', record.
|
|
1251
|
+
this.#emitHook('dispose', record.plugin.name, record.order, 'starting');
|
|
1133
1252
|
try {
|
|
1134
|
-
await this.#withHookBudget(record.
|
|
1253
|
+
await this.#withHookBudget(record.plugin.name, 'dispose', () => withPluginHookSpan({
|
|
1135
1254
|
appName: this.#appName,
|
|
1136
|
-
|
|
1137
|
-
|
|
1255
|
+
pluginName: record.plugin.name,
|
|
1256
|
+
pluginVersion: record.plugin.version,
|
|
1138
1257
|
hook: 'dispose',
|
|
1139
1258
|
order: record.order,
|
|
1140
|
-
logger:
|
|
1141
|
-
}, () => record.
|
|
1259
|
+
logger: pluginLogger,
|
|
1260
|
+
}, () => record.plugin.hooks.dispose(ctx)));
|
|
1142
1261
|
record.booted = false;
|
|
1143
1262
|
const durationMs = performance.now() - startedAt;
|
|
1144
1263
|
record.lifecycleDiagnostics.push({
|
|
1145
|
-
|
|
1264
|
+
plugin: record.plugin.name,
|
|
1146
1265
|
hook: 'dispose',
|
|
1147
1266
|
state: 'disposed',
|
|
1148
1267
|
order: record.order,
|
|
1149
1268
|
durationMs,
|
|
1150
1269
|
issues: [],
|
|
1151
1270
|
});
|
|
1152
|
-
this.#emitHook('dispose', record.
|
|
1153
|
-
|
|
1271
|
+
this.#emitHook('dispose', record.plugin.name, record.order, 'completed', { durationMs });
|
|
1272
|
+
pluginLogger.debug('dispose: done', { 'dot.plugin.duration.ms': durationMs });
|
|
1154
1273
|
}
|
|
1155
1274
|
catch (error) {
|
|
1156
1275
|
const durationMs = performance.now() - startedAt;
|
|
1157
1276
|
const issue = makeIssue({
|
|
1158
1277
|
code: DotLifecycleErrorCode.DisposeFailed,
|
|
1159
|
-
|
|
1160
|
-
message: `dispose hook threw for
|
|
1161
|
-
remediation: `Fix the error in the dispose() hook of "${record.
|
|
1278
|
+
plugin: record.plugin.name,
|
|
1279
|
+
message: `dispose hook threw for plugin "${record.plugin.name}": ${stringifyError(error)}`,
|
|
1280
|
+
remediation: `Fix the error in the dispose() hook of "${record.plugin.name}". Dispose continues through individual failures and reports an aggregate error.`,
|
|
1162
1281
|
docsAnchor: 'dispose-failed',
|
|
1163
1282
|
});
|
|
1164
1283
|
record.issues.push(issue);
|
|
1165
1284
|
record.lifecycleDiagnostics.push({
|
|
1166
|
-
|
|
1285
|
+
plugin: record.plugin.name,
|
|
1167
1286
|
hook: 'dispose',
|
|
1168
1287
|
state: 'failed',
|
|
1169
1288
|
order: record.order,
|
|
1170
1289
|
durationMs,
|
|
1171
1290
|
issues: [issue],
|
|
1172
1291
|
});
|
|
1173
|
-
this.#emitHook('dispose', record.
|
|
1174
|
-
failures.push({
|
|
1175
|
-
|
|
1176
|
-
'dot.
|
|
1292
|
+
this.#emitHook('dispose', record.plugin.name, record.order, 'failed', { durationMs, error });
|
|
1293
|
+
failures.push({ plugin: record.plugin.name, phase: 'dispose', error });
|
|
1294
|
+
pluginLogger.error('dispose: failed (continuing)', {
|
|
1295
|
+
'dot.plugin.error.message': stringifyError(error),
|
|
1177
1296
|
});
|
|
1178
1297
|
}
|
|
1179
1298
|
}
|
|
@@ -1187,62 +1306,67 @@ export class DotAppImpl {
|
|
|
1187
1306
|
});
|
|
1188
1307
|
}
|
|
1189
1308
|
#buildManifest() {
|
|
1190
|
-
const
|
|
1191
|
-
const
|
|
1309
|
+
const plugins = [];
|
|
1310
|
+
const actions = [];
|
|
1192
1311
|
const services = [];
|
|
1193
1312
|
const lifecycle = [];
|
|
1194
|
-
|
|
1195
|
-
//
|
|
1313
|
+
const projections = [];
|
|
1314
|
+
// Edges are observed at boot time: consumer plugin → the plugin whose
|
|
1315
|
+
// published wire key satisfied its need. Before boot, the per-plugin
|
|
1196
1316
|
// `dependencies` array (wire-key strings) is the declarative view.
|
|
1197
1317
|
const dependencies = this.#wiringEdges.map(e => ({ ...e }));
|
|
1198
|
-
for (const
|
|
1199
|
-
const record = this.#records.get(
|
|
1200
|
-
const needWireKeys = Object.entries(
|
|
1201
|
-
|
|
1202
|
-
name:
|
|
1203
|
-
version:
|
|
1318
|
+
for (const plugin of this.#ordered) {
|
|
1319
|
+
const record = this.#records.get(plugin.name);
|
|
1320
|
+
const needWireKeys = Object.entries(plugin.needs).map(([alias, witness]) => wireKeyOf(witness, alias));
|
|
1321
|
+
plugins.push({
|
|
1322
|
+
name: plugin.name,
|
|
1323
|
+
version: plugin.version,
|
|
1204
1324
|
dependencies: needWireKeys,
|
|
1205
1325
|
provides: [...record.provides],
|
|
1206
1326
|
});
|
|
1207
|
-
|
|
1327
|
+
actions.push(...record.actions);
|
|
1208
1328
|
services.push(...record.services);
|
|
1329
|
+
projections.push(...record.projections);
|
|
1209
1330
|
lifecycle.push({
|
|
1210
|
-
|
|
1331
|
+
plugin: plugin.name,
|
|
1211
1332
|
hooks: [...record.hooks],
|
|
1212
1333
|
});
|
|
1213
1334
|
}
|
|
1214
1335
|
return {
|
|
1336
|
+
manifestVersion: 2,
|
|
1215
1337
|
app: {
|
|
1216
1338
|
name: this.#appName,
|
|
1217
1339
|
version: this.#appVersion,
|
|
1218
1340
|
},
|
|
1219
|
-
|
|
1220
|
-
|
|
1341
|
+
plugins,
|
|
1342
|
+
actions,
|
|
1221
1343
|
services,
|
|
1222
1344
|
lifecycle,
|
|
1223
1345
|
dependencies,
|
|
1346
|
+
projections,
|
|
1224
1347
|
};
|
|
1225
1348
|
}
|
|
1226
1349
|
#buildDiagnostics() {
|
|
1227
|
-
const
|
|
1228
|
-
const
|
|
1350
|
+
const pluginDiagnostics = [];
|
|
1351
|
+
const actionDiagnostics = [];
|
|
1229
1352
|
const serviceDiagnostics = [];
|
|
1230
1353
|
const lifecycleDiagnostics = [];
|
|
1231
1354
|
const issues = [];
|
|
1232
|
-
for (const
|
|
1233
|
-
const record = this.#records.get(
|
|
1234
|
-
// Per-
|
|
1355
|
+
for (const plugin of this.#ordered) {
|
|
1356
|
+
const record = this.#records.get(plugin.name);
|
|
1357
|
+
// Per-plugin status: failed if any issue with severity error exists, ok otherwise.
|
|
1235
1358
|
const hasError = record.issues.some(i => i.severity === 'error');
|
|
1236
1359
|
const status = hasError ? 'failed' : 'ok';
|
|
1237
|
-
|
|
1238
|
-
|
|
1360
|
+
pluginDiagnostics.push({
|
|
1361
|
+
plugin: plugin.name,
|
|
1239
1362
|
status,
|
|
1240
1363
|
issues: [...record.issues],
|
|
1241
1364
|
});
|
|
1242
|
-
for (const
|
|
1243
|
-
|
|
1244
|
-
id:
|
|
1245
|
-
|
|
1365
|
+
for (const action of record.actions) {
|
|
1366
|
+
actionDiagnostics.push({
|
|
1367
|
+
id: action.id,
|
|
1368
|
+
plugin: plugin.name,
|
|
1369
|
+
binding: action.binding,
|
|
1246
1370
|
status: hasError ? 'failed' : 'ok',
|
|
1247
1371
|
issues: [],
|
|
1248
1372
|
});
|
|
@@ -1250,7 +1374,7 @@ export class DotAppImpl {
|
|
|
1250
1374
|
for (const svc of record.services) {
|
|
1251
1375
|
serviceDiagnostics.push({
|
|
1252
1376
|
service: svc.name,
|
|
1253
|
-
|
|
1377
|
+
plugin: plugin.name,
|
|
1254
1378
|
status: hasError
|
|
1255
1379
|
? 'failed'
|
|
1256
1380
|
: record.booted || this.#state === 'defined' || this.#state === 'configured'
|
|
@@ -1263,13 +1387,14 @@ export class DotAppImpl {
|
|
|
1263
1387
|
issues.push(...record.issues);
|
|
1264
1388
|
}
|
|
1265
1389
|
return {
|
|
1390
|
+
snapshotVersion: 2,
|
|
1266
1391
|
generatedAt: new Date().toISOString(),
|
|
1267
1392
|
app: {
|
|
1268
1393
|
name: this.#appName,
|
|
1269
1394
|
state: this.#state,
|
|
1270
1395
|
},
|
|
1271
|
-
|
|
1272
|
-
|
|
1396
|
+
plugins: pluginDiagnostics,
|
|
1397
|
+
actions: actionDiagnostics,
|
|
1273
1398
|
services: serviceDiagnostics,
|
|
1274
1399
|
lifecycle: lifecycleDiagnostics,
|
|
1275
1400
|
issues,
|
|
@@ -1298,7 +1423,13 @@ function reverseRecords(records) {
|
|
|
1298
1423
|
// eslint-disable-next-line unicorn/no-array-reverse -- lib target is ES2022, toReversed is ES2023.
|
|
1299
1424
|
return [...records].reverse();
|
|
1300
1425
|
}
|
|
1301
|
-
|
|
1426
|
+
function hasToDotAction(source) {
|
|
1427
|
+
return (typeof source === 'object' &&
|
|
1428
|
+
source !== null &&
|
|
1429
|
+
'toDotAction' in source &&
|
|
1430
|
+
typeof source.toDotAction === 'function');
|
|
1431
|
+
}
|
|
1432
|
+
/** Re-export `ServiceKind` for the kernel's internal use. */
|
|
1302
1433
|
export {} from '../manifest.js';
|
|
1303
|
-
export {} from '../
|
|
1434
|
+
export {} from '../plugin-contract.js';
|
|
1304
1435
|
//# sourceMappingURL=app-instance.js.map
|