@vielzeug/stateit 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/README.md +337 -0
- package/dist/batch.cjs +2 -0
- package/dist/batch.cjs.map +1 -0
- package/dist/batch.d.ts +5 -0
- package/dist/batch.d.ts.map +1 -0
- package/dist/batch.js +28 -0
- package/dist/batch.js.map +1 -0
- package/dist/computed.cjs +2 -0
- package/dist/computed.cjs.map +1 -0
- package/dist/computed.d.ts +58 -0
- package/dist/computed.d.ts.map +1 -0
- package/dist/computed.js +65 -0
- package/dist/computed.js.map +1 -0
- package/dist/effect.cjs +2 -0
- package/dist/effect.cjs.map +1 -0
- package/dist/effect.d.ts +31 -0
- package/dist/effect.d.ts.map +1 -0
- package/dist/effect.js +53 -0
- package/dist/effect.js.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/runtime.cjs +2 -0
- package/dist/runtime.cjs.map +1 -0
- package/dist/runtime.d.ts +40 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +47 -0
- package/dist/runtime.js.map +1 -0
- package/dist/signal.cjs +2 -0
- package/dist/signal.cjs.map +1 -0
- package/dist/signal.d.ts +26 -0
- package/dist/signal.d.ts.map +1 -0
- package/dist/signal.js +40 -0
- package/dist/signal.js.map +1 -0
- package/dist/stateit.cjs +2 -0
- package/dist/stateit.cjs.map +1 -0
- package/dist/stateit.js +270 -0
- package/dist/stateit.js.map +1 -0
- package/dist/store.cjs +2 -0
- package/dist/store.cjs.map +1 -0
- package/dist/store.d.ts +32 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +51 -0
- package/dist/store.js.map +1 -0
- package/dist/types.cjs +2 -0
- package/dist/types.cjs.map +1 -0
- package/dist/types.d.ts +39 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/watch.cjs +2 -0
- package/dist/watch.cjs.map +1 -0
- package/dist/watch.d.ts +36 -0
- package/dist/watch.d.ts.map +1 -0
- package/dist/watch.js +32 -0
- package/dist/watch.js.map +1 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# @vielzeug/stateit
|
|
2
|
+
|
|
3
|
+
> Tiny, zero-dependency reactive state — signals, computed values, effects, and typed object stores
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@vielzeug/stateit) [](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
**Stateit** is a tiny, zero-dependency reactive library with two complementary primitives:
|
|
8
|
+
|
|
9
|
+
- **Signals** — fine-grained reactive atoms with `signal()`, `computed()`, `effect()`, `watch()`, `batch()`, and more
|
|
10
|
+
- **Stores** — typed reactive object containers with partial patching, updater functions, selectors, and freeze
|
|
11
|
+
|
|
12
|
+
A `Store<T>` extends `Signal<T>`, so every signal primitive — `effect`, `watch`, `computed`, `batch` — works seamlessly on stores.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
pnpm add @vielzeug/stateit
|
|
18
|
+
# npm install @vielzeug/stateit
|
|
19
|
+
# yarn add @vielzeug/stateit
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
### Signals
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { signal, computed, effect, watch, batch } from '@vielzeug/stateit';
|
|
28
|
+
|
|
29
|
+
const count = signal(0);
|
|
30
|
+
const doubled = computed(() => count.value * 2);
|
|
31
|
+
|
|
32
|
+
// effect runs immediately and re-runs on every dependency change
|
|
33
|
+
const sub = effect(() => {
|
|
34
|
+
console.log(`count=${count.value}, doubled=${doubled.value}`);
|
|
35
|
+
});
|
|
36
|
+
// → count=0, doubled=0
|
|
37
|
+
|
|
38
|
+
count.value = 3;
|
|
39
|
+
// → count=3, doubled=6
|
|
40
|
+
|
|
41
|
+
// Update via function — shorthand for count.value = count.value + n
|
|
42
|
+
count.update((n) => n + 1);
|
|
43
|
+
// → count=4, doubled=8
|
|
44
|
+
|
|
45
|
+
// Watch explicitly (does not fire immediately by default)
|
|
46
|
+
const stopWatch = watch(count, (next, prev) => console.log(prev, '→', next));
|
|
47
|
+
count.value = 5; // → 4 → 5
|
|
48
|
+
|
|
49
|
+
// Batch coalesces notifications into one flush
|
|
50
|
+
batch(() => {
|
|
51
|
+
count.value = 10;
|
|
52
|
+
count.value = 20;
|
|
53
|
+
});
|
|
54
|
+
// One notification: → 5 → 20
|
|
55
|
+
|
|
56
|
+
sub.dispose(); // or: sub() — direct call also disposes
|
|
57
|
+
doubled.dispose();
|
|
58
|
+
stopWatch.dispose();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Stores
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { store, watch, batch } from '@vielzeug/stateit';
|
|
65
|
+
|
|
66
|
+
const counter = store({ count: 0, user: null as User | null });
|
|
67
|
+
|
|
68
|
+
// Read
|
|
69
|
+
console.log(counter.value); // { count: 0, user: null }
|
|
70
|
+
|
|
71
|
+
// Partial patch
|
|
72
|
+
counter.patch({ count: 1 });
|
|
73
|
+
|
|
74
|
+
// Updater function
|
|
75
|
+
counter.update((s) => ({ ...s, count: s.count + 1 }));
|
|
76
|
+
|
|
77
|
+
// Watch a slice — compose store.select() with watch()
|
|
78
|
+
const countSignal = counter.select((s) => s.count);
|
|
79
|
+
const stopWatch = watch(countSignal, (count, prev) => {
|
|
80
|
+
console.log('count:', prev, '→', count);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Batch updates into one notification
|
|
84
|
+
batch(() => {
|
|
85
|
+
counter.patch({ count: 10 });
|
|
86
|
+
counter.patch({ user: currentUser });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Reset to initial state
|
|
90
|
+
counter.reset();
|
|
91
|
+
|
|
92
|
+
stopWatch.dispose();
|
|
93
|
+
counter.freeze();
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Features
|
|
97
|
+
|
|
98
|
+
- ✅ **Signals** — reactive atoms with `.value` getter/setter, `.update(fn)`, and `.peek()` for untracked reads
|
|
99
|
+
- ✅ **Computed** — lazy derived signals; call `.dispose()` to stop tracking dependencies
|
|
100
|
+
- ✅ **Effects** — run immediately and re-run when dependencies change; support cleanup returns; return a `Subscription`
|
|
101
|
+
- ✅ **Watch** — explicit subscriptions with `{ immediate?, once?, equals? }` options; returns a `Subscription`
|
|
102
|
+
- ✅ **Batch** — coalesce multiple writes into a single downstream notification
|
|
103
|
+
- ✅ **Untrack** — read signals without registering reactive dependencies
|
|
104
|
+
- ✅ **derived** — multi-source computed from an array of signals
|
|
105
|
+
- ✅ **nextValue** — `Promise` that resolves on the next matching signal emission
|
|
106
|
+
- ✅ **Custom equality** — `signal/computed/watch/store` all accept a custom `equals` function
|
|
107
|
+
- ✅ **Stores** — object state with `patch(partial)`, `update(fn)`, `reset()`, `select()`, and `freeze()`
|
|
108
|
+
- ✅ **Writable computed** — bidirectional computed with `writable(get, set)`
|
|
109
|
+
- ✅ **Symbol.dispose** — all dispose handles support `using` declarations (TC39 explicit resource management)
|
|
110
|
+
- ✅ **Type guards** — `isSignal(v)` and `isStore(v)`
|
|
111
|
+
- ✅ **Zero dependencies** — no supply-chain risk, minimal bundle size
|
|
112
|
+
- ✅ **Framework agnostic** — works in React, Vue, Svelte, or plain TypeScript
|
|
113
|
+
|
|
114
|
+
## Usage
|
|
115
|
+
|
|
116
|
+
### Signals
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import { signal, readonly } from '@vielzeug/stateit';
|
|
120
|
+
|
|
121
|
+
const count = signal(0);
|
|
122
|
+
count.value = 5;
|
|
123
|
+
count.peek(); // read without tracking
|
|
124
|
+
count.update((n) => n + 1); // derive next value in place
|
|
125
|
+
|
|
126
|
+
// Narrow to read-only view (no proxy overhead)
|
|
127
|
+
const ro = readonly(count);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Computed
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { signal, computed } from '@vielzeug/stateit';
|
|
134
|
+
|
|
135
|
+
const first = signal('Ada');
|
|
136
|
+
const last = signal('Lovelace');
|
|
137
|
+
const full = computed(() => `${first.value} ${last.value}`);
|
|
138
|
+
|
|
139
|
+
console.log(full.value); // 'Ada Lovelace'
|
|
140
|
+
full.stale; // false — just computed
|
|
141
|
+
first.value = 'Grace';
|
|
142
|
+
full.stale; // true — deps changed, not yet re-read
|
|
143
|
+
|
|
144
|
+
// Dispose (stop tracking dependencies)
|
|
145
|
+
full.dispose();
|
|
146
|
+
// or: using full = computed(...) — TC39 using declaration
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Effects
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import { signal, effect, onCleanup } from '@vielzeug/stateit';
|
|
153
|
+
|
|
154
|
+
const name = signal('Ada');
|
|
155
|
+
|
|
156
|
+
const sub = effect(() => {
|
|
157
|
+
console.log('Hello,', name.value);
|
|
158
|
+
return () => console.log('cleanup'); // optional teardown
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
name.value = 'Grace'; // logs 'cleanup' then 'Hello, Grace'
|
|
162
|
+
sub.dispose(); // logs 'cleanup', effect is removed
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Watch
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import { signal, store, watch } from '@vielzeug/stateit';
|
|
169
|
+
|
|
170
|
+
const count = signal(0);
|
|
171
|
+
|
|
172
|
+
// Plain watch
|
|
173
|
+
const sub = watch(count, (next, prev) => console.log(prev, '→', next));
|
|
174
|
+
|
|
175
|
+
// Slice watch — compose with store.select()
|
|
176
|
+
const user = store({ name: 'Ada', age: 30 });
|
|
177
|
+
const nameSig = user.select((s) => s.name);
|
|
178
|
+
watch(nameSig, (name) => console.log('name:', name));
|
|
179
|
+
|
|
180
|
+
// Options
|
|
181
|
+
watch(count, (v) => console.log(v), { immediate: true, once: true });
|
|
182
|
+
|
|
183
|
+
sub.dispose();
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Stores
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
import { store, watch, batch } from '@vielzeug/stateit';
|
|
190
|
+
|
|
191
|
+
const cart = store({ items: [] as string[], total: 0 });
|
|
192
|
+
|
|
193
|
+
// Partial patch
|
|
194
|
+
cart.patch({ total: 42 });
|
|
195
|
+
|
|
196
|
+
// Updater function
|
|
197
|
+
cart.update((s) => ({ ...s, items: [...s.items, 'apple'] }));
|
|
198
|
+
|
|
199
|
+
// Derived slice
|
|
200
|
+
const totalSignal = cart.select((s) => s.total);
|
|
201
|
+
|
|
202
|
+
// Watch slice
|
|
203
|
+
watch(totalSignal, (total) => console.log('total:', total));
|
|
204
|
+
|
|
205
|
+
// Batch
|
|
206
|
+
batch(() => {
|
|
207
|
+
cart.patch({ total: 0 });
|
|
208
|
+
cart.update((s) => ({ ...s, items: [] }));
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
cart.reset();
|
|
212
|
+
cart.freeze();
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Writable Computed
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
import { signal, writable } from '@vielzeug/stateit';
|
|
219
|
+
|
|
220
|
+
const celsius = signal(0);
|
|
221
|
+
const fahrenheit = writable(
|
|
222
|
+
() => (celsius.value * 9) / 5 + 32,
|
|
223
|
+
(f) => {
|
|
224
|
+
celsius.value = ((f - 32) * 5) / 9;
|
|
225
|
+
},
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
fahrenheit.value = 212;
|
|
229
|
+
console.log(celsius.value); // 100
|
|
230
|
+
|
|
231
|
+
fahrenheit.update((f) => f + 10); // increment via fn
|
|
232
|
+
fahrenheit.dispose();
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### `nextValue` — Async Watch
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
import { signal, nextValue } from '@vielzeug/stateit';
|
|
239
|
+
|
|
240
|
+
const status = signal<'idle' | 'loading' | 'done'>('idle');
|
|
241
|
+
|
|
242
|
+
// Wait for the next change (any value)
|
|
243
|
+
const next = await nextValue(status);
|
|
244
|
+
|
|
245
|
+
// Wait for a specific condition
|
|
246
|
+
const done = await nextValue(status, (v) => v === 'done');
|
|
247
|
+
|
|
248
|
+
// Abortable wait
|
|
249
|
+
const controller = new AbortController();
|
|
250
|
+
const p = nextValue(status, (v) => v === 'done', { signal: controller.signal });
|
|
251
|
+
controller.abort(new Error('cancelled'));
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Custom Equality
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
import { signal, computed } from '@vielzeug/stateit';
|
|
258
|
+
|
|
259
|
+
// Signal only notifies when the array reference AND contents differ
|
|
260
|
+
const tags = signal(['ts'], { equals: (a, b) => JSON.stringify(a) === JSON.stringify(b) });
|
|
261
|
+
|
|
262
|
+
// Computed suppresses downstream when result is structurally unchanged
|
|
263
|
+
const sorted = computed(() => [...tags.value].sort(), { equals: (a, b) => a.join() === b.join() });
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## API
|
|
267
|
+
|
|
268
|
+
### Signal Functions
|
|
269
|
+
|
|
270
|
+
| Export | Signature | Returns |
|
|
271
|
+
| ------------------ | -------------------------------------------------------- | ------------------------ |
|
|
272
|
+
| `signal` | `signal(initial, options?)` | `Signal<T>` |
|
|
273
|
+
| `computed` | `computed(fn, options?)` | `ComputedSignal<T>` |
|
|
274
|
+
| `writable` | `writable(get, set, options?)` | `WritableSignal<T>` |
|
|
275
|
+
| `derived` | `derived(sources, fn, options?)` | `ComputedSignal<R>` |
|
|
276
|
+
| `effect` | `effect(fn, options?)` | `Subscription` |
|
|
277
|
+
| `watch` | `watch(source, cb, options?)` | `Subscription` |
|
|
278
|
+
| `nextValue` | `nextValue(source, predicate?, { signal? }?)` | `Promise<T>` |
|
|
279
|
+
| `batch` | `batch(fn)` | `T` |
|
|
280
|
+
| `untrack` | `untrack(fn)` | `T` |
|
|
281
|
+
| `onCleanup` | `onCleanup(fn)` | `void` |
|
|
282
|
+
| `readonly` | `readonly(sig)` | `ReadonlySignal<T>` |
|
|
283
|
+
| `toValue` | `toValue(v)` | `T` |
|
|
284
|
+
| `peekValue` | `peekValue(v)` | `T` |
|
|
285
|
+
| `isSignal` | `isSignal(v)` | `v is ReadonlySignal<T>` |
|
|
286
|
+
| `isStore` | `isStore(v)` | `v is Store<T>` |
|
|
287
|
+
| `shallowEqual` | `shallowEqual(a, b)` | `boolean` |
|
|
288
|
+
| `configureStateit` | `configureStateit(opts)` | `void` |
|
|
289
|
+
| `_resetContextForTesting` | `_resetContextForTesting()` | `void` |
|
|
290
|
+
|
|
291
|
+
### Store Functions
|
|
292
|
+
|
|
293
|
+
| Export | Signature | Returns |
|
|
294
|
+
| ------- | -------------------------- | ---------- |
|
|
295
|
+
| `store` | `store(initial, options?)` | `Store<T>` |
|
|
296
|
+
|
|
297
|
+
### `Store<T>` Methods
|
|
298
|
+
|
|
299
|
+
| Member | Description |
|
|
300
|
+
| ----------------------------- | ----------------------------------------------------------------------- |
|
|
301
|
+
| `.value` | Read the current state (tracked) |
|
|
302
|
+
| `.peek()` | Read the current state (untracked) |
|
|
303
|
+
| `.update(fn)` | Derive next state via `fn(copy) => next` |
|
|
304
|
+
| `.patch(partial)` | Shallow-merge a `Partial<T>` into state |
|
|
305
|
+
| `.reset()` | Restore the original initial state |
|
|
306
|
+
| `.select(selector, options?)` | Lazily derived `ComputedSignal<U>` from a slice; compose with `watch()` |
|
|
307
|
+
| `.frozen` | `true` after `freeze()` has been called |
|
|
308
|
+
| `.freeze()` | Freeze the store; further writes are silently ignored |
|
|
309
|
+
|
|
310
|
+
### Types
|
|
311
|
+
|
|
312
|
+
| Type | Description |
|
|
313
|
+
| -------------------- | ------------------------------------------------------------------------------------ |
|
|
314
|
+
| `Signal<T>` | Readable/writable reactive atom with `.update(fn)` |
|
|
315
|
+
| `ReadonlySignal<T>` | Read-only signal (no setter) |
|
|
316
|
+
| `ComputedSignal<T>` | `ReadonlySignal<T> & Disposable` — has `.stale`, `.dispose()` |
|
|
317
|
+
| `WritableSignal<T>` | `Signal<T> & Disposable` — has `.stale`, `.update(fn)`, `.dispose()` |
|
|
318
|
+
| `Store<T>` | Object store extending `Signal<T>` |
|
|
319
|
+
| `Subscription` | Callable + `.dispose()` + `[Symbol.dispose]` — returned by `effect`/`watch` |
|
|
320
|
+
| `Disposable` | `.dispose()` + `[Symbol.dispose]` — implemented by `ComputedSignal`/`WritableSignal` |
|
|
321
|
+
| `CleanupFn` | `() => void` |
|
|
322
|
+
| `EffectCallback` | `() => CleanupFn \| void` |
|
|
323
|
+
| `EffectOptions` | `{ maxIterations?, onError? }` |
|
|
324
|
+
| `EqualityFn<T>` | `(a: T, b: T) => boolean` |
|
|
325
|
+
| `ReactiveOptions<T>` | `{ equals?: EqualityFn<T> }` |
|
|
326
|
+
| `WatchOptions<T>` | `{ immediate?, once?, equals? }` |
|
|
327
|
+
| `StoreOptions<T>` | `{ equals?: EqualityFn<T> }` |
|
|
328
|
+
|
|
329
|
+
## Documentation
|
|
330
|
+
|
|
331
|
+
Full docs at **[vielzeug.dev/stateit](https://vielzeug.dev/stateit)**
|
|
332
|
+
|
|
333
|
+
| | |
|
|
334
|
+
| ------------------------------------------------- | -------------------------------------- |
|
|
335
|
+
| [Usage Guide](https://vielzeug.dev/stateit/usage) | Concepts, patterns, and best practices |
|
|
336
|
+
| [API Reference](https://vielzeug.dev/stateit/api) | Complete type signatures |
|
|
337
|
+
| [Examples](https://vielzeug.dev/stateit/examples) | Framework integrations and recipes |
|
package/dist/batch.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require(`./runtime.cjs`);var t=()=>{let t=[...e.queue.pending];e.queue.pending.clear();let n=[];for(let e of t)try{e()}catch(e){n.push(e)}if(n.length)throw n.length===1?n[0]:AggregateError(n,`[stateit] batch errors`)},n=n=>{e.queue.depth++;try{let r=n();return--e.queue.depth===0&&t(),r}catch(n){if(--e.queue.depth===0)try{t()}catch{}throw n}};exports._flushPending=t,exports.batch=n;
|
|
2
|
+
//# sourceMappingURL=batch.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batch.cjs","names":[],"sources":["../src/batch.ts"],"sourcesContent":["import { _DEV, queue } from './runtime';\n\n/** @internal Drain and run all pending effects from the batch queue, collecting errors. */\nexport const _flushPending = (): void => {\n const toFlush = [...queue.pending];\n\n queue.pending.clear();\n\n const errors: unknown[] = [];\n\n for (const f of toFlush) {\n try {\n f();\n } catch (e) {\n errors.push(e);\n }\n }\n\n if (errors.length) throw errors.length === 1 ? errors[0] : new AggregateError(errors, '[stateit] batch errors');\n};\n\n/** Runs fn and defers all Signal notifications until fn returns, then flushes once. */\nexport const batch = <T>(fn: () => T): T => {\n queue.depth++;\n\n try {\n const result = fn();\n\n if (--queue.depth === 0) _flushPending();\n\n return result;\n } catch (e) {\n if (--queue.depth === 0) {\n try {\n _flushPending();\n } catch (flushErr) {\n if (_DEV)\n console.error(\n '[stateit] batch: a secondary flush error was suppressed (callback error takes precedence)',\n flushErr,\n );\n }\n }\n\n throw e;\n }\n};\n"],"mappings":"iCAGA,IAAa,MAA4B,CACvC,IAAM,EAAU,CAAC,GAAG,EAAA,MAAM,QAAQ,CAElC,EAAA,MAAM,QAAQ,OAAO,CAErB,IAAM,EAAoB,EAAE,CAE5B,IAAK,IAAM,KAAK,EACd,GAAI,CACF,GAAG,OACI,EAAG,CACV,EAAO,KAAK,EAAE,CAIlB,GAAI,EAAO,OAAQ,MAAM,EAAO,SAAW,EAAI,EAAO,GAAS,eAAe,EAAQ,yBAAyB,EAIpG,EAAY,GAAmB,CAC1C,EAAA,MAAM,QAEN,GAAI,CACF,IAAM,EAAS,GAAI,CAInB,MAFI,EAAE,EAAA,MAAM,QAAU,GAAG,GAAe,CAEjC,QACA,EAAG,CACV,GAAI,EAAE,EAAA,MAAM,QAAU,EACpB,GAAI,CACF,GAAe,MACE,EASrB,MAAM"}
|
package/dist/batch.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** @internal Drain and run all pending effects from the batch queue, collecting errors. */
|
|
2
|
+
export declare const _flushPending: () => void;
|
|
3
|
+
/** Runs fn and defers all Signal notifications until fn returns, then flushes once. */
|
|
4
|
+
export declare const batch: <T>(fn: () => T) => T;
|
|
5
|
+
//# sourceMappingURL=batch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batch.d.ts","sourceRoot":"","sources":["../src/batch.ts"],"names":[],"mappings":"AAEA,2FAA2F;AAC3F,eAAO,MAAM,aAAa,QAAO,IAgBhC,CAAC;AAEF,uFAAuF;AACvF,eAAO,MAAM,KAAK,GAAI,CAAC,EAAE,IAAI,MAAM,CAAC,KAAG,CAwBtC,CAAC"}
|
package/dist/batch.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { queue as e } from "./runtime.js";
|
|
2
|
+
//#region src/batch.ts
|
|
3
|
+
var t = () => {
|
|
4
|
+
let t = [...e.pending];
|
|
5
|
+
e.pending.clear();
|
|
6
|
+
let n = [];
|
|
7
|
+
for (let e of t) try {
|
|
8
|
+
e();
|
|
9
|
+
} catch (e) {
|
|
10
|
+
n.push(e);
|
|
11
|
+
}
|
|
12
|
+
if (n.length) throw n.length === 1 ? n[0] : AggregateError(n, "[stateit] batch errors");
|
|
13
|
+
}, n = (n) => {
|
|
14
|
+
e.depth++;
|
|
15
|
+
try {
|
|
16
|
+
let r = n();
|
|
17
|
+
return --e.depth === 0 && t(), r;
|
|
18
|
+
} catch (n) {
|
|
19
|
+
if (--e.depth === 0) try {
|
|
20
|
+
t();
|
|
21
|
+
} catch {}
|
|
22
|
+
throw n;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { t as _flushPending, n as batch };
|
|
27
|
+
|
|
28
|
+
//# sourceMappingURL=batch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batch.js","names":[],"sources":["../src/batch.ts"],"sourcesContent":["import { _DEV, queue } from './runtime';\n\n/** @internal Drain and run all pending effects from the batch queue, collecting errors. */\nexport const _flushPending = (): void => {\n const toFlush = [...queue.pending];\n\n queue.pending.clear();\n\n const errors: unknown[] = [];\n\n for (const f of toFlush) {\n try {\n f();\n } catch (e) {\n errors.push(e);\n }\n }\n\n if (errors.length) throw errors.length === 1 ? errors[0] : new AggregateError(errors, '[stateit] batch errors');\n};\n\n/** Runs fn and defers all Signal notifications until fn returns, then flushes once. */\nexport const batch = <T>(fn: () => T): T => {\n queue.depth++;\n\n try {\n const result = fn();\n\n if (--queue.depth === 0) _flushPending();\n\n return result;\n } catch (e) {\n if (--queue.depth === 0) {\n try {\n _flushPending();\n } catch (flushErr) {\n if (_DEV)\n console.error(\n '[stateit] batch: a secondary flush error was suppressed (callback error takes precedence)',\n flushErr,\n );\n }\n }\n\n throw e;\n }\n};\n"],"mappings":";;AAGA,IAAa,UAA4B;CACvC,IAAM,IAAU,CAAC,GAAG,EAAM,QAAQ;AAElC,GAAM,QAAQ,OAAO;CAErB,IAAM,IAAoB,EAAE;AAE5B,MAAK,IAAM,KAAK,EACd,KAAI;AACF,KAAG;UACI,GAAG;AACV,IAAO,KAAK,EAAE;;AAIlB,KAAI,EAAO,OAAQ,OAAM,EAAO,WAAW,IAAI,EAAO,KAAS,eAAe,GAAQ,yBAAyB;GAIpG,KAAY,MAAmB;AAC1C,GAAM;AAEN,KAAI;EACF,IAAM,IAAS,GAAI;AAInB,SAFI,EAAE,EAAM,UAAU,KAAG,GAAe,EAEjC;UACA,GAAG;AACV,MAAI,EAAE,EAAM,UAAU,EACpB,KAAI;AACF,MAAe;UACE;AASrB,QAAM"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require(`./runtime.cjs`),t=require(`./types.cjs`);var n=class extends e.ReactiveNode{[t._SIGNAL_BRAND]=!0;#e;#t=e._UNINITIALIZED;#n=!0;#r=!1;#i;#a=new Set;#o=()=>{this.#n||(this.#n=!0,this._notify())};constructor(e,t){super(),this.#e=e,this.#i=t?.equals??Object.is,t?.lazy||this.#s()}#s(){for(let e of this.#a)e();this.#a.clear();let t=e._withCtx(this.#o,this.#a,null,this.#e);this.#n=!1,(this.#t===e._UNINITIALIZED||!this.#i(this.#t,t))&&(this.#t=t)}get value(){return this.#r?this.#t:(this.#n&&this.#s(),this._track(),this.#t)}peek(){if(this.#t===e._UNINITIALIZED){if(this.#r)return;this.#s()}return this.#t}get stale(){return this.#n||this.#r}dispose(){if(!this.#r){this.#r=!0;for(let e of this.#a)e();this.#a.clear()}}[Symbol.dispose](){this.dispose()}},r=(e,t)=>new n(e,t),i=class extends n{#e;constructor(e,t,n){super(e,n),this.#e=t}get value(){return super.value}set value(e){this.#e(e)}update(e){this.#e(e(this.peek()))}},a=(e,t,n)=>new i(e,t,n),o=(e,t,n)=>r(()=>t(...e.map(e=>e.value)),n);exports.ComputedNode=n,exports.WritableNode=i,exports.computed=r,exports.derived=o,exports.writable=a;
|
|
2
|
+
//# sourceMappingURL=computed.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"computed.cjs","names":["#onDepChange","#dirty","#compute","#equals","#recompute","#deps","#value","#disposed","#set"],"sources":["../src/computed.ts"],"sourcesContent":["import { ReactiveNode, _DEV, _UNINITIALIZED, _withCtx } from './runtime';\nimport {\n type CleanupFn,\n type Disposable,\n type EffectCallback,\n type EqualityFn,\n type ReactiveOptions,\n type ReadonlySignal,\n type Signal,\n _SIGNAL_BRAND,\n} from './types';\n\n/** A derived read-only signal with an explicit dispose method. */\nexport interface ComputedSignal<T> extends ReadonlySignal<T>, Disposable {\n /** True when the computed value is stale (deps changed but not yet re-read) or disposed. */\n readonly stale: boolean;\n}\n\n/** @internal\n * Lazy computed node. Seeded once at construction so .peek() is immediately valid\n * (unless { lazy: true } is passed, in which case first compute defers to first read).\n * - On dep change: marks dirty, notifies downstream (no recompute yet).\n * - On .value read: recomputes if dirty, re-tracks fresh deps.\n * - After dispose: _track() is skipped to prevent leaking outer effects.\n */\nexport class ComputedNode<T> extends ReactiveNode implements ComputedSignal<T> {\n readonly [_SIGNAL_BRAND] = true as const;\n #compute: () => T;\n #value: T | typeof _UNINITIALIZED = _UNINITIALIZED;\n #dirty = true;\n #disposed = false;\n #equals: EqualityFn<T>;\n #deps = new Set<CleanupFn>();\n\n readonly #onDepChange: EffectCallback = () => {\n if (!this.#dirty) {\n this.#dirty = true;\n this._notify();\n }\n };\n\n constructor(compute: () => T, options?: ReactiveOptions<T> & { lazy?: boolean }) {\n super();\n this.#compute = compute;\n this.#equals = options?.equals ?? Object.is;\n\n if (!options?.lazy) this.#recompute(); // seed: ensures peek() is valid immediately\n }\n\n #recompute(): void {\n for (const unsub of this.#deps) unsub();\n this.#deps.clear();\n\n const result = _withCtx(this.#onDepChange, this.#deps, null, this.#compute);\n\n this.#dirty = false;\n\n if (this.#value === _UNINITIALIZED || !this.#equals(this.#value as T, result)) {\n this.#value = result;\n }\n }\n\n get value(): T {\n if (this.#disposed) {\n if (_DEV) console.warn('[stateit] Reading a disposed ComputedSignal returns a stale value.');\n\n return this.#value as T;\n }\n\n if (this.#dirty) this.#recompute();\n\n this._track();\n\n return this.#value as T;\n }\n\n peek(): T {\n if (this.#value === _UNINITIALIZED) {\n if (this.#disposed) {\n if (_DEV)\n console.warn(\n '[stateit] peek() called on a disposed lazy ComputedSignal that was never read — returning undefined.',\n );\n\n return undefined as unknown as T;\n }\n\n this.#recompute();\n }\n\n return this.#value as T;\n }\n\n get stale(): boolean {\n return this.#dirty || this.#disposed;\n }\n\n dispose(): void {\n if (this.#disposed) return;\n\n this.#disposed = true;\n for (const unsub of this.#deps) unsub();\n this.#deps.clear();\n }\n\n [Symbol.dispose](): void {\n this.dispose();\n }\n}\n\n/** Creates a derived read-only Signal whose value is recomputed lazily on `.value` read\n * when dependencies have changed. Call `.dispose()` to stop tracking and free dependencies.\n * Pass `{ lazy: true }` to defer the initial computation until the first `.value` read. */\nexport const computed = <T>(compute: () => T, options?: ReactiveOptions<T> & { lazy?: boolean }): ComputedSignal<T> =>\n new ComputedNode(compute, options);\n\n/** A bidirectional computed Signal with an explicit dispose method. */\nexport interface WritableSignal<T> extends Signal<T>, Disposable {\n /** True when the backing computed value is stale (deps changed but not yet re-read) or disposed. */\n readonly stale: boolean;\n}\n\n/** @internal */\nexport class WritableNode<T> extends ComputedNode<T> implements WritableSignal<T> {\n readonly #set: (v: T) => void;\n\n constructor(get: () => T, set: (v: T) => void, options?: ReactiveOptions<T>) {\n super(get, options);\n this.#set = set;\n }\n\n override get value(): T {\n return super.value;\n }\n\n set value(v: T) {\n this.#set(v);\n }\n\n update(fn: (current: T) => T): void {\n this.#set(fn(this.peek()));\n }\n}\n\n/** Creates a bidirectional computed Signal. Reads track the getter reactively;\n * writes are forwarded to `set`. Call `.dispose()` to stop tracking and free dependencies. */\nexport const writable = <T>(get: () => T, set: (value: T) => void, options?: ReactiveOptions<T>): WritableSignal<T> =>\n new WritableNode(get, set, options);\n\n/**\n * Creates a derived ComputedSignal by combining multiple source signals through a projector\n * function. Each source is passed as a positional argument to `fn`; the projector is\n * re-evaluated whenever any source changes.\n *\n * @example\n * const total = derived([price, quantity, discount], (p, q, d) => p * q * (1 - d));\n */\nexport const derived = <const Srcs extends ReadonlyArray<ReadonlySignal<unknown>>, R>(\n sources: Srcs,\n fn: (...values: { [K in keyof Srcs]: Srcs[K] extends ReadonlySignal<infer V> ? V : never }) => R,\n options?: ReactiveOptions<R>,\n): ComputedSignal<R> => computed(() => (fn as (...args: any[]) => R)(...sources.map((s) => s.value)), options);\n"],"mappings":"0DAyBA,IAAa,EAAb,cAAqC,EAAA,YAA0C,CAC7E,CAAU,EAAA,eAAiB,GAC3B,GACA,GAAoC,EAAA,eACpC,GAAS,GACT,GAAY,GACZ,GACA,GAAQ,IAAI,IAEZ,OAA8C,CACvC,MAAA,IACH,MAAA,EAAc,GACd,KAAK,SAAS,GAIlB,YAAY,EAAkB,EAAmD,CAC/E,OAAO,CACP,MAAA,EAAgB,EAChB,MAAA,EAAe,GAAS,QAAU,OAAO,GAEpC,GAAS,MAAM,MAAA,GAAiB,CAGvC,IAAmB,CACjB,IAAK,IAAM,KAAS,MAAA,EAAY,GAAO,CACvC,MAAA,EAAW,OAAO,CAElB,IAAM,EAAS,EAAA,SAAS,MAAA,EAAmB,MAAA,EAAY,KAAM,MAAA,EAAc,CAE3E,MAAA,EAAc,IAEV,MAAA,IAAgB,EAAA,gBAAkB,CAAC,MAAA,EAAa,MAAA,EAAkB,EAAO,IAC3E,MAAA,EAAc,GAIlB,IAAI,OAAW,CAWb,OAVI,MAAA,EAGK,MAAA,GAGL,MAAA,GAAa,MAAA,GAAiB,CAElC,KAAK,QAAQ,CAEN,MAAA,GAGT,MAAU,CACR,GAAI,MAAA,IAAgB,EAAA,eAAgB,CAClC,GAAI,MAAA,EAMF,OAGF,MAAA,GAAiB,CAGnB,OAAO,MAAA,EAGT,IAAI,OAAiB,CACnB,OAAO,MAAA,GAAe,MAAA,EAGxB,SAAgB,CACV,UAAA,EAEJ,OAAA,EAAiB,GACjB,IAAK,IAAM,KAAS,MAAA,EAAY,GAAO,CACvC,MAAA,EAAW,OAAO,EAGpB,CAAC,OAAO,UAAiB,CACvB,KAAK,SAAS,GAOL,GAAe,EAAkB,IAC5C,IAAI,EAAa,EAAS,EAAQ,CASvB,EAAb,cAAqC,CAA6C,CAChF,GAEA,YAAY,EAAc,EAAqB,EAA8B,CAC3E,MAAM,EAAK,EAAQ,CACnB,MAAA,EAAY,EAGd,IAAa,OAAW,CACtB,OAAO,MAAM,MAGf,IAAI,MAAM,EAAM,CACd,MAAA,EAAU,EAAE,CAGd,OAAO,EAA6B,CAClC,MAAA,EAAU,EAAG,KAAK,MAAM,CAAC,CAAC,GAMjB,GAAe,EAAc,EAAyB,IACjE,IAAI,EAAa,EAAK,EAAK,EAAQ,CAUxB,GACX,EACA,EACA,IACsB,MAAgB,EAA6B,GAAG,EAAQ,IAAK,GAAM,EAAE,MAAM,CAAC,CAAE,EAAQ"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ReactiveNode } from './runtime';
|
|
2
|
+
import { type Disposable, type ReactiveOptions, type ReadonlySignal, type Signal, _SIGNAL_BRAND } from './types';
|
|
3
|
+
/** A derived read-only signal with an explicit dispose method. */
|
|
4
|
+
export interface ComputedSignal<T> extends ReadonlySignal<T>, Disposable {
|
|
5
|
+
/** True when the computed value is stale (deps changed but not yet re-read) or disposed. */
|
|
6
|
+
readonly stale: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** @internal
|
|
9
|
+
* Lazy computed node. Seeded once at construction so .peek() is immediately valid
|
|
10
|
+
* (unless { lazy: true } is passed, in which case first compute defers to first read).
|
|
11
|
+
* - On dep change: marks dirty, notifies downstream (no recompute yet).
|
|
12
|
+
* - On .value read: recomputes if dirty, re-tracks fresh deps.
|
|
13
|
+
* - After dispose: _track() is skipped to prevent leaking outer effects.
|
|
14
|
+
*/
|
|
15
|
+
export declare class ComputedNode<T> extends ReactiveNode implements ComputedSignal<T> {
|
|
16
|
+
#private;
|
|
17
|
+
readonly [_SIGNAL_BRAND]: true;
|
|
18
|
+
constructor(compute: () => T, options?: ReactiveOptions<T> & {
|
|
19
|
+
lazy?: boolean;
|
|
20
|
+
});
|
|
21
|
+
get value(): T;
|
|
22
|
+
peek(): T;
|
|
23
|
+
get stale(): boolean;
|
|
24
|
+
dispose(): void;
|
|
25
|
+
[Symbol.dispose](): void;
|
|
26
|
+
}
|
|
27
|
+
/** Creates a derived read-only Signal whose value is recomputed lazily on `.value` read
|
|
28
|
+
* when dependencies have changed. Call `.dispose()` to stop tracking and free dependencies.
|
|
29
|
+
* Pass `{ lazy: true }` to defer the initial computation until the first `.value` read. */
|
|
30
|
+
export declare const computed: <T>(compute: () => T, options?: ReactiveOptions<T> & {
|
|
31
|
+
lazy?: boolean;
|
|
32
|
+
}) => ComputedSignal<T>;
|
|
33
|
+
/** A bidirectional computed Signal with an explicit dispose method. */
|
|
34
|
+
export interface WritableSignal<T> extends Signal<T>, Disposable {
|
|
35
|
+
/** True when the backing computed value is stale (deps changed but not yet re-read) or disposed. */
|
|
36
|
+
readonly stale: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** @internal */
|
|
39
|
+
export declare class WritableNode<T> extends ComputedNode<T> implements WritableSignal<T> {
|
|
40
|
+
#private;
|
|
41
|
+
constructor(get: () => T, set: (v: T) => void, options?: ReactiveOptions<T>);
|
|
42
|
+
get value(): T;
|
|
43
|
+
set value(v: T);
|
|
44
|
+
update(fn: (current: T) => T): void;
|
|
45
|
+
}
|
|
46
|
+
/** Creates a bidirectional computed Signal. Reads track the getter reactively;
|
|
47
|
+
* writes are forwarded to `set`. Call `.dispose()` to stop tracking and free dependencies. */
|
|
48
|
+
export declare const writable: <T>(get: () => T, set: (value: T) => void, options?: ReactiveOptions<T>) => WritableSignal<T>;
|
|
49
|
+
/**
|
|
50
|
+
* Creates a derived ComputedSignal by combining multiple source signals through a projector
|
|
51
|
+
* function. Each source is passed as a positional argument to `fn`; the projector is
|
|
52
|
+
* re-evaluated whenever any source changes.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* const total = derived([price, quantity, discount], (p, q, d) => p * q * (1 - d));
|
|
56
|
+
*/
|
|
57
|
+
export declare const derived: <const Srcs extends ReadonlyArray<ReadonlySignal<unknown>>, R>(sources: Srcs, fn: (...values: { [K in keyof Srcs]: Srcs[K] extends ReadonlySignal<infer V> ? V : never; }) => R, options?: ReactiveOptions<R>) => ComputedSignal<R>;
|
|
58
|
+
//# sourceMappingURL=computed.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"computed.d.ts","sourceRoot":"","sources":["../src/computed.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAkC,MAAM,WAAW,CAAC;AACzE,OAAO,EAEL,KAAK,UAAU,EAGf,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,MAAM,EACX,aAAa,EACd,MAAM,SAAS,CAAC;AAEjB,kEAAkE;AAClE,MAAM,WAAW,cAAc,CAAC,CAAC,CAAE,SAAQ,cAAc,CAAC,CAAC,CAAC,EAAE,UAAU;IACtE,4FAA4F;IAC5F,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;GAMG;AACH,qBAAa,YAAY,CAAC,CAAC,CAAE,SAAQ,YAAa,YAAW,cAAc,CAAC,CAAC,CAAC;;IAC5E,QAAQ,CAAC,CAAC,aAAa,CAAC,EAAG,IAAI,CAAU;gBAe7B,OAAO,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE;IAqB/E,IAAI,KAAK,IAAI,CAAC,CAYb;IAED,IAAI,IAAI,CAAC;IAiBT,IAAI,KAAK,IAAI,OAAO,CAEnB;IAED,OAAO,IAAI,IAAI;IAQf,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI;CAGzB;AAED;;2FAE2F;AAC3F,eAAO,MAAM,QAAQ,GAAI,CAAC,EAAE,SAAS,MAAM,CAAC,EAAE,UAAU,eAAe,CAAC,CAAC,CAAC,GAAG;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,KAAG,cAAc,CAAC,CAAC,CAC9E,CAAC;AAErC,uEAAuE;AACvE,MAAM,WAAW,cAAc,CAAC,CAAC,CAAE,SAAQ,MAAM,CAAC,CAAC,CAAC,EAAE,UAAU;IAC9D,oGAAoG;IACpG,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED,gBAAgB;AAChB,qBAAa,YAAY,CAAC,CAAC,CAAE,SAAQ,YAAY,CAAC,CAAC,CAAE,YAAW,cAAc,CAAC,CAAC,CAAC;;gBAGnE,GAAG,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,OAAO,CAAC,EAAE,eAAe,CAAC,CAAC,CAAC;IAK3E,IAAa,KAAK,IAAI,CAAC,CAEtB;IAED,IAAI,KAAK,CAAC,CAAC,EAAE,CAAC,EAEb;IAED,MAAM,CAAC,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,GAAG,IAAI;CAGpC;AAED;8FAC8F;AAC9F,eAAO,MAAM,QAAQ,GAAI,CAAC,EAAE,KAAK,MAAM,CAAC,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,EAAE,UAAU,eAAe,CAAC,CAAC,CAAC,KAAG,cAAc,CAAC,CAAC,CAC7E,CAAC;AAEtC;;;;;;;GAOG;AACH,eAAO,MAAM,OAAO,GAAI,KAAK,CAAC,IAAI,SAAS,aAAa,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,EAClF,SAAS,IAAI,EACb,IAAI,CAAC,GAAG,MAAM,EAAE,GAAG,CAAC,IAAI,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,SAAS,cAAc,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,GAAE,KAAK,CAAC,EAChG,UAAU,eAAe,CAAC,CAAC,CAAC,KAC3B,cAAc,CAAC,CAAC,CAA2F,CAAC"}
|
package/dist/computed.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { ReactiveNode as e, _UNINITIALIZED as t, _withCtx as n } from "./runtime.js";
|
|
2
|
+
import { _SIGNAL_BRAND as r } from "./types.js";
|
|
3
|
+
//#region src/computed.ts
|
|
4
|
+
var i = class extends e {
|
|
5
|
+
[r] = !0;
|
|
6
|
+
#e;
|
|
7
|
+
#t = t;
|
|
8
|
+
#n = !0;
|
|
9
|
+
#r = !1;
|
|
10
|
+
#i;
|
|
11
|
+
#a = /* @__PURE__ */ new Set();
|
|
12
|
+
#o = () => {
|
|
13
|
+
this.#n || (this.#n = !0, this._notify());
|
|
14
|
+
};
|
|
15
|
+
constructor(e, t) {
|
|
16
|
+
super(), this.#e = e, this.#i = t?.equals ?? Object.is, t?.lazy || this.#s();
|
|
17
|
+
}
|
|
18
|
+
#s() {
|
|
19
|
+
for (let e of this.#a) e();
|
|
20
|
+
this.#a.clear();
|
|
21
|
+
let e = n(this.#o, this.#a, null, this.#e);
|
|
22
|
+
this.#n = !1, (this.#t === t || !this.#i(this.#t, e)) && (this.#t = e);
|
|
23
|
+
}
|
|
24
|
+
get value() {
|
|
25
|
+
return this.#r ? this.#t : (this.#n && this.#s(), this._track(), this.#t);
|
|
26
|
+
}
|
|
27
|
+
peek() {
|
|
28
|
+
if (this.#t === t) {
|
|
29
|
+
if (this.#r) return;
|
|
30
|
+
this.#s();
|
|
31
|
+
}
|
|
32
|
+
return this.#t;
|
|
33
|
+
}
|
|
34
|
+
get stale() {
|
|
35
|
+
return this.#n || this.#r;
|
|
36
|
+
}
|
|
37
|
+
dispose() {
|
|
38
|
+
if (!this.#r) {
|
|
39
|
+
this.#r = !0;
|
|
40
|
+
for (let e of this.#a) e();
|
|
41
|
+
this.#a.clear();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
[Symbol.dispose]() {
|
|
45
|
+
this.dispose();
|
|
46
|
+
}
|
|
47
|
+
}, a = (e, t) => new i(e, t), o = class extends i {
|
|
48
|
+
#e;
|
|
49
|
+
constructor(e, t, n) {
|
|
50
|
+
super(e, n), this.#e = t;
|
|
51
|
+
}
|
|
52
|
+
get value() {
|
|
53
|
+
return super.value;
|
|
54
|
+
}
|
|
55
|
+
set value(e) {
|
|
56
|
+
this.#e(e);
|
|
57
|
+
}
|
|
58
|
+
update(e) {
|
|
59
|
+
this.#e(e(this.peek()));
|
|
60
|
+
}
|
|
61
|
+
}, s = (e, t, n) => new o(e, t, n), c = (e, t, n) => a(() => t(...e.map((e) => e.value)), n);
|
|
62
|
+
//#endregion
|
|
63
|
+
export { i as ComputedNode, o as WritableNode, a as computed, c as derived, s as writable };
|
|
64
|
+
|
|
65
|
+
//# sourceMappingURL=computed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"computed.js","names":["#onDepChange","#dirty","#compute","#equals","#recompute","#deps","#value","#disposed","#set"],"sources":["../src/computed.ts"],"sourcesContent":["import { ReactiveNode, _DEV, _UNINITIALIZED, _withCtx } from './runtime';\nimport {\n type CleanupFn,\n type Disposable,\n type EffectCallback,\n type EqualityFn,\n type ReactiveOptions,\n type ReadonlySignal,\n type Signal,\n _SIGNAL_BRAND,\n} from './types';\n\n/** A derived read-only signal with an explicit dispose method. */\nexport interface ComputedSignal<T> extends ReadonlySignal<T>, Disposable {\n /** True when the computed value is stale (deps changed but not yet re-read) or disposed. */\n readonly stale: boolean;\n}\n\n/** @internal\n * Lazy computed node. Seeded once at construction so .peek() is immediately valid\n * (unless { lazy: true } is passed, in which case first compute defers to first read).\n * - On dep change: marks dirty, notifies downstream (no recompute yet).\n * - On .value read: recomputes if dirty, re-tracks fresh deps.\n * - After dispose: _track() is skipped to prevent leaking outer effects.\n */\nexport class ComputedNode<T> extends ReactiveNode implements ComputedSignal<T> {\n readonly [_SIGNAL_BRAND] = true as const;\n #compute: () => T;\n #value: T | typeof _UNINITIALIZED = _UNINITIALIZED;\n #dirty = true;\n #disposed = false;\n #equals: EqualityFn<T>;\n #deps = new Set<CleanupFn>();\n\n readonly #onDepChange: EffectCallback = () => {\n if (!this.#dirty) {\n this.#dirty = true;\n this._notify();\n }\n };\n\n constructor(compute: () => T, options?: ReactiveOptions<T> & { lazy?: boolean }) {\n super();\n this.#compute = compute;\n this.#equals = options?.equals ?? Object.is;\n\n if (!options?.lazy) this.#recompute(); // seed: ensures peek() is valid immediately\n }\n\n #recompute(): void {\n for (const unsub of this.#deps) unsub();\n this.#deps.clear();\n\n const result = _withCtx(this.#onDepChange, this.#deps, null, this.#compute);\n\n this.#dirty = false;\n\n if (this.#value === _UNINITIALIZED || !this.#equals(this.#value as T, result)) {\n this.#value = result;\n }\n }\n\n get value(): T {\n if (this.#disposed) {\n if (_DEV) console.warn('[stateit] Reading a disposed ComputedSignal returns a stale value.');\n\n return this.#value as T;\n }\n\n if (this.#dirty) this.#recompute();\n\n this._track();\n\n return this.#value as T;\n }\n\n peek(): T {\n if (this.#value === _UNINITIALIZED) {\n if (this.#disposed) {\n if (_DEV)\n console.warn(\n '[stateit] peek() called on a disposed lazy ComputedSignal that was never read — returning undefined.',\n );\n\n return undefined as unknown as T;\n }\n\n this.#recompute();\n }\n\n return this.#value as T;\n }\n\n get stale(): boolean {\n return this.#dirty || this.#disposed;\n }\n\n dispose(): void {\n if (this.#disposed) return;\n\n this.#disposed = true;\n for (const unsub of this.#deps) unsub();\n this.#deps.clear();\n }\n\n [Symbol.dispose](): void {\n this.dispose();\n }\n}\n\n/** Creates a derived read-only Signal whose value is recomputed lazily on `.value` read\n * when dependencies have changed. Call `.dispose()` to stop tracking and free dependencies.\n * Pass `{ lazy: true }` to defer the initial computation until the first `.value` read. */\nexport const computed = <T>(compute: () => T, options?: ReactiveOptions<T> & { lazy?: boolean }): ComputedSignal<T> =>\n new ComputedNode(compute, options);\n\n/** A bidirectional computed Signal with an explicit dispose method. */\nexport interface WritableSignal<T> extends Signal<T>, Disposable {\n /** True when the backing computed value is stale (deps changed but not yet re-read) or disposed. */\n readonly stale: boolean;\n}\n\n/** @internal */\nexport class WritableNode<T> extends ComputedNode<T> implements WritableSignal<T> {\n readonly #set: (v: T) => void;\n\n constructor(get: () => T, set: (v: T) => void, options?: ReactiveOptions<T>) {\n super(get, options);\n this.#set = set;\n }\n\n override get value(): T {\n return super.value;\n }\n\n set value(v: T) {\n this.#set(v);\n }\n\n update(fn: (current: T) => T): void {\n this.#set(fn(this.peek()));\n }\n}\n\n/** Creates a bidirectional computed Signal. Reads track the getter reactively;\n * writes are forwarded to `set`. Call `.dispose()` to stop tracking and free dependencies. */\nexport const writable = <T>(get: () => T, set: (value: T) => void, options?: ReactiveOptions<T>): WritableSignal<T> =>\n new WritableNode(get, set, options);\n\n/**\n * Creates a derived ComputedSignal by combining multiple source signals through a projector\n * function. Each source is passed as a positional argument to `fn`; the projector is\n * re-evaluated whenever any source changes.\n *\n * @example\n * const total = derived([price, quantity, discount], (p, q, d) => p * q * (1 - d));\n */\nexport const derived = <const Srcs extends ReadonlyArray<ReadonlySignal<unknown>>, R>(\n sources: Srcs,\n fn: (...values: { [K in keyof Srcs]: Srcs[K] extends ReadonlySignal<infer V> ? V : never }) => R,\n options?: ReactiveOptions<R>,\n): ComputedSignal<R> => computed(() => (fn as (...args: any[]) => R)(...sources.map((s) => s.value)), options);\n"],"mappings":";;;AAyBA,IAAa,IAAb,cAAqC,EAA0C;CAC7E,CAAU,KAAiB;CAC3B;CACA,KAAoC;CACpC,KAAS;CACT,KAAY;CACZ;CACA,qBAAQ,IAAI,KAAgB;CAE5B,WAA8C;AAC5C,EAAK,MAAA,MACH,MAAA,IAAc,IACd,KAAK,SAAS;;CAIlB,YAAY,GAAkB,GAAmD;AAK/E,EAJA,OAAO,EACP,MAAA,IAAgB,GAChB,MAAA,IAAe,GAAS,UAAU,OAAO,IAEpC,GAAS,QAAM,MAAA,GAAiB;;CAGvC,KAAmB;AACjB,OAAK,IAAM,KAAS,MAAA,EAAY,IAAO;AACvC,QAAA,EAAW,OAAO;EAElB,IAAM,IAAS,EAAS,MAAA,GAAmB,MAAA,GAAY,MAAM,MAAA,EAAc;AAI3E,EAFA,MAAA,IAAc,KAEV,MAAA,MAAgB,KAAkB,CAAC,MAAA,EAAa,MAAA,GAAkB,EAAO,MAC3E,MAAA,IAAc;;CAIlB,IAAI,QAAW;AAWb,SAVI,MAAA,IAGK,MAAA,KAGL,MAAA,KAAa,MAAA,GAAiB,EAElC,KAAK,QAAQ,EAEN,MAAA;;CAGT,OAAU;AACR,MAAI,MAAA,MAAgB,GAAgB;AAClC,OAAI,MAAA,EAMF;AAGF,SAAA,GAAiB;;AAGnB,SAAO,MAAA;;CAGT,IAAI,QAAiB;AACnB,SAAO,MAAA,KAAe,MAAA;;CAGxB,UAAgB;AACV,aAAA,GAEJ;SAAA,IAAiB;AACjB,QAAK,IAAM,KAAS,MAAA,EAAY,IAAO;AACvC,SAAA,EAAW,OAAO;;;CAGpB,CAAC,OAAO,WAAiB;AACvB,OAAK,SAAS;;GAOL,KAAe,GAAkB,MAC5C,IAAI,EAAa,GAAS,EAAQ,EASvB,IAAb,cAAqC,EAA6C;CAChF;CAEA,YAAY,GAAc,GAAqB,GAA8B;AAE3E,EADA,MAAM,GAAK,EAAQ,EACnB,MAAA,IAAY;;CAGd,IAAa,QAAW;AACtB,SAAO,MAAM;;CAGf,IAAI,MAAM,GAAM;AACd,QAAA,EAAU,EAAE;;CAGd,OAAO,GAA6B;AAClC,QAAA,EAAU,EAAG,KAAK,MAAM,CAAC,CAAC;;GAMjB,KAAe,GAAc,GAAyB,MACjE,IAAI,EAAa,GAAK,GAAK,EAAQ,EAUxB,KACX,GACA,GACA,MACsB,QAAgB,EAA6B,GAAG,EAAQ,KAAK,MAAM,EAAE,MAAM,CAAC,EAAE,EAAQ"}
|
package/dist/effect.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require(`./runtime.cjs`);var t=(t,n)=>{let r,i=new Set,a=!1,o=!1,s=!1,c=n?.maxIterations??e._maxEffectIterations,l=()=>{r?.(),r=void 0;for(let e of i)e();i.clear()},u=()=>{o=!1,l();let a=[],c,u=!1;try{e._withCtx(d,i,a,()=>{let e=t();typeof e==`function`&&a.push(e)})}catch(e){if(n?.onError)u=!0,c=e;else throw e}return r=a.length>0?()=>{for(let e of a)e()}:void 0,u&&(l(),n.onError(c),s=!0),u},d=()=>{if(s||a){a&&(o=!0);return}a=!0;try{let e=0;do{if(++e>c)throw Error(`[stateit] effect: possible infinite reactive loop (> ${c} iterations)`);if(u())break}while(o)}finally{a=!1}};d();let f=()=>{s||(s=!0,l())};return Object.assign(f,{dispose:f,[Symbol.dispose]:f})},n=t=>e._withCtx(null,null,null,t),r=t=>{e.scope.cleanups?.push(t)};exports.effect=t,exports.onCleanup=r,exports.untrack=n;
|
|
2
|
+
//# sourceMappingURL=effect.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"effect.cjs","names":[],"sources":["../src/effect.ts"],"sourcesContent":["import { _DEV, _maxEffectIterations, _withCtx, scope } from './runtime';\nimport { type CleanupFn, type EffectCallback, type Subscription } from './types';\n\n/** Options for the `effect()` primitive. */\nexport type EffectOptions = {\n /**\n * Maximum re-entrant iterations before throwing to prevent infinite loops.\n * Overrides the global value set via `configureStateit` for this specific effect.\n */\n maxIterations?: number;\n /**\n * Called when the effect function throws. When provided, the effect is automatically\n * disposed after the first error — it won't re-run on subsequent dependency changes.\n */\n onError?: (error: unknown) => void;\n};\n\n/** Runs fn immediately and re-runs it whenever any Signal read inside it changes. Returns a dispose handle. */\nexport const effect = (fn: EffectCallback, options?: EffectOptions): Subscription => {\n let cleanup: CleanupFn | undefined;\n const deps = new Set<CleanupFn>();\n let running = false;\n let dirty = false;\n let disposed = false;\n const maxIter = options?.maxIterations ?? _maxEffectIterations;\n\n /** Tears down the current run: calls cleanup and unsubscribes all deps. */\n const teardown = (): void => {\n cleanup?.();\n cleanup = undefined;\n for (const unsub of deps) unsub();\n deps.clear();\n };\n\n /** Runs a single iteration: tears down previous deps/cleanup, re-executes fn, registers fresh deps.\n * Returns true if onError handled a throw (caller should break the loop). */\n const runIteration = (): boolean => {\n dirty = false;\n teardown();\n\n const cleanups: CleanupFn[] = [];\n let thrownError: unknown;\n let threw = false;\n\n try {\n _withCtx(runner, deps, cleanups, () => {\n const result = fn();\n\n if (typeof result === 'function') cleanups.push(result);\n });\n } catch (e) {\n if (options?.onError) {\n threw = true;\n thrownError = e;\n } else throw e;\n }\n\n cleanup =\n cleanups.length > 0\n ? () => {\n for (const c of cleanups) c();\n }\n : undefined;\n\n if (threw) {\n teardown(); // flush onCleanup registrations from the throwing run\n options!.onError!(thrownError);\n disposed = true;\n }\n\n return threw;\n };\n\n const runner: EffectCallback = () => {\n if (disposed || running) {\n if (running) dirty = true;\n\n return;\n }\n\n running = true;\n\n try {\n let iterations = 0;\n\n do {\n if (++iterations > maxIter)\n throw new Error(`[stateit] effect: possible infinite reactive loop (> ${maxIter} iterations)`);\n\n if (runIteration()) break;\n } while (dirty);\n } finally {\n running = false;\n }\n };\n\n runner();\n\n const dispose = (): void => {\n if (disposed) return;\n\n disposed = true;\n teardown();\n };\n\n return Object.assign(dispose, { dispose, [Symbol.dispose]: dispose }) as Subscription;\n};\n\n/** Runs fn without registering any reactive dependencies. */\nexport const untrack = <T>(fn: () => T): T => _withCtx(null, null, null, fn);\n\n/**\n * Registers a cleanup function within the currently running effect.\n * Called before the effect re-runs and on final dispose.\n * Allows nested helpers to register teardown without needing the effect's return value.\n *\n * @example\n * effect(() => {\n * const id = setInterval(() => { ... }, 1000);\n * onCleanup(() => clearInterval(id));\n * });\n */\nexport const onCleanup = (fn: CleanupFn): void => {\n if (_DEV && scope.cleanups === null) {\n console.warn('[stateit] onCleanup() called outside of an active effect — the cleanup will never run.');\n }\n\n scope.cleanups?.push(fn);\n};\n"],"mappings":"iCAkBA,IAAa,GAAU,EAAoB,IAA0C,CACnF,IAAI,EACE,EAAO,IAAI,IACb,EAAU,GACV,EAAQ,GACR,EAAW,GACT,EAAU,GAAS,eAAiB,EAAA,qBAGpC,MAAuB,CAC3B,KAAW,CACX,EAAU,IAAA,GACV,IAAK,IAAM,KAAS,EAAM,GAAO,CACjC,EAAK,OAAO,EAKR,MAA8B,CAClC,EAAQ,GACR,GAAU,CAEV,IAAM,EAAwB,EAAE,CAC5B,EACA,EAAQ,GAEZ,GAAI,CACF,EAAA,SAAS,EAAQ,EAAM,MAAgB,CACrC,IAAM,EAAS,GAAI,CAEf,OAAO,GAAW,YAAY,EAAS,KAAK,EAAO,EACvD,OACK,EAAG,CACV,GAAI,GAAS,QACX,EAAQ,GACR,EAAc,OACT,MAAM,EAgBf,MAbA,GACE,EAAS,OAAS,MACR,CACJ,IAAK,IAAM,KAAK,EAAU,GAAG,EAE/B,IAAA,GAEF,IACF,GAAU,CACV,EAAS,QAAS,EAAY,CAC9B,EAAW,IAGN,GAGH,MAA+B,CACnC,GAAI,GAAY,EAAS,CACnB,IAAS,EAAQ,IAErB,OAGF,EAAU,GAEV,GAAI,CACF,IAAI,EAAa,EAEjB,EAAG,CACD,GAAI,EAAE,EAAa,EACjB,MAAU,MAAM,wDAAwD,EAAQ,cAAc,CAEhG,GAAI,GAAc,CAAE,YACb,UACD,CACR,EAAU,KAId,GAAQ,CAER,IAAM,MAAsB,CACtB,IAEJ,EAAW,GACX,GAAU,GAGZ,OAAO,OAAO,OAAO,EAAS,CAAE,WAAU,OAAO,SAAU,EAAS,CAAC,EAI1D,EAAc,GAAmB,EAAA,SAAS,KAAM,KAAM,KAAM,EAAG,CAa/D,EAAa,GAAwB,CAKhD,EAAA,MAAM,UAAU,KAAK,EAAG"}
|
package/dist/effect.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type CleanupFn, type EffectCallback, type Subscription } from './types';
|
|
2
|
+
/** Options for the `effect()` primitive. */
|
|
3
|
+
export type EffectOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* Maximum re-entrant iterations before throwing to prevent infinite loops.
|
|
6
|
+
* Overrides the global value set via `configureStateit` for this specific effect.
|
|
7
|
+
*/
|
|
8
|
+
maxIterations?: number;
|
|
9
|
+
/**
|
|
10
|
+
* Called when the effect function throws. When provided, the effect is automatically
|
|
11
|
+
* disposed after the first error — it won't re-run on subsequent dependency changes.
|
|
12
|
+
*/
|
|
13
|
+
onError?: (error: unknown) => void;
|
|
14
|
+
};
|
|
15
|
+
/** Runs fn immediately and re-runs it whenever any Signal read inside it changes. Returns a dispose handle. */
|
|
16
|
+
export declare const effect: (fn: EffectCallback, options?: EffectOptions) => Subscription;
|
|
17
|
+
/** Runs fn without registering any reactive dependencies. */
|
|
18
|
+
export declare const untrack: <T>(fn: () => T) => T;
|
|
19
|
+
/**
|
|
20
|
+
* Registers a cleanup function within the currently running effect.
|
|
21
|
+
* Called before the effect re-runs and on final dispose.
|
|
22
|
+
* Allows nested helpers to register teardown without needing the effect's return value.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* effect(() => {
|
|
26
|
+
* const id = setInterval(() => { ... }, 1000);
|
|
27
|
+
* onCleanup(() => clearInterval(id));
|
|
28
|
+
* });
|
|
29
|
+
*/
|
|
30
|
+
export declare const onCleanup: (fn: CleanupFn) => void;
|
|
31
|
+
//# sourceMappingURL=effect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"effect.d.ts","sourceRoot":"","sources":["../src/effect.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,cAAc,EAAE,KAAK,YAAY,EAAE,MAAM,SAAS,CAAC;AAEjF,4CAA4C;AAC5C,MAAM,MAAM,aAAa,GAAG;IAC1B;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC,CAAC;AAEF,+GAA+G;AAC/G,eAAO,MAAM,MAAM,GAAI,IAAI,cAAc,EAAE,UAAU,aAAa,KAAG,YAwFpE,CAAC;AAEF,6DAA6D;AAC7D,eAAO,MAAM,OAAO,GAAI,CAAC,EAAE,IAAI,MAAM,CAAC,KAAG,CAAmC,CAAC;AAE7E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,GAAI,IAAI,SAAS,KAAG,IAMzC,CAAC"}
|