@nerdalytics/beacon 1000.2.3 → 1000.2.5

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,7 +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
+ [![Socket Badge](https://badge.socket.dev/npm/package/@nerdalytics/beacon/1000.2.4)](https://socket.dev/npm/package/@nerdalytics/beacon/overview/1000.2.4)
8
8
 
9
9
  [![tech:nodejs](https://img.shields.io/badge/Node%20js-339933?style=for-the-badge&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
10
10
  [![language:typescript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)](https://typescriptlang.org/)
@@ -12,536 +12,33 @@
12
12
 
13
13
  A lightweight reactive state library for Node.js backends. Enables reactive state management with automatic dependency tracking and efficient updates for server-side applications.
14
14
 
15
- <details>
16
- <summary><Strong>Table of Contents</Strong></summary>
15
+ ## Installation
17
16
 
18
- - [Features](#features)
19
- - [Quick Start](#quick-start)
20
- - [Core Concepts](#core-concepts)
21
- - [API Reference](#api-reference)
22
- - [Core Primitives](#core-primitives)
23
- - [state](#statetinitialvalue-t-equalityfn-a-t-b-t--boolean-statet)
24
- - [derive](#derivetfn---t-readonlystatet)
25
- - [effect](#effectfn---void---void)
26
- - [batch](#batchtfn---t-t)
27
- - [select](#selectt-rsource-readonlystatet-selectorfn-state-t--r-equalityfn-a-r-b-r--boolean-readonlystater)
28
- - [lens](#lenst-ksource-statet-accessor-state-t--k-statek)
29
- - [Access Control](#access-control)
30
- - [readonlyState](#readonlystatetstate-statet-readonlystatet)
31
- - [protectedState](#protectedstatetinitialvalue-t-equalityfn-a-t-b-t--boolean-readonlystatet-writeablestatet)
32
- - [Advanced Features](#advanced-features)
33
- - [Infinite Loop Protection](#infinite-loop-protection)
34
- - [Automatic Cleanup](#automatic-cleanup)
35
- - [Custom Equality Functions](#custom-equality-functions)
36
- - [Design Philosophy](#design-philosophy)
37
- - [Architecture](#architecture)
38
- - [Development](#development)
39
- - [Key Differences vs TC39 Proposal](#key-differences-vs-tc39-proposal)
40
- - [FAQ](#faq)
41
- - [Why "Beacon" Instead of "Signal"?](#why-beacon-instead-of-signal)
42
- - [How does Beacon handle memory management?](#how-does-beacon-handle-memory-management)
43
- - [Can I use Beacon with Express or other frameworks?](#can-i-use-beacon-with-express-or-other-frameworks)
44
- - [Can Beacon be used in browser applications?](#can-beacon-be-used-in-browser-applications)
45
- - [License](#license)
46
-
47
- </details>
48
-
49
- ## Features
50
-
51
- - 📶 **Reactive state** - Create reactive values that automatically track dependencies
52
- - 🧮 **Computed values** - Derive values from other states with automatic updates
53
- - 🔍 **Fine-grained reactivity** - Dependencies are tracked precisely at the state level
54
- - 🏎️ **Efficient updates** - Only recompute values when dependencies change
55
- - 📦 **Batched updates** - Group multiple updates for performance
56
- - 🎯 **Targeted subscriptions** - Select and subscribe to specific parts of state objects
57
- - 🧹 **Automatic cleanup** - Effects and computations automatically clean up dependencies
58
- - ♻️ **Cycle handling** - Safely manages cyclic dependencies without crashing
59
- - 🚨 **Infinite loop detection** - Automatically detects and prevents infinite update loops
60
- - 🛠️ **TypeScript-first** - Full TypeScript support with generics
61
- - 🪶 **Lightweight** - Zero dependencies
62
- - ✅ **Node.js compatibility** - Works with Node.js LTS v20+ and v22+
63
-
64
- ## Quick Start
65
-
66
- ```other
17
+ ```
67
18
  npm install @nerdalytics/beacon --save-exact
68
19
  ```
69
20
 
21
+ ## Quick Start
22
+
70
23
  ```typescript
71
24
  import { state, derive, effect } from '@nerdalytics/beacon';
72
25
 
73
- // Create reactive state
74
26
  const count = state(0);
75
-
76
- // Create a derived value
77
27
  const doubled = derive(() => count() * 2);
78
28
 
79
- // Set up an effect
80
29
  effect(() => {
81
30
  console.log(`Count: ${count()}, Doubled: ${doubled()}`);
82
31
  });
83
- // => "Count: 0, Doubled: 0"
84
32
 
85
- // Update the state - effect runs automatically
86
33
  count.set(5);
87
34
  // => "Count: 5, Doubled: 10"
88
35
  ```
89
36
 
90
- ## Core Concepts
91
-
92
- Beacon is built around three core primitives:
93
-
94
- 1. **States**: Mutable, reactive values
95
- 2. **Derived States**: Read-only computed values that update automatically
96
- 3. **Effects**: Side effects that run automatically when dependencies change
97
-
98
- The library handles all the dependency tracking and updates automatically, so you can focus on your business logic.
99
-
100
- ## API Reference
101
-
102
- ### Version Compatibility
103
-
104
- The table below tracks when features were introduced and when function signatures were changed.
105
-
106
- | API | Introduced | Last Updated | Notes |
107
- |-----|------------|--------------|-------|
108
- | `state` | v1.0.0 | v1000.2.0 | Added `equalityFn` parameter |
109
- | `derive` | v1.0.0 | v1000.0.0 | Renamed `derived` → `derive` |
110
- | `effect` | v1.0.0 | - | - |
111
- | `batch` | v1.0.0 | - | - |
112
- | `select` | v1000.0.0 | - | - |
113
- | `lens` | v1000.1.0 | - | - |
114
- | `readonlyState` | v1000.0.0 | - | - |
115
- | `protectedState` | v1000.0.0 | v1000.2.0 | Added `equalityFn` parameter |
116
-
117
- ### Core Primitives
118
-
119
- #### `state<T>(initialValue: T, equalityFn?: (a: T, b: T) => boolean): State<T>`
120
- > *Since v1.0.0*
121
-
122
- The foundation of Beacon's reactivity system. Create with `state()` and use like a function.
123
-
124
- ```typescript
125
- import { state } from '@nerdalytics/beacon';
126
-
127
- const counter = state(0);
128
-
129
- // Read current value
130
- console.log(counter()); // => 0
131
-
132
- // Update value
133
- counter.set(5);
134
- console.log(counter()); // => 5
135
-
136
- // Update with a function
137
- counter.update(n => n + 1);
138
- console.log(counter()); // => 6
139
-
140
- // With custom equality function
141
- const deepCounter = state({ value: 0 }, (a, b) => {
142
- // Deep equality check
143
- return a.value === b.value;
144
- });
145
-
146
- // This won't trigger effects because values are deeply equal
147
- deepCounter.set({ value: 0 });
148
- ```
149
-
150
- #### `derive<T>(fn: () => T): ReadOnlyState<T>`
151
- > *Since v1.0.0*
152
-
153
- Calculate values based on other states. Updates automatically when dependencies change.
154
-
155
- ```typescript
156
- import { state, derive } from '@nerdalytics/beacon';
157
-
158
- const firstName = state('John');
159
- const lastName = state('Doe');
160
-
161
- const fullName = derive(() => `${firstName()} ${lastName()}`);
162
-
163
- console.log(fullName()); // => "John Doe"
164
-
165
- firstName.set('Jane');
166
- console.log(fullName()); // => "Jane Doe"
167
- ```
168
-
169
- #### `effect(fn: () => void): () => void`
170
- > *Since v1.0.0*
171
-
172
- Run side effects when reactive values change.
173
-
174
- ```typescript
175
- import { state, effect } from '@nerdalytics/beacon';
176
-
177
- const user = state({ name: 'Alice', loggedIn: false });
178
-
179
- const cleanup = effect(() => {
180
- console.log(`User ${user().name} is ${user().loggedIn ? 'online' : 'offline'}`);
181
- });
182
- // => "User Alice is offline" (effect runs immediately when created)
183
-
184
- user.update(u => ({ ...u, loggedIn: true }));
185
- // => "User Alice is online"
186
-
187
- // Stop the effect and clean up all subscriptions
188
- cleanup();
189
- ```
190
-
191
- #### `batch<T>(fn: () => T): T`
192
- > *Since v1.0.0*
193
-
194
- Group multiple updates to trigger effects only once.
195
-
196
- ```typescript
197
- import { state, effect, batch } from "@nerdalytics/beacon";
198
-
199
- const count = state(0);
200
-
201
- effect(() => {
202
- console.log(`Count is ${count()}`);
203
- });
204
- // => "Count is 0" (effect runs immediately)
205
-
206
- // Without batching, effects run after each update
207
- count.set(1);
208
- // => "Count is 1"
209
- count.set(2);
210
- // => "Count is 2"
211
-
212
- // Batch updates (only triggers effects once at the end)
213
- batch(() => {
214
- count.set(10);
215
- count.set(20);
216
- count.set(30);
217
- });
218
- // => "Count is 30" (only once)
219
- ```
220
-
221
- #### `select<T, R>(source: ReadOnlyState<T>, selectorFn: (state: T) => R, equalityFn?: (a: R, b: R) => boolean): ReadOnlyState<R>`
222
- > *Since v1000.0.0*
223
-
224
- Subscribe to specific parts of a state object.
225
-
226
- ```typescript
227
- import { state, select, effect } from '@nerdalytics/beacon';
228
-
229
- const user = state({
230
- profile: { name: 'Alice' },
231
- preferences: { theme: 'dark' }
232
- });
233
-
234
- // Only triggers when name changes
235
- const nameState = select(user, u => u.profile.name);
236
-
237
- effect(() => {
238
- console.log(`Name: ${nameState()}`);
239
- });
240
- // => "Name: Alice"
241
-
242
- // This triggers the effect
243
- user.update(u => ({
244
- ...u,
245
- profile: { ...u.profile, name: 'Bob' }
246
- }));
247
- // => "Name: Bob"
248
-
249
- // This doesn't trigger the effect (theme changed, not name)
250
- user.update(u => ({
251
- ...u,
252
- preferences: { ...u.preferences, theme: 'light' }
253
- }));
254
- ```
255
-
256
- #### `lens<T, K>(source: State<T>, accessor: (state: T) => K): State<K>`
257
- > *Since v1000.1.0*
258
-
259
- Two-way binding to deeply nested properties.
260
-
261
- ```typescript
262
- import { state, lens, effect } from "@nerdalytics/beacon";
263
-
264
- const nested = state({
265
- user: {
266
- profile: {
267
- settings: {
268
- theme: "dark",
269
- notifications: true
270
- }
271
- }
272
- }
273
- });
274
-
275
- // Create a lens focused on a deeply nested property
276
- const themeLens = lens(nested, n => n.user.profile.settings.theme);
277
-
278
- // Read the focused value
279
- console.log(themeLens()); // => "dark"
280
-
281
- // Update the focused value directly (maintains referential integrity)
282
- themeLens.set("light");
283
- console.log(themeLens()); // => "light"
284
- console.log(nested().user.profile.settings.theme); // => "light"
285
-
286
- // The entire object is updated with proper referential integrity
287
- // This makes it easy to detect changes throughout the object tree
288
- ```
289
-
290
- ### Access Control
291
-
292
- Control who can read vs. write to your state.
293
-
294
- #### `readonlyState<T>(state: State<T>): ReadOnlyState<T>`
295
- > *Since v1000.0.0*
296
-
297
- Creates a read-only view of a state, hiding mutation methods. Useful when you want to expose state to other parts of your application without allowing direct mutations.
298
-
299
- ```typescript
300
- import { state, readonlyState } from "@nerdalytics/beacon";
301
-
302
- const counter = state(0);
303
- const readonlyCounter = readonlyState(counter);
304
-
305
- // Reading works
306
- console.log(readonlyCounter()); // => 0
307
-
308
- // Updating the original state reflects in the readonly view
309
- counter.set(5);
310
- console.log(readonlyCounter()); // => 5
311
-
312
- // This would cause a TypeScript error since readonlyCounter has no set method
313
- // readonlyCounter.set(10); // Error: Property 'set' does not exist
314
- ```
315
-
316
- #### `protectedState<T>(initialValue: T, equalityFn?: (a: T, b: T) => boolean): [ReadOnlyState<T>, WriteableState<T>]`
317
- > *Since v1000.0.0*
318
-
319
- Creates a state with separated read and write capabilities, returning a tuple of reader and writer. This pattern allows you to expose only the reading capability to consuming code while keeping the writing capability private.
320
-
321
- ```typescript
322
- import { protectedState } from "@nerdalytics/beacon";
323
-
324
- // Create a state with separated read and write capabilities
325
- const [getUser, setUser] = protectedState({ name: 'Alice' });
326
-
327
- // Read the state
328
- console.log(getUser()); // => { name: 'Alice' }
329
-
330
- // Update the state
331
- setUser.set({ name: 'Bob' });
332
- console.log(getUser()); // => { name: 'Bob' }
333
-
334
- // This is useful for exposing only read access to outside consumers
335
- function createProtectedCounter() {
336
- const [getCount, setCount] = protectedState(0);
337
-
338
- return {
339
- value: getCount,
340
- increment: () => setCount.update(n => n + 1),
341
- decrement: () => setCount.update(n => n - 1)
342
- };
343
- }
344
-
345
- const counter = createProtectedCounter();
346
- console.log(counter.value()); // => 0
347
- counter.increment();
348
- console.log(counter.value()); // => 1
349
- ```
350
-
351
- ## Advanced Features
352
-
353
- Beacon includes several advanced capabilities that help you build robust applications.
354
-
355
- ### Infinite Loop Protection
356
-
357
- Beacon prevents common mistakes that could cause infinite loops:
358
-
359
- ```typescript
360
- import { state, effect } from '@nerdalytics/beacon';
361
-
362
- const counter = state(0);
363
-
364
- // This would throw an error
365
- effect(() => {
366
- const value = counter();
367
- counter.set(value + 1); // Error: Infinite loop detected!
368
- });
369
-
370
- // Instead, use proper patterns like:
371
- const increment = () => counter.update(n => n + 1);
372
- ```
373
-
374
- ### Automatic Cleanup
375
-
376
- All subscriptions are automatically cleaned up when effects are unsubscribed:
377
-
378
- ```typescript
379
- import { state, effect } from '@nerdalytics/beacon';
380
-
381
- const data = state({ loading: true, items: [] });
382
-
383
- // Effect with nested effect
384
- const cleanup = effect(() => {
385
- if (data().loading) {
386
- console.log('Loading...');
387
- } else {
388
- // This nested effect is automatically cleaned up when the parent is
389
- effect(() => {
390
- console.log(`${data().items.length} items loaded`);
391
- });
392
- }
393
- });
394
-
395
- // Unsubscribe cleans up everything, including nested effects
396
- cleanup();
397
- ```
398
-
399
- ### Custom Equality Functions
400
-
401
- Control when subscribers are notified with custom equality checks. You can provide custom equality functions to `state`, `select`, and `protectedState`:
402
-
403
- ```typescript
404
- import { state, select, effect, protectedState } from '@nerdalytics/beacon';
405
-
406
- // Custom equality function for state
407
- // Only trigger updates when references are different (useful for logging)
408
- const logMessages = state([], (a, b) => a === b); // Reference equality
409
-
410
- // Add logs - each call triggers effects even with identical content
411
- logMessages.set(['System started']); // Triggers effects
412
- logMessages.set(['System started']); // Triggers effects again
413
-
414
- // Protected state with custom equality function
415
- const [getConfig, setConfig] = protectedState({ theme: 'dark' }, (a, b) => {
416
- // Only consider configs equal if all properties match
417
- return a.theme === b.theme;
418
- });
419
-
420
- // Custom equality with select
421
- const list = state([1, 2, 3]);
422
-
423
- // Only notify when array length changes, not on content changes
424
- const listLengthState = select(
425
- list,
426
- arr => arr.length,
427
- (a, b) => a === b
428
- );
429
-
430
- effect(() => {
431
- console.log(`List has ${listLengthState()} items`);
432
- });
433
- ```
434
-
435
- ## Design Philosophy
436
-
437
- Beacon follows these key principles:
438
-
439
- 1. **Simplicity**: Minimal API surface with powerful primitives
440
- 2. **Fine-grained reactivity**: Track dependencies at exactly the right level
441
- 3. **Predictability**: State changes flow predictably through the system
442
- 4. **Performance**: Optimize for server workloads and memory efficiency
443
- 5. **Type safety**: Full TypeScript support with generics
444
-
445
- ## Architecture
446
-
447
- Beacon is built around a centralized reactivity system with fine-grained dependency tracking. Here's how it works:
448
-
449
- - **Automatic Dependency Collection**: When a state is read inside an effect, Beacon automatically records this dependency
450
- - **WeakMap-based Tracking**: Uses WeakMaps for automatic garbage collection
451
- - **Topological Updates**: Updates flow through the dependency graph in the correct order
452
- - **Memory-Efficient**: Designed for long-running Node.js processes
453
-
454
- ### Dependency Tracking
455
-
456
- When a state is read inside an effect, Beacon automatically records this dependency relationship and sets up a subscription.
457
-
458
- ### Infinite Loop Prevention
459
-
460
- Beacon actively detects when an effect tries to update a state it depends on, preventing common infinite update cycles:
461
-
462
- ```typescript
463
- // This would throw: "Infinite loop detected"
464
- effect(() => {
465
- const value = counter();
466
- counter.set(value + 1); // Error! Updating a state the effect depends on
467
- });
468
- ```
469
-
470
- ### Cyclic Dependencies
471
-
472
- Beacon employs two complementary strategies for handling cyclical updates:
473
-
474
- 1. **Active Detection**: The system tracks which states an effect reads from and writes to. If an effect attempts to directly update a state it depends on, Beacon throws a clear error.
475
- 2. **Safe Cycles**: For indirect cycles and safe update patterns, Beacon uses a queue-based update system that won't crash even with cyclical dependencies. When states form a cycle where values eventually stabilize, the system handles these updates efficiently without stack overflows.
476
-
477
- ## Development
478
-
479
- ```other
480
- # Install dependencies
481
- npm install
482
-
483
- # Run tests
484
- npm test
485
- ```
37
+ ## Documentation
486
38
 
487
- ## Key Differences vs [TC39 Proposal][1]
488
-
489
- | **Aspect** | **@nerdalytics/beacon** | **TC39 Proposal** |
490
- | --------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
491
- | **API Style** | Functional approach (`state()`, `derive()`) | Class-based design (`Signal.State`, `Signal.Computed`) |
492
- | **Reading/Writing Pattern** | Function call for reading (`count()`), methods for writing (`count.set(5)`) | Method-based access (`get()`/`set()`) |
493
- | **Framework Support** | High-level abstractions like `effect()` and `batch()` | Lower-level primitives (`Signal.subtle.Watcher`) that frameworks build upon |
494
- | **Advanced Features** | Focused on core reactivity | Includes introspection capabilities, watched/unwatched callbacks, and Signal.subtle namespace |
495
- | **Scope and Purpose** | Practical Node.js use cases with minimal API surface | Standardization with robust interoperability between frameworks |
496
-
497
- ## FAQ
498
-
499
- #### Why "Beacon" Instead of "Signal"?
500
-
501
- Beacon represents how the library broadcasts notifications when state changes—just like a lighthouse guides ships. The name avoids confusion with the TC39 proposal and similar libraries while accurately describing the core functionality.
502
-
503
- #### How does Beacon handle memory management?
504
-
505
- Beacon uses WeakMaps for dependency tracking, ensuring that unused states and effects can be garbage collected. When you unsubscribe an effect, all its internal subscriptions are automatically cleaned up.
506
-
507
- #### Can I use Beacon with Express or other frameworks?
508
-
509
- Yes! Beacon works well as a state management solution in any Node.js application:
510
-
511
- ```typescript
512
- import express from 'express';
513
- import { state, effect } from '@nerdalytics/beacon';
514
-
515
- const app = express();
516
- const stats = state({ requests: 0, errors: 0 });
517
-
518
- // Update stats on each request
519
- app.use((req, res, next) => {
520
- stats.update(s => ({ ...s, requests: s.requests + 1 }));
521
- next();
522
- });
523
-
524
- // Log stats every minute
525
- effect(() => {
526
- console.log(`Stats: ${stats().requests} requests, ${stats().errors} errors`);
527
- });
528
-
529
- app.listen(3000);
530
- ```
531
-
532
- #### Can Beacon be used in browser applications?
533
-
534
- While Beacon is optimized for Node.js server-side applications, its core principles would work in browser environments. However, the library is specifically designed for backend use cases and hasn't been optimized for browser bundle sizes or DOM integration patterns.
39
+ Full documentation, API reference, and examples available at:
40
+ **[github.com/nerdalytics/beacon](https://github.com/nerdalytics/beacon)**
535
41
 
536
42
  ## License
537
43
 
538
- This project is licensed under the MIT License. See the [LICENSE][2] file for details.
539
-
540
- <div align="center">
541
- <img src="https://raw.githubusercontent.com/nerdalytics/nerdalytics/refs/heads/main/nerdalytics-logo-gray-transparent.svg" width="128px">
542
- </div>
543
-
544
- <!-- Links collection -->
545
-
546
- [1]: https://github.com/tc39/proposal-signals
547
- [2]: ./LICENSE
44
+ MIT - See [LICENSE](./LICENSE) for details.
@@ -1 +1 @@
1
- let i=Symbol("STATE_ID"),a=(e,t=Object.is)=>l.createState(e,t);var e=e=>l.createEffect(e),t=e=>l.executeBatch(e),r=e=>l.createDerive(e),s=(e,t,r=Object.is)=>l.createSelect(e,t,r);let c=e=>()=>e();var n=(e,t=Object.is)=>{let r=a(e,t);return[()=>c(r)(),{set:e=>r.set(e),update:e=>r.update(e)}]},u=(e,t)=>l.createLens(e,t);class l{static currentSubscriber=null;static pendingSubscribers=new Set;static isNotifying=!1;static batchDepth=0;static deferredEffectCreations=[];static activeSubscribers=new Set;static stateTracking=new WeakMap;static subscriberDependencies=new WeakMap;static parentSubscriber=new WeakMap;static childSubscribers=new WeakMap;value;subscribers=new Set;stateId=Symbol();equalityFn;constructor(e,t=Object.is){this.value=e,this.equalityFn=t}static createState=(e,t=Object.is)=>{let r=new l(e,t);e=()=>r.get();return e.set=e=>r.set(e),e.update=e=>r.update(e),e[i]=r.stateId,e};get=()=>{var r=l.currentSubscriber;if(r){this.subscribers.add(r);let e=l.subscriberDependencies.get(r),t=(e||(e=new Set,l.subscriberDependencies.set(r,e)),e.add(this.subscribers),l.stateTracking.get(r));t||(t=new Set,l.stateTracking.set(r,t)),t.add(this.stateId)}return this.value};set=e=>{if(!this.equalityFn(this.value,e)){var t=l.currentSubscriber;if(t)if(l.stateTracking.get(t)?.has(this.stateId)&&!l.parentSubscriber.get(t))throw new Error("Infinite loop detected: effect() cannot update a state() it depends on!");if(this.value=e,0!==this.subscribers.size){for(var r of this.subscribers)l.pendingSubscribers.add(r);0!==l.batchDepth||l.isNotifying||l.notifySubscribers()}}};update=e=>{this.set(e(this.value))};static createEffect=e=>{let r=()=>{if(!l.activeSubscribers.has(r)){l.activeSubscribers.add(r);var t=l.currentSubscriber;try{if(l.cleanupEffect(r),l.currentSubscriber=r,l.stateTracking.set(r,new Set),t){l.parentSubscriber.set(r,t);let e=l.childSubscribers.get(t);e||(e=new Set,l.childSubscribers.set(t,e)),e.add(r)}e()}finally{l.currentSubscriber=t,l.activeSubscribers.delete(r)}}};if(0===l.batchDepth)r();else{if(l.currentSubscriber){var t=l.currentSubscriber;l.parentSubscriber.set(r,t);let e=l.childSubscribers.get(t);e||(e=new Set,l.childSubscribers.set(t,e)),e.add(r)}l.deferredEffectCreations.push(r)}return()=>{l.cleanupEffect(r),l.pendingSubscribers.delete(r),l.activeSubscribers.delete(r),l.stateTracking.delete(r);var e=l.parentSubscriber.get(r),e=(e&&(e=l.childSubscribers.get(e))&&e.delete(r),l.parentSubscriber.delete(r),l.childSubscribers.get(r));if(e){for(var t of e)l.cleanupEffect(t);e.clear(),l.childSubscribers.delete(r)}}};static executeBatch=e=>{l.batchDepth++;try{return e()}catch(e){throw 1===l.batchDepth&&(l.pendingSubscribers.clear(),l.deferredEffectCreations.length=0),e}finally{if(l.batchDepth--,0===l.batchDepth){if(0<l.deferredEffectCreations.length){var t,e=[...l.deferredEffectCreations];l.deferredEffectCreations.length=0;for(t of e)t()}0<l.pendingSubscribers.size&&!l.isNotifying&&l.notifySubscribers()}}};static createDerive=e=>{let t={cachedValue:void 0,computeFn:e,initialized:!1,valueState:l.createState(void 0)};return l.createEffect(function(){var e=t.computeFn();t.initialized&&Object.is(t.cachedValue,e)||(t.cachedValue=e,t.valueState.set(e)),t.initialized=!0}),function(){return t.initialized||(t.cachedValue=t.computeFn(),t.initialized=!0,t.valueState.set(t.cachedValue)),t.valueState()}};static createSelect=(e,t,r=Object.is)=>{let i={equalityFn:r,initialized:!1,lastSelectedValue:void 0,lastSourceValue:void 0,selectorFn:t,source:e,valueState:l.createState(void 0)};return l.createEffect(function(){var e=i.source();i.initialized&&Object.is(i.lastSourceValue,e)||(i.lastSourceValue=e,e=i.selectorFn(e),i.initialized&&void 0!==i.lastSelectedValue&&i.equalityFn(i.lastSelectedValue,e))||(i.lastSelectedValue=e,i.valueState.set(e),i.initialized=!0)}),function(){return i.initialized||(i.lastSourceValue=i.source(),i.lastSelectedValue=i.selectorFn(i.lastSourceValue),i.valueState.set(i.lastSelectedValue),i.initialized=!0),i.valueState()}};static createLens=(e,t)=>{let a={accessor:t,isUpdating:!1,lensState:null,originalSet:null,path:[],source:e};return a.path=(()=>{let r=[],i=new Proxy({},{get:(e,t)=>("string"!=typeof t&&"number"!=typeof t||r.push(t),i)});try{a.accessor(i)}catch{}return r})(),a.lensState=l.createState(a.accessor(a.source())),a.originalSet=a.lensState.set,l.createEffect(function(){if(!a.isUpdating){a.isUpdating=!0;try{a.lensState.set(a.accessor(a.source()))}finally{a.isUpdating=!1}}}),a.lensState.set=function(t){if(!a.isUpdating){a.isUpdating=!0;try{a.originalSet(t),a.source.update(e=>p(e,a.path,t))}finally{a.isUpdating=!1}}},a.lensState.update=function(e){a.lensState.set(e(a.lensState()))},a.lensState};static notifySubscribers=()=>{if(!l.isNotifying){l.isNotifying=!0;try{for(;0<l.pendingSubscribers.size;){var e,t=Array.from(l.pendingSubscribers);l.pendingSubscribers.clear();for(e of t)e()}}finally{l.isNotifying=!1}}};static cleanupEffect=e=>{l.pendingSubscribers.delete(e);var t=l.subscriberDependencies.get(e);if(t){for(var r of t)r.delete(e);t.clear(),l.subscriberDependencies.delete(e)}}}let b=(e,t,r)=>{e=[...e];return e[t]=r,e},d=(e,t,r)=>{e={...e};return e[t]=r,e},f=e=>"number"==typeof e||!Number.isNaN(Number(e))?[]:{},S=(e,t,r)=>{var i=Number(t[0]);if(1===t.length)return b(e,i,r);var a=[...e],t=t.slice(1),s=t[0];let c=e[i];return null==c&&(c=void 0!==s?f(s):{}),a[i]=p(c,t,r),a},o=(e,t,r)=>{var i=t[0];if(void 0===i)return e;if(1===t.length)return d(e,i,r);var t=t.slice(1),a=t[0];let s=e[i];null==s&&(s=void 0!==a?f(a):{});a={...e};return a[i]=p(s,t,r),a},p=(e,t,r)=>0===t.length?r:null==e?p({},t,r):void 0===t[0]?e:(Array.isArray(e)?S:o)(e,t,r);export{a as state,e as effect,t as batch,r as derive,s as select,c as readonlyState,n as protectedState,u as lens};
1
+ let i=Symbol("STATE_ID"),a=(e,t=Object.is)=>l.createState(e,t);var e=e=>l.createEffect(e),t=e=>l.executeBatch(e),r=e=>l.createDerive(e),s=(e,t,r=Object.is)=>l.createSelect(e,t,r);let c=e=>()=>e();var n=(e,t=Object.is)=>{let r=a(e,t);return[()=>c(r)(),{set:e=>r.set(e),update:e=>r.update(e)}]},u=(e,t)=>l.createLens(e,t);class l{static currentSubscriber=null;static pendingSubscribers=new Set;static isNotifying=!1;static batchDepth=0;static deferredEffectCreations=[];static activeSubscribers=new Set;static stateTracking=new WeakMap;static subscriberDependencies=new WeakMap;static parentSubscriber=new WeakMap;static childSubscribers=new WeakMap;value;subscribers=new Set;stateId=Symbol();equalityFn;constructor(e,t=Object.is){this.value=e,this.equalityFn=t}static createState=(e,t=Object.is)=>{let r=new l(e,t);e=()=>r.get();return e.set=e=>r.set(e),e.update=e=>r.update(e),e[i]=r.stateId,e};get=()=>{var r=l.currentSubscriber;if(r){this.subscribers.add(r);let e=l.subscriberDependencies.get(r),t=(e||(e=new Set,l.subscriberDependencies.set(r,e)),e.add(this.subscribers),l.stateTracking.get(r));t||(t=new Set,l.stateTracking.set(r,t)),t.add(this.stateId)}return this.value};set=e=>{if(!this.equalityFn(this.value,e)){var t=l.currentSubscriber;if(t)if(l.stateTracking.get(t)?.has(this.stateId)&&!l.parentSubscriber.get(t))throw new Error("Infinite loop detected: effect() cannot update a state() it depends on!");if(this.value=e,0!==this.subscribers.size){for(var r of this.subscribers)l.pendingSubscribers.add(r);0!==l.batchDepth||l.isNotifying||l.notifySubscribers()}}};update=e=>{this.set(e(this.value))};static createEffect=e=>{let r=()=>{if(!l.activeSubscribers.has(r)){l.activeSubscribers.add(r);var t=l.currentSubscriber;try{if(l.cleanupEffect(r),l.currentSubscriber=r,l.stateTracking.set(r,new Set),t){l.parentSubscriber.set(r,t);let e=l.childSubscribers.get(t);e||(e=new Set,l.childSubscribers.set(t,e)),e.add(r)}e()}finally{l.currentSubscriber=t,l.activeSubscribers.delete(r)}}};if(0===l.batchDepth)r();else{if(l.currentSubscriber){var t=l.currentSubscriber;l.parentSubscriber.set(r,t);let e=l.childSubscribers.get(t);e||(e=new Set,l.childSubscribers.set(t,e)),e.add(r)}l.deferredEffectCreations.push(r)}return()=>{l.cleanupEffect(r),l.pendingSubscribers.delete(r),l.activeSubscribers.delete(r),l.stateTracking.delete(r);var e=l.parentSubscriber.get(r),e=(e&&(e=l.childSubscribers.get(e))&&e.delete(r),l.parentSubscriber.delete(r),l.childSubscribers.get(r));if(e){for(var t of e)l.cleanupEffect(t);e.clear(),l.childSubscribers.delete(r)}}};static executeBatch=e=>{l.batchDepth++;try{return e()}catch(e){throw 1===l.batchDepth&&(l.pendingSubscribers.clear(),l.deferredEffectCreations.length=0),e}finally{if(l.batchDepth--,0===l.batchDepth){if(0<l.deferredEffectCreations.length){var t,e=l.deferredEffectCreations;l.deferredEffectCreations=[];for(t of e)t()}0<l.pendingSubscribers.size&&!l.isNotifying&&l.notifySubscribers()}}};static createDerive=e=>{let t={cachedValue:void 0,computeFn:e,initialized:!1,valueState:l.createState(void 0)};return l.createEffect(function(){var e=t.computeFn();t.initialized&&Object.is(t.cachedValue,e)||(t.cachedValue=e,t.valueState.set(e)),t.initialized=!0}),function(){return t.initialized||(t.cachedValue=t.computeFn(),t.initialized=!0,t.valueState.set(t.cachedValue)),t.valueState()}};static createSelect=(e,t,r=Object.is)=>{let i={equalityFn:r,initialized:!1,lastSelectedValue:void 0,lastSourceValue:void 0,selectorFn:t,source:e,valueState:l.createState(void 0)};return l.createEffect(function(){var e=i.source();i.initialized&&Object.is(i.lastSourceValue,e)||(i.lastSourceValue=e,e=i.selectorFn(e),i.initialized&&void 0!==i.lastSelectedValue&&i.equalityFn(i.lastSelectedValue,e))||(i.lastSelectedValue=e,i.valueState.set(e),i.initialized=!0)}),function(){return i.initialized||(i.lastSourceValue=i.source(),i.lastSelectedValue=i.selectorFn(i.lastSourceValue),i.valueState.set(i.lastSelectedValue),i.initialized=!0),i.valueState()}};static createLens=(e,t)=>{let a={accessor:t,isUpdating:!1,lensState:null,originalSet:null,path:[],source:e};return a.path=(()=>{let r=[],i=new Proxy({},{get:(e,t)=>("string"!=typeof t&&"number"!=typeof t||r.push(t),i)});try{a.accessor(i)}catch{}return r})(),a.lensState=l.createState(a.accessor(a.source())),a.originalSet=a.lensState.set,l.createEffect(function(){if(!a.isUpdating){a.isUpdating=!0;try{a.lensState.set(a.accessor(a.source()))}finally{a.isUpdating=!1}}}),a.lensState.set=function(t){if(!a.isUpdating){a.isUpdating=!0;try{a.originalSet(t),a.source.update(e=>p(e,a.path,t))}finally{a.isUpdating=!1}}},a.lensState.update=function(e){a.lensState.set(e(a.lensState()))},a.lensState};static notifySubscribers=()=>{if(!l.isNotifying){l.isNotifying=!0;try{for(;0<l.pendingSubscribers.size;){var e,t=l.pendingSubscribers;l.pendingSubscribers=new Set;for(e of t)e()}}finally{l.isNotifying=!1}}};static cleanupEffect=e=>{l.pendingSubscribers.delete(e);var t=l.subscriberDependencies.get(e);if(t){for(var r of t)r.delete(e);t.clear(),l.subscriberDependencies.delete(e)}}}let b=(e,t,r)=>{e=[...e];return e[t]=r,e},d=(e,t,r)=>{e={...e};return e[t]=r,e},f=e=>"number"==typeof e||!Number.isNaN(Number(e))?[]:{},S=(e,t,r)=>{var i=Number(t[0]);if(1===t.length)return b(e,i,r);var a=[...e],t=t.slice(1),s=t[0];let c=e[i];return null==c&&(c=void 0!==s?f(s):{}),a[i]=p(c,t,r),a},o=(e,t,r)=>{var i=t[0];if(void 0===i)return e;if(1===t.length)return d(e,i,r);var t=t.slice(1),a=t[0];let s=e[i];null==s&&(s=void 0!==a?f(a):{});a={...e};return a[i]=p(s,t,r),a},p=(e,t,r)=>0===t.length?r:null==e?p({},t,r):void 0===t[0]?e:(Array.isArray(e)?S:o)(e,t,r);export{a as state,e as effect,t as batch,r as derive,s as select,c as readonlyState,n as protectedState,u as lens};
package/package.json CHANGED
@@ -2,9 +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.7",
6
- "@types/node": "24.9.1",
7
- "npm-check-updates": "19.1.1",
5
+ "@biomejs/biome": "2.3.13",
6
+ "@types/node": "25.0.10",
7
+ "npm-check-updates": "19.3.2",
8
8
  "typescript": "5.9.3",
9
9
  "uglify-js": "3.19.3"
10
10
  },
@@ -50,7 +50,7 @@
50
50
  "license": "MIT",
51
51
  "main": "dist/src/index.min.js",
52
52
  "name": "@nerdalytics/beacon",
53
- "packageManager": "npm@11.6.2",
53
+ "packageManager": "npm@11.8.0",
54
54
  "repository": {
55
55
  "type": "git",
56
56
  "url": "git+https://github.com/nerdalytics/beacon.git"
@@ -93,5 +93,5 @@
93
93
  },
94
94
  "type": "module",
95
95
  "types": "dist/src/index.d.ts",
96
- "version": "1000.2.3"
96
+ "version": "1000.2.5"
97
97
  }
package/src/index.ts CHANGED
@@ -310,10 +310,9 @@ class StateImpl<T> {
310
310
  if (StateImpl.batchDepth === 0) {
311
311
  // Process effects created during the batch
312
312
  if (StateImpl.deferredEffectCreations.length > 0) {
313
- const effectsToRun = [
314
- ...StateImpl.deferredEffectCreations,
315
- ]
316
- StateImpl.deferredEffectCreations.length = 0
313
+ // Swap reference instead of spread copy to avoid array allocation
314
+ const effectsToRun = StateImpl.deferredEffectCreations
315
+ StateImpl.deferredEffectCreations = []
317
316
  for (const effect of effectsToRun) {
318
317
  effect()
319
318
  }
@@ -526,10 +525,9 @@ class StateImpl<T> {
526
525
  // Process all pending effects in batches for better perf,
527
526
  // ensuring topological execution order is maintained
528
527
  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()
528
+ // Swap with empty Set instead of Array.from() to avoid array allocation
529
+ const subscribers = StateImpl.pendingSubscribers
530
+ StateImpl.pendingSubscribers = new Set()
533
531
 
534
532
  for (const effect of subscribers) {
535
533
  effect()