@setu-ts/kernel 0.0.0-bootstrap.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +143 -2
- package/dist/application/application.d.ts +172 -0
- package/dist/application/application.d.ts.map +1 -0
- package/dist/application/application.js +1862 -0
- package/dist/application/application.js.map +1 -0
- package/dist/application/inject-body.d.ts +58 -0
- package/dist/application/inject-body.d.ts.map +1 -0
- package/dist/application/inject-body.js +82 -0
- package/dist/application/inject-body.js.map +1 -0
- package/dist/context/request-context.d.ts +32 -0
- package/dist/context/request-context.d.ts.map +1 -0
- package/dist/context/request-context.js +136 -0
- package/dist/context/request-context.js.map +1 -0
- package/dist/context/response.d.ts +60 -0
- package/dist/context/response.d.ts.map +1 -0
- package/dist/context/response.js +193 -0
- package/dist/context/response.js.map +1 -0
- package/dist/diagnostics/buffer.d.ts +143 -0
- package/dist/diagnostics/buffer.d.ts.map +1 -0
- package/dist/diagnostics/buffer.js +185 -0
- package/dist/diagnostics/buffer.js.map +1 -0
- package/dist/diagnostics/collector.d.ts +284 -0
- package/dist/diagnostics/collector.d.ts.map +1 -0
- package/dist/diagnostics/collector.js +812 -0
- package/dist/diagnostics/collector.js.map +1 -0
- package/dist/diagnostics/projection.d.ts +153 -0
- package/dist/diagnostics/projection.d.ts.map +1 -0
- package/dist/diagnostics/projection.js +226 -0
- package/dist/diagnostics/projection.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/lifecycle/lifecycle-manager.d.ts +136 -0
- package/dist/lifecycle/lifecycle-manager.d.ts.map +1 -0
- package/dist/lifecycle/lifecycle-manager.js +206 -0
- package/dist/lifecycle/lifecycle-manager.js.map +1 -0
- package/dist/pipeline/execute-chain.d.ts +71 -0
- package/dist/pipeline/execute-chain.d.ts.map +1 -0
- package/dist/pipeline/execute-chain.js +104 -0
- package/dist/pipeline/execute-chain.js.map +1 -0
- package/dist/pipeline/middleware-pipeline.d.ts +68 -0
- package/dist/pipeline/middleware-pipeline.d.ts.map +1 -0
- package/dist/pipeline/middleware-pipeline.js +82 -0
- package/dist/pipeline/middleware-pipeline.js.map +1 -0
- package/dist/registry/plugin-resolver.d.ts +42 -0
- package/dist/registry/plugin-resolver.d.ts.map +1 -0
- package/dist/registry/plugin-resolver.js +140 -0
- package/dist/registry/plugin-resolver.js.map +1 -0
- package/dist/registry/service-registry.d.ts +110 -0
- package/dist/registry/service-registry.d.ts.map +1 -0
- package/dist/registry/service-registry.js +197 -0
- package/dist/registry/service-registry.js.map +1 -0
- package/dist/router/route-matcher.d.ts +85 -0
- package/dist/router/route-matcher.d.ts.map +1 -0
- package/dist/router/route-matcher.js +102 -0
- package/dist/router/route-matcher.js.map +1 -0
- package/dist/router/router.d.ts +130 -0
- package/dist/router/router.d.ts.map +1 -0
- package/dist/router/router.js +350 -0
- package/dist/router/router.js.map +1 -0
- package/package.json +40 -3
- package/src/application/application.ts +2299 -0
- package/src/application/inject-body.ts +131 -0
- package/src/context/request-context.ts +169 -0
- package/src/context/response.ts +254 -0
- package/src/diagnostics/buffer.ts +241 -0
- package/src/diagnostics/collector.ts +1031 -0
- package/src/diagnostics/projection.ts +322 -0
- package/src/index.ts +20 -0
- package/src/lifecycle/lifecycle-manager.ts +287 -0
- package/src/pipeline/execute-chain.ts +164 -0
- package/src/pipeline/middleware-pipeline.ts +131 -0
- package/src/registry/plugin-resolver.ts +185 -0
- package/src/registry/service-registry.ts +267 -0
- package/src/router/route-matcher.ts +131 -0
- package/src/router/router.ts +485 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { monotonicElapsed } from "../diagnostics/projection.js";
|
|
2
|
+
/**
|
|
3
|
+
* Default implementation of {@linkcode ILifecycleApi}. Stores hook arrays
|
|
4
|
+
* and exposes execution methods for each lifecycle phase.
|
|
5
|
+
*/
|
|
6
|
+
export class LifecycleManager {
|
|
7
|
+
#register = [];
|
|
8
|
+
/** Count of onRegister hooks already drained by {@linkcode runRegister}. */
|
|
9
|
+
#registerCursor = 0;
|
|
10
|
+
#init = [];
|
|
11
|
+
#bootstrap = [];
|
|
12
|
+
#request = [];
|
|
13
|
+
#response = [];
|
|
14
|
+
#error = [];
|
|
15
|
+
#stopping = [];
|
|
16
|
+
#shutdown = [];
|
|
17
|
+
#close = [];
|
|
18
|
+
#observer;
|
|
19
|
+
/**
|
|
20
|
+
* Installs the diagnostics observer for hook invocations. Pass `undefined`
|
|
21
|
+
* to detach (teardown).
|
|
22
|
+
*
|
|
23
|
+
* @param observer - The observer, or `undefined`
|
|
24
|
+
* @since 0.8.0
|
|
25
|
+
*/
|
|
26
|
+
setLifecycleObserver(observer) {
|
|
27
|
+
this.#observer = observer;
|
|
28
|
+
}
|
|
29
|
+
onRegister(fn) {
|
|
30
|
+
this.#register.push(fn);
|
|
31
|
+
}
|
|
32
|
+
onInit(fn) {
|
|
33
|
+
this.#init.push(fn);
|
|
34
|
+
}
|
|
35
|
+
onBootstrap(fn) {
|
|
36
|
+
this.#bootstrap.push(fn);
|
|
37
|
+
}
|
|
38
|
+
onRequest(fn) {
|
|
39
|
+
this.#request.push(fn);
|
|
40
|
+
}
|
|
41
|
+
onResponse(fn) {
|
|
42
|
+
this.#response.push(fn);
|
|
43
|
+
}
|
|
44
|
+
onError(fn) {
|
|
45
|
+
this.#error.push(fn);
|
|
46
|
+
}
|
|
47
|
+
onStopping(fn) {
|
|
48
|
+
this.#stopping.push(fn);
|
|
49
|
+
}
|
|
50
|
+
onShutdown(fn) {
|
|
51
|
+
this.#shutdown.push(fn);
|
|
52
|
+
}
|
|
53
|
+
onClose(fn) {
|
|
54
|
+
this.#close.push(fn);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Runs onRegister hooks added since the previous call, in registration
|
|
58
|
+
* order. The kernel invokes this immediately after each plugin's
|
|
59
|
+
* `register()` returns, so a plugin's onRegister hooks run "during the
|
|
60
|
+
* owning plugin's registration" (per {@linkcode ILifecycleApi.onRegister})
|
|
61
|
+
* — after that plugin and before the next one. A cursor tracks how many
|
|
62
|
+
* hooks have already run so each plugin only fires the hooks it added; a
|
|
63
|
+
* hook that registers a further onRegister hook drains it in the same pass.
|
|
64
|
+
*/
|
|
65
|
+
async runRegister() {
|
|
66
|
+
while (this.#registerCursor < this.#register.length) {
|
|
67
|
+
const fn = this.#register[this.#registerCursor];
|
|
68
|
+
this.#registerCursor++;
|
|
69
|
+
await this.#runObserved('register-hook', this.#registerCursor, fn);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Runs all onInit hooks in registration order. */
|
|
73
|
+
async runInit() {
|
|
74
|
+
let ordinal = 0;
|
|
75
|
+
for (const fn of this.#init) {
|
|
76
|
+
ordinal++;
|
|
77
|
+
await this.#runObserved('init', ordinal, fn);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Runs all onBootstrap hooks in registration order. */
|
|
81
|
+
async runBootstrap() {
|
|
82
|
+
let ordinal = 0;
|
|
83
|
+
for (const fn of this.#bootstrap) {
|
|
84
|
+
ordinal++;
|
|
85
|
+
await this.#runObserved('bootstrap', ordinal, fn);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Whether any onStopping hook is registered.
|
|
90
|
+
*
|
|
91
|
+
* `Application` checks this rather than awaiting unconditionally: awaiting
|
|
92
|
+
* an already-resolved promise still defers the rest of `stop()` by a
|
|
93
|
+
* microtask, which would move when `#stopping` flips and change the answer a
|
|
94
|
+
* request arriving in that same tick gets. Branching keeps the new phase
|
|
95
|
+
* genuinely zero-width for an application that registers no hook.
|
|
96
|
+
*/
|
|
97
|
+
hasStopping() {
|
|
98
|
+
return this.#stopping.length > 0;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Runs stopping hooks in reverse registration order (LIFO), before the
|
|
102
|
+
* application starts refusing requests.
|
|
103
|
+
*/
|
|
104
|
+
async runStopping() {
|
|
105
|
+
for (let i = this.#stopping.length - 1; i >= 0; i--) {
|
|
106
|
+
await this.#runObserved('stopping', this.#stopping.length - i, this.#stopping[i]);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Runs shutdown hooks in reverse registration order (LIFO cleanup). */
|
|
110
|
+
async runShutdown() {
|
|
111
|
+
for (let i = this.#shutdown.length - 1; i >= 0; i--) {
|
|
112
|
+
await this.#runObserved('shutdown', this.#shutdown.length - i, this.#shutdown[i]);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Runs close hooks in registration order (after shutdown completes).
|
|
117
|
+
*
|
|
118
|
+
* EVERY hook runs, even when an earlier one rejects: a close hook releases
|
|
119
|
+
* one plugin's resources, and letting the first failure abort the loop meant
|
|
120
|
+
* one plugin that could not disconnect kept every later plugin from
|
|
121
|
+
* releasing anything — the M50 `onStopping` defect in a second place. The
|
|
122
|
+
* caller still learns about the failures, from an `AggregateError` raised
|
|
123
|
+
* once the whole list has run; a single failure is rethrown as itself, so an
|
|
124
|
+
* existing `instanceof` check on the hook's own error still matches.
|
|
125
|
+
*
|
|
126
|
+
* @throws {AggregateError} When more than one hook rejected
|
|
127
|
+
*/
|
|
128
|
+
async runClose() {
|
|
129
|
+
const errors = [];
|
|
130
|
+
let ordinal = 0;
|
|
131
|
+
for (const fn of this.#close) {
|
|
132
|
+
ordinal++;
|
|
133
|
+
try {
|
|
134
|
+
await this.#runObserved('close', ordinal, fn);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
errors.push(error);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (errors.length === 1) {
|
|
141
|
+
throw errors[0];
|
|
142
|
+
}
|
|
143
|
+
if (errors.length > 1) {
|
|
144
|
+
throw new AggregateError(errors, 'One or more close hooks failed.');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Returns the request hooks list for the application to invoke during
|
|
149
|
+
* request processing.
|
|
150
|
+
*/
|
|
151
|
+
getRequestHooks() {
|
|
152
|
+
return this.#request;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Returns the response hooks list for the application to invoke after
|
|
156
|
+
* the response is produced.
|
|
157
|
+
*/
|
|
158
|
+
getResponseHooks() {
|
|
159
|
+
return this.#response;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Returns the error hooks list for the application to invoke when an
|
|
163
|
+
* error escapes middleware or a handler.
|
|
164
|
+
*/
|
|
165
|
+
getErrorHooks() {
|
|
166
|
+
return this.#error;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Runs one hook, observing its completion when an observer is installed.
|
|
170
|
+
* With no observer this is a bare `await fn()` — zero added work. A
|
|
171
|
+
* throwing hook is recorded as `failed` (never with the error itself) and
|
|
172
|
+
* re-thrown with its identity intact, so ordering, aggregation, and
|
|
173
|
+
* propagation semantics are unchanged.
|
|
174
|
+
*/
|
|
175
|
+
async #runObserved(phase, ordinal, fn) {
|
|
176
|
+
const observer = this.#observer;
|
|
177
|
+
if (observer === undefined) {
|
|
178
|
+
await fn();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const startedAtMs = observer.clock();
|
|
182
|
+
let failed = false;
|
|
183
|
+
try {
|
|
184
|
+
await fn();
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
failed = true;
|
|
188
|
+
observer.onHook({
|
|
189
|
+
phase,
|
|
190
|
+
ordinal,
|
|
191
|
+
failed,
|
|
192
|
+
startedAtMs,
|
|
193
|
+
durationMs: monotonicElapsed(observer.clock, startedAtMs),
|
|
194
|
+
});
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
observer.onHook({
|
|
198
|
+
phase,
|
|
199
|
+
ordinal,
|
|
200
|
+
failed,
|
|
201
|
+
startedAtMs,
|
|
202
|
+
durationMs: monotonicElapsed(observer.clock, startedAtMs),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=lifecycle-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lifecycle-manager.js","sourceRoot":"","sources":["../../src/lifecycle/lifecycle-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared middleware-chain executor — classic next()-chaining with a
|
|
3
|
+
* double-`next` guard and defense-in-depth against a stage that responds
|
|
4
|
+
* AND calls `next()`.
|
|
5
|
+
*
|
|
6
|
+
* Both the global pipeline ({@linkcode MiddlewarePipeline}) and the
|
|
7
|
+
* per-route middleware chain (dispatched from the application) delegate to
|
|
8
|
+
* this helper so the chaining semantics are defined in exactly one place
|
|
9
|
+
* (AI_GUIDELINES §11.1 — DRY).
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
import type { DiagnosticsEventOutcome, IRequestContext, MiddlewareFunction } from '@setu-ts/common';
|
|
14
|
+
/**
|
|
15
|
+
* One executed stage's completion record, reported in actual completion order.
|
|
16
|
+
* Skipped stages (the defense-in-depth early returns) emit no record.
|
|
17
|
+
*
|
|
18
|
+
* @since 0.8.0
|
|
19
|
+
*/
|
|
20
|
+
export interface ChainStageObservation {
|
|
21
|
+
/** 0-based index of the stage in the chain it was registered in. */
|
|
22
|
+
readonly position: number;
|
|
23
|
+
/** How the stage completed. */
|
|
24
|
+
readonly outcome: Extract<DiagnosticsEventOutcome, 'ok' | 'error' | 'short-circuit' | 'downstream-skipped'>;
|
|
25
|
+
/** Monotonic start offset, or `null` when no runtime clock is available. */
|
|
26
|
+
readonly startedAtMs: number | null;
|
|
27
|
+
/** Inclusive monotonic elapsed ms (including downstream `next()` work). */
|
|
28
|
+
readonly durationMs: number | null;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The diagnostics observer the application passes to instrument a chain. The
|
|
32
|
+
* clock is injected because observation must not read ambient time.
|
|
33
|
+
*
|
|
34
|
+
* @since 0.8.0
|
|
35
|
+
*/
|
|
36
|
+
export interface ChainObserver {
|
|
37
|
+
/** Monotonic clock; returns `null` when no runtime clock is available. */
|
|
38
|
+
readonly clock: () => number | null;
|
|
39
|
+
/** Called once per EXECUTED stage, in completion order. */
|
|
40
|
+
readonly onStage: (observation: ChainStageObservation) => void;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Executes a chain of middleware with classic `next()`-chaining semantics.
|
|
44
|
+
*
|
|
45
|
+
* Each middleware receives a `next` function that advances to the next
|
|
46
|
+
* stage. A stage that returns without calling `next()` short-circuits the
|
|
47
|
+
* chain — subsequent stages and the terminal are not invoked. As
|
|
48
|
+
* defense-in-depth, before invoking the next stage (and before the
|
|
49
|
+
* terminal) the executor also checks `ResponseBuilder.ended` on
|
|
50
|
+
* `ctx.response`: if a stage has already produced a terminal response
|
|
51
|
+
* (e.g. called `json()`), downstream stages are skipped even if that
|
|
52
|
+
* stage incorrectly called `next()`.
|
|
53
|
+
*
|
|
54
|
+
* @param chain - The ordered middleware functions to execute
|
|
55
|
+
* @param ctx - The request context (its `response` is a {@linkcode ResponseBuilder})
|
|
56
|
+
* @param terminal - Called when every middleware has called `next()`
|
|
57
|
+
* @param names - Optional diagnostic names, positionally matching `chain`, used
|
|
58
|
+
* to identify the offending stage in the double-`next()` error. The global
|
|
59
|
+
* pipeline supplies the names registered through `MiddlewareOptions.name`;
|
|
60
|
+
* route-level chains have none and fall back to the function's own name.
|
|
61
|
+
* @param observer - Optional diagnostics observer. When absent the executor
|
|
62
|
+
* performs no additional work beyond the original path; when present, each
|
|
63
|
+
* stage's completion is observed inline without changing dispatch semantics —
|
|
64
|
+
* a throw is recorded and re-thrown with its identity intact, and the
|
|
65
|
+
* `downstream-skipped` outcome records a stage that called `next()` after the
|
|
66
|
+
* response had already ended.
|
|
67
|
+
* @throws {Error} If `next()` is called multiple times within a single middleware
|
|
68
|
+
* @since 0.1.0
|
|
69
|
+
*/
|
|
70
|
+
export declare function executeChain(chain: readonly MiddlewareFunction[], ctx: IRequestContext, terminal: () => Promise<void>, names?: readonly string[], observer?: ChainObserver): Promise<void>;
|
|
71
|
+
//# sourceMappingURL=execute-chain.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"execute-chain.d.ts","sourceRoot":"","sources":["../../src/pipeline/execute-chain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,uBAAuB,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAKpG;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,+BAA+B;IAC/B,QAAQ,CAAC,OAAO,EAAE,OAAO,CACvB,uBAAuB,EACvB,IAAI,GAAG,OAAO,GAAG,eAAe,GAAG,oBAAoB,CACxD,CAAC;IACF,4EAA4E;IAC5E,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,2EAA2E;IAC3E,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,0EAA0E;IAC1E,QAAQ,CAAC,KAAK,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IACpC,2DAA2D;IAC3D,QAAQ,CAAC,OAAO,EAAE,CAAC,WAAW,EAAE,qBAAqB,KAAK,IAAI,CAAC;CAChE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,YAAY,CAChC,KAAK,EAAE,SAAS,kBAAkB,EAAE,EACpC,GAAG,EAAE,eAAe,EACpB,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,EAC7B,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,EACzB,QAAQ,CAAC,EAAE,aAAa,GACvB,OAAO,CAAC,IAAI,CAAC,CA+Ef"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { monotonicElapsed } from "../diagnostics/projection.js";
|
|
2
|
+
/**
|
|
3
|
+
* Executes a chain of middleware with classic `next()`-chaining semantics.
|
|
4
|
+
*
|
|
5
|
+
* Each middleware receives a `next` function that advances to the next
|
|
6
|
+
* stage. A stage that returns without calling `next()` short-circuits the
|
|
7
|
+
* chain — subsequent stages and the terminal are not invoked. As
|
|
8
|
+
* defense-in-depth, before invoking the next stage (and before the
|
|
9
|
+
* terminal) the executor also checks `ResponseBuilder.ended` on
|
|
10
|
+
* `ctx.response`: if a stage has already produced a terminal response
|
|
11
|
+
* (e.g. called `json()`), downstream stages are skipped even if that
|
|
12
|
+
* stage incorrectly called `next()`.
|
|
13
|
+
*
|
|
14
|
+
* @param chain - The ordered middleware functions to execute
|
|
15
|
+
* @param ctx - The request context (its `response` is a {@linkcode ResponseBuilder})
|
|
16
|
+
* @param terminal - Called when every middleware has called `next()`
|
|
17
|
+
* @param names - Optional diagnostic names, positionally matching `chain`, used
|
|
18
|
+
* to identify the offending stage in the double-`next()` error. The global
|
|
19
|
+
* pipeline supplies the names registered through `MiddlewareOptions.name`;
|
|
20
|
+
* route-level chains have none and fall back to the function's own name.
|
|
21
|
+
* @param observer - Optional diagnostics observer. When absent the executor
|
|
22
|
+
* performs no additional work beyond the original path; when present, each
|
|
23
|
+
* stage's completion is observed inline without changing dispatch semantics —
|
|
24
|
+
* a throw is recorded and re-thrown with its identity intact, and the
|
|
25
|
+
* `downstream-skipped` outcome records a stage that called `next()` after the
|
|
26
|
+
* response had already ended.
|
|
27
|
+
* @throws {Error} If `next()` is called multiple times within a single middleware
|
|
28
|
+
* @since 0.1.0
|
|
29
|
+
*/
|
|
30
|
+
export async function executeChain(chain, ctx, terminal, names, observer) {
|
|
31
|
+
let index = 0;
|
|
32
|
+
// Defense in depth: stop once a prior stage has ended the response. The
|
|
33
|
+
// check is guarded so callers passing a minimal context (e.g. the
|
|
34
|
+
// pipeline unit tests with an empty object) don't crash on a missing
|
|
35
|
+
// `response` — only a real ResponseBuilder exposes `ended`.
|
|
36
|
+
const responseEnded = () => {
|
|
37
|
+
const response = ctx.response;
|
|
38
|
+
return typeof response?.ended === 'boolean' ? response.ended : false;
|
|
39
|
+
};
|
|
40
|
+
const elapsed = (startedAtMs) => observer === undefined ? null : monotonicElapsed(observer.clock, startedAtMs);
|
|
41
|
+
const run = async () => {
|
|
42
|
+
if (index >= chain.length) {
|
|
43
|
+
// Defense in depth: do not run the terminal if a prior stage
|
|
44
|
+
// already ended the response.
|
|
45
|
+
if (responseEnded()) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
await terminal();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Defense in depth: a prior stage ended the response — stop.
|
|
52
|
+
if (responseEnded()) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const fn = chain[index];
|
|
56
|
+
const position = index;
|
|
57
|
+
index++;
|
|
58
|
+
let nextCalled = false;
|
|
59
|
+
let downstreamSkipped = false;
|
|
60
|
+
const next = () => {
|
|
61
|
+
if (nextCalled) {
|
|
62
|
+
// Prefer the name registered via `MiddlewareOptions.name`, then the
|
|
63
|
+
// function's own name. `fn.name` is ALWAYS a string — `''` for an
|
|
64
|
+
// anonymous function — so `??` never fell back and the message read
|
|
65
|
+
// "…in middleware " with a blank name. `||` is the correct operator.
|
|
66
|
+
const label = names?.[position] || fn.name || '<anonymous>';
|
|
67
|
+
throw new Error(`next() called multiple times in middleware ${label}`);
|
|
68
|
+
}
|
|
69
|
+
nextCalled = true;
|
|
70
|
+
if (observer !== undefined && responseEnded()) {
|
|
71
|
+
// The stage called next() after ending the response; run()'s own
|
|
72
|
+
// ended check below will skip the downstream. Recorded for the
|
|
73
|
+
// caller's completion record — the outcome, not a new behavior.
|
|
74
|
+
downstreamSkipped = true;
|
|
75
|
+
}
|
|
76
|
+
return run();
|
|
77
|
+
};
|
|
78
|
+
const startedAtMs = observer === undefined ? null : observer.clock();
|
|
79
|
+
try {
|
|
80
|
+
await fn(ctx, next);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (observer !== undefined) {
|
|
84
|
+
observer.onStage({
|
|
85
|
+
position,
|
|
86
|
+
outcome: 'error',
|
|
87
|
+
startedAtMs,
|
|
88
|
+
durationMs: elapsed(startedAtMs),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
if (observer !== undefined) {
|
|
94
|
+
observer.onStage({
|
|
95
|
+
position,
|
|
96
|
+
outcome: !nextCalled ? 'short-circuit' : downstreamSkipped ? 'downstream-skipped' : 'ok',
|
|
97
|
+
startedAtMs,
|
|
98
|
+
durationMs: elapsed(startedAtMs),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
await run();
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=execute-chain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"execute-chain.js","sourceRoot":"","sources":["../../src/pipeline/execute-chain.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Middleware pipeline — ASP.NET Core-style ordered execution with priority
|
|
3
|
+
* bands, short-circuiting, and double-next protection.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import type { IMiddlewareApi, IRequestContext, MiddlewareFunction, MiddlewareOptions } from '@setu-ts/common';
|
|
8
|
+
import type { ChainObserver } from './execute-chain.ts';
|
|
9
|
+
/**
|
|
10
|
+
* One compiled global middleware stage, projected for diagnostics: the
|
|
11
|
+
* declared name, its priority, and its execution position in the stable
|
|
12
|
+
* priority sort.
|
|
13
|
+
*
|
|
14
|
+
* @since 0.8.0
|
|
15
|
+
*/
|
|
16
|
+
export interface MiddlewareStageDescriptor {
|
|
17
|
+
/** The name passed through `MiddlewareOptions`, or the generated placeholder. */
|
|
18
|
+
readonly name: string;
|
|
19
|
+
/** The stage's execution priority. */
|
|
20
|
+
readonly priority: number;
|
|
21
|
+
/** 1-based execution position in the compiled chain. */
|
|
22
|
+
readonly position: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Middleware pipeline: collect middleware with priorities, compile into a
|
|
26
|
+
* sorted chain, then execute with classic next()-chaining semantics.
|
|
27
|
+
*/
|
|
28
|
+
export declare class MiddlewarePipeline implements IMiddlewareApi {
|
|
29
|
+
#private;
|
|
30
|
+
add(middleware: MiddlewareFunction, options?: MiddlewareOptions): void;
|
|
31
|
+
/**
|
|
32
|
+
* Sorts middleware stably by (priority, insertion order) and freezes the
|
|
33
|
+
* chain. After calling, no further middleware may be added.
|
|
34
|
+
*/
|
|
35
|
+
compile(): readonly MiddlewareFunction[];
|
|
36
|
+
/**
|
|
37
|
+
* Diagnostic names of the compiled chain, in execution order.
|
|
38
|
+
*
|
|
39
|
+
* Each entry is the `name` passed through {@linkcode MiddlewareOptions}, or a
|
|
40
|
+
* generated `<anonymous-N>` placeholder. Returns an empty array before
|
|
41
|
+
* {@linkcode MiddlewarePipeline.compile} runs.
|
|
42
|
+
*
|
|
43
|
+
* @returns The compiled stage names, positionally matching the compiled chain
|
|
44
|
+
* @internal Diagnostics seam — the kernel passes these to `executeChain` so a
|
|
45
|
+
* double-`next()` error can name the offending stage.
|
|
46
|
+
*/
|
|
47
|
+
compiledNames(): readonly string[];
|
|
48
|
+
/**
|
|
49
|
+
* The compiled stages projected as bounded descriptors, in execution order.
|
|
50
|
+
*
|
|
51
|
+
* @returns Empty before {@linkcode MiddlewarePipeline.compile} runs
|
|
52
|
+
* @internal Diagnostics seam — the collector captures these once, at
|
|
53
|
+
* compile, so middleware nodes and stage records carry the same identity.
|
|
54
|
+
* @since 0.8.0
|
|
55
|
+
*/
|
|
56
|
+
compiledDescriptors(): readonly MiddlewareStageDescriptor[];
|
|
57
|
+
/**
|
|
58
|
+
* Executes the compiled pipeline using classic next()-chaining.
|
|
59
|
+
*
|
|
60
|
+
* @param ctx - The request context
|
|
61
|
+
* @param terminal - Called when all middleware have completed
|
|
62
|
+
* @param observer - Optional diagnostics observer, forwarded to the shared
|
|
63
|
+
* chain executor; absent performs no additional work
|
|
64
|
+
* @throws {Error} If next() is called multiple times in a single middleware
|
|
65
|
+
*/
|
|
66
|
+
execute(ctx: IRequestContext, terminal: () => Promise<void>, observer?: ChainObserver): Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=middleware-pipeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware-pipeline.d.ts","sourceRoot":"","sources":["../../src/pipeline/middleware-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAWxD;;;;;;GAMG;AACH,MAAM,WAAW,yBAAyB;IACxC,iFAAiF;IACjF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,wDAAwD;IACxD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;GAGG;AACH,qBAAa,kBAAmB,YAAW,cAAc;;IAQvD,GAAG,CAAC,UAAU,EAAE,kBAAkB,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,IAAI;IAYtE;;;OAGG;IACH,OAAO,IAAI,SAAS,kBAAkB,EAAE;IAiBxC;;;;;;;;;;OAUG;IACH,aAAa,IAAI,SAAS,MAAM,EAAE;IAIlC;;;;;;;OAOG;IACH,mBAAmB,IAAI,SAAS,yBAAyB,EAAE;IAI3D;;;;;;;;OAQG;IACG,OAAO,CACX,GAAG,EAAE,eAAe,EACpB,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,EAC7B,QAAQ,CAAC,EAAE,aAAa,GACvB,OAAO,CAAC,IAAI,CAAC;CAIjB"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { executeChain } from "./execute-chain.js";
|
|
2
|
+
const DEFAULT_PRIORITY = 500;
|
|
3
|
+
/**
|
|
4
|
+
* Middleware pipeline: collect middleware with priorities, compile into a
|
|
5
|
+
* sorted chain, then execute with classic next()-chaining semantics.
|
|
6
|
+
*/
|
|
7
|
+
export class MiddlewarePipeline {
|
|
8
|
+
#entries = [];
|
|
9
|
+
#compiled = null;
|
|
10
|
+
/** Diagnostic names, positionally matching {@linkcode MiddlewarePipeline.compile}'s output. */
|
|
11
|
+
#compiledNames = null;
|
|
12
|
+
/** Compiled stage descriptors, positionally matching the chain (diagnostics seam). */
|
|
13
|
+
#compiledDescriptors = null;
|
|
14
|
+
add(middleware, options) {
|
|
15
|
+
if (this.#compiled !== null) {
|
|
16
|
+
throw new Error('Cannot add middleware after the pipeline has been compiled.');
|
|
17
|
+
}
|
|
18
|
+
this.#entries.push({
|
|
19
|
+
fn: middleware,
|
|
20
|
+
priority: options?.priority ?? DEFAULT_PRIORITY,
|
|
21
|
+
name: options?.name ?? `<anonymous-${this.#entries.length}>`,
|
|
22
|
+
index: this.#entries.length,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Sorts middleware stably by (priority, insertion order) and freezes the
|
|
27
|
+
* chain. After calling, no further middleware may be added.
|
|
28
|
+
*/
|
|
29
|
+
compile() {
|
|
30
|
+
if (this.#compiled !== null) {
|
|
31
|
+
return this.#compiled;
|
|
32
|
+
}
|
|
33
|
+
const sorted = [...this.#entries].sort((a, b) => a.priority - b.priority || a.index - b.index);
|
|
34
|
+
this.#compiled = sorted.map((entry) => entry.fn);
|
|
35
|
+
this.#compiledNames = sorted.map((entry) => entry.name);
|
|
36
|
+
this.#compiledDescriptors = sorted.map((entry, position) => ({
|
|
37
|
+
name: entry.name,
|
|
38
|
+
priority: entry.priority,
|
|
39
|
+
position: position + 1,
|
|
40
|
+
}));
|
|
41
|
+
return this.#compiled;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Diagnostic names of the compiled chain, in execution order.
|
|
45
|
+
*
|
|
46
|
+
* Each entry is the `name` passed through {@linkcode MiddlewareOptions}, or a
|
|
47
|
+
* generated `<anonymous-N>` placeholder. Returns an empty array before
|
|
48
|
+
* {@linkcode MiddlewarePipeline.compile} runs.
|
|
49
|
+
*
|
|
50
|
+
* @returns The compiled stage names, positionally matching the compiled chain
|
|
51
|
+
* @internal Diagnostics seam — the kernel passes these to `executeChain` so a
|
|
52
|
+
* double-`next()` error can name the offending stage.
|
|
53
|
+
*/
|
|
54
|
+
compiledNames() {
|
|
55
|
+
return this.#compiledNames ?? [];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The compiled stages projected as bounded descriptors, in execution order.
|
|
59
|
+
*
|
|
60
|
+
* @returns Empty before {@linkcode MiddlewarePipeline.compile} runs
|
|
61
|
+
* @internal Diagnostics seam — the collector captures these once, at
|
|
62
|
+
* compile, so middleware nodes and stage records carry the same identity.
|
|
63
|
+
* @since 0.8.0
|
|
64
|
+
*/
|
|
65
|
+
compiledDescriptors() {
|
|
66
|
+
return this.#compiledDescriptors ?? [];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Executes the compiled pipeline using classic next()-chaining.
|
|
70
|
+
*
|
|
71
|
+
* @param ctx - The request context
|
|
72
|
+
* @param terminal - Called when all middleware have completed
|
|
73
|
+
* @param observer - Optional diagnostics observer, forwarded to the shared
|
|
74
|
+
* chain executor; absent performs no additional work
|
|
75
|
+
* @throws {Error} If next() is called multiple times in a single middleware
|
|
76
|
+
*/
|
|
77
|
+
async execute(ctx, terminal, observer) {
|
|
78
|
+
const chain = this.#compiled ?? this.compile();
|
|
79
|
+
await executeChain(chain, ctx, terminal, this.compiledNames(), observer);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=middleware-pipeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware-pipeline.js","sourceRoot":"","sources":["../../src/pipeline/middleware-pipeline.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAUlD,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAkB7B;;;GAGG;AACH,MAAM,OAAO,kBAAkB;IACpB,QAAQ,GAAsB,EAAE,CAAC;IAC1C,SAAS,GAAgC,IAAI,CAAC;IAC9C,+FAA+F;IAC/F,cAAc,GAAoB,IAAI,CAAC;IACvC,sFAAsF;IACtF,oBAAoB,GAAuC,IAAI,CAAC;IAEhE,GAAG,CAAC,UAA8B,EAAE,OAA2B;QAC7D,IAAI,IAAI,CAAC,SAAS,KAAK,IAAI,EAAE,CAAC;YAC5B,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC;QACjF,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;YACjB,EAAE,EAAE,UAAU;YACd,QAAQ,EAAE,OAAO,EAAE,QAAQ,IAAI,gBAAgB;YAC/C,IAAI,EAAE,OAAO,EAAE,IAAI,IAAI,cAAc,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG;YAC5D,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM;SAC5B,CAAC,CAAC;IACL,CAAC;IAED;;;OAGG;IACH,OAAO;QACL,IAAI,IAAI,CAAC,SAAS,KAAK,IAAI,EAAE,CAAC;YAC5B,OAAO,IAAI,CAAC,SAAS,CAAC;QACxB,CAAC;QACD,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CACpC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CACvD,CAAC;QACF,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACjD,IAAI,CAAC,cAAc,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACxD,IAAI,CAAC,oBAAoB,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC;YAC3D,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,QAAQ,EAAE,QAAQ,GAAG,CAAC;SACvB,CAAC,CAAC,CAAC;QACJ,OAAO,IAAI,CAAC,SAAS,CAAC;IACxB,CAAC;IAED;;;;;;;;;;OAUG;IACH,aAAa;QACX,OAAO,IAAI,CAAC,cAAc,IAAI,EAAE,CAAC;IACnC,CAAC;IAED;;;;;;;OAOG;IACH,mBAAmB;QACjB,OAAO,IAAI,CAAC,oBAAoB,IAAI,EAAE,CAAC;IACzC,CAAC;IAED;;;;;;;;OAQG;IACH,KAAK,CAAC,OAAO,CACX,GAAoB,EACpB,QAA6B,EAC7B,QAAwB;QAExB,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QAC/C,MAAM,YAAY,CAAC,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,CAAC,aAAa,EAAE,EAAE,QAAQ,CAAC,CAAC;IAC3E,CAAC;CACF"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { CapabilityToken, IPlugin } from '@setu-ts/common';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves plugin registration order.
|
|
4
|
+
*
|
|
5
|
+
* Order is determined by: dependency edges first (a plugin's dependencies
|
|
6
|
+
* register before it), then declared priority (lower first), then original
|
|
7
|
+
* registration order. The plugin providing the `runtime` capability always
|
|
8
|
+
* registers first, regardless of priority.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolvePluginOrder(plugins: readonly IPlugin[]): readonly IPlugin[];
|
|
11
|
+
/**
|
|
12
|
+
* One plugin's declared `consumes` capability for which no provider exists.
|
|
13
|
+
* Used to build a soft startup diagnostic in the application.
|
|
14
|
+
*
|
|
15
|
+
* Unlike a missing {@linkcode IPlugin.dependencies} entry (which throws in
|
|
16
|
+
* {@linkcode resolvePluginOrder}), an unsatisfied `consumes` is not a
|
|
17
|
+
* resolve-time error: the plugin resolves the capability lazily at request
|
|
18
|
+
* time, so the failure would otherwise surface only later, as a runtime throw
|
|
19
|
+
* from `services.get`. Reporting it at startup names the consumer up front.
|
|
20
|
+
*/
|
|
21
|
+
export interface UnsatisfiedConsumer {
|
|
22
|
+
/** Name of the plugin that declares the consumed capability. */
|
|
23
|
+
readonly plugin: string;
|
|
24
|
+
/** The consumed capability token that no registered plugin provides. */
|
|
25
|
+
readonly capability: CapabilityToken;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Finds every `consumes` capability, across all plugins, that `isProvided`
|
|
29
|
+
* reports as absent.
|
|
30
|
+
*
|
|
31
|
+
* Pure by design: the caller supplies the provider check — in the kernel, the
|
|
32
|
+
* live service registry's `has` after all plugins have registered, so
|
|
33
|
+
* capabilities registered imperatively (not declared in `provides`) still
|
|
34
|
+
* count as satisfied and never produce a false warning.
|
|
35
|
+
*
|
|
36
|
+
* @param plugins - All registered plugins
|
|
37
|
+
* @param isProvided - Predicate: does a provider exist for this capability?
|
|
38
|
+
* @returns One entry per unsatisfied (plugin, capability) pair, in plugin then
|
|
39
|
+
* declaration order
|
|
40
|
+
*/
|
|
41
|
+
export declare function findUnsatisfiedConsumers(plugins: readonly IPlugin[], isProvided: (token: CapabilityToken) => boolean): readonly UnsatisfiedConsumer[];
|
|
42
|
+
//# sourceMappingURL=plugin-resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-resolver.d.ts","sourceRoot":"","sources":["../../src/registry/plugin-resolver.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAEhE;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,SAAS,OAAO,EAAE,GAAG,SAAS,OAAO,EAAE,CAwClF;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,mBAAmB;IAClC,gEAAgE;IAChE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,wEAAwE;IACxE,QAAQ,CAAC,UAAU,EAAE,eAAe,CAAC;CACtC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,SAAS,OAAO,EAAE,EAC3B,UAAU,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,OAAO,GAC9C,SAAS,mBAAmB,EAAE,CAUhC"}
|