@rlabs-inc/signals 1.4.0 → 1.4.2

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.
Files changed (2) hide show
  1. package/README.md +353 -229
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -2,23 +2,16 @@
2
2
 
3
3
  **Production-grade fine-grained reactivity for TypeScript.**
4
4
 
5
- A complete standalone mirror of Svelte 5's reactivity system. No compiler needed, no DOM, works anywhere - Bun, Node, Deno, or browser.
5
+ A complete standalone mirror of Svelte 5's reactivity system, enhanced with Angular's linkedSignal, Solid's createSelector, and Vue's effectScope. No compiler needed, no DOM, works anywhere - Bun, Node, Deno, or browser.
6
6
 
7
- ## Features
7
+ ## Why This Library?
8
8
 
9
9
  - **True Fine-Grained Reactivity** - Changes to deeply nested properties only trigger effects that read that exact path
10
10
  - **Per-Property Tracking** - Proxy-based deep reactivity with lazy signal creation per property
11
- - **Reactive Bindings** - Two-way data binding with `bind()` for connecting reactive values
12
- - **linkedSignal** - Angular's killer feature: writable computed that resets when source changes
13
- - **createSelector** - Solid's O(n)→O(2) optimization for list selection
14
- - **effectScope** - Vue's lifecycle management with pause/resume support
15
- - **Three-State Dirty Tracking** - Efficient CLEAN/MAYBE_DIRTY/DIRTY propagation
16
- - **Automatic Cleanup** - Effects clean up when disposed, no memory leaks
17
- - **Batching** - Group updates to prevent redundant effect runs
18
- - **Self-Referencing Effects** - Effects can write to their own dependencies
19
- - **Infinite Loop Protection** - Throws after 1000 iterations to catch bugs
20
- - **Reactive Collections** - ReactiveMap, ReactiveSet, ReactiveDate
21
- - **TypeScript Native** - Full type safety with generics
11
+ - **Framework-Agnostic** - Use the best patterns from Svelte, Angular, Solid, and Vue in any environment
12
+ - **Zero Dependencies** - Pure TypeScript, no runtime dependencies
13
+ - **Automatic Memory Cleanup** - FinalizationRegistry ensures signals are garbage collected properly
14
+ - **Battle-Tested Patterns** - Based on proven reactivity systems from major frameworks
22
15
 
23
16
  ## Installation
24
17
 
@@ -52,39 +45,88 @@ count.value = 5
52
45
  flushSync() // Logs: "Count: 5, Doubled: 10"
53
46
  ```
54
47
 
55
- ## API Reference
48
+ ---
56
49
 
57
- ### Signals
50
+ ## Core Primitives
58
51
 
59
- #### `signal<T>(initialValue: T, options?): WritableSignal<T>`
52
+ ### signal
60
53
 
61
54
  Create a reactive value with `.value` getter/setter.
62
55
 
56
+ ```typescript
57
+ signal<T>(initialValue: T, options?: { equals?: (a: T, b: T) => boolean }): WritableSignal<T>
58
+ ```
59
+
63
60
  ```typescript
64
61
  const name = signal('John')
65
62
  console.log(name.value) // 'John'
66
63
  name.value = 'Jane' // Triggers effects
67
64
  ```
68
65
 
69
- **Options:**
70
- - `equals?: (a: T, b: T) => boolean` - Custom equality function
66
+ ### signals
71
67
 
72
- #### `state<T extends object>(initialValue: T): T`
68
+ Create multiple signals at once from an object.
69
+
70
+ ```typescript
71
+ signals<T>(initial: T): { [K in keyof T]: WritableSignal<T[K]> }
72
+ ```
73
+
74
+ ```typescript
75
+ // Instead of:
76
+ const content = signal('hello')
77
+ const width = signal(40)
78
+ const visible = signal(true)
79
+
80
+ // Do this:
81
+ const ui = signals({ content: 'hello', width: 40, visible: true })
82
+ ui.content.value = 'updated'
83
+ ui.width.value = 60
84
+ ```
85
+
86
+ ### state
73
87
 
74
88
  Create a deeply reactive object. No `.value` needed - access properties directly.
75
89
 
76
90
  ```typescript
77
- const user = state({ name: 'John', address: { city: 'NYC' } })
91
+ state<T extends object>(initialValue: T): T
92
+ ```
93
+
94
+ ```typescript
95
+ const user = state({
96
+ name: 'John',
97
+ address: { city: 'NYC' }
98
+ })
78
99
  user.name = 'Jane' // Reactive
79
100
  user.address.city = 'LA' // Also reactive, deeply
80
101
  ```
81
102
 
82
- ### Derived Values
103
+ ### stateRaw
83
104
 
84
- #### `derived<T>(fn: () => T): DerivedSignal<T>`
105
+ Create a signal that holds an object reference without deep reactivity. Only triggers when the reference changes, not on mutations.
106
+
107
+ ```typescript
108
+ stateRaw<T>(initialValue: T): WritableSignal<T>
109
+ ```
110
+
111
+ ```typescript
112
+ const canvas = stateRaw(document.createElement('canvas'))
113
+ // Only triggers when canvas.value is reassigned
114
+ // Mutations to the canvas element don't trigger effects
115
+ ```
116
+
117
+ Use `stateRaw` for:
118
+ - DOM elements
119
+ - Class instances
120
+ - Large objects where you only care about replacement
121
+
122
+ ### derived
85
123
 
86
124
  Create a computed value that automatically updates when dependencies change.
87
125
 
126
+ ```typescript
127
+ derived<T>(fn: () => T, options?: { equals?: Equals<T> }): DerivedSignal<T>
128
+ ```
129
+
88
130
  ```typescript
89
131
  const firstName = signal('John')
90
132
  const lastName = signal('Doe')
@@ -98,132 +140,143 @@ Deriveds are:
98
140
  - **Cached** - Value is memoized until dependencies change
99
141
  - **Pure** - Cannot write to signals inside (throws error)
100
142
 
101
- ### Bindings
102
-
103
- #### `bind<T>(source: WritableSignal<T>): Binding<T>`
143
+ ---
104
144
 
105
- Create a reactive binding that forwards reads and writes to a source signal. Useful for two-way data binding and connecting reactive values across components.
145
+ ## Effects
106
146
 
107
- ```typescript
108
- const source = signal(0)
109
- const binding = bind(source)
147
+ ### effect
110
148
 
111
- // Reading through binding reads from source
112
- console.log(binding.value) // 0
149
+ Create a side effect that re-runs when dependencies change.
113
150
 
114
- // Writing through binding writes to source
115
- binding.value = 42
116
- console.log(source.value) // 42
151
+ ```typescript
152
+ effect(fn: () => void | CleanupFn): DisposeFn
117
153
  ```
118
154
 
119
- **Use cases:**
120
- - Two-way binding for form inputs
121
- - Connecting parent state to child components
122
- - Creating reactive links between signals
123
-
124
155
  ```typescript
125
- // Two-way binding example
126
- const username = signal('')
127
- const inputBinding = bind(username)
156
+ const count = signal(0)
128
157
 
129
- // When user types (e.g., in a UI framework):
130
- inputBinding.value = 'alice' // Updates username signal!
158
+ const dispose = effect(() => {
159
+ console.log('Count is:', count.value)
131
160
 
132
- // When username changes programmatically:
133
- username.value = 'bob' // inputBinding.value is now 'bob'
161
+ // Optional cleanup function
162
+ return () => {
163
+ console.log('Cleaning up...')
164
+ }
165
+ })
166
+
167
+ // Stop the effect
168
+ dispose()
134
169
  ```
135
170
 
136
- #### `bindReadonly<T>(source: ReadableSignal<T>): ReadonlyBinding<T>`
171
+ ### effect.pre
137
172
 
138
- Create a read-only binding. Attempting to write throws an error.
173
+ Create an effect that runs synchronously (like `$effect.pre` in Svelte).
139
174
 
140
175
  ```typescript
141
- const source = signal(0)
142
- const readonly = bindReadonly(source)
143
-
144
- console.log(readonly.value) // 0
145
- // readonly.value = 42 // Would throw at compile time
176
+ effect.pre(() => {
177
+ // Runs immediately, not on microtask
178
+ })
146
179
  ```
147
180
 
148
- #### `isBinding(value): boolean`
181
+ ### effect.root
149
182
 
150
- Check if a value is a binding.
183
+ Create an effect scope that can contain nested effects.
151
184
 
152
185
  ```typescript
153
- const binding = bind(signal(0))
154
- console.log(isBinding(binding)) // true
155
- console.log(isBinding(signal(0))) // false
186
+ const dispose = effect.root(() => {
187
+ effect(() => { /* ... */ })
188
+ effect(() => { /* ... */ })
189
+ })
190
+
191
+ // Disposes all nested effects
192
+ dispose()
156
193
  ```
157
194
 
158
- #### `unwrap<T>(value: T | Binding<T>): T`
195
+ ### effect.tracking
159
196
 
160
- Get the value from a binding, or return the value directly if not a binding.
197
+ Check if currently inside a reactive tracking context.
161
198
 
162
199
  ```typescript
163
- const arr: (string | Binding<string>)[] = [
164
- 'static',
165
- bind(signal('dynamic'))
166
- ]
167
-
168
- arr.map(unwrap) // ['static', 'dynamic']
200
+ if (effect.tracking()) {
201
+ console.log('Inside an effect or derived')
202
+ }
169
203
  ```
170
204
 
171
- ### Effects
205
+ ---
172
206
 
173
- #### `effect(fn: () => void | CleanupFn): DisposeFn`
207
+ ## Bindings
174
208
 
175
- Create a side effect that re-runs when dependencies change.
209
+ Two-way reactive pointers that forward reads and writes to a source signal.
210
+
211
+ ### bind
212
+
213
+ Create a reactive binding.
176
214
 
177
215
  ```typescript
178
- const count = signal(0)
216
+ bind<T>(source: WritableSignal<T> | Binding<T> | T | (() => T)): Binding<T>
217
+ ```
179
218
 
180
- const dispose = effect(() => {
181
- console.log('Count is:', count.value)
219
+ ```typescript
220
+ const source = signal(0)
221
+ const binding = bind(source)
182
222
 
183
- // Optional cleanup function
184
- return () => {
185
- console.log('Cleaning up...')
186
- }
187
- })
223
+ // Reading through binding reads from source
224
+ console.log(binding.value) // 0
188
225
 
189
- // Stop the effect
190
- dispose()
226
+ // Writing through binding writes to source
227
+ binding.value = 42
228
+ console.log(source.value) // 42
191
229
  ```
192
230
 
193
- #### `effect.root(fn: () => T): DisposeFn`
231
+ **Overloads:**
232
+ - `bind(signal)` - Creates writable binding to signal
233
+ - `bind(binding)` - Chains bindings (both point to same source)
234
+ - `bind(value)` - Wraps raw value in a signal
235
+ - `bind(() => expr)` - Creates read-only binding from getter
194
236
 
195
- Create an effect scope that can contain nested effects.
237
+ **Use cases:**
238
+ - Two-way binding for form inputs
239
+ - Connecting parent state to child components
240
+ - Creating reactive links between signals
241
+
242
+ ### bindReadonly
243
+
244
+ Create a read-only binding.
196
245
 
197
246
  ```typescript
198
- const dispose = effect.root(() => {
199
- effect(() => { /* ... */ })
200
- effect(() => { /* ... */ })
201
- })
247
+ const source = signal(0)
248
+ const readonly = bindReadonly(source)
202
249
 
203
- // Disposes all nested effects
204
- dispose()
250
+ console.log(readonly.value) // 0
251
+ // readonly.value = 42 // TypeScript error + runtime error
205
252
  ```
206
253
 
207
- #### `effect.pre(fn: () => void): DisposeFn`
208
-
209
- Create an effect that runs synchronously (like `$effect.pre` in Svelte).
254
+ ### isBinding / unwrap
210
255
 
211
256
  ```typescript
212
- effect.pre(() => {
213
- // Runs immediately, no flushSync needed
214
- })
257
+ // Check if a value is a binding
258
+ isBinding(maybeBinding) // true or false
259
+
260
+ // Get value from binding or return value directly
261
+ const arr: (string | Binding<string>)[] = ['static', bind(signal('dynamic'))]
262
+ arr.map(unwrap) // ['static', 'dynamic']
215
263
  ```
216
264
 
217
- ### Linked Signals (Angular's killer feature)
265
+ ---
218
266
 
219
- #### `linkedSignal<D>(fn: () => D): WritableSignal<D>`
220
- #### `linkedSignal<S, D>(options: LinkedSignalOptions<S, D>): WritableSignal<D>`
267
+ ## Advanced Features
221
268
 
222
- Create a writable signal that derives from a source but can be manually overridden.
223
- When the source changes, the linked signal resets to the computed value.
269
+ ### linkedSignal (Angular's killer feature)
270
+
271
+ Create a writable signal that derives from a source but can be manually overridden. When the source changes, the linked signal resets to the computed value.
224
272
 
225
273
  ```typescript
226
- // Simple form - derives directly from source
274
+ linkedSignal<D>(fn: () => D): WritableSignal<D>
275
+ linkedSignal<S, D>(options: LinkedSignalOptions<S, D>): WritableSignal<D>
276
+ ```
277
+
278
+ ```typescript
279
+ // Simple form - dropdown selection
227
280
  const options = signal(['a', 'b', 'c'])
228
281
  const selected = linkedSignal(() => options.value[0])
229
282
 
@@ -237,7 +290,7 @@ console.log(selected.value) // 'x' (reset to computed value)
237
290
  ```
238
291
 
239
292
  ```typescript
240
- // Advanced form - with previous value tracking
293
+ // Advanced form - keep valid selection
241
294
  const items = signal([1, 2, 3])
242
295
  const selectedItem = linkedSignal({
243
296
  source: () => items.value,
@@ -256,12 +309,16 @@ const selectedItem = linkedSignal({
256
309
  - Selection state that persists within valid options
257
310
  - Derived values that can be temporarily overridden
258
311
 
259
- ### Selectors (Solid's O(n)O(2) optimization)
312
+ ### createSelector (Solid's O(n) to O(2) optimization)
260
313
 
261
- #### `createSelector<T, U = T>(source: () => T, fn?: (key: U, value: T) => boolean): SelectorFn<T, U>`
314
+ Create a selector function for efficient list selection tracking. Instead of O(n) effects re-running, only affected items run = O(2).
262
315
 
263
- Create a selector function for efficient list selection tracking.
264
- Instead of O(n) effects re-running, only affected items run = O(2).
316
+ ```typescript
317
+ createSelector<T, U = T>(
318
+ source: () => T,
319
+ fn?: (key: U, value: T) => boolean
320
+ ): SelectorFn<T, U>
321
+ ```
265
322
 
266
323
  ```typescript
267
324
  const selectedId = signal(1)
@@ -284,17 +341,27 @@ items.forEach(item => {
284
341
  })
285
342
  ```
286
343
 
287
- ### Effect Scope (Vue's lifecycle management)
288
-
289
- #### `effectScope(detached?: boolean): EffectScope`
344
+ ### effectScope (Vue's lifecycle management)
290
345
 
291
346
  Create an effect scope to group effects for batch disposal with pause/resume support.
292
347
 
348
+ ```typescript
349
+ effectScope(detached?: boolean): EffectScope
350
+
351
+ interface EffectScope {
352
+ readonly active: boolean
353
+ readonly paused: boolean
354
+ run<R>(fn: () => R): R | undefined
355
+ stop(): void
356
+ pause(): void
357
+ resume(): void
358
+ }
359
+ ```
360
+
293
361
  ```typescript
294
362
  const scope = effectScope()
295
363
 
296
364
  scope.run(() => {
297
- // Effects created here are tracked by the scope
298
365
  effect(() => console.log(count.value))
299
366
  effect(() => console.log(name.value))
300
367
 
@@ -303,198 +370,211 @@ scope.run(() => {
303
370
  })
304
371
  })
305
372
 
373
+ // Pause execution temporarily
374
+ scope.pause()
375
+ count.value = 5 // Effect doesn't run
376
+
377
+ // Resume and run pending updates
378
+ scope.resume()
379
+
306
380
  // Later, dispose all effects at once
307
381
  scope.stop()
308
382
  ```
309
383
 
310
- #### Pause and Resume
384
+ ### onScopeDispose
311
385
 
312
- ```typescript
313
- scope.pause() // Effects won't run while paused
314
-
315
- count.value = 5 // Effect doesn't run
386
+ Register a cleanup function on the current scope.
316
387
 
317
- scope.resume() // Pending updates run
388
+ ```typescript
389
+ scope.run(() => {
390
+ const timer = setInterval(() => log('tick'), 1000)
391
+ onScopeDispose(() => clearInterval(timer))
392
+ })
318
393
  ```
319
394
 
320
- #### `getCurrentScope(): EffectScope | null`
395
+ ### getCurrentScope
321
396
 
322
397
  Get the currently active effect scope.
323
398
 
324
- #### `onScopeDispose(fn: () => void): void`
399
+ ```typescript
400
+ const scope = getCurrentScope()
401
+ if (scope) {
402
+ // Inside a scope
403
+ }
404
+ ```
325
405
 
326
- Register a cleanup function on the current scope.
406
+ ---
327
407
 
328
- ### Batching & Scheduling
408
+ ## Reactive Collections
329
409
 
330
- #### `batch(fn: () => T): T`
410
+ ### ReactiveMap
331
411
 
332
- Batch multiple signal updates into a single effect run.
412
+ A Map with per-key reactivity.
333
413
 
334
414
  ```typescript
335
- const a = signal(1)
336
- const b = signal(2)
337
-
338
- effect(() => console.log(a.value + b.value))
415
+ const users = new ReactiveMap<string, User>()
339
416
 
340
- batch(() => {
341
- a.value = 10
342
- b.value = 20
417
+ effect(() => {
418
+ console.log(users.get('john')) // Only re-runs when 'john' changes
343
419
  })
344
- // Effect runs once with final values, not twice
420
+
421
+ users.set('jane', { name: 'Jane' }) // Doesn't trigger above effect
422
+ users.set('john', { name: 'John!' }) // Triggers above effect
345
423
  ```
346
424
 
347
- #### `flushSync<T>(fn?: () => T): T | undefined`
425
+ ### ReactiveSet
348
426
 
349
- Synchronously flush all pending effects.
427
+ A Set with per-item reactivity.
350
428
 
351
429
  ```typescript
352
- count.value = 5
353
- flushSync() // Effects run NOW, not on next microtask
354
- ```
355
-
356
- #### `tick(): Promise<void>`
430
+ const tags = new ReactiveSet<string>()
357
431
 
358
- Wait for the next update cycle.
432
+ effect(() => {
433
+ console.log(tags.has('important')) // Only re-runs when 'important' changes
434
+ })
359
435
 
360
- ```typescript
361
- count.value = 5
362
- await tick() // Effects have run
436
+ tags.add('todo') // Doesn't trigger above effect
437
+ tags.add('important') // Triggers above effect
363
438
  ```
364
439
 
365
- ### Utilities
366
-
367
- #### `untrack<T>(fn: () => T): T`
440
+ ### ReactiveDate
368
441
 
369
- Read signals without creating dependencies.
442
+ A Date with reactive getters/setters.
370
443
 
371
444
  ```typescript
445
+ const date = new ReactiveDate()
446
+
372
447
  effect(() => {
373
- const a = count.value // Creates dependency
374
- const b = untrack(() => other.value) // No dependency
448
+ console.log(date.getHours()) // Re-runs when time changes
375
449
  })
376
- ```
377
450
 
378
- #### `peek<T>(signal: Source<T>): T`
451
+ date.setHours(12) // Triggers effect
452
+ ```
379
453
 
380
- Read a signal's value without tracking (low-level).
454
+ ---
381
455
 
382
- ### Deep Reactivity
456
+ ## Utilities
383
457
 
384
- #### `proxy<T extends object>(value: T): T`
458
+ ### batch
385
459
 
386
- Create a deeply reactive proxy (used internally by `state()`).
460
+ Batch multiple signal updates into a single effect run.
387
461
 
388
462
  ```typescript
389
- const obj = proxy({ a: { b: { c: 1 } } })
390
- obj.a.b.c = 2 // Only triggers effects reading a.b.c
391
- ```
392
-
393
- #### `toRaw<T>(value: T): T`
463
+ const a = signal(1)
464
+ const b = signal(2)
394
465
 
395
- Get the original object from a proxy.
466
+ effect(() => console.log(a.value + b.value))
396
467
 
397
- ```typescript
398
- const raw = toRaw(user) // Original non-reactive object
468
+ batch(() => {
469
+ a.value = 10
470
+ b.value = 20
471
+ })
472
+ // Effect runs once with final values, not twice
399
473
  ```
400
474
 
401
- #### `isReactive(value: unknown): boolean`
402
-
403
- Check if a value is a reactive proxy.
404
-
405
- ### Reactive Collections
475
+ ### untrack / peek
406
476
 
407
- #### `ReactiveMap<K, V>`
408
-
409
- A Map with per-key reactivity.
477
+ Read signals without creating dependencies.
410
478
 
411
479
  ```typescript
412
- const users = new ReactiveMap<string, User>()
413
-
414
480
  effect(() => {
415
- console.log(users.get('john')) // Only re-runs when 'john' changes
481
+ const a = count.value // Creates dependency
482
+ const b = untrack(() => other.value) // No dependency
416
483
  })
417
484
 
418
- users.set('jane', { name: 'Jane' }) // Doesn't trigger above effect
485
+ // peek is an alias for untrack
486
+ const value = peek(() => signal.value)
419
487
  ```
420
488
 
421
- #### `ReactiveSet<T>`
489
+ ### flushSync
422
490
 
423
- A Set with per-item reactivity.
491
+ Synchronously flush all pending effects.
424
492
 
425
493
  ```typescript
426
- const tags = new ReactiveSet<string>()
427
-
428
- effect(() => {
429
- console.log(tags.has('important')) // Only re-runs when 'important' changes
430
- })
494
+ count.value = 5
495
+ flushSync() // Effects run NOW, not on next microtask
431
496
  ```
432
497
 
433
- #### `ReactiveDate`
498
+ ### tick
434
499
 
435
- A Date with reactive getters/setters.
500
+ Wait for the next update cycle (async).
436
501
 
437
502
  ```typescript
438
- const date = new ReactiveDate()
439
-
440
- effect(() => {
441
- console.log(date.getHours()) // Re-runs when time changes
442
- })
443
-
444
- date.setHours(12) // Triggers effect
503
+ count.value = 5
504
+ await tick() // Effects have run
445
505
  ```
446
506
 
447
- ## Advanced Usage
507
+ ---
508
+
509
+ ## Deep Reactivity
448
510
 
449
- ### Self-Referencing Effects
511
+ ### proxy
450
512
 
451
- Effects can write to signals they depend on:
513
+ Create a deeply reactive proxy (used internally by `state()`).
452
514
 
453
515
  ```typescript
454
- const count = signal(0)
516
+ const obj = proxy({ a: { b: { c: 1 } } })
455
517
 
456
- effect(() => {
457
- if (count.value < 10) {
458
- count.value++ // Will re-run until count reaches 10
459
- }
460
- })
518
+ effect(() => console.log('c changed:', obj.a.b.c))
519
+ effect(() => console.log('a changed:', obj.a))
520
+
521
+ obj.a.b.c = 2 // Only triggers first effect (fine-grained!)
461
522
  ```
462
523
 
463
- **Note:** Unguarded self-references throw after 1000 iterations.
524
+ ### toRaw
464
525
 
465
- ### Custom Equality
526
+ Get the original object from a proxy.
466
527
 
467
528
  ```typescript
468
- import { signal, shallowEquals } from '@rlabs-inc/signals'
529
+ const raw = toRaw(user) // Original non-reactive object
530
+ ```
469
531
 
470
- const obj = signal({ a: 1 }, { equals: shallowEquals })
471
- obj.value = { a: 1 } // Won't trigger - shallowly equal
532
+ ### isReactive
533
+
534
+ Check if a value is a reactive proxy.
535
+
536
+ ```typescript
537
+ if (isReactive(value)) {
538
+ console.log('This is a proxy')
539
+ }
472
540
  ```
473
541
 
474
- Built-in equality functions:
475
- - `equals` - Default, uses `Object.is`
476
- - `safeEquals` - Handles NaN correctly
477
- - `shallowEquals` - Shallow object comparison
478
- - `neverEquals` - Always triggers (always false)
479
- - `alwaysEquals` - Never triggers (always true)
542
+ ---
480
543
 
481
- ### Low-Level API
544
+ ## Equality Functions
482
545
 
483
- For advanced use cases, you can access internal primitives:
546
+ Control when signals trigger updates:
484
547
 
485
548
  ```typescript
486
- import { source, get, set } from '@rlabs-inc/signals'
549
+ import { signal, equals, safeEquals, shallowEquals, neverEquals, alwaysEquals, createEquals } from '@rlabs-inc/signals'
487
550
 
488
- // Create a raw source (no .value wrapper)
489
- const src = source(0)
551
+ // Default - uses Object.is
552
+ const a = signal(0) // Uses equals by default
490
553
 
491
- // Read with tracking
492
- const value = get(src)
554
+ // Safe equality - handles NaN correctly
555
+ const b = signal(NaN, { equals: safeEquals })
493
556
 
494
- // Write with notification
495
- set(src, 10)
557
+ // Shallow comparison - compares one level deep
558
+ const c = signal({ a: 1 }, { equals: shallowEquals })
559
+ c.value = { a: 1 } // Won't trigger - shallowly equal
560
+
561
+ // Always trigger updates
562
+ const d = signal(0, { equals: neverEquals })
563
+ d.value = 0 // Still triggers!
564
+
565
+ // Never trigger updates
566
+ const e = signal(0, { equals: alwaysEquals })
567
+ e.value = 100 // Doesn't trigger
568
+
569
+ // Custom equality
570
+ const customEquals = createEquals((a, b) =>
571
+ JSON.stringify(a) === JSON.stringify(b)
572
+ )
573
+ const f = signal([], { equals: customEquals })
496
574
  ```
497
575
 
576
+ ---
577
+
498
578
  ## Error Handling
499
579
 
500
580
  ### "Cannot write to signals inside a derived"
@@ -534,29 +614,73 @@ effect(() => {
534
614
  })
535
615
  ```
536
616
 
617
+ ---
618
+
537
619
  ## Performance
538
620
 
539
621
  This library is designed for performance:
540
622
 
623
+ | Operation | Complexity | Notes |
624
+ |-----------|------------|-------|
625
+ | Signal read/write | O(1) | Direct property access |
626
+ | Derived read | O(1) | Cached after first computation |
627
+ | Effect trigger | O(deps) | Only runs if dependencies change |
628
+ | `batch()` | O(1) cycle | Multiple updates, single flush |
629
+ | `createSelector()` | O(2) | Only changed items' effects run |
630
+ | Proxy property access | O(1) | Per-property signal lookup |
631
+ | `ReactiveMap.get()` | O(1) | Per-key tracking |
632
+
633
+ **Key optimizations:**
541
634
  - **Lazy evaluation** - Deriveds only compute when read
542
635
  - **Version-based deduplication** - No duplicate dependency tracking
543
636
  - **Linked list effect tree** - O(1) effect insertion/removal
544
637
  - **Microtask batching** - Updates coalesce automatically
545
638
  - **Per-property signals** - Fine-grained updates at any depth
639
+ - **FinalizationRegistry cleanup** - Automatic memory management
640
+
641
+ ---
642
+
643
+ ## Framework Comparison
644
+
645
+ | Feature | @rlabs-inc/signals | Svelte 5 | Vue 3 | Angular | Solid.js |
646
+ |---------|-------------------|----------|-------|---------|----------|
647
+ | `signal()` | `signal()` | `$state` | `ref()` | `signal()` | `createSignal()` |
648
+ | `derived()` | `derived()` | `$derived` | `computed()` | `computed()` | `createMemo()` |
649
+ | `effect()` | `effect()` | `$effect` | `watchEffect()` | `effect()` | `createEffect()` |
650
+ | Deep reactivity | `state()` | `$state` | `reactive()` | - | - |
651
+ | `linkedSignal()` | Yes | - | - | Yes | - |
652
+ | `createSelector()` | Yes | - | - | - | Yes |
653
+ | `effectScope()` | Yes | - | Yes | - | - |
654
+ | Compiler required | No | Yes | No | No | No |
655
+ | DOM integration | No | Yes | Yes | Yes | Yes |
656
+
657
+ ---
658
+
659
+ ## Low-Level API
660
+
661
+ For advanced use cases (framework authors, custom reactivity):
662
+
663
+ ```typescript
664
+ import {
665
+ source, mutableSource, // Raw signal creation
666
+ get, set, // Track/update values
667
+ isDirty, // Check if needs update
668
+ markReactions, // Notify dependents
669
+ createEffect, // Raw effect creation
670
+ createDerived, // Raw derived creation
671
+ } from '@rlabs-inc/signals'
672
+
673
+ // Create a raw source (no .value wrapper)
674
+ const src = source(0)
675
+
676
+ // Read with tracking
677
+ const value = get(src)
678
+
679
+ // Write with notification
680
+ set(src, 10)
681
+ ```
546
682
 
547
- ## Comparison with Svelte 5
548
-
549
- | Feature | Svelte 5 | @rlabs-inc/signals |
550
- |---------|----------|-------------------|
551
- | Compiler required | Yes | No |
552
- | DOM integration | Yes | No |
553
- | Fine-grained reactivity | Yes | Yes |
554
- | Deep proxy reactivity | Yes | Yes |
555
- | Reactive bindings | `bind:` directive | `bind()` function |
556
- | Batching | Yes | Yes |
557
- | Effect cleanup | Yes | Yes |
558
- | TypeScript | Yes | Yes |
559
- | Runs in Node/Bun | Needs adapter | Native |
683
+ ---
560
684
 
561
685
  ## License
562
686
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rlabs-inc/signals",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "Production-grade fine-grained reactivity for TypeScript. A complete standalone mirror of Svelte 5's reactivity system - signals, effects, derived values, deep reactivity, reactive collections, and reactive bindings.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",