@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.
Files changed (70) hide show
  1. package/README.md +37 -12
  2. package/SKILL.md +130 -16
  3. package/dist/index.d.ts +767 -0
  4. package/dist/index.js +544 -0
  5. package/dist/inspect.d.ts +252 -0
  6. package/dist/inspect.js +300 -0
  7. package/dist/shared.d.ts +361 -0
  8. package/dist/shared.js +1496 -0
  9. package/dist/shared2.d.ts +93 -0
  10. package/dist/shared2.js +59 -0
  11. package/dist/shared3.d.ts +54 -0
  12. package/dist/shared3.js +17 -0
  13. package/dist/{types/converters/solid.d.ts → solid.d.ts} +10 -8
  14. package/dist/solid.js +63 -0
  15. package/package.json +38 -34
  16. package/dist/picoflow.js +0 -1283
  17. package/dist/types/api/base/flowConfig.d.ts +0 -17
  18. package/dist/types/api/base/flowDisposable.d.ts +0 -40
  19. package/dist/types/api/base/flowErrors.d.ts +0 -1
  20. package/dist/types/api/base/flowObservable.d.ts +0 -26
  21. package/dist/types/api/base/flowPrimitiveOptions.d.ts +0 -14
  22. package/dist/types/api/base/flowSubscribable.d.ts +0 -78
  23. package/dist/types/api/base/flowTracker.d.ts +0 -7
  24. package/dist/types/api/base/index.d.ts +0 -7
  25. package/dist/types/api/index.d.ts +0 -2
  26. package/dist/types/api/nodes/async/flowConstantAsync.d.ts +0 -32
  27. package/dist/types/api/nodes/async/flowDerivationAsync.d.ts +0 -37
  28. package/dist/types/api/nodes/async/flowStateAsync.d.ts +0 -42
  29. package/dist/types/api/nodes/async/flowWritableDerivationAsync.d.ts +0 -31
  30. package/dist/types/api/nodes/async/index.d.ts +0 -4
  31. package/dist/types/api/nodes/collections/flowArray.d.ts +0 -138
  32. package/dist/types/api/nodes/collections/flowMap.d.ts +0 -100
  33. package/dist/types/api/nodes/collections/index.d.ts +0 -2
  34. package/dist/types/api/nodes/flowEffect.d.ts +0 -28
  35. package/dist/types/api/nodes/flowSignal.d.ts +0 -26
  36. package/dist/types/api/nodes/flowValue.d.ts +0 -35
  37. package/dist/types/api/nodes/index.d.ts +0 -7
  38. package/dist/types/api/nodes/sync/flowConstant.d.ts +0 -30
  39. package/dist/types/api/nodes/sync/flowDerivation.d.ts +0 -37
  40. package/dist/types/api/nodes/sync/flowState.d.ts +0 -40
  41. package/dist/types/api/nodes/sync/flowWritableDerivation.d.ts +0 -29
  42. package/dist/types/api/nodes/sync/index.d.ts +0 -4
  43. package/dist/types/api/nodes/utils.d.ts +0 -22
  44. package/dist/types/base/dependenciesRegistry.d.ts +0 -1
  45. package/dist/types/base/dependentsRegistry.d.ts +0 -1
  46. package/dist/types/base/disposable.d.ts +0 -15
  47. package/dist/types/base/errors.d.ts +0 -31
  48. package/dist/types/base/executionStack.d.ts +0 -31
  49. package/dist/types/base/index.d.ts +0 -8
  50. package/dist/types/base/node.d.ts +0 -27
  51. package/dist/types/base/observable.d.ts +0 -34
  52. package/dist/types/base/observer.d.ts +0 -33
  53. package/dist/types/converters/index.d.ts +0 -1
  54. package/dist/types/index.d.ts +0 -2
  55. package/dist/types/nodes/actionNode.d.ts +0 -1
  56. package/dist/types/nodes/arrayNode.d.ts +0 -1
  57. package/dist/types/nodes/effectNode.d.ts +0 -1
  58. package/dist/types/nodes/index.d.ts +0 -8
  59. package/dist/types/nodes/mapNode.d.ts +0 -1
  60. package/dist/types/nodes/signalNode.d.ts +0 -1
  61. package/dist/types/nodes/valueAsyncNode.d.ts +0 -1
  62. package/dist/types/nodes/valueNode.d.ts +0 -1
  63. package/dist/types/nodes/valueSyncNode.d.ts +0 -1
  64. package/dist/types/schedulers/asyncResolver.d.ts +0 -1
  65. package/dist/types/schedulers/asyncScheduler.d.ts +0 -1
  66. package/dist/types/schedulers/index.d.ts +0 -4
  67. package/dist/types/schedulers/pendingError.d.ts +0 -1
  68. package/dist/types/schedulers/scheduler.d.ts +0 -1
  69. package/dist/types/schedulers/syncResolver.d.ts +0 -1
  70. 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 v1.x?** See the [Upgrade Guide](https://ersbeth-web.gitlab.io/picoflow/guide/advanced/migration-v2.html) for migration instructions.
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
- ## Documentation
56
+ ## Entry points
47
57
 
48
- For comprehensive guides and API documentation, visit the [official website](https://ersbeth-web.gitlab.io/picoflow/)
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
- ## Development
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
- ```bash
53
- pnpm install
54
- pnpm playwright:install # once — downloads Chromium for the browser tests
55
- pnpm test # watch Node tests
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
- See [`CONTRIBUTING.md`](../../CONTRIBUTING.md) for the full setup, the conventions and the release procedure. Known issues and their status are tracked in [`audits/FINDINGS.md`](../../audits/FINDINGS.md); planned breaking changes in [`ROADMAP.md`](../../ROADMAP.md); vulnerability reporting in [`SECURITY.md`](../../SECURITY.md).
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`, all extending `PicoFlowError`), so `catch` blocks can
31
- discriminate with `instanceof` instead of matching on message text.
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. Always produce a new array/object (`items.map(...)`, `{ ...user, name: 'x' }`), or use the
58
- primitive's own mutation methods (`$array.push(x)`, `$map.update(k, v)`).
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
- - **Using `add()`/`update()` on `array`/`map` without checking preconditions** — `array.update(index, x)`
74
- throws if the index is out of bounds; `map.add(key, x)` throws if the key exists; `map.update(key, x)`
75
- throws if it doesn't. Check first (`.pick()`) or branch on it, don't assume.
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 pre-v2 (deprecated) API usage
81
-
82
- If existing code uses any of these, it predates PicoFlow v2 — flag it and suggest migrating (full guide:
83
- https://ersbeth-web.gitlab.io/picoflow/guide/advanced/migration-v2.html):
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` signal
137
+ - `$map.$lastAdded` / `$lastUpdated` / `$lastDeleted` — replaced by a single `$map.$lastAction` stream
87
138
  with a `type` discriminant.
88
- - `$array.setItem(index, value)` — renamed to `$array.update(index, value)`.
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.set((n) => n + 1) // atomic updater form, prefer it when the new value depends on the current one
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