fyflow-orchestration 0.8.0 → 0.9.1
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/AGENTS.md +107 -7
- package/README.md +2 -1
- package/dist/browser/chunks/{chunk-EUU44PHD.js → chunk-I3KDEOLB.js} +749 -244
- package/dist/browser/index.js +3 -1
- package/dist/browser/scheduler/index.js +61 -13
- package/dist/node/chunks/{chunk-EUU44PHD.js → chunk-I3KDEOLB.js} +749 -244
- package/dist/node/index.js +3 -1
- package/dist/node/scheduler/index.js +61 -13
- package/dist/types/src/definition/regions.d.ts +12 -0
- package/dist/types/src/driver/driver.d.ts +80 -21
- package/dist/types/src/fold/accumulator.d.ts +94 -0
- package/dist/types/src/fold/run-state.d.ts +30 -3
- package/dist/types/src/scheduler/runtime.d.ts +39 -7
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -67,7 +67,7 @@ void shape;
|
|
|
67
67
|
**Two documents, and one file of your own.** A REGISTRY says what exists — a worker module, and a
|
|
68
68
|
node type that runs on it. A FLOW says what happens — nodes naming those types, and the edges between
|
|
69
69
|
them. `createRuntime` builds every pool, group and scheduler the registry names and hands back
|
|
70
|
-
|
|
70
|
+
**a handle** per run — `{ driver, done, header, stop, state, outcome, node }`: the driver already running, and `done` a promise that resolves with
|
|
71
71
|
the run's terminal state ([ADR 0058](doc/decisions/0058-runtime-start-returns-a-completion-handle-the-runtime-keeps.md)). **The only code you write is the worker**:
|
|
72
72
|
`examples/getting-started/worker.ts`, a class with a `run` method and nothing else — no base class,
|
|
73
73
|
no import. Everything below is executed by the gate on every `npm run gate`, and the same snippet is
|
|
@@ -477,6 +477,16 @@ once per item* is a fact about your document rather than about your run. **Measu
|
|
|
477
477
|
obstacle** — a size measurement is about 0.03% of what it would warn about at arity 1000.
|
|
478
478
|
`spec/flow-definition.md` section 3 carries the whole verdict and the measurements.
|
|
479
479
|
|
|
480
|
+
**Finding what a reference names costs its ANSWER** — from 0.9.1
|
|
481
|
+
([ADR 0124](doc/decisions/0124-a-reference-read-costs-its-answer.md), `spec/flow-definition.md` v0.46). A branch task
|
|
482
|
+
may read any earlier task of its own `map` instance by id — one step back or several, where `"."` reaches only its
|
|
483
|
+
predecessor — and the read takes that one output, however many instances the map has: the driver files a node's outputs
|
|
484
|
+
by the branch-path prefix its readers share. In 0.9.0 every read searched every output of the node it names, so such a
|
|
485
|
+
map cost O(items²) (the driver alone at 8,000 items: 22 s in 0.9.0, 0.22 s from 0.9.1, node 24). **A read never costs
|
|
486
|
+
less than its answer**: `["n"]` from a reader outside every region `n` is in — a SIBLING map's reader — is handed every
|
|
487
|
+
output of `n`, so two maps of `n` and `m` instances compose `n·m` entries. That is the answer's price, the cliff above by
|
|
488
|
+
another road, and the remedy is the same.
|
|
489
|
+
|
|
480
490
|
### `"$payload"` — what reached the RUN, from any node
|
|
481
491
|
|
|
482
492
|
**`"$payload"` names the value the run was STARTED with** — `start(flow, payload)`'s `payload` — on any node that may
|
|
@@ -1734,6 +1744,20 @@ A resolved node is **indistinguishable** from one that settled that way: same no
|
|
|
1734
1744
|
**Run state is a pure fold of an ordered log.** The driver produces the sequence; `fold` turns it
|
|
1735
1745
|
into state and touches nothing else.
|
|
1736
1746
|
|
|
1747
|
+
**What it costs** — `RN-R43`, from 0.9.0 ([ADR 0120](doc/decisions/0120-the-fold-is-one-accumulator-and-step-stays-pure.md)):
|
|
1748
|
+
**`fold(inputs)` is linear in its inputs** — a `map` over 4,000 items folds in milliseconds, where 0.8.0 took half a
|
|
1749
|
+
minute — and **`step(state, input)` is pure and costs O(the state it is handed)**: it never mutates the state you give
|
|
1750
|
+
it, so wherever the input moves something it copies what changed into a new state (and hands yours back where nothing
|
|
1751
|
+
moves) — and stepping every input of a long run yourself costs O(run²) in total. Fold the sequence when you
|
|
1752
|
+
need the state; step only where you hold a short one. Both return exactly what they did in 0.8.0, on every sequence a driver appends (`RN-R43` states its one limit).
|
|
1753
|
+
**The driver's own work per settlement does not grow with the run either** — `RN-R13` v0.30, from 0.9.0
|
|
1754
|
+
([ADR 0121](doc/decisions/0121-the-driver-keeps-a-per-task-view-of-its-log.md)): every question it asks of its log about
|
|
1755
|
+
ONE task — a settlement's effective outcome, whether a task is parked, what is in flight, what a `needs` can reach — reads
|
|
1756
|
+
that task's records alone (and from 0.9.1 a `needs` read takes the outputs it is handed rather than searching its node's —
|
|
1757
|
+
`RN-R13` v0.32, [ADR 0124](doc/decisions/0124-a-reference-read-costs-its-answer.md)), where 0.8.0 scanned the whole log (a 16,000-item `map` driven on a scheduler double: 999 ms in
|
|
1758
|
+
0.8.0, 229 ms from 0.9.0, node 24). `snapshot()`, `foldInputs()` and `checkpoint()` still hand you a copy of the whole run,
|
|
1759
|
+
O(run) a call: read them when you need the run, not after every event.
|
|
1760
|
+
|
|
1737
1761
|
```ts run
|
|
1738
1762
|
import { fold, isTerminal, type FoldInput } from 'fyflow-orchestration';
|
|
1739
1763
|
|
|
@@ -1839,7 +1863,8 @@ if (isTerminal(state)) console.log('finished as', state.outcome);
|
|
|
1839
1863
|
fold is defined over any prefix, and a prefix taken mid-turn is not a resting state: a settlement is
|
|
1840
1864
|
recorded before the routing it causes, so there are instants where nothing is running and the run is
|
|
1841
1865
|
not over. If you fold `driver.foldInputs()` after the fact you are always at a quiescent point; if
|
|
1842
|
-
you
|
|
1866
|
+
you read a run as it goes — a runtime's `handle.outcome()` from `onFoldInput` (below), or a fold you keep yourself on a
|
|
1867
|
+
bare `Driver` — read on a `queueMicrotask` — and that microtask is
|
|
1843
1868
|
**load-bearing rather than decoration**, which is why `done` asks its own predicate at one
|
|
1844
1869
|
([ADR 0058](doc/decisions/0058-runtime-start-returns-a-completion-handle-the-runtime-keeps.md)
|
|
1845
1870
|
rule 6). *(That phrase lived in §2's copied block until [W78](doc/work-items/w78-the-run-a-consumer-holds-in-one-line.md)
|
|
@@ -1850,7 +1875,7 @@ the block — W78's conformity reader, finding 3.)*
|
|
|
1850
1875
|
`awaiting_action` and `cancelling` and true for everything else — `interrupted` included, though a resume ELSEWHERE continues that run — and `stuck`, which is the point: before
|
|
1851
1876
|
`stuck` existed, a permanently hung run folded to `completed` and a waiter reported success.
|
|
1852
1877
|
|
|
1853
|
-
**And `done` is that rule applied for you.** `runtime.start` returns `{ driver, done, header }` —
|
|
1878
|
+
**And `done` is that rule applied for you.** `runtime.start` returns `{ driver, done, header, stop, state, outcome, node }` —
|
|
1854
1879
|
`header` is the run's CHECKPOINT header since
|
|
1855
1880
|
[ADR 0064](doc/decisions/0064-a-checkpoint-is-the-whole-event-sequence-and-a-header-and-it-is-a-copy.md),
|
|
1856
1881
|
and `driver.checkpoint()` hands out the header with every event so far (`spec/run-state.md`
|
|
@@ -1858,7 +1883,8 @@ section 8) — and, since 0.8.0, `start`'s `onHeader` option hands you the same
|
|
|
1858
1883
|
([ADR 0065](doc/decisions/0065-a-resume-replays-the-stored-stimuli-refuses-what-it-cannot-regenerate-and-re-dispatches-what-was-in-flight.md), `spec/run-state.md` section 9 — section 18 teaches it, as an executed snippet) — and `done`
|
|
1859
1884
|
resolves with the first `RunState` this predicate calls terminal
|
|
1860
1885
|
([ADR 0058](doc/decisions/0058-runtime-start-returns-a-completion-handle-the-runtime-keeps.md)). It
|
|
1861
|
-
is the same two functions — the runtime keeps
|
|
1886
|
+
is the same two functions — the runtime keeps the fold's own accumulator open, applies each input to it (`RN-R44`, from
|
|
1887
|
+
0.9.0; it stepped one `RunState` per input until then) and asks
|
|
1862
1888
|
`isTerminal` at a quiescent point — so there is one definition of *over* with one more caller, and
|
|
1863
1889
|
nothing above about reading at rest stops being true. **Three things follow, and two of them catch
|
|
1864
1890
|
people:**
|
|
@@ -1878,6 +1904,77 @@ people:**
|
|
|
1878
1904
|
`Promise.race([done, timeout])` is your one line — and a library default would be wrong for the
|
|
1879
1905
|
bullet above.
|
|
1880
1906
|
|
|
1907
|
+
### Reading a run WHILE it runs — `handle.state()`, `outcome()` and `node()`
|
|
1908
|
+
|
|
1909
|
+
**From 0.9.0 the handle `start` and `resume` return reads the run's state live** (`RN-R44`,
|
|
1910
|
+
[ADR 0122](doc/decisions/0122-a-runtimes-live-state-is-a-frozen-copy-on-its-handle.md)) — so a consumer showing a run as it
|
|
1911
|
+
goes no longer folds it a second time in its own `onFoldInput`:
|
|
1912
|
+
|
|
1913
|
+
| read | what it is | costs |
|
|
1914
|
+
|---|---|---|
|
|
1915
|
+
| `handle.outcome()` | the run's outcome now | O(1) — **the read to make after every input** |
|
|
1916
|
+
| `handle.node(taskId)` | that task's `nodes` entry now — the same frozen object `state()` holds — or `null` for a task the run never dispatched | O(1) in the run — its first read of an entry freezes that entry once |
|
|
1917
|
+
| `handle.state()` | the whole `RunState` now: a COPY, **frozen at every depth**, and the SAME object on every read until an input moves the run | O(run) for the first read after each input that moved it, nothing after — **so reading it after EVERY input costs O(run²) over a run**: read it when you draw the run |
|
|
1918
|
+
|
|
1919
|
+
**At a quiescent point `handle.state()` IS `fold(handle.driver.foldInputs())`** — the same rule as above: read on a
|
|
1920
|
+
`queueMicrotask` from `onFoldInput`, or outside the driver's turn. **It is not a live view** — a read is the state at that
|
|
1921
|
+
moment, and the next input makes a new one beside it; writing to it throws in strict code and reaches nothing the runtime
|
|
1922
|
+
keeps. **`done` resolves with the object `state()` returns then**, so `await done === handle.state()` once it has settled —
|
|
1923
|
+
until you `unpause` a run that finished while paused, the one run control a finished run still takes: `state()` follows
|
|
1924
|
+
that element, and `done` keeps the state at settlement. On a RESUMED run the reads hold the stored events too. **What
|
|
1925
|
+
`Object.freeze` does not stop stays writable** — a typed array's elements, an `ArrayBuffer`'s bytes, a `Date`'s time, a
|
|
1926
|
+
`Map`'s or `Set`'s membership — and a write there reaches later reads and `done`: return plain data, read plain data. **Nothing is added to `onFoldInput`**:
|
|
1927
|
+
what it hands you is unchanged, and the runtime keeps its own copy of every result rather than sharing yours.
|
|
1928
|
+
|
|
1929
|
+
**BREAKING in 0.9.0** (P39 (d) and (f)): `RunHandle` gains `state`, `outcome` and `node`, which breaks only code that BUILDS
|
|
1930
|
+
a handle; and `await done` is FROZEN — code that wrote into the state `done` resolved with now throws.
|
|
1931
|
+
|
|
1932
|
+
```ts run
|
|
1933
|
+
import { createRuntime } from 'fyflow-orchestration/scheduler';
|
|
1934
|
+
import { fold, type FlowDefinition, type RegistryDocument } from 'fyflow-orchestration';
|
|
1935
|
+
|
|
1936
|
+
const registry: RegistryDocument = {
|
|
1937
|
+
workers: { scorer: { url: '../../examples/getting-started/scorer.ts' } },
|
|
1938
|
+
nodeTypes: { Scorer: { worker: 'scorer', inline: true } },
|
|
1939
|
+
};
|
|
1940
|
+
const flow: FlowDefinition = {
|
|
1941
|
+
flowId: 'live', version: 1, entry: 'a',
|
|
1942
|
+
nodes: [
|
|
1943
|
+
{ id: 'a', kind: 'task', type: 'Scorer', settings: { score: 0.1 }, edges: [{ to: 'split' }] },
|
|
1944
|
+
{ id: 'split', kind: 'fanout', branches: ['b', 'c'] },
|
|
1945
|
+
{ id: 'b', kind: 'task', type: 'Scorer', settings: { score: 0.2 }, edges: [{ to: 'j' }] },
|
|
1946
|
+
{ id: 'c', kind: 'task', type: 'Scorer', settings: { score: 0.3 }, edges: [{ to: 'j' }] },
|
|
1947
|
+
{ id: 'j', kind: 'join', of: 'split', edges: [{ to: 'e' }] },
|
|
1948
|
+
{ id: 'e', kind: 'end', outcome: 'completed' },
|
|
1949
|
+
],
|
|
1950
|
+
};
|
|
1951
|
+
|
|
1952
|
+
const runtime = createRuntime(registry, {}, { base: import.meta.url });
|
|
1953
|
+
// The CHEAP read after every input, on a microtask: the driver's turn is over by then (RN-R12). The microtasks of the
|
|
1954
|
+
// inputs `start` delivers before it returns run after it has, so `handle` is there for every one.
|
|
1955
|
+
const outcomes: string[] = [];
|
|
1956
|
+
const handle = runtime.start(flow, {}, {
|
|
1957
|
+
onFoldInput: () => queueMicrotask(() => { outcomes.push(handle.outcome()); }),
|
|
1958
|
+
});
|
|
1959
|
+
const end = await handle.done;
|
|
1960
|
+
await runtime.shutdown();
|
|
1961
|
+
console.log(outcomes.length, outcomes.at(-1), end.outcome); // one outcome per input, the last 'completed'
|
|
1962
|
+
|
|
1963
|
+
const a = `${handle.driver.runId}:a:-:0`;
|
|
1964
|
+
if (outcomes.length < 10 || outcomes.at(-1) !== 'completed') throw new Error(`one outcome() per input, the last completed: ${outcomes.join()}`);
|
|
1965
|
+
if (outcomes.some((o) => o !== 'running' && o !== 'completed')) throw new Error(`a run with nobody in it reads running, then completed: ${outcomes.join()}`);
|
|
1966
|
+
// `done` and `state()` are ONE object, and it is the caller's own fold — key order included.
|
|
1967
|
+
if (end !== handle.state()) throw new Error('await done is the object state() returns — one copy, not two');
|
|
1968
|
+
if (JSON.stringify(end) !== JSON.stringify(fold(handle.driver.foldInputs()))) throw new Error('state() is the fold of what the driver appended');
|
|
1969
|
+
// `node()` is the state's own entry — no copy — and null for a task the run never dispatched.
|
|
1970
|
+
if (handle.node(a) !== end.nodes[a] || handle.node(`${handle.driver.runId}:nope:-:0`) !== null) throw new Error('node() reads the state`s own entry, or null');
|
|
1971
|
+
// Frozen at every depth: the run, its nodes, an entry and the result inside it.
|
|
1972
|
+
if (!Object.isFrozen(end) || !Object.isFrozen(end.nodes) || !Object.isFrozen(end.nodes[a]) || !Object.isFrozen(end.nodes[a]!.result)) throw new Error('state() is frozen at every depth');
|
|
1973
|
+
let wrote = true;
|
|
1974
|
+
try { (end.nodes[a]!.result as { score: number }).score = 1; } catch { wrote = false; }
|
|
1975
|
+
if (wrote) throw new Error('a write into the run`s state must throw — it is a copy to read, not a document to edit');
|
|
1976
|
+
```
|
|
1977
|
+
|
|
1881
1978
|
### Pausing a run — `driver.pause` / `driver.unpause`, and a FLAG that is not an outcome
|
|
1882
1979
|
|
|
1883
1980
|
**`driver.pause({ actor })` holds a run without ending it, and `driver.unpause({ actor })` lets it go on** (`RN-R35`,
|
|
@@ -2757,6 +2854,7 @@ if (!haltedRun.haltedBy?.startsWith('UncloneableResult')) throw new Error(`halte
|
|
|
2757
2854
|
| **nested fan-out** — a branch that fans out again | **built, and the join over it is settled.** `FD-V15` is retired; a task inside a nested region is dispatched at its whole dotted `branchPath` (`r1:scoreC:1.0:0`); and every policy in section 5 is **transitive** — each region answers exactly as it would if it were not nested ([ADR 0030](doc/decisions/0030-a-join-is-transitive-and-a-dead-join-ends-the-branch-it-sits-in.md)). One thing to know: if an inner join is reported through `onUnsatisfiableJoin`, the outer branch containing it **ends** rather than waiting, so the outer join may still fire and its successor may run — and once that work settles the run folds to `stuck`, because a region that never fired is what `stuck` reads. **Fold too early and you will see `running`.** The four matrix rows and the join seam are tested at **depth 2**; the dead-join cascade is asserted at depth 3, and nothing else about depth 3 is a tested claim |
|
|
2758
2855
|
| ~~**map-style fan-out**~~ — **BUILT** since [W33](doc/work-items/w33-a-map-at-run-time-arity.md); see section 3 |
|
|
2759
2856
|
| ~~**RUNNING an `ask`**~~ — a node that waits for a person, its answer at `resolve()` | **BUILT** since [W180](doc/work-items/w180-asking-and-answering-in-a-run.md): a run parks at an ask with nothing handed to the scheduler, and its answer is its result — section 7. Under a cancel, a stop, a pause and a resume an ask is a parked task — never in flight, so nothing is ever sent or handed on for it — since [W181](doc/work-items/w181-an-ask-under-run-control.md) (section 7) |
|
|
2857
|
+
| a **live, mutable view** of a run's state, or the state handed to `onFoldInput` beside each input | **no** — [P39](doc/plans/p39-what-a-step-costs-and-where-a-live-run-state-is-read.md) (b), [ADR 0122](doc/decisions/0122-a-runtimes-live-state-is-a-frozen-copy-on-its-handle.md): `handle.state()` is a frozen COPY, cached until the run moves, and `handle.outcome()` / `handle.node()` are the O(1) reads — section 8 |
|
|
2760
2858
|
| a **join timeout** | nothing. A stuck join is detected by the `stuck` outcome, with no clock |
|
|
2761
2859
|
| **per-join policy choice** | one policy per driver |
|
|
2762
2860
|
| a **declared event family** for `fyflow-react-components` | `onRegionEvent`, `onFoldInput` and `joinStatusOf()` — see below |
|
|
@@ -2765,7 +2863,9 @@ if (!haltedRun.haltedBy?.startsWith('UncloneableResult')) throw new Error(`halte
|
|
|
2765
2863
|
scheduler cannot show which node a run is at, which branch it took, or what a join is waiting on.
|
|
2766
2864
|
That is [Q21](doc/questions/q21.md), and it is about the scheduler's stream, not about this
|
|
2767
2865
|
library — **this library does expose all three.** `onFoldInput` is the push half of `foldInputs()`,
|
|
2768
|
-
so you can
|
|
2866
|
+
so you can see a run finish without polling — on a runtime, read `handle.outcome()` after each input and
|
|
2867
|
+
`handle.state()` when you draw the run (section 8, *Reading a run WHILE it runs*); on a bare `Driver`, mind section 8's
|
|
2868
|
+
*What it costs*: a `step` per input is O(run) each; `onRegionEvent` reports branches
|
|
2769
2869
|
opening, arriving, exiting, suspending and resuming; `joinStatusOf()` answers what a join is waiting
|
|
2770
2870
|
on. What is missing is an agreed envelope shape between the two repositories, not the data.
|
|
2771
2871
|
|
|
@@ -2785,7 +2885,7 @@ is recorded rather than added to the gate.
|
|
|
2785
2885
|
`fyflow-orchestration/scheduler`, [W66](doc/work-items/w66-the-runtime-entry.md). It resolves every
|
|
2786
2886
|
name the document uses against the code you supply, refuses every defect at construction by a named
|
|
2787
2887
|
error, builds one pool per type instance and one object per group, holds a `FyflowScheduler` over
|
|
2788
|
-
them, and hands you
|
|
2888
|
+
them, and hands you **a handle per run** — `{ driver, done, header, stop, state, outcome, node }`: the driver already running, `done` a
|
|
2789
2889
|
promise that resolves with the run's terminal `RunState`, and `header` the run's checkpoint header — handed to
|
|
2790
2890
|
`start`'s `onHeader` option too, before the run's first event, for a store that writes it first (section 18)
|
|
2791
2891
|
([ADR 0058](doc/decisions/0058-runtime-start-returns-a-completion-handle-the-runtime-keeps.md);
|
|
@@ -4304,7 +4404,7 @@ checkpoint is an append to it**. You never have to call `driver.checkpoint()` to
|
|
|
4304
4404
|
|---|---|---|
|
|
4305
4405
|
| the header | **`onHeader(header)`** on `start`'s options — handed to you inside `start`, BEFORE the first event and before anything reaches the scheduler; and `runtime.start(…)` returns the same one as `header`, beside `driver` and `done` | write it once, FIRST |
|
|
4306
4406
|
| every event | `onFoldInput(event, position)` | write it AT `position` — the driver's index, never a count you start again in each process |
|
|
4307
|
-
| a resumed run | `runtime.resume(flow, { header, events }, options)` returns the same `{ driver, done, header }` — and calls no `onHeader`: you hold the header already | keep writing: it hands you positions from the STORED LENGTH on |
|
|
4407
|
+
| a resumed run | `runtime.resume(flow, { header, events }, options)` returns the same handle — `{ driver, done, header, stop, state, outcome, node }` — and calls no `onHeader`: you hold the header already | keep writing: it hands you positions from the STORED LENGTH on |
|
|
4308
4408
|
|
|
4309
4409
|
**Write the header FIRST, through `onHeader`** (`RN-R39`, [ADR 0112](doc/decisions/0112-a-run-delivers-its-header-before-its-first-event.md),
|
|
4310
4410
|
since 0.8.0). The first events are delivered WHILE `start` runs, so the handle's `header` arrives after them, and a process
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ worker returned, routing can **fan out** into parallel branches, and a **join**
|
|
|
17
17
|
The scheduler runs tasks; this library decides **which task runs next**. `fyflow-scheduler` has no
|
|
18
18
|
dependency graph of its own, so sequence, decision, fan-out and join live here.
|
|
19
19
|
|
|
20
|
-
Published on npm as `fyflow-orchestration` and on JSR as `@fyflow/orchestration`. The manifests read `0.
|
|
20
|
+
Published on npm as `fyflow-orchestration` and on JSR as `@fyflow/orchestration`. The manifests read `0.9.1`.
|
|
21
21
|
|
|
22
22
|
## Install
|
|
23
23
|
|
|
@@ -258,6 +258,7 @@ if (after.counts.no !== 0) throw new Error('and the flow that ran has no unwired
|
|
|
258
258
|
| A task **cancelled** by its id — the run ends `cancelled` and refuses a resume | [AGENTS.md](AGENTS.md) section 6 |
|
|
259
259
|
| **Cancelling a run** — `driver.cancel`, the run `cancelling` until its last task has stopped | section 6 |
|
|
260
260
|
| **Pausing** a run and unpausing it — every dispatch held, then handed on in order | section 8 |
|
|
261
|
+
| Reading a run's state **while it runs** — `handle.outcome()` after every input, `handle.state()`, a frozen copy, when you draw it | section 8 |
|
|
261
262
|
| A node that **waits for a person** by design — an `ask`, which keeps what reached it and holds the answer to a schema | section 7 |
|
|
262
263
|
| A task's **messages and progress**, each with its run, node and attempt as fields — `onTaskMessage`, `onTaskProgress` | section 8 |
|
|
263
264
|
| A **worker's own messages**, outside any task — `worker.message` on its pool, reached through `createScheduler` | section 8 |
|