@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.
Files changed (57) hide show
  1. package/dist/application.d.ts +73 -28
  2. package/dist/application.js +334 -99
  3. package/dist/chain.d.ts +68 -0
  4. package/dist/chain.js +372 -0
  5. package/dist/command.d.ts +201 -46
  6. package/dist/command.js +713 -57
  7. package/dist/environment.d.ts +22 -0
  8. package/dist/environment.js +1 -0
  9. package/dist/errors.d.ts +31 -51
  10. package/dist/errors.js +58 -90
  11. package/dist/extension.d.ts +99 -0
  12. package/dist/extension.js +330 -0
  13. package/dist/facts.d.ts +39 -0
  14. package/dist/facts.js +95 -0
  15. package/dist/globals.d.ts +49 -28
  16. package/dist/globals.js +104 -58
  17. package/dist/glyphs.generated.d.ts +464 -0
  18. package/dist/glyphs.generated.js +491 -0
  19. package/dist/host.js +2 -1
  20. package/dist/index.d.ts +21 -6
  21. package/dist/index.js +7 -2
  22. package/dist/inspect.d.ts +69 -12
  23. package/dist/inspect.js +83 -26
  24. package/dist/lanes.d.ts +26 -0
  25. package/dist/lanes.js +45 -0
  26. package/dist/options.d.ts +7 -0
  27. package/dist/options.js +9 -0
  28. package/dist/output.d.ts +93 -15
  29. package/dist/output.js +307 -34
  30. package/dist/plugin.d.ts +132 -0
  31. package/dist/plugin.js +278 -0
  32. package/dist/rendering.d.ts +21 -0
  33. package/dist/rendering.js +72 -0
  34. package/dist/sequence.d.ts +41 -0
  35. package/dist/sequence.js +225 -0
  36. package/dist/signals.d.ts +52 -0
  37. package/dist/signals.js +85 -0
  38. package/dist/style-ansi.d.ts +13 -0
  39. package/dist/style-ansi.js +306 -0
  40. package/dist/style-layout.d.ts +29 -0
  41. package/dist/style-layout.js +228 -0
  42. package/dist/style-resolve.d.ts +6 -0
  43. package/dist/style-resolve.js +26 -0
  44. package/dist/style-state.d.ts +14 -0
  45. package/dist/style-state.js +179 -0
  46. package/dist/style-wire.d.ts +31 -0
  47. package/dist/style-wire.js +201 -0
  48. package/dist/style.d.ts +86 -0
  49. package/dist/style.js +201 -0
  50. package/dist/theme.d.ts +3 -0
  51. package/dist/theme.js +22 -0
  52. package/dist/types.d.ts +222 -26
  53. package/dist/validation.d.ts +12 -3
  54. package/dist/validation.js +34 -17
  55. package/dist/view.d.ts +180 -0
  56. package/dist/view.js +307 -0
  57. package/package.json +2 -1
package/dist/plugin.js ADDED
@@ -0,0 +1,278 @@
1
+ import { DeclarationError } from './errors.js';
2
+ import { buildExtensions, isDescriptor, registerDescriptor } from './extension.js';
3
+ import { checkDeprecated, checkDescription, checkHidden, isPlainObject } from './facts.js';
4
+ import { isProcessSignal } from './signals.js';
5
+ import { buildTheme } from './theme.js';
6
+ import { captureConfig, checkDeclarations } from './validation.js';
7
+ /** Authored values register here, so the public type publishes no state to reach or replace. */
8
+ const nodes = new WeakMap();
9
+ /**
10
+ * The runtime value `plugin()` returns. `Options` appears in a read position alone, which makes it
11
+ * covariant: a `plugins` list holds plugins with different options the way `views` holds
12
+ * overrides for different keys, and `Middleware` and `load` accept a narrower plugin.
13
+ */
14
+ class PluginDeclaration {
15
+ constructor(node) {
16
+ nodes.set(this, node);
17
+ Object.freeze(this);
18
+ }
19
+ }
20
+ /**
21
+ * One plugin: an identity and the contributions it carries. Creating and installing the value runs
22
+ * none of its code: a hook runs at graph build, and the middleware runs inside an invocation, so an
23
+ * installed plugin an invocation never reaches costs that invocation its hooks alone.
24
+ */
25
+ function plugin(identity, definition) {
26
+ const captured = {
27
+ ...definition,
28
+ ...(definition?.theme === undefined
29
+ ? {}
30
+ : { theme: isPlainObject(definition.theme) ? { ...definition.theme } : definition.theme }),
31
+ };
32
+ return new PluginDeclaration({
33
+ definition: isPlainObject(definition) ? captured : definition,
34
+ identity,
35
+ });
36
+ }
37
+ /** How every plugin diagnostic names one plugin at the start of a sentence. */
38
+ function pluginSentence(identity) {
39
+ return `Plugin "${identity}"`;
40
+ }
41
+ /** Reads the declarations behind an installed value; anything else is a declaration error. */
42
+ function nodeOf(value) {
43
+ const node = typeof value === 'object' && value !== null ? nodes.get(value) : undefined;
44
+ if (!node) {
45
+ throw new DeclarationError('The Application holds a value that is not a plugin. Supply the value returned by plugin(identity, definition).');
46
+ }
47
+ return node;
48
+ }
49
+ /** The identity one installed value declares, which is a nonempty string installed once. */
50
+ function readIdentity(node, installed) {
51
+ const { identity } = node;
52
+ if (typeof identity !== 'string') {
53
+ throw new DeclarationError('A plugin declares an identity that is not a string. Supply a nonempty string, such as the package name.');
54
+ }
55
+ if (identity === '') {
56
+ throw new DeclarationError('A plugin declares an empty identity. Supply a nonempty string, such as the package name.');
57
+ }
58
+ if (installed.has(identity)) {
59
+ throw new DeclarationError(`The Application installs plugin "${identity}" twice. Install each plugin once.`);
60
+ }
61
+ return identity;
62
+ }
63
+ /** The declarations one plugin value carries, which a JavaScript author reaches as any value. */
64
+ function definitionOf(identity, node) {
65
+ const { definition } = node;
66
+ if (!isPlainObject(definition)) {
67
+ throw new DeclarationError(`${pluginSentence(identity)} declares a definition that is not an object. Supply { options, middleware, extensions, views }.`);
68
+ }
69
+ return definition;
70
+ }
71
+ /**
72
+ * The installed list in composition order, with the rules that read the list itself. The slot is
73
+ * read defensively, because a JavaScript author reaches it with any value. Each plugin's own
74
+ * declarations are read by the build steps that consume them, in the order those steps run.
75
+ */
76
+ function installPlugins(plugins) {
77
+ if (!Array.isArray(plugins)) {
78
+ throw new DeclarationError('The Application plugins must be an array. Supply a list of plugin values.');
79
+ }
80
+ const installed = [];
81
+ const identities = new Set();
82
+ for (const value of plugins) {
83
+ const node = nodeOf(value);
84
+ const identity = readIdentity(node, identities);
85
+ identities.add(identity);
86
+ installed.push({ declaration: definitionOf(identity, node), identity });
87
+ }
88
+ return installed;
89
+ }
90
+ /** The keys a plugin option may not declare, in the order its diagnostic names them. */
91
+ const forbidden = ['validate', 'validateOmitted', 'required'];
92
+ /** The rules a plugin option answers before every rule an ordinary declaration carries. */
93
+ function checkPluginOption(sentence, config) {
94
+ if (!isPlainObject(config)) {
95
+ throw new DeclarationError(`${sentence} is not an option declaration. Supply { type, ... }.`);
96
+ }
97
+ const rejected = forbidden.find((key) => key in config);
98
+ if (rejected !== undefined) {
99
+ throw new DeclarationError(`${sentence} declares ${rejected}. Remove it; a plugin option carries no schema or presence rule, and the middleware interprets the value.`);
100
+ }
101
+ checkDescription(sentence, config.description);
102
+ checkHidden(sentence, config.hidden);
103
+ checkDeprecated(sentence, config.deprecated);
104
+ }
105
+ /**
106
+ * One plugin's option declarations, in declaration order. They join the globals table, so the rules
107
+ * that pair them with another scope's options belong to that table and not to this reading.
108
+ */
109
+ function readOptions(identity, declared, build) {
110
+ if (declared !== undefined && !isPlainObject(declared)) {
111
+ throw new DeclarationError(`${pluginSentence(identity)} declares options that are not an object. Supply a record of option declarations.`);
112
+ }
113
+ const inputs = [];
114
+ for (const [name, config] of Object.entries(declared ?? {})) {
115
+ const sentence = `${pluginSentence(identity)} option "${name}"`;
116
+ checkPluginOption(sentence, config);
117
+ const input = { config: captureConfig(config), kind: 'option', name };
118
+ // The shared rules name the plugin and the option, so a fault reads with its contributor.
119
+ checkDeclarations([input], sentence);
120
+ build.extensions.set(input, buildExtensions({
121
+ declared: config.extensions,
122
+ descriptors: build.descriptors,
123
+ subject: {
124
+ phrase: `on ${sentence.slice(0, 1).toLowerCase()}${sentence.slice(1)}`,
125
+ sentence,
126
+ },
127
+ target: 'option',
128
+ }));
129
+ inputs.push(input);
130
+ }
131
+ return inputs;
132
+ }
133
+ /** One activation name, which must be one of the plugin's own declared options. */
134
+ function readActivationName(identity, name, names) {
135
+ if (typeof name !== 'string' || !names.has(name)) {
136
+ throw new DeclarationError(`${pluginSentence(identity)} activates middleware on option "${String(name)}", which it does not declare. Name one of the plugin's own options.`);
137
+ }
138
+ return name;
139
+ }
140
+ /** The activation a middleware declares, checked against the options its own plugin declares. */
141
+ function readActivation(identity, declared, names) {
142
+ if (declared === 'always') {
143
+ return 'always';
144
+ }
145
+ if (!Array.isArray(declared)) {
146
+ throw new DeclarationError(`${pluginSentence(identity)} declares middleware with no activation. Supply activate: 'always' or a list of the plugin's own option names.`);
147
+ }
148
+ const list = declared;
149
+ if (list.length === 0) {
150
+ throw new DeclarationError(`${pluginSentence(identity)} declares middleware with an empty activation list. Name at least one of the plugin's options or use 'always'.`);
151
+ }
152
+ return list.map((name) => readActivationName(identity, name, names));
153
+ }
154
+ /**
155
+ * The loader a middleware declares. Core calls it with no arguments and reads whatever it resolves
156
+ * to, so being callable is the whole runtime claim this check makes.
157
+ */
158
+ function isLoader(value) {
159
+ return typeof value === 'function';
160
+ }
161
+ /** One plugin's middleware, or `undefined` for a plugin that declares none. */
162
+ function readMiddleware(identity, declared, names) {
163
+ if (declared === undefined) {
164
+ return undefined;
165
+ }
166
+ if (!isPlainObject(declared)) {
167
+ throw new DeclarationError(`${pluginSentence(identity)} declares middleware that is not an object. Supply { activate, load }.`);
168
+ }
169
+ const activate = readActivation(identity, declared.activate, names);
170
+ const { load } = declared;
171
+ if (!isLoader(load)) {
172
+ throw new DeclarationError(`${pluginSentence(identity)} declares middleware with no load function. Supply load: () => import('./middleware.js').`);
173
+ }
174
+ return { activate, load };
175
+ }
176
+ /**
177
+ * One plugin's claim on the signals slot, drawn from the closed set core installs listeners for.
178
+ * An empty list claims nothing, so it leaves the slot free for another plugin.
179
+ * Each signal is claimed once, because core installs one listener per entry and a second listener
180
+ * on one signal would take the force path on the first signal the run receives.
181
+ */
182
+ function readSignals(identity, declared) {
183
+ if (declared === undefined) {
184
+ return [];
185
+ }
186
+ if (!Array.isArray(declared)) {
187
+ throw new DeclarationError(`${pluginSentence(identity)} declares signals that are not an array. Supply a list of signal names.`);
188
+ }
189
+ const list = declared;
190
+ const claimed = new Set();
191
+ for (const value of list) {
192
+ if (!isProcessSignal(value)) {
193
+ throw new DeclarationError(`${pluginSentence(identity)} claims signal "${String(value)}". Claim SIGINT or SIGTERM.`);
194
+ }
195
+ if (claimed.has(value)) {
196
+ throw new DeclarationError(`${pluginSentence(identity)} claims signal "${value}" twice. Claim each signal once.`);
197
+ }
198
+ claimed.add(value);
199
+ }
200
+ return [...claimed];
201
+ }
202
+ /**
203
+ * A lifecycle hook is a function core calls at one named point, so being callable is the whole
204
+ * claim this check makes; every rule the calls it makes carry belongs to the build that calls it.
205
+ */
206
+ function isHook(value) {
207
+ return typeof value === 'function';
208
+ }
209
+ /** One plugin's `onCommandAttach` hook, or `undefined` for a plugin that declares none. */
210
+ function readHook(identity, declared) {
211
+ if (declared === undefined) {
212
+ return undefined;
213
+ }
214
+ if (!isHook(declared)) {
215
+ throw new DeclarationError(`${pluginSentence(identity)} declares onCommandAttach that is not a function. Supply a function of the Command.`);
216
+ }
217
+ return declared;
218
+ }
219
+ /** A plugin's own list names the extensions it defines, before any declaration carries one. */
220
+ function defineExtensions(identity, declaration, build) {
221
+ const { extensions } = declaration;
222
+ if (extensions !== undefined && !Array.isArray(extensions)) {
223
+ throw new DeclarationError(`${pluginSentence(identity)} declares extensions that are not an array. Supply a list of extension descriptors.`);
224
+ }
225
+ for (const descriptor of extensions ?? []) {
226
+ if (!isDescriptor(descriptor)) {
227
+ throw new DeclarationError(`${pluginSentence(identity)} holds a value that is not an extension. Supply the value returned by extension(identity, config).`);
228
+ }
229
+ registerDescriptor(build.descriptors, descriptor);
230
+ }
231
+ }
232
+ /**
233
+ * Every installed plugin's declarations, in installation order. A plugin's own extensions register
234
+ * before any declaration carries a value, so a duplicated package copy is reported from the list
235
+ * that installed it.
236
+ */
237
+ function buildPlugins(installed, build) {
238
+ /**
239
+ * The signals slot has one owner, so the first plugin to claim it names the second claimant's
240
+ * diagnostic. An empty claim leaves the slot free.
241
+ */
242
+ let owner = undefined;
243
+ let themeOwner = undefined;
244
+ return installed.map(({ declaration, identity }) => {
245
+ let theme = undefined;
246
+ if (declaration.theme !== undefined) {
247
+ if (themeOwner !== undefined) {
248
+ throw new DeclarationError(`${pluginSentence(identity)} claims the theme slot, which plugin "${themeOwner}" already holds. Install one owner.`);
249
+ }
250
+ theme = buildTheme(declaration.theme, identity);
251
+ themeOwner = identity;
252
+ }
253
+ defineExtensions(identity, declaration, build);
254
+ const inputs = readOptions(identity, declaration.options, build);
255
+ const names = new Set(inputs.map((input) => input.name));
256
+ const signals = readSignals(identity, declaration.signals);
257
+ if (signals.length > 0) {
258
+ if (owner !== undefined) {
259
+ throw new DeclarationError(`${pluginSentence(identity)} claims the signals slot, which plugin "${owner}" already holds. Install one owner.`);
260
+ }
261
+ owner = identity;
262
+ }
263
+ return {
264
+ identity,
265
+ inputs,
266
+ middleware: readMiddleware(identity, declaration.middleware, names),
267
+ onCommandAttach: readHook(identity, declaration.onCommandAttach),
268
+ signals,
269
+ theme,
270
+ views: declaration.views,
271
+ };
272
+ });
273
+ }
274
+ /** The signals the one slot owner claimed, or none when no installed plugin claims the slot. */
275
+ function ownedSignals(plugins) {
276
+ return plugins.find((entry) => entry.signals.length > 0)?.signals ?? [];
277
+ }
278
+ export { buildPlugins, installPlugins, ownedSignals, plugin, pluginSentence };
@@ -0,0 +1,21 @@
1
+ import type { Host } from './types.js';
2
+ type SwitchPolicy = 'auto' | 'always' | 'never';
3
+ interface RenderingPolicy {
4
+ color?: SwitchPolicy | undefined;
5
+ modifiers?: SwitchPolicy | undefined;
6
+ hyperlinks?: SwitchPolicy | undefined;
7
+ terminalControls?: 'strip' | 'preserve' | undefined;
8
+ }
9
+ interface Capabilities {
10
+ color: boolean;
11
+ modifiers: boolean;
12
+ hyperlinks: boolean;
13
+ terminalControls: boolean;
14
+ depth: 16 | 256 | 'truecolor';
15
+ mainGlyphs: boolean;
16
+ }
17
+ declare function renderingPolicy(value: unknown): RenderingPolicy;
18
+ /** A run owns captured facts; stdout and stderr each resolve this one policy independently. */
19
+ declare function capabilities(host: Host, destination: 'stdout' | 'stderr', policy: RenderingPolicy): Capabilities;
20
+ export type { Capabilities, RenderingPolicy };
21
+ export { capabilities, renderingPolicy };
@@ -0,0 +1,72 @@
1
+ import { DeclarationError } from './errors.js';
2
+ import { isPlainObject } from './facts.js';
3
+ function renderingPolicy(value) {
4
+ if (value === undefined) {
5
+ return {};
6
+ }
7
+ if (!isPlainObject(value)) {
8
+ throw new DeclarationError('The rendering policy must be an object.');
9
+ }
10
+ const result = {};
11
+ for (const field of ['color', 'modifiers', 'hyperlinks']) {
12
+ const policy = value[field];
13
+ if (policy !== undefined && policy !== 'auto' && policy !== 'always' && policy !== 'never') {
14
+ throw new DeclarationError(`Rendering ${field} must be auto, always, or never.`);
15
+ }
16
+ if (policy !== undefined) {
17
+ result[field] = policy;
18
+ }
19
+ }
20
+ const controls = value.terminalControls;
21
+ if (controls !== undefined && controls !== 'strip' && controls !== 'preserve') {
22
+ throw new DeclarationError('Rendering terminalControls must be strip or preserve.');
23
+ }
24
+ if (controls !== undefined) {
25
+ result.terminalControls = controls;
26
+ }
27
+ return result;
28
+ }
29
+ function colorDepth(env) {
30
+ if (env.COLORTERM === 'truecolor' ||
31
+ env.COLORTERM === '24bit' ||
32
+ ['xterm-kitty', 'xterm-ghostty', 'wezterm'].includes(env.TERM ?? '')) {
33
+ return 'truecolor';
34
+ }
35
+ if (env.TERM_PROGRAM === 'iTerm.app') {
36
+ const major = /^\d+/u.exec(env.TERM_PROGRAM_VERSION ?? '')?.[0];
37
+ return major !== undefined && Number(major) >= 3 ? 'truecolor' : 256;
38
+ }
39
+ return env.TERM_PROGRAM === 'Apple_Terminal' || /-256(?:color)?$/iu.test(env.TERM ?? '')
40
+ ? 256
41
+ : 16;
42
+ }
43
+ function mainGlyphs({ env, platform }) {
44
+ if (platform !== 'win32') {
45
+ return env.TERM !== 'linux';
46
+ }
47
+ return (Boolean(env.CI || env.WT_SESSION || env.TERMINUS_SUBLIME) ||
48
+ env.ConEmuTask === '{cmd::Cmder}' ||
49
+ env.TERM_PROGRAM === 'Terminus-Sublime' ||
50
+ env.TERM_PROGRAM === 'vscode' ||
51
+ env.TERM === 'xterm-256color' ||
52
+ env.TERM === 'alacritty' ||
53
+ env.TERMINAL_EMULATOR === 'JetBrains-JediTerm');
54
+ }
55
+ function enabled(policy, automatic) {
56
+ return policy === 'always' || (policy !== 'never' && automatic);
57
+ }
58
+ /** A run owns captured facts; stdout and stderr each resolve this one policy independently. */
59
+ function capabilities(host, destination, policy) {
60
+ const tty = host.terminal[destination].isTTY;
61
+ const capable = tty && host.env.TERM !== 'dumb';
62
+ const forced = Boolean(host.env.FORCE_COLOR);
63
+ return {
64
+ color: enabled(policy.color, forced || (!host.env.NO_COLOR && capable)),
65
+ depth: colorDepth(host.env),
66
+ hyperlinks: enabled(policy.hyperlinks, tty),
67
+ mainGlyphs: mainGlyphs(host),
68
+ modifiers: enabled(policy.modifiers, forced || capable),
69
+ terminalControls: policy.terminalControls === 'preserve',
70
+ };
71
+ }
72
+ export { capabilities, renderingPolicy };
@@ -0,0 +1,41 @@
1
+ import type { IncompleteResult } from './lanes.js';
2
+ import type { ViewContext } from './types.js';
3
+ import type { ResolvedRowView } from './view.js';
4
+ /**
5
+ * The view one sequence writes through. A row view writes each row as the source yields it; a
6
+ * whole view collects every row and renders once at the end of the source.
7
+ */
8
+ type SequenceView<Row> = {
9
+ kind: 'rows';
10
+ view: ResolvedRowView<Row>;
11
+ } | {
12
+ kind: 'whole';
13
+ render: (rows: readonly Row[], context: ViewContext) => unknown;
14
+ };
15
+ /** What one sequence writes through, supplied by the output that reserved its place. */
16
+ interface SequenceWriter<Row> {
17
+ /** The context of the destination the pieces reach. */
18
+ context: ViewContext;
19
+ /** Ends the reservation, so later output to that destination writes after this sequence. */
20
+ close: () => void;
21
+ /** Writes the incomplete-result line on stderr, before the fault's own report. */
22
+ incomplete: (facts: IncompleteResult) => void;
23
+ path: readonly string[];
24
+ /** Resolves and queues one piece on the reserved place, rejecting on a view or write fault. */
25
+ piece: (produce: () => unknown) => Promise<void>;
26
+ signal: AbortSignal;
27
+ source: Iterable<Row> | AsyncIterable<Row>;
28
+ /** Records a source failure, which this invocation reports after its primary outcome. */
29
+ stopped: (cause: unknown) => void;
30
+ view: SequenceView<Row>;
31
+ }
32
+ /** One sequence in flight: the promise its call answers with, and the switch that ends it early. */
33
+ interface LiveSequence {
34
+ pending: Promise<void>;
35
+ /** Ends the sequence where it next would go on. A finished sequence answers it with nothing. */
36
+ stop: () => void;
37
+ }
38
+ /** One sequence, started here and stoppable from the invocation that issued it. */
39
+ declare function writeSequence<Row>(writer: SequenceWriter<Row>): LiveSequence;
40
+ export type { LiveSequence, SequenceView, SequenceWriter };
41
+ export { writeSequence };
@@ -0,0 +1,225 @@
1
+ import { InternalError, routedSubject } from './errors.js';
2
+ /**
3
+ * A failure the source raised, so the writer tells it apart from the view and write faults its own
4
+ * pieces raise, which the output path has accounted for already.
5
+ */
6
+ class SourceFault {
7
+ cause;
8
+ constructor(cause) {
9
+ this.cause = cause;
10
+ }
11
+ }
12
+ /**
13
+ * The value a stopped writer unwinds with, compared by identity where the sequence ends. The
14
+ * invocation that stopped it has a primary failure of its own, so this value reports nothing.
15
+ */
16
+ const stopRequested = Symbol('stopped');
17
+ /**
18
+ * The steps one source answers with, under whichever protocol it carries. The asynchronous one
19
+ * answers first, as the language's own `for await` does.
20
+ * A value that iterates neither way answers with nothing, and its caller reports the fault.
21
+ */
22
+ function iterate(source) {
23
+ const asynchronous = source?.[Symbol.asyncIterator];
24
+ if (typeof asynchronous === 'function') {
25
+ return asynchronous.call(source);
26
+ }
27
+ const synchronous = source?.[Symbol.iterator];
28
+ if (typeof synchronous === 'function') {
29
+ return synchronous.call(source);
30
+ }
31
+ return undefined;
32
+ }
33
+ /**
34
+ * The steps one source answers with, with whatever the protocol raised marked as the source's own
35
+ * failure. A value that iterates neither way is the same fault, named for the Command that emitted
36
+ * it, because the types reject it and a JavaScript author alone reaches it.
37
+ */
38
+ function stepsOf(writer) {
39
+ let steps = undefined;
40
+ try {
41
+ steps = iterate(writer.source);
42
+ }
43
+ catch (error) {
44
+ throw new SourceFault(error);
45
+ }
46
+ if (!steps) {
47
+ throw new SourceFault(new InternalError(`The result of ${routedSubject(writer.path)} is not iterable.`, undefined));
48
+ }
49
+ return steps;
50
+ }
51
+ /** One request of the source, with whatever it raised marked as the source's own failure. */
52
+ async function pull(steps) {
53
+ try {
54
+ const result = await steps.next();
55
+ return result.done === true ? { done: true } : { done: false, value: result.value };
56
+ }
57
+ catch (error) {
58
+ throw new SourceFault(error);
59
+ }
60
+ }
61
+ /**
62
+ * The stop, read before the writer requests the next row and before it writes the piece it holds.
63
+ * A cancelled run stops here too, so core requests no further rows and writes no further pieces
64
+ * from the moment the signal aborted.
65
+ */
66
+ function halt(reading) {
67
+ if (reading.live.stopped || reading.signal.aborted) {
68
+ throw stopRequested;
69
+ }
70
+ }
71
+ /** One step, counted where the source produced a row. A stopped writer requests none. */
72
+ async function step(reading) {
73
+ halt(reading);
74
+ const result = await pull(reading.steps);
75
+ if (!result.done) {
76
+ reading.counts.yielded += 1;
77
+ }
78
+ // A row an in-flight request delivered after the stop is counted, and nothing is written for it.
79
+ halt(reading);
80
+ return result;
81
+ }
82
+ /**
83
+ * The source is told the writer wants no more rows, so a generator runs its own cleanup.
84
+ * The settlement is never awaited: a cleanup that never settles would pin the destination's tail,
85
+ * and the failure that stopped the sequence would never be reported.
86
+ * Whatever that cleanup raises is the source's business, and it is observed here alone, because the
87
+ * stop is reported through the fault that raised it and a second failure would replace it.
88
+ */
89
+ function endSteps(steps) {
90
+ try {
91
+ const ended = steps.return?.();
92
+ void Promise.resolve(ended).catch(() => undefined);
93
+ }
94
+ catch {
95
+ // A cleanup that throws outright is the same business, and it ends here too.
96
+ }
97
+ }
98
+ /** The opening piece, skipped where the view supplies no `head` function. */
99
+ async function writeHead(writer, produce) {
100
+ if (produce) {
101
+ await writer.piece(() => produce(writer.context));
102
+ }
103
+ }
104
+ /** The closing piece, skipped where the view supplies no `tail` function. */
105
+ async function writeTail(writer, produce, count) {
106
+ if (produce) {
107
+ await writer.piece(() => produce(count, writer.context));
108
+ }
109
+ }
110
+ /**
111
+ * Every piece but `tail`: `head`, then each row as the source yields it. Each piece is awaited
112
+ * before the next row is requested, so a slow destination applies back-pressure to the source.
113
+ */
114
+ async function writeEachRow(writer, view, reading) {
115
+ halt(reading);
116
+ await writeHead(writer, view.head);
117
+ for (;;) {
118
+ const next = await step(reading);
119
+ if (next.done) {
120
+ return;
121
+ }
122
+ const index = reading.counts.yielded - 1;
123
+ await writer.piece(() => view.row(next.value, index, writer.context));
124
+ reading.counts.written += 1;
125
+ }
126
+ }
127
+ /** Every row the source produced, which a whole view renders once at the end of the source. */
128
+ async function collectRows(reading) {
129
+ const collected = [];
130
+ for (;;) {
131
+ const next = await step(reading);
132
+ if (next.done) {
133
+ return collected;
134
+ }
135
+ collected.push(next.value);
136
+ }
137
+ }
138
+ /**
139
+ * Every piece the source decides, and the closing piece a complete sequence ends with: `head` and
140
+ * each row under a row view, whose closing piece is `tail`, and the collected rows under a whole
141
+ * view, whose closing piece is its one render. A whole view queues nothing before the source ends,
142
+ * so `written` counts no row under it.
143
+ */
144
+ async function writePieces(writer, reading) {
145
+ if (writer.view.kind === 'whole') {
146
+ const { render } = writer.view;
147
+ const collected = await collectRows(reading);
148
+ return () => writer.piece(() => render(collected, writer.context));
149
+ }
150
+ const { view } = writer.view;
151
+ await writeEachRow(writer, view, reading);
152
+ return () => writeTail(writer, view.tail, reading.counts.written);
153
+ }
154
+ /** The same pieces, with the source told to stop where one of them raised. */
155
+ async function closingPiece(writer, reading) {
156
+ try {
157
+ return await writePieces(writer, reading);
158
+ }
159
+ catch (error) {
160
+ endSteps(reading.steps);
161
+ throw error;
162
+ }
163
+ }
164
+ /**
165
+ * The sequence itself: every piece the source decides, then the closing piece. Every piece is
166
+ * awaited before the next row is requested, so a slow destination applies back-pressure to the
167
+ * source. The stop is read once more between the source's end and the closing piece, so a run
168
+ * cancelled there, and an action that failed there, never write it and a truncated result never
169
+ * reads as a complete one.
170
+ */
171
+ async function writeRows(writer, live, counts) {
172
+ const reading = { counts, live, signal: writer.signal, steps: stepsOf(writer) };
173
+ const close = await closingPiece(writer, reading);
174
+ if (live.stopped || writer.signal.aborted) {
175
+ return false;
176
+ }
177
+ await close();
178
+ return true;
179
+ }
180
+ /**
181
+ * One sequence written on the place its call reserved. A stop before the end retracts nothing, adds
182
+ * no `tail`, and writes one incomplete-result line on stderr before the fault's own report; a
183
+ * source failure is recorded there, because a call the action never awaited observes none.
184
+ */
185
+ async function runSequence(writer, live) {
186
+ const counts = { written: 0, yielded: 0 };
187
+ try {
188
+ if (!(await writeRows(writer, live, counts))) {
189
+ writer.incomplete({ path: writer.path, ...counts });
190
+ }
191
+ }
192
+ catch (error) {
193
+ writer.incomplete({ path: writer.path, ...counts });
194
+ raise(writer, error);
195
+ }
196
+ finally {
197
+ writer.close();
198
+ }
199
+ }
200
+ /**
201
+ * What one stop leaves behind once its line is written: nothing, where the invocation asked for
202
+ * the stop and reports its own failure, the source's failure recorded and raised, or the fault of
203
+ * a piece, which the output path has accounted for already.
204
+ */
205
+ function raise(writer, error) {
206
+ if (error === stopRequested) {
207
+ return;
208
+ }
209
+ if (error instanceof SourceFault) {
210
+ writer.stopped(error.cause);
211
+ throw error.cause;
212
+ }
213
+ throw error;
214
+ }
215
+ /** One sequence, started here and stoppable from the invocation that issued it. */
216
+ function writeSequence(writer) {
217
+ const live = { stopped: false };
218
+ return {
219
+ pending: runSequence(writer, live),
220
+ stop: () => {
221
+ live.stopped = true;
222
+ },
223
+ };
224
+ }
225
+ export { writeSequence };