@raphy-js/raph 1.0.0-dev.2 → 1.0.1-dev.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1096 -0
- package/dist/api/Raph.d.ts +59 -190
- package/dist/api/RaphFacadeSession.d.ts +90 -0
- package/dist/api/RaphReactivityInstance.d.ts +72 -0
- package/dist/api/errors.d.ts +9 -4
- package/dist/api/types.d.ts +162 -17
- package/dist/core-CyilRD7s.js +1055 -0
- package/dist/core-ugchZNqC.cjs +1 -0
- package/dist/debug/api/Raph.cjs +1 -1
- package/dist/debug/api/Raph.js +368 -366
- package/dist/debug/api/RaphFacadeSession.cjs +1 -0
- package/dist/debug/api/RaphFacadeSession.js +306 -0
- package/dist/debug/api/RaphReactivityInstance.cjs +1 -0
- package/dist/debug/api/RaphReactivityInstance.js +149 -0
- package/dist/debug/api/errors.cjs +1 -1
- package/dist/debug/api/errors.js +10 -7
- package/dist/debug/path/DataPath.cjs +1 -0
- package/dist/debug/path/{data-path.js → DataPath.js} +151 -180
- package/dist/debug/path/PathRouter.cjs +1 -0
- package/dist/debug/path/{path-router.js → PathRouter.js} +1 -1
- package/dist/debug/path/RaphKernel.cjs +1 -1
- package/dist/debug/path/RaphKernel.js +258 -207
- package/dist/debug/path/data-path.utils.cjs +1 -0
- package/dist/debug/path/data-path.utils.js +56 -0
- package/dist/debug/path/derived/DerivedRegistry.cjs +1 -0
- package/dist/debug/path/{DerivedRegistry.js → derived/DerivedRegistry.js} +5 -5
- package/dist/debug/path/derived/derived-collection.utils.cjs +1 -0
- package/dist/debug/path/{derived-collection.js → derived/derived-collection.utils.js} +1 -1
- package/dist/debug/path/derived/derived-strategy.types.cjs +1 -0
- package/dist/debug/path/derived/derived-strategy.types.js +1 -0
- package/dist/debug/path/{MetaController.cjs → meta/MetaController.cjs} +1 -1
- package/dist/debug/path/{MetaController.js → meta/MetaController.js} +3 -3
- package/dist/debug/path/meta/RaphMetaStore.cjs +1 -0
- package/dist/debug/path/{RaphMetaStore.js → meta/RaphMetaStore.js} +1 -1
- package/dist/debug/path/meta/meta.types.cjs +1 -0
- package/dist/debug/path/meta/meta.types.js +1 -0
- package/dist/debug/path/storage/AdapterPathStore.cjs +1 -0
- package/dist/debug/path/{AdapterPathStore.js → storage/AdapterPathStore.js} +9 -9
- package/dist/debug/path/storage/DefaultDataAdapter.cjs +1 -0
- package/dist/debug/path/{data-adapter.js → storage/DefaultDataAdapter.js} +2 -2
- package/dist/debug/path/storage/PathEventHub.cjs +1 -0
- package/dist/debug/path/{PathEventHub.js → storage/PathEventHub.js} +34 -34
- package/dist/debug/path/storage/PathStore.cjs +1 -0
- package/dist/debug/path/{PathStore.js → storage/PathStore.js} +28 -28
- package/dist/debug/path/storage/data-adapter.types.cjs +1 -0
- package/dist/debug/path/storage/data-adapter.types.js +1 -0
- package/dist/debug/reactivity/engine.cjs +1 -1
- package/dist/debug/reactivity/engine.js +478 -306
- package/dist/debug/reactivity/reactive-object.cjs +1 -0
- package/dist/debug/reactivity/reactive-object.js +280 -0
- package/dist/debug/runtime/RaphRuntime.cjs +1 -1
- package/dist/debug/runtime/RaphRuntime.js +798 -519
- package/dist/debug/runtime/diagnostics/RaphDebug.cjs +1 -0
- package/dist/debug/runtime/diagnostics/RaphDebug.js +297 -0
- package/dist/debug/runtime/errors.cjs +1 -1
- package/dist/debug/runtime/errors.js +6 -3
- package/dist/debug/runtime/ownership/RaphEffectScope.cjs +1 -0
- package/dist/debug/runtime/ownership/RaphEffectScope.js +171 -0
- package/dist/debug/runtime/ownership/RaphNode.cjs +1 -0
- package/dist/debug/runtime/ownership/RaphNode.js +206 -0
- package/dist/debug/runtime/ownership/RaphScope.cjs +1 -0
- package/dist/debug/runtime/ownership/RaphScope.js +101 -0
- package/dist/debug/runtime/ownership/parent-graph.types.cjs +1 -0
- package/dist/debug/runtime/ownership/parent-graph.types.js +1 -0
- package/dist/debug/runtime/phases/RaphPhasePipeline.cjs +1 -0
- package/dist/debug/runtime/phases/RaphPhasePipeline.js +587 -0
- package/dist/debug/runtime/phases/orderPhaseTasks.cjs +1 -0
- package/dist/debug/runtime/phases/orderPhaseTasks.js +59 -0
- package/dist/debug/runtime/reactivity/PathHandle.cjs +1 -0
- package/dist/debug/runtime/{PathHandle.js → reactivity/PathHandle.js} +75 -39
- package/dist/debug/runtime/reactivity/RaphMetaSource.cjs +1 -0
- package/dist/debug/runtime/reactivity/RaphMetaSource.js +112 -0
- package/dist/debug/runtime/reactivity/RaphWorkGraph.cjs +1 -0
- package/dist/debug/runtime/reactivity/RaphWorkGraph.js +995 -0
- package/dist/debug/runtime/reactivity/RuntimeReactivity.cjs +1 -0
- package/dist/debug/runtime/reactivity/RuntimeReactivity.js +156 -0
- package/dist/debug/runtime/reactivity/RuntimeWatch.cjs +1 -0
- package/dist/debug/runtime/reactivity/RuntimeWatch.js +88 -0
- package/dist/debug/runtime/reactivity/runtime-reactivity.types.cjs +1 -0
- package/dist/debug/runtime/reactivity/runtime-reactivity.types.js +1 -0
- package/dist/entrypoints/machine-testing.d.ts +2 -0
- package/dist/entrypoints/machine.d.ts +5 -0
- package/dist/entrypoints/path.d.ts +14 -0
- package/dist/entrypoints/reactivity.d.ts +1 -0
- package/dist/entrypoints/runtime.d.ts +17 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +46 -38
- package/dist/machine/compiler.d.ts +71 -0
- package/dist/machine/core.d.ts +50 -0
- package/dist/machine/persistence.d.ts +5 -0
- package/dist/machine/runtime.d.ts +4 -0
- package/dist/machine/testing.d.ts +43 -0
- package/dist/machine/types.d.ts +155 -0
- package/dist/machine-testing.cjs +1 -0
- package/dist/machine-testing.js +188 -0
- package/dist/machine.cjs +1 -0
- package/dist/machine.js +837 -0
- package/dist/main.d.ts +20 -0
- package/dist/path/{data-path.d.ts → DataPath.d.ts} +16 -40
- package/dist/path/{path-router.d.ts → PathRouter.d.ts} +1 -1
- package/dist/path/RaphKernel.d.ts +13 -10
- package/dist/path/data-path.types.d.ts +34 -0
- package/dist/path/data-path.utils.d.ts +9 -0
- package/dist/path/{DerivedRegistry.d.ts → derived/DerivedRegistry.d.ts} +2 -2
- package/dist/path/{RaphDerivedHandle.d.ts → derived/RaphDerivedHandle.d.ts} +3 -3
- package/dist/path/derived/derived-collection.types.d.ts +12 -0
- package/dist/path/{derived-collection.d.ts → derived/derived-collection.utils.d.ts} +3 -13
- package/dist/path/{derived-strategies.d.ts → derived/derived-strategy.types.d.ts} +0 -3
- package/dist/path/derived/derived-strategy.utils.d.ts +4 -0
- package/dist/path/{MetaController.d.ts → meta/MetaController.d.ts} +4 -4
- package/dist/path/{RaphMeta.d.ts → meta/RaphMeta.d.ts} +2 -2
- package/dist/path/{RaphMetaStore.d.ts → meta/RaphMetaStore.d.ts} +1 -1
- package/dist/path/{meta.types.d.ts → meta/meta.types.d.ts} +2 -4
- package/dist/path/path.types.d.ts +3 -2
- package/dist/path/{segment-key.d.ts → pathSegmentKey.d.ts} +1 -1
- package/dist/path/{AdapterPathStore.d.ts → storage/AdapterPathStore.d.ts} +4 -4
- package/dist/path/{data-adapter.d.ts → storage/DefaultDataAdapter.d.ts} +2 -17
- package/dist/path/{PathEventHub.d.ts → storage/PathEventHub.d.ts} +9 -9
- package/dist/path/{PathStore.d.ts → storage/PathStore.d.ts} +3 -3
- package/dist/path/storage/data-adapter.types.d.ts +17 -0
- package/dist/path.cjs +1 -1
- package/dist/path.js +16 -16
- package/dist/raph.cjs +1 -1
- package/dist/raph.js +46 -38
- package/dist/reactivity/engine.d.ts +17 -3
- package/dist/reactivity/reactive-object.d.ts +36 -0
- package/dist/reactivity/types.d.ts +25 -0
- package/dist/reactivity.cjs +1 -1
- package/dist/reactivity.js +12 -12
- package/dist/runtime/RaphRuntime.d.ts +132 -192
- package/dist/runtime/diagnostics/RaphDebug.d.ts +239 -0
- package/dist/runtime/errors.d.ts +5 -0
- package/dist/runtime/ownership/RaphEffectScope.d.ts +29 -0
- package/dist/runtime/ownership/RaphNode.d.ts +40 -0
- package/dist/runtime/ownership/RaphScope.d.ts +38 -0
- package/dist/runtime/{parent-graph.d.ts → ownership/parent-graph.types.d.ts} +0 -1
- package/dist/runtime/ownership/parentGraph.d.ts +3 -0
- package/dist/runtime/phases/RaphPhasePipeline.d.ts +79 -0
- package/dist/runtime/phases/orderPhaseTasks.d.ts +6 -0
- package/dist/runtime/phases/phase.types.d.ts +92 -0
- package/dist/runtime/{PathHandle.d.ts → reactivity/PathHandle.d.ts} +14 -6
- package/dist/runtime/reactivity/RaphMetaSource.d.ts +32 -0
- package/dist/runtime/reactivity/RaphWorkGraph.d.ts +171 -0
- package/dist/runtime/{RuntimeReactivity.d.ts → reactivity/RuntimeReactivity.d.ts} +14 -9
- package/dist/runtime/{RuntimeWatch.d.ts → reactivity/RuntimeWatch.d.ts} +9 -3
- package/dist/runtime/{runtime-reactivity.types.d.ts → reactivity/runtime-reactivity.types.d.ts} +3 -1
- package/dist/runtime.cjs +1 -1
- package/dist/runtime.js +21 -16
- package/package.json +68 -35
- package/ReadMe.md +0 -153
- package/dist/api/public.d.ts +0 -22
- package/dist/debug/api/public.cjs +0 -1
- package/dist/debug/api/public.js +0 -40
- package/dist/debug/path/AdapterPathStore.cjs +0 -1
- package/dist/debug/path/DerivedRegistry.cjs +0 -1
- package/dist/debug/path/PathEventHub.cjs +0 -1
- package/dist/debug/path/PathStore.cjs +0 -1
- package/dist/debug/path/RaphMetaStore.cjs +0 -1
- package/dist/debug/path/data-adapter.cjs +0 -1
- package/dist/debug/path/data-path.cjs +0 -1
- package/dist/debug/path/derived-collection.cjs +0 -1
- package/dist/debug/path/path-router.cjs +0 -1
- package/dist/debug/path/public.cjs +0 -1
- package/dist/debug/path/public.js +0 -18
- package/dist/debug/runtime/PathHandle.cjs +0 -1
- package/dist/debug/runtime/RaphDebug.cjs +0 -1
- package/dist/debug/runtime/RaphDebug.js +0 -15
- package/dist/debug/runtime/RaphNode.cjs +0 -1
- package/dist/debug/runtime/RaphNode.js +0 -143
- package/dist/debug/runtime/RaphPhasePipeline.cjs +0 -1
- package/dist/debug/runtime/RaphPhasePipeline.js +0 -526
- package/dist/debug/runtime/RaphScope.cjs +0 -1
- package/dist/debug/runtime/RaphScope.js +0 -64
- package/dist/debug/runtime/RuntimeReactivity.cjs +0 -1
- package/dist/debug/runtime/RuntimeReactivity.js +0 -154
- package/dist/debug/runtime/RuntimeWatch.cjs +0 -1
- package/dist/debug/runtime/RuntimeWatch.js +0 -71
- package/dist/debug/runtime/public.cjs +0 -1
- package/dist/debug/runtime/public.js +0 -18
- package/dist/path/public.d.ts +0 -14
- package/dist/runtime/RaphDebug.d.ts +0 -32
- package/dist/runtime/RaphNode.d.ts +0 -68
- package/dist/runtime/RaphPhasePipeline.d.ts +0 -133
- package/dist/runtime/RaphScope.d.ts +0 -50
- package/dist/runtime/phase.types.d.ts +0 -49
- package/dist/runtime/public.d.ts +0 -13
- /package/dist/debug/path/{meta.types.cjs → data-path.types.cjs} +0 -0
- /package/dist/debug/path/{meta.types.js → data-path.types.js} +0 -0
- /package/dist/debug/path/{RaphDerivedHandle.cjs → derived/RaphDerivedHandle.cjs} +0 -0
- /package/dist/debug/path/{RaphDerivedHandle.js → derived/RaphDerivedHandle.js} +0 -0
- /package/dist/debug/{runtime/runtime-reactivity.types.cjs → path/derived/derived-collection.types.cjs} +0 -0
- /package/dist/debug/{runtime/runtime-reactivity.types.js → path/derived/derived-collection.types.js} +0 -0
- /package/dist/debug/path/{derived-strategies.cjs → derived/derived-strategy.utils.cjs} +0 -0
- /package/dist/debug/path/{derived-strategies.js → derived/derived-strategy.utils.js} +0 -0
- /package/dist/debug/path/{RaphMeta.cjs → meta/RaphMeta.cjs} +0 -0
- /package/dist/debug/path/{RaphMeta.js → meta/RaphMeta.js} +0 -0
- /package/dist/debug/path/{segment-key.cjs → pathSegmentKey.cjs} +0 -0
- /package/dist/debug/path/{segment-key.js → pathSegmentKey.js} +0 -0
- /package/dist/debug/path/{data-draft.cjs → storage/createDataDraft.cjs} +0 -0
- /package/dist/debug/path/{data-draft.js → storage/createDataDraft.js} +0 -0
- /package/dist/debug/runtime/{parent-graph.cjs → ownership/parentGraph.cjs} +0 -0
- /package/dist/debug/runtime/{parent-graph.js → ownership/parentGraph.js} +0 -0
- /package/dist/debug/runtime/{disposal.cjs → ownership/throwDisposalErrors.cjs} +0 -0
- /package/dist/debug/runtime/{disposal.js → ownership/throwDisposalErrors.js} +0 -0
- /package/dist/debug/runtime/{RaphFrameLoop.cjs → phases/RaphFrameLoop.cjs} +0 -0
- /package/dist/debug/runtime/{RaphFrameLoop.js → phases/RaphFrameLoop.js} +0 -0
- /package/dist/debug/runtime/{phase.types.cjs → phases/phase.types.cjs} +0 -0
- /package/dist/debug/runtime/{phase.types.js → phases/phase.types.js} +0 -0
- /package/dist/path/{data-draft.d.ts → storage/createDataDraft.d.ts} +0 -0
- /package/dist/runtime/{disposal.d.ts → ownership/throwDisposalErrors.d.ts} +0 -0
- /package/dist/runtime/{RaphFrameLoop.d.ts → phases/RaphFrameLoop.d.ts} +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,1096 @@
|
|
|
1
|
+
# Raph: from your first signal to a state machine
|
|
2
|
+
|
|
3
|
+
Raph has three layers that you can use independently: a reactive graph,
|
|
4
|
+
a runtime that owns and schedules work, and a state machine for processes
|
|
5
|
+
with explicit transitions. This guide builds them up in that order.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @raphy-js/raph@dev
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The examples are independent unless a section says otherwise. Short
|
|
12
|
+
fragments reuse names from their section. Imports use public package
|
|
13
|
+
entrypoints.
|
|
14
|
+
|
|
15
|
+
```mermaid
|
|
16
|
+
flowchart LR
|
|
17
|
+
A["Reactivity<br/>signal, computed, effect"] --> B["Runtime<br/>ownership, lifecycle, scheduler"]
|
|
18
|
+
B --> C["Phases<br/>update, render, commit..."]
|
|
19
|
+
B --> D["Machine execution<br/>createMachine"]
|
|
20
|
+
E["Pure machine<br/>defineMachine, transition"] --> D
|
|
21
|
+
D --> F["Reactive snapshot"]
|
|
22
|
+
F --> C
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 1. Standalone reactivity
|
|
26
|
+
|
|
27
|
+
Start with the low-level reactive graph:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import {
|
|
31
|
+
signal,
|
|
32
|
+
computed,
|
|
33
|
+
effect,
|
|
34
|
+
watch,
|
|
35
|
+
batch,
|
|
36
|
+
untracked,
|
|
37
|
+
} from '@raphy-js/raph/reactivity'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
There is no runtime, ownership tree, phase scheduler, or state machine here.
|
|
41
|
+
|
|
42
|
+
### 1.1. Signal
|
|
43
|
+
|
|
44
|
+
A signal holds a mutable reactive value:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
const counter = signal(0)
|
|
48
|
+
|
|
49
|
+
console.log(counter.get()) // 0
|
|
50
|
+
counter.set(1)
|
|
51
|
+
console.log(counter.get()) // 1
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Its main operations are `get()` (read and track a dependency), `peek()`
|
|
55
|
+
(read without tracking), and `set(value)` (write). Call the method to read
|
|
56
|
+
the value: `counter.get` is the function itself.
|
|
57
|
+
|
|
58
|
+
### 1.2. Computed
|
|
59
|
+
|
|
60
|
+
`computed` derives a value from other reactive values:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
const counter = signal(2)
|
|
64
|
+
const doubled = computed(() => counter.get() * 2)
|
|
65
|
+
|
|
66
|
+
console.log(doubled.get()) // 4
|
|
67
|
+
counter.set(5)
|
|
68
|
+
console.log(doubled.get()) // 10
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
It has no independent mutable state. Raph discovers its dependencies,
|
|
72
|
+
caches the result, and recomputes lazily when it is read again.
|
|
73
|
+
|
|
74
|
+
```mermaid
|
|
75
|
+
flowchart LR
|
|
76
|
+
C["counter<br/>signal"] --> D["doubled<br/>computed"]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 1.3. Effect
|
|
80
|
+
|
|
81
|
+
An effect reacts to values read while it runs:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const counter = signal(0)
|
|
85
|
+
|
|
86
|
+
const stop = effect(() => {
|
|
87
|
+
console.log('counter:', counter.get())
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Immediately: counter: 0
|
|
91
|
+
counter.set(1) // counter: 1
|
|
92
|
+
counter.set(2) // counter: 2
|
|
93
|
+
|
|
94
|
+
stop()
|
|
95
|
+
counter.set(3) // No further effect run
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The call to `counter.get()` establishes the dependency. An effect may
|
|
99
|
+
return cleanup, which runs before its next execution and when stopped:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
const stop = effect(() => {
|
|
103
|
+
const value = counter.get()
|
|
104
|
+
console.log('start', value)
|
|
105
|
+
|
|
106
|
+
return () => console.log('cleanup', value)
|
|
107
|
+
})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 1.4. A complete reactive graph
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const c = signal(0)
|
|
114
|
+
const d = computed(() => c.get() * 2)
|
|
115
|
+
|
|
116
|
+
const stop = effect(() => {
|
|
117
|
+
console.log({ c: c.get(), d: d.get() })
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The first effect run creates three edges:
|
|
122
|
+
|
|
123
|
+
```mermaid
|
|
124
|
+
flowchart LR
|
|
125
|
+
C["C: signal"] -->|E1| D["D: computed"]
|
|
126
|
+
D -->|E2| E["E: effect"]
|
|
127
|
+
C -->|E3| E
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
After `c.set(1)`, `d` becomes stale and the effect is scheduled. When the
|
|
131
|
+
effect reads `d.get()`, it recomputes and logs `{ c: 1, d: 2 }`.
|
|
132
|
+
|
|
133
|
+
### 1.5. Dynamic dependencies
|
|
134
|
+
|
|
135
|
+
Dependencies can change between effect runs:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
const enabled = signal(true)
|
|
139
|
+
const first = signal(10)
|
|
140
|
+
const second = signal(20)
|
|
141
|
+
|
|
142
|
+
effect(() => {
|
|
143
|
+
console.log(enabled.get() ? first.get() : second.get())
|
|
144
|
+
})
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
When `enabled` is true, the effect depends on `enabled` and `first`.
|
|
148
|
+
When it becomes false, the edge to `first` is removed and an edge to
|
|
149
|
+
`second` is added.
|
|
150
|
+
|
|
151
|
+
### 1.6. Watch
|
|
152
|
+
|
|
153
|
+
Use `watch` when you need the previous and current values of an explicit
|
|
154
|
+
source:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
const counter = signal(0)
|
|
158
|
+
|
|
159
|
+
const stop = watch(
|
|
160
|
+
counter,
|
|
161
|
+
(value, previous) => console.log({ previous, value }),
|
|
162
|
+
{ immediate: true },
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
counter.set(1)
|
|
166
|
+
counter.set(2)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
An `effect` tracks everything it reads. A `watch` observes the source you
|
|
170
|
+
provide; reads inside its callback do not add dependencies.
|
|
171
|
+
|
|
172
|
+
### 1.7. Batch
|
|
173
|
+
|
|
174
|
+
`batch` groups writes into one reactive boundary:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const firstName = signal('John')
|
|
178
|
+
const lastName = signal('Smith')
|
|
179
|
+
const fullName = computed(() => `${firstName.get()} ${lastName.get()}`)
|
|
180
|
+
|
|
181
|
+
effect(() => console.log(fullName.get()))
|
|
182
|
+
|
|
183
|
+
batch(() => {
|
|
184
|
+
firstName.set('Jane')
|
|
185
|
+
lastName.set('Doe')
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The effect sees `Jane Doe`, rather than the intermediate `Jane Smith`.
|
|
190
|
+
`batch` does not provide rollback: writes already made remain if its
|
|
191
|
+
callback throws.
|
|
192
|
+
|
|
193
|
+
### 1.8. Untracked
|
|
194
|
+
|
|
195
|
+
`untracked` reads a value without creating a dependency:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
effect(() => {
|
|
199
|
+
const reactiveValue = counter.get()
|
|
200
|
+
const informationalValue = untracked(() => someOtherSignal.get())
|
|
201
|
+
console.log(reactiveValue, informationalValue)
|
|
202
|
+
})
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
This effect depends on `counter`, but not `someOtherSignal`.
|
|
206
|
+
|
|
207
|
+
## 2. Add a runtime
|
|
208
|
+
|
|
209
|
+
Standalone reactivity does not know who owns a signal, when to dispose an
|
|
210
|
+
effect, or which phase should deliver it. A runtime supplies those rules:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
import {
|
|
214
|
+
RaphRuntime,
|
|
215
|
+
RaphSchedulerType,
|
|
216
|
+
} from '@raphy-js/raph/runtime'
|
|
217
|
+
|
|
218
|
+
const runtime = new RaphRuntime({
|
|
219
|
+
id: 'application',
|
|
220
|
+
scheduler: RaphSchedulerType.Microtask,
|
|
221
|
+
})
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`Sync` flushes in the current call stack, `Microtask` in the next
|
|
225
|
+
microtask, and `AnimationFrame` in an animation frame.
|
|
226
|
+
|
|
227
|
+
### 2.1. Phases
|
|
228
|
+
|
|
229
|
+
Define phases before registering effects:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
runtime.definePhases([
|
|
233
|
+
{ name: 'update' },
|
|
234
|
+
{ name: 'render' },
|
|
235
|
+
])
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
They run in array order: `update -> render`. A phase is a named work queue,
|
|
239
|
+
not a callback you must supply. Without `process`, the runtime runs its
|
|
240
|
+
queued tasks automatically.
|
|
241
|
+
|
|
242
|
+
### 2.2. Nodes and ownership
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
const counterNode = runtime.node({ id: 'counter' })
|
|
246
|
+
const counter = counterNode.signal(0)
|
|
247
|
+
|
|
248
|
+
counterNode.effect('render', () => {
|
|
249
|
+
console.log('render counter:', counter.get())
|
|
250
|
+
})
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
The node owns its signal and effect. `counterNode.dispose()` releases
|
|
254
|
+
owned effects, watchers, signals, computed values, machines, custom
|
|
255
|
+
cleanup, and child nodes. With standalone reactivity, you normally keep
|
|
256
|
+
each disposer yourself.
|
|
257
|
+
|
|
258
|
+
### 2.3. What a signal write does
|
|
259
|
+
|
|
260
|
+
`counter.set(1)` invalidates the graph. It queues a persistent phase task,
|
|
261
|
+
and the scheduler requests a flush:
|
|
262
|
+
|
|
263
|
+
```mermaid
|
|
264
|
+
sequenceDiagram
|
|
265
|
+
participant Code
|
|
266
|
+
participant Signal
|
|
267
|
+
participant Graph
|
|
268
|
+
participant Phase
|
|
269
|
+
participant Scheduler
|
|
270
|
+
participant Effect
|
|
271
|
+
|
|
272
|
+
Code->>Signal: set(1)
|
|
273
|
+
Signal->>Graph: invalidate
|
|
274
|
+
Graph->>Phase: enqueue persistent task
|
|
275
|
+
Phase->>Scheduler: request flush
|
|
276
|
+
Scheduler->>Phase: begin render
|
|
277
|
+
Phase->>Effect: task.run()
|
|
278
|
+
Effect->>Signal: get()
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
With `Microtask`, the effect does not run inside `set()`:
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
counter.set(1)
|
|
285
|
+
console.log('This runs before the render effect')
|
|
286
|
+
await runtime.afterFlush()
|
|
287
|
+
console.log('All scheduled phases have finished')
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
`Sync` may flush in the same call stack, but effects still pass through
|
|
291
|
+
phase tasks.
|
|
292
|
+
|
|
293
|
+
### 2.4. Complete runtime example
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
import {
|
|
297
|
+
RaphRuntime,
|
|
298
|
+
RaphSchedulerType,
|
|
299
|
+
} from '@raphy-js/raph/runtime'
|
|
300
|
+
|
|
301
|
+
async function main() {
|
|
302
|
+
const runtime = new RaphRuntime({
|
|
303
|
+
id: 'counter-app',
|
|
304
|
+
scheduler: RaphSchedulerType.Microtask,
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
runtime.definePhases([
|
|
308
|
+
{ name: 'update' },
|
|
309
|
+
{ name: 'render' },
|
|
310
|
+
])
|
|
311
|
+
|
|
312
|
+
const counterNode = runtime.node({ id: 'counter' })
|
|
313
|
+
const counter = counterNode.signal(0)
|
|
314
|
+
const doubled = counterNode.computed(() => counter.get() * 2)
|
|
315
|
+
|
|
316
|
+
counterNode.effect('render', () => {
|
|
317
|
+
console.log({ counter: counter.get(), doubled: doubled.get() })
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
await runtime.afterFlush() // Initial effect delivery
|
|
321
|
+
|
|
322
|
+
counter.set(1)
|
|
323
|
+
counter.set(2)
|
|
324
|
+
await runtime.afterFlush() // Both writes coalesce into one phase delivery
|
|
325
|
+
|
|
326
|
+
runtime.dispose()
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
void main()
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### 2.5. Custom phase processing
|
|
333
|
+
|
|
334
|
+
The default phase runner calls `task.run()` for each task. Define
|
|
335
|
+
`process` only when the phase needs to control delivery:
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
runtime.definePhases([
|
|
339
|
+
{
|
|
340
|
+
name: 'render',
|
|
341
|
+
process(batch) {
|
|
342
|
+
console.log('frame:', batch.frame.frame)
|
|
343
|
+
for (const task of batch.tasks) task.run()
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
])
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
A custom processor must resolve every task with `run()` (now), `defer()`
|
|
350
|
+
(next scheduler turn), or `cancel()` (discard this delivery). Leaving a
|
|
351
|
+
task unresolved makes the runtime defer it and report
|
|
352
|
+
`RaphPhaseProcessError`. An implicit effect's initial task may not be
|
|
353
|
+
cancelled before its first dependency collection.
|
|
354
|
+
|
|
355
|
+
### 2.6. Effects and watches in a runtime
|
|
356
|
+
|
|
357
|
+
An effect collects implicit dependencies:
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
node.effect('render', () => console.log(nodeSignal.get()))
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
A watch names its source explicitly:
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
viewNode.watch(modelSignal, 'render', () => {
|
|
367
|
+
console.log('model changed:', modelSignal.peek())
|
|
368
|
+
})
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
An effect may implicitly read producers owned by its own node, an
|
|
372
|
+
ancestor, or the runtime root scope. A sibling branch requires an
|
|
373
|
+
explicit `watch`:
|
|
374
|
+
|
|
375
|
+
```text
|
|
376
|
+
root
|
|
377
|
+
├── modelNode
|
|
378
|
+
│ └── signal
|
|
379
|
+
└── viewNode
|
|
380
|
+
└── watch(modelSignal)
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
The task belongs to `viewNode`; the signal owned by `modelNode` causes it.
|
|
384
|
+
|
|
385
|
+
### 2.7. Actions
|
|
386
|
+
|
|
387
|
+
An action is a reusable named operation run as `batch + untracked`:
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
const increment = counterNode.action('increment', () => {
|
|
391
|
+
counter.set(counter.peek() + 1)
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
increment()
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
When the runtime is created with `enforceActions: true`, writes are
|
|
398
|
+
allowed only inside an action or batch.
|
|
399
|
+
|
|
400
|
+
## 3. Why use a state machine?
|
|
401
|
+
|
|
402
|
+
You could model a dialog with three signals:
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
const isOpen = signal(false)
|
|
406
|
+
const isClosing = signal(false)
|
|
407
|
+
const hasOverlay = signal(false)
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
That leaves room for combinations that make no sense, such as a closed
|
|
411
|
+
dialog that is still closing. A machine names the allowed states and the
|
|
412
|
+
events that move between them:
|
|
413
|
+
|
|
414
|
+
```mermaid
|
|
415
|
+
stateDiagram-v2
|
|
416
|
+
[*] --> closed
|
|
417
|
+
closed --> open: OPEN
|
|
418
|
+
open --> closed: CLOSE
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
A signal answers "what value is there?" A computed value derives another
|
|
422
|
+
value; an effect runs after change. A machine answers "which mode is this
|
|
423
|
+
process in, and which transitions are allowed?"
|
|
424
|
+
|
|
425
|
+
## 4. Machine vocabulary
|
|
426
|
+
|
|
427
|
+
| Term | Meaning |
|
|
428
|
+
| --- | --- |
|
|
429
|
+
| Model | Compiled machine description |
|
|
430
|
+
| State | Allowed mode of the process |
|
|
431
|
+
| Event | External occurrence or intent |
|
|
432
|
+
| Transition | Current state's response to an event |
|
|
433
|
+
| Context | Data carried across states |
|
|
434
|
+
| Guard | Pure check that permits or rejects a transition |
|
|
435
|
+
| Reducer | Pure calculation of the next context |
|
|
436
|
+
| Snapshot | Immutable view of the machine state |
|
|
437
|
+
| Output | One-time side effect after commit |
|
|
438
|
+
| Resource | Side effect active for a state's lifetime |
|
|
439
|
+
| Task | Asynchronous work tied to a state |
|
|
440
|
+
| Delay | Timed transition driven by a `MachineClock` |
|
|
441
|
+
| Actor | Child machine with its own queue and lifecycle |
|
|
442
|
+
|
|
443
|
+
### 4.1. State and context are different
|
|
444
|
+
|
|
445
|
+
```text
|
|
446
|
+
state: open
|
|
447
|
+
context: {
|
|
448
|
+
opens: 4,
|
|
449
|
+
lastCloseReason: "escape"
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
The state names the mode. The context stores data. A counter belongs in
|
|
454
|
+
context; it does not need states such as `openWithTwoPreviousOpens`.
|
|
455
|
+
|
|
456
|
+
## 5. A pure machine without a runtime
|
|
457
|
+
|
|
458
|
+
Machine APIs live in their own entrypoint:
|
|
459
|
+
|
|
460
|
+
```ts
|
|
461
|
+
import {
|
|
462
|
+
defineMachine,
|
|
463
|
+
getInitialSnapshot,
|
|
464
|
+
transition,
|
|
465
|
+
} from '@raphy-js/raph/machine'
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
They are separate from the reactivity and runtime entrypoints.
|
|
469
|
+
|
|
470
|
+
### 5.1. Define context and events
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
interface DialogContext {
|
|
474
|
+
readonly opens: number
|
|
475
|
+
readonly lastCloseReason?: 'button' | 'escape'
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
type DialogEvent =
|
|
479
|
+
| { readonly type: 'OPEN' }
|
|
480
|
+
| {
|
|
481
|
+
readonly type: 'CLOSE'
|
|
482
|
+
readonly reason: 'button' | 'escape'
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### 5.2. Define the model
|
|
487
|
+
|
|
488
|
+
```ts
|
|
489
|
+
import {
|
|
490
|
+
defineMachine,
|
|
491
|
+
type MachineBindings,
|
|
492
|
+
} from '@raphy-js/raph/machine'
|
|
493
|
+
|
|
494
|
+
const dialogModel = defineMachine<DialogContext, DialogEvent>({
|
|
495
|
+
id: 'dialog',
|
|
496
|
+
context: { opens: 0 },
|
|
497
|
+
initial: 'closed',
|
|
498
|
+
states: {
|
|
499
|
+
closed: {
|
|
500
|
+
on: {
|
|
501
|
+
OPEN: {
|
|
502
|
+
target: 'open',
|
|
503
|
+
reduce: 'countOpen',
|
|
504
|
+
emit: 'opened',
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
open: {
|
|
509
|
+
tags: ['visible'],
|
|
510
|
+
on: {
|
|
511
|
+
CLOSE: {
|
|
512
|
+
target: 'closed',
|
|
513
|
+
reduce: 'rememberCloseReason',
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
})
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
`defineMachine()` validates the description, compiles paths and
|
|
522
|
+
transitions, and returns an immutable `MachineModel`. It does not start
|
|
523
|
+
anything.
|
|
524
|
+
|
|
525
|
+
### 5.3. Bind the named operations
|
|
526
|
+
|
|
527
|
+
The names in the model are connected to implementations separately:
|
|
528
|
+
|
|
529
|
+
```ts
|
|
530
|
+
const dialogBindings: MachineBindings<
|
|
531
|
+
DialogContext,
|
|
532
|
+
DialogEvent,
|
|
533
|
+
void
|
|
534
|
+
> = {
|
|
535
|
+
reducers: {
|
|
536
|
+
countOpen: ({ context }) => ({
|
|
537
|
+
...context,
|
|
538
|
+
opens: context.opens + 1,
|
|
539
|
+
}),
|
|
540
|
+
|
|
541
|
+
rememberCloseReason: ({ context, event }) => ({
|
|
542
|
+
...context,
|
|
543
|
+
lastCloseReason:
|
|
544
|
+
event.type === 'CLOSE'
|
|
545
|
+
? event.reason
|
|
546
|
+
: context.lastCloseReason,
|
|
547
|
+
}),
|
|
548
|
+
},
|
|
549
|
+
|
|
550
|
+
outputs: {
|
|
551
|
+
opened: ({ snapshot }) => {
|
|
552
|
+
console.log('Dialog committed:', snapshot.active)
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
}
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
A reducer computes `next context` from context, event, and input. It
|
|
559
|
+
should not mutate the DOM, fetch data, write a signal, or start a timer.
|
|
560
|
+
Use outputs, resources, or tasks for those effects.
|
|
561
|
+
|
|
562
|
+
### 5.4. Get the initial snapshot
|
|
563
|
+
|
|
564
|
+
```ts
|
|
565
|
+
let snapshot = getInitialSnapshot(dialogModel)
|
|
566
|
+
console.log(snapshot)
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
In simplified form:
|
|
570
|
+
|
|
571
|
+
```ts
|
|
572
|
+
{
|
|
573
|
+
active: ['closed'],
|
|
574
|
+
context: { opens: 0 },
|
|
575
|
+
history: {},
|
|
576
|
+
revision: 0,
|
|
577
|
+
status: 'active',
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
Snapshots are immutable; do not edit them by hand.
|
|
582
|
+
|
|
583
|
+
### 5.5. Run a pure transition
|
|
584
|
+
|
|
585
|
+
```ts
|
|
586
|
+
snapshot = transition(
|
|
587
|
+
dialogModel,
|
|
588
|
+
snapshot,
|
|
589
|
+
{ type: 'OPEN' },
|
|
590
|
+
dialogBindings,
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
console.log(snapshot.active) // ['open']
|
|
594
|
+
console.log(snapshot.context.opens) // 1
|
|
595
|
+
console.log(snapshot.revision) // 1
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
`transition()` calculates the next snapshot. It does not run outputs,
|
|
599
|
+
resources, timers, or tasks, so the `opened` output above does not log.
|
|
600
|
+
This pure path is useful for transition analysis, deterministic replay,
|
|
601
|
+
SSR, and tests without a runtime.
|
|
602
|
+
|
|
603
|
+
### 5.6. Ignored events
|
|
604
|
+
|
|
605
|
+
The `open` state has no handler for another `OPEN`:
|
|
606
|
+
|
|
607
|
+
```ts
|
|
608
|
+
const sameSnapshot = transition(
|
|
609
|
+
dialogModel,
|
|
610
|
+
snapshot,
|
|
611
|
+
{ type: 'OPEN' },
|
|
612
|
+
dialogBindings,
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
console.log(sameSnapshot === snapshot) // true
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
An ignored event returns the same snapshot object.
|
|
619
|
+
|
|
620
|
+
## 6. A live machine inside a runtime
|
|
621
|
+
|
|
622
|
+
`createMachine` connects the pure model to runtime execution:
|
|
623
|
+
|
|
624
|
+
```ts
|
|
625
|
+
import { createMachine } from '@raphy-js/raph/machine'
|
|
626
|
+
|
|
627
|
+
const machine = createMachine(
|
|
628
|
+
dialogNode,
|
|
629
|
+
dialogModel,
|
|
630
|
+
dialogBindings,
|
|
631
|
+
).start()
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
The owner is a `RaphNode` or `RaphScope`. A standalone reactive scope
|
|
635
|
+
does not provide the lifecycle and execution boundary a live machine
|
|
636
|
+
needs. The following example uses `dialogModel` and `dialogBindings`
|
|
637
|
+
from section 5.
|
|
638
|
+
|
|
639
|
+
### 6.1. Complete live example
|
|
640
|
+
|
|
641
|
+
```ts
|
|
642
|
+
import {
|
|
643
|
+
RaphRuntime,
|
|
644
|
+
RaphSchedulerType,
|
|
645
|
+
} from '@raphy-js/raph/runtime'
|
|
646
|
+
import { createMachine } from '@raphy-js/raph/machine'
|
|
647
|
+
|
|
648
|
+
async function main() {
|
|
649
|
+
const runtime = new RaphRuntime({
|
|
650
|
+
id: 'dialog-app',
|
|
651
|
+
scheduler: RaphSchedulerType.Microtask,
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
runtime.definePhases([
|
|
655
|
+
{ name: 'update' },
|
|
656
|
+
{ name: 'render' },
|
|
657
|
+
])
|
|
658
|
+
|
|
659
|
+
const dialogNode = runtime.node({ id: 'dialog' })
|
|
660
|
+
const machine = createMachine(
|
|
661
|
+
dialogNode,
|
|
662
|
+
dialogModel,
|
|
663
|
+
dialogBindings,
|
|
664
|
+
).start()
|
|
665
|
+
|
|
666
|
+
dialogNode.effect('render', () => {
|
|
667
|
+
// snapshot is a regular ReadonlySignal.
|
|
668
|
+
const snapshot = machine.snapshot.get()
|
|
669
|
+
console.log('render dialog', {
|
|
670
|
+
active: snapshot.active,
|
|
671
|
+
context: snapshot.context,
|
|
672
|
+
revision: snapshot.revision,
|
|
673
|
+
})
|
|
674
|
+
})
|
|
675
|
+
|
|
676
|
+
await runtime.afterFlush() // Initial render
|
|
677
|
+
|
|
678
|
+
const result = machine.send({ type: 'OPEN' })
|
|
679
|
+
|
|
680
|
+
// send() has already updated the snapshot synchronously.
|
|
681
|
+
console.log(result.status) // transitioned
|
|
682
|
+
console.log(machine.matches('open')) // true
|
|
683
|
+
console.log(machine.hasTag('visible')) // true
|
|
684
|
+
|
|
685
|
+
await runtime.afterFlush() // Deliver the render effect
|
|
686
|
+
|
|
687
|
+
machine.send({ type: 'CLOSE', reason: 'escape' })
|
|
688
|
+
await runtime.afterFlush()
|
|
689
|
+
|
|
690
|
+
console.log(
|
|
691
|
+
machine.snapshot.peek().context.lastCloseReason,
|
|
692
|
+
) // escape
|
|
693
|
+
|
|
694
|
+
runtime.dispose()
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
void main()
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
### 6.2. What happens during `send()`
|
|
701
|
+
|
|
702
|
+
```mermaid
|
|
703
|
+
sequenceDiagram
|
|
704
|
+
participant Caller
|
|
705
|
+
participant Machine
|
|
706
|
+
participant PureCore
|
|
707
|
+
participant Snapshot
|
|
708
|
+
participant Lifecycle
|
|
709
|
+
participant Phase
|
|
710
|
+
|
|
711
|
+
Caller->>Machine: send(OPEN)
|
|
712
|
+
Machine->>Machine: enqueue event in FIFO
|
|
713
|
+
Machine->>PureCore: guards + reducers + transition
|
|
714
|
+
PureCore-->>Machine: next snapshot + effect plan
|
|
715
|
+
Machine->>Snapshot: one commit inside runtime.batch
|
|
716
|
+
Machine->>Lifecycle: outputs/resources/timers/tasks
|
|
717
|
+
Snapshot->>Phase: invalidate render effect
|
|
718
|
+
Machine-->>Caller: MachineSendResult
|
|
719
|
+
Phase->>Phase: scheduler flush
|
|
720
|
+
Phase->>Snapshot: effect reads snapshot.get()
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
Guards, reducers, transition calculation, snapshot commit, and machine
|
|
724
|
+
lifecycle work happen in the machine macrostep. Effects and explicit
|
|
725
|
+
watches that read `machine.snapshot` run through runtime phases. A phase
|
|
726
|
+
controls reactive runtime delivery; it is not the sole executor of
|
|
727
|
+
machine logic.
|
|
728
|
+
|
|
729
|
+
## 7. Guards
|
|
730
|
+
|
|
731
|
+
A guard decides whether a transition is allowed:
|
|
732
|
+
|
|
733
|
+
```ts
|
|
734
|
+
interface Context {
|
|
735
|
+
readonly canClose: boolean
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
type Event =
|
|
739
|
+
| { readonly type: 'OPEN' }
|
|
740
|
+
| { readonly type: 'CLOSE' }
|
|
741
|
+
|
|
742
|
+
const model = defineMachine<Context, Event>({
|
|
743
|
+
id: 'guarded-dialog',
|
|
744
|
+
context: { canClose: false },
|
|
745
|
+
initial: 'closed',
|
|
746
|
+
states: {
|
|
747
|
+
closed: {
|
|
748
|
+
on: { OPEN: 'open' },
|
|
749
|
+
},
|
|
750
|
+
open: {
|
|
751
|
+
on: {
|
|
752
|
+
CLOSE: {
|
|
753
|
+
target: 'closed',
|
|
754
|
+
guard: 'canClose',
|
|
755
|
+
},
|
|
756
|
+
},
|
|
757
|
+
},
|
|
758
|
+
},
|
|
759
|
+
})
|
|
760
|
+
|
|
761
|
+
const bindings = {
|
|
762
|
+
guards: {
|
|
763
|
+
canClose: ({ context }: { context: Readonly<Context> }) => {
|
|
764
|
+
return context.canClose
|
|
765
|
+
},
|
|
766
|
+
},
|
|
767
|
+
}
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
When a guard returns `false`, Raph checks the next transition in
|
|
771
|
+
declaration order. If none matches, the event is ignored. Guards should
|
|
772
|
+
be pure.
|
|
773
|
+
|
|
774
|
+
## 8. Outputs and resources
|
|
775
|
+
|
|
776
|
+
### 8.1. Output
|
|
777
|
+
|
|
778
|
+
An output is one-time work after a successful commit. A transition can
|
|
779
|
+
name one:
|
|
780
|
+
|
|
781
|
+
```ts
|
|
782
|
+
closed: {
|
|
783
|
+
on: {
|
|
784
|
+
OPEN: {
|
|
785
|
+
target: 'open',
|
|
786
|
+
emit: 'notifyOpened',
|
|
787
|
+
},
|
|
788
|
+
},
|
|
789
|
+
}
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
Bind its implementation separately:
|
|
793
|
+
|
|
794
|
+
```ts
|
|
795
|
+
outputs: {
|
|
796
|
+
notifyOpened: ({ event, snapshot }) => {
|
|
797
|
+
console.log(event.type)
|
|
798
|
+
console.log(snapshot.active) // Already ['open']
|
|
799
|
+
},
|
|
800
|
+
}
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
The snapshot has already been published. An output error does not roll
|
|
804
|
+
back that commit.
|
|
805
|
+
|
|
806
|
+
### 8.2. Resource
|
|
807
|
+
|
|
808
|
+
A resource lives while its state is active:
|
|
809
|
+
|
|
810
|
+
```ts
|
|
811
|
+
open: {
|
|
812
|
+
resources: ['presence'],
|
|
813
|
+
on: { CLOSE: 'closed' },
|
|
814
|
+
}
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
```ts
|
|
818
|
+
resources: {
|
|
819
|
+
presence: ({ signal }) => {
|
|
820
|
+
console.log('open-state resource started')
|
|
821
|
+
|
|
822
|
+
signal.addEventListener('abort', () => {
|
|
823
|
+
console.log('resource aborted')
|
|
824
|
+
})
|
|
825
|
+
|
|
826
|
+
return () => console.log('open-state resource cleaned')
|
|
827
|
+
},
|
|
828
|
+
}
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
Raph starts it when entering `open`. It cleans up when leaving `open`,
|
|
832
|
+
stopping the machine, disposing its owner, or disposing the runtime.
|
|
833
|
+
|
|
834
|
+
## 9. Delays
|
|
835
|
+
|
|
836
|
+
A state may request a timed transition:
|
|
837
|
+
|
|
838
|
+
```ts
|
|
839
|
+
visible: {
|
|
840
|
+
after: {
|
|
841
|
+
autoClose: 'hidden',
|
|
842
|
+
},
|
|
843
|
+
}
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
The binding supplies the duration:
|
|
847
|
+
|
|
848
|
+
```ts
|
|
849
|
+
const machine = createMachine(owner, model, {
|
|
850
|
+
delays: { autoClose: 2_000 },
|
|
851
|
+
}).start()
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
You can pass a custom `MachineClock` to test time without waiting for a
|
|
855
|
+
real timer.
|
|
856
|
+
|
|
857
|
+
## 10. Asynchronous tasks
|
|
858
|
+
|
|
859
|
+
An invoked task belongs to the state that starts it:
|
|
860
|
+
|
|
861
|
+
```ts
|
|
862
|
+
const loaderModel = defineMachine({
|
|
863
|
+
id: 'loader',
|
|
864
|
+
context: {},
|
|
865
|
+
initial: 'idle',
|
|
866
|
+
states: {
|
|
867
|
+
idle: {
|
|
868
|
+
on: { LOAD: 'loading' },
|
|
869
|
+
},
|
|
870
|
+
loading: {
|
|
871
|
+
invoke: {
|
|
872
|
+
src: 'loadData',
|
|
873
|
+
onDone: 'success',
|
|
874
|
+
onError: 'failed',
|
|
875
|
+
},
|
|
876
|
+
on: { CANCEL: 'idle' },
|
|
877
|
+
},
|
|
878
|
+
success: {},
|
|
879
|
+
failed: {},
|
|
880
|
+
},
|
|
881
|
+
})
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
Bind the task at runtime:
|
|
885
|
+
|
|
886
|
+
```ts
|
|
887
|
+
const loader = createMachine(owner, loaderModel, {
|
|
888
|
+
tasks: {
|
|
889
|
+
loadData: async ({ signal }) => {
|
|
890
|
+
const response = await fetch('/api/data', { signal })
|
|
891
|
+
return response.json()
|
|
892
|
+
},
|
|
893
|
+
},
|
|
894
|
+
}).start()
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
If `loader.send({ type: 'CANCEL' })` leaves `loading`, the task is
|
|
898
|
+
aborted. A late result from that old task is ignored and cannot change
|
|
899
|
+
the current state.
|
|
900
|
+
|
|
901
|
+
## 11. Nested states
|
|
902
|
+
|
|
903
|
+
Machines support compound states:
|
|
904
|
+
|
|
905
|
+
```ts
|
|
906
|
+
const model = defineMachine({
|
|
907
|
+
id: 'dialog',
|
|
908
|
+
context: {},
|
|
909
|
+
initial: 'closed',
|
|
910
|
+
states: {
|
|
911
|
+
closed: {
|
|
912
|
+
on: { OPEN: 'open.idle' },
|
|
913
|
+
},
|
|
914
|
+
open: {
|
|
915
|
+
initial: 'idle',
|
|
916
|
+
states: {
|
|
917
|
+
idle: {
|
|
918
|
+
on: { CLOSE: 'open.closing' },
|
|
919
|
+
},
|
|
920
|
+
closing: {
|
|
921
|
+
always: 'closed',
|
|
922
|
+
},
|
|
923
|
+
},
|
|
924
|
+
},
|
|
925
|
+
},
|
|
926
|
+
})
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
An active leaf path looks like `['open.idle']`. While there,
|
|
930
|
+
`machine.matches('open')` and `machine.matches('open.idle')` are both
|
|
931
|
+
true. Machines also support parallel states, shallow and deep history,
|
|
932
|
+
final states, `onDone`, `always`, `raise`, tags, metadata, and actors.
|
|
933
|
+
|
|
934
|
+
Within a macrostep, Raph stabilizes internal transitions. A limit of 100
|
|
935
|
+
microsteps protects against endless `always` or `raise` cycles.
|
|
936
|
+
|
|
937
|
+
## 12. The event queue
|
|
938
|
+
|
|
939
|
+
If an output, task, or actor sends an event while another is being
|
|
940
|
+
processed, Raph puts it in one FIFO queue. It does not recursively run a
|
|
941
|
+
nested transition:
|
|
942
|
+
|
|
943
|
+
```text
|
|
944
|
+
OPEN
|
|
945
|
+
├── commit open
|
|
946
|
+
└── output sends FOCUS
|
|
947
|
+
|
|
948
|
+
FIFO:
|
|
949
|
+
finish OPEN
|
|
950
|
+
then process FOCUS
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
This keeps ordering deterministic and avoids reentrant transitions.
|
|
954
|
+
|
|
955
|
+
## 13. Lifecycle order
|
|
956
|
+
|
|
957
|
+
A transition performs lifecycle work in this order:
|
|
958
|
+
|
|
959
|
+
```text
|
|
960
|
+
1. Clean up resources, tasks, and timers for exited states
|
|
961
|
+
2. Exit outputs
|
|
962
|
+
3. Transition outputs
|
|
963
|
+
4. Entry outputs
|
|
964
|
+
5. Entry resources
|
|
965
|
+
6. Timers and invokes
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
Guards and reducers run before commit; outputs and resources run after
|
|
969
|
+
commit. A guard or reducer error prevents a new snapshot. An output or
|
|
970
|
+
resource error does not undo an already published snapshot.
|
|
971
|
+
|
|
972
|
+
## 14. Persistence
|
|
973
|
+
|
|
974
|
+
Save a running machine with:
|
|
975
|
+
|
|
976
|
+
```ts
|
|
977
|
+
const persisted = machine.persist()
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
The persisted snapshot contains active states, context, history, status,
|
|
981
|
+
revision, final output, and the model ID and version. It does not save
|
|
982
|
+
live timers, promises, resources, actors, or the event queue.
|
|
983
|
+
|
|
984
|
+
Restore it with the same model and bindings:
|
|
985
|
+
|
|
986
|
+
```ts
|
|
987
|
+
machine.stop()
|
|
988
|
+
|
|
989
|
+
const restored = createMachine(
|
|
990
|
+
owner,
|
|
991
|
+
dialogModel,
|
|
992
|
+
dialogBindings,
|
|
993
|
+
{ persisted },
|
|
994
|
+
).start()
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
Restore does not replay old outputs. It restarts resources for active
|
|
998
|
+
states, restarts delays at their full duration, and creates tasks and
|
|
999
|
+
actors again. Migrations handle context format changes between model
|
|
1000
|
+
versions.
|
|
1001
|
+
|
|
1002
|
+
## 15. Testing machines
|
|
1003
|
+
|
|
1004
|
+
Testing helpers have a separate entrypoint:
|
|
1005
|
+
|
|
1006
|
+
```ts
|
|
1007
|
+
import {
|
|
1008
|
+
inspectMachine,
|
|
1009
|
+
enumerateMachinePaths,
|
|
1010
|
+
createMachineCoverage,
|
|
1011
|
+
createManualMachineClock,
|
|
1012
|
+
} from '@raphy-js/raph/machine/testing'
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
A manual clock makes timed transitions deterministic:
|
|
1016
|
+
|
|
1017
|
+
```ts
|
|
1018
|
+
const clock = createManualMachineClock()
|
|
1019
|
+
|
|
1020
|
+
const machine = createMachine(
|
|
1021
|
+
owner,
|
|
1022
|
+
model,
|
|
1023
|
+
bindings,
|
|
1024
|
+
{ clock },
|
|
1025
|
+
).start()
|
|
1026
|
+
|
|
1027
|
+
clock.advance(1_999)
|
|
1028
|
+
console.log(machine.matches('visible')) // true
|
|
1029
|
+
|
|
1030
|
+
clock.advance(1)
|
|
1031
|
+
console.log(machine.matches('hidden')) // true
|
|
1032
|
+
```
|
|
1033
|
+
|
|
1034
|
+
## 16. Choosing the right layer
|
|
1035
|
+
|
|
1036
|
+
```text
|
|
1037
|
+
Reactivity
|
|
1038
|
+
├── signal mutable value
|
|
1039
|
+
├── computed derived value
|
|
1040
|
+
├── effect automatic reaction
|
|
1041
|
+
├── watch explicit observation
|
|
1042
|
+
├── batch grouped writes
|
|
1043
|
+
└── untracked read without a dependency
|
|
1044
|
+
|
|
1045
|
+
Runtime
|
|
1046
|
+
├── RaphScope resource owner
|
|
1047
|
+
├── RaphNode owner with a tree position
|
|
1048
|
+
├── Scheduler decides when a flush starts
|
|
1049
|
+
├── Phase named work queue
|
|
1050
|
+
├── Task delivery of one phase effect
|
|
1051
|
+
├── Watch explicit producer-to-consumer link
|
|
1052
|
+
├── Action batch + untracked + diagnostics
|
|
1053
|
+
└── Disposal automatic cleanup of an ownership subtree
|
|
1054
|
+
|
|
1055
|
+
Pure machine core
|
|
1056
|
+
├── Model
|
|
1057
|
+
├── State
|
|
1058
|
+
├── Event
|
|
1059
|
+
├── Transition
|
|
1060
|
+
├── Snapshot
|
|
1061
|
+
├── Context
|
|
1062
|
+
├── Guard
|
|
1063
|
+
└── Reducer
|
|
1064
|
+
|
|
1065
|
+
Machine runtime execution
|
|
1066
|
+
├── createMachine(owner, model)
|
|
1067
|
+
├── FIFO events
|
|
1068
|
+
├── one snapshot commit per macrostep
|
|
1069
|
+
├── outputs
|
|
1070
|
+
├── resources
|
|
1071
|
+
├── delays
|
|
1072
|
+
├── tasks
|
|
1073
|
+
├── actors
|
|
1074
|
+
├── persistence
|
|
1075
|
+
└── reactive snapshot
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
Use a signal for a simple mutable value, a computed value for a
|
|
1079
|
+
derivation, and an effect for a reaction. Add a runtime when execution
|
|
1080
|
+
timing, local lifecycle, or cleanup matters. Use a machine for a process
|
|
1081
|
+
with discrete modes and allowed transitions. A simple button rarely
|
|
1082
|
+
needs one; a dialog, loader, connection lifecycle, or multistep workflow
|
|
1083
|
+
often does.
|
|
1084
|
+
|
|
1085
|
+
## 17. Public entrypoints
|
|
1086
|
+
|
|
1087
|
+
| Import | Purpose |
|
|
1088
|
+
| --- | --- |
|
|
1089
|
+
| `@raphy-js/raph/reactivity` | Standalone reactive graph |
|
|
1090
|
+
| `@raphy-js/raph/runtime` | Ownership, nodes, phases, and scheduling |
|
|
1091
|
+
| `@raphy-js/raph/machine` | Pure machine model and runtime execution |
|
|
1092
|
+
| `@raphy-js/raph/machine/testing` | Inspection, coverage, and manual clock |
|
|
1093
|
+
|
|
1094
|
+
The complete Raph package also exposes paths and other runtime APIs.
|
|
1095
|
+
This guide follows the signal-to-machine route; each entrypoint exports
|
|
1096
|
+
its public TypeScript declarations.
|