@ersbeth/picoflow 1.1.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/settings.json +3 -3
- package/CHANGELOG.md +43 -0
- package/README.md +2 -18
- package/biome.json +45 -35
- package/dist/picoflow.js +856 -1530
- package/dist/types/api/base/flowDisposable.d.ts +41 -0
- package/dist/types/api/base/flowDisposable.d.ts.map +1 -0
- package/dist/types/api/base/flowObservable.d.ts +27 -0
- package/dist/types/api/base/flowObservable.d.ts.map +1 -0
- package/dist/types/api/base/flowSubscribable.d.ts +79 -0
- package/dist/types/api/base/flowSubscribable.d.ts.map +1 -0
- package/dist/types/api/base/flowTracker.d.ts +8 -0
- package/dist/types/api/base/flowTracker.d.ts.map +1 -0
- package/dist/types/api/base/index.d.ts +5 -0
- package/dist/types/api/base/index.d.ts.map +1 -0
- package/dist/types/api/index.d.ts +3 -0
- package/dist/types/api/index.d.ts.map +1 -0
- package/dist/types/api/nodes/async/flowConstantAsync.d.ts +31 -0
- package/dist/types/api/nodes/async/flowConstantAsync.d.ts.map +1 -0
- package/dist/types/api/nodes/async/flowDerivationAsync.d.ts +37 -0
- package/dist/types/api/nodes/async/flowDerivationAsync.d.ts.map +1 -0
- package/dist/types/api/nodes/async/flowStateAsync.d.ts +41 -0
- package/dist/types/api/nodes/async/flowStateAsync.d.ts.map +1 -0
- package/dist/types/api/nodes/async/flowWritableDerivationAsync.d.ts +30 -0
- package/dist/types/api/nodes/async/flowWritableDerivationAsync.d.ts.map +1 -0
- package/dist/types/{flow → api}/nodes/async/index.d.ts +1 -2
- package/dist/types/api/nodes/async/index.d.ts.map +1 -0
- package/dist/types/api/nodes/collections/flowArray.d.ts +134 -0
- package/dist/types/api/nodes/collections/flowArray.d.ts.map +1 -0
- package/dist/types/api/nodes/collections/flowMap.d.ts +98 -0
- package/dist/types/api/nodes/collections/flowMap.d.ts.map +1 -0
- package/dist/types/api/nodes/collections/index.d.ts.map +1 -0
- package/dist/types/api/nodes/flowEffect.d.ts +28 -0
- package/dist/types/api/nodes/flowEffect.d.ts.map +1 -0
- package/dist/types/api/nodes/flowSignal.d.ts +25 -0
- package/dist/types/api/nodes/flowSignal.d.ts.map +1 -0
- package/dist/types/api/nodes/flowValue.d.ts +35 -0
- package/dist/types/api/nodes/flowValue.d.ts.map +1 -0
- package/dist/types/api/nodes/index.d.ts +8 -0
- package/dist/types/api/nodes/index.d.ts.map +1 -0
- package/dist/types/api/nodes/sync/flowConstant.d.ts +29 -0
- package/dist/types/api/nodes/sync/flowConstant.d.ts.map +1 -0
- package/dist/types/api/nodes/sync/flowDerivation.d.ts +36 -0
- package/dist/types/api/nodes/sync/flowDerivation.d.ts.map +1 -0
- package/dist/types/api/nodes/sync/flowState.d.ts +39 -0
- package/dist/types/api/nodes/sync/flowState.d.ts.map +1 -0
- package/dist/types/api/nodes/sync/flowWritableDerivation.d.ts +28 -0
- package/dist/types/api/nodes/sync/flowWritableDerivation.d.ts.map +1 -0
- package/dist/types/{flow → api}/nodes/sync/index.d.ts +1 -2
- package/dist/types/api/nodes/sync/index.d.ts.map +1 -0
- package/dist/types/api/nodes/utils.d.ts +22 -0
- package/dist/types/api/nodes/utils.d.ts.map +1 -0
- package/dist/types/base/disposable.d.ts +11 -0
- package/dist/types/base/disposable.d.ts.map +1 -0
- package/dist/types/base/executionStack.d.ts +14 -0
- package/dist/types/base/executionStack.d.ts.map +1 -0
- package/dist/types/base/index.d.ts +6 -0
- package/dist/types/base/index.d.ts.map +1 -0
- package/dist/types/base/node.d.ts +27 -0
- package/dist/types/base/node.d.ts.map +1 -0
- package/dist/types/base/observable.d.ts +37 -0
- package/dist/types/base/observable.d.ts.map +1 -0
- package/dist/types/base/observer.d.ts +25 -0
- package/dist/types/base/observer.d.ts.map +1 -0
- package/dist/types/converters/index.d.ts +2 -0
- package/dist/types/converters/index.d.ts.map +1 -0
- package/dist/types/converters/solid.d.ts +46 -0
- package/dist/types/converters/solid.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -63
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/nodes/arrayNode.d.ts +2 -0
- package/dist/types/nodes/arrayNode.d.ts.map +1 -0
- package/dist/types/nodes/effectNode.d.ts +2 -0
- package/dist/types/nodes/effectNode.d.ts.map +1 -0
- package/dist/types/nodes/index.d.ts +9 -0
- package/dist/types/nodes/index.d.ts.map +1 -0
- package/dist/types/nodes/mapNode.d.ts +2 -0
- package/dist/types/nodes/mapNode.d.ts.map +1 -0
- package/dist/types/nodes/signalNode.d.ts +2 -0
- package/dist/types/nodes/signalNode.d.ts.map +1 -0
- package/dist/types/nodes/valueAsyncNode.d.ts +2 -0
- package/dist/types/nodes/valueAsyncNode.d.ts.map +1 -0
- package/dist/types/nodes/valueNode.d.ts +2 -0
- package/dist/types/nodes/valueNode.d.ts.map +1 -0
- package/dist/types/nodes/valueSyncNode.d.ts +2 -0
- package/dist/types/nodes/valueSyncNode.d.ts.map +1 -0
- package/dist/types/schedulers/asyncResolver.d.ts +2 -0
- package/dist/types/schedulers/asyncResolver.d.ts.map +1 -0
- package/dist/types/schedulers/asyncScheduler.d.ts +2 -0
- package/dist/types/schedulers/asyncScheduler.d.ts.map +1 -0
- package/dist/types/schedulers/index.d.ts +5 -0
- package/dist/types/schedulers/index.d.ts.map +1 -0
- package/dist/types/schedulers/pendingError.d.ts +2 -0
- package/dist/types/schedulers/pendingError.d.ts.map +1 -0
- package/dist/types/schedulers/scheduler.d.ts +2 -0
- package/dist/types/schedulers/scheduler.d.ts.map +1 -0
- package/dist/types/schedulers/syncResolver.d.ts +2 -0
- package/dist/types/schedulers/syncResolver.d.ts.map +1 -0
- package/dist/types/schedulers/syncScheduler.d.ts +2 -0
- package/dist/types/schedulers/syncScheduler.d.ts.map +1 -0
- package/docs/.vitepress/config.mts +128 -93
- package/docs/api/functions/array.md +14 -37
- package/docs/api/functions/constant.md +13 -25
- package/docs/api/functions/constantAsync.md +69 -0
- package/docs/api/functions/derivation.md +14 -33
- package/docs/api/functions/derivationAsync.md +34 -0
- package/docs/api/functions/from.md +62 -153
- package/docs/api/functions/isDisposable.md +8 -30
- package/docs/api/functions/map.md +15 -36
- package/docs/api/functions/signal.md +8 -23
- package/docs/api/functions/state.md +43 -23
- package/docs/api/functions/stateAsync.md +69 -0
- package/docs/api/functions/subscribe.md +40 -0
- package/docs/api/functions/writableDerivation.md +33 -0
- package/docs/api/functions/writableDerivationAsync.md +34 -0
- package/docs/api/index.md +45 -102
- package/docs/api/interfaces/FlowArray.md +439 -0
- package/docs/api/interfaces/FlowConstant.md +220 -0
- package/docs/api/interfaces/FlowConstantAsync.md +221 -0
- package/docs/api/interfaces/FlowDerivation.md +241 -0
- package/docs/api/interfaces/FlowDerivationAsync.md +242 -0
- package/docs/api/interfaces/FlowDisposable.md +32 -38
- package/docs/api/interfaces/FlowEffect.md +64 -0
- package/docs/api/interfaces/FlowMap.md +374 -0
- package/docs/api/interfaces/FlowObservable.md +155 -0
- package/docs/api/interfaces/FlowSignal.md +156 -0
- package/docs/api/interfaces/FlowState.md +269 -0
- package/docs/api/interfaces/FlowStateAsync.md +268 -0
- package/docs/api/interfaces/FlowSubscribable.md +55 -0
- package/docs/api/interfaces/FlowTracker.md +61 -0
- package/docs/api/interfaces/FlowValue.md +222 -0
- package/docs/api/interfaces/FlowWritableDerivation.md +292 -0
- package/docs/api/interfaces/FlowWritableDerivationAsync.md +293 -0
- package/docs/api/type-aliases/DerivationFunction.md +28 -0
- package/docs/api/type-aliases/DerivationFunctionAsync.md +28 -0
- package/docs/api/type-aliases/FlowArrayAction.md +19 -8
- package/docs/api/type-aliases/FlowDataTracker.md +33 -0
- package/docs/api/type-aliases/FlowMapAction.md +48 -0
- package/docs/api/type-aliases/FlowOnDataListener.md +33 -0
- package/docs/api/type-aliases/FlowOnErrorListener.md +27 -0
- package/docs/api/type-aliases/FlowOnPendingListener.md +21 -0
- package/docs/api/type-aliases/FlowReadonly.md +22 -0
- package/docs/api/type-aliases/InitFunction.md +21 -0
- package/docs/api/type-aliases/InitFunctionAsync.md +21 -0
- package/docs/api/type-aliases/NotPromise.md +6 -3
- package/docs/api/type-aliases/UpdateFunction.md +27 -0
- package/docs/api/type-aliases/UpdateFunctionAsync.md +27 -0
- package/docs/api/typedoc-sidebar.json +1 -81
- package/docs/examples/examples.md +0 -2
- package/docs/guide/advanced/architecture.md +1234 -0
- package/docs/guide/advanced/migration-v2.md +204 -0
- package/docs/guide/advanced/solidjs.md +2 -88
- package/docs/guide/introduction/concepts.md +4 -3
- package/docs/guide/introduction/conventions.md +2 -33
- package/docs/guide/introduction/getting-started.md +28 -23
- package/docs/guide/introduction/lifecycle.md +16 -19
- package/docs/guide/primitives/array.md +102 -216
- package/docs/guide/primitives/constant.md +39 -212
- package/docs/guide/primitives/derivations.md +55 -122
- package/docs/guide/primitives/effects.md +155 -241
- package/docs/guide/primitives/map.md +64 -186
- package/docs/guide/primitives/overview.md +45 -128
- package/docs/guide/primitives/signal.md +51 -88
- package/docs/guide/primitives/state.md +34 -130
- package/package.json +56 -60
- package/src/api/base/flowDisposable.ts +44 -0
- package/src/api/base/flowObservable.ts +28 -0
- package/src/api/base/flowSubscribable.ts +87 -0
- package/src/api/base/flowTracker.ts +7 -0
- package/src/api/base/index.ts +4 -0
- package/src/{flow → api}/index.ts +0 -1
- package/src/api/nodes/async/flowConstantAsync.ts +36 -0
- package/src/api/nodes/async/flowDerivationAsync.ts +42 -0
- package/src/api/nodes/async/flowStateAsync.ts +47 -0
- package/src/api/nodes/async/flowWritableDerivationAsync.ts +33 -0
- package/src/{flow → api}/nodes/async/index.ts +1 -2
- package/src/api/nodes/collections/flowArray.ts +155 -0
- package/src/api/nodes/collections/flowMap.ts +115 -0
- package/src/api/nodes/flowEffect.ts +42 -0
- package/src/api/nodes/flowSignal.ts +28 -0
- package/src/api/nodes/flowValue.ts +36 -0
- package/src/api/nodes/index.ts +7 -0
- package/src/api/nodes/sync/flowConstant.ts +33 -0
- package/src/api/nodes/sync/flowDerivation.ts +41 -0
- package/src/api/nodes/sync/flowState.ts +45 -0
- package/src/api/nodes/sync/flowWritableDerivation.ts +31 -0
- package/src/{flow → api}/nodes/sync/index.ts +1 -2
- package/src/api/nodes/utils.ts +22 -0
- package/src/base/disposable.ts +18 -0
- package/src/base/executionStack.ts +42 -0
- package/src/base/index.ts +5 -0
- package/src/base/node.ts +98 -0
- package/src/base/observable.ts +87 -0
- package/src/base/observer.ts +51 -0
- package/src/converters/index.ts +1 -0
- package/src/converters/solid.ts +109 -0
- package/src/index.ts +2 -64
- package/src/nodes/arrayNode.ts +172 -0
- package/src/nodes/effectNode.ts +59 -0
- package/src/nodes/index.ts +8 -0
- package/src/nodes/mapNode.ts +127 -0
- package/src/nodes/signalNode.ts +21 -0
- package/src/nodes/valueAsyncNode.ts +88 -0
- package/src/nodes/valueNode.ts +144 -0
- package/src/nodes/valueSyncNode.ts +128 -0
- package/src/schedulers/asyncResolver.ts +78 -0
- package/src/schedulers/asyncScheduler.ts +66 -0
- package/src/schedulers/index.ts +4 -0
- package/src/schedulers/pendingError.ts +13 -0
- package/src/schedulers/scheduler.ts +9 -0
- package/src/schedulers/syncResolver.ts +69 -0
- package/src/schedulers/syncScheduler.ts +55 -0
- package/test/base/pendingError.test.ts +67 -0
- package/test/converters/solid.derivation.browser.test.tsx +69 -0
- package/test/converters/solid.node.test.ts +654 -0
- package/test/converters/solid.state.browser.test.tsx +1592 -0
- package/test/reactivity/flowSignal.test.ts +226 -0
- package/test/reactivity/nodes/async/asyncScheduler/asyncResolver.test.ts +593 -0
- package/test/reactivity/nodes/async/asyncScheduler/asyncScheduler.test.ts +317 -0
- package/test/reactivity/nodes/async/flowConstantAsync.test.ts +652 -0
- package/test/reactivity/nodes/async/flowDerivation.test.ts +898 -0
- package/test/reactivity/nodes/async/flowDerivationAsync.test.ts +1716 -0
- package/test/reactivity/nodes/async/flowStateAsync.test.ts +708 -0
- package/test/reactivity/nodes/async/flowWritableDerivationAsync.test.ts +614 -0
- package/test/reactivity/nodes/collections/flowArray.asyncStates.test.ts +1289 -0
- package/test/reactivity/nodes/collections/flowArray.scalars.test.ts +961 -0
- package/test/reactivity/nodes/collections/flowArray.states.test.ts +1035 -0
- package/test/reactivity/nodes/collections/flowMap.asyncStates.test.ts +960 -0
- package/test/reactivity/nodes/collections/flowMap.scalars.test.ts +775 -0
- package/test/reactivity/nodes/collections/flowMap.states.test.ts +958 -0
- package/test/reactivity/nodes/sync/flowConstant.test.ts +377 -0
- package/test/reactivity/nodes/sync/flowDerivation.test.ts +896 -0
- package/test/reactivity/nodes/sync/flowState.test.ts +341 -0
- package/test/reactivity/nodes/sync/flowWritableDerivation.test.ts +603 -0
- package/test/vitest.d.ts +10 -0
- package/tsconfig.json +31 -20
- package/typedoc.json +35 -35
- package/vite.config.ts +25 -23
- package/vitest.browser.config.ts +21 -0
- package/vitest.config.ts +12 -12
- package/.cursor/plans/unifier-flowresource-avec-flowderivation-c9506e24.plan.md +0 -372
- package/.cursor/plans/update-js-e795d61b.plan.md +0 -567
- package/dist/types/flow/base/flowDisposable.d.ts +0 -67
- package/dist/types/flow/base/flowDisposable.d.ts.map +0 -1
- package/dist/types/flow/base/flowEffect.d.ts +0 -127
- package/dist/types/flow/base/flowEffect.d.ts.map +0 -1
- package/dist/types/flow/base/flowGraph.d.ts +0 -97
- package/dist/types/flow/base/flowGraph.d.ts.map +0 -1
- package/dist/types/flow/base/flowSignal.d.ts +0 -134
- package/dist/types/flow/base/flowSignal.d.ts.map +0 -1
- package/dist/types/flow/base/flowTracker.d.ts +0 -15
- package/dist/types/flow/base/flowTracker.d.ts.map +0 -1
- package/dist/types/flow/base/index.d.ts +0 -7
- package/dist/types/flow/base/index.d.ts.map +0 -1
- package/dist/types/flow/base/utils.d.ts +0 -20
- package/dist/types/flow/base/utils.d.ts.map +0 -1
- package/dist/types/flow/collections/flowArray.d.ts +0 -148
- package/dist/types/flow/collections/flowArray.d.ts.map +0 -1
- package/dist/types/flow/collections/flowMap.d.ts +0 -224
- package/dist/types/flow/collections/flowMap.d.ts.map +0 -1
- package/dist/types/flow/collections/index.d.ts.map +0 -1
- package/dist/types/flow/index.d.ts +0 -4
- package/dist/types/flow/index.d.ts.map +0 -1
- package/dist/types/flow/nodes/async/flowConstantAsync.d.ts +0 -137
- package/dist/types/flow/nodes/async/flowConstantAsync.d.ts.map +0 -1
- package/dist/types/flow/nodes/async/flowDerivationAsync.d.ts +0 -137
- package/dist/types/flow/nodes/async/flowDerivationAsync.d.ts.map +0 -1
- package/dist/types/flow/nodes/async/flowNodeAsync.d.ts +0 -343
- package/dist/types/flow/nodes/async/flowNodeAsync.d.ts.map +0 -1
- package/dist/types/flow/nodes/async/flowReadonlyAsync.d.ts +0 -81
- package/dist/types/flow/nodes/async/flowReadonlyAsync.d.ts.map +0 -1
- package/dist/types/flow/nodes/async/flowStateAsync.d.ts +0 -111
- package/dist/types/flow/nodes/async/flowStateAsync.d.ts.map +0 -1
- package/dist/types/flow/nodes/async/index.d.ts.map +0 -1
- package/dist/types/flow/nodes/index.d.ts +0 -3
- package/dist/types/flow/nodes/index.d.ts.map +0 -1
- package/dist/types/flow/nodes/sync/flowConstant.d.ts +0 -108
- package/dist/types/flow/nodes/sync/flowConstant.d.ts.map +0 -1
- package/dist/types/flow/nodes/sync/flowDerivation.d.ts +0 -100
- package/dist/types/flow/nodes/sync/flowDerivation.d.ts.map +0 -1
- package/dist/types/flow/nodes/sync/flowNode.d.ts +0 -314
- package/dist/types/flow/nodes/sync/flowNode.d.ts.map +0 -1
- package/dist/types/flow/nodes/sync/flowReadonly.d.ts +0 -57
- package/dist/types/flow/nodes/sync/flowReadonly.d.ts.map +0 -1
- package/dist/types/flow/nodes/sync/flowState.d.ts +0 -96
- package/dist/types/flow/nodes/sync/flowState.d.ts.map +0 -1
- package/dist/types/flow/nodes/sync/index.d.ts.map +0 -1
- package/dist/types/solid/converters.d.ts +0 -57
- package/dist/types/solid/converters.d.ts.map +0 -1
- package/dist/types/solid/index.d.ts +0 -3
- package/dist/types/solid/index.d.ts.map +0 -1
- package/dist/types/solid/primitives.d.ts +0 -181
- package/dist/types/solid/primitives.d.ts.map +0 -1
- package/docs/api/classes/FlowArray.md +0 -489
- package/docs/api/classes/FlowConstant.md +0 -350
- package/docs/api/classes/FlowDerivation.md +0 -334
- package/docs/api/classes/FlowEffect.md +0 -100
- package/docs/api/classes/FlowMap.md +0 -512
- package/docs/api/classes/FlowObservable.md +0 -306
- package/docs/api/classes/FlowResource.md +0 -380
- package/docs/api/classes/FlowResourceAsync.md +0 -362
- package/docs/api/classes/FlowSignal.md +0 -160
- package/docs/api/classes/FlowState.md +0 -368
- package/docs/api/classes/FlowStream.md +0 -367
- package/docs/api/classes/FlowStreamAsync.md +0 -364
- package/docs/api/classes/SolidDerivation.md +0 -75
- package/docs/api/classes/SolidResource.md +0 -91
- package/docs/api/classes/SolidState.md +0 -71
- package/docs/api/classes/TrackingContext.md +0 -33
- package/docs/api/functions/effect.md +0 -49
- package/docs/api/functions/resource.md +0 -52
- package/docs/api/functions/resourceAsync.md +0 -50
- package/docs/api/functions/stream.md +0 -53
- package/docs/api/functions/streamAsync.md +0 -50
- package/docs/api/interfaces/SolidObservable.md +0 -19
- package/docs/api/type-aliases/FlowStreamDisposer.md +0 -15
- package/docs/api/type-aliases/FlowStreamSetter.md +0 -27
- package/docs/api/type-aliases/FlowStreamUpdater.md +0 -32
- package/docs/api/type-aliases/SolidGetter.md +0 -17
- package/docs/guide/primitives/resources.md +0 -858
- package/docs/guide/primitives/streams.md +0 -931
- package/src/flow/base/flowDisposable.ts +0 -71
- package/src/flow/base/flowEffect.ts +0 -171
- package/src/flow/base/flowGraph.ts +0 -288
- package/src/flow/base/flowSignal.ts +0 -207
- package/src/flow/base/flowTracker.ts +0 -17
- package/src/flow/base/index.ts +0 -6
- package/src/flow/base/utils.ts +0 -19
- package/src/flow/collections/flowArray.ts +0 -409
- package/src/flow/collections/flowMap.ts +0 -398
- package/src/flow/nodes/async/flowConstantAsync.ts +0 -142
- package/src/flow/nodes/async/flowDerivationAsync.ts +0 -143
- package/src/flow/nodes/async/flowNodeAsync.ts +0 -474
- package/src/flow/nodes/async/flowReadonlyAsync.ts +0 -81
- package/src/flow/nodes/async/flowStateAsync.ts +0 -116
- package/src/flow/nodes/await/advanced/index.ts +0 -5
- package/src/flow/nodes/await/advanced/resource.ts +0 -134
- package/src/flow/nodes/await/advanced/resourceAsync.ts +0 -109
- package/src/flow/nodes/await/advanced/stream.ts +0 -188
- package/src/flow/nodes/await/advanced/streamAsync.ts +0 -176
- package/src/flow/nodes/await/flowConstantAwait.ts +0 -154
- package/src/flow/nodes/await/flowDerivationAwait.ts +0 -154
- package/src/flow/nodes/await/flowNodeAwait.ts +0 -508
- package/src/flow/nodes/await/flowReadonlyAwait.ts +0 -89
- package/src/flow/nodes/await/flowStateAwait.ts +0 -130
- package/src/flow/nodes/await/index.ts +0 -5
- package/src/flow/nodes/index.ts +0 -3
- package/src/flow/nodes/sync/flowConstant.ts +0 -111
- package/src/flow/nodes/sync/flowDerivation.ts +0 -105
- package/src/flow/nodes/sync/flowNode.ts +0 -439
- package/src/flow/nodes/sync/flowReadonly.ts +0 -57
- package/src/flow/nodes/sync/flowState.ts +0 -101
- package/src/solid/converters.ts +0 -148
- package/src/solid/index.ts +0 -2
- package/src/solid/primitives.ts +0 -215
- package/test/base/flowEffect.test.ts +0 -108
- package/test/base/flowGraph.test.ts +0 -485
- package/test/base/flowSignal.test.ts +0 -372
- package/test/collections/flowArray.asyncStates.test.ts +0 -1553
- package/test/collections/flowArray.scalars.test.ts +0 -1129
- package/test/collections/flowArray.states.test.ts +0 -1365
- package/test/collections/flowMap.asyncStates.test.ts +0 -1105
- package/test/collections/flowMap.scalars.test.ts +0 -877
- package/test/collections/flowMap.states.test.ts +0 -1097
- package/test/nodes/async/flowConstantAsync.test.ts +0 -860
- package/test/nodes/async/flowDerivationAsync.test.ts +0 -1517
- package/test/nodes/async/flowStateAsync.test.ts +0 -1387
- package/test/nodes/await/advanced/resource.test.ts +0 -129
- package/test/nodes/await/advanced/resourceAsync.test.ts +0 -108
- package/test/nodes/await/advanced/stream.test.ts +0 -198
- package/test/nodes/await/advanced/streamAsync.test.ts +0 -196
- package/test/nodes/await/flowConstantAwait.test.ts +0 -643
- package/test/nodes/await/flowDerivationAwait.test.ts +0 -1583
- package/test/nodes/await/flowStateAwait.test.ts +0 -999
- package/test/nodes/mixed/derivation.test.ts +0 -1527
- package/test/nodes/sync/flowConstant.test.ts +0 -620
- package/test/nodes/sync/flowDerivation.test.ts +0 -1373
- package/test/nodes/sync/flowState.test.ts +0 -945
- package/test/solid/converters.test.ts +0 -721
- package/test/solid/primitives.test.ts +0 -1031
- /package/dist/types/{flow → api/nodes}/collections/index.d.ts +0 -0
- /package/docs/guide/advanced/{upgrading.md → migration-v1.md} +0 -0
- /package/src/{flow → api/nodes}/collections/index.ts +0 -0
|
@@ -0,0 +1,1234 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
This document provides a deep dive into PicoFlow's internal architecture. Whether you're a curious user wanting to understand how things work under the hood, or a potential contributor looking to get oriented before diving into the source code, this guide will help you understand the core concepts, design patterns, and implementation details that make PicoFlow tick.
|
|
4
|
+
|
|
5
|
+
## Why Understanding Architecture Matters
|
|
6
|
+
|
|
7
|
+
Understanding PicoFlow's architecture helps you:
|
|
8
|
+
|
|
9
|
+
- **Debug effectively** - Know where to look when things go wrong
|
|
10
|
+
- **Optimize performance** - Understand the performance implications of different patterns
|
|
11
|
+
- **Contribute confidently** - Navigate the codebase with a mental model
|
|
12
|
+
- **Make better design decisions** - Use PicoFlow in ways that align with its design
|
|
13
|
+
|
|
14
|
+
## Document Organization
|
|
15
|
+
|
|
16
|
+
This guide progresses from high-level concepts to implementation details:
|
|
17
|
+
|
|
18
|
+
1. **Architecture Overview** - The layered structure
|
|
19
|
+
2. **Core Concepts** - Fundamental patterns and ideas
|
|
20
|
+
3. **Layer Deep Dives** - Detailed exploration of each layer
|
|
21
|
+
4. **Design Patterns** - Key patterns used throughout
|
|
22
|
+
5. **Data Flows** - How data moves through the system
|
|
23
|
+
6. **For Contributors** - Practical guidance for contributing
|
|
24
|
+
|
|
25
|
+
## Architecture Overview
|
|
26
|
+
|
|
27
|
+
PicoFlow follows a **layered architecture** with clear separation of concerns. Each layer has a specific responsibility and builds upon the layer below it.
|
|
28
|
+
|
|
29
|
+
```mermaid
|
|
30
|
+
graph TB
|
|
31
|
+
API["Public API Layer<br/>(src/api/)<br/>────────────────<br/>Factory functions & Interfaces<br/>state(), derivation(), etc."]
|
|
32
|
+
Nodes["Nodes Layer<br/>(src/nodes/)<br/>────────────────<br/>Concrete implementations<br/>ValueSyncNode, EffectNode, etc."]
|
|
33
|
+
Base["Base Layer<br/>(src/base/)<br/>────────────────<br/>Abstract classes & patterns<br/>Observable, Observer, Node"]
|
|
34
|
+
Schedulers["Schedulers Layer<br/>(src/schedulers/)<br/>────────────────<br/>Computation lifecycle<br/>SyncScheduler, AsyncScheduler"]
|
|
35
|
+
|
|
36
|
+
API --> Nodes
|
|
37
|
+
Nodes --> Base
|
|
38
|
+
Nodes --> Schedulers
|
|
39
|
+
Base --> Schedulers
|
|
40
|
+
|
|
41
|
+
style API fill:#e1f5ff
|
|
42
|
+
style Nodes fill:#fff4e1
|
|
43
|
+
style Base fill:#ffe1f5
|
|
44
|
+
style Schedulers fill:#e1ffe1
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Layer Responsibilities
|
|
48
|
+
|
|
49
|
+
**Public API Layer** (`src/api/`)
|
|
50
|
+
- Factory functions that users call (`state()`, `derivation()`, etc.)
|
|
51
|
+
- TypeScript interfaces that define contracts (`FlowState`, `FlowObservable`)
|
|
52
|
+
- Hides implementation details from users
|
|
53
|
+
- Provides a stable, semantic API
|
|
54
|
+
|
|
55
|
+
**Nodes Layer** (`src/nodes/`)
|
|
56
|
+
- Concrete implementations of reactive primitives
|
|
57
|
+
- Each primitive type has its own class (`ValueSyncNode`, `EffectNode`, etc.)
|
|
58
|
+
- Manages the reactive value lifecycle
|
|
59
|
+
- Coordinates between Base and Schedulers
|
|
60
|
+
|
|
61
|
+
**Base Layer** (`src/base/`)
|
|
62
|
+
- Abstract classes implementing core patterns
|
|
63
|
+
- Observable/Observer pattern implementation
|
|
64
|
+
- Dependency graph management
|
|
65
|
+
- Execution batching and ordering
|
|
66
|
+
|
|
67
|
+
**Schedulers Layer** (`src/schedulers/`)
|
|
68
|
+
- Manages computation execution lifecycle
|
|
69
|
+
- Handles synchronous vs asynchronous execution
|
|
70
|
+
- Prevents race conditions with iteration tracking
|
|
71
|
+
- Provides consistent promise settling behavior
|
|
72
|
+
|
|
73
|
+
## Core Concepts
|
|
74
|
+
|
|
75
|
+
### The Observer Pattern
|
|
76
|
+
|
|
77
|
+
PicoFlow is built on the **Observer Pattern**, where objects (observables) can notify other objects (observers) when their state changes.
|
|
78
|
+
|
|
79
|
+
```mermaid
|
|
80
|
+
graph LR
|
|
81
|
+
Observable["Observable<br/>(can be watched)"]
|
|
82
|
+
Observer1["Observer 1<br/>(watches changes)"]
|
|
83
|
+
Observer2["Observer 2<br/>(watches changes)"]
|
|
84
|
+
Observer3["Observer 3<br/>(watches changes)"]
|
|
85
|
+
|
|
86
|
+
Observable -->|notifies| Observer1
|
|
87
|
+
Observable -->|notifies| Observer2
|
|
88
|
+
Observable -->|notifies| Observer3
|
|
89
|
+
|
|
90
|
+
style Observable fill:#ffeb99
|
|
91
|
+
style Observer1 fill:#99e1ff
|
|
92
|
+
style Observer2 fill:#99e1ff
|
|
93
|
+
style Observer3 fill:#99e1ff
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Key insight**: In PicoFlow, most primitives are *both* observable AND observer. A derivation observes a state (it's an observer), but can itself be observed by an effect (it's an observable).
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// $count is an Observable
|
|
100
|
+
const $count = state(0)
|
|
101
|
+
|
|
102
|
+
// $doubled is both:
|
|
103
|
+
// - An Observer (observes $count)
|
|
104
|
+
// - An Observable (can be observed by others)
|
|
105
|
+
const $doubled = derivation((t) => $count.get(t) * 2)
|
|
106
|
+
|
|
107
|
+
// effect is an Observer
|
|
108
|
+
const fx = subscribe(
|
|
109
|
+
(t) => $doubled.get(t),
|
|
110
|
+
(value) => console.log(value)
|
|
111
|
+
)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Dependency Graph
|
|
115
|
+
|
|
116
|
+
Primitives form a **directed acyclic graph (DAG)** of dependencies:
|
|
117
|
+
|
|
118
|
+
```mermaid
|
|
119
|
+
graph TB
|
|
120
|
+
State1["state(0)<br/>Observable"]
|
|
121
|
+
State2["state(1)<br/>Observable"]
|
|
122
|
+
Deriv["derivation()<br/>Observer + Observable"]
|
|
123
|
+
Effect["effect()<br/>Observer"]
|
|
124
|
+
|
|
125
|
+
State1 --> Deriv
|
|
126
|
+
State2 --> Deriv
|
|
127
|
+
Deriv --> Effect
|
|
128
|
+
|
|
129
|
+
style State1 fill:#ffe1e1
|
|
130
|
+
style State2 fill:#ffe1e1
|
|
131
|
+
style Deriv fill:#e1ffe1
|
|
132
|
+
style Effect fill:#e1e1ff
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
When a state changes:
|
|
136
|
+
1. It notifies its dependents (the derivation)
|
|
137
|
+
2. The derivation recomputes and notifies its dependents (the effect)
|
|
138
|
+
3. The effect re-executes
|
|
139
|
+
|
|
140
|
+
This creates a **push-based** reactive system where changes automatically propagate through the graph.
|
|
141
|
+
|
|
142
|
+
### Reactive Tracking
|
|
143
|
+
|
|
144
|
+
How does PicoFlow know which primitives depend on which? Through **automatic dependency tracking**.
|
|
145
|
+
|
|
146
|
+
When you call `get(tracker)` on a primitive, it registers itself as a dependency:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
const $count = state(0)
|
|
150
|
+
|
|
151
|
+
// When this derivation executes...
|
|
152
|
+
const $doubled = derivation((tracker) => {
|
|
153
|
+
// ...this get() call automatically registers $count as a dependency
|
|
154
|
+
return $count.get(tracker) * 2
|
|
155
|
+
})
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**The FlowTracker** is the key. It's passed to all computation functions and used to record dependencies:
|
|
159
|
+
|
|
160
|
+
```mermaid
|
|
161
|
+
sequenceDiagram
|
|
162
|
+
participant D as Derivation
|
|
163
|
+
participant T as Tracker
|
|
164
|
+
participant S as State
|
|
165
|
+
|
|
166
|
+
D->>D: execute()
|
|
167
|
+
D->>S: get(tracker)
|
|
168
|
+
S->>T: registerDependency(this)
|
|
169
|
+
T->>T: Add state to dependencies
|
|
170
|
+
S->>D: Return value
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
This automatic tracking means you don't manually wire up subscriptions - just read values and PicoFlow handles the rest.
|
|
174
|
+
|
|
175
|
+
## Base Layer (src/base/)
|
|
176
|
+
|
|
177
|
+
The base layer provides the fundamental abstractions that all reactive primitives build upon.
|
|
178
|
+
|
|
179
|
+
### Disposable
|
|
180
|
+
|
|
181
|
+
The simplest building block - explicit resource management:
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
abstract class Disposable {
|
|
185
|
+
protected _disposed = false
|
|
186
|
+
|
|
187
|
+
get disposed(): boolean {
|
|
188
|
+
return this._disposed
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
dispose(): void {
|
|
192
|
+
if (this._disposed) throw new Error("Already disposed")
|
|
193
|
+
this._disposed = true
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Why this matters**: Every PicoFlow primitive is disposable. This prevents memory leaks by allowing explicit cleanup of subscriptions and resources.
|
|
199
|
+
|
|
200
|
+
### Observable
|
|
201
|
+
|
|
202
|
+
Manages a list of dependents and notifies them of changes:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
abstract class Observable<T> extends Disposable {
|
|
206
|
+
private _dependents = new Set<IObserver>()
|
|
207
|
+
|
|
208
|
+
registerDependent(dependent: IObserver): void {
|
|
209
|
+
this._dependents.add(dependent)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
notifyDependents(): void {
|
|
213
|
+
this._dependents.forEach(dep => dep.notify())
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
watch(tracker: FlowTracker): void {
|
|
217
|
+
(tracker as IObserver).registerDependency(this)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Key methods**:
|
|
223
|
+
- `watch()` - Register this observable as a dependency of a tracker
|
|
224
|
+
- `trigger()` - Manually notify all dependents
|
|
225
|
+
- `notifyDependents()` - Internal method to propagate changes
|
|
226
|
+
|
|
227
|
+
### Observer
|
|
228
|
+
|
|
229
|
+
Manages a list of dependencies and reacts to changes:
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
abstract class Observer extends Disposable {
|
|
233
|
+
private _dependencies = new Set<IObservable<unknown>>()
|
|
234
|
+
|
|
235
|
+
registerDependency(dependency: IObservable<unknown>): void {
|
|
236
|
+
this._dependencies.add(dependency)
|
|
237
|
+
dependency.registerDependent(this)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
clearDependencies(): void {
|
|
241
|
+
this._dependencies.forEach(dep =>
|
|
242
|
+
dep.unregisterDependent(this)
|
|
243
|
+
)
|
|
244
|
+
this._dependencies.clear()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
abstract notify(): void
|
|
248
|
+
abstract execute(): void
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**Key methods**:
|
|
253
|
+
- `registerDependency()` - Add a new dependency
|
|
254
|
+
- `clearDependencies()` - Clear all dependencies (before recomputing)
|
|
255
|
+
- `notify()` - Called when a dependency changes
|
|
256
|
+
- `execute()` - Run the observer's computation
|
|
257
|
+
|
|
258
|
+
### Node
|
|
259
|
+
|
|
260
|
+
The `Node` class combines Observable and Observer, creating primitives that can both watch others and be watched:
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
abstract class Node<T> extends Disposable
|
|
264
|
+
implements IObservable<T>, IObserver {
|
|
265
|
+
|
|
266
|
+
private _dependencies = new Set<IObservable<unknown>>()
|
|
267
|
+
private _dependents = new Set<IObserver>()
|
|
268
|
+
|
|
269
|
+
// Observable behavior
|
|
270
|
+
registerDependent(dependent: IObserver): void { /*...*/ }
|
|
271
|
+
notifyDependents(): void { /*...*/ }
|
|
272
|
+
|
|
273
|
+
// Observer behavior
|
|
274
|
+
registerDependency(dependency: IObservable<unknown>): void { /*...*/ }
|
|
275
|
+
notify(): void {
|
|
276
|
+
if (this.status === "dirty") return
|
|
277
|
+
this.status = "dirty"
|
|
278
|
+
this.notifyDependents()
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
abstract execute(): void
|
|
282
|
+
abstract subscribe(...): () => void
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
This dual nature is what enables chaining: state → derivation → derivation → effect.
|
|
287
|
+
|
|
288
|
+
### ExecutionStack
|
|
289
|
+
|
|
290
|
+
The `ExecutionStack` is a global singleton that batches reactive updates to prevent unnecessary cascading executions:
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
class ExecutionStack {
|
|
294
|
+
private static _pendingQueue: IObserver[] = []
|
|
295
|
+
private static _effectQueue: IObserver[] = []
|
|
296
|
+
private static _executionScheduled?: Promise<void>
|
|
297
|
+
|
|
298
|
+
static pushPending(node: IObserver): void {
|
|
299
|
+
this._scheduleExecution()
|
|
300
|
+
this._pendingQueue.push(node)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
static pushEffect(effect: IObserver): void {
|
|
304
|
+
this._scheduleExecution()
|
|
305
|
+
this._effectQueue.push(effect)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private static _execute(): void {
|
|
309
|
+
// First, resolve all pending computations
|
|
310
|
+
this._pendingQueue.forEach(node => node.execute())
|
|
311
|
+
this._pendingQueue.length = 0
|
|
312
|
+
|
|
313
|
+
// Then, run effects
|
|
314
|
+
this._effectQueue.forEach(effect => effect.execute())
|
|
315
|
+
this._effectQueue.length = 0
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**Why two queues?**
|
|
321
|
+
- **Pending queue**: For value computations (derivations) that other things depend on
|
|
322
|
+
- **Effect queue**: For side effects that don't produce values
|
|
323
|
+
|
|
324
|
+
This ordering ensures that all values are computed before any effects run, preventing effects from running with stale data.
|
|
325
|
+
|
|
326
|
+
**Async scheduling**: Executions are batched with `setTimeout(..., 0)` to:
|
|
327
|
+
- Allow the current call stack to complete
|
|
328
|
+
- Batch multiple synchronous changes into one execution
|
|
329
|
+
- Prevent stack overflow from deep reactive chains
|
|
330
|
+
|
|
331
|
+
## Nodes Layer (src/nodes/)
|
|
332
|
+
|
|
333
|
+
The nodes layer contains concrete implementations of reactive primitives.
|
|
334
|
+
|
|
335
|
+
### ValueNode
|
|
336
|
+
|
|
337
|
+
The abstract `ValueNode` is the foundation for all value-holding primitives (state, derivation, constant, etc.):
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
abstract class ValueNode<T> extends Node<T> {
|
|
341
|
+
protected abstract _scheduler: Scheduler
|
|
342
|
+
protected _value?: NotPromise<T>
|
|
343
|
+
protected _error?: unknown
|
|
344
|
+
protected _status: ObservableStatus = "resolved"
|
|
345
|
+
|
|
346
|
+
get(tracker: FlowTracker): T {
|
|
347
|
+
this.watch(tracker)
|
|
348
|
+
|
|
349
|
+
switch (this.status) {
|
|
350
|
+
case "resolved": return this._value as T
|
|
351
|
+
case "error": throw this._error
|
|
352
|
+
case "pending": throw new PendingError(this._scheduler.settled)
|
|
353
|
+
case "dirty":
|
|
354
|
+
this.execute()
|
|
355
|
+
// Check status again after execution
|
|
356
|
+
// ...
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async pick(): Promise<T> {
|
|
361
|
+
switch (this.status) {
|
|
362
|
+
case "resolved": return this._value as T
|
|
363
|
+
case "pending":
|
|
364
|
+
await this._scheduler.settled
|
|
365
|
+
return this._value as T
|
|
366
|
+
case "dirty":
|
|
367
|
+
this.execute()
|
|
368
|
+
// ...
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
**Value states**:
|
|
375
|
+
- `resolved` - Has a computed value, ready to use
|
|
376
|
+
- `pending` - Async computation in progress
|
|
377
|
+
- `error` - Computation failed
|
|
378
|
+
- `dirty` - Needs recomputation (dependency changed)
|
|
379
|
+
|
|
380
|
+
**Two access methods**:
|
|
381
|
+
- `get(tracker)` - Reactive read that registers a dependency
|
|
382
|
+
- `pick()` - Non-reactive async read
|
|
383
|
+
|
|
384
|
+
### ValueSyncNode
|
|
385
|
+
|
|
386
|
+
Handles synchronous computations:
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
class ValueSyncNode<T> extends ValueNode<T> {
|
|
390
|
+
protected _scheduler: SyncScheduler<T>
|
|
391
|
+
private _compute: () => T
|
|
392
|
+
|
|
393
|
+
constructor(valueOrCompute: T | ComputeFunction<T>) {
|
|
394
|
+
super()
|
|
395
|
+
|
|
396
|
+
if (typeof valueOrCompute === "function") {
|
|
397
|
+
this._compute = () => valueOrCompute(this, this._value)
|
|
398
|
+
} else {
|
|
399
|
+
this._compute = () => valueOrCompute
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
this._scheduler = new SyncScheduler(
|
|
403
|
+
() => this._compute(),
|
|
404
|
+
(value) => this._onResolve(value),
|
|
405
|
+
(error) => this._onReject(error)
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
set(value: T): void {
|
|
410
|
+
this._scheduler.overwrite(value)
|
|
411
|
+
this.notifyDependents()
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
refresh(): void {
|
|
415
|
+
this.execute()
|
|
416
|
+
this.notifyDependents()
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Used by: `state()`, `constant()`, `derivation()`, `writableDerivation()`
|
|
422
|
+
|
|
423
|
+
### ValueAsyncNode
|
|
424
|
+
|
|
425
|
+
Handles asynchronous computations with promises:
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
class ValueAsyncNode<T> extends ValueNode<T> {
|
|
429
|
+
protected _scheduler: AsyncScheduler<T>
|
|
430
|
+
private _compute: () => Promise<T>
|
|
431
|
+
|
|
432
|
+
constructor(promiseOrCompute: Promise<T> | ComputeFunctionAsync<T>) {
|
|
433
|
+
super()
|
|
434
|
+
|
|
435
|
+
if (typeof promiseOrCompute === "function") {
|
|
436
|
+
this._compute = () => promiseOrCompute(this, this._value)
|
|
437
|
+
} else {
|
|
438
|
+
this._compute = () => promiseOrCompute
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
this._scheduler = new AsyncScheduler(
|
|
442
|
+
() => this._compute(),
|
|
443
|
+
(value) => {
|
|
444
|
+
this._onResolve(value)
|
|
445
|
+
this.notifyDependents() // Async: notify after resolution
|
|
446
|
+
},
|
|
447
|
+
(error) => {
|
|
448
|
+
this._onReject(error)
|
|
449
|
+
this.notifyDependents()
|
|
450
|
+
}
|
|
451
|
+
)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
set(promise: Promise<T>): void {
|
|
455
|
+
this._scheduler.overwrite(promise)
|
|
456
|
+
this.notifyDependents() // Immediately notify (status becomes pending)
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
**Key difference from sync**: Async nodes notify dependents twice:
|
|
462
|
+
1. Immediately when computation starts (status becomes "pending")
|
|
463
|
+
2. Again when computation completes (status becomes "resolved" or "error")
|
|
464
|
+
|
|
465
|
+
Used by: `stateAsync()`, `constantAsync()`, `derivationAsync()`, `writableDerivationAsync()`
|
|
466
|
+
|
|
467
|
+
### EffectNode
|
|
468
|
+
|
|
469
|
+
Effects are observers that execute side effects but don't produce values:
|
|
470
|
+
|
|
471
|
+
```typescript
|
|
472
|
+
class EffectNode<T> extends Observer {
|
|
473
|
+
private _data: FlowDataTracker<T>
|
|
474
|
+
private _onData: FlowOnDataListener<T>
|
|
475
|
+
private _onError?: FlowOnErrorListener
|
|
476
|
+
private _onPending?: FlowOnPendingListener
|
|
477
|
+
|
|
478
|
+
constructor(
|
|
479
|
+
data: FlowDataTracker<T>,
|
|
480
|
+
onData: FlowOnDataListener<T>,
|
|
481
|
+
onError?: FlowOnErrorListener,
|
|
482
|
+
onPending?: FlowOnPendingListener
|
|
483
|
+
) {
|
|
484
|
+
super()
|
|
485
|
+
this._data = data
|
|
486
|
+
this._onData = onData
|
|
487
|
+
this._onError = onError
|
|
488
|
+
this._onPending = onPending
|
|
489
|
+
this.execute() // Run immediately
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
execute(): void {
|
|
493
|
+
try {
|
|
494
|
+
this.clearDependencies() // Clear old dependencies
|
|
495
|
+
const data = this._data(this) // Track new dependencies
|
|
496
|
+
this._onData(data)
|
|
497
|
+
} catch (error) {
|
|
498
|
+
if (error instanceof PendingError) {
|
|
499
|
+
this._onPending?.()
|
|
500
|
+
} else {
|
|
501
|
+
if (this._onError) {
|
|
502
|
+
this._onError(error)
|
|
503
|
+
} else {
|
|
504
|
+
throw error // Re-throw if no error handler
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
notify(): void {
|
|
511
|
+
ExecutionStack.pushEffect(this)
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
Used by: `subscribe()` (the public API for effects)
|
|
517
|
+
|
|
518
|
+
### SignalNode
|
|
519
|
+
|
|
520
|
+
Signals are events without values:
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
class SignalNode extends Observable<void> {
|
|
524
|
+
subscribe(
|
|
525
|
+
onTrigger: FlowOnDataListener<void>,
|
|
526
|
+
onError?: FlowOnErrorListener,
|
|
527
|
+
onPending?: FlowOnPendingListener
|
|
528
|
+
): () => void {
|
|
529
|
+
const effect = new EffectNode(
|
|
530
|
+
(t) => this.watch(t),
|
|
531
|
+
onTrigger,
|
|
532
|
+
onError,
|
|
533
|
+
onPending
|
|
534
|
+
)
|
|
535
|
+
return () => effect.dispose()
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
Signals are useful for:
|
|
541
|
+
- User actions (button clicks, form submissions)
|
|
542
|
+
- Timer events
|
|
543
|
+
- Custom events that don't carry data
|
|
544
|
+
|
|
545
|
+
### Collections (ArrayNode, MapNode)
|
|
546
|
+
|
|
547
|
+
Collections provide fine-grained reactivity with mutation tracking:
|
|
548
|
+
|
|
549
|
+
```typescript
|
|
550
|
+
class ArrayNode<T> extends ValueSyncNode<T[]> {
|
|
551
|
+
$lastAction: ValueSyncNode<FlowArrayAction<T>>
|
|
552
|
+
|
|
553
|
+
push(item: T): void {
|
|
554
|
+
this._value.push(item)
|
|
555
|
+
this.notifyDependents()
|
|
556
|
+
this.$lastAction.set({
|
|
557
|
+
type: "push",
|
|
558
|
+
addedItem: item
|
|
559
|
+
})
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Similar for pop, shift, unshift, splice, etc.
|
|
563
|
+
}
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
The `$lastAction` primitive allows you to react specifically to certain mutation types:
|
|
567
|
+
|
|
568
|
+
```typescript
|
|
569
|
+
const $todos = array<Todo>([])
|
|
570
|
+
|
|
571
|
+
// React to any change
|
|
572
|
+
subscribe(
|
|
573
|
+
(t) => $todos.get(t),
|
|
574
|
+
(todos) => console.log("Todos changed", todos)
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
// React only to additions
|
|
578
|
+
subscribe(
|
|
579
|
+
(t) => {
|
|
580
|
+
const action = $todos.$lastAction.get(t)
|
|
581
|
+
if (action.type === "push") return action.addedItem
|
|
582
|
+
throw new PendingError(Promise.resolve())
|
|
583
|
+
},
|
|
584
|
+
(item) => console.log("Todo added", item)
|
|
585
|
+
)
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
## Schedulers Layer (src/schedulers/)
|
|
589
|
+
|
|
590
|
+
Schedulers manage the lifecycle of computations, handling both synchronous and asynchronous execution.
|
|
591
|
+
|
|
592
|
+
### Why Schedulers?
|
|
593
|
+
|
|
594
|
+
Schedulers solve several problems:
|
|
595
|
+
|
|
596
|
+
1. **Lifecycle management** - Track whether a computation is pending, resolved, or errored
|
|
597
|
+
2. **Async/sync separation** - Different execution strategies for sync vs async
|
|
598
|
+
3. **Race conditions** - Prevent stale results from async operations
|
|
599
|
+
4. **Promise settling** - Provide consistent `settled` promise for awaiting completion
|
|
600
|
+
|
|
601
|
+
### Scheduler Interface
|
|
602
|
+
|
|
603
|
+
All schedulers implement this contract:
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
interface Scheduler {
|
|
607
|
+
compute(): void // Start/restart computation
|
|
608
|
+
dispose(): void // Clean up resources
|
|
609
|
+
settled: Promise<void> // Promise that resolves when computation settles
|
|
610
|
+
}
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
### SyncScheduler & SyncResolver
|
|
614
|
+
|
|
615
|
+
For synchronous computations:
|
|
616
|
+
|
|
617
|
+
```mermaid
|
|
618
|
+
sequenceDiagram
|
|
619
|
+
participant N as Node
|
|
620
|
+
participant SS as SyncScheduler
|
|
621
|
+
participant SR as SyncResolver
|
|
622
|
+
|
|
623
|
+
N->>SS: compute()
|
|
624
|
+
SS->>SR: compute()
|
|
625
|
+
SR->>SR: _compute()
|
|
626
|
+
alt Success
|
|
627
|
+
SR->>SS: onResolve(value)
|
|
628
|
+
SS->>N: callback with value
|
|
629
|
+
else Error
|
|
630
|
+
SR->>SS: onReject(error)
|
|
631
|
+
SS->>N: callback with error
|
|
632
|
+
end
|
|
633
|
+
SS->>SS: settled.resolve()
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
**SyncResolver iteration tracking**:
|
|
637
|
+
|
|
638
|
+
```typescript
|
|
639
|
+
class SyncResolver<T> {
|
|
640
|
+
private _iteration = 0
|
|
641
|
+
|
|
642
|
+
compute(): void {
|
|
643
|
+
this._iteration++
|
|
644
|
+
const currentIteration = this._iteration
|
|
645
|
+
|
|
646
|
+
try {
|
|
647
|
+
const value = this._compute()
|
|
648
|
+
// Only use result if no newer computation started
|
|
649
|
+
if (this._iteration === currentIteration) {
|
|
650
|
+
this._onValue(value)
|
|
651
|
+
}
|
|
652
|
+
} catch (error) {
|
|
653
|
+
if (this._iteration === currentIteration) {
|
|
654
|
+
this._onError(error)
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
This prevents race conditions if `compute()` is called multiple times rapidly.
|
|
662
|
+
|
|
663
|
+
### AsyncScheduler & AsyncResolver
|
|
664
|
+
|
|
665
|
+
For asynchronous computations:
|
|
666
|
+
|
|
667
|
+
```mermaid
|
|
668
|
+
sequenceDiagram
|
|
669
|
+
participant N as Node
|
|
670
|
+
participant AS as AsyncScheduler
|
|
671
|
+
participant AR as AsyncResolver
|
|
672
|
+
|
|
673
|
+
N->>AS: compute()
|
|
674
|
+
AS->>AR: compute()
|
|
675
|
+
AR->>AR: _compute() (returns Promise)
|
|
676
|
+
AR-->>AS: Promise pending
|
|
677
|
+
|
|
678
|
+
Note over AR: Async work happening...
|
|
679
|
+
|
|
680
|
+
AR->>AS: Promise resolves
|
|
681
|
+
AS->>N: onResolve(value)
|
|
682
|
+
AS->>AS: settled.resolve()
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
**AsyncResolver race condition prevention**:
|
|
686
|
+
|
|
687
|
+
```typescript
|
|
688
|
+
class AsyncResolver<T> {
|
|
689
|
+
private _iteration = 0
|
|
690
|
+
|
|
691
|
+
compute(): void {
|
|
692
|
+
this._iteration++
|
|
693
|
+
const currentIteration = this._iteration
|
|
694
|
+
|
|
695
|
+
this._compute()
|
|
696
|
+
.then(value => {
|
|
697
|
+
// Only use result if no newer computation started
|
|
698
|
+
if (this._iteration === currentIteration && !this._aborted) {
|
|
699
|
+
this._computed.resolve(value)
|
|
700
|
+
}
|
|
701
|
+
})
|
|
702
|
+
.catch(error => {
|
|
703
|
+
if (this._iteration === currentIteration && !this._aborted) {
|
|
704
|
+
this._computed.reject(error)
|
|
705
|
+
}
|
|
706
|
+
})
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
If a new computation starts while an async operation is in flight, the old result is discarded.
|
|
712
|
+
|
|
713
|
+
### PendingError
|
|
714
|
+
|
|
715
|
+
`PendingError` is a special error type used to propagate pending states:
|
|
716
|
+
|
|
717
|
+
```typescript
|
|
718
|
+
class PendingError extends Error {
|
|
719
|
+
readonly pendingPromise: Promise<unknown>
|
|
720
|
+
|
|
721
|
+
constructor(promise: Promise<unknown>) {
|
|
722
|
+
super("[PicoFlow] PendingResolver pending")
|
|
723
|
+
this.pendingPromise = promise
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
**How it's used**:
|
|
729
|
+
|
|
730
|
+
```typescript
|
|
731
|
+
const $user = stateAsync(fetchUser()) // Async state
|
|
732
|
+
|
|
733
|
+
const $userName = derivation((t) => {
|
|
734
|
+
const user = $user.get(t) // Throws PendingError while loading
|
|
735
|
+
return user.name
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
subscribe(
|
|
739
|
+
(t) => $userName.get(t),
|
|
740
|
+
(name) => console.log("Name:", name),
|
|
741
|
+
(error) => console.error("Error:", error),
|
|
742
|
+
() => console.log("Loading...") // Called when PendingError is caught
|
|
743
|
+
)
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
When `$user` is pending, `$user.get()` throws a `PendingError`. This propagates up, the effect catches it, and calls the `onPending` callback.
|
|
747
|
+
|
|
748
|
+
## Public API Layer (src/api/)
|
|
749
|
+
|
|
750
|
+
The API layer provides the user-facing interface to PicoFlow.
|
|
751
|
+
|
|
752
|
+
### Factory Pattern
|
|
753
|
+
|
|
754
|
+
Users never instantiate node classes directly. Instead, they use factory functions:
|
|
755
|
+
|
|
756
|
+
```typescript
|
|
757
|
+
// In src/api/nodes/sync/flowState.ts
|
|
758
|
+
export function state<T>(value: T): FlowState<T> {
|
|
759
|
+
return new ValueSyncNode(value)
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// In src/api/nodes/sync/flowDerivation.ts
|
|
763
|
+
export function derivation<T>(
|
|
764
|
+
compute: DerivationFunction<T>
|
|
765
|
+
): FlowDerivation<T> {
|
|
766
|
+
return new ValueSyncNode(compute)
|
|
767
|
+
}
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
**Benefits**:
|
|
771
|
+
- **Encapsulation** - Users don't need to know about implementation classes
|
|
772
|
+
- **Flexibility** - We can change implementations without breaking the API
|
|
773
|
+
- **Semantic naming** - `state()` is clearer than `new ValueSyncNode()`
|
|
774
|
+
- **Type safety** - Return types are specific interfaces, not implementation classes
|
|
775
|
+
|
|
776
|
+
### Interface Design
|
|
777
|
+
|
|
778
|
+
Public interfaces define contracts, not implementations:
|
|
779
|
+
|
|
780
|
+
```typescript
|
|
781
|
+
// Public interface
|
|
782
|
+
export interface FlowState<T> extends FlowValue<T> {
|
|
783
|
+
set(value: T): void
|
|
784
|
+
set(updater: UpdateFunction<T>): void
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Users interact with FlowState interface
|
|
788
|
+
// Implementation (ValueSyncNode) is hidden
|
|
789
|
+
const $count: FlowState<number> = state(0)
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
This separation allows:
|
|
793
|
+
- Documentation to focus on contracts, not implementations
|
|
794
|
+
- Implementation changes without API changes
|
|
795
|
+
- Clear separation of public vs internal
|
|
796
|
+
|
|
797
|
+
### Type Hierarchy
|
|
798
|
+
|
|
799
|
+
```mermaid
|
|
800
|
+
graph TB
|
|
801
|
+
FlowDisposable["FlowDisposable<br/>disposed, dispose()"]
|
|
802
|
+
FlowSubscribable["FlowSubscribable<br/>subscribe()"]
|
|
803
|
+
FlowObservable["FlowObservable<br/>watch(), trigger()"]
|
|
804
|
+
FlowTracker["FlowTracker<br/>(for dependency tracking)"]
|
|
805
|
+
FlowValue["FlowValue<br/>get(), pick()"]
|
|
806
|
+
|
|
807
|
+
FlowState["FlowState<br/>set()"]
|
|
808
|
+
FlowDerivation["FlowDerivation<br/>refresh()"]
|
|
809
|
+
FlowConstant["FlowConstant<br/>(no mutations)"]
|
|
810
|
+
|
|
811
|
+
FlowDisposable --> FlowObservable
|
|
812
|
+
FlowSubscribable --> FlowObservable
|
|
813
|
+
FlowTracker --> FlowDisposable
|
|
814
|
+
FlowObservable --> FlowValue
|
|
815
|
+
|
|
816
|
+
FlowValue --> FlowState
|
|
817
|
+
FlowValue --> FlowDerivation
|
|
818
|
+
FlowValue --> FlowConstant
|
|
819
|
+
|
|
820
|
+
style FlowDisposable fill:#ffe1e1
|
|
821
|
+
style FlowObservable fill:#e1ffe1
|
|
822
|
+
style FlowValue fill:#e1e1ff
|
|
823
|
+
style FlowState fill:#fff4e1
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
This hierarchy expresses capabilities:
|
|
827
|
+
- All primitives are **Disposable**
|
|
828
|
+
- All primitives are **Subscribable** (can create effects)
|
|
829
|
+
- All primitives are **Observable** (can be watched)
|
|
830
|
+
- Value primitives add **get/pick** methods
|
|
831
|
+
- Specific types add their own methods (set, refresh, etc.)
|
|
832
|
+
|
|
833
|
+
## Key Design Patterns
|
|
834
|
+
|
|
835
|
+
### Factory Pattern
|
|
836
|
+
|
|
837
|
+
**Intent**: Provide a simple creation interface while hiding complex construction logic.
|
|
838
|
+
|
|
839
|
+
**Implementation**:
|
|
840
|
+
```typescript
|
|
841
|
+
// Factory function (public API)
|
|
842
|
+
export function state<T>(value: T): FlowState<T> {
|
|
843
|
+
return new ValueSyncNode(value)
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Complex internal class (hidden from users)
|
|
847
|
+
class ValueSyncNode<T> extends ValueNode<T> {
|
|
848
|
+
// Complex internal logic...
|
|
849
|
+
}
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
**Benefits**: Semantic naming, encapsulation, flexibility to change implementations.
|
|
853
|
+
|
|
854
|
+
### Strategy Pattern
|
|
855
|
+
|
|
856
|
+
**Intent**: Define a family of algorithms (sync vs async execution) and make them interchangeable.
|
|
857
|
+
|
|
858
|
+
**Implementation**:
|
|
859
|
+
```typescript
|
|
860
|
+
// Strategy interface
|
|
861
|
+
interface Scheduler {
|
|
862
|
+
compute(): void
|
|
863
|
+
dispose(): void
|
|
864
|
+
settled: Promise<void>
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Concrete strategies
|
|
868
|
+
class SyncScheduler implements Scheduler { /* sync logic */ }
|
|
869
|
+
class AsyncScheduler implements Scheduler { /* async logic */ }
|
|
870
|
+
|
|
871
|
+
// Context uses the strategy
|
|
872
|
+
class ValueNode {
|
|
873
|
+
protected abstract _scheduler: Scheduler // Can be either strategy
|
|
874
|
+
}
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
**Benefits**: Separate sync/async concerns, easy to add new execution strategies.
|
|
878
|
+
|
|
879
|
+
### Observer Pattern
|
|
880
|
+
|
|
881
|
+
**Intent**: Define a one-to-many dependency where changes in one object notify all dependents.
|
|
882
|
+
|
|
883
|
+
**Implementation**:
|
|
884
|
+
```typescript
|
|
885
|
+
class Observable {
|
|
886
|
+
private _dependents = new Set<IObserver>()
|
|
887
|
+
|
|
888
|
+
notifyDependents(): void {
|
|
889
|
+
this._dependents.forEach(dep => dep.notify())
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
class Observer {
|
|
894
|
+
notify(): void {
|
|
895
|
+
// React to change
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
**Benefits**: Decoupling, automatic propagation, dynamic subscription.
|
|
901
|
+
|
|
902
|
+
### Disposable Pattern
|
|
903
|
+
|
|
904
|
+
**Intent**: Explicit resource management to prevent memory leaks.
|
|
905
|
+
|
|
906
|
+
**Implementation**:
|
|
907
|
+
```typescript
|
|
908
|
+
class Disposable {
|
|
909
|
+
protected _disposed = false
|
|
910
|
+
|
|
911
|
+
dispose(): void {
|
|
912
|
+
if (this._disposed) throw new Error("Already disposed")
|
|
913
|
+
// Clean up resources
|
|
914
|
+
this._disposed = true
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
**Benefits**: Explicit cleanup, prevents memory leaks, clear resource ownership.
|
|
920
|
+
|
|
921
|
+
## Data Flow Examples
|
|
922
|
+
|
|
923
|
+
### Creating a State
|
|
924
|
+
|
|
925
|
+
```mermaid
|
|
926
|
+
sequenceDiagram
|
|
927
|
+
participant U as User Code
|
|
928
|
+
participant API as state()
|
|
929
|
+
participant VSN as ValueSyncNode
|
|
930
|
+
participant SS as SyncScheduler
|
|
931
|
+
participant SR as SyncResolver
|
|
932
|
+
|
|
933
|
+
U->>API: state(0)
|
|
934
|
+
API->>VSN: new ValueSyncNode(0)
|
|
935
|
+
VSN->>SS: new SyncScheduler(compute, onResolve, onReject)
|
|
936
|
+
SS->>SR: new SyncResolver(compute, onResolve, onReject)
|
|
937
|
+
VSN->>VSN: status = "dirty"
|
|
938
|
+
Note over VSN: Lazy - won't compute until accessed
|
|
939
|
+
API->>U: return VSN (as FlowState)
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
### Reactive Read with Tracking
|
|
943
|
+
|
|
944
|
+
```mermaid
|
|
945
|
+
sequenceDiagram
|
|
946
|
+
participant D as Derivation
|
|
947
|
+
participant S as State
|
|
948
|
+
|
|
949
|
+
Note over D: Creating derivation
|
|
950
|
+
D->>D: new ValueSyncNode(compute)
|
|
951
|
+
|
|
952
|
+
Note over D: First access triggers execution
|
|
953
|
+
D->>D: get(tracker)
|
|
954
|
+
D->>D: execute()
|
|
955
|
+
D->>S: get(tracker)
|
|
956
|
+
S->>D: registerDependency(this)
|
|
957
|
+
Note over D,S: Dependency registered!
|
|
958
|
+
S->>D: return value
|
|
959
|
+
D->>D: compute result
|
|
960
|
+
D->>D: status = "resolved"
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
### Update Propagation
|
|
964
|
+
|
|
965
|
+
```mermaid
|
|
966
|
+
sequenceDiagram
|
|
967
|
+
participant U as User Code
|
|
968
|
+
participant S as State
|
|
969
|
+
participant D as Derivation
|
|
970
|
+
participant E as Effect
|
|
971
|
+
participant ES as ExecutionStack
|
|
972
|
+
|
|
973
|
+
U->>S: set(1)
|
|
974
|
+
S->>S: _value = 1
|
|
975
|
+
S->>D: notifyDependents()
|
|
976
|
+
D->>D: notify()
|
|
977
|
+
D->>D: status = "dirty"
|
|
978
|
+
D->>E: notifyDependents()
|
|
979
|
+
E->>E: notify()
|
|
980
|
+
E->>ES: pushEffect(this)
|
|
981
|
+
ES->>ES: scheduleExecution()
|
|
982
|
+
|
|
983
|
+
Note over ES: setTimeout completes
|
|
984
|
+
|
|
985
|
+
ES->>E: execute()
|
|
986
|
+
E->>E: clearDependencies()
|
|
987
|
+
E->>D: get(tracker)
|
|
988
|
+
D->>D: status is "dirty", recompute
|
|
989
|
+
D->>S: get(tracker)
|
|
990
|
+
S->>D: return new value
|
|
991
|
+
D->>E: return computed value
|
|
992
|
+
E->>E: call onData callback
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
### Async Computation Flow
|
|
996
|
+
|
|
997
|
+
```mermaid
|
|
998
|
+
sequenceDiagram
|
|
999
|
+
participant U as User Code
|
|
1000
|
+
participant VAN as ValueAsyncNode
|
|
1001
|
+
participant AS as AsyncScheduler
|
|
1002
|
+
participant AR as AsyncResolver
|
|
1003
|
+
|
|
1004
|
+
U->>VAN: stateAsync(fetchUser())
|
|
1005
|
+
VAN->>AS: new AsyncScheduler()
|
|
1006
|
+
AS->>AR: new AsyncResolver()
|
|
1007
|
+
|
|
1008
|
+
Note over VAN: First access
|
|
1009
|
+
U->>VAN: get(tracker)
|
|
1010
|
+
VAN->>VAN: status is "dirty"
|
|
1011
|
+
VAN->>VAN: execute()
|
|
1012
|
+
VAN->>AS: compute()
|
|
1013
|
+
AS->>AR: compute()
|
|
1014
|
+
AR->>AR: _compute() returns Promise
|
|
1015
|
+
VAN->>VAN: status = "pending"
|
|
1016
|
+
VAN->>U: throw PendingError
|
|
1017
|
+
|
|
1018
|
+
Note over AR: Async operation completes
|
|
1019
|
+
|
|
1020
|
+
AR->>AS: Promise resolves
|
|
1021
|
+
AS->>VAN: onResolve(value)
|
|
1022
|
+
VAN->>VAN: status = "resolved"
|
|
1023
|
+
VAN->>VAN: notifyDependents()
|
|
1024
|
+
|
|
1025
|
+
Note over VAN: Dependents re-execute
|
|
1026
|
+
Note over VAN: Next get() returns value immediately
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
## Performance Considerations
|
|
1030
|
+
|
|
1031
|
+
### Batching with ExecutionStack
|
|
1032
|
+
|
|
1033
|
+
Multiple synchronous state changes are batched into a single update cycle:
|
|
1034
|
+
|
|
1035
|
+
```typescript
|
|
1036
|
+
const $a = state(0)
|
|
1037
|
+
const $b = state(0)
|
|
1038
|
+
|
|
1039
|
+
const $sum = derivation((t) => $a.get(t) + $b.get(t))
|
|
1040
|
+
|
|
1041
|
+
subscribe((t) => $sum.get(t), (sum) => console.log(sum))
|
|
1042
|
+
|
|
1043
|
+
// These changes are batched
|
|
1044
|
+
$a.set(1) // Doesn't immediately trigger effect
|
|
1045
|
+
$b.set(2) // Doesn't immediately trigger effect
|
|
1046
|
+
// Effect runs once after current call stack completes
|
|
1047
|
+
// Output: 3 (not 1, then 3)
|
|
1048
|
+
```
|
|
1049
|
+
|
|
1050
|
+
**Performance benefit**: O(1) updates regardless of number of changes, prevents redundant computations.
|
|
1051
|
+
|
|
1052
|
+
### Lazy Evaluation
|
|
1053
|
+
|
|
1054
|
+
Primitives don't compute until accessed:
|
|
1055
|
+
|
|
1056
|
+
```typescript
|
|
1057
|
+
const $expensive = derivation((t) => {
|
|
1058
|
+
// This only runs when something actually uses $expensive
|
|
1059
|
+
return expensiveComputation()
|
|
1060
|
+
})
|
|
1061
|
+
|
|
1062
|
+
// No computation yet...
|
|
1063
|
+
// Still no computation...
|
|
1064
|
+
|
|
1065
|
+
// Now it computes
|
|
1066
|
+
subscribe((t) => $expensive.get(t), console.log)
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
**Performance benefit**: Unused primitives have zero overhead.
|
|
1070
|
+
|
|
1071
|
+
### Memoization
|
|
1072
|
+
|
|
1073
|
+
Computed values are cached until dependencies change:
|
|
1074
|
+
|
|
1075
|
+
```typescript
|
|
1076
|
+
const $count = state(0)
|
|
1077
|
+
const $squared = derivation((t) => {
|
|
1078
|
+
console.log("Computing...")
|
|
1079
|
+
return $count.get(t) ** 2
|
|
1080
|
+
})
|
|
1081
|
+
|
|
1082
|
+
$squared.get(tracker) // "Computing..." → returns 0
|
|
1083
|
+
$squared.get(tracker) // Uses cached value → returns 0 (no log)
|
|
1084
|
+
$squared.get(tracker) // Uses cached value → returns 0 (no log)
|
|
1085
|
+
|
|
1086
|
+
$count.set(1)
|
|
1087
|
+
$squared.get(tracker) // "Computing..." → returns 1 (recomputed)
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
**Performance benefit**: Expensive computations only run when necessary.
|
|
1091
|
+
|
|
1092
|
+
### Dirty Checking
|
|
1093
|
+
|
|
1094
|
+
Changes short-circuit if the value hasn't actually changed:
|
|
1095
|
+
|
|
1096
|
+
```typescript
|
|
1097
|
+
const $count = state(0)
|
|
1098
|
+
const $effect = subscribe(
|
|
1099
|
+
(t) => $count.get(t),
|
|
1100
|
+
(value) => console.log(value)
|
|
1101
|
+
)
|
|
1102
|
+
|
|
1103
|
+
$count.set(0) // Same value - effect doesn't run
|
|
1104
|
+
$count.set(1) // Different value - effect runs
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
**Performance benefit**: Prevents unnecessary downstream updates.
|
|
1108
|
+
|
|
1109
|
+
## For Contributors
|
|
1110
|
+
|
|
1111
|
+
### Adding a New Primitive
|
|
1112
|
+
|
|
1113
|
+
To add a new reactive primitive:
|
|
1114
|
+
|
|
1115
|
+
1. **Decide on the base class**:
|
|
1116
|
+
- Extends `ValueNode` if it holds a value
|
|
1117
|
+
- Extends `Observer` if it's an effect-like primitive
|
|
1118
|
+
- Extends `Node` if it needs custom observable/observer behavior
|
|
1119
|
+
|
|
1120
|
+
2. **Implement the node class** in `src/nodes/`:
|
|
1121
|
+
```typescript
|
|
1122
|
+
class MyNewNode extends ValueNode<T> {
|
|
1123
|
+
protected _scheduler: SyncScheduler<T>
|
|
1124
|
+
|
|
1125
|
+
constructor(/* params */) {
|
|
1126
|
+
super()
|
|
1127
|
+
// Initialize scheduler
|
|
1128
|
+
this._scheduler = new SyncScheduler(/* ... */)
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
execute(): void {
|
|
1132
|
+
// Computation logic
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
```
|
|
1136
|
+
|
|
1137
|
+
3. **Create public API** in `src/api/`:
|
|
1138
|
+
```typescript
|
|
1139
|
+
export interface FlowMyNew<T> extends FlowValue<T> {
|
|
1140
|
+
// Add any specific methods
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
export function myNew<T>(/* params */): FlowMyNew<T> {
|
|
1144
|
+
return new MyNewNode(/* params */)
|
|
1145
|
+
}
|
|
1146
|
+
```
|
|
1147
|
+
|
|
1148
|
+
4. **Write tests** in `test/`:
|
|
1149
|
+
```typescript
|
|
1150
|
+
describe("myNew", () => {
|
|
1151
|
+
it("should work correctly", () => {
|
|
1152
|
+
const $val = myNew(/* ... */)
|
|
1153
|
+
// Test behavior
|
|
1154
|
+
})
|
|
1155
|
+
})
|
|
1156
|
+
```
|
|
1157
|
+
|
|
1158
|
+
### Code Conventions
|
|
1159
|
+
|
|
1160
|
+
**@internal vs @public tags**:
|
|
1161
|
+
- Use `@public` on API elements (in `src/api/`)
|
|
1162
|
+
- Use `@internal` on implementation elements (in `src/base/`, `src/nodes/`, `src/schedulers/`)
|
|
1163
|
+
|
|
1164
|
+
**Naming conventions**:
|
|
1165
|
+
- Classes: PascalCase (`ValueSyncNode`, `AsyncScheduler`)
|
|
1166
|
+
- Functions: camelCase (`state`, `derivation`)
|
|
1167
|
+
- Private fields: underscore prefix (`_value`, `_dependencies`)
|
|
1168
|
+
- Public interfaces: Flow prefix (`FlowState`, `FlowObservable`)
|
|
1169
|
+
|
|
1170
|
+
**Error handling**:
|
|
1171
|
+
- Throw errors for programmer mistakes (wrong usage)
|
|
1172
|
+
- Use PendingError for async pending states
|
|
1173
|
+
- Provide onError callbacks for runtime errors
|
|
1174
|
+
|
|
1175
|
+
### Testing Structure
|
|
1176
|
+
|
|
1177
|
+
Tests are organized by concern:
|
|
1178
|
+
|
|
1179
|
+
```
|
|
1180
|
+
test/
|
|
1181
|
+
├── base/ # Tests for base classes
|
|
1182
|
+
├── converters/ # Tests for integrations
|
|
1183
|
+
├── reactivity/ # Tests for reactive behavior
|
|
1184
|
+
│ └── nodes/ # Tests for specific node types
|
|
1185
|
+
└── vitest.d.ts # Type definitions
|
|
1186
|
+
```
|
|
1187
|
+
|
|
1188
|
+
Run tests:
|
|
1189
|
+
```bash
|
|
1190
|
+
pnpm test # Run all tests
|
|
1191
|
+
pnpm test:watch # Watch mode
|
|
1192
|
+
pnpm test:coverage # With coverage
|
|
1193
|
+
```
|
|
1194
|
+
|
|
1195
|
+
## Glossary
|
|
1196
|
+
|
|
1197
|
+
**Disposable** - An object that can be explicitly cleaned up to free resources and prevent memory leaks. All PicoFlow primitives are disposable.
|
|
1198
|
+
|
|
1199
|
+
**Observable** - An object that can be watched and notifies its dependents when it changes. Can have multiple observers.
|
|
1200
|
+
|
|
1201
|
+
**Observer** - An object that watches one or more observables and reacts to their changes. Can have multiple dependencies.
|
|
1202
|
+
|
|
1203
|
+
**Tracker** - An object (typically an Observer) passed to computation functions to automatically record dependencies when values are accessed.
|
|
1204
|
+
|
|
1205
|
+
**Primitive** - A fundamental reactive building block in PicoFlow (state, derivation, effect, signal, etc.).
|
|
1206
|
+
|
|
1207
|
+
**Node** - An internal implementation class that combines Observable and Observer behavior. Users never interact with nodes directly.
|
|
1208
|
+
|
|
1209
|
+
**Scheduler** - Manages the execution lifecycle of computations, handling both synchronous and asynchronous execution strategies.
|
|
1210
|
+
|
|
1211
|
+
**Resolver** - Manages iteration tracking within a scheduler to prevent race conditions from rapid recomputations.
|
|
1212
|
+
|
|
1213
|
+
**Dependency Graph** - The directed acyclic graph formed by primitives observing each other. Changes propagate from sources (states) through intermediaries (derivations) to sinks (effects).
|
|
1214
|
+
|
|
1215
|
+
**Batching** - Combining multiple synchronous changes into a single update cycle to optimize performance.
|
|
1216
|
+
|
|
1217
|
+
**Lazy Evaluation** - Deferring computation until a value is actually needed, avoiding work for unused primitives.
|
|
1218
|
+
|
|
1219
|
+
**PendingError** - A special error type thrown when an async computation is in progress, used to propagate pending states through the dependency graph.
|
|
1220
|
+
|
|
1221
|
+
**ExecutionStack** - A global singleton that batches and orders reactive executions to prevent cascading updates and stack overflow.
|
|
1222
|
+
|
|
1223
|
+
---
|
|
1224
|
+
|
|
1225
|
+
## Further Reading
|
|
1226
|
+
|
|
1227
|
+
- **[Concepts](/guide/introduction/concepts)** - High-level reactive concepts
|
|
1228
|
+
- **[Lifecycle](/guide/introduction/lifecycle)** - Primitive lifecycle and states
|
|
1229
|
+
- **[Disposal](/guide/advanced/disposal)** - Memory management best practices
|
|
1230
|
+
- **[API Reference](/api/)** - Complete API documentation
|
|
1231
|
+
|
|
1232
|
+
---
|
|
1233
|
+
|
|
1234
|
+
This architecture enables PicoFlow to provide fine-grained, explicit, and performant reactivity with a clean separation between public API and internal implementation. Whether you're debugging a tricky reactive chain or contributing a new feature, understanding these architectural patterns will help you work effectively with PicoFlow.
|