@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
|
@@ -1,372 +0,0 @@
|
|
|
1
|
-
# Side Effects & Subscribe
|
|
2
|
-
|
|
3
|
-
Side effects are how you perform **actions** in response to reactive changes. They're the bridge between your reactive data and the outside world - updating the DOM, making API calls, saving to localStorage, and more.
|
|
4
|
-
|
|
5
|
-
Side effects are created by subscribing to reactive primitives using `subscribe()` or `.subscribe()`. When you subscribe, you get a `FlowEffect` object that can be disposed to stop the side effect.
|
|
6
|
-
|
|
7
|
-
### Key Characteristics
|
|
8
|
-
|
|
9
|
-
- **Two syntaxes**: `.subscribe()` for single primitive, `subscribe()` for multiple
|
|
10
|
-
- **Immediate execution**: Runs synchronously when created
|
|
11
|
-
- **Automatic re-execution**: Re-runs when dependencies change
|
|
12
|
-
- **Dynamic dependency tracking**: Dependencies adapt at runtime
|
|
13
|
-
- **Returns FlowEffect**: Disposable handle to stop the subscription
|
|
14
|
-
|
|
15
|
-
## When to Use Subscribe
|
|
16
|
-
|
|
17
|
-
Use subscribe when you need to:
|
|
18
|
-
|
|
19
|
-
- ✅ Perform side effects (DOM updates, API calls, localStorage)
|
|
20
|
-
- ✅ React to changes in reactive values
|
|
21
|
-
- ✅ Coordinate with external systems
|
|
22
|
-
- ✅ Log or debug reactive changes
|
|
23
|
-
|
|
24
|
-
Don't use subscribe when:
|
|
25
|
-
|
|
26
|
-
- ❌ Computing derived values (use derivations instead)
|
|
27
|
-
- ❌ You only need to run code once (use regular functions)
|
|
28
|
-
|
|
29
|
-
## Creating Side Effects
|
|
30
|
-
|
|
31
|
-
There are two ways to create side effects, depending on your needs:
|
|
32
|
-
|
|
33
|
-
### Single Primitive - `.subscribe()`
|
|
34
|
-
|
|
35
|
-
When reacting to a single primitive, use the `.subscribe()` method:
|
|
36
|
-
|
|
37
|
-
```typescript
|
|
38
|
-
import { state } from '@ersbeth/picoflow'
|
|
39
|
-
|
|
40
|
-
const $count = state(0)
|
|
41
|
-
|
|
42
|
-
// Subscribe directly to the primitive
|
|
43
|
-
const effect = $count.subscribe((value) => {
|
|
44
|
-
console.log('Count is:', value)
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
// Later: dispose the effect
|
|
48
|
-
effect.dispose()
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
### Multiple Primitives - `subscribe()`
|
|
52
|
-
|
|
53
|
-
When combining multiple primitives, use the global `subscribe()` function:
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
import { state, subscribe } from '@ersbeth/picoflow'
|
|
57
|
-
|
|
58
|
-
const $firstName = state('Alice')
|
|
59
|
-
const $lastName = state('Smith')
|
|
60
|
-
|
|
61
|
-
// Combine multiple values
|
|
62
|
-
const effect = subscribe(
|
|
63
|
-
(t) => ({ first: $firstName.get(t), last: $lastName.get(t) }),
|
|
64
|
-
(data) => {
|
|
65
|
-
console.log('Full name:', data.first, data.last)
|
|
66
|
-
}
|
|
67
|
-
)
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
The parameter `t` is the **FlowTracker** - your tool for creating dependencies.
|
|
71
|
-
|
|
72
|
-
## Using Side-Effects
|
|
73
|
-
|
|
74
|
-
### Conditional Dependencies
|
|
75
|
-
|
|
76
|
-
Dependencies can be conditional - they're re-evaluated on each execution:
|
|
77
|
-
|
|
78
|
-
```typescript
|
|
79
|
-
const $showDetails = state(false)
|
|
80
|
-
const $details = state({ info: 'secret' })
|
|
81
|
-
|
|
82
|
-
subscribe(
|
|
83
|
-
(t) => {
|
|
84
|
-
const show = $showDetails.get(t)
|
|
85
|
-
if (show) {
|
|
86
|
-
return { show: true, details: $details.get(t) }
|
|
87
|
-
}
|
|
88
|
-
return { show: false, details: null }
|
|
89
|
-
},
|
|
90
|
-
(data) => {
|
|
91
|
-
if (data.show) {
|
|
92
|
-
console.log('Details:', data.details)
|
|
93
|
-
} else {
|
|
94
|
-
console.log('Details hidden')
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
// When $showDetails is false, changes to $details don't trigger the subscription
|
|
100
|
-
$details.set({ info: 'new secret' }) // Subscription doesn't run
|
|
101
|
-
|
|
102
|
-
// But once $showDetails is true...
|
|
103
|
-
$showDetails.set(true) // Subscription runs and now tracks $details
|
|
104
|
-
$details.set({ info: 'updated' }) // Now this triggers the subscription!
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
### Dispose
|
|
108
|
-
|
|
109
|
-
Subscriptions run indefinitely unless disposed. Always clean them up when no longer needed:
|
|
110
|
-
|
|
111
|
-
```typescript
|
|
112
|
-
const $count = state(0)
|
|
113
|
-
|
|
114
|
-
// Create subscription
|
|
115
|
-
const effect = $count.subscribe((value) => {
|
|
116
|
-
console.log(value)
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
// Later... stop the subscription
|
|
120
|
-
effect.dispose()
|
|
121
|
-
|
|
122
|
-
// After disposal, the subscription won't run anymore
|
|
123
|
-
$count.set(10) // No console log
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
**Always dispose subscriptions when:**
|
|
127
|
-
- Component unmounts (in UI frameworks)
|
|
128
|
-
- Feature is disabled
|
|
129
|
-
- User navigates away
|
|
130
|
-
- App shuts down
|
|
131
|
-
|
|
132
|
-
**Memory leak example:**
|
|
133
|
-
|
|
134
|
-
```typescript
|
|
135
|
-
// ❌ Memory leak
|
|
136
|
-
function createUserPanel(userId: number) {
|
|
137
|
-
const $user = state(getUserData(userId))
|
|
138
|
-
|
|
139
|
-
$user.subscribe((user) => {
|
|
140
|
-
updateUI(user)
|
|
141
|
-
})
|
|
142
|
-
// Subscription never disposed! ⚠️
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Called 100 times = 100 subscriptions still running!
|
|
146
|
-
|
|
147
|
-
// ✅ Proper cleanup
|
|
148
|
-
function createUserPanel(userId: number) {
|
|
149
|
-
const $user = state(getUserData(userId))
|
|
150
|
-
|
|
151
|
-
const effect = $user.subscribe((user) => {
|
|
152
|
-
updateUI(user)
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
return {
|
|
156
|
-
dispose: () => {
|
|
157
|
-
effect.dispose()
|
|
158
|
-
$user.dispose()
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
## Lifecycle
|
|
165
|
-
|
|
166
|
-
Subscriptions execute immediately when created, track their dependencies, and re-execute when any dependency changes. Understanding this lifecycle is key to writing efficient reactive code.
|
|
167
|
-
|
|
168
|
-
When you create a subscription, it runs **synchronously** - not on the next tick, but right away. This allows subscriptions to establish initial dependencies and perform initial side effects. When a dependency changes, the subscription re-executes synchronously.
|
|
169
|
-
|
|
170
|
-
```mermaid
|
|
171
|
-
sequenceDiagram
|
|
172
|
-
participant User
|
|
173
|
-
participant $count as $count (State)
|
|
174
|
-
participant Subscription
|
|
175
|
-
|
|
176
|
-
Note over User,Subscription: 1. Creation Phase
|
|
177
|
-
User->>Subscription: "Create $count.subscribe(...)"
|
|
178
|
-
activate Subscription
|
|
179
|
-
Note over Subscription: Execute immediately
|
|
180
|
-
|
|
181
|
-
Subscription->>$count: Read value
|
|
182
|
-
activate $count
|
|
183
|
-
Note over $count: Register Subscription as dependent
|
|
184
|
-
$count-->>Subscription: 0
|
|
185
|
-
deactivate $count
|
|
186
|
-
|
|
187
|
-
Note over Subscription: "console.log(\"Count: 0\")"
|
|
188
|
-
Note over Subscription: Track: depends on $count
|
|
189
|
-
deactivate Subscription
|
|
190
|
-
|
|
191
|
-
Note over User,Subscription: 2. Change Phase
|
|
192
|
-
User->>$count: set(5)
|
|
193
|
-
activate $count
|
|
194
|
-
Note over $count: Value changed: 0 → 5
|
|
195
|
-
|
|
196
|
-
$count->>Subscription: Notify & schedule
|
|
197
|
-
deactivate $count
|
|
198
|
-
|
|
199
|
-
activate Subscription
|
|
200
|
-
Note over Subscription: Clear old dependencies<br/>Execute callback
|
|
201
|
-
|
|
202
|
-
Subscription->>$count: Read value
|
|
203
|
-
activate $count
|
|
204
|
-
Note over $count: Register Subscription as dependent
|
|
205
|
-
$count-->>Subscription: 5
|
|
206
|
-
deactivate $count
|
|
207
|
-
|
|
208
|
-
Note over Subscription: "console.log(\"Count: 5\")"
|
|
209
|
-
Note over Subscription: Track: depends on $count
|
|
210
|
-
deactivate Subscription
|
|
211
|
-
|
|
212
|
-
Note over User,Subscription: 3. Disposal Phase
|
|
213
|
-
User->>Subscription: dispose()
|
|
214
|
-
activate Subscription
|
|
215
|
-
Note over Subscription: Clear dependencies<br/>Unregister from $count
|
|
216
|
-
Note over Subscription: Mark as disposed
|
|
217
|
-
deactivate Subscription
|
|
218
|
-
|
|
219
|
-
User->>$count: set(10)
|
|
220
|
-
activate $count
|
|
221
|
-
Note over $count: Value changed<br/>No dependents to notify
|
|
222
|
-
deactivate $count
|
|
223
|
-
Note over Subscription: (Does not run - disposed)
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
### Key Lifecycle Points
|
|
227
|
-
|
|
228
|
-
1. **Creation & Immediate Execution**: The subscription callback runs synchronously during creation
|
|
229
|
-
2. **Dependency Registration**: Reactive reads register the subscription as a dependent
|
|
230
|
-
3. **Change Notification**: When dependencies change, the subscription is scheduled to re-run
|
|
231
|
-
4. **Re-execution**: Subscription clears old dependencies, runs again, re-registers new dependencies
|
|
232
|
-
5. **Disposal**: Clears all dependencies, unregisters from all reactive values, prevents future execution
|
|
233
|
-
|
|
234
|
-
This immediate, synchronous execution model means:
|
|
235
|
-
- **Subscriptions establish dependencies on first run** (no waiting)
|
|
236
|
-
- **Changes trigger re-execution immediately** (synchronous)
|
|
237
|
-
- **Dynamic dependencies adapt automatically** (conditional tracking)
|
|
238
|
-
|
|
239
|
-
## Best Practices
|
|
240
|
-
|
|
241
|
-
### Keep Subscriptions Focused
|
|
242
|
-
|
|
243
|
-
Each subscription should do one thing:
|
|
244
|
-
|
|
245
|
-
```typescript
|
|
246
|
-
// ❌ Doing too much
|
|
247
|
-
$count.subscribe((value) => {
|
|
248
|
-
updateDOM(value)
|
|
249
|
-
saveToStorage(value)
|
|
250
|
-
sendAnalytics(value)
|
|
251
|
-
updateTitle(value)
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
// ✅ Separate concerns
|
|
255
|
-
$count.subscribe((value) => updateDOM(value))
|
|
256
|
-
$count.subscribe((value) => saveToStorage(value))
|
|
257
|
-
$count.subscribe((value) => sendAnalytics(value))
|
|
258
|
-
$count.subscribe((value) => updateTitle(value))
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
**Why?** Easier to debug, test, and disable individual subscriptions.
|
|
262
|
-
|
|
263
|
-
### Avoid Creating Subscriptions Inside Subscriptions
|
|
264
|
-
|
|
265
|
-
```typescript
|
|
266
|
-
// ❌ Bad - creates new subscriptions on every run
|
|
267
|
-
$count.subscribe((count) => {
|
|
268
|
-
$other.subscribe((other) => {
|
|
269
|
-
console.log('Nested:', count, other) // Memory leak!
|
|
270
|
-
})
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
// ✅ Good - create subscriptions at the top level
|
|
274
|
-
subscribe(
|
|
275
|
-
(t) => ({ count: $count.get(t), other: $other.get(t) }),
|
|
276
|
-
(data) => {
|
|
277
|
-
console.log('Values:', data.count, data.other)
|
|
278
|
-
}
|
|
279
|
-
)
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
### Handle Errors
|
|
283
|
-
|
|
284
|
-
Use the `onError` callback or wrap risky operations in try-catch:
|
|
285
|
-
|
|
286
|
-
```typescript
|
|
287
|
-
// ✅ Using onError callback
|
|
288
|
-
subscribe(
|
|
289
|
-
(t) => $data.get(t),
|
|
290
|
-
(data) => {
|
|
291
|
-
updateDOM(data)
|
|
292
|
-
},
|
|
293
|
-
(error) => {
|
|
294
|
-
console.error('Failed to update DOM:', error)
|
|
295
|
-
}
|
|
296
|
-
)
|
|
297
|
-
|
|
298
|
-
// ✅ Or use try-catch
|
|
299
|
-
$data.subscribe((data) => {
|
|
300
|
-
try {
|
|
301
|
-
updateDOM(data)
|
|
302
|
-
} catch (error) {
|
|
303
|
-
console.error('Failed to update DOM:', error)
|
|
304
|
-
}
|
|
305
|
-
})
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
### Be Careful with Infinite Loops
|
|
309
|
-
|
|
310
|
-
Don't update dependencies inside the same subscription:
|
|
311
|
-
|
|
312
|
-
```typescript
|
|
313
|
-
const $count = state(0)
|
|
314
|
-
|
|
315
|
-
// ❌ Infinite loop!
|
|
316
|
-
$count.subscribe((count) => {
|
|
317
|
-
$count.set(count + 1) // Triggers itself!
|
|
318
|
-
})
|
|
319
|
-
|
|
320
|
-
// ✅ Use a different state
|
|
321
|
-
$count.subscribe((count) => {
|
|
322
|
-
$doubled.set(count * 2) // Updates different state
|
|
323
|
-
})
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
### Use Signals for Event-Only Tracking
|
|
327
|
-
|
|
328
|
-
When you don't need a value, just an event:
|
|
329
|
-
|
|
330
|
-
```typescript
|
|
331
|
-
const $refresh = signal()
|
|
332
|
-
|
|
333
|
-
$refresh.subscribe(() => {
|
|
334
|
-
fetchData()
|
|
335
|
-
})
|
|
336
|
-
|
|
337
|
-
// Trigger refresh
|
|
338
|
-
$refresh.trigger()
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
## Common Pitfalls
|
|
342
|
-
|
|
343
|
-
### Not Disposing Subscriptions
|
|
344
|
-
|
|
345
|
-
**Problem**: Subscriptions continue running even when no longer needed.
|
|
346
|
-
|
|
347
|
-
```typescript
|
|
348
|
-
// ❌ Memory leak in component lifecycle
|
|
349
|
-
function MyComponent() {
|
|
350
|
-
$data.subscribe((data) => {
|
|
351
|
-
updateView(data)
|
|
352
|
-
})
|
|
353
|
-
// Never disposed!
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// Each component creation adds another subscription that never stops
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
**Solution**: Always dispose subscriptions when done:
|
|
360
|
-
|
|
361
|
-
```typescript
|
|
362
|
-
// ✅ Proper cleanup
|
|
363
|
-
function MyComponent() {
|
|
364
|
-
const effect = $data.subscribe((data) => {
|
|
365
|
-
updateView(data)
|
|
366
|
-
})
|
|
367
|
-
|
|
368
|
-
return {
|
|
369
|
-
cleanup: () => effect.dispose()
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
```
|
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
# Maps
|
|
2
|
-
|
|
3
|
-
PicoFlow provides reactive maps with **fine-grained tracking**. Instead of reacting to the entire map changing, you can react to specific operations like adding, updating, or deleting individual entries.
|
|
4
|
-
|
|
5
|
-
## When to Use Maps
|
|
6
|
-
|
|
7
|
-
Use maps when you need to:
|
|
8
|
-
|
|
9
|
-
- ✅ Track large collections with fine-grained updates
|
|
10
|
-
- ✅ React to specific operations (additions, updates, deletions) separately
|
|
11
|
-
- ✅ Optimize performance by avoiding full map re-processing
|
|
12
|
-
|
|
13
|
-
Don't use maps when:
|
|
14
|
-
|
|
15
|
-
- ❌ You don't need fine-grained tracking (use `state` with a Record/Map)
|
|
16
|
-
- ❌ You need array-like operations (use `array` instead)
|
|
17
|
-
|
|
18
|
-
## Creating Maps
|
|
19
|
-
|
|
20
|
-
Creating a reactive map is straightforward:
|
|
21
|
-
|
|
22
|
-
```typescript
|
|
23
|
-
import { map } from '@ersbeth/picoflow'
|
|
24
|
-
|
|
25
|
-
// Create with initial values (key is user id, value is User object)
|
|
26
|
-
const $usersByID = map<string, User>({
|
|
27
|
-
aliceID: { name: 'Alice', email: 'alice@example.com', role: 'admin' },
|
|
28
|
-
bobID: { name: 'Bob', email: 'bob@example.com', role: 'user' }
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
// Create empty
|
|
32
|
-
const $cache = map<string, Data>()
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
The initial values (if provided) are converted to a native JavaScript `Map`.
|
|
36
|
-
|
|
37
|
-
## Using Maps
|
|
38
|
-
|
|
39
|
-
Maps provide multiple ways to track changes and perform operations.
|
|
40
|
-
|
|
41
|
-
### Map Operations
|
|
42
|
-
|
|
43
|
-
Maps provide three main operations: `add()`, `update()`, and `delete()`.
|
|
44
|
-
|
|
45
|
-
```typescript
|
|
46
|
-
const $cache = map<string, Data>()
|
|
47
|
-
|
|
48
|
-
// Add a new key-value pair
|
|
49
|
-
$cache.add('user:1', userData)
|
|
50
|
-
|
|
51
|
-
// Update an existing key-value pair
|
|
52
|
-
$cache.update('user:1', updatedUserData)
|
|
53
|
-
|
|
54
|
-
// Delete a key-value pair
|
|
55
|
-
$cache.delete('user:1')
|
|
56
|
-
|
|
57
|
-
// Read (reactive)
|
|
58
|
-
$cache.subscribe((data) => {
|
|
59
|
-
// Track entire map - returns Map<string, Data>
|
|
60
|
-
const hasUser = data.has('user:1')
|
|
61
|
-
if (hasUser) {
|
|
62
|
-
const user = data.get('user:1')
|
|
63
|
-
// Use the user data
|
|
64
|
-
}
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
// Read (non-reactive)
|
|
68
|
-
const snapshot = $cache.pick() // Returns Map<string, Data>
|
|
69
|
-
const user = snapshot.get('user:1')
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
**Important**:
|
|
73
|
-
- `add(key, value)` throws an error if the key already exists
|
|
74
|
-
- `update(key, value)` throws an error if the key doesn't exist
|
|
75
|
-
- `delete(key)` throws an error if the key doesn't exist
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
### Whole Map Tracking
|
|
79
|
-
|
|
80
|
-
Track when the entire map changes (any operation):
|
|
81
|
-
|
|
82
|
-
```typescript
|
|
83
|
-
const $items = map<string, number>()
|
|
84
|
-
|
|
85
|
-
// Initialize with values
|
|
86
|
-
$items.add('a', 1)
|
|
87
|
-
$items.add('b', 2)
|
|
88
|
-
|
|
89
|
-
$items.subscribe((items) => {
|
|
90
|
-
console.log('Map size:', items.size)
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
$items.add('c', 3) // Logs: "Map size: 3"
|
|
94
|
-
$items.delete('a') // Logs: "Map size: 2"
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
Use `.get(t)` when you need the entire map's current state. It returns a native `Map<K, V>`.
|
|
98
|
-
|
|
99
|
-
### Fine-Grained Tracking
|
|
100
|
-
|
|
101
|
-
Track specific operations with `$lastAction`:
|
|
102
|
-
|
|
103
|
-
```typescript
|
|
104
|
-
const $items = map<string, number>()
|
|
105
|
-
|
|
106
|
-
$items.$lastAction.subscribe((action) => {
|
|
107
|
-
if (!action) return
|
|
108
|
-
|
|
109
|
-
switch (action.type) {
|
|
110
|
-
case 'add':
|
|
111
|
-
console.log(`Added ${action.key} = ${action.addedValue}`)
|
|
112
|
-
break
|
|
113
|
-
case 'update':
|
|
114
|
-
console.log(`Updated ${action.key}: ${action.clearedValue} → ${action.setValue}`)
|
|
115
|
-
break
|
|
116
|
-
case 'delete':
|
|
117
|
-
console.log(`Deleted ${action.key}, was: ${action.removedValue}`)
|
|
118
|
-
break
|
|
119
|
-
case 'clear':
|
|
120
|
-
console.log(`Cleared ${action.clearedMap.size} entries`)
|
|
121
|
-
break
|
|
122
|
-
case 'set':
|
|
123
|
-
console.log(`Set to ${action.setMap.size} entries`)
|
|
124
|
-
break
|
|
125
|
-
}
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
$items.add('a', 1) // Logs: "Added a = 1"
|
|
129
|
-
$items.add('b', 2) // Logs: "Added b = 2"
|
|
130
|
-
$items.update('a', 10) // Logs: "Updated a: 1 → 10"
|
|
131
|
-
$items.delete('a') // Logs: "Deleted a, was: 10"
|
|
132
|
-
$items.clear() // Logs: "Cleared 1 entries"
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
## Lifecycle
|
|
136
|
-
|
|
137
|
-
When you create a subscription that tracks a map:
|
|
138
|
-
|
|
139
|
-
1. **Registration**: The map registers the subscription as a watcher
|
|
140
|
-
2. **Operation**: When you call `add()`, `update()`, `delete()`, `set()`, or `clear()`, the map updates internally
|
|
141
|
-
3. **Signal Update**: The `$lastAction` signal is updated with the operation details
|
|
142
|
-
4. **Notification**: All watching subscriptions are scheduled to run
|
|
143
|
-
5. **Re-execution**: Each watching subscription re-executes its function
|
|
144
|
-
|
|
145
|
-
```mermaid
|
|
146
|
-
sequenceDiagram
|
|
147
|
-
participant User
|
|
148
|
-
participant Map as $users (Map)
|
|
149
|
-
participant Signal as $lastAction
|
|
150
|
-
participant Subscription
|
|
151
|
-
|
|
152
|
-
Note over User,Subscription: 1. Setup Phase
|
|
153
|
-
User->>Subscription: Create subscription
|
|
154
|
-
activate Subscription
|
|
155
|
-
Subscription->>Map: get(t)
|
|
156
|
-
Note over Map: Register Subscription as watcher
|
|
157
|
-
Subscription->>Signal: get(t)
|
|
158
|
-
Note over Signal: Register Subscription as watcher
|
|
159
|
-
Subscription->>Subscription: Execute function
|
|
160
|
-
Note over Subscription: Initial render
|
|
161
|
-
deactivate Subscription
|
|
162
|
-
|
|
163
|
-
Note over User,Subscription: 2. Operation Phase
|
|
164
|
-
User->>Map: add('user1', userData)
|
|
165
|
-
activate Map
|
|
166
|
-
Note over Map: Update internal Map
|
|
167
|
-
Map->>Signal: "set({ type: 'add', key, addedValue })"
|
|
168
|
-
activate Signal
|
|
169
|
-
Note over Signal: Notify watchers
|
|
170
|
-
Signal->>Subscription: Schedule execution
|
|
171
|
-
deactivate Signal
|
|
172
|
-
Map->>Map: Notify whole map watchers
|
|
173
|
-
Map->>Subscription: Schedule execution
|
|
174
|
-
deactivate Map
|
|
175
|
-
|
|
176
|
-
activate Subscription
|
|
177
|
-
Note over Subscription: Re-execute function
|
|
178
|
-
Subscription->>Signal: get(t)
|
|
179
|
-
Signal-->>Subscription: "{ type: 'add', key: 'user1', addedValue: userData }"
|
|
180
|
-
Subscription->>Subscription: Handle new user
|
|
181
|
-
Note over Subscription: Update UI
|
|
182
|
-
deactivate Subscription
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
## Common Pitfalls
|
|
186
|
-
|
|
187
|
-
### Using Wrong Tracking
|
|
188
|
-
|
|
189
|
-
**Problem**: Tracking the entire map when you only need specific operations.
|
|
190
|
-
|
|
191
|
-
```typescript
|
|
192
|
-
// ❌ Tracks entire map when you only need additions
|
|
193
|
-
$items.subscribe((items) => {
|
|
194
|
-
const keys = Array.from(items.keys())
|
|
195
|
-
const lastKey = keys[keys.length - 1]
|
|
196
|
-
animateNewItem(lastKey) // Runs on ALL changes
|
|
197
|
-
})
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
**Solution**: Track only what you need:
|
|
201
|
-
|
|
202
|
-
```typescript
|
|
203
|
-
// ✅ Track only additions
|
|
204
|
-
$items.$lastAction.subscribe((action) => {
|
|
205
|
-
if (action && action.type === 'add') {
|
|
206
|
-
animateNewItem(action.key)
|
|
207
|
-
}
|
|
208
|
-
})
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
### Confusing Add and Update
|
|
212
|
-
|
|
213
|
-
**Problem**: Using `add()` when the key might already exist, or `update()` when it might not.
|
|
214
|
-
|
|
215
|
-
```typescript
|
|
216
|
-
// ❌ Will throw if key exists
|
|
217
|
-
function setUser(id: number, user: User) {
|
|
218
|
-
$users.add(id, user) // Error if user already exists!
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// ❌ Will throw if key doesn't exist
|
|
222
|
-
function changeUser(id: number, user: User) {
|
|
223
|
-
$users.update(id, user) // Error if user doesn't exist!
|
|
224
|
-
}
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
**Solution**: Check first or handle errors:
|
|
228
|
-
|
|
229
|
-
```typescript
|
|
230
|
-
// ✅ Check before operation
|
|
231
|
-
function setUser(id: number, user: User) {
|
|
232
|
-
const users = $users.pick()
|
|
233
|
-
if (users.has(id)) {
|
|
234
|
-
$users.update(id, user)
|
|
235
|
-
} else {
|
|
236
|
-
$users.add(id, user)
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
### Mutating Retrieved Maps
|
|
242
|
-
|
|
243
|
-
**Problem**: Mutating the map directly instead of using reactive methods.
|
|
244
|
-
|
|
245
|
-
```typescript
|
|
246
|
-
// ❌ Mutating the map directly
|
|
247
|
-
const items = $items.pick()
|
|
248
|
-
items.set('newKey', newValue) // Doesn't trigger reactivity!
|
|
249
|
-
|
|
250
|
-
// ❌ Mutating the map directly
|
|
251
|
-
const items = $items.get(t)
|
|
252
|
-
items.set('newKey', newValue) // Changes internal state but doesn't notify properly!
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
**Solution**: Use reactive methods:
|
|
256
|
-
|
|
257
|
-
```typescript
|
|
258
|
-
// ✅ Use reactive methods
|
|
259
|
-
const hasItem = $items.pick().has('newKey');
|
|
260
|
-
if (hasItem) {
|
|
261
|
-
$items.update('newKey', newValue)
|
|
262
|
-
} else {
|
|
263
|
-
$items.add('newKey', newValue)
|
|
264
|
-
}
|
|
265
|
-
```
|