@zakkster/lite-signal 1.1.1 → 1.1.2
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 +19 -24
- package/Signal.d.ts +20 -20
- package/Signal.js +74 -33
- package/llms.txt +11 -2
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -476,7 +476,7 @@ The "0 per fire" property for `watch` is deliberate engineering — the inner `u
|
|
|
476
476
|
|
|
477
477
|
### Tree-shaking
|
|
478
478
|
|
|
479
|
-
All three primitives live in a separate module (`
|
|
479
|
+
All three primitives live in a separate module (`Watch.js`) and are re-exported from the main entry (which binds them to its own `effect`/`untrack`, so there is exactly one engine instance). If your bundle doesn't import them, they won't appear in the output — modern ESM tree-shaking (Vite, Rollup, esbuild) handles this reliably.
|
|
480
480
|
|
|
481
481
|
---
|
|
482
482
|
|
|
@@ -537,7 +537,7 @@ Three tiers, all reproducible.
|
|
|
537
537
|
|
|
538
538
|
### Tier 1 — Behavior (unit tests, fast)
|
|
539
539
|
|
|
540
|
-
`npm test` runs the suite in `test
|
|
540
|
+
`npm test` runs the suite in `test/`, covering:
|
|
541
541
|
|
|
542
542
|
- **`01-core.test.mjs`** — signal/computed/effect basics, equality semantics, NaN/±0, subscribe/peek/update, untrack, batch, cleanup ordering, first-run error recovery, nested object reference-identity gotchas.
|
|
543
543
|
- **`02-topology.test.mjs`** — diamond glitch-freedom, 256-deep and 1024-deep computed chains, wide fan-out (1000 effects from one signal), dynamic dependency switching, conditional fan-out, nested effects, cycle detection (`CycleError`).
|
|
@@ -705,11 +705,19 @@ function spawnPlugin(pluginCode) {
|
|
|
705
705
|
|
|
706
706
|
## Conformance
|
|
707
707
|
|
|
708
|
-
lite-signal
|
|
708
|
+
lite-signal is evaluated against the
|
|
709
709
|
[reactive-framework-test-suite](https://github.com/johnsoncodehk/reactive-framework-test-suite),
|
|
710
710
|
the most comprehensive behavioral test battery for JavaScript reactive
|
|
711
711
|
libraries.
|
|
712
712
|
|
|
713
|
+
As of **v1.1.2**, the conformance items that were open at v1.1.0 — batch revert
|
|
714
|
+
detection (#123 / #132 / #147), throw isolation in flush (#121), and
|
|
715
|
+
inner-write propagation through computed chains (#180 / #213) — are **closed**
|
|
716
|
+
(landed in v1.1.1). The remaining gaps are one deliberate design choice (#179,
|
|
717
|
+
below) and the owner-tree items (#209 / #210), which land with the v1.2
|
|
718
|
+
ownership hybrid. The exact post-1.1.1 pass count is being re-run against the
|
|
719
|
+
upstream suite; per-test results and the runner adapter live in `/conformance/`.
|
|
720
|
+
|
|
713
721
|
**174 of 177 tests pass (98.3%)**, placing lite-signal **in a tie for second place of sixteen**
|
|
714
722
|
evaluated libraries — just behind alien-signals (177).
|
|
715
723
|
|
|
@@ -732,23 +740,9 @@ more useful to library users than a green checkmark.
|
|
|
732
740
|
- **Auto-unsubscribe** on first-run effect throws — matches preact, reatom,
|
|
733
741
|
solid. Half the field leaks the subscription.
|
|
734
742
|
|
|
735
|
-
### What
|
|
736
|
-
|
|
737
|
-
10 tests fail. We've categorized them by intent.
|
|
738
|
-
|
|
739
|
-
**Targeted for v1.2** (6 tests):
|
|
743
|
+
### What lite-signal does NOT do yet
|
|
740
744
|
|
|
741
|
-
|
|
742
|
-
`batch()` that net to no change still mark dependents as dirty. Vue,
|
|
743
|
-
Solid, Mobx, and roughly half the field share this behavior. v1.2 will
|
|
744
|
-
capture pre-batch values per signal and skip propagation on revert.
|
|
745
|
-
- **Throw isolation in batch flush** (#121): if an effect throws during
|
|
746
|
-
flush, lite-signal currently halts the flush. v1.2 will collect errors,
|
|
747
|
-
finish the flush, then re-throw as `AggregateError`.
|
|
748
|
-
- **Inner-write propagation through computed chains** (#180, #213): two
|
|
749
|
-
specific propagation paths where lite-signal disagrees with the field.
|
|
750
|
-
Both are propagation-order bugs in the recursive computed resolver,
|
|
751
|
-
not zero-GC tradeoffs.
|
|
745
|
+
The remaining open items, by intent.
|
|
752
746
|
|
|
753
747
|
**Design choices we will not change** (2 tests):
|
|
754
748
|
|
|
@@ -760,13 +754,14 @@ more useful to library users than a green checkmark.
|
|
|
760
754
|
effect's own implicit batching. Most libraries behave this way. Wrap the
|
|
761
755
|
batch outside the effect for the intended semantics.
|
|
762
756
|
|
|
763
|
-
**
|
|
757
|
+
**Landing in v1.2** (2 tests):
|
|
764
758
|
|
|
765
759
|
- **Solid-style cascading disposal of nested effects** (#209, #210):
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
tansu, and Solid 1.x.
|
|
769
|
-
|
|
760
|
+
the baseline 1.1.x engine maintains no owner tree of parent-child
|
|
761
|
+
effects — matching preact, vue, mobx, the TC39 polyfill, Angular,
|
|
762
|
+
Svelte, tansu, and Solid 1.x. The v1.2 ownership hybrid adds an
|
|
763
|
+
owner tree so nested effects/computeds auto-dispose with their parent;
|
|
764
|
+
the #209 / #210 conformance tests are wired and skipped until then.
|
|
770
765
|
|
|
771
766
|
Per-test results, the runner adapter, and reproductions live in
|
|
772
767
|
`/conformance/`.
|
package/Signal.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @zakkster/lite-signal — zero-GC reactive graph.
|
|
3
3
|
*
|
|
4
|
-
* Public type surface for the JavaScript implementation in `
|
|
4
|
+
* Public type surface for the JavaScript implementation in `Signal.js`.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
// ─── Options ──────────────────────────────────────────────────────────────────
|
|
@@ -115,7 +115,7 @@ export interface RegistryConfig {
|
|
|
115
115
|
* - `"grow"`: double the pool. Links are bounded by `maxLinks * 16`.
|
|
116
116
|
*/
|
|
117
117
|
onCapacityExceeded?: "throw" | "grow";
|
|
118
|
-
/** Max effect-queue drain passes before a
|
|
118
|
+
/** Max effect-queue drain passes before a flush-cycle `Error` (message prefixed `"CycleError:"`) is thrown. Default: 100. */
|
|
119
119
|
maxFlushPasses?: number;
|
|
120
120
|
}
|
|
121
121
|
|
|
@@ -177,24 +177,24 @@ export interface WatchOptions {
|
|
|
177
177
|
immediate?: boolean;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
|
|
181
|
-
* Track a reactive source and run a callback whenever its projected value
|
|
182
|
-
* changes. The callback receives `(newValue, oldValue, stop)` — the third
|
|
183
|
-
* argument is a dispose function that can be called from inside the callback
|
|
184
|
-
* to terminate the watcher.
|
|
185
|
-
*
|
|
186
|
-
* Internal reads inside the callback are untracked.
|
|
187
|
-
*
|
|
188
|
-
* Uses `Object.is` to guard against the raw-getter case where a dep mutation
|
|
189
|
-
* fires the effect but the projected value is unchanged.
|
|
190
|
-
*
|
|
191
|
-
* @param source Reactive read function.
|
|
192
|
-
* @param callback Called with the new and previous values plus a stop handle.
|
|
193
|
-
* @param options `immediate: true` runs the callback once on registration
|
|
194
|
-
* with `oldValue = undefined`.
|
|
195
|
-
* @returns Dispose function. Idempotent and safe to call at any time, including
|
|
196
|
-
* synchronously during the immediate callback.
|
|
197
|
-
*/
|
|
180
|
+
/**
|
|
181
|
+
* Track a reactive source and run a callback whenever its projected value
|
|
182
|
+
* changes. The callback receives `(newValue, oldValue, stop)` — the third
|
|
183
|
+
* argument is a dispose function that can be called from inside the callback
|
|
184
|
+
* to terminate the watcher.
|
|
185
|
+
*
|
|
186
|
+
* Internal reads inside the callback are untracked.
|
|
187
|
+
*
|
|
188
|
+
* Uses `Object.is` to guard against the raw-getter case where a dep mutation
|
|
189
|
+
* fires the effect but the projected value is unchanged.
|
|
190
|
+
*
|
|
191
|
+
* @param source Reactive read function.
|
|
192
|
+
* @param callback Called with the new and previous values plus a stop handle.
|
|
193
|
+
* @param options `immediate: true` runs the callback once on registration
|
|
194
|
+
* with `oldValue = undefined`.
|
|
195
|
+
* @returns Dispose function. Idempotent and safe to call at any time, including
|
|
196
|
+
* synchronously during the immediate callback.
|
|
197
|
+
*/
|
|
198
198
|
export function watch<T>(
|
|
199
199
|
source: () => T,
|
|
200
200
|
callback: (newValue: T, oldValue: T | undefined, stop: () => void) => void,
|
package/Signal.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @zakkster/lite-signal
|
|
2
|
+
* @zakkster/lite-signal v1.1.2
|
|
3
3
|
* --------------------
|
|
4
4
|
* Zero-GC reactive graph.
|
|
5
5
|
*
|
|
@@ -143,10 +143,10 @@ export function createRegistry(config = {}) {
|
|
|
143
143
|
const NODE_PTR = Symbol("node_ptr");
|
|
144
144
|
const NODE_GEN = Symbol("node_gen");
|
|
145
145
|
|
|
146
|
-
let currentNodesCapacity = config.maxNodes
|
|
147
|
-
let currentLinkCapacity = config.maxLinks
|
|
148
|
-
const policy = config.onCapacityExceeded
|
|
149
|
-
const maxFlushPasses = config.maxFlushPasses
|
|
146
|
+
let currentNodesCapacity = config.maxNodes ?? 1024;
|
|
147
|
+
let currentLinkCapacity = config.maxLinks ?? currentNodesCapacity * 4;
|
|
148
|
+
const policy = config.onCapacityExceeded ?? "throw";
|
|
149
|
+
const maxFlushPasses = config.maxFlushPasses ?? 100;
|
|
150
150
|
const maxLinkLimit = currentLinkCapacity * 16;
|
|
151
151
|
|
|
152
152
|
// --- ZERO-GC OBJECT POOLS ---
|
|
@@ -440,19 +440,21 @@ export function createRegistry(config = {}) {
|
|
|
440
440
|
* @private
|
|
441
441
|
*/
|
|
442
442
|
function markDownstream(startNode) {
|
|
443
|
-
let stackLen = 0
|
|
444
|
-
markStack[stackLen] = startNode;
|
|
445
|
-
stackLen = (stackLen + 1) | 0;
|
|
443
|
+
let stackLen = 0;
|
|
444
|
+
markStack[stackLen++] = startNode;
|
|
446
445
|
|
|
447
|
-
while (stackLen
|
|
448
|
-
|
|
449
|
-
const n = markStack[stackLen];
|
|
446
|
+
while (stackLen !== 0) {
|
|
447
|
+
const n = markStack[--stackLen];
|
|
450
448
|
|
|
451
449
|
let link = n.headSub;
|
|
452
450
|
while (link !== null) {
|
|
453
451
|
const t = link.target;
|
|
454
452
|
if ((t.markEpoch | 0) !== (globalVersion | 0)) {
|
|
455
453
|
t.markEpoch = globalVersion | 0;
|
|
454
|
+
// flags read stays INSIDE the markEpoch guard on purpose: hoisting it
|
|
455
|
+
// above the guard would load t.flags for every already-marked revisit
|
|
456
|
+
// too, adding work on exactly the dedup-skip path that markEpoch exists
|
|
457
|
+
// to make cheap (diamond / shared-computed fan-out).
|
|
456
458
|
const flags = t.flags | 0;
|
|
457
459
|
|
|
458
460
|
if ((flags & FLAG_EFFECT) !== 0) {
|
|
@@ -461,14 +463,12 @@ export function createRegistry(config = {}) {
|
|
|
461
463
|
// writes (directly or through computed chains). The effect's evalVersion
|
|
462
464
|
// is bumped to the post-write globalVersion in its executeEffect finally,
|
|
463
465
|
// so subsequent unrelated writes still propagate normally.
|
|
464
|
-
if ((flags & FLAG_QUEUED
|
|
466
|
+
if ((flags & (FLAG_QUEUED | FLAG_COMPUTING)) === 0) {
|
|
465
467
|
t.flags = flags | FLAG_QUEUED;
|
|
466
|
-
activeQueue[activeQueueLen] = t;
|
|
467
|
-
activeQueueLen = (activeQueueLen + 1) | 0;
|
|
468
|
+
activeQueue[activeQueueLen++] = t;
|
|
468
469
|
}
|
|
469
470
|
} else {
|
|
470
|
-
markStack[stackLen] = t;
|
|
471
|
-
stackLen = (stackLen + 1) | 0;
|
|
471
|
+
markStack[stackLen++] = t;
|
|
472
472
|
}
|
|
473
473
|
}
|
|
474
474
|
link = link.nextSub;
|
|
@@ -732,14 +732,28 @@ export function createRegistry(config = {}) {
|
|
|
732
732
|
* Equality predicate. Returning true short-circuits notification.
|
|
733
733
|
* @returns {Signal<T>}
|
|
734
734
|
*/
|
|
735
|
-
function signal(initial, opts
|
|
735
|
+
function signal(initial, opts) {
|
|
736
736
|
const node = createNode(initial, FLAG_SIGNAL);
|
|
737
|
-
|
|
737
|
+
// Read opts defensively instead of defaulting to `= {}`: the default allocates
|
|
738
|
+
// a throwaway object on every no-opts call (the common path) — pure nursery
|
|
739
|
+
// garbage when mounting many signals.
|
|
740
|
+
node.equals = (opts !== undefined && opts.equals !== undefined) ? opts.equals : Object.is;
|
|
738
741
|
node.version = globalVersion | 0;
|
|
739
742
|
statSignals = (statSignals + 1) | 0;
|
|
740
743
|
|
|
741
744
|
const read = () => {
|
|
742
|
-
if (isTrackingDeps && currentObserver !== null)
|
|
745
|
+
if (isTrackingDeps && currentObserver !== null) {
|
|
746
|
+
// Inlined cursor fast-path: on stable read order this matches every
|
|
747
|
+
// time, so we skip a call into the (large, non-inlinable) allocateLink
|
|
748
|
+
// frame entirely. Only a cursor miss falls through to the cold path,
|
|
749
|
+
// where allocateLink re-reads the cursor for its relink logic.
|
|
750
|
+
const expected = activeObserverCurrentDep;
|
|
751
|
+
if (expected !== null && expected.source === node) {
|
|
752
|
+
activeObserverCurrentDep = expected.nextDep;
|
|
753
|
+
} else {
|
|
754
|
+
allocateLink(node, currentObserver);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
743
757
|
return node.value;
|
|
744
758
|
};
|
|
745
759
|
read.peek = () => node.value;
|
|
@@ -774,11 +788,21 @@ export function createRegistry(config = {}) {
|
|
|
774
788
|
read.update = (fn) => read.set(fn(node.value));
|
|
775
789
|
|
|
776
790
|
read.subscribe = (fn) => {
|
|
777
|
-
|
|
778
|
-
|
|
791
|
+
// Single-closure subscription: the read is tracked (it establishes the
|
|
792
|
+
// dependency), then fn runs untracked. untrack() is inlined here to (a) drop
|
|
793
|
+
// the second per-subscription closure and (b) save an untrack()+wrapper call
|
|
794
|
+
// on every fire, not just at setup.
|
|
795
|
+
// COUPLING: this duplicates untrack()'s isTrackingDeps save/restore. If
|
|
796
|
+
// untrack ever manages additional state (e.g. currentObserver), mirror it here.
|
|
779
797
|
return effect(() => {
|
|
780
|
-
|
|
781
|
-
|
|
798
|
+
const val = read();
|
|
799
|
+
const prevTracking = isTrackingDeps;
|
|
800
|
+
isTrackingDeps = false;
|
|
801
|
+
try {
|
|
802
|
+
fn(val);
|
|
803
|
+
} finally {
|
|
804
|
+
isTrackingDeps = prevTracking;
|
|
805
|
+
}
|
|
782
806
|
});
|
|
783
807
|
};
|
|
784
808
|
|
|
@@ -800,24 +824,39 @@ export function createRegistry(config = {}) {
|
|
|
800
824
|
* Equality predicate. Returning true blocks propagation downstream.
|
|
801
825
|
* @returns {Computed<T>}
|
|
802
826
|
*/
|
|
803
|
-
function computed(fn, opts
|
|
827
|
+
function computed(fn, opts) {
|
|
804
828
|
const node = createNode(undefined, FLAG_COMPUTED);
|
|
805
829
|
node.computeFn = fn;
|
|
806
|
-
|
|
830
|
+
// Defensive opts read; avoids the `= {}` per-call allocation. See signal().
|
|
831
|
+
node.equals = (opts !== undefined && opts.equals !== undefined) ? opts.equals : Object.is;
|
|
807
832
|
statComputeds = (statComputeds + 1) | 0;
|
|
808
833
|
|
|
809
834
|
const read = () => {
|
|
810
|
-
if (isTrackingDeps && currentObserver !== null)
|
|
835
|
+
if (isTrackingDeps && currentObserver !== null) {
|
|
836
|
+
// Inlined cursor fast-path — see signal() read for rationale.
|
|
837
|
+
const expected = activeObserverCurrentDep;
|
|
838
|
+
if (expected !== null && expected.source === node) {
|
|
839
|
+
activeObserverCurrentDep = expected.nextDep;
|
|
840
|
+
} else {
|
|
841
|
+
allocateLink(node, currentObserver);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
811
844
|
return pullComputed(node);
|
|
812
845
|
};
|
|
813
846
|
read.peek = () => pullComputed(node);
|
|
814
847
|
|
|
815
848
|
read.subscribe = (fn) => {
|
|
816
|
-
|
|
817
|
-
|
|
849
|
+
// Single-closure subscription — see signal() subscribe for rationale and
|
|
850
|
+
// the untrack-inlining coupling note.
|
|
818
851
|
return effect(() => {
|
|
819
|
-
|
|
820
|
-
|
|
852
|
+
const val = read();
|
|
853
|
+
const prevTracking = isTrackingDeps;
|
|
854
|
+
isTrackingDeps = false;
|
|
855
|
+
try {
|
|
856
|
+
fn(val);
|
|
857
|
+
} finally {
|
|
858
|
+
isTrackingDeps = prevTracking;
|
|
859
|
+
}
|
|
821
860
|
});
|
|
822
861
|
};
|
|
823
862
|
|
|
@@ -842,13 +881,15 @@ export function createRegistry(config = {}) {
|
|
|
842
881
|
* @returns {() => void} Dispose function. Idempotent. Safe to call
|
|
843
882
|
* after registry.destroy().
|
|
844
883
|
*/
|
|
845
|
-
function effect(fn, opts
|
|
884
|
+
function effect(fn, opts) {
|
|
846
885
|
const node = createNode(undefined, FLAG_EFFECT);
|
|
847
886
|
node.computeFn = fn;
|
|
848
|
-
|
|
887
|
+
// Defensive opts read; avoids the `= {}` per-call allocation. Read scheduler once
|
|
888
|
+
// and reuse the local for the trampoline check below.
|
|
889
|
+
const scheduler = opts !== undefined ? opts.scheduler : undefined;
|
|
890
|
+
node.scheduler = scheduler;
|
|
849
891
|
statEffects = (statEffects + 1) | 0;
|
|
850
892
|
|
|
851
|
-
const scheduler = opts.scheduler;
|
|
852
893
|
let firstRunError = null;
|
|
853
894
|
if (scheduler) {
|
|
854
895
|
const gen = node.gen | 0;
|
package/llms.txt
CHANGED
|
@@ -46,6 +46,15 @@ triggering write — no `queueMicrotask`, no promises, no scheduler ticks.
|
|
|
46
46
|
- Stable read order: O(1) per dep via cursor reuse.
|
|
47
47
|
- Chaotic/randomized read order: degrades to O(N) per dep due to list re-insertion.
|
|
48
48
|
|
|
49
|
+
## Version notes
|
|
50
|
+
|
|
51
|
+
- **1.1.2** (current): hot-path micro-optimizations, no behavior/API change.
|
|
52
|
+
Inlined cursor fast-path in `signal`/`computed` reads (stable-order reads skip
|
|
53
|
+
the `allocateLink` call); zero-allocation creation path (`opts` read defensively
|
|
54
|
+
instead of defaulting to `{}`); single-closure `subscribe`. Owner-tree
|
|
55
|
+
conformance items #209/#210 are wired but skipped pending the v1.2 ownership
|
|
56
|
+
hybrid.
|
|
57
|
+
|
|
49
58
|
## When to use
|
|
50
59
|
|
|
51
60
|
- Animation loops, game HUDs, scoreboards, telemetry overlays.
|
|
@@ -232,7 +241,7 @@ sandbox.destroy(); // entire reactive world reset
|
|
|
232
241
|
|
|
233
242
|
## File layout
|
|
234
243
|
|
|
235
|
-
- `Signal.js` — full implementation,
|
|
244
|
+
- `Signal.js` — full implementation, single file.
|
|
236
245
|
- `Signal.d.ts` — TypeScript declarations for all public API.
|
|
237
246
|
- `test/01-core.test.mjs` — signal/computed/effect basics, equality, untrack.
|
|
238
247
|
- `test/02-topology.test.mjs` — diamonds, chains, fan-out/in, cycle detection.
|
|
@@ -243,7 +252,7 @@ sandbox.destroy(); // entire reactive world reset
|
|
|
243
252
|
- `test/07-dispose.test.mjs` — universal disposal: registry.dispose(api).
|
|
244
253
|
- `test/08-watch.test.mjs` — new watch reactivity tests.
|
|
245
254
|
- `test/09-conformance.test.mjs` — johnsoncodehk/reactive-framework-test-suite conformance fixes tests.
|
|
246
|
-
- `bench/
|
|
255
|
+
- `bench/benchmark.mjs` — comparative benchmark vs alien-signals, preact, solid.
|
|
247
256
|
- `demo/index.html` — interactive visualization of the reactive graph.
|
|
248
257
|
|
|
249
258
|
## Install
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zakkster/lite-signal",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Zero-GC reactive graph. Monomorphic object pool, versioned push-pull propagation, 32-bit modular versioning. Built for hot paths and long-running processes.",
|
|
5
5
|
"author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
"types": "./Signal.d.ts",
|
|
11
11
|
"exports": {
|
|
12
12
|
".": {
|
|
13
|
-
"
|
|
13
|
+
"node": "./Signal.js",
|
|
14
14
|
"import": "./Signal.js",
|
|
15
|
+
"types": "./Signal.d.ts",
|
|
15
16
|
"default": "./Signal.js"
|
|
16
17
|
}
|
|
17
18
|
},
|
|
@@ -27,6 +28,7 @@
|
|
|
27
28
|
"test": "node --test --test-reporter=spec",
|
|
28
29
|
"test:gc": "node --expose-gc --test --test-reporter=spec",
|
|
29
30
|
"bench": "node --expose-gc bench/benchmark.mjs",
|
|
31
|
+
"bench-reactive": "node --expose-gc bench/benchmarkReactive.mjs",
|
|
30
32
|
"verify": "npm test && npm run bench"
|
|
31
33
|
},
|
|
32
34
|
"keywords": [
|