@loomcli/core 0.1.1 → 0.3.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/dist/application.d.ts +73 -28
- package/dist/application.js +334 -99
- package/dist/chain.d.ts +68 -0
- package/dist/chain.js +372 -0
- package/dist/command.d.ts +201 -46
- package/dist/command.js +713 -57
- package/dist/environment.d.ts +22 -0
- package/dist/environment.js +1 -0
- package/dist/errors.d.ts +31 -51
- package/dist/errors.js +58 -90
- package/dist/extension.d.ts +99 -0
- package/dist/extension.js +330 -0
- package/dist/facts.d.ts +39 -0
- package/dist/facts.js +95 -0
- package/dist/globals.d.ts +49 -28
- package/dist/globals.js +104 -58
- package/dist/glyphs.generated.d.ts +464 -0
- package/dist/glyphs.generated.js +491 -0
- package/dist/host.js +2 -1
- package/dist/index.d.ts +21 -6
- package/dist/index.js +7 -2
- package/dist/inspect.d.ts +69 -12
- package/dist/inspect.js +83 -26
- package/dist/lanes.d.ts +26 -0
- package/dist/lanes.js +45 -0
- package/dist/options.d.ts +7 -0
- package/dist/options.js +9 -0
- package/dist/output.d.ts +93 -15
- package/dist/output.js +307 -34
- package/dist/plugin.d.ts +132 -0
- package/dist/plugin.js +278 -0
- package/dist/rendering.d.ts +21 -0
- package/dist/rendering.js +72 -0
- package/dist/sequence.d.ts +41 -0
- package/dist/sequence.js +225 -0
- package/dist/signals.d.ts +52 -0
- package/dist/signals.js +85 -0
- package/dist/style-ansi.d.ts +13 -0
- package/dist/style-ansi.js +306 -0
- package/dist/style-layout.d.ts +29 -0
- package/dist/style-layout.js +228 -0
- package/dist/style-resolve.d.ts +6 -0
- package/dist/style-resolve.js +26 -0
- package/dist/style-state.d.ts +14 -0
- package/dist/style-state.js +179 -0
- package/dist/style-wire.d.ts +31 -0
- package/dist/style-wire.js +201 -0
- package/dist/style.d.ts +86 -0
- package/dist/style.js +201 -0
- package/dist/theme.d.ts +3 -0
- package/dist/theme.js +22 -0
- package/dist/types.d.ts +222 -26
- package/dist/validation.d.ts +12 -3
- package/dist/validation.js +34 -17
- package/dist/view.d.ts +180 -0
- package/dist/view.js +307 -0
- package/package.json +2 -1
package/dist/chain.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { BuiltGraph } from './command.js';
|
|
2
|
+
import type { LoomError } from './errors.js';
|
|
3
|
+
import type { CommandGraph, CommandNode } from './inspect.js';
|
|
4
|
+
import type { BuiltPlugin, PluginOptions, PluginOptionValues } from './plugin.js';
|
|
5
|
+
import type { ContextualStyle } from './style.js';
|
|
6
|
+
import type { ActionChannel, Host, OpenResult, Out, Request, ResultBinding } from './types.js';
|
|
7
|
+
import type { DefaultValues } from './validation.js';
|
|
8
|
+
/**
|
|
9
|
+
* What the rest of one chain did: the action ran, a later middleware took over by returning without
|
|
10
|
+
* calling its own `next()`, or the run was cancelled before the action ran.
|
|
11
|
+
*/
|
|
12
|
+
type ChainOutcome = 'cancelled' | 'dispatched' | 'taken-over';
|
|
13
|
+
/**
|
|
14
|
+
* What one middleware receives. `graph` is the frozen graph `inspect()` returns, built once for the
|
|
15
|
+
* run, and `command` is the routed node inside it. `options` holds this plugin's own option values
|
|
16
|
+
* and never another plugin's or the application's globals. `request` is the routed Command's
|
|
17
|
+
* invocation, parsed and validated ahead of the chain, and `null` while core holds a fault and on a
|
|
18
|
+
* group. `view` names the view the result renders through: it reads as the declaration's default
|
|
19
|
+
* until a middleware assigns one, and as `null` on a Command that declares none. The last
|
|
20
|
+
* assignment before the dispatch boundary wins, and one made after it changes nothing.
|
|
21
|
+
*/
|
|
22
|
+
interface MiddlewareContext<Options extends PluginOptions = PluginOptions> {
|
|
23
|
+
readonly options: PluginOptionValues<Options>;
|
|
24
|
+
readonly graph: CommandGraph;
|
|
25
|
+
readonly command: CommandNode;
|
|
26
|
+
readonly request: Request | null;
|
|
27
|
+
get view(): string | null;
|
|
28
|
+
set view(name: string);
|
|
29
|
+
readonly host: Host;
|
|
30
|
+
readonly out: Out;
|
|
31
|
+
readonly signal: AbortSignal;
|
|
32
|
+
readonly next: () => Promise<ChainOutcome>;
|
|
33
|
+
}
|
|
34
|
+
/** Everything one invocation needs after its graph is built and its defaults are validated. */
|
|
35
|
+
interface Invocation {
|
|
36
|
+
style: ContextualStyle;
|
|
37
|
+
/** The action's own channel, built from the routed Command's declaration when it dispatches. */
|
|
38
|
+
channel: (binding: ResultBinding) => ActionChannel;
|
|
39
|
+
defaults: DefaultValues;
|
|
40
|
+
facts: {
|
|
41
|
+
description: string | undefined;
|
|
42
|
+
version: string;
|
|
43
|
+
};
|
|
44
|
+
graph: BuiltGraph;
|
|
45
|
+
host: Host;
|
|
46
|
+
name: string;
|
|
47
|
+
/**
|
|
48
|
+
* The invocation's own channel. A middleware reads it as the neutral `Out`, and the action
|
|
49
|
+
* receives the channel the results lane builds for the Command that was routed.
|
|
50
|
+
*/
|
|
51
|
+
out: Out<OpenResult>;
|
|
52
|
+
plugins: readonly BuiltPlugin[];
|
|
53
|
+
/** A fault reported after the primary outcome, which turns a would-be 0 into 1. */
|
|
54
|
+
report: (fault: LoomError) => void;
|
|
55
|
+
/** The routed path, published where routing resolved it, which output names in its own line. */
|
|
56
|
+
route: (path: readonly string[]) => void;
|
|
57
|
+
signal: AbortSignal;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Runs one invocation: the global pre-scan, routing, the dispatch this invocation prepares, and the
|
|
61
|
+
* middleware chain it then runs. Local parsing and validation run ahead of the chain so that a
|
|
62
|
+
* middleware reads the request, and the fault they find is held until the dispatch boundary. A
|
|
63
|
+
* middleware that returns without calling `next()` has taken over, so the held fault is never
|
|
64
|
+
* raised and nothing later in the chain runs.
|
|
65
|
+
*/
|
|
66
|
+
declare function runInvocation(invocation: Invocation): Promise<void>;
|
|
67
|
+
export type { ChainOutcome, Invocation, MiddlewareContext };
|
|
68
|
+
export { runInvocation };
|
package/dist/chain.js
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { prepareDispatch, routeInvocation } from './command.js';
|
|
2
|
+
import { InternalError, reasonOf, routedSubject, toFailure } from './errors.js';
|
|
3
|
+
import { inspectGraph } from './inspect.js';
|
|
4
|
+
import { booleanValue } from './options.js';
|
|
5
|
+
import { pluginSentence } from './plugin.js';
|
|
6
|
+
/**
|
|
7
|
+
* The view one run selects, which is one value whichever middleware wrote it. The assignment is
|
|
8
|
+
* kept as it arrived, because a JavaScript caller reaches the setter with any value and the check
|
|
9
|
+
* belongs at the dispatch boundary, where the fault it raises ranks behind a held fault.
|
|
10
|
+
*/
|
|
11
|
+
class ViewSelection {
|
|
12
|
+
#assigned = undefined;
|
|
13
|
+
#reached = false;
|
|
14
|
+
#result;
|
|
15
|
+
constructor(result) {
|
|
16
|
+
this.#result = result;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* What `view` reads: the assigned name, or the declaration's default until one is assigned, and
|
|
20
|
+
* `null` on a Command that declares no result, whatever was assigned there. An assignment that is
|
|
21
|
+
* not a name reads as the default, because the getter answers a view name and the assignment is
|
|
22
|
+
* the boundary's fault. The assignment itself is kept either way, so the boundary still raises it.
|
|
23
|
+
*/
|
|
24
|
+
read() {
|
|
25
|
+
if (!this.#result) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const assigned = this.#assigned;
|
|
29
|
+
if (assigned !== undefined && typeof assigned.name === 'string') {
|
|
30
|
+
return assigned.name;
|
|
31
|
+
}
|
|
32
|
+
return this.#result.default;
|
|
33
|
+
}
|
|
34
|
+
/** The last assignment before the boundary wins; one made after it changes nothing. */
|
|
35
|
+
assign(identity, name) {
|
|
36
|
+
if (!this.#reached) {
|
|
37
|
+
this.#assigned = { identity, name };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** The chain reached the dispatch boundary, so this run's view is fixed whatever follows. */
|
|
41
|
+
reach() {
|
|
42
|
+
this.#reached = true;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The name the boundary dispatches through: `null` when no middleware assigned one and the
|
|
46
|
+
* declaration's default stands. A plugin that selected a view has the name checked here.
|
|
47
|
+
*/
|
|
48
|
+
resolve(path) {
|
|
49
|
+
const assigned = this.#assigned;
|
|
50
|
+
if (assigned === undefined) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const plugin = pluginSentence(assigned.identity);
|
|
54
|
+
const { name } = assigned;
|
|
55
|
+
if (!this.#result) {
|
|
56
|
+
throw new InternalError(`${plugin} selected view "${String(name)}" on ${routedSubject(path)}, which declares no result.`, undefined);
|
|
57
|
+
}
|
|
58
|
+
if (typeof name !== 'string') {
|
|
59
|
+
throw new InternalError(`${plugin} selected a view that is not a string on ${routedSubject(path)}.`, undefined);
|
|
60
|
+
}
|
|
61
|
+
if (!this.#result.views.has(name)) {
|
|
62
|
+
throw new InternalError(`${plugin} selected view "${name}", which ${routedSubject(path)} does not name.`, undefined);
|
|
63
|
+
}
|
|
64
|
+
return name;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The default export a loader must resolve to. A loaded module is data core never declared, so the
|
|
69
|
+
* check is the one runtime fact that decides it: the export is callable. Core calls it with the
|
|
70
|
+
* context it owns and ignores whatever it returns.
|
|
71
|
+
*/
|
|
72
|
+
function isMiddlewareExport(value) {
|
|
73
|
+
return typeof value === 'function';
|
|
74
|
+
}
|
|
75
|
+
/** Whether one option name was supplied as a token, in any spelling a declaration accepts. */
|
|
76
|
+
function supplied(scan, name) {
|
|
77
|
+
return scan.strings.has(name) || scan.lists.has(name) || scan.booleans.has(name);
|
|
78
|
+
}
|
|
79
|
+
/** A collected value, or the declared array default, as this run's own copy. */
|
|
80
|
+
function collectedValue(collected, declared) {
|
|
81
|
+
if (collected) {
|
|
82
|
+
return [...collected];
|
|
83
|
+
}
|
|
84
|
+
return Array.isArray(declared) ? [...declared] : [];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* One plugin's own option values for one run: what the pre-scan produced, or the declared default,
|
|
88
|
+
* filled without validation. A collected value and an array default are copied, so a middleware
|
|
89
|
+
* that writes to what it received changes neither the declaration nor the next run.
|
|
90
|
+
*/
|
|
91
|
+
function pluginValues(inputs, scan) {
|
|
92
|
+
const values = {};
|
|
93
|
+
for (const { config, name } of inputs) {
|
|
94
|
+
const declared = config.default;
|
|
95
|
+
if (config.type === 'boolean') {
|
|
96
|
+
values[name] = booleanValue(scan, name, config);
|
|
97
|
+
}
|
|
98
|
+
else if (config.multiple === true) {
|
|
99
|
+
values[name] = collectedValue(scan.lists.get(name), declared);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
// Build already proved that a string option without a schema declares a string default.
|
|
103
|
+
values[name] =
|
|
104
|
+
scan.strings.get(name) ?? (typeof declared === 'string' ? declared : undefined);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return values;
|
|
108
|
+
}
|
|
109
|
+
/** Whether one plugin's declared activation matched the tokens the pre-scan consumed. */
|
|
110
|
+
function activates(installed, scan) {
|
|
111
|
+
const { middleware } = installed;
|
|
112
|
+
if (!middleware) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
return (middleware.activate === 'always' || middleware.activate.some((name) => supplied(scan, name)));
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The chain for one invocation: each installed plugin whose activation matched, in installation
|
|
119
|
+
* order. Activation is read from the pre-scan, before any plugin code loads, so a plugin whose
|
|
120
|
+
* option was never supplied is not in the chain and its loader is never called.
|
|
121
|
+
*/
|
|
122
|
+
function activatedEntries(plugins, scan) {
|
|
123
|
+
return plugins
|
|
124
|
+
.filter((installed) => activates(installed, scan))
|
|
125
|
+
.map((installed) => ({
|
|
126
|
+
identity: installed.identity,
|
|
127
|
+
// Activation proved the middleware exists, so the empty loader is never the one core calls.
|
|
128
|
+
load: installed.middleware?.load ?? (() => undefined),
|
|
129
|
+
options: pluginValues(installed.inputs, scan),
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The routed node inside the inspected graph, which routing already proved reachable. A missing
|
|
134
|
+
* segment means the two readings of one graph disagree, so the chain stops rather than hand a
|
|
135
|
+
* middleware the wrong Command.
|
|
136
|
+
*/
|
|
137
|
+
function nodeAt(graph, path) {
|
|
138
|
+
let node = graph.root;
|
|
139
|
+
for (const name of path) {
|
|
140
|
+
const child = node.children.find((entry) => entry.name === name);
|
|
141
|
+
if (!child) {
|
|
142
|
+
throw new InternalError(`The routed command "${path.join(' ')}" is not in the inspected graph.`, undefined);
|
|
143
|
+
}
|
|
144
|
+
node = child;
|
|
145
|
+
}
|
|
146
|
+
return node;
|
|
147
|
+
}
|
|
148
|
+
/** A downstream promise core awaits for its completion alone; its outcome was recorded already. */
|
|
149
|
+
async function quiet(pending) {
|
|
150
|
+
if (pending) {
|
|
151
|
+
try {
|
|
152
|
+
await pending;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// The rejection was recorded where it crossed the `next()` boundary.
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The outcome one entry reports to its caller. `'cancelled'` wins over `'taken-over'`, so a later
|
|
161
|
+
* middleware that returned because it saw the abort reports as cancelled, the order the exit codes
|
|
162
|
+
* follow. An action that already ran still reports as dispatched.
|
|
163
|
+
*/
|
|
164
|
+
function reported(chain, outcome) {
|
|
165
|
+
return outcome === 'taken-over' && chain.cancelled() ? 'cancelled' : outcome;
|
|
166
|
+
}
|
|
167
|
+
/** A `next()` call that is no longer live: it dispatches nothing and rejects. */
|
|
168
|
+
function misuse(turn) {
|
|
169
|
+
const fault = new InternalError(`${pluginSentence(turn.entry.identity)} called next() ${turn.state.returned ? 'after its middleware returned' : 'twice'}.`, undefined);
|
|
170
|
+
turn.chain.report(fault);
|
|
171
|
+
const rejected = Promise.reject(fault);
|
|
172
|
+
void rejected.catch(() => undefined);
|
|
173
|
+
return rejected;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The `next` one middleware receives. It is live until that middleware's own result settles, so a
|
|
177
|
+
* second call, or a call after the middleware returned, rejects and continues nothing.
|
|
178
|
+
*/
|
|
179
|
+
function nextOf(turn) {
|
|
180
|
+
const { chain, state } = turn;
|
|
181
|
+
return () => {
|
|
182
|
+
if (state.returned || state.calls > 0) {
|
|
183
|
+
return misuse(turn);
|
|
184
|
+
}
|
|
185
|
+
state.calls += 1;
|
|
186
|
+
const pending = chain.step(turn.index + 1).then((outcome) => {
|
|
187
|
+
state.outcome = outcome;
|
|
188
|
+
state.settled = true;
|
|
189
|
+
return outcome;
|
|
190
|
+
}, (error) => {
|
|
191
|
+
state.rejection = { value: error };
|
|
192
|
+
state.settled = true;
|
|
193
|
+
chain.record(error);
|
|
194
|
+
throw error;
|
|
195
|
+
});
|
|
196
|
+
state.downstream = pending;
|
|
197
|
+
// Core awaits the downstream promise itself.
|
|
198
|
+
// A middleware that never awaits `next()` still holds the chain open.
|
|
199
|
+
// The run therefore never ends with an unobserved rejection.
|
|
200
|
+
void pending.catch(() => undefined);
|
|
201
|
+
return pending;
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/** The middleware's own result as a value, so the decision below reads one shape. */
|
|
205
|
+
async function call(middleware, context) {
|
|
206
|
+
try {
|
|
207
|
+
await middleware(context);
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
return { value: error };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* What one entry reports to its caller once its own result has settled. A throw before `next()`
|
|
216
|
+
* settled, or without calling it, is this invocation's failure; a throw during unwinding is an
|
|
217
|
+
* internal error reported after the primary outcome, which keeps its own code.
|
|
218
|
+
*/
|
|
219
|
+
async function settle(turn, thrown) {
|
|
220
|
+
const { chain, state } = turn;
|
|
221
|
+
if (thrown) {
|
|
222
|
+
const propagated = state.rejection !== undefined && thrown.value === state.rejection.value;
|
|
223
|
+
if (propagated || !state.settled) {
|
|
224
|
+
await quiet(state.downstream);
|
|
225
|
+
throw thrown.value;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* A fault core recorded where it was raised, such as a misused `next()`, reaches this point
|
|
229
|
+
* again when the middleware let it escape. It keeps the one report it already has.
|
|
230
|
+
*/
|
|
231
|
+
if (!chain.announced(thrown.value)) {
|
|
232
|
+
chain.report(new InternalError(reasonOf(thrown.value), thrown.value));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (state.calls === 0) {
|
|
236
|
+
return reported(chain, 'taken-over');
|
|
237
|
+
}
|
|
238
|
+
await quiet(state.downstream);
|
|
239
|
+
// A middleware that caught the rejection reports what the chain reached.
|
|
240
|
+
// The recorded failure still decides the exit code.
|
|
241
|
+
return reported(chain, state.outcome ?? (chain.invoked() ? 'dispatched' : 'taken-over'));
|
|
242
|
+
}
|
|
243
|
+
/** The module one loader answers with, whether it throws where it is called or rejects later. */
|
|
244
|
+
async function loadModule(entry) {
|
|
245
|
+
try {
|
|
246
|
+
return await entry.load();
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
throw new InternalError(`Loading plugin "${entry.identity}" failed: ${reasonOf(error)}`, error);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/** A plugin's module is loaded when the chain reaches it, never before. */
|
|
253
|
+
async function loadMiddleware(entry) {
|
|
254
|
+
const module = await loadModule(entry);
|
|
255
|
+
const handler = module !== null && typeof module === 'object' && 'default' in module
|
|
256
|
+
? module.default
|
|
257
|
+
: undefined;
|
|
258
|
+
if (!isMiddlewareExport(handler)) {
|
|
259
|
+
throw new InternalError(`Loading plugin "${entry.identity}" failed: the module exports no default middleware function.`, undefined);
|
|
260
|
+
}
|
|
261
|
+
return handler;
|
|
262
|
+
}
|
|
263
|
+
/** One entry's turn: its module loads here, when the chain reaches it and never before. */
|
|
264
|
+
async function runEntry(entry, index, chain) {
|
|
265
|
+
const middleware = await loadMiddleware(entry);
|
|
266
|
+
if (chain.cancelled()) {
|
|
267
|
+
/**
|
|
268
|
+
* A module import cannot be aborted, so a loader already in flight settles and core starts
|
|
269
|
+
* nothing with it: the middleware it resolved to is skipped.
|
|
270
|
+
*/
|
|
271
|
+
return 'cancelled';
|
|
272
|
+
}
|
|
273
|
+
const state = { calls: 0, returned: false, settled: false };
|
|
274
|
+
const turn = { chain, entry, index, state };
|
|
275
|
+
const thrown = await call(middleware, chain.context(entry, nextOf(turn)));
|
|
276
|
+
state.returned = true;
|
|
277
|
+
return settle(turn, thrown);
|
|
278
|
+
}
|
|
279
|
+
/** The whole chain, answering with the failure it raised when a middleware caught that failure. */
|
|
280
|
+
async function runChain(invocation, routed, prepared) {
|
|
281
|
+
const entries = activatedEntries(invocation.plugins, routed.scan);
|
|
282
|
+
const run = { invoked: false, raised: undefined };
|
|
283
|
+
const selection = new ViewSelection(prepared.result);
|
|
284
|
+
/**
|
|
285
|
+
* The dispatch boundary: the point the chain reaches when its last middleware continues. Core
|
|
286
|
+
* raises the held fault here, so it ranks ahead of a bad view assignment, or else reads the
|
|
287
|
+
* selected view and dispatches the action.
|
|
288
|
+
*/
|
|
289
|
+
const terminal = async () => {
|
|
290
|
+
selection.reach();
|
|
291
|
+
if (prepared.kind === 'held') {
|
|
292
|
+
throw prepared.fault;
|
|
293
|
+
}
|
|
294
|
+
const view = selection.resolve(routed.path);
|
|
295
|
+
run.invoked = true;
|
|
296
|
+
await prepared.dispatch(view);
|
|
297
|
+
return 'dispatched';
|
|
298
|
+
};
|
|
299
|
+
const cancelled = () => invocation.signal.aborted;
|
|
300
|
+
if (entries.length === 0) {
|
|
301
|
+
if (!cancelled()) {
|
|
302
|
+
await terminal();
|
|
303
|
+
}
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
// The graph a middleware reads is the one `inspect()` returns, built once for the run.
|
|
307
|
+
const graph = inspectGraph(invocation.name, invocation.graph, invocation.facts);
|
|
308
|
+
const command = nodeAt(graph, routed.path);
|
|
309
|
+
// Every fault this chain has reported, so the same one raised again carries no second report.
|
|
310
|
+
const announced = new WeakSet();
|
|
311
|
+
const chain = {
|
|
312
|
+
announced: (value) => typeof value === 'object' && value !== null && announced.has(value),
|
|
313
|
+
cancelled,
|
|
314
|
+
context: (entry, next) => ({
|
|
315
|
+
command,
|
|
316
|
+
graph,
|
|
317
|
+
host: invocation.host,
|
|
318
|
+
next,
|
|
319
|
+
options: entry.options,
|
|
320
|
+
out: invocation.out,
|
|
321
|
+
request: prepared.request,
|
|
322
|
+
signal: invocation.signal,
|
|
323
|
+
get view() {
|
|
324
|
+
return selection.read();
|
|
325
|
+
},
|
|
326
|
+
set view(name) {
|
|
327
|
+
selection.assign(entry.identity, name);
|
|
328
|
+
},
|
|
329
|
+
}),
|
|
330
|
+
invoked: () => run.invoked,
|
|
331
|
+
record: (error) => {
|
|
332
|
+
run.raised ??= toFailure(error);
|
|
333
|
+
},
|
|
334
|
+
report: (fault) => {
|
|
335
|
+
announced.add(fault);
|
|
336
|
+
invocation.report(fault);
|
|
337
|
+
},
|
|
338
|
+
step: (index) => {
|
|
339
|
+
if (cancelled()) {
|
|
340
|
+
/**
|
|
341
|
+
* Core starts nothing new after cancellation: a middleware the chain has not reached and
|
|
342
|
+
* an action not yet dispatched are skipped, and the entries already running unwind.
|
|
343
|
+
*/
|
|
344
|
+
return Promise.resolve('cancelled');
|
|
345
|
+
}
|
|
346
|
+
const entry = entries[index];
|
|
347
|
+
return entry ? runEntry(entry, index, chain) : terminal();
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
await chain.step(0);
|
|
351
|
+
return run.raised;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Runs one invocation: the global pre-scan, routing, the dispatch this invocation prepares, and the
|
|
355
|
+
* middleware chain it then runs. Local parsing and validation run ahead of the chain so that a
|
|
356
|
+
* middleware reads the request, and the fault they find is held until the dispatch boundary. A
|
|
357
|
+
* middleware that returns without calling `next()` has taken over, so the held fault is never
|
|
358
|
+
* raised and nothing later in the chain runs.
|
|
359
|
+
*/
|
|
360
|
+
async function runInvocation(invocation) {
|
|
361
|
+
const routed = routeInvocation(invocation.graph, invocation.host.argv);
|
|
362
|
+
invocation.route(routed.path);
|
|
363
|
+
const prepared = await prepareDispatch(invocation.graph, routed, invocation);
|
|
364
|
+
const raised = await runChain(invocation, routed, prepared);
|
|
365
|
+
if (raised) {
|
|
366
|
+
// The chain resolved because a middleware caught the rejection.
|
|
367
|
+
// The failure it caught still decides the exit code.
|
|
368
|
+
// That is the rule an action's caught output rejection already follows.
|
|
369
|
+
throw raised;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
export { runInvocation };
|