@ersbeth/picoflow 2.3.1 → 3.0.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 +37 -12
- package/SKILL.md +130 -16
- package/dist/index.d.ts +767 -0
- package/dist/index.js +544 -0
- package/dist/inspect.d.ts +252 -0
- package/dist/inspect.js +300 -0
- package/dist/shared.d.ts +361 -0
- package/dist/shared.js +1496 -0
- package/dist/shared2.d.ts +93 -0
- package/dist/shared2.js +59 -0
- package/dist/shared3.d.ts +54 -0
- package/dist/shared3.js +17 -0
- package/dist/{types/converters/solid.d.ts → solid.d.ts} +10 -8
- package/dist/solid.js +63 -0
- package/package.json +38 -34
- package/dist/picoflow.js +0 -1283
- package/dist/types/api/base/flowConfig.d.ts +0 -17
- package/dist/types/api/base/flowDisposable.d.ts +0 -40
- package/dist/types/api/base/flowErrors.d.ts +0 -1
- package/dist/types/api/base/flowObservable.d.ts +0 -26
- package/dist/types/api/base/flowPrimitiveOptions.d.ts +0 -14
- package/dist/types/api/base/flowSubscribable.d.ts +0 -78
- package/dist/types/api/base/flowTracker.d.ts +0 -7
- package/dist/types/api/base/index.d.ts +0 -7
- package/dist/types/api/index.d.ts +0 -2
- package/dist/types/api/nodes/async/flowConstantAsync.d.ts +0 -32
- package/dist/types/api/nodes/async/flowDerivationAsync.d.ts +0 -37
- package/dist/types/api/nodes/async/flowStateAsync.d.ts +0 -42
- package/dist/types/api/nodes/async/flowWritableDerivationAsync.d.ts +0 -31
- package/dist/types/api/nodes/async/index.d.ts +0 -4
- package/dist/types/api/nodes/collections/flowArray.d.ts +0 -138
- package/dist/types/api/nodes/collections/flowMap.d.ts +0 -100
- package/dist/types/api/nodes/collections/index.d.ts +0 -2
- package/dist/types/api/nodes/flowEffect.d.ts +0 -28
- package/dist/types/api/nodes/flowSignal.d.ts +0 -26
- package/dist/types/api/nodes/flowValue.d.ts +0 -35
- package/dist/types/api/nodes/index.d.ts +0 -7
- package/dist/types/api/nodes/sync/flowConstant.d.ts +0 -30
- package/dist/types/api/nodes/sync/flowDerivation.d.ts +0 -37
- package/dist/types/api/nodes/sync/flowState.d.ts +0 -40
- package/dist/types/api/nodes/sync/flowWritableDerivation.d.ts +0 -29
- package/dist/types/api/nodes/sync/index.d.ts +0 -4
- package/dist/types/api/nodes/utils.d.ts +0 -22
- package/dist/types/base/dependenciesRegistry.d.ts +0 -1
- package/dist/types/base/dependentsRegistry.d.ts +0 -1
- package/dist/types/base/disposable.d.ts +0 -15
- package/dist/types/base/errors.d.ts +0 -31
- package/dist/types/base/executionStack.d.ts +0 -31
- package/dist/types/base/index.d.ts +0 -8
- package/dist/types/base/node.d.ts +0 -27
- package/dist/types/base/observable.d.ts +0 -34
- package/dist/types/base/observer.d.ts +0 -33
- package/dist/types/converters/index.d.ts +0 -1
- package/dist/types/index.d.ts +0 -2
- package/dist/types/nodes/actionNode.d.ts +0 -1
- package/dist/types/nodes/arrayNode.d.ts +0 -1
- package/dist/types/nodes/effectNode.d.ts +0 -1
- package/dist/types/nodes/index.d.ts +0 -8
- package/dist/types/nodes/mapNode.d.ts +0 -1
- package/dist/types/nodes/signalNode.d.ts +0 -1
- package/dist/types/nodes/valueAsyncNode.d.ts +0 -1
- package/dist/types/nodes/valueNode.d.ts +0 -1
- package/dist/types/nodes/valueSyncNode.d.ts +0 -1
- package/dist/types/schedulers/asyncResolver.d.ts +0 -1
- package/dist/types/schedulers/asyncScheduler.d.ts +0 -1
- package/dist/types/schedulers/index.d.ts +0 -4
- package/dist/types/schedulers/pendingError.d.ts +0 -1
- package/dist/types/schedulers/scheduler.d.ts +0 -1
- package/dist/types/schedulers/syncResolver.d.ts +0 -1
- package/dist/types/schedulers/syncScheduler.d.ts +0 -1
package/README.md
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
**Picoflow** is a lightweight reactive dataflow library that provides fine-grained reactive primitives. It gives you an intuitive API for signals, state, (asynchronous) derivations, effects, and reactive maps/arrays. Picoflow uses an explicit tracking context to automatically track reactive dependencies.
|
|
4
4
|
|
|
5
|
-
> **Upgrading from
|
|
5
|
+
> **Upgrading from v2.x?** See the [v2 → v3 migration guide](https://ersbeth-web.gitlab.io/picoflow/guide/advanced/migration-v3.html).
|
|
6
|
+
> Coming from v1.x, take the [v1 → v2 guide](https://ersbeth-web.gitlab.io/picoflow/guide/advanced/migration-v2.html) first.
|
|
6
7
|
|
|
7
8
|
```typescript
|
|
8
9
|
import { derivation, state, subscribe } from "@ersbeth/picoflow";
|
|
@@ -16,10 +17,14 @@ const effect = subscribe(
|
|
|
16
17
|
);
|
|
17
18
|
// logs "0 is even"
|
|
18
19
|
|
|
19
|
-
$count.set(1); // logs "1 is odd"
|
|
20
|
+
$count.set(1); // logs "1 is odd", on the next task
|
|
20
21
|
effect.dispose(); // stop reacting
|
|
21
22
|
```
|
|
22
23
|
|
|
24
|
+
Reactivity is flushed asynchronously: a write schedules a flush rather than running effects on the
|
|
25
|
+
spot, so writes made in one turn are batched and an assertion placed directly after `.set()` sees the
|
|
26
|
+
old value. Await the value (`await $count.pick()`) when you need the graph settled.
|
|
27
|
+
|
|
23
28
|
Dependencies are tracked explicitly: `$count.get(t)` both reads the value and registers it as a
|
|
24
29
|
dependency of the enclosing `derivation`/`subscribe`, using the `t` (tracker) each one receives. Nothing
|
|
25
30
|
is tracked implicitly and there is no `untrack()` to opt back out of — if a read doesn't go through
|
|
@@ -30,6 +35,11 @@ its main safety property: what an effect depends on is always visible at the cal
|
|
|
30
35
|
|
|
31
36
|
The schedulers use `Promise.withResolvers()`, so picoflow needs **Node ≥ 22**, or Chrome ≥ 119, Safari ≥ 17.4, Firefox ≥ 121 in the browser. There is no fallback: on an older runtime the failure is immediate, on the first primitive you create.
|
|
32
37
|
|
|
38
|
+
On the type side, the published declarations reference TypeScript's `esnext.disposable` lib, which is
|
|
39
|
+
what types the `using` support: they need **TypeScript ≥ 5.5**, where the reference is honoured whatever
|
|
40
|
+
your own `lib` setting is. On TypeScript 5.2 to 5.4 the lib exists but the reference is ignored, so add
|
|
41
|
+
`esnext.disposable` to your `lib` if you pin it and keep `skipLibCheck: false`.
|
|
42
|
+
|
|
33
43
|
## Installation
|
|
34
44
|
|
|
35
45
|
```bash
|
|
@@ -43,20 +53,35 @@ pnpm add @ersbeth/picoflow
|
|
|
43
53
|
yarn add @ersbeth/picoflow
|
|
44
54
|
```
|
|
45
55
|
|
|
46
|
-
##
|
|
56
|
+
## Entry points
|
|
47
57
|
|
|
48
|
-
|
|
58
|
+
| Import | What it gives you | Needs |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `@ersbeth/picoflow` | the reactive core — every primitive, and the whole everyday API | nothing |
|
|
61
|
+
| `@ersbeth/picoflow/solid` | `from()`, adapting a picoflow value into a SolidJS `Resource` | the optional `solid-js` peer |
|
|
62
|
+
| `@ersbeth/picoflow/inspect` | `inspect()`, `graphSnapshot()`, `formatGraph()`, `enableTrace()` — reading the graph to answer "why did this not update?" | nothing |
|
|
49
63
|
|
|
50
|
-
|
|
64
|
+
The root never resolves `solid-js`, so an application that does not use the converter installs
|
|
65
|
+
nothing but picoflow. The inspector is a separate subpath so that an application that never inspects
|
|
66
|
+
anything never bundles it.
|
|
51
67
|
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
pnpm test:browser # watch browser tests
|
|
68
|
+
```typescript
|
|
69
|
+
import { formatGraph } from "@ersbeth/picoflow/inspect";
|
|
70
|
+
|
|
71
|
+
console.log(formatGraph()); // every live node, its status and its edges
|
|
57
72
|
```
|
|
58
73
|
|
|
59
|
-
|
|
74
|
+
## Documentation
|
|
75
|
+
|
|
76
|
+
For comprehensive guides and API documentation, visit the [official website](https://ersbeth-web.gitlab.io/picoflow/)
|
|
77
|
+
|
|
78
|
+
## Project
|
|
79
|
+
|
|
80
|
+
Picoflow is developed at [gitlab.com/ersbeth-web/picoflow](https://gitlab.com/ersbeth-web/picoflow).
|
|
81
|
+
Known issues and their status are tracked as
|
|
82
|
+
[GitLab issues](https://gitlab.com/ersbeth-web/picoflow/-/issues), and vulnerability reporting is
|
|
83
|
+
covered by the repository's `SECURITY.md`. Contributions are welcome — its `CONTRIBUTING.md` has the
|
|
84
|
+
setup and conventions.
|
|
60
85
|
|
|
61
86
|
## AI Coding Agents
|
|
62
87
|
|
|
@@ -70,4 +95,4 @@ npx skills add ./node_modules/@ersbeth/picoflow
|
|
|
70
95
|
|
|
71
96
|
## License
|
|
72
97
|
|
|
73
|
-
This project is licensed under the [MIT License](LICENSE).
|
|
98
|
+
This project is licensed under the [MIT License](LICENSE).
|
package/SKILL.md
CHANGED
|
@@ -14,10 +14,18 @@ imports from `@ersbeth/picoflow`.
|
|
|
14
14
|
Every primitive is read one of two ways:
|
|
15
15
|
|
|
16
16
|
- **`.get(t)`** — tracked read. `t` is a `FlowTracker`, only available inside a `derivation()` callback
|
|
17
|
-
or a `subscribe()` data function. Registers the caller as a dependent.
|
|
17
|
+
or a `subscribe()` data function. Registers the caller as a dependent. The type is opaque: a tracker
|
|
18
|
+
is received, never constructed, so hand-rolling one to read synchronously is a compile error — use
|
|
19
|
+
`.value` or `.latest` below for that.
|
|
18
20
|
- **`.pick()`** — untracked async read (`Promise<T>`). Use outside reactive contexts (event handlers,
|
|
19
21
|
one-off reads) when you don't want to react to future changes.
|
|
20
22
|
|
|
23
|
+
Two synchronous getters exist for *looking* without taking part: **`.value`** returns a snapshot
|
|
24
|
+
`{ status, data? }` — `data` is present only when `status === "resolved"`, so narrow on it — and
|
|
25
|
+
**`.latest`** returns the last resolved value (`T | undefined`) even while pending or dirty. Neither
|
|
26
|
+
computes a lazy derivation, throws, or tracks; they are for debuggers, dev panels and quick checks,
|
|
27
|
+
not a replacement for `.pick()` when you need the value to be fresh.
|
|
28
|
+
|
|
21
29
|
Never call `.get(t)` outside a derivation/subscribe callback — `t` isn't available there. Never use
|
|
22
30
|
`.pick()` inside a derivation/subscribe when you actually want reactivity — it silently creates no
|
|
23
31
|
dependency, which is a common source of "why doesn't this update" bugs.
|
|
@@ -27,8 +35,11 @@ new code in a PicoFlow codebase, and use it to tell reactive values apart from p
|
|
|
27
35
|
Every factory also takes an optional trailing `{ name?: string }`, e.g. `state(0, { name: "$count" })` —
|
|
28
36
|
pass the same `$`-prefixed name so it shows up in error messages (`[PicoFlow] Primitive "$count" is
|
|
29
37
|
disposed`) instead of the unnamed generic message. Errors are also typed (`PicoFlowDisposedError`,
|
|
30
|
-
`PicoFlowCycleError`, `PicoFlowInternalError`,
|
|
31
|
-
|
|
38
|
+
`PicoFlowCycleError`, `PicoFlowInternalError`, `PicoFlowPreconditionError` — the collection
|
|
39
|
+
preconditions and `from()`'s input check — all extending `PicoFlowError`, which is never thrown
|
|
40
|
+
itself), so `catch` blocks can discriminate with `instanceof` instead of matching on message text. A computation that throws a
|
|
41
|
+
non-`Error` value has it wrapped once (`new Error(String(thrown), { cause: thrown })`), so `pick()`,
|
|
42
|
+
`get(t)`, `.value.error` and `onError` all hand you the same `Error` instance.
|
|
32
43
|
|
|
33
44
|
## Choosing a primitive
|
|
34
45
|
|
|
@@ -39,14 +50,19 @@ discriminate with `instanceof` instead of matching on message text.
|
|
|
39
50
|
| Expensive one-time computation, never changes | `constant(() => ...)` — **always a function**, computed lazily on first access | a plain top-level `const` if nothing reactive reads it |
|
|
40
51
|
| Pure computed value from other primitives | `derivation((t) => ...)` | putting side effects in it — use `subscribe` |
|
|
41
52
|
| A derivation that occasionally needs manual override | `writableDerivation((t) => ...)` | — |
|
|
53
|
+
| A derivation that must recompute without waiting for a reader (pay for an expensive compute during the flush, keep a cache warm, observe `status` with no value reader) | `derivation((t) => ..., { eager: true })` — derivations are lazy by default; the option also exists on the writable/async variants | an `effect`/`subscribe` reading it just to force the computation |
|
|
42
54
|
| List with fine-grained mutation tracking | `array(initial?)` | `state(someArray)` if you need per-operation tracking |
|
|
43
55
|
| Key-value store with fine-grained mutation tracking | `map(initial?)` | `state(someMap)` if you need per-operation tracking |
|
|
44
56
|
| Running side effects (DOM, network, storage, logging) in reaction to changes | `.subscribe()` (single primitive) or `subscribe()` (multiple) | `derivation` — derivations must stay pure |
|
|
57
|
+
| An external resource re-created whenever a value changes (socket, timer, listener) | `derivation<R>((t, previous) => ...)` — read the dependencies, release `previous`, return the next one; after `dispose()`, release `$r.latest` by hand | `subscribe()` holding the resource in a closure — there is no per-run `onCleanup()`, by design |
|
|
45
58
|
|
|
46
59
|
Async variants exist for state/constant/derivation/writableDerivation (`stateAsync`, `constantAsync`,
|
|
47
60
|
`derivationAsync`, `writableDerivationAsync`) — same rules, the held value is a `Promise<T>`, and reads
|
|
48
61
|
of a still-pending value are handled automatically (surfaced via `subscribe()`'s optional `onPending`
|
|
49
|
-
callback) rather than needing manual `await`/try-catch inside a derivation.
|
|
62
|
+
callback) rather than needing manual `await`/try-catch inside a derivation. Handing a promise to a sync
|
|
63
|
+
primitive is a compile error whose message names the async variant to use (`NotPromise<T>` resolves to
|
|
64
|
+
the `PromiseNotAllowed` literal); the check is one level deep — a promise nested inside an object is not
|
|
65
|
+
detected, so reach for the async variant deliberately.
|
|
50
66
|
|
|
51
67
|
## Mistakes to catch when reviewing PicoFlow code
|
|
52
68
|
|
|
@@ -54,8 +70,15 @@ callback) rather than needing manual `await`/try-catch inside a derivation.
|
|
|
54
70
|
be pure; move the side effect into a `subscribe()`.
|
|
55
71
|
- **Mutating a collection returned by `.get(t)`/`.pick()` in place**, e.g. `items.push(x)` on a picked
|
|
56
72
|
array, or `user.name = 'x'` on a picked object, then relying on it to update — this changes nothing
|
|
57
|
-
observable.
|
|
58
|
-
|
|
73
|
+
observable. `array()`/`map()` reads are the live store typed `readonly T[]`/`ReadonlyMap<K, V>`, so
|
|
74
|
+
the compiler catches the first case. Always produce a new array/object (`items.map(...)`,
|
|
75
|
+
`{ ...user, name: 'x' }`), or use the primitive's own mutation methods (`$array.push(x)`,
|
|
76
|
+
`$map.setAt(k, v)`). An `equals` comparator does **not** fix this: both sides are the same reference, so it still reports "unchanged".
|
|
77
|
+
- **A state that legitimately holds `NaN`, left on the default comparator** — change detection is `===`,
|
|
78
|
+
and `NaN !== NaN`, so every write re-runs the whole downstream subtree. Pass
|
|
79
|
+
`state(value, { equals: Object.is })`. The same option takes a structural comparator
|
|
80
|
+
(`{ equals: (a, b) => a.id === b.id }`) when writes routinely produce a *fresh* object with equal
|
|
81
|
+
fields. Value primitives only — `array()`, `map()` and `signal()` ignore `equals`.
|
|
59
82
|
- **`constant(value)` with a direct value instead of a function** — constants always take an initializer
|
|
60
83
|
function (`constant(() => value)`), enforcing lazy evaluation. A direct value is a v1 pattern (see below).
|
|
61
84
|
- **Not disposing subscriptions/primitives** (`.dispose()`) when they're no longer needed — every
|
|
@@ -70,23 +93,53 @@ callback) rather than needing manual `await`/try-catch inside a derivation.
|
|
|
70
93
|
- **`subscribe(fn)` with no `onError`, and no global `onFlushError()` handler installed** — any exception
|
|
71
94
|
thrown inside `fn` is only logged via `console.error` by default; install `onFlushError((error) => ...)`
|
|
72
95
|
once at startup if the application needs to observe or report these instead of the default logging.
|
|
73
|
-
-
|
|
74
|
-
|
|
75
|
-
|
|
96
|
+
- **`$x.set(fn)` expecting an updater, or `$x.update(fn)` on an errored state** — `set()` only replaces
|
|
97
|
+
(a function passed to it is stored as the value); `update(fn)` derives from the stabilised current
|
|
98
|
+
value and is skipped, leaving the error in place, when the state holds an error. Recover with `set()`
|
|
99
|
+
first. Collections have no `update(fn)` at all: use their mutation methods, or `set(items)`.
|
|
100
|
+
- **Using `add()`/`setAt()`/`splice()` on `array`/`map` without checking preconditions** —
|
|
101
|
+
`array.setAt(index, x)` throws if the index is out of bounds; `map.add(key, x)` throws if the key
|
|
102
|
+
exists; `map.setAt(key, x)` throws if it doesn't; `array.splice(start, deleteCount, …)` throws unless
|
|
103
|
+
both bounds are integers addressing the array as it stands — it does not clamp them the way
|
|
104
|
+
`Array.prototype.splice` does, so `splice(-1, 1)` and `splice(i, Infinity)` are errors, not the
|
|
105
|
+
from-the-end and to-the-end forms. Check first (`.pick()`) or branch on it, don't assume. Writes
|
|
106
|
+
(`set`, `setAt`, `push`, `unshift`, `add`) return nothing; removals (`pop`, `shift`, `splice`,
|
|
107
|
+
`clear`, `delete`) return what they removed. The item a `setAt()` replaced is in `$lastAction`
|
|
108
|
+
(`clearedItem` / `clearedValue`), not in its return value.
|
|
76
109
|
- **Tracking the whole `array`/`map` (`.subscribe()`/`.get(t)`) when only specific operations matter** —
|
|
77
110
|
prefer `$collection.$lastAction.subscribe(...)` (a discriminated union with a `type` field) to react
|
|
78
111
|
only to the mutation kind you care about, instead of re-running on every change.
|
|
79
112
|
|
|
80
|
-
## Recognizing
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
113
|
+
## Recognizing outdated API usage
|
|
114
|
+
|
|
115
|
+
### Predates v3 (full guide: https://ersbeth-web.gitlab.io/picoflow/guide/advanced/migration-v3.html)
|
|
116
|
+
|
|
117
|
+
- `$x.set((prev) => ...)` — the updater form is now `$x.update((prev) => ...)`. **Flag this first.** On a
|
|
118
|
+
typed primitive the compiler catches it, but on a `state<unknown>`, a state that legitimately holds a
|
|
119
|
+
function, or in plain JavaScript, `set()` stores the function *as the value* and nothing complains.
|
|
120
|
+
- `$array.update(index, item)` / `$map.update(key, value)` — renamed `setAt(...)`, and the matching
|
|
121
|
+
`$lastAction` type `"update"` is now `"setAt"`, so a `case "update":` branch is dead code.
|
|
122
|
+
- Binding the result of `setAt()` — writes return nothing now; only removals return what they removed.
|
|
123
|
+
The displaced entry is on `$lastAction` as `clearedItem` / `clearedValue`.
|
|
124
|
+
- `$x.disposed` — removed. Read `$x.status === "disposed"`, and drop the guard entirely where it only
|
|
125
|
+
protected a `dispose()` call, which is idempotent.
|
|
126
|
+
- `$array.length` — removed. `$items.get(t).length` to depend on it, `$items.latest?.length` to read it
|
|
127
|
+
passively.
|
|
128
|
+
- `import { from } from '@ersbeth/picoflow'` — `from()` lives at `@ersbeth/picoflow/solid`.
|
|
129
|
+
- `set`, `update` or `refresh` reached on a `constant()` or `derivation()` through a cast — they no
|
|
130
|
+
longer exist at runtime, so the call yields `undefined` rather than working.
|
|
131
|
+
- Mutating the array or map handed back by a read — reads now return the live backing store, typed
|
|
132
|
+
read-only, so this changes the collection with no notification. Copy it (`[...$items.latest ?? []]`).
|
|
133
|
+
|
|
134
|
+
### Predates v2 (full guide: https://ersbeth-web.gitlab.io/picoflow/guide/advanced/migration-v2.html)
|
|
84
135
|
|
|
85
136
|
- `effect((t) => ...)` — removed. Replace with `.subscribe()` (single primitive) or `subscribe()` (multiple).
|
|
86
|
-
- `$map.$lastAdded` / `$lastUpdated` / `$lastDeleted` — replaced by a single `$map.$lastAction`
|
|
137
|
+
- `$map.$lastAdded` / `$lastUpdated` / `$lastDeleted` — replaced by a single `$map.$lastAction` stream
|
|
87
138
|
with a `type` discriminant.
|
|
88
|
-
- `$array.setItem(index, value)` — renamed
|
|
139
|
+
- `$array.setItem(index, value)` — renamed `$array.update(index, value)` in v2, and again to
|
|
140
|
+
`$array.setAt(index, value)` in v3.
|
|
89
141
|
- `constant(value)` with a direct (non-function) value.
|
|
142
|
+
- `TrackingContext` — the exported type is `FlowTracker`.
|
|
90
143
|
|
|
91
144
|
## Minimal example (the shape most PicoFlow code follows)
|
|
92
145
|
|
|
@@ -101,13 +154,74 @@ const effect = subscribe(
|
|
|
101
154
|
(data) => console.log(`Count is ${data.count}, which is ${data.even ? 'even' : 'odd'}`),
|
|
102
155
|
)
|
|
103
156
|
|
|
104
|
-
$count.
|
|
157
|
+
$count.update((n) => n + 1) // derive from the current value; set(v) replaces without looking at it
|
|
105
158
|
|
|
106
159
|
// when done:
|
|
107
160
|
effect.dispose()
|
|
108
161
|
$count.dispose()
|
|
109
162
|
```
|
|
110
163
|
|
|
164
|
+
## Testing code that uses PicoFlow
|
|
165
|
+
|
|
166
|
+
Reactivity is flushed asynchronously, so a synchronous assertion right after `.set()` always fails.
|
|
167
|
+
Await the value (`await $count.pick()`) or poll (`await vi.waitFor(() => ...)`) before asserting.
|
|
168
|
+
|
|
169
|
+
Scheduling is process-global: work queued by one test can still be waiting when the next one starts,
|
|
170
|
+
and an `onFlushError()` handler stays installed until replaced. Call `reset()` from an `afterEach` to
|
|
171
|
+
put the scheduler back to its just-started state — both queues emptied, any scheduled flush cancelled,
|
|
172
|
+
the error handler back to `console.error`.
|
|
173
|
+
|
|
174
|
+
`dispose()` is idempotent, so cleanup never needs a guard — and to ask whether something was released,
|
|
175
|
+
read `$x.status` and compare against `"disposed"` (values report `resolved`/`pending`/`error`/`dirty`
|
|
176
|
+
otherwise; effects report `idle`/`queued`). For a primitive whose
|
|
177
|
+
lifetime is one test, `using $count = state(0)` disposes it on block exit even if an assertion throws
|
|
178
|
+
first — which a trailing `dispose()` would skip. (The `using` syntax needs Node 24 or a transpiler;
|
|
179
|
+
most test runners transpile already.)
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
import { reset } from '@ersbeth/picoflow'
|
|
183
|
+
|
|
184
|
+
afterEach(() => {
|
|
185
|
+
reset()
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`reset()` **discards** queued work rather than running it: primitives created earlier stay usable, but
|
|
190
|
+
any recomputation that was waiting is lost, and those nodes only recompute if something notifies them
|
|
191
|
+
again. It cancels pending reactivity — it is not a way to force it to complete. Use `pick()` for that.
|
|
192
|
+
|
|
193
|
+
## Debugging: inspecting the graph
|
|
194
|
+
|
|
195
|
+
When something "doesn't update", read the graph rather than guessing. The inspector lives behind its
|
|
196
|
+
own subpath, `@ersbeth/picoflow/inspect`, and is passive by construction — it reads `status` and the
|
|
197
|
+
same snapshot `.value` exposes and walks the dependency registries, never `get()`/`pick()`/`subscribe()`,
|
|
198
|
+
so looking at a graph cannot change what it then does. It works on disposed primitives too.
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
import { formatGraph, graphSnapshot, inspect } from '@ersbeth/picoflow/inspect'
|
|
202
|
+
|
|
203
|
+
console.log(formatGraph()) // the whole graph as text — for a browser console
|
|
204
|
+
inspect($count) // one node: kind, name, status, value, dependencies, dependents
|
|
205
|
+
graphSnapshot() // every node as JSON: same data, for a tool or an agent
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
- **"My effect never ran."** `inspect(effect).dependencies` empty where you expected a source is the
|
|
209
|
+
`pick()`-instead-of-`get(t)` mistake, seen directly: the edge was never created.
|
|
210
|
+
- **"Something is leaking."** `graphSnapshot().summary` counts `orphans` — nodes with no edge in
|
|
211
|
+
either direction. A count that climbs as the application runs is undisposed nodes piling up.
|
|
212
|
+
- **"In what order did this happen?"** `enableTrace()` records the engine's own steps (notify, queue,
|
|
213
|
+
execute, resolve, effect run) into a bounded buffer; `trace.events` reads them back, and a feedback
|
|
214
|
+
loop ends in a `cycle-error` preceded by the effects that caused it. Call `trace.dispose()` when done.
|
|
215
|
+
|
|
216
|
+
Two things to expect rather than report as bugs: a **lazy derivation never read has computed nothing**,
|
|
217
|
+
so it shows `dirty`, no value and no dependencies (create it with `{ eager: true }`, or exercise the
|
|
218
|
+
app first); and a node's `value` in a dump is a short **rendered string**, present only when the status
|
|
219
|
+
is `resolved` — introspection describes the graph, it does not hand out its data.
|
|
220
|
+
|
|
221
|
+
Name the nodes you expect to debug. Every factory takes `{ name }`, and as of v3 so does
|
|
222
|
+
`subscribe(data, onData, onError?, onPending?, { name })` — an unnamed node is only ever labelled
|
|
223
|
+
`derivation#12`.
|
|
224
|
+
|
|
111
225
|
## Further reference
|
|
112
226
|
|
|
113
227
|
This file covers the rules needed to write and review correct PicoFlow code. For the full generated API
|