fyflow-orchestration 0.7.0 → 0.8.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.
@@ -50,19 +50,23 @@
50
50
  * `bootstrap`, every declared one goes to the scheduler — and direction ONE (a pool naming a group
51
51
  * the document lacks) is untouched, refused by the resolver before anything is built.
52
52
  *
53
- * **Why `flows` is an argument here.** A language adapter's worker half reaches a typed pool through
54
- * that pool's config, and W61 measured that it is written ONLY where a node of that type declares a
55
- * transform — so the same type carries the engine on one flow and not on another. Pools are built
56
- * once, at `createRuntime`, so the flows a runtime will run are named there; `start` REFUSES a flow
57
- * whose transforming type was not accounted for, naming the type, the language and the fix, rather
58
- * than letting the wrapper meet a transform with nothing to evaluate it.
53
+ * **Why there is no `flows` argument here, and what `check` is** —
54
+ * [ADR 0096](../../doc/decisions/0096-a-runtime-is-its-registry-and-start-refuses-from-runtime-check.md),
55
+ * [W162](../../doc/work-items/w162-a-runtimes-languages-are-its-registrys-and-runtime-check.md). A runtime IS its
56
+ * registry: its pools are built once, here, and `fyflow-scheduler` cannot add one later (P37 fact 8). Until W162 a
57
+ * language reached a pool only where a node of that type transformed in a flow given here as `flows`, and `start`
58
+ * refused any other flow's transform (`LanguageNotBuilt`) — so a flow published after the runtime started could not
59
+ * transform on a type no earlier flow had. Now EVERY typed pool carries every language the registry names, and a flow
60
+ * needs nothing passed in advance. **`check(flow)`** returns every finding `start` would refuse that flow on that
61
+ * depends on the flow and this runtime alone, and **`start` and `resume` refuse from that same call**, so the two
62
+ * cannot disagree.
59
63
  */
60
64
  import { FyflowScheduler } from 'fyflow-scheduler';
61
65
  import type { ResourceGroup, WorkerManager } from 'fyflow-scheduler';
62
66
  import { Driver } from '../driver/driver.js';
63
- import type { Checkpoint, CheckpointHeader, DriverOptions } from '../driver/driver.js';
67
+ import type { Checkpoint, CheckpointHeader, DriverOptions, RunStopOptions } from '../driver/driver.js';
64
68
  import type { RunState } from '../fold/run-state.js';
65
- import type { FlowDefinition, JsonSchema, RegistryCode, RegistryDocument, ResolvedPool, ResolvedRegistry, RetryPolicy, TaskNode } from '../definition/types.js';
69
+ import type { Finding, FlowDefinition, JsonSchema, RegistryCode, RegistryDocument, ResolvedPool, ResolvedRegistry, RetryPolicy, TaskNode } from '../definition/types.js';
66
70
  import type { PoolFactory } from './bootstrap.js';
67
71
  /**
68
72
  * **A runtime that cannot be built, and why** — thrown at `createRuntime` or at `start`, before
@@ -81,9 +85,6 @@ export declare class GroupUnusable extends RuntimeError {
81
85
  /** A group type named by the document has no factory: a built-in this module does not implement, or a `code.groupTypes` entry `resolveRegistry` admitted and this cannot call. */
82
86
  export declare class GroupNotBuildable extends RuntimeError {
83
87
  }
84
- /** `start` was given a flow whose transforming type runs on pools this runtime built without that language — the pool would meet the transform with nothing to evaluate it. */
85
- export declare class LanguageNotBuilt extends RuntimeError {
86
- }
87
88
  /**
88
89
  * **A `start` or a `resume` naming a run id a driver of THIS runtime already holds** — `RN-R25`,
89
90
  * [ADR 0065](../../doc/decisions/0065-a-resume-replays-the-stored-stimuli-refuses-what-it-cannot-regenerate-and-re-dispatches-what-was-in-flight.md),
@@ -93,6 +94,17 @@ export declare class LanguageNotBuilt extends RuntimeError {
93
94
  */
94
95
  export declare class RunIdInUse extends RuntimeError {
95
96
  }
97
+ /**
98
+ * **A `start` or a `resume` given no `createTask`, of a flow a node of which reaches a group the BUILT-IN
99
+ * `KeyedRateLimitGroup` factory built** — `FD-R6`,
100
+ * [ADR 0097](../../doc/decisions/0097-what-start-refuses-that-the-flow-does-not-decide.md), P37 fact 1 and
101
+ * stop-and-ask (c). That factory gives the group no `keyFrom`, so its key is the task's `limitKey`, which nothing here
102
+ * sets — and the scheduler THROWS in `addTask` for a keyed group with no key. `at` is the first such node in the flow's
103
+ * order; the message carries every `FD-R6` finding `check` returned. Thrown before a run id is minted or a driver built,
104
+ * so the refused call costs nothing.
105
+ */
106
+ export declare class LimitKeyMissing extends RuntimeError {
107
+ }
96
108
  /**
97
109
  * **The three built-in group types' FACTORIES, and they are three different constructors** (P18
98
110
  * fact 2, re-run at W66): `(limit, id)`, `(windows, id)` and `(windows, { id })`, the third keyed.
@@ -109,6 +121,10 @@ export declare const BUILT_IN_GROUP_FACTORIES: Readonly<Record<string, (config:
109
121
  * **What a created group must carry.** The first three are what the scheduler CALLS on the acquire
110
122
  * path, the last three what the interface requires and a viewer reads — the union, by the
111
123
  * maintainer's decision at W66's declaration step. `EventTarget` is deliberately not required.
124
+ * **The `why` strings say "measured on 0.5.2" and stay dated**: W157 re-read the current pin's source
125
+ * rather than re-running the table above — `onStart`/`onFinish` are still optional in
126
+ * `ResourceGroup` (`groups/resourceGroup.ts:114`, `:117`) and still called unguarded
127
+ * (`core/FyflowScheduler.ts:894`, `:976`), so S10 stands unchanged.
112
128
  */
113
129
  export declare const GROUP_MEMBERS: readonly {
114
130
  readonly name: string;
@@ -124,16 +140,17 @@ export interface RuntimeOptions {
124
140
  * ([Q261](../../doc/questions/q261.md)), which is why the runtime cannot guess it.
125
141
  */
126
142
  base?: string;
127
- /**
128
- * **The flows this runtime will run**, read for WHICH types transform in WHICH languages (W61).
129
- * A flow given to `start` that names a transform on a type no pool was built for is refused.
130
- */
131
- flows?: readonly FlowDefinition[];
132
143
  /** The pool seam, as `bootstrap`'s — omitted, a real `WorkerManager`. */
133
144
  createPool?: PoolFactory;
134
145
  /** The scheduler seam. Omitted, a real `FyflowScheduler` over the pools and groups this runtime built. */
135
146
  createScheduler?: (pools: Record<string, WorkerManager>, groups: Record<string, ResourceGroup>) => FyflowScheduler;
136
- /** Applied to every node of every flow this runtime starts that declares no `retryPolicy` and whose type declares none — the driver's option, held to `FD-V30` there. */
147
+ /**
148
+ * Applied to every node of every flow this runtime starts that declares no `retryPolicy` and whose type declares none —
149
+ * the driver's option. **Held to `FD-V30` HERE, at `createRuntime`**, before anything is built — `InvalidDefinition`,
150
+ * the class and message `new Driver` throws for it ([ADR 0097](../../doc/decisions/0097-what-start-refuses-that-the-flow-does-not-decide.md)
151
+ * rule 5, P37 stop-and-ask (b)): until W163 it was read only by each driver, so a malformed one was accepted here and
152
+ * every `start` threw.
153
+ */
137
154
  defaultRetryPolicy?: RetryPolicy;
138
155
  /** `FD-R4`'s switch, forwarded to every driver this runtime starts (W73): the payload at `start` and a person's resolution at `resolve`, each held to the worker's schema once. Default ON; `false` turns both off runtime-wide, since the schemas are the document's. */
139
156
  validateBoundaries?: boolean;
@@ -168,7 +185,7 @@ export interface RuntimeOptions {
168
185
  * `createRuntime` could reach** ([Q297](../../doc/questions/q297.md), [W71](../../doc/work-items/w71-the-reference-stops-teaching-a-fake-scheduler.md)).
169
186
  * Of the eighteen members AS THEY STOOD a consumer reaches **two** because the runtime supplies them (`runId`,
170
187
  * `scheduler`), **six** through `RuntimeRegistry`, **two** through `RuntimeOptions` — and only two,
171
- * because `base`, `flows`, `createPool` and `createScheduler` are the runtime's own and are not
188
+ * because `base`, `flows` (as it stood: W162 removed it), `createPool` and `createScheduler` are the runtime's own and are not
172
189
  * `DriverOptions` members at all — and **seven** through this `Pick` as it stood. 2 + 6 + 2 + 7 is
173
190
  * seventeen; the eighteenth was `onRegionEvent`. *P20 fact 1 states the third road as SIX, which
174
191
  * makes its table 21 slots over 18 members and therefore not a partition at all; what survives is
@@ -187,8 +204,21 @@ export interface RuntimeOptions {
187
204
  * `Pick` and not in `RuntimeOptions` beside `validateBoundaries`: that one is a POLICY every driver
188
205
  * a runtime starts inherits, and this one names a `runId`. The orphan count stays **0** and its pin
189
206
  * moved 18 to 19 in the same diff, which is what the test's own comment asks for.
207
+ *
208
+ * **`onHeader` is the TWENTY-FIRST `DriverOptions` member and the twelfth here** — `RN-R39`,
209
+ * [ADR 0112](../../doc/decisions/0112-a-run-delivers-its-header-before-its-first-event.md), P36 (d),
210
+ * [W185](../../doc/work-items/w185-the-header-before-the-first-event.md). A DRIVER member, because only the driver stands
211
+ * between the header and the run's first dispatch: `start` dispatches synchronously inside `driver.start`, so a runtime
212
+ * could call it no earlier than the handle it returns. Conditionally spread, the binding's first road, and OMITTED from
213
+ * {@link ResumeOptions} below: `resume()` never calls it.
214
+ *
215
+ * **`onTaskMessage` and `onTaskProgress` are the TWENTY-SECOND and TWENTY-THIRD members, and the thirteenth and fourteenth
216
+ * here** — `RN-R42`, [ADR 0114](../../doc/decisions/0114-a-tasks-attribution-is-fields-and-its-messages-and-progress-are-passed-through.md),
217
+ * P36 (k), [W187](../../doc/work-items/w187-attribution-on-the-fold-and-messages-passed-through.md). Per-RUN callbacks, as
218
+ * every `on*` member here is; conditionally spread, the binding's first road — and KEPT by {@link ResumeOptions}: a
219
+ * resumed run's re-dispatched tasks emit as a started run's do, and nothing about the pass-through is the stored run's.
190
220
  */
191
- export interface StartOptions extends Pick<DriverOptions, 'createTask' | 'joinPolicy' | 'onFoldInput' | 'onNonconformingResult' | 'onRegionEvent' | 'onRunFailure' | 'onUnsatisfiableJoin' | 'onUnmintedTask' | 'now'> {
221
+ export interface StartOptions extends Pick<DriverOptions, 'createTask' | 'joinPolicy' | 'onFoldInput' | 'onHeader' | 'onNonconformingResult' | 'onRegionEvent' | 'onRunFailure' | 'onTaskMessage' | 'onTaskProgress' | 'onUnsatisfiableJoin' | 'onUnmintedTask' | 'now'> {
192
222
  /**
193
223
  * Omitted, the runtime mints `r<n>` from its own counter — inside `ID-R2`'s charset, unique per RUNTIME and not globally
194
224
  * (decision 10). **Since W89 the counter SKIPS an id in use on this runtime, and a caller naming one is refused** (`RunIdInUse`,
@@ -199,8 +229,10 @@ export interface StartOptions extends Pick<DriverOptions, 'createTask' | 'joinPo
199
229
  /**
200
230
  * **What `resume` may say about the run it resumes: everything `start` may, but its id** — `RN-R25`. The run id is the
201
231
  * checkpoint's (`header.runId`), because every task id the stored run dispatched embeds it; there is nothing to choose.
232
+ * **Nor its `onHeader`** (`RN-R39`, P36 (d)): the caller of `resume` already holds the header — it is the checkpoint's —
233
+ * and `Driver.resume` never calls it, so a member here would be a typed option that does nothing.
202
234
  */
203
- export type ResumeOptions = Omit<StartOptions, 'runId'>;
235
+ export type ResumeOptions = Omit<StartOptions, 'runId' | 'onHeader'>;
204
236
  /**
205
237
  * **What a driver is handed, and therefore what `runtime.registry` IS** — decision 14. Note
206
238
  * `predicates`: the driver wants the FUNCTIONS, keyed by name, where the validator's `Registry`
@@ -232,8 +264,8 @@ export type RuntimeRegistry = Pick<DriverOptions, 'predicates' | 'workers' | 'no
232
264
  *
233
265
  * **`done` RESOLVES and never rejects, and that reversed the plan's own lean on a measurement.**
234
266
  * Its settlement rule is terminality and nothing else, so a run the driver REFUSES resolves with
235
- * `outcome: 'stopped'` and `stoppedBy` carrying the reason rather than throwing it away — `failRun`
236
- * appends the `stopped` element BEFORE it calls `onRunFailure`, so a refusal is already terminal
267
+ * `outcome: 'halted'` and `haltedBy` carrying the reason rather than throwing it away — `failRun`
268
+ * appends the `halted` element BEFORE it calls `onRunFailure`, so a refusal is already terminal
237
269
  * when a consumer hears of it. The road not taken rejects there, and on the one class of refusal
238
270
  * that is reported AND rethrown (`start()` has the caller on the stack) it rejects a promise the
239
271
  * caller is never handed: measured in a child process with no listener, **exit 1, the process
@@ -250,8 +282,14 @@ export interface RunHandle {
250
282
  /** The run's `Driver`, already started — exactly what `start` returned before ADR 0058. */
251
283
  readonly driver: Driver;
252
284
  /**
253
- * **The run's end.** Resolves with the terminal `RunState` — `completed`, `failed`, `stuck` or
254
- * `stopped` — and never rejects. Pending while the outcome is `running` or `awaiting_action`.
285
+ * **The run's end.** Resolves with the terminal `RunState` — `completed`, `failed`, `stuck`, `cancelled` or
286
+ * `halted` (0.7.0's `stopped`, ADR 0102) — and never rejects. Pending while the outcome is `running`, `awaiting_action`
287
+ * or — since W173 — `cancelling`: a run its owner cancelled through `driver.cancel` settles `cancelled` after its LAST
288
+ * task has stopped, and is released then (ADR 0104, `RN-R30`); one a task the scheduler cancelled on its own ended
289
+ * settles at that first `task.cancelled` (`RN-R28`). **A run its owner STOPPED** (`stop` below, or `driver.stop`) settles
290
+ * `interrupted` — terminal on this runtime, resumable on another — once the stop's cancels have RESOLVED, not at the
291
+ * element: the stopped driver is deaf, so no settlement could move the fold again, and the runtime waits on the cancels
292
+ * the driver sent through this run's view of the scheduler instead (`RN-R33`, ADR 0105, amending ADR 0058 rule 3).
255
293
  *
256
294
  * It is the SAME expression a consumer would have written, moved: the runtime keeps ONE
257
295
  * `RunState` and `step`s it per fold input, which is what `fold` is
@@ -261,18 +299,34 @@ export interface RunHandle {
261
299
  * is a second route rather than a second definition.
262
300
  */
263
301
  readonly done: Promise<RunState>;
302
+ /**
303
+ * **Stop this run so that a resume on ANOTHER runtime can continue it** — `driver.stop(options)`, forwarded, and
304
+ * nothing else (`RN-R33`, [ADR 0105](../../doc/decisions/0105-a-run-its-owner-stops-goes-deaf-before-it-cancels-and-resumes-elsewhere.md),
305
+ * P34 stop-and-ask (d)): the run is held and the driver goes deaf, ONE `interrupted` element is appended, and each task in
306
+ * flight is cancelled; `done` then settles `interrupted` once those cancels have resolved, and the run is released.
307
+ * **One run, touching no sibling** on this runtime. Resume its checkpoint on a FRESH runtime: this one keeps the run
308
+ * id in use (`RN-R25`). **BREAKING in 0.8.0 for code that BUILDS a `RunHandle`** (P34 decision 10) — a new member.
309
+ *
310
+ * @throws RunControlRefused — `control: 'stop'`, as `driver.stop` throws it; nothing recorded or sent.
311
+ * @throws the consumer's own error — an `onFoldInput` that threw on the `interrupted` element (`OR-R10` v0.19, `RN-R33`
312
+ * v0.24, P36 (b), W184): the cancels are still sent, and the run is HALTED and reported once — `halted` outranks
313
+ * `interrupted`, so it is NOT resumable and `done` settles `halted` at once, without waiting on the cancels.
314
+ */
315
+ stop(options: RunStopOptions): Promise<void>;
264
316
  /**
265
317
  * **What a consumer writes FIRST when it stores this run by appending its events** — P23
266
318
  * decision 4, [ADR 0064](../../doc/decisions/0064-a-checkpoint-is-the-whole-event-sequence-and-a-header-and-it-is-a-copy.md)
267
319
  * rule 5. Equal to `driver.checkpoint().header` and a separate copy of it.
268
320
  *
269
- * **It arrives AFTER the run's first events, and that is measured rather than avoidable.** `start`
270
- * dispatches the entry node synchronously, so a consumer's `onFoldInput` has been handed **1 to 5**
271
- * elements before this handle exists — 1 on 59 of L6's 62 starts, 5 on a nested map at the entry
272
- * (W88's declaration step) — and it is not bounded by 5: an entry FAN-OUT of N branches delivers
273
- * `regionEntered`, N `branch.opened` and N `dispatched`, 2N + 1, before `start` returns (W88's
274
- * conformity reader, finding 11). That is what `onFoldInput`'s `seq` is for: a store keyed by position
275
- * does not care which was written first (`RN-R18`).
321
+ * **ON THE HANDLE it arrives AFTER the run's first events.** `start` dispatches the entry node synchronously, so a
322
+ * consumer's `onFoldInput` has been handed **1 to 5** elements before this handle exists — 1 on 59 of L6's 62 starts, 5
323
+ * on a nested map at the entry (W88's declaration step) — and it is not bounded by 5: an entry FAN-OUT of N branches
324
+ * delivers `regionEntered`, N `branch.opened` and N `dispatched`, 2N + 1, before `start` returns (W88's conformity
325
+ * reader, finding 11). **A store that must write its header FIRST passes `onHeader`** (`StartOptions`, `RN-R39`,
326
+ * [ADR 0112](../../doc/decisions/0112-a-run-delivers-its-header-before-its-first-event.md)): the same header, handed over
327
+ * inside `start` before the first element and before anything reaches the scheduler. *(This read *measured rather than
328
+ * avoidable* until W185: true of the handle, and the avoidance is the driver's.)* `onFoldInput`'s `seq` still names what a
329
+ * store holds whichever is written first (`RN-R18`).
276
330
  */
277
331
  readonly header: CheckpointHeader;
278
332
  }
@@ -287,6 +341,20 @@ export interface Runtime {
287
341
  readonly pools: Readonly<Record<string, ResolvedPool>>;
288
342
  /** The group objects this runtime CREATED, keyed as the document declared them. */
289
343
  readonly groups: Readonly<Record<string, ResourceGroup>>;
344
+ /**
345
+ * **Every finding `start(flow, payload, {})` would refuse `flow` on that depends on the FLOW and this runtime
346
+ * alone** — [ADR 0096](../../doc/decisions/0096-a-runtime-is-its-registry-and-start-refuses-from-runtime-check.md)
347
+ * rule 4, P37 stop-and-ask (b). `[]` means `start` will not refuse the flow; a non-empty result is what `start`
348
+ * throws, built from this same call (rule 5): the `FD-V` findings as `InvalidDefinition`, and — since
349
+ * [ADR 0097](../../doc/decisions/0097-what-start-refuses-that-the-flow-does-not-decide.md) — the `FD-R6` findings, a
350
+ * node reaching a group the built-in `KeyedRateLimitGroup` factory built, as `LimitKeyMissing` when `start` is given
351
+ * no `createTask`. **`check` reads as `start` given none** (P37 stop-and-ask (c)), so a caller who will pass a
352
+ * `createTask` setting `limitKey` filters `FD-R6` out by its rule id. **It never throws for a flow's defect**: every
353
+ * finding is returned, as `validateDefinition` returns them, against this runtime's own registry. **Not included,
354
+ * by name**: the PAYLOAD's refusals (`FD-R4`, the entry payload's clone) and the RUN ID's (`ID-R2`, `RunIdInUse`),
355
+ * which depend on what a run is started with. *Register* a flow on a running runtime by calling this, then `start`.
356
+ */
357
+ check(flow: FlowDefinition): Finding[];
290
358
  /**
291
359
  * Start one run of `flow` with `payload`, and return its {@link RunHandle} — the `Driver`,
292
360
  * already started, and a `done` promise that resolves when the run reaches a terminal
@@ -296,7 +364,7 @@ export interface Runtime {
296
364
  /**
297
365
  * **Resume the run a checkpoint stored**, on THIS runtime's scheduler, and return the same {@link RunHandle} `start` does —
298
366
  * `RN-R19` to `RN-R25`, [ADR 0065](../../doc/decisions/0065-a-resume-replays-the-stored-stimuli-refuses-what-it-cannot-regenerate-and-re-dispatches-what-was-in-flight.md).
299
- * The header is refused first and before any driver exists (`resumeRefusal` — format, flow id, version, a stopped run),
367
+ * The header is refused first and before any driver exists (`resumeRefusal` — format, flow id, version, a halted run, a cancelled run),
300
368
  * then a run id in use (`RunIdInUse`); then a driver is built for `header.runId` exactly as `start` builds one and
301
369
  * `Driver.resume` replays the stored events, re-dispatches what was in flight AT-LEAST-ONCE, and appends `resumed`.
302
370
  *
@@ -304,6 +372,11 @@ export interface Runtime {
304
372
  * regenerated past the stored events (the lost tail), then `resumed`, then whatever the run does next — so a store that
305
373
  * appends what it is handed holds the whole run again (`RN-R24`). `done` resolves with the terminal `RunState` of the
306
374
  * WHOLE run, stored elements included.
375
+ *
376
+ * **A consumer `onFoldInput` that THROWS on the `resumed` element** (`RN-R22` v0.24, P36 (c), W184): the run is HALTED and
377
+ * reported once, NOTHING of the in-flight set is handed to this runtime's scheduler, and the consumer's own error is
378
+ * thrown here — after the driver was built, so the run id is spent on this runtime (ADR 0091 rule 4, as for a resume
379
+ * refused during its replay).
307
380
  */
308
381
  resume(flow: FlowDefinition, checkpoint: Checkpoint, options?: ResumeOptions): RunHandle;
309
382
  /** The scheduler's, forwarded — a runtime owns its pools, so it owns their shutdown. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fyflow-orchestration",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Flow orchestration over fyflow-scheduler: sequence, decision, fan-out and join",
5
5
  "keywords": [
6
6
  "workflow",
@@ -126,12 +126,12 @@
126
126
  "devDependencies": {
127
127
  "@types/node": "^24.13.3",
128
128
  "esbuild": "^0.25.12",
129
- "fyflow-scheduler": "0.5.2",
129
+ "fyflow-scheduler": "0.6.0",
130
130
  "playwright": "^1.62.1",
131
131
  "typescript": "^5.9.2"
132
132
  },
133
133
  "peerDependencies": {
134
- "fyflow-scheduler": "0.5.2"
134
+ "fyflow-scheduler": "0.6.0"
135
135
  },
136
136
  "dependencies": {
137
137
  "jsonata": "2.2.2"