@ersbeth/picoflow 2.0.2 → 2.1.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/LICENSE +21 -0
- package/README.md +25 -0
- package/SKILL.md +106 -0
- package/dist/picoflow.js +204 -60
- package/dist/types/api/base/flowObservable.d.ts +3 -3
- package/dist/types/api/base/flowObservable.d.ts.map +1 -1
- package/dist/types/api/base/flowSubscribable.d.ts +2 -2
- package/dist/types/api/base/flowSubscribable.d.ts.map +1 -1
- package/dist/types/api/base/flowTracker.d.ts +1 -1
- package/dist/types/api/base/flowTracker.d.ts.map +1 -1
- package/dist/types/api/base/index.d.ts +4 -4
- package/dist/types/api/base/index.d.ts.map +1 -1
- package/dist/types/api/index.d.ts +2 -2
- package/dist/types/api/index.d.ts.map +1 -1
- package/dist/types/api/nodes/async/flowConstantAsync.d.ts +1 -1
- package/dist/types/api/nodes/async/flowConstantAsync.d.ts.map +1 -1
- package/dist/types/api/nodes/async/flowDerivationAsync.d.ts +3 -3
- package/dist/types/api/nodes/async/flowDerivationAsync.d.ts.map +1 -1
- package/dist/types/api/nodes/async/flowStateAsync.d.ts +3 -3
- package/dist/types/api/nodes/async/flowStateAsync.d.ts.map +1 -1
- package/dist/types/api/nodes/async/flowWritableDerivationAsync.d.ts +2 -2
- package/dist/types/api/nodes/async/flowWritableDerivationAsync.d.ts.map +1 -1
- package/dist/types/api/nodes/async/index.d.ts +4 -4
- package/dist/types/api/nodes/async/index.d.ts.map +1 -1
- package/dist/types/api/nodes/collections/flowArray.d.ts +7 -4
- package/dist/types/api/nodes/collections/flowArray.d.ts.map +1 -1
- package/dist/types/api/nodes/collections/flowMap.d.ts +3 -2
- package/dist/types/api/nodes/collections/flowMap.d.ts.map +1 -1
- package/dist/types/api/nodes/collections/index.d.ts +2 -2
- package/dist/types/api/nodes/collections/index.d.ts.map +1 -1
- package/dist/types/api/nodes/flowEffect.d.ts +2 -2
- package/dist/types/api/nodes/flowEffect.d.ts.map +1 -1
- package/dist/types/api/nodes/flowSignal.d.ts +1 -1
- package/dist/types/api/nodes/flowSignal.d.ts.map +1 -1
- package/dist/types/api/nodes/flowValue.d.ts +2 -2
- package/dist/types/api/nodes/flowValue.d.ts.map +1 -1
- package/dist/types/api/nodes/index.d.ts +7 -7
- package/dist/types/api/nodes/index.d.ts.map +1 -1
- package/dist/types/api/nodes/sync/flowConstant.d.ts +2 -2
- package/dist/types/api/nodes/sync/flowConstant.d.ts.map +1 -1
- package/dist/types/api/nodes/sync/flowDerivation.d.ts +3 -3
- package/dist/types/api/nodes/sync/flowDerivation.d.ts.map +1 -1
- package/dist/types/api/nodes/sync/flowState.d.ts +3 -3
- package/dist/types/api/nodes/sync/flowState.d.ts.map +1 -1
- package/dist/types/api/nodes/sync/flowWritableDerivation.d.ts +2 -2
- package/dist/types/api/nodes/sync/flowWritableDerivation.d.ts.map +1 -1
- package/dist/types/api/nodes/sync/index.d.ts +4 -4
- package/dist/types/api/nodes/sync/index.d.ts.map +1 -1
- package/dist/types/api/nodes/utils.d.ts +1 -1
- package/dist/types/api/nodes/utils.d.ts.map +1 -1
- package/dist/types/base/disposable.d.ts +1 -1
- package/dist/types/base/disposable.d.ts.map +1 -1
- package/dist/types/base/executionStack.d.ts +22 -1
- package/dist/types/base/executionStack.d.ts.map +1 -1
- package/dist/types/base/index.d.ts +5 -5
- package/dist/types/base/index.d.ts.map +1 -1
- package/dist/types/base/node.d.ts +4 -4
- package/dist/types/base/node.d.ts.map +1 -1
- package/dist/types/base/observable.d.ts +3 -3
- package/dist/types/base/observable.d.ts.map +1 -1
- package/dist/types/base/observer.d.ts +2 -2
- package/dist/types/base/observer.d.ts.map +1 -1
- package/dist/types/converters/index.d.ts +1 -1
- package/dist/types/converters/index.d.ts.map +1 -1
- package/dist/types/converters/solid.d.ts +12 -8
- package/dist/types/converters/solid.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/nodes/actionNode.d.ts +23 -0
- package/dist/types/nodes/actionNode.d.ts.map +1 -0
- package/dist/types/nodes/arrayNode.d.ts +4 -3
- package/dist/types/nodes/arrayNode.d.ts.map +1 -1
- package/dist/types/nodes/effectNode.d.ts +18 -2
- package/dist/types/nodes/effectNode.d.ts.map +1 -1
- package/dist/types/nodes/index.d.ts +8 -7
- package/dist/types/nodes/index.d.ts.map +1 -1
- package/dist/types/nodes/mapNode.d.ts +4 -3
- package/dist/types/nodes/mapNode.d.ts.map +1 -1
- package/dist/types/nodes/signalNode.d.ts +2 -2
- package/dist/types/nodes/signalNode.d.ts.map +1 -1
- package/dist/types/nodes/valueAsyncNode.d.ts +3 -3
- package/dist/types/nodes/valueAsyncNode.d.ts.map +1 -1
- package/dist/types/nodes/valueNode.d.ts +3 -3
- package/dist/types/nodes/valueNode.d.ts.map +1 -1
- package/dist/types/nodes/valueSyncNode.d.ts +3 -3
- package/dist/types/nodes/valueSyncNode.d.ts.map +1 -1
- package/dist/types/schedulers/asyncScheduler.d.ts +2 -1
- package/dist/types/schedulers/asyncScheduler.d.ts.map +1 -1
- package/dist/types/schedulers/index.d.ts +4 -4
- package/dist/types/schedulers/index.d.ts.map +1 -1
- package/dist/types/schedulers/syncResolver.d.ts +1 -2
- package/dist/types/schedulers/syncResolver.d.ts.map +1 -1
- package/dist/types/schedulers/syncScheduler.d.ts +1 -1
- package/dist/types/schedulers/syncScheduler.d.ts.map +1 -1
- package/package.json +24 -1
- package/.gitlab-ci.yml +0 -24
- package/.vscode/settings.json +0 -5
- package/CHANGELOG.md +0 -94
- package/biome.json +0 -47
- package/docs/.vitepress/config.mts +0 -145
- package/docs/api/functions/array.md +0 -35
- package/docs/api/functions/constant.md +0 -33
- package/docs/api/functions/constantAsync.md +0 -69
- package/docs/api/functions/derivation.md +0 -34
- package/docs/api/functions/derivationAsync.md +0 -34
- package/docs/api/functions/from.md +0 -129
- package/docs/api/functions/isDisposable.md +0 -27
- package/docs/api/functions/map.md +0 -36
- package/docs/api/functions/signal.md +0 -21
- package/docs/api/functions/state.md +0 -67
- package/docs/api/functions/stateAsync.md +0 -69
- package/docs/api/functions/subscribe.md +0 -40
- package/docs/api/functions/writableDerivation.md +0 -33
- package/docs/api/functions/writableDerivationAsync.md +0 -34
- package/docs/api/index.md +0 -61
- package/docs/api/interfaces/FlowArray.md +0 -439
- package/docs/api/interfaces/FlowConstant.md +0 -220
- package/docs/api/interfaces/FlowConstantAsync.md +0 -221
- package/docs/api/interfaces/FlowDerivation.md +0 -241
- package/docs/api/interfaces/FlowDerivationAsync.md +0 -242
- package/docs/api/interfaces/FlowDisposable.md +0 -59
- package/docs/api/interfaces/FlowEffect.md +0 -64
- package/docs/api/interfaces/FlowMap.md +0 -374
- package/docs/api/interfaces/FlowObservable.md +0 -155
- package/docs/api/interfaces/FlowSignal.md +0 -156
- package/docs/api/interfaces/FlowState.md +0 -269
- package/docs/api/interfaces/FlowStateAsync.md +0 -268
- package/docs/api/interfaces/FlowSubscribable.md +0 -55
- package/docs/api/interfaces/FlowTracker.md +0 -61
- package/docs/api/interfaces/FlowValue.md +0 -222
- package/docs/api/interfaces/FlowWritableDerivation.md +0 -292
- package/docs/api/interfaces/FlowWritableDerivationAsync.md +0 -293
- package/docs/api/type-aliases/DerivationFunction.md +0 -28
- package/docs/api/type-aliases/DerivationFunctionAsync.md +0 -28
- package/docs/api/type-aliases/FlowArrayAction.md +0 -60
- package/docs/api/type-aliases/FlowDataTracker.md +0 -33
- package/docs/api/type-aliases/FlowMapAction.md +0 -48
- package/docs/api/type-aliases/FlowOnDataListener.md +0 -33
- package/docs/api/type-aliases/FlowOnErrorListener.md +0 -27
- package/docs/api/type-aliases/FlowOnPendingListener.md +0 -21
- package/docs/api/type-aliases/FlowReadonly.md +0 -22
- package/docs/api/type-aliases/InitFunction.md +0 -21
- package/docs/api/type-aliases/InitFunctionAsync.md +0 -21
- package/docs/api/type-aliases/NotPromise.md +0 -21
- package/docs/api/type-aliases/UpdateFunction.md +0 -27
- package/docs/api/type-aliases/UpdateFunctionAsync.md +0 -27
- package/docs/api/typedoc-sidebar.json +0 -65
- package/docs/examples/examples.md +0 -2311
- package/docs/examples/patterns.md +0 -649
- package/docs/guide/advanced/architecture.md +0 -1234
- package/docs/guide/advanced/disposal.md +0 -426
- package/docs/guide/advanced/migration-v1.md +0 -464
- package/docs/guide/advanced/migration-v2.md +0 -204
- package/docs/guide/advanced/solidjs.md +0 -135
- package/docs/guide/introduction/concepts.md +0 -57
- package/docs/guide/introduction/conventions.md +0 -30
- package/docs/guide/introduction/getting-started.md +0 -139
- package/docs/guide/introduction/lifecycle.md +0 -368
- package/docs/guide/primitives/array.md +0 -286
- package/docs/guide/primitives/constant.md +0 -207
- package/docs/guide/primitives/derivations.md +0 -281
- package/docs/guide/primitives/effects.md +0 -372
- package/docs/guide/primitives/map.md +0 -265
- package/docs/guide/primitives/overview.md +0 -92
- package/docs/guide/primitives/signal.md +0 -222
- package/docs/guide/primitives/state.md +0 -272
- package/docs/index.md +0 -47
- package/docs/public/logo.svg +0 -1
- package/src/api/base/flowDisposable.ts +0 -44
- package/src/api/base/flowObservable.ts +0 -28
- package/src/api/base/flowSubscribable.ts +0 -87
- package/src/api/base/flowTracker.ts +0 -7
- package/src/api/base/index.ts +0 -4
- package/src/api/index.ts +0 -2
- package/src/api/nodes/async/flowConstantAsync.ts +0 -36
- package/src/api/nodes/async/flowDerivationAsync.ts +0 -42
- package/src/api/nodes/async/flowStateAsync.ts +0 -47
- package/src/api/nodes/async/flowWritableDerivationAsync.ts +0 -33
- package/src/api/nodes/async/index.ts +0 -4
- package/src/api/nodes/collections/flowArray.ts +0 -155
- package/src/api/nodes/collections/flowMap.ts +0 -115
- package/src/api/nodes/collections/index.ts +0 -2
- package/src/api/nodes/flowEffect.ts +0 -42
- package/src/api/nodes/flowSignal.ts +0 -28
- package/src/api/nodes/flowValue.ts +0 -37
- package/src/api/nodes/index.ts +0 -7
- package/src/api/nodes/sync/flowConstant.ts +0 -33
- package/src/api/nodes/sync/flowDerivation.ts +0 -41
- package/src/api/nodes/sync/flowState.ts +0 -45
- package/src/api/nodes/sync/flowWritableDerivation.ts +0 -31
- package/src/api/nodes/sync/index.ts +0 -4
- package/src/api/nodes/utils.ts +0 -24
- package/src/base/disposable.ts +0 -18
- package/src/base/executionStack.ts +0 -42
- package/src/base/index.ts +0 -5
- package/src/base/node.ts +0 -98
- package/src/base/observable.ts +0 -92
- package/src/base/observer.ts +0 -51
- package/src/converters/index.ts +0 -1
- package/src/converters/solid.ts +0 -109
- package/src/index.ts +0 -2
- package/src/nodes/arrayNode.ts +0 -180
- package/src/nodes/effectNode.ts +0 -58
- package/src/nodes/index.ts +0 -7
- package/src/nodes/mapNode.ts +0 -125
- package/src/nodes/signalNode.ts +0 -19
- package/src/nodes/valueAsyncNode.ts +0 -85
- package/src/nodes/valueNode.ts +0 -148
- package/src/nodes/valueSyncNode.ts +0 -125
- package/src/schedulers/asyncResolver.ts +0 -78
- package/src/schedulers/asyncScheduler.ts +0 -66
- package/src/schedulers/index.ts +0 -4
- package/src/schedulers/pendingError.ts +0 -13
- package/src/schedulers/scheduler.ts +0 -9
- package/src/schedulers/syncResolver.ts +0 -69
- package/src/schedulers/syncScheduler.ts +0 -55
- package/test/base/pendingError.test.ts +0 -67
- package/test/converters/solid.derivation.browser.test.tsx +0 -69
- package/test/converters/solid.node.test.ts +0 -654
- package/test/converters/solid.state.browser.test.tsx +0 -1592
- package/test/reactivity/flowSignal.test.ts +0 -226
- package/test/reactivity/nodes/async/asyncScheduler/asyncResolver.test.ts +0 -593
- package/test/reactivity/nodes/async/asyncScheduler/asyncScheduler.test.ts +0 -317
- package/test/reactivity/nodes/async/flowConstantAsync.test.ts +0 -652
- package/test/reactivity/nodes/async/flowDerivation.test.ts +0 -898
- package/test/reactivity/nodes/async/flowDerivationAsync.test.ts +0 -1716
- package/test/reactivity/nodes/async/flowStateAsync.test.ts +0 -708
- package/test/reactivity/nodes/async/flowWritableDerivationAsync.test.ts +0 -614
- package/test/reactivity/nodes/collections/flowArray.asyncStates.test.ts +0 -1289
- package/test/reactivity/nodes/collections/flowArray.scalars.test.ts +0 -961
- package/test/reactivity/nodes/collections/flowArray.states.test.ts +0 -1035
- package/test/reactivity/nodes/collections/flowMap.asyncStates.test.ts +0 -960
- package/test/reactivity/nodes/collections/flowMap.scalars.test.ts +0 -775
- package/test/reactivity/nodes/collections/flowMap.states.test.ts +0 -958
- package/test/reactivity/nodes/sync/flowConstant.test.ts +0 -377
- package/test/reactivity/nodes/sync/flowDerivation.test.ts +0 -896
- package/test/reactivity/nodes/sync/flowState.test.ts +0 -341
- package/test/reactivity/nodes/sync/flowWritableDerivation.test.ts +0 -603
- package/test/vitest.d.ts +0 -10
- package/tsconfig.json +0 -37
- package/typedoc.json +0 -37
- package/vite.config.ts +0 -31
- package/vitest.browser.config.ts +0 -21
- package/vitest.config.ts +0 -17
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Elisabeth Rousset
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
> **Upgrading from v1.x?** See the [Upgrade Guide](https://ersbeth-web.gitlab.io/picoflow/guide/advanced/migration-v2.html) for migration instructions.
|
|
6
6
|
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
7
11
|
## Installation
|
|
8
12
|
|
|
9
13
|
```bash
|
|
@@ -21,6 +25,27 @@ yarn add @ersbeth/picoflow
|
|
|
21
25
|
|
|
22
26
|
For comprehensive guides and API documentation, visit the [official website](https://ersbeth-web.gitlab.io/picoflow/)
|
|
23
27
|
|
|
28
|
+
## Development
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pnpm install
|
|
32
|
+
pnpm playwright:install # once — downloads Chromium for the browser tests
|
|
33
|
+
pnpm test # watch Node tests
|
|
34
|
+
pnpm test:browser # watch browser tests
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
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).
|
|
38
|
+
|
|
39
|
+
## AI Coding Agents
|
|
40
|
+
|
|
41
|
+
Picoflow ships a [`SKILL.md`](SKILL.md) with the npm package, matching the exact version you have
|
|
42
|
+
installed. If you use an agent skill tool (e.g. [`npx skills`](https://github.com/vercel-labs/skills)),
|
|
43
|
+
install it straight from `node_modules` after installing the package:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx skills add ./node_modules/@ersbeth/picoflow
|
|
47
|
+
```
|
|
48
|
+
|
|
24
49
|
## License
|
|
25
50
|
|
|
26
51
|
This project is licensed under the [MIT License](LICENSE).
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: picoflow
|
|
3
|
+
description: Use when writing, reviewing, or debugging TypeScript code that uses @ersbeth/picoflow's reactive primitives (signal, state, constant, derivation, writableDerivation, array, map, subscribe). Covers the explicit tracking model (get(t) vs pick()), disposal, and the mistakes that silently break reactivity.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# PicoFlow
|
|
7
|
+
|
|
8
|
+
PicoFlow is a reactive dataflow library with **explicit tracking** — there is no automatic dependency
|
|
9
|
+
detection like in Vue, MobX or Solid. Follow the rules below whenever you write or review code that
|
|
10
|
+
imports from `@ersbeth/picoflow`.
|
|
11
|
+
|
|
12
|
+
## The core rule: `.get(t)` vs `.pick()`
|
|
13
|
+
|
|
14
|
+
Every primitive is read one of two ways:
|
|
15
|
+
|
|
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.
|
|
18
|
+
- **`.pick()`** — untracked async read (`Promise<T>`). Use outside reactive contexts (event handlers,
|
|
19
|
+
one-off reads) when you don't want to react to future changes.
|
|
20
|
+
|
|
21
|
+
Never call `.get(t)` outside a derivation/subscribe callback — `t` isn't available there. Never use
|
|
22
|
+
`.pick()` inside a derivation/subscribe when you actually want reactivity — it silently creates no
|
|
23
|
+
dependency, which is a common source of "why doesn't this update" bugs.
|
|
24
|
+
|
|
25
|
+
Reactive values are named with a `$` prefix (`$count`, `$user`) by convention — keep this when writing
|
|
26
|
+
new code in a PicoFlow codebase, and use it to tell reactive values apart from plain ones at a glance.
|
|
27
|
+
|
|
28
|
+
## Choosing a primitive
|
|
29
|
+
|
|
30
|
+
| Need | Use | Not |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| Event with no payload (trigger a refresh, coordinate actions) | `signal()` | `state()` for a flag no one reads |
|
|
33
|
+
| Mutable value that changes over time | `state(value)` | `constant` (if it truly never changes) |
|
|
34
|
+
| 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 |
|
|
35
|
+
| Pure computed value from other primitives | `derivation((t) => ...)` | putting side effects in it — use `subscribe` |
|
|
36
|
+
| A derivation that occasionally needs manual override | `writableDerivation((t) => ...)` | — |
|
|
37
|
+
| List with fine-grained mutation tracking | `array(initial?)` | `state(someArray)` if you need per-operation tracking |
|
|
38
|
+
| Key-value store with fine-grained mutation tracking | `map(initial?)` | `state(someMap)` if you need per-operation tracking |
|
|
39
|
+
| Running side effects (DOM, network, storage, logging) in reaction to changes | `.subscribe()` (single primitive) or `subscribe()` (multiple) | `derivation` — derivations must stay pure |
|
|
40
|
+
|
|
41
|
+
Async variants exist for state/constant/derivation/writableDerivation (`stateAsync`, `constantAsync`,
|
|
42
|
+
`derivationAsync`, `writableDerivationAsync`) — same rules, the held value is a `Promise<T>`, and reads
|
|
43
|
+
of a still-pending value are handled automatically (surfaced via `subscribe()`'s optional `onPending`
|
|
44
|
+
callback) rather than needing manual `await`/try-catch inside a derivation.
|
|
45
|
+
|
|
46
|
+
## Mistakes to catch when reviewing PicoFlow code
|
|
47
|
+
|
|
48
|
+
- **Side effects inside `derivation()`** (logging, API calls, mutating outside state) — derivations must
|
|
49
|
+
be pure; move the side effect into a `subscribe()`.
|
|
50
|
+
- **Mutating a collection returned by `.get(t)`/`.pick()` in place**, e.g. `items.push(x)` on a picked
|
|
51
|
+
array, or `user.name = 'x'` on a picked object, then relying on it to update — this changes nothing
|
|
52
|
+
observable. Always produce a new array/object (`items.map(...)`, `{ ...user, name: 'x' }`), or use the
|
|
53
|
+
primitive's own mutation methods (`$array.push(x)`, `$map.update(k, v)`).
|
|
54
|
+
- **`constant(value)` with a direct value instead of a function** — constants always take an initializer
|
|
55
|
+
function (`constant(() => value)`), enforcing lazy evaluation. A direct value is a v1 pattern (see below).
|
|
56
|
+
- **Not disposing subscriptions/primitives** (`.dispose()`) when they're no longer needed — every
|
|
57
|
+
`subscribe()`/`.subscribe()` call keeps running until disposed; forgetting this in code that creates
|
|
58
|
+
subscriptions repeatedly (e.g. once per component instance, once per request) is a memory leak.
|
|
59
|
+
- **Creating a subscription inside another subscription's callback** — it re-creates one on every run
|
|
60
|
+
and leaks. Combine into a single `subscribe()` whose data function reads all the needed primitives.
|
|
61
|
+
- **A `subscribe()`/`.subscribe()` callback that calls `.set()` on a primitive it also reads** — infinite
|
|
62
|
+
loop.
|
|
63
|
+
- **Using `add()`/`update()` on `array`/`map` without checking preconditions** — `array.update(index, x)`
|
|
64
|
+
throws if the index is out of bounds; `map.add(key, x)` throws if the key exists; `map.update(key, x)`
|
|
65
|
+
throws if it doesn't. Check first (`.pick()`) or branch on it, don't assume.
|
|
66
|
+
- **Tracking the whole `array`/`map` (`.subscribe()`/`.get(t)`) when only specific operations matter** —
|
|
67
|
+
prefer `$collection.$lastAction.subscribe(...)` (a discriminated union with a `type` field) to react
|
|
68
|
+
only to the mutation kind you care about, instead of re-running on every change.
|
|
69
|
+
|
|
70
|
+
## Recognizing pre-v2 (deprecated) API usage
|
|
71
|
+
|
|
72
|
+
If existing code uses any of these, it predates PicoFlow v2 — flag it and suggest migrating (full guide:
|
|
73
|
+
https://ersbeth-web.gitlab.io/picoflow/guide/advanced/migration-v2.html):
|
|
74
|
+
|
|
75
|
+
- `effect((t) => ...)` — removed. Replace with `.subscribe()` (single primitive) or `subscribe()` (multiple).
|
|
76
|
+
- `$map.$lastAdded` / `$lastUpdated` / `$lastDeleted` — replaced by a single `$map.$lastAction` signal
|
|
77
|
+
with a `type` discriminant.
|
|
78
|
+
- `$array.setItem(index, value)` — renamed to `$array.update(index, value)`.
|
|
79
|
+
- `constant(value)` with a direct (non-function) value.
|
|
80
|
+
|
|
81
|
+
## Minimal example (the shape most PicoFlow code follows)
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { state, derivation, subscribe } from '@ersbeth/picoflow'
|
|
85
|
+
|
|
86
|
+
const $count = state(0)
|
|
87
|
+
const $isEven = derivation((t) => $count.get(t) % 2 === 0)
|
|
88
|
+
|
|
89
|
+
const effect = subscribe(
|
|
90
|
+
(t) => ({ count: $count.get(t), even: $isEven.get(t) }),
|
|
91
|
+
(data) => console.log(`Count is ${data.count}, which is ${data.even ? 'even' : 'odd'}`),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
$count.set((n) => n + 1) // atomic updater form, prefer it when the new value depends on the current one
|
|
95
|
+
|
|
96
|
+
// when done:
|
|
97
|
+
effect.dispose()
|
|
98
|
+
$count.dispose()
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Further reference
|
|
102
|
+
|
|
103
|
+
This file covers the rules needed to write and review correct PicoFlow code. For the full generated API
|
|
104
|
+
reference (every method, accessor, inherited member) or the guide with longer walkthroughs, see
|
|
105
|
+
https://ersbeth-web.gitlab.io/picoflow/ (or its condensed single-file form at
|
|
106
|
+
https://ersbeth-web.gitlab.io/picoflow/llms-full.txt).
|
package/dist/picoflow.js
CHANGED
|
@@ -7,7 +7,7 @@ function isDisposable(obj) {
|
|
|
7
7
|
class PendingError extends Error {
|
|
8
8
|
pendingPromise;
|
|
9
9
|
constructor(promise) {
|
|
10
|
-
super("[PicoFlow]
|
|
10
|
+
super("[PicoFlow] Computation is pending");
|
|
11
11
|
this.name = "PendingError";
|
|
12
12
|
this.pendingPromise = promise;
|
|
13
13
|
}
|
|
@@ -36,8 +36,8 @@ class AsyncResolver {
|
|
|
36
36
|
return this._finished;
|
|
37
37
|
}
|
|
38
38
|
compute() {
|
|
39
|
-
if (this._finished) throw new Error("[
|
|
40
|
-
if (this._aborted) throw new Error("[
|
|
39
|
+
if (this._finished) throw new Error("[PicoFlow] AsyncResolver: Can't restart a settled resolver");
|
|
40
|
+
if (this._aborted) throw new Error("[PicoFlow] AsyncResolver: Can't restart an aborted resolver");
|
|
41
41
|
this._iteration++;
|
|
42
42
|
const currentIteration = this._iteration;
|
|
43
43
|
this._compute().then((value) => {
|
|
@@ -51,8 +51,8 @@ class AsyncResolver {
|
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
53
|
overwrite(promise) {
|
|
54
|
-
if (this._finished) throw new Error("[
|
|
55
|
-
if (this._aborted) throw new Error("[
|
|
54
|
+
if (this._finished) throw new Error("[PicoFlow] AsyncResolver: Can't overwrite a settled resolver");
|
|
55
|
+
if (this._aborted) throw new Error("[PicoFlow] AsyncResolver: Can't overwrite an aborted resolver");
|
|
56
56
|
this._iteration++;
|
|
57
57
|
const currentIteration = this._iteration;
|
|
58
58
|
promise.then((value) => {
|
|
@@ -81,7 +81,7 @@ class AsyncScheduler {
|
|
|
81
81
|
this._onResolve = onResolve;
|
|
82
82
|
this._onReject = onReject;
|
|
83
83
|
this._resolver = new AsyncResolver(compute);
|
|
84
|
-
this.
|
|
84
|
+
this._attachHandlers(this._resolver);
|
|
85
85
|
}
|
|
86
86
|
get settled() {
|
|
87
87
|
if (this._disposed) throw new Error("[PicoFlow] ComputationScheduler is disposed");
|
|
@@ -95,7 +95,7 @@ class AsyncScheduler {
|
|
|
95
95
|
if (this._resolver.finished) {
|
|
96
96
|
this._resolver = new AsyncResolver(this._compute);
|
|
97
97
|
this._settled = Promise.withResolvers();
|
|
98
|
-
this.
|
|
98
|
+
this._attachHandlers(this._resolver);
|
|
99
99
|
}
|
|
100
100
|
this._resolver.overwrite(promise);
|
|
101
101
|
}
|
|
@@ -104,10 +104,19 @@ class AsyncScheduler {
|
|
|
104
104
|
if (this._resolver.finished) {
|
|
105
105
|
this._resolver = new AsyncResolver(this._compute);
|
|
106
106
|
this._settled = Promise.withResolvers();
|
|
107
|
-
this.
|
|
107
|
+
this._attachHandlers(this._resolver);
|
|
108
108
|
}
|
|
109
109
|
this._resolver.compute();
|
|
110
110
|
}
|
|
111
|
+
_attachHandlers(resolver) {
|
|
112
|
+
resolver.computed.then((value) => {
|
|
113
|
+
if (this._disposed) return;
|
|
114
|
+
this._onResolve(value);
|
|
115
|
+
}).catch((error) => {
|
|
116
|
+
if (this._disposed) return;
|
|
117
|
+
this._onReject(error);
|
|
118
|
+
}).finally(() => this._settled.resolve());
|
|
119
|
+
}
|
|
111
120
|
dispose() {
|
|
112
121
|
this._resolver.abort();
|
|
113
122
|
this._disposed = true;
|
|
@@ -116,7 +125,6 @@ class AsyncScheduler {
|
|
|
116
125
|
|
|
117
126
|
class SyncResolver {
|
|
118
127
|
_compute;
|
|
119
|
-
_iteration = 0;
|
|
120
128
|
_aborted = false;
|
|
121
129
|
_finished = false;
|
|
122
130
|
_onValue;
|
|
@@ -133,18 +141,16 @@ class SyncResolver {
|
|
|
133
141
|
return this._finished;
|
|
134
142
|
}
|
|
135
143
|
compute() {
|
|
136
|
-
if (this._finished) throw new Error("[
|
|
137
|
-
if (this._aborted) throw new Error("[
|
|
138
|
-
this._iteration++;
|
|
139
|
-
const currentIteration = this._iteration;
|
|
144
|
+
if (this._finished) throw new Error("[PicoFlow] SyncResolver: Can't restart a settled resolver");
|
|
145
|
+
if (this._aborted) throw new Error("[PicoFlow] SyncResolver: Can't restart an aborted resolver");
|
|
140
146
|
try {
|
|
141
147
|
const value = this._compute();
|
|
142
|
-
if (this.
|
|
148
|
+
if (!this._finished && !this._aborted) {
|
|
143
149
|
this._finished = true;
|
|
144
150
|
this._onValue(value);
|
|
145
151
|
}
|
|
146
152
|
} catch (error) {
|
|
147
|
-
if (this.
|
|
153
|
+
if (!this._finished && !this._aborted) {
|
|
148
154
|
if (!(error instanceof PendingError)) {
|
|
149
155
|
this._finished = true;
|
|
150
156
|
this._onError(error);
|
|
@@ -153,17 +159,12 @@ class SyncResolver {
|
|
|
153
159
|
}
|
|
154
160
|
}
|
|
155
161
|
overwrite(value) {
|
|
156
|
-
if (this._finished) throw new Error("[
|
|
157
|
-
if (this._aborted) throw new Error("[
|
|
158
|
-
this.
|
|
159
|
-
|
|
160
|
-
if (this._iteration === currentIteration && !this._aborted) {
|
|
161
|
-
this._finished = true;
|
|
162
|
-
this._onValue(value);
|
|
163
|
-
}
|
|
162
|
+
if (this._finished) throw new Error("[PicoFlow] SyncResolver: Can't overwrite a settled resolver");
|
|
163
|
+
if (this._aborted) throw new Error("[PicoFlow] SyncResolver: Can't overwrite an aborted resolver");
|
|
164
|
+
this._finished = true;
|
|
165
|
+
this._onValue(value);
|
|
164
166
|
}
|
|
165
167
|
abort() {
|
|
166
|
-
this._iteration++;
|
|
167
168
|
this._aborted = true;
|
|
168
169
|
}
|
|
169
170
|
}
|
|
@@ -224,17 +225,48 @@ class Disposable {
|
|
|
224
225
|
}
|
|
225
226
|
|
|
226
227
|
class ExecutionStack {
|
|
228
|
+
static _MAX_FLUSH_STEPS = 1e4;
|
|
227
229
|
static _pendingQueue = [];
|
|
228
230
|
static _effectQueue = [];
|
|
229
231
|
static _executionScheduled;
|
|
232
|
+
static _coalesceResetScheduled = false;
|
|
233
|
+
/**
|
|
234
|
+
* True once the current synchronous call stack has finished and microtasks have started.
|
|
235
|
+
* EffectNode uses this to treat microtask notifications (e.g. async resolve) differently
|
|
236
|
+
* from coalesced sync-batch notifications.
|
|
237
|
+
*/
|
|
238
|
+
static _pastSyncTurn = false;
|
|
239
|
+
/** @internal Used by EffectNode to distinguish sync-batch from microtask notifications. */
|
|
240
|
+
static get pastSyncTurn() {
|
|
241
|
+
return ExecutionStack._pastSyncTurn;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* @internal Runs after the current sync turn ends (queueMicrotask).
|
|
245
|
+
* Marks pastSyncTurn so the next coalesced effect notify can bump its epoch,
|
|
246
|
+
* and resets per-effect coalesce flags via onReset.
|
|
247
|
+
*/
|
|
248
|
+
static scheduleCoalesceReset(onReset) {
|
|
249
|
+
if (ExecutionStack._coalesceResetScheduled) return;
|
|
250
|
+
ExecutionStack._coalesceResetScheduled = true;
|
|
251
|
+
queueMicrotask(() => {
|
|
252
|
+
ExecutionStack._pastSyncTurn = true;
|
|
253
|
+
ExecutionStack._coalesceResetScheduled = false;
|
|
254
|
+
onReset();
|
|
255
|
+
});
|
|
256
|
+
}
|
|
230
257
|
static pushPending(node) {
|
|
258
|
+
ExecutionStack._beginSyncTurn();
|
|
231
259
|
ExecutionStack._scheduleExecution();
|
|
232
260
|
ExecutionStack._pendingQueue.push(node);
|
|
233
261
|
}
|
|
234
262
|
static pushEffect(effect) {
|
|
263
|
+
ExecutionStack._beginSyncTurn();
|
|
235
264
|
ExecutionStack._scheduleExecution();
|
|
236
265
|
ExecutionStack._effectQueue.push(effect);
|
|
237
266
|
}
|
|
267
|
+
static _beginSyncTurn() {
|
|
268
|
+
ExecutionStack._pastSyncTurn = false;
|
|
269
|
+
}
|
|
238
270
|
static _scheduleExecution() {
|
|
239
271
|
if (ExecutionStack._executionScheduled) return;
|
|
240
272
|
ExecutionStack._executionScheduled = new Promise((resolve) => {
|
|
@@ -245,15 +277,21 @@ class ExecutionStack {
|
|
|
245
277
|
}, 0);
|
|
246
278
|
});
|
|
247
279
|
}
|
|
280
|
+
static _drain(queue) {
|
|
281
|
+
for (let i = 0; i < queue.length; i++) {
|
|
282
|
+
if (i >= ExecutionStack._MAX_FLUSH_STEPS) {
|
|
283
|
+
ExecutionStack._pendingQueue.length = 0;
|
|
284
|
+
ExecutionStack._effectQueue.length = 0;
|
|
285
|
+
throw new Error("[PicoFlow] Reactive update cycle exceeded maximum depth");
|
|
286
|
+
}
|
|
287
|
+
const node = queue[i];
|
|
288
|
+
node?.execute();
|
|
289
|
+
}
|
|
290
|
+
queue.length = 0;
|
|
291
|
+
}
|
|
248
292
|
static _execute() {
|
|
249
|
-
ExecutionStack._pendingQueue
|
|
250
|
-
|
|
251
|
-
});
|
|
252
|
-
ExecutionStack._pendingQueue.length = 0;
|
|
253
|
-
ExecutionStack._effectQueue.forEach((effect) => {
|
|
254
|
-
effect.execute();
|
|
255
|
-
});
|
|
256
|
-
ExecutionStack._effectQueue.length = 0;
|
|
293
|
+
ExecutionStack._drain(ExecutionStack._pendingQueue);
|
|
294
|
+
ExecutionStack._drain(ExecutionStack._effectQueue);
|
|
257
295
|
}
|
|
258
296
|
}
|
|
259
297
|
|
|
@@ -403,32 +441,67 @@ class EffectNode extends Observer {
|
|
|
403
441
|
_onData;
|
|
404
442
|
_onError;
|
|
405
443
|
_onPending;
|
|
444
|
+
/** True while this effect is already waiting in the effect queue for the next flush. */
|
|
445
|
+
_queued = false;
|
|
446
|
+
/** Guards the single coalesced bump of _notifyEpoch while _queued during one sync turn. */
|
|
447
|
+
_coalescedWhileQueued = false;
|
|
448
|
+
/** Monotonic count of notifications that must eventually be executed. */
|
|
449
|
+
_notifyEpoch = 0;
|
|
450
|
+
/** How many notification epochs have been executed so far. */
|
|
451
|
+
_executedEpoch = 0;
|
|
406
452
|
constructor(data, onData, onError, onPending) {
|
|
407
453
|
super();
|
|
408
454
|
this._data = data;
|
|
409
455
|
this._onData = onData;
|
|
410
456
|
this._onError = onError;
|
|
411
457
|
this._onPending = onPending;
|
|
458
|
+
this._notifyEpoch = 1;
|
|
412
459
|
this.execute();
|
|
413
460
|
}
|
|
414
461
|
notify() {
|
|
415
462
|
if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
463
|
+
if (this._queued) {
|
|
464
|
+
if (!this._coalescedWhileQueued) {
|
|
465
|
+
this._coalescedWhileQueued = true;
|
|
466
|
+
if (ExecutionStack.pastSyncTurn) {
|
|
467
|
+
this._notifyEpoch++;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
this._notifyEpoch++;
|
|
473
|
+
this._queued = true;
|
|
474
|
+
ExecutionStack.scheduleCoalesceReset(() => {
|
|
475
|
+
this._coalescedWhileQueued = false;
|
|
476
|
+
});
|
|
416
477
|
ExecutionStack.pushEffect(this);
|
|
417
478
|
}
|
|
418
479
|
execute() {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
this.
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
480
|
+
this._queued = false;
|
|
481
|
+
if (this._disposed) {
|
|
482
|
+
this._executedEpoch = this._notifyEpoch;
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const targetEpoch = this._notifyEpoch;
|
|
486
|
+
while (this._executedEpoch < targetEpoch) {
|
|
487
|
+
this._executedEpoch++;
|
|
488
|
+
if (this._disposed) {
|
|
489
|
+
this._executedEpoch = this._notifyEpoch;
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
try {
|
|
493
|
+
this.clearDependencies();
|
|
494
|
+
const data = this._data(this);
|
|
495
|
+
this._onData(data);
|
|
496
|
+
} catch (error) {
|
|
497
|
+
if (error instanceof PendingError) {
|
|
498
|
+
this._onPending?.();
|
|
430
499
|
} else {
|
|
431
|
-
|
|
500
|
+
if (this._onError) {
|
|
501
|
+
this._onError(error instanceof Error ? error : new Error(String(error)));
|
|
502
|
+
} else {
|
|
503
|
+
throw error;
|
|
504
|
+
}
|
|
432
505
|
}
|
|
433
506
|
}
|
|
434
507
|
}
|
|
@@ -570,12 +643,14 @@ class ValueAsyncNode extends ValueNode {
|
|
|
570
643
|
);
|
|
571
644
|
}
|
|
572
645
|
_onResolve(value) {
|
|
646
|
+
if (this.disposed) return;
|
|
573
647
|
this.status = "resolved";
|
|
574
648
|
this._value = value;
|
|
575
649
|
this._error = void 0;
|
|
576
650
|
this.notifyDependents();
|
|
577
651
|
}
|
|
578
652
|
_onReject(error) {
|
|
653
|
+
if (this.disposed) return;
|
|
579
654
|
this.status = "error";
|
|
580
655
|
this._error = error;
|
|
581
656
|
this.notifyDependents();
|
|
@@ -724,11 +799,66 @@ class ValueSyncNode extends ValueNode {
|
|
|
724
799
|
}
|
|
725
800
|
}
|
|
726
801
|
|
|
802
|
+
class ActionNode extends ValueSyncNode {
|
|
803
|
+
_slots = /* @__PURE__ */ new Set();
|
|
804
|
+
_enqueueForSubscribers(action) {
|
|
805
|
+
for (const slot of this._slots) {
|
|
806
|
+
slot.queue.push(action);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
set(valueOrUpdater) {
|
|
810
|
+
if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
811
|
+
if (typeof valueOrUpdater === "function") {
|
|
812
|
+
const updater = valueOrUpdater;
|
|
813
|
+
const nextValue2 = updater(this._value);
|
|
814
|
+
if (this.status === "resolved" && nextValue2 === this._value) return;
|
|
815
|
+
this._enqueueForSubscribers(nextValue2);
|
|
816
|
+
super.set(updater);
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
const nextValue = valueOrUpdater;
|
|
820
|
+
if (this.status === "resolved" && nextValue === this._value) return;
|
|
821
|
+
this._enqueueForSubscribers(nextValue);
|
|
822
|
+
super.set(nextValue);
|
|
823
|
+
}
|
|
824
|
+
subscribe(onValue, onError, onPending) {
|
|
825
|
+
if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
826
|
+
const slot = { queue: [] };
|
|
827
|
+
this._slots.add(slot);
|
|
828
|
+
const effect = new EffectNode(
|
|
829
|
+
(tracker) => {
|
|
830
|
+
this.watch(tracker);
|
|
831
|
+
if (slot.queue.length === 0) {
|
|
832
|
+
return [this.get(tracker)];
|
|
833
|
+
}
|
|
834
|
+
return slot.queue.splice(0);
|
|
835
|
+
},
|
|
836
|
+
(actions) => {
|
|
837
|
+
for (const action of actions) {
|
|
838
|
+
onValue(action);
|
|
839
|
+
}
|
|
840
|
+
},
|
|
841
|
+
onError,
|
|
842
|
+
onPending
|
|
843
|
+
);
|
|
844
|
+
const originalDispose = effect.dispose.bind(effect);
|
|
845
|
+
effect.dispose = () => {
|
|
846
|
+
this._slots.delete(slot);
|
|
847
|
+
originalDispose();
|
|
848
|
+
};
|
|
849
|
+
return effect;
|
|
850
|
+
}
|
|
851
|
+
dispose() {
|
|
852
|
+
this._slots.clear();
|
|
853
|
+
super.dispose();
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
727
857
|
class ArrayNode extends ValueSyncNode {
|
|
728
858
|
$lastAction;
|
|
729
859
|
constructor(value = []) {
|
|
730
860
|
super(value);
|
|
731
|
-
this.$lastAction = new
|
|
861
|
+
this.$lastAction = new ActionNode({
|
|
732
862
|
type: "set",
|
|
733
863
|
setItems: value,
|
|
734
864
|
clearedItems: []
|
|
@@ -781,6 +911,7 @@ class ArrayNode extends ValueSyncNode {
|
|
|
781
911
|
}
|
|
782
912
|
pop() {
|
|
783
913
|
if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
914
|
+
if (this._value.length === 0) return void 0;
|
|
784
915
|
const item = this._value.pop();
|
|
785
916
|
this.notifyDependents();
|
|
786
917
|
this.$lastAction.set({ type: "pop", removedItem: item });
|
|
@@ -794,6 +925,7 @@ class ArrayNode extends ValueSyncNode {
|
|
|
794
925
|
}
|
|
795
926
|
shift() {
|
|
796
927
|
if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
928
|
+
if (this._value.length === 0) return void 0;
|
|
797
929
|
const item = this._value.shift();
|
|
798
930
|
this.notifyDependents();
|
|
799
931
|
this.$lastAction.set({ type: "shift", removedItem: item });
|
|
@@ -822,9 +954,9 @@ class ArrayNode extends ValueSyncNode {
|
|
|
822
954
|
}
|
|
823
955
|
dispose() {
|
|
824
956
|
super.dispose();
|
|
825
|
-
this.
|
|
826
|
-
|
|
827
|
-
}
|
|
957
|
+
if (!this.$lastAction.disposed) {
|
|
958
|
+
this.$lastAction.dispose();
|
|
959
|
+
}
|
|
828
960
|
this._value = [];
|
|
829
961
|
}
|
|
830
962
|
}
|
|
@@ -837,7 +969,7 @@ class MapNode extends ValueSyncNode {
|
|
|
837
969
|
$lastAction;
|
|
838
970
|
constructor(value = /* @__PURE__ */ new Map()) {
|
|
839
971
|
super(value);
|
|
840
|
-
this.$lastAction = new
|
|
972
|
+
this.$lastAction = new ActionNode({
|
|
841
973
|
type: "set",
|
|
842
974
|
setMap: value,
|
|
843
975
|
clearedMap: /* @__PURE__ */ new Map()
|
|
@@ -845,8 +977,7 @@ class MapNode extends ValueSyncNode {
|
|
|
845
977
|
}
|
|
846
978
|
add(key, value) {
|
|
847
979
|
if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
848
|
-
|
|
849
|
-
if (previousValue) {
|
|
980
|
+
if (this._value.has(key)) {
|
|
850
981
|
throw new Error("[PicoFlow] Key already exists");
|
|
851
982
|
}
|
|
852
983
|
this._value.set(key, value);
|
|
@@ -855,8 +986,8 @@ class MapNode extends ValueSyncNode {
|
|
|
855
986
|
}
|
|
856
987
|
update(key, value) {
|
|
857
988
|
if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
989
|
+
if (!this._value.has(key)) throw new Error("[PicoFlow] Key does not exist");
|
|
858
990
|
const previousValue = this._value.get(key);
|
|
859
|
-
if (!previousValue) throw new Error("[PicoFlow] Key does not exist");
|
|
860
991
|
this._value.set(key, value);
|
|
861
992
|
this.notifyDependents();
|
|
862
993
|
this.$lastAction.set({
|
|
@@ -869,8 +1000,8 @@ class MapNode extends ValueSyncNode {
|
|
|
869
1000
|
}
|
|
870
1001
|
delete(key) {
|
|
871
1002
|
if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
|
|
1003
|
+
if (!this._value.has(key)) throw new Error("[PicoFlow] Key does not exist");
|
|
872
1004
|
const value = this._value.get(key);
|
|
873
|
-
if (value === void 0) throw new Error("[PicoFlow] Key does not exist");
|
|
874
1005
|
this._value.delete(key);
|
|
875
1006
|
this.notifyDependents();
|
|
876
1007
|
this.$lastAction.set({ type: "delete", key, removedValue: value });
|
|
@@ -897,9 +1028,9 @@ class MapNode extends ValueSyncNode {
|
|
|
897
1028
|
}
|
|
898
1029
|
dispose() {
|
|
899
1030
|
super.dispose();
|
|
900
|
-
this.
|
|
901
|
-
|
|
902
|
-
}
|
|
1031
|
+
if (!this.$lastAction.disposed) {
|
|
1032
|
+
this.$lastAction.dispose();
|
|
1033
|
+
}
|
|
903
1034
|
this._value.clear();
|
|
904
1035
|
}
|
|
905
1036
|
}
|
|
@@ -943,7 +1074,15 @@ function writableDerivation(compute) {
|
|
|
943
1074
|
return new ValueSyncNode(compute);
|
|
944
1075
|
}
|
|
945
1076
|
|
|
946
|
-
function
|
|
1077
|
+
function describeFlowInput(value) {
|
|
1078
|
+
if (value === null) return "null";
|
|
1079
|
+
if (typeof value === "object") {
|
|
1080
|
+
const name = value.constructor?.name;
|
|
1081
|
+
return name ? `object (${name})` : "object";
|
|
1082
|
+
}
|
|
1083
|
+
return typeof value;
|
|
1084
|
+
}
|
|
1085
|
+
function fromNode(node, options) {
|
|
947
1086
|
const [resource, { refetch }] = createResource(() => node.pick());
|
|
948
1087
|
let fx;
|
|
949
1088
|
onMount(() => {
|
|
@@ -968,14 +1107,19 @@ function fromNode(node) {
|
|
|
968
1107
|
resource.error ?? resource();
|
|
969
1108
|
resetErrorBoundaries();
|
|
970
1109
|
});
|
|
971
|
-
onCleanup(() =>
|
|
1110
|
+
onCleanup(() => {
|
|
1111
|
+
fx.dispose();
|
|
1112
|
+
if (options?.disposeNode && !node.disposed) {
|
|
1113
|
+
node.dispose();
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
972
1116
|
return resource;
|
|
973
1117
|
}
|
|
974
1118
|
function fromGetter(getter) {
|
|
975
1119
|
const derivation = new ValueSyncNode((t) => {
|
|
976
1120
|
return getter(t);
|
|
977
1121
|
});
|
|
978
|
-
return fromNode(derivation);
|
|
1122
|
+
return fromNode(derivation, { disposeNode: true });
|
|
979
1123
|
}
|
|
980
1124
|
function from(flow) {
|
|
981
1125
|
if (flow instanceof ValueAsyncNode || flow instanceof ValueSyncNode) {
|
|
@@ -984,7 +1128,7 @@ function from(flow) {
|
|
|
984
1128
|
if (typeof flow === "function") {
|
|
985
1129
|
return fromGetter(flow);
|
|
986
1130
|
}
|
|
987
|
-
throw new Error(
|
|
1131
|
+
throw new Error(`[PicoFlow] from(): expected a FlowValue or getter function, received ${describeFlowInput(flow)}`);
|
|
988
1132
|
}
|
|
989
1133
|
|
|
990
1134
|
export { array, constant, constantAsync, derivation, derivationAsync, from, isDisposable, map, signal, state, stateAsync, subscribe, writableDerivation, writableDerivationAsync };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { FlowDisposable } from './flowDisposable';
|
|
2
|
-
import { FlowSubscribable } from './flowSubscribable';
|
|
3
|
-
import { FlowTracker } from './flowTracker';
|
|
1
|
+
import { FlowDisposable } from './flowDisposable.js';
|
|
2
|
+
import { FlowSubscribable } from './flowSubscribable.js';
|
|
3
|
+
import { FlowTracker } from './flowTracker.js';
|
|
4
4
|
/**
|
|
5
5
|
* Contract for reactive primitives that emit changes and notify their dependents in the reactivity graph.
|
|
6
6
|
* @public
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flowObservable.d.ts","sourceRoot":"","sources":["../../../../src/api/base/flowObservable.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"flowObservable.d.ts","sourceRoot":"","sources":["../../../../src/api/base/flowObservable.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD;;;GAGG;AACH,MAAM,WAAW,cAAc,CAAC,CAAC,CAAE,SAAQ,cAAc,EAAE,gBAAgB,CAAC,CAAC,CAAC;IAC1E;;;;;;;OAOG;IACH,KAAK,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAAC;IAElC;;;;;;OAMG;IACH,OAAO,IAAI,IAAI,CAAC;CACnB"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { FlowEffect } from '../nodes/flowEffect';
|
|
2
|
-
import { FlowTracker } from './flowTracker';
|
|
1
|
+
import { FlowEffect } from '../nodes/flowEffect.js';
|
|
2
|
+
import { FlowTracker } from './flowTracker.js';
|
|
3
3
|
/**
|
|
4
4
|
* Function that tracks reactive dependencies and returns data.
|
|
5
5
|
*
|