@nerdalytics/beacon 1000.3.2 → 1000.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/src/index.d.ts +1 -4
- package/dist/src/index.min.js +1 -1
- package/package.json +12 -34
- package/src/index.ts +70 -77
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.3.3)
|
|
8
8
|
|
|
9
9
|
[](https://nodejs.org/)
|
|
10
10
|
[](https://typescriptlang.org/)
|
package/dist/src/index.d.ts
CHANGED
|
@@ -4,10 +4,7 @@ interface WriteableState<T> {
|
|
|
4
4
|
set(value: T): void;
|
|
5
5
|
update(fn: (value: T) => T): void;
|
|
6
6
|
}
|
|
7
|
-
|
|
8
|
-
type State<T> = ReadOnlyState<T> & WriteableState<T> & {
|
|
9
|
-
[STATE_ID]?: symbol;
|
|
10
|
-
};
|
|
7
|
+
type State<T> = ReadOnlyState<T> & WriteableState<T>;
|
|
11
8
|
declare const createState: <T>(initialValue: T, equalityFn?: (a: T, b: T) => boolean) => State<T>;
|
|
12
9
|
declare const createEffect: (fn: () => void) => Unsubscribe;
|
|
13
10
|
declare const executeBatch: <T>(fn: () => T) => T;
|
package/dist/src/index.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
let
|
|
1
|
+
let o=null,r=new Set,a=new Set,s=a,u=!1,c=0,n=[],f=new Set,i=new WeakMap,v=new WeakMap,d=new WeakMap,l=new WeakMap,w=new Set(["__proto__","constructor","prototype"]),p=(e,t,r)=>{let a=e.get(t);return a||(a=r(),e.set(t,a)),a},y=()=>{if(!u){u=!0;try{for(;0<s.size;){var e,t=s;s=t===a?r:a;for(e of t)e();t.clear()}}finally{u=!1}}},S=e=>{s.delete(e);var t=v.get(e);if(t){for(var r of t)r.delete(e);t.clear(),v.delete(e)}},g=e=>{S(e),f.delete(e),i.delete(e);var t=d.get(e),t=(t&&(t=l.get(t))&&t.delete(e),d.delete(e),l.get(e));if(t){for(var r of t)g(r);t.clear(),l.delete(e)}},h=(e,a=Object.is)=>{let n=e,f=new Set,l=Symbol(),t=()=>{var e=o;return e&&(f.add(e),p(v,e,()=>new Set).add(f),p(i,e,()=>new Set).add(l)),n};return t.set=e=>{if(!a(n,e)){var t=o;if(t)if(i.get(t)?.has(l)&&!d.get(t))throw new Error("Infinite loop detected: effect() cannot update a state() it depends on!");if(n=e,0!==f.size){for(var r of f)s.add(r);0!==c||u||y()}}},t.update=e=>{t.set(e(n))},t},b=r=>{let a=()=>{if(!f.has(a)){f.add(a);var e=o;try{S(a),o=a;var t=i.get(a);t?t.clear():i.set(a,new Set),e&&(d.set(a,e),p(l,e,()=>new Set).add(a)),r()}finally{o=e,f.delete(a)}}};var e;return 0===c?a():(o&&(e=o,d.set(a,e),p(l,e,()=>new Set).add(a)),n.push(a)),()=>{g(a)}};var e=e=>{c++;try{return e()}catch(e){throw 1===c&&(s.clear(),n.length=0),e}finally{if(0===--c){if(0<n.length){var t,e=n;n=[];for(t of e)t()}0<s.size&&!u&&y()}}},t=r=>{let a=void 0,n=!1,f=new Set;return b(function(){var e=r();if(!n||!Object.is(a,e)){a=e;for(var t of f)s.add(t);0!==c||u||y()}n=!0}),function(){var e=o;return e&&(f.add(e),p(v,e,()=>new Set).add(f)),n||(a=r(),n=!0),a}},j=(r,a,n=Object.is)=>{let f=!1,l,i,d=new Set;return b(function(){var e=r();if(!f||!Object.is(i,e)){i=e;e=a(e);if(!f||void 0===l||!n(l,e)){l=e,f=!0;for(var t of d)s.add(t);0!==c||u||y()}}}),function(){var e=o;return e&&(d.add(e),p(v,e,()=>new Set).add(d)),f||(i=r(),l=a(i),f=!0),l}};let N=(e,t)=>null!=e?e:void 0===t||Number.isNaN(Number(t))?{}:[],O=(t,e,r,a)=>{if(r>=e.length)return a;if(null==t)return O({},e,r,a);var n=e[r];if(void 0===n)return t;var f=Array.isArray(t),n=f?Number(n):n;if(r===e.length-1){if(f)return(l=[...t])[n]=a,l;let e={...t};return e[n]=a,e}var l=r+1,r=e[l],i=t[n],i=N(i,r);if(f)return(r=[...t])[n]=O(i,e,l,a),r;let d={...t};return d[n]=O(i,e,l,a),d};var k=(e,t)=>{let r=!1;let a=(()=>{let r=[],a=!1,n=new Proxy({},{get:(e,t)=>(a||"string"!=typeof t||(w.has(String(t))?a=!0:r.push(t)),n)});try{t(n)}catch{}return a?[]:r})(),n=h(t(e())),f=n.set;return b(function(){if(!r){r=!0;try{n.set(t(e()))}finally{r=!1}}}),n.set=function(t){if(!r&&0!==a.length){r=!0;try{f(t),e.update(e=>O(e,a,0,t))}finally{r=!1}}},n.update=function(e){n.set(e(n()))},n};let m=e=>()=>e();var M=(e,t=Object.is)=>{let r=h(e,t);return[m(r),{set:e=>r.set(e),update:e=>r.update(e)}]};export{t as derive,b as effect,k as lens,M as protectedState,m as readonlyState,j as select,h as state,e as batch};
|
package/package.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"author": "Denny Trebbin (nerdalytics)",
|
|
3
3
|
"description": "Reactive dependency graph runtime for Node.js backends. Tracks dependencies between signals and propagates updates automatically.",
|
|
4
4
|
"devDependencies": {
|
|
5
|
-
"@biomejs/biome": "2.
|
|
6
|
-
"@types/node": "25.
|
|
7
|
-
"npm-check-updates": "
|
|
8
|
-
"typescript": "
|
|
5
|
+
"@biomejs/biome": "2.4.11",
|
|
6
|
+
"@types/node": "25.5.2",
|
|
7
|
+
"npm-check-updates": "20.0.0",
|
|
8
|
+
"typescript": "6.0.2",
|
|
9
9
|
"uglify-js": "3.19.3"
|
|
10
10
|
},
|
|
11
11
|
"engines": {
|
|
@@ -32,32 +32,25 @@
|
|
|
32
32
|
],
|
|
33
33
|
"keywords": [
|
|
34
34
|
"backend",
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"effects",
|
|
39
|
-
"fine-grained",
|
|
40
|
-
"lightweight",
|
|
41
|
-
"memory-management",
|
|
35
|
+
"batch",
|
|
36
|
+
"dependency-graph",
|
|
37
|
+
"effect",
|
|
42
38
|
"nodejs",
|
|
43
|
-
"performance",
|
|
44
39
|
"reactive",
|
|
45
|
-
"server-side",
|
|
46
40
|
"signals",
|
|
47
|
-
"state
|
|
48
|
-
"typescript"
|
|
41
|
+
"state"
|
|
49
42
|
],
|
|
50
43
|
"license": "MIT",
|
|
51
44
|
"main": "dist/src/index.min.js",
|
|
52
45
|
"name": "@nerdalytics/beacon",
|
|
53
|
-
"packageManager": "npm@11.
|
|
46
|
+
"packageManager": "npm@11.12.1",
|
|
54
47
|
"repository": {
|
|
55
48
|
"type": "git",
|
|
56
49
|
"url": "git+https://github.com/nerdalytics/beacon.git"
|
|
57
50
|
},
|
|
58
51
|
"scripts": {
|
|
59
52
|
"benchmark": "node --expose-gc scripts/benchmark.ts -R 3",
|
|
60
|
-
|
|
53
|
+
|
|
61
54
|
"build": "npm run build:lts",
|
|
62
55
|
"build:lts": "tsc -p tsconfig.lts.json",
|
|
63
56
|
"check": "npx @biomejs/biome check",
|
|
@@ -71,28 +64,13 @@
|
|
|
71
64
|
"prepublishOnly": "npm run build:lts",
|
|
72
65
|
"pretest:lts": "node scripts/run-lts-tests.js",
|
|
73
66
|
"test": "node --test --test-skip-pattern=\"COMPONENT NAME\" tests/**/*.ts",
|
|
74
|
-
"test:behavior": "node --test tests/infinite-loop.test.ts tests/cyclic-dependency.test.ts tests/cleanup.test.ts",
|
|
75
|
-
"test:core": "node --test tests/*-core.test.ts",
|
|
76
67
|
"test:coverage": "node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info tests/**/*.test.ts",
|
|
77
|
-
"test:integration": "node --test tests/state-*.test.ts tests/batch-integration.test.ts",
|
|
78
68
|
"test:lts:20": "node --test dist/tests/**.js",
|
|
79
69
|
"test:lts:22": "node --test --test-skip-pattern=\"COMPONENT NAME\" dist/tests/**/*.js",
|
|
80
|
-
"
|
|
81
|
-
"test:unit:cleanup": "node --test tests/cleanup.test.ts",
|
|
82
|
-
"test:unit:custom-equality": "node --test tests/custom-equality.test.ts",
|
|
83
|
-
"test:unit:cyclic-dependency": "node --test tests/cyclic-dependency.test.ts",
|
|
84
|
-
"test:unit:deep-chain": "node --test tests/deep-chain.test.ts",
|
|
85
|
-
"test:unit:derive": "node --test tests/derive.test.ts",
|
|
86
|
-
"test:unit:effect": "node --test tests/effect.test.ts",
|
|
87
|
-
"test:unit:infinite-loop": "node --test tests/infinite-loop.test.ts",
|
|
88
|
-
"test:unit:lens": "node --test tests/lens.test.ts",
|
|
89
|
-
"test:unit:select": "node --test tests/select.test.ts",
|
|
90
|
-
"test:unit:state": "node --test tests/state.test.ts",
|
|
91
|
-
"update-dependencies": "npx npm-check-updates --interactive --upgrade --removeRange",
|
|
92
|
-
"update-performance-docs": "node --experimental-config-file=node.config.json scripts/update-performance-docs.ts"
|
|
70
|
+
"update-dependencies": "npx npm-check-updates --interactive --upgrade --removeRange"
|
|
93
71
|
},
|
|
94
72
|
"sideEffects": false,
|
|
95
73
|
"type": "module",
|
|
96
74
|
"types": "dist/src/index.d.ts",
|
|
97
|
-
"version": "1000.3.
|
|
75
|
+
"version": "1000.3.3"
|
|
98
76
|
}
|
package/src/index.ts
CHANGED
|
@@ -7,17 +7,13 @@ interface WriteableState<T> {
|
|
|
7
7
|
update(fn: (value: T) => T): void
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
const STATE_ID: unique symbol = Symbol('STATE_ID')
|
|
12
|
-
|
|
13
|
-
type State<T> = ReadOnlyState<T> &
|
|
14
|
-
WriteableState<T> & {
|
|
15
|
-
[STATE_ID]?: symbol
|
|
16
|
-
}
|
|
10
|
+
type State<T> = ReadOnlyState<T> & WriteableState<T>
|
|
17
11
|
|
|
18
12
|
// Module-level reactive state
|
|
19
13
|
let currentSubscriber: Subscriber | null = null
|
|
20
|
-
|
|
14
|
+
const flushing: Set<Subscriber> = new Set<Subscriber>()
|
|
15
|
+
const queued: Set<Subscriber> = new Set<Subscriber>()
|
|
16
|
+
let pendingSubscribers: Set<Subscriber> = queued
|
|
21
17
|
let isNotifying = false
|
|
22
18
|
let batchDepth = 0
|
|
23
19
|
let deferredEffectCreations: Subscriber[] = []
|
|
@@ -54,11 +50,12 @@ const notifySubscribers = (): void => {
|
|
|
54
50
|
try {
|
|
55
51
|
while (pendingSubscribers.size > 0) {
|
|
56
52
|
const subscribers = pendingSubscribers
|
|
57
|
-
pendingSubscribers =
|
|
53
|
+
pendingSubscribers = subscribers === queued ? flushing : queued
|
|
58
54
|
|
|
59
55
|
for (const effect of subscribers) {
|
|
60
56
|
effect()
|
|
61
57
|
}
|
|
58
|
+
subscribers.clear()
|
|
62
59
|
}
|
|
63
60
|
} finally {
|
|
64
61
|
isNotifying = false
|
|
@@ -151,7 +148,6 @@ const createState = <T>(initialValue: T, equalityFn: (a: T, b: T) => boolean = O
|
|
|
151
148
|
get.set(fn(value))
|
|
152
149
|
}
|
|
153
150
|
|
|
154
|
-
get[STATE_ID] = stateId
|
|
155
151
|
return get as State<T>
|
|
156
152
|
}
|
|
157
153
|
|
|
@@ -236,26 +232,37 @@ const executeBatch = <T>(fn: () => T): T => {
|
|
|
236
232
|
const createDerive = <T>(computeFn: () => T): ReadOnlyState<T> => {
|
|
237
233
|
let cachedValue: T = undefined as unknown as T
|
|
238
234
|
let initialized = false
|
|
239
|
-
const
|
|
235
|
+
const subscribers = new Set<Subscriber>()
|
|
240
236
|
|
|
241
237
|
createEffect(function deriveEffect(): void {
|
|
242
238
|
const newValue = computeFn()
|
|
243
239
|
|
|
244
240
|
if (!(initialized && Object.is(cachedValue, newValue))) {
|
|
245
241
|
cachedValue = newValue
|
|
246
|
-
|
|
242
|
+
|
|
243
|
+
for (const sub of subscribers) {
|
|
244
|
+
pendingSubscribers.add(sub)
|
|
245
|
+
}
|
|
246
|
+
if (batchDepth === 0 && !isNotifying) {
|
|
247
|
+
notifySubscribers()
|
|
248
|
+
}
|
|
247
249
|
}
|
|
248
250
|
|
|
249
251
|
initialized = true
|
|
250
252
|
})
|
|
251
253
|
|
|
252
254
|
return function deriveGetter(): T {
|
|
255
|
+
const currentEffect = currentSubscriber
|
|
256
|
+
if (currentEffect) {
|
|
257
|
+
subscribers.add(currentEffect)
|
|
258
|
+
getOrCreate(subscriberDependencies, currentEffect, () => new Set()).add(subscribers)
|
|
259
|
+
}
|
|
260
|
+
|
|
253
261
|
if (!initialized) {
|
|
254
262
|
cachedValue = computeFn()
|
|
255
263
|
initialized = true
|
|
256
|
-
valueState.set(cachedValue)
|
|
257
264
|
}
|
|
258
|
-
return
|
|
265
|
+
return cachedValue
|
|
259
266
|
}
|
|
260
267
|
}
|
|
261
268
|
|
|
@@ -267,7 +274,7 @@ const createSelect = <T, R>(
|
|
|
267
274
|
let initialized = false
|
|
268
275
|
let lastSelectedValue: R | undefined
|
|
269
276
|
let lastSourceValue: T | undefined
|
|
270
|
-
const
|
|
277
|
+
const subscribers = new Set<Subscriber>()
|
|
271
278
|
|
|
272
279
|
createEffect(function selectEffect(): void {
|
|
273
280
|
const sourceValue = source()
|
|
@@ -284,55 +291,45 @@ const createSelect = <T, R>(
|
|
|
284
291
|
}
|
|
285
292
|
|
|
286
293
|
lastSelectedValue = newSelectedValue
|
|
287
|
-
valueState.set(newSelectedValue)
|
|
288
294
|
initialized = true
|
|
295
|
+
|
|
296
|
+
for (const sub of subscribers) {
|
|
297
|
+
pendingSubscribers.add(sub)
|
|
298
|
+
}
|
|
299
|
+
if (batchDepth === 0 && !isNotifying) {
|
|
300
|
+
notifySubscribers()
|
|
301
|
+
}
|
|
289
302
|
})
|
|
290
303
|
|
|
291
304
|
return function selectGetter(): R {
|
|
305
|
+
const currentEffect = currentSubscriber
|
|
306
|
+
if (currentEffect) {
|
|
307
|
+
subscribers.add(currentEffect)
|
|
308
|
+
getOrCreate(subscriberDependencies, currentEffect, () => new Set()).add(subscribers)
|
|
309
|
+
}
|
|
310
|
+
|
|
292
311
|
if (!initialized) {
|
|
293
312
|
lastSourceValue = source()
|
|
294
313
|
lastSelectedValue = selectorFn(lastSourceValue)
|
|
295
|
-
valueState.set(lastSelectedValue)
|
|
296
314
|
initialized = true
|
|
297
315
|
}
|
|
298
|
-
return
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Helper for array updates
|
|
303
|
-
const updateArrayItem = <V>(arr: unknown[], index: number, value: V): unknown[] => {
|
|
304
|
-
const copy = [
|
|
305
|
-
...arr,
|
|
306
|
-
]
|
|
307
|
-
copy[index] = value
|
|
308
|
-
return copy
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Helper for single-level updates (optimization)
|
|
312
|
-
const updateShallowProperty = <V>(
|
|
313
|
-
obj: Record<string | number, unknown>,
|
|
314
|
-
key: string | number,
|
|
315
|
-
value: V
|
|
316
|
-
): Record<string | number, unknown> => {
|
|
317
|
-
const result = {
|
|
318
|
-
...obj,
|
|
316
|
+
return lastSelectedValue as R
|
|
319
317
|
}
|
|
320
|
-
result[key] = value
|
|
321
|
-
return result
|
|
322
318
|
}
|
|
323
319
|
|
|
324
|
-
//
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
320
|
+
// Returns the value if non-nullish, otherwise creates the appropriate container type
|
|
321
|
+
const ensureContainer = (value: unknown, nextKey: string | undefined): unknown => {
|
|
322
|
+
if (value != null) return value
|
|
323
|
+
if (nextKey !== undefined && !Number.isNaN(Number(nextKey))) return []
|
|
324
|
+
return {}
|
|
328
325
|
}
|
|
329
326
|
|
|
330
|
-
const setValueAtPath = <V, O>(obj: O, pathSegments:
|
|
327
|
+
const setValueAtPath = <V, O>(obj: O, pathSegments: string[], depth: number, value: V): O => {
|
|
331
328
|
if (depth >= pathSegments.length) {
|
|
332
329
|
return value as unknown as O
|
|
333
330
|
}
|
|
334
331
|
|
|
335
|
-
if (obj
|
|
332
|
+
if (obj == null) {
|
|
336
333
|
return setValueAtPath({} as O, pathSegments, depth, value)
|
|
337
334
|
}
|
|
338
335
|
|
|
@@ -341,60 +338,56 @@ const setValueAtPath = <V, O>(obj: O, pathSegments: (string | number)[], depth:
|
|
|
341
338
|
return obj
|
|
342
339
|
}
|
|
343
340
|
|
|
344
|
-
|
|
345
|
-
|
|
341
|
+
const isArray = Array.isArray(obj)
|
|
342
|
+
const key = isArray ? Number(currentKey) : currentKey
|
|
346
343
|
|
|
347
|
-
|
|
348
|
-
|
|
344
|
+
if (depth === pathSegments.length - 1) {
|
|
345
|
+
if (isArray) {
|
|
346
|
+
const copy = [
|
|
347
|
+
...(obj as unknown[]),
|
|
348
|
+
]
|
|
349
|
+
copy[key as number] = value
|
|
350
|
+
return copy as unknown as O
|
|
349
351
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
...obj,
|
|
353
|
-
]
|
|
354
|
-
const nextDepth = depth + 1
|
|
355
|
-
const nextKey = pathSegments[nextDepth]
|
|
356
|
-
|
|
357
|
-
let nextValue = obj[index]
|
|
358
|
-
if (nextValue === undefined || nextValue === null) {
|
|
359
|
-
nextValue = nextKey === undefined ? {} : createContainer(nextKey)
|
|
352
|
+
const result = {
|
|
353
|
+
...(obj as Record<string, unknown>),
|
|
360
354
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
return copy as unknown as O
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const record = obj as Record<string | number, unknown>
|
|
367
|
-
|
|
368
|
-
if (depth === pathSegments.length - 1) {
|
|
369
|
-
return updateShallowProperty(record, currentKey, value) as unknown as O
|
|
355
|
+
result[key] = value
|
|
356
|
+
return result as unknown as O
|
|
370
357
|
}
|
|
371
358
|
|
|
372
359
|
const nextDepth = depth + 1
|
|
373
360
|
const nextKey = pathSegments[nextDepth]
|
|
361
|
+
const source = isArray ? (obj as unknown[])[key as number] : (obj as Record<string | number, unknown>)[key]
|
|
362
|
+
|
|
363
|
+
const nextValue = ensureContainer(source, nextKey)
|
|
374
364
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
365
|
+
if (isArray) {
|
|
366
|
+
const copy = [
|
|
367
|
+
...(obj as unknown[]),
|
|
368
|
+
]
|
|
369
|
+
copy[key as number] = setValueAtPath(nextValue, pathSegments, nextDepth, value)
|
|
370
|
+
return copy as unknown as O
|
|
378
371
|
}
|
|
379
372
|
|
|
380
373
|
const result = {
|
|
381
|
-
...
|
|
374
|
+
...(obj as Record<string | number, unknown>),
|
|
382
375
|
}
|
|
383
|
-
result[
|
|
376
|
+
result[key] = setValueAtPath(nextValue, pathSegments, nextDepth, value)
|
|
384
377
|
return result as unknown as O
|
|
385
378
|
}
|
|
386
379
|
|
|
387
380
|
const createLens = <T, K>(source: State<T>, accessor: (state: T) => K): State<K> => {
|
|
388
381
|
let isUpdating = false
|
|
389
382
|
|
|
390
|
-
const extractPath = ():
|
|
391
|
-
const pathCollector:
|
|
383
|
+
const extractPath = (): string[] => {
|
|
384
|
+
const pathCollector: string[] = []
|
|
392
385
|
let tainted = false
|
|
393
386
|
const proxy = new Proxy(
|
|
394
387
|
{},
|
|
395
388
|
{
|
|
396
389
|
get: (_: object, prop: string | symbol): unknown => {
|
|
397
|
-
if (!tainted &&
|
|
390
|
+
if (!tainted && typeof prop === 'string') {
|
|
398
391
|
if (DANGEROUS_KEYS.has(String(prop))) {
|
|
399
392
|
tainted = true
|
|
400
393
|
} else {
|