@particle-academy/fancy-flow 0.72.1 → 0.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/dist/durable/index.d.cts +94 -2
- package/dist/durable/index.d.ts +94 -2
- package/dist/durable.cjs +52 -2
- package/dist/durable.cjs.map +1 -1
- package/dist/durable.js +51 -3
- package/dist/durable.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -633,6 +633,28 @@ registerNodeKind({
|
|
|
633
633
|
`awaiting-input:` prefixes, so runs that parked under an older version still
|
|
634
634
|
resume.
|
|
635
635
|
|
|
636
|
+
### Queued runs: one node at a time
|
|
637
|
+
|
|
638
|
+
`@particle-academy/fancy-flow/durable` splits a run into one queue job per node.
|
|
639
|
+
`Coordinator.advance()` says what to dispatch, and `runNode()` claims, executes
|
|
640
|
+
and checkpoints one node.
|
|
641
|
+
|
|
642
|
+
**Serial is the default.** `advance()` hands out one node, and the next only
|
|
643
|
+
once that node has settled, in the graph's declaration order. A node paused for
|
|
644
|
+
a person keeps its slot, so nothing else goes out while they decide. To dispatch
|
|
645
|
+
more at once, pass `maxConcurrent`:
|
|
646
|
+
|
|
647
|
+
```ts
|
|
648
|
+
import { Coordinator, UNLIMITED_CONCURRENCY } from "@particle-academy/fancy-flow/durable";
|
|
649
|
+
|
|
650
|
+
new Coordinator({ graph, executors, run, store }); // serial
|
|
651
|
+
new Coordinator({ graph, executors, run, store, maxConcurrent: 4 }); // up to 4 held
|
|
652
|
+
new Coordinator({ graph, executors, run, store, maxConcurrent: UNLIMITED_CONCURRENCY }); // whole frontier
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
The selection is pinned by the shared `flow/durable-dispatch` conformance suite,
|
|
656
|
+
which specifies the same behaviour for the PHP and Python runtimes.
|
|
657
|
+
|
|
636
658
|
## Status
|
|
637
659
|
|
|
638
660
|
Shipping. Since this list was last written, all of the following landed:
|
package/dist/durable/index.d.cts
CHANGED
|
@@ -108,6 +108,10 @@ declare class InMemoryClaimStore implements NodeClaimStore {
|
|
|
108
108
|
/**
|
|
109
109
|
* Drop a paused node's claim so a recorded answer can re-run it.
|
|
110
110
|
*
|
|
111
|
+
* A PAUSED row holds one of the run's dispatch slots (see `selectDispatch`),
|
|
112
|
+
* so this is also what frees the slot: until the row is released, a serial
|
|
113
|
+
* run's `advance()` hands out nothing, the gate included.
|
|
114
|
+
*
|
|
111
115
|
* Not part of the interface: resuming a human gate is the host's decision and
|
|
112
116
|
* its storage's business. Provided here because the in-memory store is also
|
|
113
117
|
* what the tests resume through.
|
|
@@ -180,6 +184,57 @@ declare const Frontier: {
|
|
|
180
184
|
settleSkips(store: NodeClaimStore, runKey: string, skipped: readonly string[]): Promise<string[]>;
|
|
181
185
|
};
|
|
182
186
|
|
|
187
|
+
/**
|
|
188
|
+
* How many of ONE run's nodes may be held at once, and which ready nodes go next.
|
|
189
|
+
*
|
|
190
|
+
* ## Serial is the default
|
|
191
|
+
*
|
|
192
|
+
* A queued run hands a node to the queue only once the node before it has
|
|
193
|
+
* settled: one node of a run held at a time, in the graph's own declaration
|
|
194
|
+
* order. Parallel dispatch of a ready frontier is something a host ASKS for,
|
|
195
|
+
* with `maxConcurrent` on the {@link Coordinator}.
|
|
196
|
+
*
|
|
197
|
+
* | `maxConcurrent` | meaning |
|
|
198
|
+
* |---|---|
|
|
199
|
+
* | unset | **1**: serial |
|
|
200
|
+
* | `N >= 1` | up to N of the run's nodes held at once |
|
|
201
|
+
* | {@link UNLIMITED_CONCURRENCY} (`0`) | the whole ready frontier |
|
|
202
|
+
* | anything else | refused, by name |
|
|
203
|
+
*
|
|
204
|
+
* A negative number is refused rather than read as "unlimited". Under a serial
|
|
205
|
+
* default, a typo that silently turned a run parallel is the failure to avoid.
|
|
206
|
+
*
|
|
207
|
+
* ## Held means claimed OR paused
|
|
208
|
+
*
|
|
209
|
+
* A node parked on a person keeps its slot. A pause does not park the whole run
|
|
210
|
+
* in this runtime: `advance()` is called whenever any job settles, and without
|
|
211
|
+
* this rule it would hand out the gate's siblings while the person is still
|
|
212
|
+
* deciding.
|
|
213
|
+
*
|
|
214
|
+
* ## Measured against held work, not the batch
|
|
215
|
+
*
|
|
216
|
+
* Two nodes settling at once each trigger an `advance()` on a real queue. A cap
|
|
217
|
+
* applied to one batch would let each of them dispatch its own quota, so the
|
|
218
|
+
* budget is `maxConcurrent - held`, counted off the claim rows.
|
|
219
|
+
*
|
|
220
|
+
* This is the TypeScript member of a three-runtime contract. The
|
|
221
|
+
* `flow/durable-dispatch` conformance suite pins it, and names
|
|
222
|
+
* `fancy-flow-php`'s `DispatchLimit` and the Python runtime's
|
|
223
|
+
* `fancy_flow.durable.select_dispatch` as the other two implementations.
|
|
224
|
+
*/
|
|
225
|
+
|
|
226
|
+
/** Dispatch the whole ready frontier. Named so a host never writes a bare `0`. */
|
|
227
|
+
declare const UNLIMITED_CONCURRENCY = 0;
|
|
228
|
+
/**
|
|
229
|
+
* The ready nodes that may be dispatched now, in the order given.
|
|
230
|
+
*
|
|
231
|
+
* `ready` comes from `Frontier.compute`, in declaration order, and that order is
|
|
232
|
+
* kept: this slices, it never sorts. `state` must already include any skips the
|
|
233
|
+
* frontier just settled. Skips are never held, so that changes no count, but the
|
|
234
|
+
* state must describe the run as it is after the decision.
|
|
235
|
+
*/
|
|
236
|
+
declare function selectDispatch(ready: readonly string[], state: Record<string, NodeState>, maxConcurrent: number): string[];
|
|
237
|
+
|
|
183
238
|
/**
|
|
184
239
|
* Run ONE node of a graph — through the real engine, not around it.
|
|
185
240
|
*
|
|
@@ -396,7 +451,10 @@ declare function durableApproval(submissions: Submissions): NodeExecutor;
|
|
|
396
451
|
*
|
|
397
452
|
* `advance()`
|
|
398
453
|
* Ask the frontier what is unblocked, settle the skip cascade, and report the
|
|
399
|
-
*
|
|
454
|
+
* node ids that may be dispatched NOW. A queue adapter dispatches one job per
|
|
455
|
+
* id. By default that is at most one id: a run holds one node at a time, and
|
|
456
|
+
* the next node is handed out only when the one before it settles. See
|
|
457
|
+
* {@link CoordinatorOptions.maxConcurrent}.
|
|
400
458
|
*
|
|
401
459
|
* `runNode()`
|
|
402
460
|
* Claim one node, replay the graph through the real engine fenced to that
|
|
@@ -420,6 +478,12 @@ declare function durableApproval(submissions: Submissions): NodeExecutor;
|
|
|
420
478
|
* A human gate returns `paused`. `runToCompletion` returns immediately when it
|
|
421
479
|
* sees one — it does not spin, sleep or poll. The run is parked in the store,
|
|
422
480
|
* the process is free, and a recorded answer is what starts the next job.
|
|
481
|
+
*
|
|
482
|
+
* A paused node keeps its dispatch slot, so under any finite `maxConcurrent`
|
|
483
|
+
* (serial, by default) a queue adapter's `advance()` hands out nothing
|
|
484
|
+
* alongside a gate while the person decides. Resuming releases the row — see
|
|
485
|
+
* `InMemoryClaimStore.release` — which frees the slot for the gate to run
|
|
486
|
+
* again.
|
|
423
487
|
*/
|
|
424
488
|
|
|
425
489
|
/** What happened to one node. */
|
|
@@ -467,6 +531,19 @@ type CoordinatorOptions = {
|
|
|
467
531
|
initialInputs?: Record<string, Record<string, unknown>>;
|
|
468
532
|
retry?: RetryPolicy;
|
|
469
533
|
onEvent?: (event: RunEvent) => void;
|
|
534
|
+
/**
|
|
535
|
+
* How many of this run's nodes may be HELD at once. Held means CLAIMED by a
|
|
536
|
+
* worker or PAUSED on a person: a paused gate keeps its slot.
|
|
537
|
+
*
|
|
538
|
+
* **Defaults to `1`: serial.** `advance()` hands out one node, and the next
|
|
539
|
+
* only once that one has settled, in the graph's declaration order.
|
|
540
|
+
*
|
|
541
|
+
* A positive integer raises the cap. `UNLIMITED_CONCURRENCY` (`0`) hands out
|
|
542
|
+
* the whole ready frontier at once, which is what every queued run did before
|
|
543
|
+
* this option existed. A negative, fractional or non-numeric value throws
|
|
544
|
+
* here, at construction.
|
|
545
|
+
*/
|
|
546
|
+
maxConcurrent?: number;
|
|
470
547
|
};
|
|
471
548
|
declare class Coordinator {
|
|
472
549
|
readonly graph: FlowGraph;
|
|
@@ -475,12 +552,21 @@ declare class Coordinator {
|
|
|
475
552
|
readonly store: NodeClaimStore;
|
|
476
553
|
readonly initialInputs: Record<string, Record<string, unknown>>;
|
|
477
554
|
readonly retry: RetryPolicy;
|
|
555
|
+
/** The dispatch cap `advance()` applies. `0` is unlimited. */
|
|
556
|
+
readonly maxConcurrent: number;
|
|
478
557
|
private readonly onEvent?;
|
|
479
558
|
constructor(options: CoordinatorOptions);
|
|
480
559
|
get runKey(): string;
|
|
481
560
|
/**
|
|
482
561
|
* Which nodes may be dispatched right now.
|
|
483
562
|
*
|
|
563
|
+
* The ready frontier, cut to the run's {@link maxConcurrent} budget: the
|
|
564
|
+
* first `maxConcurrent - held` ready ids in declaration order, where `held`
|
|
565
|
+
* counts CLAIMED and PAUSED rows. Under the serial default that is at most one
|
|
566
|
+
* id, and none while a node is still running or a gate is waiting on a person.
|
|
567
|
+
* An empty result with work held is a throttled run, not a stalled one: the
|
|
568
|
+
* held node's settle is what calls this again.
|
|
569
|
+
*
|
|
484
570
|
* Also settles the skip cascade, because a skip is a decision the frontier
|
|
485
571
|
* just made and a second caller must not make it again.
|
|
486
572
|
*
|
|
@@ -507,6 +593,12 @@ declare class Coordinator {
|
|
|
507
593
|
/**
|
|
508
594
|
* Drive the graph here, in this process, one node at a time.
|
|
509
595
|
*
|
|
596
|
+
* It asks `advance()` exactly as a queue adapter does, so it runs nodes in the
|
|
597
|
+
* order a queued run under the same `maxConcurrent` would dispatch them. A run
|
|
598
|
+
* that finishes produces the same outputs under every limit. A run that stops
|
|
599
|
+
* on a pause or a failure can have run a different set of nodes before it
|
|
600
|
+
* stopped, because the order among nodes that are ready together differs.
|
|
601
|
+
*
|
|
510
602
|
* Every checkpoint is written exactly as a queued run writes it, so a crash
|
|
511
603
|
* mid-loop resumes from the same place a crashed worker would.
|
|
512
604
|
*
|
|
@@ -561,4 +653,4 @@ declare class Coordinator {
|
|
|
561
653
|
private forward;
|
|
562
654
|
}
|
|
563
655
|
|
|
564
|
-
export { BOUNDARY, Coordinator, type CoordinatorOptions, type DurableRunResult, FENCE_PORT, Frontier, type FrontierResult, InMemoryClaimStore, type NodeClaimStore, type NodeOutcome, NodeRunStatus, type NodeRunStatusValue, type NodeState, NotAwaitingHuman, type ReplayOptions, type ReplayResult, RetryPolicy, type RetryPolicyOptions, SETTLED, Submissions, UNSAFE_TO_REPLAY, durableApproval, durableUserInput, isBoundary, isSettled, replayUpTo };
|
|
656
|
+
export { BOUNDARY, Coordinator, type CoordinatorOptions, type DurableRunResult, FENCE_PORT, Frontier, type FrontierResult, InMemoryClaimStore, type NodeClaimStore, type NodeOutcome, NodeRunStatus, type NodeRunStatusValue, type NodeState, NotAwaitingHuman, type ReplayOptions, type ReplayResult, RetryPolicy, type RetryPolicyOptions, SETTLED, Submissions, UNLIMITED_CONCURRENCY, UNSAFE_TO_REPLAY, durableApproval, durableUserInput, isBoundary, isSettled, replayUpTo, selectDispatch };
|
package/dist/durable/index.d.ts
CHANGED
|
@@ -108,6 +108,10 @@ declare class InMemoryClaimStore implements NodeClaimStore {
|
|
|
108
108
|
/**
|
|
109
109
|
* Drop a paused node's claim so a recorded answer can re-run it.
|
|
110
110
|
*
|
|
111
|
+
* A PAUSED row holds one of the run's dispatch slots (see `selectDispatch`),
|
|
112
|
+
* so this is also what frees the slot: until the row is released, a serial
|
|
113
|
+
* run's `advance()` hands out nothing, the gate included.
|
|
114
|
+
*
|
|
111
115
|
* Not part of the interface: resuming a human gate is the host's decision and
|
|
112
116
|
* its storage's business. Provided here because the in-memory store is also
|
|
113
117
|
* what the tests resume through.
|
|
@@ -180,6 +184,57 @@ declare const Frontier: {
|
|
|
180
184
|
settleSkips(store: NodeClaimStore, runKey: string, skipped: readonly string[]): Promise<string[]>;
|
|
181
185
|
};
|
|
182
186
|
|
|
187
|
+
/**
|
|
188
|
+
* How many of ONE run's nodes may be held at once, and which ready nodes go next.
|
|
189
|
+
*
|
|
190
|
+
* ## Serial is the default
|
|
191
|
+
*
|
|
192
|
+
* A queued run hands a node to the queue only once the node before it has
|
|
193
|
+
* settled: one node of a run held at a time, in the graph's own declaration
|
|
194
|
+
* order. Parallel dispatch of a ready frontier is something a host ASKS for,
|
|
195
|
+
* with `maxConcurrent` on the {@link Coordinator}.
|
|
196
|
+
*
|
|
197
|
+
* | `maxConcurrent` | meaning |
|
|
198
|
+
* |---|---|
|
|
199
|
+
* | unset | **1**: serial |
|
|
200
|
+
* | `N >= 1` | up to N of the run's nodes held at once |
|
|
201
|
+
* | {@link UNLIMITED_CONCURRENCY} (`0`) | the whole ready frontier |
|
|
202
|
+
* | anything else | refused, by name |
|
|
203
|
+
*
|
|
204
|
+
* A negative number is refused rather than read as "unlimited". Under a serial
|
|
205
|
+
* default, a typo that silently turned a run parallel is the failure to avoid.
|
|
206
|
+
*
|
|
207
|
+
* ## Held means claimed OR paused
|
|
208
|
+
*
|
|
209
|
+
* A node parked on a person keeps its slot. A pause does not park the whole run
|
|
210
|
+
* in this runtime: `advance()` is called whenever any job settles, and without
|
|
211
|
+
* this rule it would hand out the gate's siblings while the person is still
|
|
212
|
+
* deciding.
|
|
213
|
+
*
|
|
214
|
+
* ## Measured against held work, not the batch
|
|
215
|
+
*
|
|
216
|
+
* Two nodes settling at once each trigger an `advance()` on a real queue. A cap
|
|
217
|
+
* applied to one batch would let each of them dispatch its own quota, so the
|
|
218
|
+
* budget is `maxConcurrent - held`, counted off the claim rows.
|
|
219
|
+
*
|
|
220
|
+
* This is the TypeScript member of a three-runtime contract. The
|
|
221
|
+
* `flow/durable-dispatch` conformance suite pins it, and names
|
|
222
|
+
* `fancy-flow-php`'s `DispatchLimit` and the Python runtime's
|
|
223
|
+
* `fancy_flow.durable.select_dispatch` as the other two implementations.
|
|
224
|
+
*/
|
|
225
|
+
|
|
226
|
+
/** Dispatch the whole ready frontier. Named so a host never writes a bare `0`. */
|
|
227
|
+
declare const UNLIMITED_CONCURRENCY = 0;
|
|
228
|
+
/**
|
|
229
|
+
* The ready nodes that may be dispatched now, in the order given.
|
|
230
|
+
*
|
|
231
|
+
* `ready` comes from `Frontier.compute`, in declaration order, and that order is
|
|
232
|
+
* kept: this slices, it never sorts. `state` must already include any skips the
|
|
233
|
+
* frontier just settled. Skips are never held, so that changes no count, but the
|
|
234
|
+
* state must describe the run as it is after the decision.
|
|
235
|
+
*/
|
|
236
|
+
declare function selectDispatch(ready: readonly string[], state: Record<string, NodeState>, maxConcurrent: number): string[];
|
|
237
|
+
|
|
183
238
|
/**
|
|
184
239
|
* Run ONE node of a graph — through the real engine, not around it.
|
|
185
240
|
*
|
|
@@ -396,7 +451,10 @@ declare function durableApproval(submissions: Submissions): NodeExecutor;
|
|
|
396
451
|
*
|
|
397
452
|
* `advance()`
|
|
398
453
|
* Ask the frontier what is unblocked, settle the skip cascade, and report the
|
|
399
|
-
*
|
|
454
|
+
* node ids that may be dispatched NOW. A queue adapter dispatches one job per
|
|
455
|
+
* id. By default that is at most one id: a run holds one node at a time, and
|
|
456
|
+
* the next node is handed out only when the one before it settles. See
|
|
457
|
+
* {@link CoordinatorOptions.maxConcurrent}.
|
|
400
458
|
*
|
|
401
459
|
* `runNode()`
|
|
402
460
|
* Claim one node, replay the graph through the real engine fenced to that
|
|
@@ -420,6 +478,12 @@ declare function durableApproval(submissions: Submissions): NodeExecutor;
|
|
|
420
478
|
* A human gate returns `paused`. `runToCompletion` returns immediately when it
|
|
421
479
|
* sees one — it does not spin, sleep or poll. The run is parked in the store,
|
|
422
480
|
* the process is free, and a recorded answer is what starts the next job.
|
|
481
|
+
*
|
|
482
|
+
* A paused node keeps its dispatch slot, so under any finite `maxConcurrent`
|
|
483
|
+
* (serial, by default) a queue adapter's `advance()` hands out nothing
|
|
484
|
+
* alongside a gate while the person decides. Resuming releases the row — see
|
|
485
|
+
* `InMemoryClaimStore.release` — which frees the slot for the gate to run
|
|
486
|
+
* again.
|
|
423
487
|
*/
|
|
424
488
|
|
|
425
489
|
/** What happened to one node. */
|
|
@@ -467,6 +531,19 @@ type CoordinatorOptions = {
|
|
|
467
531
|
initialInputs?: Record<string, Record<string, unknown>>;
|
|
468
532
|
retry?: RetryPolicy;
|
|
469
533
|
onEvent?: (event: RunEvent) => void;
|
|
534
|
+
/**
|
|
535
|
+
* How many of this run's nodes may be HELD at once. Held means CLAIMED by a
|
|
536
|
+
* worker or PAUSED on a person: a paused gate keeps its slot.
|
|
537
|
+
*
|
|
538
|
+
* **Defaults to `1`: serial.** `advance()` hands out one node, and the next
|
|
539
|
+
* only once that one has settled, in the graph's declaration order.
|
|
540
|
+
*
|
|
541
|
+
* A positive integer raises the cap. `UNLIMITED_CONCURRENCY` (`0`) hands out
|
|
542
|
+
* the whole ready frontier at once, which is what every queued run did before
|
|
543
|
+
* this option existed. A negative, fractional or non-numeric value throws
|
|
544
|
+
* here, at construction.
|
|
545
|
+
*/
|
|
546
|
+
maxConcurrent?: number;
|
|
470
547
|
};
|
|
471
548
|
declare class Coordinator {
|
|
472
549
|
readonly graph: FlowGraph;
|
|
@@ -475,12 +552,21 @@ declare class Coordinator {
|
|
|
475
552
|
readonly store: NodeClaimStore;
|
|
476
553
|
readonly initialInputs: Record<string, Record<string, unknown>>;
|
|
477
554
|
readonly retry: RetryPolicy;
|
|
555
|
+
/** The dispatch cap `advance()` applies. `0` is unlimited. */
|
|
556
|
+
readonly maxConcurrent: number;
|
|
478
557
|
private readonly onEvent?;
|
|
479
558
|
constructor(options: CoordinatorOptions);
|
|
480
559
|
get runKey(): string;
|
|
481
560
|
/**
|
|
482
561
|
* Which nodes may be dispatched right now.
|
|
483
562
|
*
|
|
563
|
+
* The ready frontier, cut to the run's {@link maxConcurrent} budget: the
|
|
564
|
+
* first `maxConcurrent - held` ready ids in declaration order, where `held`
|
|
565
|
+
* counts CLAIMED and PAUSED rows. Under the serial default that is at most one
|
|
566
|
+
* id, and none while a node is still running or a gate is waiting on a person.
|
|
567
|
+
* An empty result with work held is a throttled run, not a stalled one: the
|
|
568
|
+
* held node's settle is what calls this again.
|
|
569
|
+
*
|
|
484
570
|
* Also settles the skip cascade, because a skip is a decision the frontier
|
|
485
571
|
* just made and a second caller must not make it again.
|
|
486
572
|
*
|
|
@@ -507,6 +593,12 @@ declare class Coordinator {
|
|
|
507
593
|
/**
|
|
508
594
|
* Drive the graph here, in this process, one node at a time.
|
|
509
595
|
*
|
|
596
|
+
* It asks `advance()` exactly as a queue adapter does, so it runs nodes in the
|
|
597
|
+
* order a queued run under the same `maxConcurrent` would dispatch them. A run
|
|
598
|
+
* that finishes produces the same outputs under every limit. A run that stops
|
|
599
|
+
* on a pause or a failure can have run a different set of nodes before it
|
|
600
|
+
* stopped, because the order among nodes that are ready together differs.
|
|
601
|
+
*
|
|
510
602
|
* Every checkpoint is written exactly as a queued run writes it, so a crash
|
|
511
603
|
* mid-loop resumes from the same place a crashed worker would.
|
|
512
604
|
*
|
|
@@ -561,4 +653,4 @@ declare class Coordinator {
|
|
|
561
653
|
private forward;
|
|
562
654
|
}
|
|
563
655
|
|
|
564
|
-
export { BOUNDARY, Coordinator, type CoordinatorOptions, type DurableRunResult, FENCE_PORT, Frontier, type FrontierResult, InMemoryClaimStore, type NodeClaimStore, type NodeOutcome, NodeRunStatus, type NodeRunStatusValue, type NodeState, NotAwaitingHuman, type ReplayOptions, type ReplayResult, RetryPolicy, type RetryPolicyOptions, SETTLED, Submissions, UNSAFE_TO_REPLAY, durableApproval, durableUserInput, isBoundary, isSettled, replayUpTo };
|
|
656
|
+
export { BOUNDARY, Coordinator, type CoordinatorOptions, type DurableRunResult, FENCE_PORT, Frontier, type FrontierResult, InMemoryClaimStore, type NodeClaimStore, type NodeOutcome, NodeRunStatus, type NodeRunStatusValue, type NodeState, NotAwaitingHuman, type ReplayOptions, type ReplayResult, RetryPolicy, type RetryPolicyOptions, SETTLED, Submissions, UNLIMITED_CONCURRENCY, UNSAFE_TO_REPLAY, durableApproval, durableUserInput, isBoundary, isSettled, replayUpTo, selectDispatch };
|
package/dist/durable.cjs
CHANGED
|
@@ -72,6 +72,10 @@ var InMemoryClaimStore = class {
|
|
|
72
72
|
/**
|
|
73
73
|
* Drop a paused node's claim so a recorded answer can re-run it.
|
|
74
74
|
*
|
|
75
|
+
* A PAUSED row holds one of the run's dispatch slots (see `selectDispatch`),
|
|
76
|
+
* so this is also what frees the slot: until the row is released, a serial
|
|
77
|
+
* run's `advance()` hands out nothing, the gate included.
|
|
78
|
+
*
|
|
75
79
|
* Not part of the interface: resuming a human gate is the host's decision and
|
|
76
80
|
* its storage's business. Provided here because the in-memory store is also
|
|
77
81
|
* what the tests resume through.
|
|
@@ -2734,6 +2738,31 @@ var Frontier = {
|
|
|
2734
2738
|
}
|
|
2735
2739
|
};
|
|
2736
2740
|
|
|
2741
|
+
// src/durable/dispatch.ts
|
|
2742
|
+
var UNLIMITED_CONCURRENCY = 0;
|
|
2743
|
+
var DEFAULT_MAX_CONCURRENT = 1;
|
|
2744
|
+
function assertMaxConcurrent(value) {
|
|
2745
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0) return value;
|
|
2746
|
+
throw new RangeError(
|
|
2747
|
+
`maxConcurrent must be a positive integer, or UNLIMITED_CONCURRENCY (0) for the whole ready frontier, or unset for serial; got ${describe(value)}.`
|
|
2748
|
+
);
|
|
2749
|
+
}
|
|
2750
|
+
function selectDispatch(ready, state, maxConcurrent) {
|
|
2751
|
+
const limit = assertMaxConcurrent(maxConcurrent);
|
|
2752
|
+
if (limit === UNLIMITED_CONCURRENCY) return [...ready];
|
|
2753
|
+
let held = 0;
|
|
2754
|
+
for (const entry of Object.values(state)) {
|
|
2755
|
+
if (entry.status === NodeRunStatus.CLAIMED || entry.status === NodeRunStatus.PAUSED) held++;
|
|
2756
|
+
}
|
|
2757
|
+
return ready.slice(0, Math.max(0, limit - held));
|
|
2758
|
+
}
|
|
2759
|
+
function describe(value) {
|
|
2760
|
+
if (typeof value === "number") return String(value);
|
|
2761
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
2762
|
+
if (value === null) return "null";
|
|
2763
|
+
return typeof value;
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2737
2766
|
// src/durable/replay.ts
|
|
2738
2767
|
var BOUNDARY = "fancy-flow:node-boundary";
|
|
2739
2768
|
var FENCE_PORT = "fancy-flow:fenced";
|
|
@@ -2933,6 +2962,7 @@ var Coordinator = class {
|
|
|
2933
2962
|
this.store = options.store ?? new InMemoryClaimStore();
|
|
2934
2963
|
this.initialInputs = options.initialInputs ?? {};
|
|
2935
2964
|
this.retry = options.retry ?? new RetryPolicy();
|
|
2965
|
+
this.maxConcurrent = assertMaxConcurrent(options.maxConcurrent ?? DEFAULT_MAX_CONCURRENT);
|
|
2936
2966
|
this.onEvent = options.onEvent;
|
|
2937
2967
|
}
|
|
2938
2968
|
get runKey() {
|
|
@@ -2942,6 +2972,13 @@ var Coordinator = class {
|
|
|
2942
2972
|
/**
|
|
2943
2973
|
* Which nodes may be dispatched right now.
|
|
2944
2974
|
*
|
|
2975
|
+
* The ready frontier, cut to the run's {@link maxConcurrent} budget: the
|
|
2976
|
+
* first `maxConcurrent - held` ready ids in declaration order, where `held`
|
|
2977
|
+
* counts CLAIMED and PAUSED rows. Under the serial default that is at most one
|
|
2978
|
+
* id, and none while a node is still running or a gate is waiting on a person.
|
|
2979
|
+
* An empty result with work held is a throttled run, not a stalled one: the
|
|
2980
|
+
* held node's settle is what calls this again.
|
|
2981
|
+
*
|
|
2945
2982
|
* Also settles the skip cascade, because a skip is a decision the frontier
|
|
2946
2983
|
* just made and a second caller must not make it again.
|
|
2947
2984
|
*
|
|
@@ -2957,7 +2994,12 @@ var Coordinator = class {
|
|
|
2957
2994
|
const frontier = Frontier.compute(this.graph, state);
|
|
2958
2995
|
const settled = await Frontier.settleSkips(this.store, this.runKey, frontier.skipped);
|
|
2959
2996
|
this.warnForSkipped(settled, state);
|
|
2960
|
-
|
|
2997
|
+
const after = { ...state };
|
|
2998
|
+
for (const nodeId of frontier.skipped) {
|
|
2999
|
+
const row = state[nodeId];
|
|
3000
|
+
after[nodeId] = row ? { ...row, status: NodeRunStatus.SKIPPED, ports: [] } : { status: NodeRunStatus.SKIPPED, ports: [], attempts: 0, firstAttemptAt: "" };
|
|
3001
|
+
}
|
|
3002
|
+
return selectDispatch(frontier.ready, after, this.maxConcurrent);
|
|
2961
3003
|
}
|
|
2962
3004
|
/**
|
|
2963
3005
|
* Claim, execute and checkpoint one node.
|
|
@@ -3019,6 +3061,12 @@ var Coordinator = class {
|
|
|
3019
3061
|
/**
|
|
3020
3062
|
* Drive the graph here, in this process, one node at a time.
|
|
3021
3063
|
*
|
|
3064
|
+
* It asks `advance()` exactly as a queue adapter does, so it runs nodes in the
|
|
3065
|
+
* order a queued run under the same `maxConcurrent` would dispatch them. A run
|
|
3066
|
+
* that finishes produces the same outputs under every limit. A run that stops
|
|
3067
|
+
* on a pause or a failure can have run a different set of nodes before it
|
|
3068
|
+
* stopped, because the order among nodes that are ready together differs.
|
|
3069
|
+
*
|
|
3022
3070
|
* Every checkpoint is written exactly as a queued run writes it, so a crash
|
|
3023
3071
|
* mid-loop resumes from the same place a crashed worker would.
|
|
3024
3072
|
*
|
|
@@ -3027,7 +3075,7 @@ var Coordinator = class {
|
|
|
3027
3075
|
* claim with the same owner token — so the step key it derives is unchanged,
|
|
3028
3076
|
* which is what makes the retry idempotent rather than duplicative.
|
|
3029
3077
|
*/
|
|
3030
|
-
async runToCompletion(maxPasses = 1e4) {
|
|
3078
|
+
async runToCompletion(maxPasses = Math.max(1e4, this.graph.nodes.length + 1)) {
|
|
3031
3079
|
for (let pass = 0; pass < maxPasses; pass++) {
|
|
3032
3080
|
const ready = await this.advance();
|
|
3033
3081
|
if (ready.length === 0) break;
|
|
@@ -3185,11 +3233,13 @@ exports.NotAwaitingHuman = NotAwaitingHuman;
|
|
|
3185
3233
|
exports.RetryPolicy = RetryPolicy;
|
|
3186
3234
|
exports.SETTLED = SETTLED;
|
|
3187
3235
|
exports.Submissions = Submissions;
|
|
3236
|
+
exports.UNLIMITED_CONCURRENCY = UNLIMITED_CONCURRENCY;
|
|
3188
3237
|
exports.UNSAFE_TO_REPLAY = UNSAFE_TO_REPLAY;
|
|
3189
3238
|
exports.durableApproval = durableApproval;
|
|
3190
3239
|
exports.durableUserInput = durableUserInput;
|
|
3191
3240
|
exports.isBoundary = isBoundary;
|
|
3192
3241
|
exports.isSettled = isSettled;
|
|
3193
3242
|
exports.replayUpTo = replayUpTo;
|
|
3243
|
+
exports.selectDispatch = selectDispatch;
|
|
3194
3244
|
//# sourceMappingURL=durable.cjs.map
|
|
3195
3245
|
//# sourceMappingURL=durable.cjs.map
|