@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 +9 -512
- package/dist/src/index.min.js +1 -1
- package/package.json +5 -5
- package/src/index.ts +6 -8
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://github.com/nerdalytics/beacon/blob/trunk/LICENSE)
|
|
6
6
|
[](https://www.npmjs.com/package/@nerdalytics/beacon)
|
|
7
|
-
[](https://socket.dev/npm/package/@nerdalytics/beacon/overview/1000.2.4)
|
|
8
8
|
|
|
9
9
|
[](https://nodejs.org/)
|
|
10
10
|
[](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
|
-
|
|
16
|
-
<summary><Strong>Table of Contents</Strong></summary>
|
|
15
|
+
## Installation
|
|
17
16
|
|
|
18
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
package/dist/src/index.min.js
CHANGED
|
@@ -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=
|
|
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.
|
|
6
|
-
"@types/node": "
|
|
7
|
-
"npm-check-updates": "19.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
314
|
-
|
|
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
|
-
//
|
|
530
|
-
|
|
531
|
-
|
|
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()
|