@nerdalytics/beacon 1000.2.2 → 1000.2.3

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 CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  [![license:mit](https://flat.badgen.net/static/license/MIT/blue)](https://github.com/nerdalytics/beacon/blob/trunk/LICENSE)
6
6
  [![registry:npm:version](https://img.shields.io/npm/v/@nerdalytics/beacon.svg)](https://www.npmjs.com/package/@nerdalytics/beacon)
7
+ [![Socket Badge](https://badge.socket.dev/npm/package/@nerdalytics/beacon/latest)](https://badge.socket.dev/npm/package/@nerdalytics/beacon/latest)
7
8
 
8
9
  [![tech:nodejs](https://img.shields.io/badge/Node%20js-339933?style=for-the-badge&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
9
10
  [![language:typescript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)](https://typescriptlang.org/)
package/package.json CHANGED
@@ -2,8 +2,9 @@
2
2
  "author": "Denny Trebbin (nerdalytics)",
3
3
  "description": "A lightweight reactive state library for Node.js backends. Enables reactive state management with automatic dependency tracking and efficient updates for server-side applications.",
4
4
  "devDependencies": {
5
- "@biomejs/biome": "2.2.6",
5
+ "@biomejs/biome": "2.2.7",
6
6
  "@types/node": "24.9.1",
7
+ "npm-check-updates": "19.1.1",
7
8
  "typescript": "5.9.3",
8
9
  "uglify-js": "3.19.3"
9
10
  },
@@ -20,13 +21,13 @@
20
21
  "default": "./dist/src/index.min.js"
21
22
  },
22
23
  "types": "./dist/src/index.d.ts",
23
- "typescript": "./dist/src/index.ts"
24
+ "typescript": "./src/index.ts"
24
25
  }
25
26
  },
26
27
  "files": [
27
- "dist/src/index.js",
28
+ "dist/src/index.min.js",
28
29
  "dist/src/index.d.ts",
29
- "dist/src/index.ts",
30
+ "src/index.ts",
30
31
  "LICENSE"
31
32
  ],
32
33
  "keywords": [
@@ -49,7 +50,7 @@
49
50
  "license": "MIT",
50
51
  "main": "dist/src/index.min.js",
51
52
  "name": "@nerdalytics/beacon",
52
- "packageManager": "npm@11.6.0",
53
+ "packageManager": "npm@11.6.2",
53
54
  "repository": {
54
55
  "type": "git",
55
56
  "url": "git+https://github.com/nerdalytics/beacon.git"
@@ -87,9 +88,10 @@
87
88
  "test:unit:lens": "node --test tests/lens.test.ts",
88
89
  "test:unit:select": "node --test tests/select.test.ts",
89
90
  "test:unit:state": "node --test tests/state.test.ts",
91
+ "update-dependencies": "npx npm-check-updates --interactive --upgrade --removeRange",
90
92
  "update-performance-docs": "node --experimental-config-file=node.config.json scripts/update-performance-docs.ts"
91
93
  },
92
94
  "type": "module",
93
95
  "types": "dist/src/index.d.ts",
94
- "version": "1000.2.2"
96
+ "version": "1000.2.3"
95
97
  }
package/src/index.ts ADDED
@@ -0,0 +1,673 @@
1
+ // Core types for reactive primitives
2
+ type Subscriber = () => void
3
+ export type Unsubscribe = () => void
4
+ export type ReadOnlyState<T> = () => T
5
+ export interface WriteableState<T> {
6
+ set(value: T): void
7
+ update(fn: (value: T) => T): void
8
+ }
9
+
10
+ // Special symbol used for internal tracking
11
+ const STATE_ID: unique symbol = Symbol('STATE_ID')
12
+
13
+ export type State<T> = ReadOnlyState<T> &
14
+ WriteableState<T> & {
15
+ [STATE_ID]?: symbol
16
+ }
17
+
18
+ /**
19
+ * Creates a reactive state container with the provided initial value.
20
+ */
21
+ export const state = <T>(initialValue: T, equalityFn: (a: T, b: T) => boolean = Object.is): State<T> =>
22
+ StateImpl.createState(initialValue, equalityFn)
23
+
24
+ /**
25
+ * Registers a function to run whenever its reactive dependencies change.
26
+ */
27
+ export const effect = (fn: () => void): Unsubscribe => StateImpl.createEffect(fn)
28
+
29
+ /**
30
+ * Groups multiple state updates to trigger effects only once at the end.
31
+ */
32
+ export const batch = <T>(fn: () => T): T => StateImpl.executeBatch(fn)
33
+
34
+ /**
35
+ * Creates a read-only computed value that updates when its dependencies change.
36
+ */
37
+ export const derive = <T>(computeFn: () => T): ReadOnlyState<T> => StateImpl.createDerive(computeFn)
38
+
39
+ /**
40
+ * Creates an efficient subscription to a subset of a state value.
41
+ */
42
+ export const select = <T, R>(
43
+ source: ReadOnlyState<T>,
44
+ selectorFn: (state: T) => R,
45
+ equalityFn: (a: R, b: R) => boolean = Object.is
46
+ ): ReadOnlyState<R> => StateImpl.createSelect(source, selectorFn, equalityFn)
47
+
48
+ /**
49
+ * Creates a read-only view of a state, hiding mutation methods.
50
+ */
51
+ export const readonlyState =
52
+ <T>(state: State<T>): ReadOnlyState<T> =>
53
+ (): T =>
54
+ state()
55
+
56
+ /**
57
+ * Creates a state with access control, returning a tuple of reader and writer.
58
+ */
59
+ export const protectedState = <T>(
60
+ initialValue: T,
61
+ equalityFn: (a: T, b: T) => boolean = Object.is
62
+ ): [
63
+ ReadOnlyState<T>,
64
+ WriteableState<T>,
65
+ ] => {
66
+ const fullState = state(initialValue, equalityFn)
67
+ return [
68
+ (): T => readonlyState(fullState)(),
69
+ {
70
+ set: (value: T): void => fullState.set(value),
71
+ update: (fn: (value: T) => T): void => fullState.update(fn),
72
+ },
73
+ ]
74
+ }
75
+
76
+ /**
77
+ * Creates a lens for direct updates to nested properties of a state.
78
+ */
79
+ export const lens = <T, K>(source: State<T>, accessor: (state: T) => K): State<K> =>
80
+ StateImpl.createLens(source, accessor)
81
+
82
+ class StateImpl<T> {
83
+ // Static fields track global reactivity state - this centralized approach allows
84
+ // for coordinated updates while maintaining individual state isolation
85
+ private static currentSubscriber: Subscriber | null = null
86
+ private static pendingSubscribers = new Set<Subscriber>()
87
+ private static isNotifying = false
88
+ private static batchDepth = 0
89
+ private static deferredEffectCreations: Subscriber[] = []
90
+ private static activeSubscribers = new Set<Subscriber>()
91
+
92
+ // WeakMaps enable automatic garbage collection when subscribers are no
93
+ // longer referenced, preventing memory leaks in long-running applications
94
+ private static stateTracking = new WeakMap<Subscriber, Set<symbol>>()
95
+ private static subscriberDependencies = new WeakMap<Subscriber, Set<Set<Subscriber>>>()
96
+ private static parentSubscriber = new WeakMap<Subscriber, Subscriber>()
97
+ private static childSubscribers = new WeakMap<Subscriber, Set<Subscriber>>()
98
+
99
+ // Instance state - each state has unique subscribers and ID
100
+ private value: T
101
+ private subscribers = new Set<Subscriber>()
102
+ private stateId = Symbol()
103
+ private equalityFn: (a: T, b: T) => boolean
104
+
105
+ constructor(initialValue: T, equalityFn: (a: T, b: T) => boolean = Object.is) {
106
+ this.value = initialValue
107
+ this.equalityFn = equalityFn
108
+ }
109
+
110
+ /**
111
+ * Creates a reactive state container with the provided initial value.
112
+ * Implementation of the public 'state' function.
113
+ */
114
+ static createState = <T>(initialValue: T, equalityFn: (a: T, b: T) => boolean = Object.is): State<T> => {
115
+ const instance = new StateImpl<T>(initialValue, equalityFn)
116
+ const get = (): T => instance.get()
117
+ get.set = (value: T): void => instance.set(value)
118
+ get.update = (fn: (currentValue: T) => T): void => instance.update(fn)
119
+ get[STATE_ID] = instance.stateId
120
+ return get as State<T>
121
+ }
122
+
123
+ // Auto-tracks dependencies when called within effects, creating a fine-grained
124
+ // reactivity graph that only updates affected components
125
+ get = (): T => {
126
+ const currentEffect = StateImpl.currentSubscriber
127
+ if (currentEffect) {
128
+ // Add this effect to subscribers for future notification
129
+ this.subscribers.add(currentEffect)
130
+
131
+ // Maintain bidirectional dependency tracking to enable precise cleanup
132
+ // when effects are unsubscribed, preventing memory leaks
133
+ let dependencies = StateImpl.subscriberDependencies.get(currentEffect)
134
+ if (!dependencies) {
135
+ dependencies = new Set()
136
+ StateImpl.subscriberDependencies.set(currentEffect, dependencies)
137
+ }
138
+ dependencies.add(this.subscribers)
139
+
140
+ // Track read states to detect direct cyclical dependencies that
141
+ // could cause infinite loops
142
+ let readStates = StateImpl.stateTracking.get(currentEffect)
143
+ if (!readStates) {
144
+ readStates = new Set()
145
+ StateImpl.stateTracking.set(currentEffect, readStates)
146
+ }
147
+ readStates.add(this.stateId)
148
+ }
149
+ return this.value
150
+ }
151
+
152
+ // Handles value updates with built-in optimizations and safeguards
153
+ set = (newValue: T): void => {
154
+ // Skip updates for unchanged values to prevent redundant effect executions
155
+ if (this.equalityFn(this.value, newValue)) {
156
+ return
157
+ }
158
+
159
+ // Infinite loop detection prevents direct self-mutation within effects,
160
+ // while allowing nested effect patterns that would otherwise appear cyclical
161
+ const effect = StateImpl.currentSubscriber
162
+ if (effect) {
163
+ const states = StateImpl.stateTracking.get(effect)
164
+ if (states?.has(this.stateId) && !StateImpl.parentSubscriber.get(effect)) {
165
+ throw new Error('Infinite loop detected: effect() cannot update a state() it depends on!')
166
+ }
167
+ }
168
+
169
+ this.value = newValue
170
+
171
+ // Skip updates when there are no subscribers, avoiding unnecessary processing
172
+ if (this.subscribers.size === 0) {
173
+ return
174
+ }
175
+
176
+ // Queue notifications instead of executing immediately to support batch operations
177
+ // and prevent redundant effect runs
178
+ for (const sub of this.subscribers) {
179
+ StateImpl.pendingSubscribers.add(sub)
180
+ }
181
+
182
+ // Immediate execution outside of batches, deferred execution inside batches
183
+ if (StateImpl.batchDepth === 0 && !StateImpl.isNotifying) {
184
+ StateImpl.notifySubscribers()
185
+ }
186
+ }
187
+
188
+ update = (fn: (currentValue: T) => T): void => {
189
+ this.set(fn(this.value))
190
+ }
191
+
192
+ /**
193
+ * Registers a function to run whenever its reactive dependencies change.
194
+ * Implementation of the public 'effect' function.
195
+ */
196
+ static createEffect = (fn: () => void): Unsubscribe => {
197
+ const runEffect = (): void => {
198
+ // Prevent re-entrance to avoid cascade updates during effect execution
199
+ if (StateImpl.activeSubscribers.has(runEffect)) {
200
+ return
201
+ }
202
+
203
+ StateImpl.activeSubscribers.add(runEffect)
204
+ const parentEffect = StateImpl.currentSubscriber
205
+
206
+ try {
207
+ // Clean existing subscriptions before running to ensure only
208
+ // currently accessed states are tracked as dependencies
209
+ StateImpl.cleanupEffect(runEffect)
210
+
211
+ // Set current context for automatic dependency tracking
212
+ StateImpl.currentSubscriber = runEffect
213
+ StateImpl.stateTracking.set(runEffect, new Set())
214
+
215
+ // Track parent-child relationships to handle nested effects correctly
216
+ // and enable hierarchical cleanup later
217
+ if (parentEffect) {
218
+ StateImpl.parentSubscriber.set(runEffect, parentEffect)
219
+ let children = StateImpl.childSubscribers.get(parentEffect)
220
+ if (!children) {
221
+ children = new Set()
222
+ StateImpl.childSubscribers.set(parentEffect, children)
223
+ }
224
+ children.add(runEffect)
225
+ }
226
+
227
+ // Execute the effect function, which will auto-track dependencies
228
+ fn()
229
+ } finally {
230
+ // Restore previous context when done
231
+ StateImpl.currentSubscriber = parentEffect
232
+ StateImpl.activeSubscribers.delete(runEffect)
233
+ }
234
+ }
235
+
236
+ // Run immediately unless we're in a batch operation
237
+ if (StateImpl.batchDepth === 0) {
238
+ runEffect()
239
+ } else {
240
+ // Still track parent-child relationship even when deferred,
241
+ // ensuring proper hierarchical cleanup later
242
+ if (StateImpl.currentSubscriber) {
243
+ const parent = StateImpl.currentSubscriber
244
+ StateImpl.parentSubscriber.set(runEffect, parent)
245
+ let children = StateImpl.childSubscribers.get(parent)
246
+ if (!children) {
247
+ children = new Set()
248
+ StateImpl.childSubscribers.set(parent, children)
249
+ }
250
+ children.add(runEffect)
251
+ }
252
+
253
+ // Queue for execution when batch completes
254
+ StateImpl.deferredEffectCreations.push(runEffect)
255
+ }
256
+
257
+ // Return cleanup function to properly disconnect from reactivity graph
258
+ return (): void => {
259
+ // Remove from dependency tracking to stop future notifications
260
+ StateImpl.cleanupEffect(runEffect)
261
+ StateImpl.pendingSubscribers.delete(runEffect)
262
+ StateImpl.activeSubscribers.delete(runEffect)
263
+ StateImpl.stateTracking.delete(runEffect)
264
+
265
+ // Clean up parent-child relationship bidirectionally
266
+ const parent = StateImpl.parentSubscriber.get(runEffect)
267
+ if (parent) {
268
+ const siblings = StateImpl.childSubscribers.get(parent)
269
+ if (siblings) {
270
+ siblings.delete(runEffect)
271
+ }
272
+ }
273
+ StateImpl.parentSubscriber.delete(runEffect)
274
+
275
+ // Recursively clean up child effects to prevent memory leaks in
276
+ // nested effect scenarios
277
+ const children = StateImpl.childSubscribers.get(runEffect)
278
+ if (children) {
279
+ for (const child of children) {
280
+ StateImpl.cleanupEffect(child)
281
+ }
282
+ children.clear()
283
+ StateImpl.childSubscribers.delete(runEffect)
284
+ }
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Groups multiple state updates to trigger effects only once at the end.
290
+ * Implementation of the public 'batch' function.
291
+ */
292
+ static executeBatch = <T>(fn: () => T): T => {
293
+ // Increment depth counter to handle nested batches correctly
294
+ StateImpl.batchDepth++
295
+ try {
296
+ return fn()
297
+ } catch (error: unknown) {
298
+ // Clean up on error to prevent stale subscribers from executing
299
+ // and potentially causing cascading errors
300
+ if (StateImpl.batchDepth === 1) {
301
+ StateImpl.pendingSubscribers.clear()
302
+ StateImpl.deferredEffectCreations.length = 0
303
+ }
304
+ throw error
305
+ } finally {
306
+ StateImpl.batchDepth--
307
+
308
+ // Only process effects when exiting the outermost batch,
309
+ // maintaining proper execution order while avoiding redundant runs
310
+ if (StateImpl.batchDepth === 0) {
311
+ // Process effects created during the batch
312
+ if (StateImpl.deferredEffectCreations.length > 0) {
313
+ const effectsToRun = [
314
+ ...StateImpl.deferredEffectCreations,
315
+ ]
316
+ StateImpl.deferredEffectCreations.length = 0
317
+ for (const effect of effectsToRun) {
318
+ effect()
319
+ }
320
+ }
321
+
322
+ // Process state updates that occurred during the batch
323
+ if (StateImpl.pendingSubscribers.size > 0 && !StateImpl.isNotifying) {
324
+ StateImpl.notifySubscribers()
325
+ }
326
+ }
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Creates a read-only computed value that updates when its dependencies change.
332
+ * Implementation of the public 'derive' function.
333
+ */
334
+ static createDerive = <T>(computeFn: () => T): ReadOnlyState<T> => {
335
+ // Create a container to hold state and minimize closure captures
336
+ const container = {
337
+ cachedValue: undefined as unknown as T,
338
+ computeFn,
339
+ initialized: false,
340
+ valueState: StateImpl.createState<T | undefined>(undefined),
341
+ }
342
+
343
+ // Internal effect automatically tracks dependencies and updates the derived value
344
+ StateImpl.createEffect(function deriveEffect(): void {
345
+ const newValue = container.computeFn()
346
+
347
+ // Only update if the value actually changed to preserve referential equality
348
+ // and prevent unnecessary downstream updates
349
+ if (!(container.initialized && Object.is(container.cachedValue, newValue))) {
350
+ container.cachedValue = newValue
351
+ container.valueState.set(newValue)
352
+ }
353
+
354
+ container.initialized = true
355
+ })
356
+
357
+ // Return function with lazy initialization - ensures value is available
358
+ // even when accessed before its dependencies have had a chance to update
359
+ return function deriveGetter(): T {
360
+ if (!container.initialized) {
361
+ container.cachedValue = container.computeFn()
362
+ container.initialized = true
363
+ container.valueState.set(container.cachedValue)
364
+ }
365
+ return container.valueState() as T
366
+ }
367
+ }
368
+
369
+ /**
370
+ * Creates an efficient subscription to a subset of a state value.
371
+ * Implementation of the public 'select' function.
372
+ */
373
+ static createSelect = <T, R>(
374
+ source: ReadOnlyState<T>,
375
+ selectorFn: (state: T) => R,
376
+ equalityFn: (a: R, b: R) => boolean = Object.is
377
+ ): ReadOnlyState<R> => {
378
+ // Create a container to hold state and minimize closure captures
379
+ const container = {
380
+ equalityFn,
381
+ initialized: false,
382
+ lastSelectedValue: undefined as R | undefined,
383
+ lastSourceValue: undefined as T | undefined,
384
+ selectorFn,
385
+ source,
386
+ valueState: StateImpl.createState<R | undefined>(undefined),
387
+ }
388
+
389
+ // Internal effect to track the source and update only when needed
390
+ StateImpl.createEffect(function selectEffect(): void {
391
+ const sourceValue = container.source()
392
+
393
+ // Skip computation if source reference hasn't changed
394
+ if (container.initialized && Object.is(container.lastSourceValue, sourceValue)) {
395
+ return
396
+ }
397
+
398
+ container.lastSourceValue = sourceValue
399
+ const newSelectedValue = container.selectorFn(sourceValue)
400
+
401
+ // Use custom equality function to determine if value semantically changed,
402
+ // allowing for deep equality comparisons with complex objects
403
+ if (
404
+ container.initialized &&
405
+ container.lastSelectedValue !== undefined &&
406
+ container.equalityFn(container.lastSelectedValue, newSelectedValue)
407
+ ) {
408
+ return
409
+ }
410
+
411
+ // Update cache and notify subscribers due the value has changed
412
+ container.lastSelectedValue = newSelectedValue
413
+ container.valueState.set(newSelectedValue)
414
+ container.initialized = true
415
+ })
416
+
417
+ // Return function with eager initialization capability
418
+ return function selectGetter(): R {
419
+ if (!container.initialized) {
420
+ container.lastSourceValue = container.source()
421
+ container.lastSelectedValue = container.selectorFn(container.lastSourceValue)
422
+ container.valueState.set(container.lastSelectedValue)
423
+ container.initialized = true
424
+ }
425
+ return container.valueState() as R
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Creates a lens for direct updates to nested properties of a state.
431
+ * Implementation of the public 'lens' function.
432
+ */
433
+ static createLens = <T, K>(source: State<T>, accessor: (state: T) => K): State<K> => {
434
+ // Create a container to hold lens state and minimize closure captures
435
+ const container = {
436
+ accessor,
437
+ isUpdating: false,
438
+ lensState: null as unknown as State<K>,
439
+ originalSet: null as unknown as (value: K) => void,
440
+ path: [] as (string | number)[],
441
+ source,
442
+ }
443
+
444
+ // Extract the property path once during lens creation
445
+ const extractPath = (): (string | number)[] => {
446
+ const pathCollector: (string | number)[] = []
447
+ const proxy = new Proxy(
448
+ {},
449
+ {
450
+ get: (_: object, prop: string | symbol): unknown => {
451
+ if (typeof prop === 'string' || typeof prop === 'number') {
452
+ pathCollector.push(prop)
453
+ }
454
+ return proxy
455
+ },
456
+ }
457
+ )
458
+
459
+ try {
460
+ container.accessor(proxy as unknown as T)
461
+ } catch {
462
+ // Ignore errors, we're just collecting the path
463
+ }
464
+
465
+ return pathCollector
466
+ }
467
+
468
+ // Capture the path once
469
+ container.path = extractPath()
470
+
471
+ // Create a state with the initial value from the source
472
+ container.lensState = StateImpl.createState<K>(container.accessor(container.source()))
473
+ container.originalSet = container.lensState.set
474
+
475
+ // Set up an effect to sync from source to lens
476
+ StateImpl.createEffect(function lensEffect(): void {
477
+ if (container.isUpdating) {
478
+ return
479
+ }
480
+
481
+ container.isUpdating = true
482
+ try {
483
+ container.lensState.set(container.accessor(container.source()))
484
+ } finally {
485
+ container.isUpdating = false
486
+ }
487
+ })
488
+
489
+ // Override the lens state's set method to update the source
490
+ container.lensState.set = function lensSet(value: K): void {
491
+ if (container.isUpdating) {
492
+ return
493
+ }
494
+
495
+ container.isUpdating = true
496
+ try {
497
+ // Update lens state
498
+ container.originalSet(value)
499
+
500
+ // Update source by modifying the value at path
501
+ container.source.update((current: T): T => setValueAtPath(current, container.path, value))
502
+ } finally {
503
+ container.isUpdating = false
504
+ }
505
+ }
506
+
507
+ // Add update method for completeness
508
+ container.lensState.update = function lensUpdate(fn: (value: K) => K): void {
509
+ container.lensState.set(fn(container.lensState()))
510
+ }
511
+
512
+ return container.lensState
513
+ }
514
+
515
+ // Processes queued subscriber notifications in a controlled, non-reentrant way
516
+ private static notifySubscribers = (): void => {
517
+ // Prevent reentrance to avoid cascading notification loops when
518
+ // effects trigger further state changes
519
+ if (StateImpl.isNotifying) {
520
+ return
521
+ }
522
+
523
+ StateImpl.isNotifying = true
524
+
525
+ try {
526
+ // Process all pending effects in batches for better perf,
527
+ // ensuring topological execution order is maintained
528
+ while (StateImpl.pendingSubscribers.size > 0) {
529
+ // Process in snapshot batches to prevent infinite loops
530
+ // when effects trigger further state changes
531
+ const subscribers = Array.from(StateImpl.pendingSubscribers)
532
+ StateImpl.pendingSubscribers.clear()
533
+
534
+ for (const effect of subscribers) {
535
+ effect()
536
+ }
537
+ }
538
+ } finally {
539
+ StateImpl.isNotifying = false
540
+ }
541
+ }
542
+
543
+ // Removes effect from dependency tracking to prevent memory leaks
544
+ private static cleanupEffect = (effect: Subscriber): void => {
545
+ // Remove from execution queue to prevent stale updates
546
+ StateImpl.pendingSubscribers.delete(effect)
547
+
548
+ // Remove bidirectional dependency references to prevent memory leaks
549
+ const deps = StateImpl.subscriberDependencies.get(effect)
550
+ if (deps) {
551
+ for (const subscribers of deps) {
552
+ subscribers.delete(effect)
553
+ }
554
+ deps.clear()
555
+ StateImpl.subscriberDependencies.delete(effect)
556
+ }
557
+ }
558
+ }
559
+ // Helper for array updates
560
+ const updateArrayItem = <V>(arr: unknown[], index: number, value: V): unknown[] => {
561
+ const copy = [
562
+ ...arr,
563
+ ]
564
+ copy[index] = value
565
+ return copy
566
+ }
567
+
568
+ // Helper for single-level updates (optimization)
569
+ const updateShallowProperty = <V>(
570
+ obj: Record<string | number, unknown>,
571
+ key: string | number,
572
+ value: V
573
+ ): Record<string | number, unknown> => {
574
+ const result = {
575
+ ...obj,
576
+ }
577
+ result[key] = value
578
+ return result
579
+ }
580
+
581
+ // Helper to create the appropriate container type
582
+ const createContainer = (key: string | number): Record<string | number, unknown> | unknown[] => {
583
+ const isArrayKey = typeof key === 'number' || !Number.isNaN(Number(key))
584
+ return isArrayKey ? [] : {}
585
+ }
586
+
587
+ // Helper for handling array path updates
588
+ const updateArrayPath = <V>(array: unknown[], pathSegments: (string | number)[], value: V): unknown[] => {
589
+ const index = Number(pathSegments[0])
590
+
591
+ if (pathSegments.length === 1) {
592
+ // Simple array item update
593
+ return updateArrayItem(array, index, value)
594
+ }
595
+
596
+ // Nested path in array
597
+ const copy = [
598
+ ...array,
599
+ ]
600
+ const nextPathSegments = pathSegments.slice(1)
601
+ const nextKey = nextPathSegments[0]
602
+
603
+ // For null/undefined values in arrays, create appropriate containers
604
+ let nextValue = array[index]
605
+ if (nextValue === undefined || nextValue === null) {
606
+ // Use empty object as default if nextKey is undefined
607
+ nextValue = nextKey !== undefined ? createContainer(nextKey) : {}
608
+ }
609
+
610
+ copy[index] = setValueAtPath(nextValue, nextPathSegments, value)
611
+ return copy
612
+ }
613
+
614
+ // Helper for handling object path updates
615
+ const updateObjectPath = <V>(
616
+ obj: Record<string | number, unknown>,
617
+ pathSegments: (string | number)[],
618
+ value: V
619
+ ): Record<string | number, unknown> => {
620
+ // Ensure we have a valid key
621
+ const currentKey = pathSegments[0]
622
+ if (currentKey === undefined) {
623
+ // This shouldn't happen given our checks in the main function
624
+ return obj
625
+ }
626
+
627
+ if (pathSegments.length === 1) {
628
+ // Simple object property update
629
+ return updateShallowProperty(obj, currentKey, value)
630
+ }
631
+
632
+ // Nested path in object
633
+ const nextPathSegments = pathSegments.slice(1)
634
+ const nextKey = nextPathSegments[0]
635
+
636
+ // For null/undefined values, create appropriate containers
637
+ let currentValue = obj[currentKey]
638
+ if (currentValue === undefined || currentValue === null) {
639
+ // Use empty object as default if nextKey is undefined
640
+ currentValue = nextKey !== undefined ? createContainer(nextKey) : {}
641
+ }
642
+
643
+ // Create new object with updated property
644
+ const result = {
645
+ ...obj,
646
+ }
647
+ result[currentKey] = setValueAtPath(currentValue, nextPathSegments, value)
648
+ return result
649
+ }
650
+
651
+ // Simplified function to update a nested value at a path
652
+ const setValueAtPath = <V, O>(obj: O, pathSegments: (string | number)[], value: V): O => {
653
+ // Handle base cases
654
+ if (pathSegments.length === 0) {
655
+ return value as unknown as O
656
+ }
657
+
658
+ if (obj === undefined || obj === null) {
659
+ return setValueAtPath({} as O, pathSegments, value)
660
+ }
661
+
662
+ const currentKey = pathSegments[0]
663
+ if (currentKey === undefined) {
664
+ return obj
665
+ }
666
+
667
+ // Delegate to specialized handlers based on data type
668
+ if (Array.isArray(obj)) {
669
+ return updateArrayPath(obj, pathSegments, value) as unknown as O
670
+ }
671
+
672
+ return updateObjectPath(obj as Record<string | number, unknown>, pathSegments, value) as unknown as O
673
+ }
package/dist/src/index.js DELETED
@@ -1,406 +0,0 @@
1
- const STATE_ID = Symbol('STATE_ID');
2
- export const state = (initialValue, equalityFn = Object.is) => StateImpl.createState(initialValue, equalityFn);
3
- export const effect = (fn) => StateImpl.createEffect(fn);
4
- export const batch = (fn) => StateImpl.executeBatch(fn);
5
- export const derive = (computeFn) => StateImpl.createDerive(computeFn);
6
- export const select = (source, selectorFn, equalityFn = Object.is) => StateImpl.createSelect(source, selectorFn, equalityFn);
7
- export const readonlyState = (state) => () => state();
8
- export const protectedState = (initialValue, equalityFn = Object.is) => {
9
- const fullState = state(initialValue, equalityFn);
10
- return [
11
- () => readonlyState(fullState)(),
12
- {
13
- set: (value) => fullState.set(value),
14
- update: (fn) => fullState.update(fn),
15
- },
16
- ];
17
- };
18
- export const lens = (source, accessor) => StateImpl.createLens(source, accessor);
19
- class StateImpl {
20
- static currentSubscriber = null;
21
- static pendingSubscribers = new Set();
22
- static isNotifying = false;
23
- static batchDepth = 0;
24
- static deferredEffectCreations = [];
25
- static activeSubscribers = new Set();
26
- static stateTracking = new WeakMap();
27
- static subscriberDependencies = new WeakMap();
28
- static parentSubscriber = new WeakMap();
29
- static childSubscribers = new WeakMap();
30
- value;
31
- subscribers = new Set();
32
- stateId = Symbol();
33
- equalityFn;
34
- constructor(initialValue, equalityFn = Object.is) {
35
- this.value = initialValue;
36
- this.equalityFn = equalityFn;
37
- }
38
- static createState = (initialValue, equalityFn = Object.is) => {
39
- const instance = new StateImpl(initialValue, equalityFn);
40
- const get = () => instance.get();
41
- get.set = (value) => instance.set(value);
42
- get.update = (fn) => instance.update(fn);
43
- get[STATE_ID] = instance.stateId;
44
- return get;
45
- };
46
- get = () => {
47
- const currentEffect = StateImpl.currentSubscriber;
48
- if (currentEffect) {
49
- this.subscribers.add(currentEffect);
50
- let dependencies = StateImpl.subscriberDependencies.get(currentEffect);
51
- if (!dependencies) {
52
- dependencies = new Set();
53
- StateImpl.subscriberDependencies.set(currentEffect, dependencies);
54
- }
55
- dependencies.add(this.subscribers);
56
- let readStates = StateImpl.stateTracking.get(currentEffect);
57
- if (!readStates) {
58
- readStates = new Set();
59
- StateImpl.stateTracking.set(currentEffect, readStates);
60
- }
61
- readStates.add(this.stateId);
62
- }
63
- return this.value;
64
- };
65
- set = (newValue) => {
66
- if (this.equalityFn(this.value, newValue)) {
67
- return;
68
- }
69
- const effect = StateImpl.currentSubscriber;
70
- if (effect) {
71
- const states = StateImpl.stateTracking.get(effect);
72
- if (states?.has(this.stateId) && !StateImpl.parentSubscriber.get(effect)) {
73
- throw new Error('Infinite loop detected: effect() cannot update a state() it depends on!');
74
- }
75
- }
76
- this.value = newValue;
77
- if (this.subscribers.size === 0) {
78
- return;
79
- }
80
- for (const sub of this.subscribers) {
81
- StateImpl.pendingSubscribers.add(sub);
82
- }
83
- if (StateImpl.batchDepth === 0 && !StateImpl.isNotifying) {
84
- StateImpl.notifySubscribers();
85
- }
86
- };
87
- update = (fn) => {
88
- this.set(fn(this.value));
89
- };
90
- static createEffect = (fn) => {
91
- const runEffect = () => {
92
- if (StateImpl.activeSubscribers.has(runEffect)) {
93
- return;
94
- }
95
- StateImpl.activeSubscribers.add(runEffect);
96
- const parentEffect = StateImpl.currentSubscriber;
97
- try {
98
- StateImpl.cleanupEffect(runEffect);
99
- StateImpl.currentSubscriber = runEffect;
100
- StateImpl.stateTracking.set(runEffect, new Set());
101
- if (parentEffect) {
102
- StateImpl.parentSubscriber.set(runEffect, parentEffect);
103
- let children = StateImpl.childSubscribers.get(parentEffect);
104
- if (!children) {
105
- children = new Set();
106
- StateImpl.childSubscribers.set(parentEffect, children);
107
- }
108
- children.add(runEffect);
109
- }
110
- fn();
111
- }
112
- finally {
113
- StateImpl.currentSubscriber = parentEffect;
114
- StateImpl.activeSubscribers.delete(runEffect);
115
- }
116
- };
117
- if (StateImpl.batchDepth === 0) {
118
- runEffect();
119
- }
120
- else {
121
- if (StateImpl.currentSubscriber) {
122
- const parent = StateImpl.currentSubscriber;
123
- StateImpl.parentSubscriber.set(runEffect, parent);
124
- let children = StateImpl.childSubscribers.get(parent);
125
- if (!children) {
126
- children = new Set();
127
- StateImpl.childSubscribers.set(parent, children);
128
- }
129
- children.add(runEffect);
130
- }
131
- StateImpl.deferredEffectCreations.push(runEffect);
132
- }
133
- return () => {
134
- StateImpl.cleanupEffect(runEffect);
135
- StateImpl.pendingSubscribers.delete(runEffect);
136
- StateImpl.activeSubscribers.delete(runEffect);
137
- StateImpl.stateTracking.delete(runEffect);
138
- const parent = StateImpl.parentSubscriber.get(runEffect);
139
- if (parent) {
140
- const siblings = StateImpl.childSubscribers.get(parent);
141
- if (siblings) {
142
- siblings.delete(runEffect);
143
- }
144
- }
145
- StateImpl.parentSubscriber.delete(runEffect);
146
- const children = StateImpl.childSubscribers.get(runEffect);
147
- if (children) {
148
- for (const child of children) {
149
- StateImpl.cleanupEffect(child);
150
- }
151
- children.clear();
152
- StateImpl.childSubscribers.delete(runEffect);
153
- }
154
- };
155
- };
156
- static executeBatch = (fn) => {
157
- StateImpl.batchDepth++;
158
- try {
159
- return fn();
160
- }
161
- catch (error) {
162
- if (StateImpl.batchDepth === 1) {
163
- StateImpl.pendingSubscribers.clear();
164
- StateImpl.deferredEffectCreations.length = 0;
165
- }
166
- throw error;
167
- }
168
- finally {
169
- StateImpl.batchDepth--;
170
- if (StateImpl.batchDepth === 0) {
171
- if (StateImpl.deferredEffectCreations.length > 0) {
172
- const effectsToRun = [
173
- ...StateImpl.deferredEffectCreations,
174
- ];
175
- StateImpl.deferredEffectCreations.length = 0;
176
- for (const effect of effectsToRun) {
177
- effect();
178
- }
179
- }
180
- if (StateImpl.pendingSubscribers.size > 0 && !StateImpl.isNotifying) {
181
- StateImpl.notifySubscribers();
182
- }
183
- }
184
- }
185
- };
186
- static createDerive = (computeFn) => {
187
- const container = {
188
- cachedValue: undefined,
189
- computeFn,
190
- initialized: false,
191
- valueState: StateImpl.createState(undefined),
192
- };
193
- StateImpl.createEffect(function deriveEffect() {
194
- const newValue = container.computeFn();
195
- if (!(container.initialized && Object.is(container.cachedValue, newValue))) {
196
- container.cachedValue = newValue;
197
- container.valueState.set(newValue);
198
- }
199
- container.initialized = true;
200
- });
201
- return function deriveGetter() {
202
- if (!container.initialized) {
203
- container.cachedValue = container.computeFn();
204
- container.initialized = true;
205
- container.valueState.set(container.cachedValue);
206
- }
207
- return container.valueState();
208
- };
209
- };
210
- static createSelect = (source, selectorFn, equalityFn = Object.is) => {
211
- const container = {
212
- equalityFn,
213
- initialized: false,
214
- lastSelectedValue: undefined,
215
- lastSourceValue: undefined,
216
- selectorFn,
217
- source,
218
- valueState: StateImpl.createState(undefined),
219
- };
220
- StateImpl.createEffect(function selectEffect() {
221
- const sourceValue = container.source();
222
- if (container.initialized && Object.is(container.lastSourceValue, sourceValue)) {
223
- return;
224
- }
225
- container.lastSourceValue = sourceValue;
226
- const newSelectedValue = container.selectorFn(sourceValue);
227
- if (container.initialized &&
228
- container.lastSelectedValue !== undefined &&
229
- container.equalityFn(container.lastSelectedValue, newSelectedValue)) {
230
- return;
231
- }
232
- container.lastSelectedValue = newSelectedValue;
233
- container.valueState.set(newSelectedValue);
234
- container.initialized = true;
235
- });
236
- return function selectGetter() {
237
- if (!container.initialized) {
238
- container.lastSourceValue = container.source();
239
- container.lastSelectedValue = container.selectorFn(container.lastSourceValue);
240
- container.valueState.set(container.lastSelectedValue);
241
- container.initialized = true;
242
- }
243
- return container.valueState();
244
- };
245
- };
246
- static createLens = (source, accessor) => {
247
- const container = {
248
- accessor,
249
- isUpdating: false,
250
- lensState: null,
251
- originalSet: null,
252
- path: [],
253
- source,
254
- };
255
- const extractPath = () => {
256
- const pathCollector = [];
257
- const proxy = new Proxy({}, {
258
- get: (_, prop) => {
259
- if (typeof prop === 'string' || typeof prop === 'number') {
260
- pathCollector.push(prop);
261
- }
262
- return proxy;
263
- },
264
- });
265
- try {
266
- container.accessor(proxy);
267
- }
268
- catch {
269
- }
270
- return pathCollector;
271
- };
272
- container.path = extractPath();
273
- container.lensState = StateImpl.createState(container.accessor(container.source()));
274
- container.originalSet = container.lensState.set;
275
- StateImpl.createEffect(function lensEffect() {
276
- if (container.isUpdating) {
277
- return;
278
- }
279
- container.isUpdating = true;
280
- try {
281
- container.lensState.set(container.accessor(container.source()));
282
- }
283
- finally {
284
- container.isUpdating = false;
285
- }
286
- });
287
- container.lensState.set = function lensSet(value) {
288
- if (container.isUpdating) {
289
- return;
290
- }
291
- container.isUpdating = true;
292
- try {
293
- container.originalSet(value);
294
- container.source.update((current) => setValueAtPath(current, container.path, value));
295
- }
296
- finally {
297
- container.isUpdating = false;
298
- }
299
- };
300
- container.lensState.update = function lensUpdate(fn) {
301
- container.lensState.set(fn(container.lensState()));
302
- };
303
- return container.lensState;
304
- };
305
- static notifySubscribers = () => {
306
- if (StateImpl.isNotifying) {
307
- return;
308
- }
309
- StateImpl.isNotifying = true;
310
- try {
311
- while (StateImpl.pendingSubscribers.size > 0) {
312
- const subscribers = Array.from(StateImpl.pendingSubscribers);
313
- StateImpl.pendingSubscribers.clear();
314
- for (const effect of subscribers) {
315
- effect();
316
- }
317
- }
318
- }
319
- finally {
320
- StateImpl.isNotifying = false;
321
- }
322
- };
323
- static cleanupEffect = (effect) => {
324
- StateImpl.pendingSubscribers.delete(effect);
325
- const deps = StateImpl.subscriberDependencies.get(effect);
326
- if (deps) {
327
- for (const subscribers of deps) {
328
- subscribers.delete(effect);
329
- }
330
- deps.clear();
331
- StateImpl.subscriberDependencies.delete(effect);
332
- }
333
- };
334
- }
335
- const updateArrayItem = (arr, index, value) => {
336
- const copy = [
337
- ...arr,
338
- ];
339
- copy[index] = value;
340
- return copy;
341
- };
342
- const updateShallowProperty = (obj, key, value) => {
343
- const result = {
344
- ...obj,
345
- };
346
- result[key] = value;
347
- return result;
348
- };
349
- const createContainer = (key) => {
350
- const isArrayKey = typeof key === 'number' || !Number.isNaN(Number(key));
351
- return isArrayKey ? [] : {};
352
- };
353
- const updateArrayPath = (array, pathSegments, value) => {
354
- const index = Number(pathSegments[0]);
355
- if (pathSegments.length === 1) {
356
- return updateArrayItem(array, index, value);
357
- }
358
- const copy = [
359
- ...array,
360
- ];
361
- const nextPathSegments = pathSegments.slice(1);
362
- const nextKey = nextPathSegments[0];
363
- let nextValue = array[index];
364
- if (nextValue === undefined || nextValue === null) {
365
- nextValue = nextKey !== undefined ? createContainer(nextKey) : {};
366
- }
367
- copy[index] = setValueAtPath(nextValue, nextPathSegments, value);
368
- return copy;
369
- };
370
- const updateObjectPath = (obj, pathSegments, value) => {
371
- const currentKey = pathSegments[0];
372
- if (currentKey === undefined) {
373
- return obj;
374
- }
375
- if (pathSegments.length === 1) {
376
- return updateShallowProperty(obj, currentKey, value);
377
- }
378
- const nextPathSegments = pathSegments.slice(1);
379
- const nextKey = nextPathSegments[0];
380
- let currentValue = obj[currentKey];
381
- if (currentValue === undefined || currentValue === null) {
382
- currentValue = nextKey !== undefined ? createContainer(nextKey) : {};
383
- }
384
- const result = {
385
- ...obj,
386
- };
387
- result[currentKey] = setValueAtPath(currentValue, nextPathSegments, value);
388
- return result;
389
- };
390
- const setValueAtPath = (obj, pathSegments, value) => {
391
- if (pathSegments.length === 0) {
392
- return value;
393
- }
394
- if (obj === undefined || obj === null) {
395
- return setValueAtPath({}, pathSegments, value);
396
- }
397
- const currentKey = pathSegments[0];
398
- if (currentKey === undefined) {
399
- return obj;
400
- }
401
- if (Array.isArray(obj)) {
402
- return updateArrayPath(obj, pathSegments, value);
403
- }
404
- return updateObjectPath(obj, pathSegments, value);
405
- };
406
- //# sourceMappingURL=index.js.map