@nerdalytics/beacon 1000.2.4 โ†’ 1000.3.0

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