@ibodr/state 0.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.
@@ -0,0 +1,1169 @@
1
+ /**
2
+ * An ArraySet operates as an array until it reaches a certain size, after which a Set is used
3
+ * instead. In either case, the same methods are used to get, set, remove, and visit the items.
4
+ * @internal
5
+ */
6
+ declare class ArraySet<T> {
7
+ private arraySize;
8
+ private array;
9
+ private set;
10
+ /**
11
+ * Get whether this ArraySet has any elements.
12
+ *
13
+ * @returns True if this ArraySet has any elements, false otherwise.
14
+ */
15
+ get isEmpty(): boolean;
16
+ /**
17
+ * Add an element to the ArraySet if it is not already present.
18
+ *
19
+ * @param elem - The element to add to the set
20
+ * @returns `true` if the element was added, `false` if it was already present
21
+ * @example
22
+ * ```ts
23
+ * const arraySet = new ArraySet<string>()
24
+ *
25
+ * console.log(arraySet.add('hello')) // true
26
+ * console.log(arraySet.add('hello')) // false (already exists)
27
+ * ```
28
+ */
29
+ add(elem: T): boolean;
30
+ /**
31
+ * Remove an element from the ArraySet if it is present.
32
+ *
33
+ * @param elem - The element to remove from the set
34
+ * @returns `true` if the element was removed, `false` if it was not present
35
+ * @example
36
+ * ```ts
37
+ * const arraySet = new ArraySet<string>()
38
+ * arraySet.add('hello')
39
+ *
40
+ * console.log(arraySet.remove('hello')) // true
41
+ * console.log(arraySet.remove('hello')) // false (not present)
42
+ * ```
43
+ */
44
+ remove(elem: T): boolean;
45
+ /**
46
+ * Execute a callback function for each element in the ArraySet.
47
+ *
48
+ * @param visitor - A function to call for each element in the set
49
+ * @example
50
+ * ```ts
51
+ * const arraySet = new ArraySet<string>()
52
+ * arraySet.add('hello')
53
+ * arraySet.add('world')
54
+ *
55
+ * arraySet.visit((item) => {
56
+ * console.log(item) // 'hello', 'world'
57
+ * })
58
+ * ```
59
+ */
60
+ visit(visitor: (item: T) => void): void;
61
+ /**
62
+ * Make the ArraySet iterable, allowing it to be used in for...of loops and with spread syntax.
63
+ *
64
+ * @returns An iterator that yields each element in the set
65
+ * @example
66
+ * ```ts
67
+ * const arraySet = new ArraySet<number>()
68
+ * arraySet.add(1)
69
+ * arraySet.add(2)
70
+ *
71
+ * for (const item of arraySet) {
72
+ * console.log(item) // 1, 2
73
+ * }
74
+ *
75
+ * const items = [...arraySet] // [1, 2]
76
+ * ```
77
+ */
78
+ [Symbol.iterator](): Generator<T, void, unknown>;
79
+ /**
80
+ * Check whether an element is present in the ArraySet.
81
+ *
82
+ * @param elem - The element to check for
83
+ * @returns `true` if the element is present, `false` otherwise
84
+ * @example
85
+ * ```ts
86
+ * const arraySet = new ArraySet<string>()
87
+ * arraySet.add('hello')
88
+ *
89
+ * console.log(arraySet.has('hello')) // true
90
+ * console.log(arraySet.has('world')) // false
91
+ * ```
92
+ */
93
+ has(elem: T): boolean;
94
+ /**
95
+ * Remove all elements from the ArraySet.
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * const arraySet = new ArraySet<string>()
100
+ * arraySet.add('hello')
101
+ * arraySet.add('world')
102
+ *
103
+ * arraySet.clear()
104
+ * console.log(arraySet.size()) // 0
105
+ * ```
106
+ */
107
+ clear(): void;
108
+ /**
109
+ * Get the number of elements in the ArraySet.
110
+ *
111
+ * @returns The number of elements in the set
112
+ * @example
113
+ * ```ts
114
+ * const arraySet = new ArraySet<string>()
115
+ * console.log(arraySet.size()) // 0
116
+ *
117
+ * arraySet.add('hello')
118
+ * console.log(arraySet.size()) // 1
119
+ * ```
120
+ */
121
+ size(): number;
122
+ }
123
+
124
+ /**
125
+ * A unique symbol used to indicate that a signal's value should be reset or that
126
+ * there is insufficient history to compute diffs between epochs.
127
+ *
128
+ * This value is returned by {@link Signal.getDiffSince} when the requested epoch
129
+ * is too far in the past and the diff sequence cannot be reconstructed.
130
+ *
131
+ * @example
132
+ * ```ts
133
+ * import { atom, getGlobalEpoch, RESET_VALUE } from '@ibodr/state'
134
+ *
135
+ * const count = atom('count', 0, { historyLength: 3 })
136
+ * const oldEpoch = getGlobalEpoch()
137
+ *
138
+ * // Make many changes that exceed history length
139
+ * count.set(1)
140
+ * count.set(2)
141
+ * count.set(3)
142
+ * count.set(4)
143
+ *
144
+ * const diffs = count.getDiffSince(oldEpoch)
145
+ * if (diffs === RESET_VALUE) {
146
+ * console.log('Too many changes, need to reset state')
147
+ * }
148
+ * ```
149
+ *
150
+ * @public
151
+ */
152
+ declare const RESET_VALUE: unique symbol;
153
+ /**
154
+ * Type representing the the unique symbol RESET_VALUE symbol, used in type annotations
155
+ * to indicate when a signal value should be reset or when diff computation
156
+ * cannot proceed due to insufficient history.
157
+ *
158
+ * @public
159
+ */
160
+ type RESET_VALUE = typeof RESET_VALUE;
161
+ /**
162
+ * A reactive value container that can change over time and track diffs between sequential values.
163
+ *
164
+ * Signals are the foundation of the \@ibodr/state reactive system. They automatically manage
165
+ * dependencies and trigger updates when their values change. Any computed signal or effect
166
+ * that reads from this signal will be automatically recomputed when the signal's value changes.
167
+ *
168
+ * There are two types of signal:
169
+ * - **Atomic signals** - Created using `atom()`. These are mutable containers that can be
170
+ * directly updated using `set()` or `update()` methods.
171
+ * - **Computed signals** - Created using `computed()`. These derive their values from other
172
+ * signals and are automatically recomputed when dependencies change.
173
+ *
174
+ * @example
175
+ * ```ts
176
+ * import { atom, computed } from '@ibodr/state'
177
+ *
178
+ * // Create an atomic signal
179
+ * const count = atom('count', 0)
180
+ *
181
+ * // Create a computed signal that derives from the atom
182
+ * const doubled = computed('doubled', () => count.get() * 2)
183
+ *
184
+ * console.log(doubled.get()) // 0
185
+ * count.set(5)
186
+ * console.log(doubled.get()) // 10
187
+ * ```
188
+ *
189
+ * @public
190
+ */
191
+ interface Signal<Value, Diff = unknown> {
192
+ /**
193
+ * A human-readable identifier for this signal, used primarily for debugging and performance profiling.
194
+ *
195
+ * The name is displayed in debug output from {@link whyAmIRunning} and other diagnostic tools.
196
+ * It does not need to be globally unique within your application.
197
+ */
198
+ name: string;
199
+ /**
200
+ * Gets the current value of the signal and establishes a dependency relationship.
201
+ *
202
+ * When called from within a computed signal or effect, this signal will be automatically
203
+ * tracked as a dependency. If this signal's value changes, any dependent computations
204
+ * or effects will be marked for re-execution.
205
+ *
206
+ * @returns The current value stored in the signal
207
+ */
208
+ get(): Value;
209
+ /**
210
+ * The global epoch number when this signal's value last changed.
211
+ *
212
+ * Note that this represents when the value actually changed, not when it was last computed.
213
+ * A computed signal may recalculate and produce the same value without changing its epoch.
214
+ * This is used internally for dependency tracking and history management.
215
+ */
216
+ lastChangedEpoch: number;
217
+ /**
218
+ * Gets the sequence of diffs that occurred between a specific epoch and the current state.
219
+ *
220
+ * This method enables incremental synchronization by providing a list of changes that
221
+ * have occurred since a specific point in time. If the requested epoch is too far in
222
+ * the past or the signal doesn't have enough history, it returns the unique symbol RESET_VALUE
223
+ * to indicate that a full state reset is required.
224
+ *
225
+ * @param epoch - The epoch timestamp to get diffs since
226
+ * @returns An array of diff objects representing changes since the epoch, or the unique symbol RESET_VALUE if insufficient history is available
227
+ */
228
+ getDiffSince(epoch: number): RESET_VALUE | Diff[];
229
+ /**
230
+ * Gets the current value of the signal without establishing a dependency relationship.
231
+ *
232
+ * This method bypasses the automatic dependency tracking system, making it useful for
233
+ * performance-critical code paths where the overhead of dependency capture would be
234
+ * problematic. Use with caution as it breaks the reactive guarantees of the system.
235
+ *
236
+ * **Warning**: This method should only be used when you're certain that you don't need
237
+ * the calling context to react to changes in this signal.
238
+ *
239
+ * @param ignoreErrors - Whether to suppress errors during value retrieval (optional)
240
+ * @returns The current value without establishing dependencies
241
+ */
242
+ __unsafe__getWithoutCapture(ignoreErrors?: boolean): Value;
243
+ /** @internal */
244
+ children: ArraySet<Child>;
245
+ }
246
+ /**
247
+ * Internal interface representing a child node in the signal dependency graph.
248
+ *
249
+ * This interface is used internally by the reactive system to manage dependencies
250
+ * between signals, computed values, and effects. Each child tracks its parent
251
+ * signals and maintains state needed for efficient dependency graph traversal
252
+ * and change propagation.
253
+ *
254
+ * @internal
255
+ */
256
+ interface Child {
257
+ /**
258
+ * The epoch when this child was last traversed during dependency graph updates.
259
+ * Used to prevent redundant traversals during change propagation.
260
+ */
261
+ lastTraversedEpoch: number;
262
+ /**
263
+ * Set of parent signals that this child depends on.
264
+ * Used for efficient lookup and cleanup operations.
265
+ */
266
+ readonly parentSet: ArraySet<Signal<any, any>>;
267
+ /**
268
+ * Array of parent signals that this child depends on.
269
+ * Maintained in parallel with parentSet for ordered access.
270
+ */
271
+ readonly parents: Signal<any, any>[];
272
+ /**
273
+ * Array of epochs corresponding to each parent signal.
274
+ * Used to detect which parents have changed since last computation.
275
+ */
276
+ readonly parentEpochs: number[];
277
+ /**
278
+ * Human-readable name for this child, used in debugging output.
279
+ */
280
+ readonly name: string;
281
+ /**
282
+ * Whether this child is currently subscribed to change notifications.
283
+ * Used to optimize resource usage by unsubscribing inactive dependencies.
284
+ */
285
+ isActivelyListening: boolean;
286
+ /**
287
+ * Debug information tracking ancestor epochs in the dependency graph.
288
+ * Only populated in debug builds for diagnostic purposes.
289
+ */
290
+ __debug_ancestor_epochs__: Map<Signal<any, any>, number> | null;
291
+ }
292
+ /**
293
+ * A function type that computes the difference between two values of a signal.
294
+ *
295
+ * This function is used to generate incremental diffs that can be applied to
296
+ * reconstruct state changes over time. It's particularly useful for features
297
+ * like undo/redo, synchronization, and change tracking.
298
+ *
299
+ * The function should analyze the previous and current values and return a
300
+ * diff object that represents the change. If the diff cannot be computed
301
+ * (e.g., the values are too different or incompatible), it should return
302
+ * the unique symbol RESET_VALUE to indicate that a full state reset is required.
303
+ *
304
+ * @param previousValue - The previous value of the signal
305
+ * @param currentValue - The current value of the signal
306
+ * @param lastComputedEpoch - The epoch when the previous value was set
307
+ * @param currentEpoch - The epoch when the current value was set
308
+ * @returns A diff object representing the change, or the unique symbol RESET_VALUE if no diff can be computed
309
+ *
310
+ * @example
311
+ * ```ts
312
+ * import { atom, RESET_VALUE } from '@ibodr/state'
313
+ *
314
+ * // Simple numeric diff
315
+ * const numberDiff: ComputeDiff<number, number> = (prev, curr) => curr - prev
316
+ *
317
+ * // Array diff with reset fallback
318
+ * const arrayDiff: ComputeDiff<string[], { added: string[], removed: string[] }> = (prev, curr) => {
319
+ * if (prev.length > 1000 || curr.length > 1000) {
320
+ * return RESET_VALUE // Too complex, force reset
321
+ * }
322
+ * return {
323
+ * added: curr.filter(item => !prev.includes(item)),
324
+ * removed: prev.filter(item => !curr.includes(item))
325
+ * }
326
+ * }
327
+ *
328
+ * const count = atom('count', 0, { computeDiff: numberDiff })
329
+ * ```
330
+ *
331
+ * @public
332
+ */
333
+ type ComputeDiff<Value, Diff> = (previousValue: Value, currentValue: Value, lastComputedEpoch: number, currentEpoch: number) => Diff | RESET_VALUE;
334
+
335
+ /**
336
+ * The options to configure an atom, passed into the {@link atom} function.
337
+ * @public
338
+ */
339
+ interface AtomOptions<Value, Diff> {
340
+ /**
341
+ * The maximum number of diffs to keep in the history buffer.
342
+ *
343
+ * If you don't need to compute diffs, or if you will supply diffs manually via {@link Atom.set}, you can leave this as `undefined` and no history buffer will be created.
344
+ *
345
+ * If you expect the value to be part of an active effect subscription all the time, and to not change multiple times inside of a single transaction, you can set this to a relatively low number (e.g. 10).
346
+ *
347
+ * Otherwise, set this to a higher number based on your usage pattern and memory constraints.
348
+ *
349
+ */
350
+ historyLength?: number;
351
+ /**
352
+ * A method used to compute a diff between the atom's old and new values. If provided, it will not be used unless you also specify {@link AtomOptions.historyLength}.
353
+ */
354
+ computeDiff?: ComputeDiff<Value, Diff>;
355
+ /**
356
+ * If provided, this will be used to compare the old and new values of the atom to determine if the value has changed.
357
+ * By default, values are compared using first using strict equality (`===`), then `Object.is`, and finally any `.equals` method present in the object's prototype chain.
358
+ * @param a - The old value
359
+ * @param b - The new value
360
+ * @returns True if the values are equal, false otherwise.
361
+ */
362
+ isEqual?(a: any, b: any): boolean;
363
+ }
364
+ /**
365
+ * An Atom is a signal that can be updated directly by calling {@link Atom.set} or {@link Atom.update}.
366
+ *
367
+ * Atoms are created using the {@link atom} function.
368
+ *
369
+ * @example
370
+ * ```ts
371
+ * const name = atom('name', 'John')
372
+ *
373
+ * print(name.get()) // 'John'
374
+ * ```
375
+ *
376
+ * @public
377
+ */
378
+ interface Atom<Value, Diff = unknown> extends Signal<Value, Diff> {
379
+ /**
380
+ * Sets the value of this atom to the given value. If the value is the same as the current value, this is a no-op.
381
+ *
382
+ * @param value - The new value to set.
383
+ * @param diff - The diff to use for the update. If not provided, the diff will be computed using {@link AtomOptions.computeDiff}.
384
+ */
385
+ set(value: Value, diff?: Diff): Value;
386
+ /**
387
+ * Updates the value of this atom using the given updater function. If the returned value is the same as the current value, this is a no-op.
388
+ *
389
+ * @param updater - A function that takes the current value and returns the new value.
390
+ */
391
+ update(updater: (value: Value) => Value): Value;
392
+ }
393
+ /**
394
+ * Creates a new {@link Atom}.
395
+ *
396
+ * An Atom is a signal that can be updated directly by calling {@link Atom.set} or {@link Atom.update}.
397
+ *
398
+ * @example
399
+ * ```ts
400
+ * const name = atom('name', 'John')
401
+ *
402
+ * name.get() // 'John'
403
+ *
404
+ * name.set('Jane')
405
+ *
406
+ * name.get() // 'Jane'
407
+ * ```
408
+ *
409
+ * @public
410
+ */
411
+ declare function atom<Value, Diff = unknown>(
412
+ /**
413
+ * A name for the signal. This is used for debugging and profiling purposes, it does not need to be unique.
414
+ */
415
+ name: string,
416
+ /**
417
+ * The initial value of the signal.
418
+ */
419
+ initialValue: Value,
420
+ /**
421
+ * The options to configure the atom. See {@link AtomOptions}.
422
+ */
423
+ options?: AtomOptions<Value, Diff>): Atom<Value, Diff>;
424
+ /**
425
+ * Returns true if the given value is an {@link Atom}.
426
+ *
427
+ * @param value - The value to check
428
+ * @returns True if the value is an Atom, false otherwise
429
+ * @example
430
+ * ```ts
431
+ * const myAtom = atom('test', 42)
432
+ * const notAtom = 'hello'
433
+ *
434
+ * console.log(isAtom(myAtom)) // true
435
+ * console.log(isAtom(notAtom)) // false
436
+ * ```
437
+ * @public
438
+ */
439
+ declare function isAtom(value: unknown): value is Atom<unknown>;
440
+
441
+ /**
442
+ * Executes the given function without capturing any parents in the current capture context.
443
+ *
444
+ * This is mainly useful if you want to run an effect only when certain signals change while also
445
+ * dereferencing other signals which should not cause the effect to rerun on their own.
446
+ *
447
+ * @example
448
+ * ```ts
449
+ * const name = atom('name', 'Sam')
450
+ * const time = atom('time', () => new Date().getTime())
451
+ *
452
+ * setInterval(() => {
453
+ * time.set(new Date().getTime())
454
+ * })
455
+ *
456
+ * react('log name changes', () => {
457
+ * print(name.get(), 'was changed at', unsafe__withoutCapture(() => time.get()))
458
+ * })
459
+ *
460
+ * ```
461
+ *
462
+ * @public
463
+ */
464
+ declare function unsafe__withoutCapture<T>(fn: () => T): T;
465
+ /**
466
+ * A debugging tool that tells you why a computed signal or effect is running.
467
+ * Call in the body of a computed signal or effect function.
468
+ *
469
+ * @example
470
+ * ```ts
471
+ * const name = atom('name', 'Bob')
472
+ * react('greeting', () => {
473
+ * whyAmIRunning()
474
+ * print('Hello', name.get())
475
+ * })
476
+ *
477
+ * name.set('Alice')
478
+ *
479
+ * // 'greeting' is running because:
480
+ * // 'name' changed => 'Alice'
481
+ * ```
482
+ *
483
+ * @public
484
+ */
485
+ declare function whyAmIRunning(): void;
486
+
487
+ /**
488
+ * A special symbol used to indicate that a computed signal has not been initialized yet.
489
+ * This is passed as the `previousValue` parameter to a computed signal function on its first run.
490
+ *
491
+ * @example
492
+ * ```ts
493
+ * const count = atom('count', 0)
494
+ * const double = computed('double', (prevValue) => {
495
+ * if (isUninitialized(prevValue)) {
496
+ * console.log('First computation!')
497
+ * }
498
+ * return count.get() * 2
499
+ * })
500
+ * ```
501
+ *
502
+ * @public
503
+ */
504
+ declare const UNINITIALIZED: unique symbol;
505
+ /**
506
+ * The type of the first value passed to a computed signal function as the 'prevValue' parameter.
507
+ * This type represents the uninitialized state of a computed signal before its first calculation.
508
+ *
509
+ * @see {@link isUninitialized}
510
+ * @public
511
+ */
512
+ type UNINITIALIZED = typeof UNINITIALIZED;
513
+ /**
514
+ * Call this inside a computed signal function to determine whether it is the first time the function is being called.
515
+ *
516
+ * Mainly useful for incremental signal computation.
517
+ *
518
+ * @example
519
+ * ```ts
520
+ * const count = atom('count', 0)
521
+ * const double = computed('double', (prevValue) => {
522
+ * if (isUninitialized(prevValue)) {
523
+ * print('First time!')
524
+ * }
525
+ * return count.get() * 2
526
+ * })
527
+ * ```
528
+ *
529
+ * @param value - The value to check.
530
+ * @public
531
+ */
532
+ declare function isUninitialized(value: any): value is UNINITIALIZED;
533
+ /**
534
+ * A singleton class used to wrap computed signal values along with their diffs.
535
+ * This class is used internally by the {@link withDiff} function to provide both
536
+ * the computed value and its diff to the signal system.
537
+ *
538
+ * @example
539
+ * ```ts
540
+ * const count = atom('count', 0)
541
+ * const double = computed('double', (prevValue) => {
542
+ * const nextValue = count.get() * 2
543
+ * if (isUninitialized(prevValue)) {
544
+ * return nextValue
545
+ * }
546
+ * return withDiff(nextValue, nextValue - prevValue)
547
+ * })
548
+ * ```
549
+ *
550
+ * @public
551
+ */
552
+ declare const WithDiff: {
553
+ new <Value, Diff>(value: Value, diff: Diff): {
554
+ value: Value;
555
+ diff: Diff;
556
+ };
557
+ };
558
+ /**
559
+ * Interface representing a value wrapped with its corresponding diff.
560
+ * Used in incremental computation to provide both the new value and the diff from the previous value.
561
+ *
562
+ * @public
563
+ */
564
+ interface WithDiff<Value, Diff> {
565
+ /**
566
+ * The computed value.
567
+ */
568
+ value: Value;
569
+ /**
570
+ * The diff between the previous and current value.
571
+ */
572
+ diff: Diff;
573
+ }
574
+ /**
575
+ * When writing incrementally-computed signals it is convenient (and usually more performant) to incrementally compute the diff too.
576
+ *
577
+ * You can use this function to wrap the return value of a computed signal function to indicate that the diff should be used instead of calculating a new one with {@link AtomOptions.computeDiff}.
578
+ *
579
+ * @example
580
+ * ```ts
581
+ * const count = atom('count', 0)
582
+ * const double = computed('double', (prevValue) => {
583
+ * const nextValue = count.get() * 2
584
+ * if (isUninitialized(prevValue)) {
585
+ * return nextValue
586
+ * }
587
+ * return withDiff(nextValue, nextValue - prevValue)
588
+ * }, { historyLength: 10 })
589
+ * ```
590
+ *
591
+ *
592
+ * @param value - The value.
593
+ * @param diff - The diff.
594
+ * @public
595
+ */
596
+ declare function withDiff<Value, Diff>(value: Value, diff: Diff): WithDiff<Value, Diff>;
597
+ /**
598
+ * Options for configuring computed signals. Used when calling `computed` or using the `@computed` decorator.
599
+ *
600
+ * @example
601
+ * ```ts
602
+ * const greeting = computed('greeting', () => `Hello ${name.get()}!`, {
603
+ * historyLength: 10,
604
+ * isEqual: (a, b) => a === b,
605
+ * computeDiff: (oldVal, newVal) => ({ type: 'change', from: oldVal, to: newVal })
606
+ * })
607
+ * ```
608
+ *
609
+ * @public
610
+ */
611
+ interface ComputedOptions<Value, Diff> {
612
+ /**
613
+ * The maximum number of diffs to keep in the history buffer.
614
+ *
615
+ * If you don't need to compute diffs, or if you will supply diffs manually via {@link Atom.set}, you can leave this as `undefined` and no history buffer will be created.
616
+ *
617
+ * If you expect the value to be part of an active effect subscription all the time, and to not change multiple times inside of a single transaction, you can set this to a relatively low number (e.g. 10).
618
+ *
619
+ * Otherwise, set this to a higher number based on your usage pattern and memory constraints.
620
+ *
621
+ */
622
+ historyLength?: number;
623
+ /**
624
+ * A method used to compute a diff between the computed's old and new values. If provided, it will not be used unless you also specify {@link ComputedOptions.historyLength}.
625
+ */
626
+ computeDiff?: ComputeDiff<Value, Diff>;
627
+ /**
628
+ * If provided, this will be used to compare the old and new values of the computed to determine if the value has changed.
629
+ * By default, values are compared using first using strict equality (`===`), then `Object.is`, and finally any `.equals` method present in the object's prototype chain.
630
+ * @param a - The old value
631
+ * @param b - The new value
632
+ * @returns True if the values are equal, false otherwise.
633
+ */
634
+ isEqual?(a: any, b: any): boolean;
635
+ }
636
+ /**
637
+ * A computed signal created via the `computed` function or `@computed` decorator.
638
+ * Computed signals derive their values from other signals and automatically update when their dependencies change.
639
+ * They use lazy evaluation, only recalculating when accessed and dependencies have changed.
640
+ *
641
+ * @example
642
+ * ```ts
643
+ * const firstName = atom('firstName', 'John')
644
+ * const lastName = atom('lastName', 'Doe')
645
+ * const fullName = computed('fullName', () => `${firstName.get()} ${lastName.get()}`)
646
+ *
647
+ * console.log(fullName.get()) // "John Doe"
648
+ * firstName.set('Jane')
649
+ * console.log(fullName.get()) // "Jane Doe"
650
+ * ```
651
+ *
652
+ * @public
653
+ */
654
+ interface Computed<Value, Diff = unknown> extends Signal<Value, Diff> {
655
+ /**
656
+ * Whether this computed signal is involved in an actively-running effect graph.
657
+ * Returns true if there are any reactions or other computed signals depending on this one.
658
+ * @public
659
+ */
660
+ readonly isActivelyListening: boolean;
661
+ /** @internal */
662
+ readonly parentSet: ArraySet<Signal<any, any>>;
663
+ /** @internal */
664
+ readonly parents: Signal<any, any>[];
665
+ /** @internal */
666
+ readonly parentEpochs: number[];
667
+ }
668
+ /**
669
+ * Retrieves the underlying computed instance for a given property created with the `computed`
670
+ * decorator.
671
+ *
672
+ * @example
673
+ * ```ts
674
+ * class Counter {
675
+ * max = 100
676
+ * count = atom(0)
677
+ *
678
+ * @computed getRemaining() {
679
+ * return this.max - this.count.get()
680
+ * }
681
+ * }
682
+ *
683
+ * const c = new Counter()
684
+ * const remaining = getComputedInstance(c, 'getRemaining')
685
+ * remaining.get() === 100 // true
686
+ * c.count.set(13)
687
+ * remaining.get() === 87 // true
688
+ * ```
689
+ *
690
+ * @param obj - The object
691
+ * @param propertyName - The property name
692
+ * @public
693
+ */
694
+ declare function getComputedInstance<Obj extends object, Prop extends keyof Obj>(obj: Obj, propertyName: Prop): Computed<Obj[Prop]>;
695
+ /**
696
+ * Creates a computed signal that derives its value from other signals.
697
+ * Computed signals automatically update when their dependencies change and use lazy evaluation
698
+ * for optimal performance.
699
+ *
700
+ * @example
701
+ * ```ts
702
+ * const name = atom('name', 'John')
703
+ * const greeting = computed('greeting', () => `Hello ${name.get()}!`)
704
+ * console.log(greeting.get()) // 'Hello John!'
705
+ * ```
706
+ *
707
+ * `computed` may also be used as a decorator for creating computed getter methods.
708
+ *
709
+ * @example
710
+ * ```ts
711
+ * class Counter {
712
+ * max = 100
713
+ * count = atom<number>(0)
714
+ *
715
+ * @computed getRemaining() {
716
+ * return this.max - this.count.get()
717
+ * }
718
+ * }
719
+ * ```
720
+ *
721
+ * You may optionally pass in a {@link ComputedOptions} when used as a decorator:
722
+ *
723
+ * @example
724
+ * ```ts
725
+ * class Counter {
726
+ * max = 100
727
+ * count = atom<number>(0)
728
+ *
729
+ * @computed({isEqual: (a, b) => a === b})
730
+ * getRemaining() {
731
+ * return this.max - this.count.get()
732
+ * }
733
+ * }
734
+ * ```
735
+ *
736
+ * @param name - The name of the signal for debugging purposes
737
+ * @param compute - The function that computes the value of the signal. Receives the previous value and last computed epoch
738
+ * @param options - Optional configuration for the computed signal
739
+ * @returns A new computed signal
740
+ * @public
741
+ */
742
+ declare function computed<Value, Diff = unknown>(name: string, compute: (previousValue: Value | typeof UNINITIALIZED, lastComputedEpoch: number) => Value | WithDiff<Value, Diff>, options?: ComputedOptions<Value, Diff>): Computed<Value, Diff>;
743
+ /**
744
+ * TC39 decorator for creating computed methods in classes.
745
+ *
746
+ * @example
747
+ * ```ts
748
+ * class MyClass {
749
+ * value = atom('value', 10)
750
+ *
751
+ * @computed
752
+ * doubled() {
753
+ * return this.value.get() * 2
754
+ * }
755
+ * }
756
+ * ```
757
+ *
758
+ * @param compute - The method to be decorated
759
+ * @param context - The decorator context provided by TypeScript
760
+ * @returns The decorated method
761
+ * @public
762
+ */
763
+ declare function computed<This extends object, Value>(compute: () => Value, context: ClassMethodDecoratorContext<This, () => Value>): () => Value;
764
+ /**
765
+ * Legacy TypeScript decorator for creating computed methods in classes.
766
+ *
767
+ * @example
768
+ * ```ts
769
+ * class MyClass {
770
+ * value = atom('value', 10)
771
+ *
772
+ * @computed
773
+ * doubled() {
774
+ * return this.value.get() * 2
775
+ * }
776
+ * }
777
+ * ```
778
+ *
779
+ * @param target - The class prototype
780
+ * @param key - The property key
781
+ * @param descriptor - The property descriptor
782
+ * @returns The modified property descriptor
783
+ * @public
784
+ */
785
+ declare function computed(target: any, key: string, descriptor: PropertyDescriptor): PropertyDescriptor;
786
+ /**
787
+ * Decorator factory for creating computed methods with options.
788
+ *
789
+ * @example
790
+ * ```ts
791
+ * class MyClass {
792
+ * items = atom('items', [1, 2, 3])
793
+ *
794
+ * @computed({ historyLength: 10 })
795
+ * sum() {
796
+ * return this.items.get().reduce((a, b) => a + b, 0)
797
+ * }
798
+ * }
799
+ * ```
800
+ *
801
+ * @param options - Configuration options for the computed signal
802
+ * @returns A decorator function that can be applied to methods
803
+ * @public
804
+ */
805
+ declare function computed<Value, Diff = unknown>(options?: ComputedOptions<Value, Diff>): ((target: any, key: string, descriptor: PropertyDescriptor) => PropertyDescriptor) & (<This>(compute: () => Value, context: ClassMethodDecoratorContext<This, () => Value>) => () => Value);
806
+
807
+ /** @public */
808
+ interface EffectSchedulerOptions {
809
+ /**
810
+ * scheduleEffect is a function that will be called when the effect is scheduled.
811
+ *
812
+ * It can be used to defer running effects until a later time, for example to batch them together with requestAnimationFrame.
813
+ *
814
+ *
815
+ * @example
816
+ * ```ts
817
+ * let isRafScheduled = false
818
+ * const scheduledEffects: Array<() => void> = []
819
+ * const scheduleEffect = (runEffect: () => void) => {
820
+ * scheduledEffects.push(runEffect)
821
+ * if (!isRafScheduled) {
822
+ * isRafScheduled = true
823
+ * requestAnimationFrame(() => {
824
+ * isRafScheduled = false
825
+ * scheduledEffects.forEach((runEffect) => runEffect())
826
+ * scheduledEffects.length = 0
827
+ * })
828
+ * }
829
+ * }
830
+ * const stop = react('set page title', () => {
831
+ * document.title = doc.title,
832
+ * }, scheduleEffect)
833
+ * ```
834
+ *
835
+ * @param execute - A function that will execute the effect.
836
+ * @returns void
837
+ */
838
+ scheduleEffect?: (execute: () => void) => void;
839
+ }
840
+ /**
841
+ * An EffectScheduler is responsible for executing side effects in response to changes in state.
842
+ *
843
+ * You probably don't need to use this directly unless you're integrating this library with a framework of some kind.
844
+ *
845
+ * Instead, use the {@link react} and {@link reactor} functions.
846
+ *
847
+ * @example
848
+ * ```ts
849
+ * const render = new EffectScheduler('render', drawToCanvas)
850
+ *
851
+ * render.attach()
852
+ * render.execute()
853
+ * ```
854
+ *
855
+ * @public
856
+ */
857
+ declare const EffectScheduler: new <Result>(name: string, runEffect: (lastReactedEpoch: number) => Result, options?: EffectSchedulerOptions) => EffectScheduler<Result>;
858
+ /** @public */
859
+ interface EffectScheduler<Result> {
860
+ /**
861
+ * Whether this scheduler is attached and actively listening to its parents.
862
+ * @public
863
+ */
864
+ readonly isActivelyListening: boolean;
865
+ /** @internal */
866
+ readonly lastTraversedEpoch: number;
867
+ /** @public */
868
+ readonly name: string;
869
+ /** @internal */
870
+ __debug_ancestor_epochs__: Map<Signal<any, any>, number> | null;
871
+ /**
872
+ * The number of times this effect has been scheduled.
873
+ * @public
874
+ */
875
+ readonly scheduleCount: number;
876
+ /** @internal */
877
+ readonly parentSet: ArraySet<Signal<any, any>>;
878
+ /** @internal */
879
+ readonly parentEpochs: number[];
880
+ /** @internal */
881
+ readonly parents: Signal<any, any>[];
882
+ /** @internal */
883
+ maybeScheduleEffect(): void;
884
+ /** @internal */
885
+ scheduleEffect(): void;
886
+ /** @internal */
887
+ maybeExecute(): void;
888
+ /**
889
+ * Makes this scheduler become 'actively listening' to its parents.
890
+ * If it has been executed before it will immediately become eligible to receive 'maybeScheduleEffect' calls.
891
+ * If it has not executed before it will need to be manually executed once to become eligible for scheduling, i.e. by calling `EffectScheduler.execute`.
892
+ * @public
893
+ */
894
+ attach(): void;
895
+ /**
896
+ * Makes this scheduler stop 'actively listening' to its parents.
897
+ * It will no longer be eligible to receive 'maybeScheduleEffect' calls until `EffectScheduler.attach` is called again.
898
+ * @public
899
+ */
900
+ detach(): void;
901
+ /**
902
+ * Executes the effect immediately and returns the result.
903
+ * @returns The result of the effect.
904
+ * @public
905
+ */
906
+ execute(): Result;
907
+ }
908
+ /**
909
+ * Starts a new effect scheduler, scheduling the effect immediately.
910
+ *
911
+ * Returns a function that can be called to stop the scheduler.
912
+ *
913
+ * @example
914
+ * ```ts
915
+ * const color = atom('color', 'red')
916
+ * const stop = react('set style', () => {
917
+ * divElem.style.color = color.get()
918
+ * })
919
+ * color.set('blue')
920
+ * // divElem.style.color === 'blue'
921
+ * stop()
922
+ * color.set('green')
923
+ * // divElem.style.color === 'blue'
924
+ * ```
925
+ *
926
+ *
927
+ * Also useful in React applications for running effects outside of the render cycle.
928
+ *
929
+ * @example
930
+ * ```ts
931
+ * useEffect(() => react('set style', () => {
932
+ * divRef.current.style.color = color.get()
933
+ * }), [])
934
+ * ```
935
+ *
936
+ * @public
937
+ */
938
+ declare function react(name: string, fn: (lastReactedEpoch: number) => any, options?: EffectSchedulerOptions): () => void;
939
+ /**
940
+ * The reactor is a user-friendly interface for starting and stopping an `EffectScheduler`.
941
+ *
942
+ * Calling `.start()` will attach the scheduler and execute the effect immediately the first time it is called.
943
+ *
944
+ * If the reactor is stopped, calling `.start()` will re-attach the scheduler but will only execute the effect if any of its parents have changed since it was stopped.
945
+ *
946
+ * You can create a reactor with {@link reactor}.
947
+ * @public
948
+ */
949
+ interface Reactor<T = unknown> {
950
+ /**
951
+ * The underlying effect scheduler.
952
+ * @public
953
+ */
954
+ scheduler: EffectScheduler<T>;
955
+ /**
956
+ * Start the scheduler. The first time this is called the effect will be scheduled immediately.
957
+ *
958
+ * If the reactor is stopped, calling this will start the scheduler again but will only execute the effect if any of its parents have changed since it was stopped.
959
+ *
960
+ * If you need to force re-execution of the effect, pass `{ force: true }`.
961
+ * @public
962
+ */
963
+ start(options?: {
964
+ force?: boolean;
965
+ }): void;
966
+ /**
967
+ * Stop the scheduler.
968
+ * @public
969
+ */
970
+ stop(): void;
971
+ }
972
+ /**
973
+ * Creates a {@link Reactor}, which is a thin wrapper around an `EffectScheduler`.
974
+ *
975
+ * @public
976
+ */
977
+ declare function reactor<Result>(name: string, fn: (lastReactedEpoch: number) => Result, options?: EffectSchedulerOptions): Reactor<Result>;
978
+
979
+ /**
980
+ * @public
981
+ */
982
+ declare const EMPTY_ARRAY: [];
983
+
984
+ /**
985
+ * Type guard function that determines whether a value is a signal (either an Atom or Computed).
986
+ *
987
+ * This utility function is helpful when working with mixed data types and you need to
988
+ * differentiate between regular values and reactive signal containers. It returns `true`
989
+ * if the provided value is either an atomic signal created with `atom()` or a computed
990
+ * signal created with `computed()`.
991
+ *
992
+ * @param value - The value to check, can be of any type
993
+ * @returns `true` if the value is a Signal (Atom or Computed), `false` otherwise
994
+ *
995
+ * @example
996
+ * ```ts
997
+ * import { atom, computed, isSignal } from '@ibodr/state'
998
+ *
999
+ * const count = atom('count', 5)
1000
+ * const doubled = computed('doubled', () => count.get() * 2)
1001
+ * const regularValue = 'hello'
1002
+ *
1003
+ * console.log(isSignal(count)) // true
1004
+ * console.log(isSignal(doubled)) // true
1005
+ * console.log(isSignal(regularValue)) // false
1006
+ * console.log(isSignal(null)) // false
1007
+ * ```
1008
+ *
1009
+ * @public
1010
+ */
1011
+ declare function isSignal(value: any): value is Signal<any>;
1012
+
1013
+ /**
1014
+ * Creates a new {@link Atom} that persists its value to localStorage.
1015
+ *
1016
+ * The atom is automatically synced with localStorage - changes to the atom are saved to localStorage,
1017
+ * and the initial value is read from localStorage if it exists. Returns both the atom and a cleanup
1018
+ * function that should be called to stop syncing when the atom is no longer needed. If you need to delete
1019
+ * the atom, you should do it manually after all cleanup functions have been called.
1020
+ *
1021
+ * @example
1022
+ * ```ts
1023
+ * const [theme, cleanup] = localStorageAtom('theme', 'light')
1024
+ *
1025
+ * theme.get() // 'light' or value from localStorage if it exists
1026
+ *
1027
+ * theme.set('dark') // updates atom and saves to localStorage
1028
+ *
1029
+ * // When done:
1030
+ * cleanup() // stops syncing to localStorage
1031
+ * ```
1032
+ *
1033
+ * @param name - The localStorage key and atom name. This is used for both localStorage persistence
1034
+ * and debugging/profiling purposes.
1035
+ * @param initialValue - The initial value of the atom, used if no value exists in localStorage.
1036
+ * @param options - Optional atom configuration. See {@link AtomOptions}.
1037
+ * @returns A tuple containing the atom and a cleanup function to stop localStorage syncing.
1038
+ * @public
1039
+ */
1040
+ declare function localStorageAtom<Value, Diff = unknown>(name: string, initialValue: Value, options?: AtomOptions<Value, Diff>): [Atom<Value, Diff>, () => void];
1041
+
1042
+ /**
1043
+ * Batches state updates, deferring side effects until after the transaction completes.
1044
+ * Unlike {@link transact}, this function always creates a new transaction, allowing for nested transactions.
1045
+ *
1046
+ * @example
1047
+ * ```ts
1048
+ * const firstName = atom('firstName', 'John')
1049
+ * const lastName = atom('lastName', 'Doe')
1050
+ *
1051
+ * react('greet', () => {
1052
+ * console.log(`Hello, ${firstName.get()} ${lastName.get()}!`)
1053
+ * })
1054
+ *
1055
+ * // Logs "Hello, John Doe!"
1056
+ *
1057
+ * transaction(() => {
1058
+ * firstName.set('Jane')
1059
+ * lastName.set('Smith')
1060
+ * })
1061
+ *
1062
+ * // Logs "Hello, Jane Smith!"
1063
+ * ```
1064
+ *
1065
+ * If the function throws, the transaction is aborted and any signals that were updated during the transaction revert to their state before the transaction began.
1066
+ *
1067
+ * @example
1068
+ * ```ts
1069
+ * const firstName = atom('firstName', 'John')
1070
+ * const lastName = atom('lastName', 'Doe')
1071
+ *
1072
+ * react('greet', () => {
1073
+ * console.log(`Hello, ${firstName.get()} ${lastName.get()}!`)
1074
+ * })
1075
+ *
1076
+ * // Logs "Hello, John Doe!"
1077
+ *
1078
+ * transaction(() => {
1079
+ * firstName.set('Jane')
1080
+ * throw new Error('oops')
1081
+ * })
1082
+ *
1083
+ * // Does not log
1084
+ * // firstName.get() === 'John'
1085
+ * ```
1086
+ *
1087
+ * A `rollback` callback is passed into the function.
1088
+ * Calling this will prevent the transaction from committing and will revert any signals that were updated during the transaction to their state before the transaction began.
1089
+ *
1090
+ * @example
1091
+ * ```ts
1092
+ * const firstName = atom('firstName', 'John')
1093
+ * const lastName = atom('lastName', 'Doe')
1094
+ *
1095
+ * react('greet', () => {
1096
+ * console.log(`Hello, ${firstName.get()} ${lastName.get()}!`)
1097
+ * })
1098
+ *
1099
+ * // Logs "Hello, John Doe!"
1100
+ *
1101
+ * transaction((rollback) => {
1102
+ * firstName.set('Jane')
1103
+ * lastName.set('Smith')
1104
+ * rollback()
1105
+ * })
1106
+ *
1107
+ * // Does not log
1108
+ * // firstName.get() === 'John'
1109
+ * // lastName.get() === 'Doe'
1110
+ * ```
1111
+ *
1112
+ * @param fn - The function to run in a transaction, called with a function to roll back the change.
1113
+ * @returns The return value of the function
1114
+ * @public
1115
+ */
1116
+ declare function transaction<T>(fn: (rollback: () => void) => T): T;
1117
+ /**
1118
+ * Like {@link transaction}, but does not create a new transaction if there is already one in progress.
1119
+ * This is the preferred way to batch state updates when you don't need the rollback functionality.
1120
+ *
1121
+ * @example
1122
+ * ```ts
1123
+ * const count = atom('count', 0)
1124
+ * const doubled = atom('doubled', 0)
1125
+ *
1126
+ * react('update doubled', () => {
1127
+ * console.log(`Count: ${count.get()}, Doubled: ${doubled.get()}`)
1128
+ * })
1129
+ *
1130
+ * // This batches both updates into a single reaction
1131
+ * transact(() => {
1132
+ * count.set(5)
1133
+ * doubled.set(count.get() * 2)
1134
+ * })
1135
+ * // Logs: "Count: 5, Doubled: 10"
1136
+ * ```
1137
+ *
1138
+ * @param fn - The function to run in a transaction
1139
+ * @returns The return value of the function
1140
+ * @public
1141
+ */
1142
+ declare function transact<T>(fn: () => T): T;
1143
+ /**
1144
+ * Defers the execution of asynchronous effects until they can be properly handled.
1145
+ * This function creates an asynchronous transaction context that batches state updates
1146
+ * across async operations while preventing conflicts with synchronous transactions.
1147
+ *
1148
+ * @example
1149
+ * ```ts
1150
+ * const data = atom('data', null)
1151
+ * const loading = atom('loading', false)
1152
+ *
1153
+ * await deferAsyncEffects(async () => {
1154
+ * loading.set(true)
1155
+ * const result = await fetch('/api/data')
1156
+ * const json = await result.json()
1157
+ * data.set(json)
1158
+ * loading.set(false)
1159
+ * })
1160
+ * ```
1161
+ *
1162
+ * @param fn - The async function to execute within the deferred context
1163
+ * @returns A promise that resolves to the return value of the function
1164
+ * @throws Will throw if called during a synchronous transaction
1165
+ * @internal
1166
+ */
1167
+ declare function deferAsyncEffects<T>(fn: () => Promise<T>): Promise<T | undefined>;
1168
+
1169
+ export { ArraySet, type Atom, type AtomOptions, type Child, type ComputeDiff, type Computed, type ComputedOptions, EMPTY_ARRAY, EffectScheduler, type EffectSchedulerOptions, RESET_VALUE, type Reactor, type Signal, UNINITIALIZED, WithDiff, atom, computed, deferAsyncEffects, getComputedInstance, isAtom, isSignal, isUninitialized, localStorageAtom, react, reactor, transact, transaction, unsafe__withoutCapture, whyAmIRunning, withDiff };