@pyreon/reactivity 0.21.0 → 0.23.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/README.md +141 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @pyreon/reactivity
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Standalone fine-grained reactivity primitives — signals, computeds, effects, stores, resources, scopes.
|
|
4
|
+
|
|
5
|
+
`@pyreon/reactivity` is the foundation layer every other Pyreon package builds on, but it has zero framework dependencies and works on its own in Node, Bun, edge workers, or any JavaScript environment without DOM or JSX. Subscribers are tracked via a `Set<() => void>`; batches use a pointer swap for zero-allocation grouping. Two-tier batch flush (computed recompute → effect run) prevents stale reads in diamond-shaped dependency graphs.
|
|
4
6
|
|
|
5
7
|
## Install
|
|
6
8
|
|
|
@@ -8,70 +10,173 @@ Signal-based fine-grained reactivity primitives for the Pyreon framework.
|
|
|
8
10
|
bun add @pyreon/reactivity
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
## Quick
|
|
13
|
+
## Quick start
|
|
12
14
|
|
|
13
15
|
```ts
|
|
14
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
signal, computed, effect, batch, onCleanup, watch, untrack,
|
|
18
|
+
createStore, createResource, effectScope,
|
|
19
|
+
} from '@pyreon/reactivity'
|
|
15
20
|
|
|
16
21
|
const count = signal(0)
|
|
17
22
|
const doubled = computed(() => count() * 2)
|
|
18
23
|
|
|
19
|
-
effect(() => {
|
|
24
|
+
const dispose = effect(() => {
|
|
20
25
|
console.log('doubled:', doubled())
|
|
26
|
+
onCleanup(() => console.log('cleaning up'))
|
|
21
27
|
})
|
|
22
28
|
|
|
29
|
+
batch(() => {
|
|
30
|
+
count.set(1)
|
|
31
|
+
count.set(2) // subscribers fire once, with doubled = 4
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
watch(() => count(), (next, prev) => console.log(`${prev} → ${next}`))
|
|
35
|
+
|
|
36
|
+
const store = createStore({ todos: [{ text: 'Learn Pyreon', done: false }] })
|
|
37
|
+
store.todos[0].done = true // fine-grained update, no immer
|
|
38
|
+
|
|
39
|
+
dispose()
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## The signal contract
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const x = signal(0)
|
|
46
|
+
x() // read (subscribes if inside a tracked scope)
|
|
47
|
+
x.set(1) // write
|
|
48
|
+
x.update(n => n + 1)
|
|
49
|
+
x.peek() // read without subscribing
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Signals are **callable functions**, not `.value` getters (Vue) and not `[state, setState]` tuples (React). Calling the signal as a function is the read; `signal(5)` does NOT set the value — it reads and discards the argument. Dev mode warns; the `@pyreon/lint` rule `signal-write-as-call` flags it statically.
|
|
53
|
+
|
|
54
|
+
Optional `name` for debugging: `signal(0, { name: 'count' })` — the `@pyreon/vite-plugin` injects names automatically in dev.
|
|
55
|
+
|
|
56
|
+
## Computed
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
const doubled = computed(() => count() * 2)
|
|
60
|
+
const sameRef = computed(() => obj(), { equals: (a, b) => a.id === b.id })
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Lazy, memoized, auto-tracking. Recomputes only when a dependency changes AND a subscriber actually reads it. Pass a custom `equals` to dedupe by structural identity instead of `Object.is`.
|
|
64
|
+
|
|
65
|
+
## Effects
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
const dispose = effect(() => {
|
|
69
|
+
console.log(count())
|
|
70
|
+
onCleanup(() => console.log('before next run / on dispose'))
|
|
71
|
+
})
|
|
72
|
+
dispose()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`effect()` re-runs on tracked-signal change; the returned function disposes. Returning a cleanup function from the effect body is supported; `onCleanup(fn)` is the explicit form. `renderEffect()` is a lighter DOM-targeted variant that does NOT support `onCleanup` and does NOT register with `EffectScope` — used internally by `@pyreon/runtime-dom`.
|
|
76
|
+
|
|
77
|
+
`watch(source, callback)` is the explicit-source variant: `source` is evaluated for tracking, `callback(next, prev)` runs on change, and returning a cleanup function is honored.
|
|
78
|
+
|
|
79
|
+
## Batching
|
|
80
|
+
|
|
81
|
+
```ts
|
|
23
82
|
batch(() => {
|
|
24
83
|
count.set(1)
|
|
25
84
|
count.set(2)
|
|
85
|
+
}) // subscribers notified ONCE with count=2
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`batch()` defers subscriber notifications until the end of the callback. `nextTick(): Promise<void>` resolves after the current flush — useful for awaiting DOM updates in tests.
|
|
89
|
+
|
|
90
|
+
## Stores
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const store = createStore({ count: 0, todos: [{ text: 'a', done: false }] })
|
|
94
|
+
store.count++ // notifies
|
|
95
|
+
store.todos[0].done = true // deep — notifies
|
|
96
|
+
|
|
97
|
+
const shallow = shallowReactive({ user: { name: 'a' } })
|
|
98
|
+
shallow.user = { name: 'b' } // notifies
|
|
99
|
+
shallow.user.name = 'c' // does NOT notify (shallow)
|
|
100
|
+
|
|
101
|
+
const raw = markRaw(thirdPartyClassInstance) // skip proxy
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`createStore` returns a deeply-reactive proxy. `shallowReactive` proxies only the top level. `markRaw` opts an object out of proxying — useful for class instances, DOM nodes, third-party objects. `reconcile(target, source)` patches an existing store to match `source` without remounting.
|
|
105
|
+
|
|
106
|
+
**Caveat:** `Map`, `Set`, `WeakMap`, `WeakSet`, `Date`, `RegExp`, `Promise`, `Error` are returned RAW. Mutating them does not notify; assign a new instance to trigger updates.
|
|
107
|
+
|
|
108
|
+
## Resources
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const user = createResource(() => userId(), async (id) => {
|
|
112
|
+
const r = await fetch(`/api/users/${id}`)
|
|
113
|
+
return r.json()
|
|
26
114
|
})
|
|
27
|
-
|
|
115
|
+
|
|
116
|
+
user.data() // T | undefined
|
|
117
|
+
user.loading() // boolean
|
|
118
|
+
user.error() // Error | undefined
|
|
119
|
+
user.refetch()
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`createResource(source, fetcher)` re-runs the fetcher whenever `source` changes; stale responses are dropped via an internal request-id guard. Resources created **outside** an `EffectScope` must be `dispose()`-d explicitly to avoid leaks.
|
|
123
|
+
|
|
124
|
+
## EffectScope
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const scope = effectScope()
|
|
128
|
+
scope.runInScope(() => {
|
|
129
|
+
effect(() => console.log(count()))
|
|
130
|
+
onScopeDispose(() => console.log('scope ended'))
|
|
131
|
+
})
|
|
132
|
+
scope.stop() // disposes every effect inside
|
|
28
133
|
```
|
|
29
134
|
|
|
30
|
-
|
|
135
|
+
Groups effects for bulk disposal — used internally by `@pyreon/runtime-dom`'s mount pipeline. `getCurrentScope()` returns the active scope; `setCurrentScope(scope)` is the escape hatch for advanced cross-tree integrations.
|
|
31
136
|
|
|
32
|
-
|
|
137
|
+
Internal arrays (`_effects`, `_updateHooks`) are lazy-allocated — scopes with no effects cost only the object itself.
|
|
33
138
|
|
|
34
|
-
|
|
35
|
-
- **`computed<T>(fn, options?): Computed<T>`** -- Derived signal that recomputes lazily when dependencies change.
|
|
36
|
-
- **`cell<T>(initial: T): Cell<T>`** -- Lightweight reactive cell.
|
|
139
|
+
## Selectors
|
|
37
140
|
|
|
38
|
-
|
|
141
|
+
```ts
|
|
142
|
+
const selected = signal<string | null>(null)
|
|
143
|
+
const isSelected = createSelector(() => selected())
|
|
39
144
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- **`setErrorHandler(handler)`** -- Sets a global error handler for effect errors.
|
|
145
|
+
<For each={items} by={i => i.id}>
|
|
146
|
+
{(item) => <li class={() => isSelected(item.id) ? 'active' : ''}>{item.name}</li>}
|
|
147
|
+
</For>
|
|
148
|
+
```
|
|
45
149
|
|
|
46
|
-
|
|
150
|
+
`createSelector(source)` returns a function that, when called with a key, only notifies subscribers when the key transitions in or out of the selected state. O(1) instead of N effect runs on selection change.
|
|
47
151
|
|
|
48
|
-
|
|
49
|
-
- **`nextTick(): Promise<void>`** -- Resolves after the current batch of updates flushes.
|
|
152
|
+
## Cell — minimal alternative to signal
|
|
50
153
|
|
|
51
|
-
|
|
154
|
+
```ts
|
|
155
|
+
import { cell } from '@pyreon/reactivity'
|
|
156
|
+
const c = cell(0)
|
|
157
|
+
c.get(); c.set(1); c.subscribe(listener)
|
|
158
|
+
```
|
|
52
159
|
|
|
53
|
-
|
|
54
|
-
- **`untrack(fn)`** -- Alias for `runUntracked`.
|
|
160
|
+
`cell()` is a class-based primitive with a single-listener fast path and one allocation per cell. It is **not** callable and **does not** participate in effect tracking — use it only for cross-cutting state where the signal-tracking overhead would be wasteful.
|
|
55
161
|
|
|
56
|
-
|
|
162
|
+
## Debugging
|
|
57
163
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
164
|
+
```ts
|
|
165
|
+
import { setErrorHandler, inspectSignal, onSignalUpdate, why, getReactiveTrace } from '@pyreon/reactivity'
|
|
166
|
+
|
|
167
|
+
setErrorHandler((err, source) => reportToSentry(err, { tag: source }))
|
|
62
168
|
|
|
63
|
-
|
|
169
|
+
const count = signal(0, { name: 'count' })
|
|
170
|
+
onSignalUpdate(count, (next, prev) => console.log('count', prev, '→', next))
|
|
171
|
+
inspectSignal(count) // { name, value, subscribers: number }
|
|
172
|
+
why(count) // print dependency graph for this signal
|
|
173
|
+
```
|
|
64
174
|
|
|
65
|
-
-
|
|
66
|
-
- **`createResource(fetcher): Resource<T>`** -- Wraps an async data source in a reactive resource.
|
|
175
|
+
`activate/deactivate/getReactiveGraph/getReactiveFires` form the **opt-in** bridge consumed by the Pyreon devtools — zero cost until activated, gated by `process.env.NODE_ENV !== 'production'`, tree-shaken in production.
|
|
67
176
|
|
|
68
|
-
|
|
177
|
+
## Documentation
|
|
69
178
|
|
|
70
|
-
|
|
71
|
-
- **`isStore(value): boolean`** -- Checks whether a value is a reactive store.
|
|
72
|
-
- **`reconcile(target, source)`** -- Efficiently patches a store to match a new value.
|
|
73
|
-
- **`shallowReactive<T>(initial): T`** -- Creates a SHALLOWLY reactive store: top-level property writes notify, but nested object mutations don't (Vue 3 parity). Use for large object graphs where deep proxying would be wasteful.
|
|
74
|
-
- **`markRaw<T>(value): T`** -- Mark an object as RAW so `createStore` and `shallowReactive` return it unwrapped (Vue 3 parity). Useful for class instances, third-party objects, DOM nodes, or any shape that shouldn't be deeply proxied. Marking is one-way (no `unmarkRaw`); mark BEFORE the object enters a store.
|
|
179
|
+
Full docs: [docs.pyreon.dev/docs/reactivity](https://docs.pyreon.dev/docs/reactivity) (or `docs/docs/reactivity.md` in this repo).
|
|
75
180
|
|
|
76
181
|
## License
|
|
77
182
|
|
package/package.json
CHANGED