@esportsplus/reactivity 0.31.3 → 0.32.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/.claude/CHANGELOG.md +43 -0
- package/README.md +13 -6
- package/bench/cellx.bench.ts +61 -0
- package/bench/dynamic.bench.ts +103 -0
- package/bench/kairo.bench.ts +368 -0
- package/bench/lib/reactive-adapter.ts +51 -0
- package/bench/molbench.bench.ts +75 -0
- package/{tests/bench/system.ts → bench/system.bench.ts} +3 -3
- package/build/compiler/array.d.ts +1 -1
- package/build/compiler/constants.d.ts +7 -6
- package/build/compiler/constants.js +6 -7
- package/build/compiler/object.d.ts +1 -1
- package/build/compiler/object.js +1 -2
- package/build/compiler/primitives.d.ts +1 -1
- package/build/constants.d.ts +3 -1
- package/build/constants.js +3 -1
- package/build/reactive/array.d.ts +8 -5
- package/build/reactive/array.js +14 -14
- package/build/reactive/index.d.ts +2 -2
- package/build/reactive/index.js +1 -1
- package/build/reactive/object.d.ts +4 -4
- package/build/reactive/object.js +8 -23
- package/build/system.d.ts +15 -6
- package/build/system.js +415 -69
- package/build/types.d.ts +16 -4
- package/package.json +5 -2
- package/src/compiler/array.ts +1 -1
- package/src/compiler/constants.ts +8 -6
- package/src/compiler/index.ts +2 -1
- package/src/compiler/object.ts +2 -2
- package/src/compiler/plugins/vite.ts +1 -0
- package/src/compiler/primitives.ts +1 -1
- package/src/constants.ts +6 -2
- package/src/reactive/array.ts +26 -23
- package/src/reactive/index.ts +6 -6
- package/src/reactive/object.ts +16 -41
- package/src/system.ts +612 -80
- package/src/types.ts +29 -9
- package/test/async-computed.test.ts +323 -0
- package/test/async-errors.test.ts +146 -0
- package/test/async-hardening.test.ts +73 -0
- package/test/async-iterable.test.ts +221 -0
- package/test/async-nested.test.ts +19 -0
- package/test/deep-graphs.test.ts +215 -0
- package/{tests/effects.ts → test/effects.test.ts} +60 -0
- package/test/equals.test.ts +142 -0
- package/test/errors.test.ts +201 -0
- package/test/flush.test.ts +185 -0
- package/test/glitch-freedom.test.ts +115 -0
- package/test/invalidate.test.ts +147 -0
- package/test/lib/wait-for.ts +28 -0
- package/test/pending-writes.test.ts +139 -0
- package/{tests/reactive.ts → test/reactive/reactive.test.ts} +20 -19
- package/test/read-dedup.test.ts +120 -0
- package/test/signal-selector.test.ts +187 -0
- package/{tests/system.ts → test/system.test.ts} +214 -23
- package/test/tsconfig.json +16 -0
- package/test/untrack.test.ts +146 -0
- package/vitest.config.ts +2 -2
- package/tests/async-computed.ts +0 -239
- /package/{tests/bench/array.ts → bench/reactive/array.bench.ts} +0 -0
- /package/{tests/bench/reactive-object.ts → bench/reactive/reactive-object.bench.ts} +0 -0
- /package/{tests → bench}/tsconfig.json +0 -0
- /package/{tests/compiler.ts → test/compiler/compiler.test.ts} +0 -0
- /package/{tests/primitives.ts → test/primitives.test.ts} +0 -0
- /package/{tests/array.ts → test/reactive/array.test.ts} +0 -0
- /package/{tests/nested.ts → test/reactive/nested.test.ts} +0 -0
- /package/{tests/objects.ts → test/reactive/objects.test.ts} +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Completed
|
|
4
|
+
- [relocate-benchmarks] 567bd82. Relocate 7 benches to bench/ mirror + config, delete tests/ — landed by direct-implement after blocked run 36ee71d0 (harness gate false-block: depless worktrees, issue 20260720T141515Z). Gate: pnpm agent:test green (tsc --noEmit + 369 tests), benches discover 7. · spec: spec-coding-standards
|
|
5
|
+
- [relocate-test-suites] 26fa1017. Relocate 23 suites to test/ mirror + config — landed by direct-implement after blocked run 36ee71d0 (harness gate false-block: depless worktrees, issue 20260720T141515Z). Gate: pnpm agent:test green (tsc --noEmit + 369 tests), benches discover 7. · spec: spec-coding-standards
|
|
6
|
+
- [waitfor-test-helper] fa0a934a. waitFor/tick helper replacing 50 fixed-sleep sync sites — landed by direct-implement after blocked run 36ee71d0 (harness gate false-block: depless worktrees, issue 20260720T141515Z). Gate: pnpm agent:test green (tsc --noEmit + 369 tests), benches discover 7. · spec: spec-coding-standards
|
|
7
|
+
- [compiler-import-order] 2610d50d. Normalize import order across compiler modules — landed by direct-implement after blocked run 36ee71d0 (harness gate false-block: depless worktrees, issue 20260720T141515Z). Gate: pnpm agent:test green (tsc --noEmit + 369 tests), benches discover 7. · spec: spec-coding-standards
|
|
8
|
+
- [reactive-index-cleanup] 00b21810. reactive() facade unknown-bridges + import order — landed by direct-implement after blocked run 36ee71d0 (harness gate false-block: depless worktrees, issue 20260720T141515Z). Gate: pnpm agent:test green (tsc --noEmit + 369 tests), benches discover 7. · spec: spec-coding-standards
|
|
9
|
+
- [reactive-array-cleanup] e0aa8b86. ReactiveArray listener typing, sort map, accessor order, imports — landed by direct-implement after blocked run 36ee71d0 (harness gate false-block: depless worktrees, issue 20260720T141515Z). Gate: pnpm agent:test green (tsc --noEmit + 369 tests), benches discover 7. · spec: spec-coding-standards
|
|
10
|
+
- [system-any-casts] b890f047. Eliminate any casts in system.ts (unknown-bridged pool sentinels) — landed by direct-implement after blocked run 36ee71d0 (harness gate false-block: depless worktrees, issue 20260720T141515Z). Gate: pnpm agent:test green (tsc --noEmit + 369 tests), benches discover 7. · spec: spec-coding-standards
|
|
11
|
+
- abandoned spend: ~$199.51 equivalent API cost across 7 terminal non-COMPLETED item(s) (system-any-casts, reactive-array-cleanup, reactive-index-cleanup, compiler-import-order, waitfor-test-helper, relocate-test-suites, relocate-benchmarks); recorded, not divided into per-item costs (run 36ee71d0)
|
|
12
|
+
- run-level spend: ~$0.98 equivalent API cost (warm implementer runs + unit-scoped + boundary seats; never divided across items; blended rate = mean(input, output, cache_read, cache_creation) per million tokens) (run 36ee71d0)
|
|
13
|
+
- [constants-export-order] 84f0b3613de0076e070ba4ddcd69c3e0d68a9f3a. Deviations: none; ladder: 3 attempts (critic → replanner completed → critic), completed. Cost: ~$20.03 equivalent API cost (blended-rate approximation). · spec: spec-coding-standards
|
|
14
|
+
- [compiler-const-enum] 1b252cd2180063eefc04bf2b221ce4e9eff969a5. Deviations: none; ladder: 3 attempts (critic → replanner completed → critic), completed. Cost: ~$34.13 equivalent API cost (blended-rate approximation). · spec: spec-coding-standards
|
|
15
|
+
- [types-variadic-any] da6b4873d4238db26d0d75f19a67f8c966a01b83. Deviations: none; ladder: 3 attempts (critic → replanner completed → critic), completed. Cost: ~$47.04 equivalent API cost (blended-rate approximation). · spec: spec-coding-standards
|
|
16
|
+
- [reactive-object-cleanup] 769d007e46a9f6b0b299c2c3d48fa55dffe29628. Deviations: none; ladder: 3 attempts (critic → replanner completed → critic), completed. Cost: ~$32.61 equivalent API cost (blended-rate approximation). · spec: spec-coding-standards
|
|
17
|
+
- cost source of truth: ccusage per-model breakdown (local transcripts carry the input/output/cache split the run journal does not) — figures above are a blended-rate approximation per contracts/models.json pricing (run 36ee71d0)
|
|
18
|
+
- [recursion-free-walks] 143d5591. Recursion-free notify(), update(), and dispose()/unlink() walks — reconciled (killed-run recovery). Deviations: unrecorded (the run died before its boundary). Cost: ~$UNMEASURED (manual pre-run, no journal)~ · spec: spec-signals-next-2
|
|
19
|
+
- [unobserved-hooks] b7a93aa4. onUnobserved() — last-subscriber lifecycle hook — reconciled (killed-run recovery). Deviations: unrecorded (the run died before its boundary). Cost: ~$UNMEASURED (manual pre-run, no journal)~ · spec: spec-signals-next-2
|
|
20
|
+
- [invalidate] 60df87ae. invalidate() — standalone re-derivation of any computed — reconciled (killed-run recovery). Deviations: unrecorded (the run died before its boundary). Cost: ~$UNMEASURED (manual pre-run, no journal)~ · spec: spec-signals-next-2
|
|
21
|
+
- [custom-equals] a4cb4e04. Custom equals on signal() and computed() — reconciled (killed-run recovery). Deviations: unrecorded (the run died before its boundary). Cost: ~$UNMEASURED (manual pre-run, no journal)~ · spec: spec-signals-next-2
|
|
22
|
+
- follow-up discharged (completed) from spec-signals-next: pending-only-writes: complete full shape — selector recursive fan-out + nextPend… — evidence: 6a9b8e6
|
|
23
|
+
- abandoned spend: ~$206.41 equivalent API cost across 4 terminal non-COMPLETED item(s) (custom-equals, invalidate, unobserved-hooks, recursion-free-walks); recorded, not divided into per-item costs (run 3306873d)
|
|
24
|
+
- run-level spend: ~$2.11 equivalent API cost (warm implementer runs + unit-scoped + boundary seats; never divided across items; blended rate = mean(input, output, cache_read, cache_creation) per million tokens) (run 3306873d)
|
|
25
|
+
- [async-iterable-resolve] 17b3878753d238869fdf6723dff468705d2795f8. Deviations: deviated — resolve()'s 'rejects when the tracked expression throws' was split into its own it() rather than combined into one test with the resolve-success case, matching the file's one-scenario-per-it convention from tests/async-hardening.ts; both scenarios from Design test 6 are covered.; added — Verified async generator .return() timing empirically (a queued return() on a generator suspended mid an unresolved internal await does not settle until that await resolves) via a throwaway Node script before writing tests 1-3, since the Design's prose doesn't spell out that an abandoned generator's pending gate must be released for its finally to run; tests 1-3 release the old generation's gate to observe closure, consistent with test 3's own description.; added — Added a gate()/tick() test-local helper pair (not in Design) to keep the generator-gating boilerplate readable across tests 1-5; mirrors async-hardening.ts's resolver-array convention.; ladder: 1 attempt (critic), completed. Cost: ~$16.19 equivalent API cost (blended-rate approximation). · spec: spec-signals-next-2
|
|
26
|
+
- cost source of truth: ccusage per-model breakdown (local transcripts carry the input/output/cache split the run journal does not) — figures above are a blended-rate approximation per contracts/models.json pricing (run 3306873d)
|
|
27
|
+
- abandoned spend: ~$229.74 equivalent API cost across 5 terminal non-COMPLETED item(s) (custom-equals, invalidate, unobserved-hooks, async-iterable-resolve, recursion-free-walks); recorded, not divided into per-item costs (run 2f191933)
|
|
28
|
+
- run-level spend: ~$0.56 equivalent API cost (warm implementer runs + unit-scoped + boundary seats; never divided across items; blended rate = mean(input, output, cache_read, cache_creation) per million tokens) (run 2f191933)
|
|
29
|
+
- [untrack-peek] 5d2a577c2b941bdc6fe869951c770ed3c12a82f8. Deviations: deviated — pull(node) extracted only the notified-broadcast + observer-nulled update() call (matching design's literal extraction target); drainPending() and the height>=heap_i||NOTIFY_MASK gate stay inline in read() exactly as before for byte-identical behavior.; deviated — peek() has no separate explicit NOTIFY_MASK pre-check before calling pull() — it always drains pending writes then calls pull(node) unconditionally for a STATE_COMPUTED node, relying on pull()'s internal notify-broadcast (which sets DIRTY/CHECK bits) plus update()'s own gv/CHECK/DIRTY branching to no-op cheaply when nothing changed. A literal external NOTIFY_MASK gate in peek() would check the bit *before* the broadcast that sets it, which never fires on a freshly-written-but-undrained dependency and would make peek() return stale values, contradicting Acceptance clause 3's 'up-to-date value for a dirty computed'. Verified via a dedicated test that peek() sees a fresh value synchronously right after write(), with 0 regressions across the full 343-test suite.; ladder: 1 attempt (critic), completed. Cost: ~$20.51 equivalent API cost (blended-rate approximation). · spec: spec-signals-next
|
|
30
|
+
- [flush-batch] 3bc415d5568da53154f59a32069afaebfc381d04. Deviations: deviated — flush() implemented as `while (stabilizer === STABILIZER_SCHEDULED) stabilize();` instead of the Design's single `if`. Traced the RESCHEDULE path: a write() during an effect's recompute only flips stabilizer to RESCHEDULE and queues the *next* microtask when its subscriber's height is at or below the current heap_i already scanned in this pass (drain-before-scan only re-triggers on a later height iteration reached within the same for-loop) — a single stabilize() call leaves that tail unsettled until the next microtask, violating Acceptance clause 1's 'settles before flush() returns'. The while-loop drains it synchronously while still satisfying the no-op cases (RUNNING/RESCHEDULE/IDLE never equal SCHEDULED) and clause 4 (any stray queued microtask fires against an already-empty/IDLE state and no-ops).; ladder: 1 attempt (critic), completed. Cost: ~$11.27 equivalent API cost (blended-rate approximation). · spec: spec-signals-next
|
|
31
|
+
- cost source of truth: ccusage per-model breakdown (local transcripts carry the input/output/cache split the run journal does not) — figures above are a blended-rate approximation per contracts/models.json pricing (run 2f191933)
|
|
32
|
+
- follow-up discharged (completed) from spec-signals-next: read-version-dedup + pending-only-writes: stamp rv (+ nextPending on Signal entr… — evidence: 6a9b8e6
|
|
33
|
+
- [cleanup-hardening] 14a40dd7. cleanup() hardening — run every disposer, never abort the batch — reconciled (killed-run recovery). Deviations: unrecorded (the run died before its boundary). Cost: ~$UNMEASURED (manual pre-run, no journal)~ · spec: spec-signals-next
|
|
34
|
+
- [async-computed-hardening] 033b205f. asyncComputed hardening — dirty-gap guard, isPending, leak test, ReactiveObject reuse — reconciled (killed-run recovery). Deviations: unrecorded (the run died before its boundary). Cost: ~$UNMEASURED (manual pre-run, no journal)~ · spec: spec-signals-next
|
|
35
|
+
- [async-error-propagation] bf8ad5c0. asyncComputed error propagation (rejections surface, ownership fixed) — reconciled (killed-run recovery). Deviations: unrecorded (the run died before its boundary). Cost: ~$UNMEASURED (manual pre-run, no journal)~ · spec: spec-signals-next
|
|
36
|
+
- [computed-error-caching] 79224d08. Computed error caching + propagation (errors never vanish) — reconciled (killed-run recovery). Deviations: unrecorded (the run died before its boundary). Cost: ~$UNMEASURED (manual pre-run, no journal)~ · spec: spec-signals-next
|
|
37
|
+
- [benchmark-harness] 9d6d7b2c. Canonical reactivity benchmark suite + glitch-freedom regression tests — reconciled (killed-run recovery). Deviations: unrecorded (the run died before its boundary). Cost: ~$UNMEASURED (manual pre-run, no journal)~ · spec: spec-signals-next
|
|
38
|
+
- [test-bench-scripts] 524d07bc. Add test/bench script aliases (agent:test, agent:bench) — reconciled (killed-run recovery). Deviations: unrecorded (the run died before its boundary). Cost: ~$UNMEASURED (manual pre-run, no journal)~ · spec: spec-signals-next
|
|
39
|
+
- [read-version-dedup] dcbaae90. O(1) interleaved repeat-read dedup (read-version stamp) — landed by hand (combined perf integration, bench-gated vs main: 0 regressions). Deviations: SelectorSignal entry-literal rv stamp deferred until signal-is-selector lands (superset-shape rule, follow-up); computed object-size assertion reconciled to 15. Cost: ~$UNMEASURED (manual integration, no journal)~ · spec: spec-signals-next
|
|
40
|
+
- [global-version-fast-path] dcbaae90. globalVersion fast path for clean-graph reads — landed by hand (combined perf integration, bench-gated vs main: 0 regressions). Deviations: computed object-size assertion reconciled to 15 (shared bump with read-version-dedup + pending-only-writes). Cost: ~$UNMEASURED (manual integration, no journal)~ · spec: spec-signals-next
|
|
41
|
+
- [pending-only-writes] dcbaae90. Pending-only writes — defer fan-out off the write hot path (reduced-shape core) — landed by hand (combined perf integration, bench-gated vs main: 0 regressions). Deviations: reduced-shape core only (intrusive nextPending queue, drainPending, per-height + read()-preamble drain); full shape (lazy mark-don't-schedule, selector recursive fan-out + nextPending on the SelectorSignal entry, tests/flush.ts invariant 2) deferred to follow-up pending flush-batch + lazy-computeds + signal-is-selector; SelectorSignal entry-literal nextPending/rv stamp deferred until signal-is-selector; computed object-size assertion reconciled to 15. Cost: ~$UNMEASURED (manual integration, no journal)~ · spec: spec-signals-next
|
|
42
|
+
- [signal-is-selector] 6a9b8e63. signal.selector() per-key selector primitive — O(2) write-path fan-out, lazy per-key entries, eviction on last unsub — landed by hand off main (dcbaae9). Deviations: SelectorSignal entry literal pre-declares nextPending + rv (perf superset-shape, discharges follow-up #1); write-path bench informal (feature, not perf) — one predicted-not-taken keys null-check, write-neutral; suite 328→336 (+8 acceptance tests incl. SameValueZero NaN/object-ref caveats); glitch-freedom green. Cost: ~$UNMEASURED (manual, no journal)~ · spec: spec-signals-next
|
|
43
|
+
- [lazy-computeds] REVERTED (no landing sha). Lazy computeds — compute on first read, sleep while unobserved — removed from scope by user decision 2026-07-19: lazy-by-default was implemented + A/B benchmarked and REGRESSED (full-suite ABBA geomean 1.04; read-class 0.77–0.88× eager; steady-read parity disproven by isolation bench). Do not re-attempt lazy-by-default naively; a non-regressing opt-in approach would be a fresh design. Patch preserved at ~/.claude/storage/lazy-bench/lazy-by-default-full.patch. Feature file removed from the spec per user request. Cost: ~$UNMEASURED (manual, no journal)~ · spec: spec-signals-next
|
package/README.md
CHANGED
|
@@ -221,22 +221,27 @@ let user = new ReactiveObject_1();
|
|
|
221
221
|
| `reactive(() => expr)` | Creates a computed value (compile-time only) |
|
|
222
222
|
| `reactive({...})` | Creates a reactive object with signals and computeds |
|
|
223
223
|
| `reactive([...])` | Creates a reactive array |
|
|
224
|
-
| `effect(fn)` | Runs a function that re-executes when dependencies change. Returns a dispose function |
|
|
224
|
+
| `effect(fn, onError?)` | Runs a function that re-executes when dependencies change. Returns a dispose function. Optional `onError` receives thrown or rejected errors |
|
|
225
225
|
| `root(fn)` | Creates an untracked scope. If `fn` accepts an argument, a dispose function is provided |
|
|
226
226
|
| `onCleanup(fn)` | Registers a cleanup function for the current effect/computed |
|
|
227
227
|
|
|
228
228
|
### Low-Level Functions
|
|
229
229
|
|
|
230
|
-
These
|
|
230
|
+
These operate directly on signal/computed nodes — the transformer emits them, and they can be called by hand:
|
|
231
231
|
|
|
232
232
|
| Function | Description |
|
|
233
233
|
|----------|-------------|
|
|
234
|
-
| `signal(value)` | Creates a raw signal |
|
|
235
|
-
| `computed(fn)` | Creates a raw computed |
|
|
236
|
-
| `read(node)` | Reads a signal or computed value |
|
|
234
|
+
| `signal(value, equals?)` | Creates a raw signal. Optional `equals` comparator suppresses writes it deems equal (default `===`) |
|
|
235
|
+
| `computed(fn, equals?)` | Creates a raw computed. A `fn` returning a `Promise` or `AsyncIterable` becomes an async computed automatically (initial value `undefined`, unwrapped to `Settled<T>`). Optional `equals` comparator |
|
|
236
|
+
| `read(node)` | Reads a signal or computed value, subscribing the current scope |
|
|
237
237
|
| `write(signal, value)` | Sets a signal value |
|
|
238
|
-
| `
|
|
238
|
+
| `peek(node)` | Reads a signal/computed's current value **without** subscribing (returns an up-to-date value even for a dirty computed) |
|
|
239
|
+
| `untrack(fn)` | Runs `fn` without tracking dependencies; returns `fn`'s result |
|
|
240
|
+
| `batch(fn)` | Groups writes so dependent effects defer until `fn` returns; pair with `flush()` for a synchronous transaction |
|
|
241
|
+
| `flush()` | Synchronously settles all pending computed/effect updates |
|
|
239
242
|
| `dispose(computed)` | Disposes a computed and its dependencies |
|
|
243
|
+
| `computed.invalidate(computed)` | Forces re-derivation of a computed on next read (re-dispatches an async computed's promise for a refetch) |
|
|
244
|
+
| `signal.selector(signal, key)` | Per-key selector: within a tracking scope, subscribes to whether `signal`'s value equals `key`, so only key-matching observers re-run on write (O(1) fan-out) |
|
|
240
245
|
|
|
241
246
|
### Type Guards
|
|
242
247
|
|
|
@@ -272,6 +277,8 @@ Symbol constants for type identification:
|
|
|
272
277
|
|------|-------------|
|
|
273
278
|
| `Signal<T>` | Signal node type |
|
|
274
279
|
| `Computed<T>` | Computed node type |
|
|
280
|
+
| `Settled<T>` | Resolved value type of a computed (unwraps `Promise<T>`/`AsyncIterable<T>` to `T`) |
|
|
281
|
+
| `SelectorSignal<T>` | Signal variant backing `signal.selector` per-key subscriptions |
|
|
275
282
|
| `Link` | Dependency graph link between nodes |
|
|
276
283
|
| `Reactive<T>` | Utility type for inferring reactive object/array types |
|
|
277
284
|
| `TransformResult` | Compiler transform output metadata |
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { bench, describe } from 'vitest';
|
|
2
|
+
import { assert, framework, ReactiveComputed } from './lib/reactive-adapter';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
// Ported from milomg/js-reactivity-benchmark packages/core/src/benches/cellxBench.ts (MIT)
|
|
6
|
+
|
|
7
|
+
interface Layer {
|
|
8
|
+
prop1: ReactiveComputed<number>;
|
|
9
|
+
prop2: ReactiveComputed<number>;
|
|
10
|
+
prop3: ReactiveComputed<number>;
|
|
11
|
+
prop4: ReactiveComputed<number>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
function cellx(layers: number, before: readonly number[], after: readonly number[]) {
|
|
16
|
+
let start = {
|
|
17
|
+
prop1: framework.signal(1),
|
|
18
|
+
prop2: framework.signal(2),
|
|
19
|
+
prop3: framework.signal(3),
|
|
20
|
+
prop4: framework.signal(4)
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let layer: Layer = start;
|
|
24
|
+
|
|
25
|
+
for (let i = layers; i > 0; i--) {
|
|
26
|
+
let m = layer;
|
|
27
|
+
|
|
28
|
+
layer = {
|
|
29
|
+
prop1: framework.computed(() => m.prop2.read()),
|
|
30
|
+
prop2: framework.computed(() => m.prop1.read() - m.prop3.read()),
|
|
31
|
+
prop3: framework.computed(() => m.prop2.read() + m.prop4.read()),
|
|
32
|
+
prop4: framework.computed(() => m.prop3.read())
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let end = layer;
|
|
37
|
+
|
|
38
|
+
assert(end.prop1.read() === before[0], 'cellx: unexpected initial prop1');
|
|
39
|
+
assert(end.prop2.read() === before[1], 'cellx: unexpected initial prop2');
|
|
40
|
+
assert(end.prop3.read() === before[2], 'cellx: unexpected initial prop3');
|
|
41
|
+
assert(end.prop4.read() === before[3], 'cellx: unexpected initial prop4');
|
|
42
|
+
|
|
43
|
+
return framework.withBatch(() => {
|
|
44
|
+
start.prop1.write(4);
|
|
45
|
+
start.prop2.write(3);
|
|
46
|
+
start.prop3.write(2);
|
|
47
|
+
start.prop4.write(1);
|
|
48
|
+
}).then(() => {
|
|
49
|
+
assert(end.prop1.read() === after[0], 'cellx: unexpected final prop1');
|
|
50
|
+
assert(end.prop2.read() === after[1], 'cellx: unexpected final prop2');
|
|
51
|
+
assert(end.prop3.read() === after[2], 'cellx: unexpected final prop3');
|
|
52
|
+
assert(end.prop4.read() === after[3], 'cellx: unexpected final prop4');
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
describe('cellx', () => {
|
|
58
|
+
bench('cellx1000', async () => {
|
|
59
|
+
await framework.withBuild(() => cellx(1000, [-3, -6, -2, 2], [-2, -4, 2, 3]));
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { bench, describe } from 'vitest';
|
|
2
|
+
import { assert, framework, ReactiveComputed } from './lib/reactive-adapter';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
// Dynamic-graph workloads: dependency SETS change between runs (branch on a control
|
|
6
|
+
// signal), exercising link/unlink churn and the link pool
|
|
7
|
+
|
|
8
|
+
function dependencySwap() {
|
|
9
|
+
let a = [],
|
|
10
|
+
b = [],
|
|
11
|
+
computeds: ReactiveComputed<number>[] = [],
|
|
12
|
+
control = framework.signal(true);
|
|
13
|
+
|
|
14
|
+
for (let i = 0; i < 10; i++) {
|
|
15
|
+
a.push(framework.signal(i));
|
|
16
|
+
b.push(framework.signal(i * 2));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
for (let i = 0; i < 20; i++) {
|
|
20
|
+
computeds.push(framework.computed(() => {
|
|
21
|
+
let source = control.read() ? a : b,
|
|
22
|
+
sum = 0;
|
|
23
|
+
|
|
24
|
+
for (let j = 0, n = source.length; j < n; j++) {
|
|
25
|
+
sum += source[j].read();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return sum;
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (let i = 0, n = computeds.length; i < n; i++) {
|
|
33
|
+
let c = computeds[i];
|
|
34
|
+
|
|
35
|
+
framework.effect(() => c.read());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let flip = true;
|
|
39
|
+
|
|
40
|
+
return async () => {
|
|
41
|
+
flip = !flip;
|
|
42
|
+
|
|
43
|
+
await framework.withBatch(() => {
|
|
44
|
+
control.write(flip);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
let expected = flip ? 45 : 90;
|
|
48
|
+
|
|
49
|
+
for (let i = 0, n = computeds.length; i < n; i++) {
|
|
50
|
+
assert(computeds[i].read() === expected, 'dynamic swap: unexpected sum');
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function movingBranchPoint() {
|
|
56
|
+
let base = framework.signal(100),
|
|
57
|
+
chain: ReactiveComputed<number>[] = [],
|
|
58
|
+
cut = framework.signal(0),
|
|
59
|
+
depth = 30,
|
|
60
|
+
source = framework.signal(1);
|
|
61
|
+
|
|
62
|
+
// Each node conditionally links `base` (a signal — safe to drop, signals never
|
|
63
|
+
// auto-dispose) while ALWAYS reading its predecessor so the chain stays alive
|
|
64
|
+
chain.push(framework.computed(() => (cut.read() > 0 ? base.read() : 0) + source.read()));
|
|
65
|
+
|
|
66
|
+
for (let i = 1; i < depth; i++) {
|
|
67
|
+
let prev = chain[i - 1];
|
|
68
|
+
|
|
69
|
+
chain.push(framework.computed(() => (cut.read() > i ? base.read() : 0) + prev.read() + 1));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let tail = chain[depth - 1];
|
|
73
|
+
|
|
74
|
+
framework.effect(() => tail.read());
|
|
75
|
+
|
|
76
|
+
return async () => {
|
|
77
|
+
await framework.withBatch(() => {
|
|
78
|
+
cut.write(15);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
assert(tail.read() === 15 * 100 + 1 + (depth - 1), 'dynamic branch: unexpected tail after cut');
|
|
82
|
+
|
|
83
|
+
await framework.withBatch(() => {
|
|
84
|
+
cut.write(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
assert(tail.read() === 1 + (depth - 1), 'dynamic branch: unexpected tail after restore');
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
describe('dynamic graphs', () => {
|
|
93
|
+
let branchRun = framework.withBuild(movingBranchPoint),
|
|
94
|
+
swapRun = framework.withBuild(dependencySwap);
|
|
95
|
+
|
|
96
|
+
bench('dependency set swap (20 computeds x 10 deps)', async () => {
|
|
97
|
+
await swapRun();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
bench('moving branch point (30-deep chain re-link)', async () => {
|
|
101
|
+
await branchRun();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { bench, describe } from 'vitest';
|
|
2
|
+
import { assert, framework, ReactiveComputed } from './lib/reactive-adapter';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
// Ported from milomg/js-reactivity-benchmark packages/core/src/benches/kairo/* (MIT)
|
|
6
|
+
|
|
7
|
+
function avoidablePropagation() {
|
|
8
|
+
let head = framework.signal(0),
|
|
9
|
+
computed1 = framework.computed(() => head.read()),
|
|
10
|
+
computed2 = framework.computed(() => (computed1.read(), 0)),
|
|
11
|
+
computed3 = framework.computed(() => (busy(), computed2.read() + 1)),
|
|
12
|
+
computed4 = framework.computed(() => computed3.read() + 2),
|
|
13
|
+
computed5 = framework.computed(() => computed4.read() + 3);
|
|
14
|
+
|
|
15
|
+
framework.effect(() => {
|
|
16
|
+
computed5.read();
|
|
17
|
+
busy();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return async () => {
|
|
21
|
+
await framework.withBatch(() => {
|
|
22
|
+
head.write(1);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
assert(computed5.read() === 6, 'kairo avoidable: unexpected terminal value');
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < 1000; i++) {
|
|
28
|
+
await framework.withBatch(() => {
|
|
29
|
+
head.write(i);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
assert(computed5.read() === 6, 'kairo avoidable: propagation was not avoided');
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function broadPropagation() {
|
|
38
|
+
let counter = { count: 0 },
|
|
39
|
+
head = framework.signal(0),
|
|
40
|
+
last: ReactiveComputed<number> = head;
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < 50; i++) {
|
|
43
|
+
let current = framework.computed(() => head.read() + i),
|
|
44
|
+
current2 = framework.computed(() => current.read() + 1);
|
|
45
|
+
|
|
46
|
+
framework.effect(() => {
|
|
47
|
+
current2.read();
|
|
48
|
+
counter.count++;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
last = current2;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return async () => {
|
|
55
|
+
await framework.withBatch(() => {
|
|
56
|
+
head.write(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
counter.count = 0;
|
|
60
|
+
|
|
61
|
+
for (let i = 0; i < 50; i++) {
|
|
62
|
+
await framework.withBatch(() => {
|
|
63
|
+
head.write(i);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
assert(last.read() === i + 50, 'kairo broad: unexpected tail value');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
assert(counter.count === 50 * 50, 'kairo broad: unexpected effect run count');
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function busy() {
|
|
74
|
+
let a = 0;
|
|
75
|
+
|
|
76
|
+
for (let i = 0; i < 100; i++) {
|
|
77
|
+
a++;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return a;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function deepPropagation() {
|
|
84
|
+
let counter = { count: 0 },
|
|
85
|
+
head = framework.signal(0),
|
|
86
|
+
len = 50,
|
|
87
|
+
current: ReactiveComputed<number> = head;
|
|
88
|
+
|
|
89
|
+
for (let i = 0; i < len; i++) {
|
|
90
|
+
let c = current;
|
|
91
|
+
|
|
92
|
+
current = framework.computed(() => c.read() + 1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
framework.effect(() => {
|
|
96
|
+
current.read();
|
|
97
|
+
counter.count++;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return async () => {
|
|
101
|
+
let iter = 50;
|
|
102
|
+
|
|
103
|
+
await framework.withBatch(() => {
|
|
104
|
+
head.write(1);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
counter.count = 0;
|
|
108
|
+
|
|
109
|
+
for (let i = 0; i < iter; i++) {
|
|
110
|
+
await framework.withBatch(() => {
|
|
111
|
+
head.write(i);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
assert(current.read() === len + i, 'kairo deep: unexpected tail value');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
assert(counter.count === iter, 'kairo deep: unexpected effect run count');
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function diamond() {
|
|
122
|
+
let counter = { count: 0 },
|
|
123
|
+
current: ReactiveComputed<number>[] = [],
|
|
124
|
+
head = framework.signal(0),
|
|
125
|
+
width = 5;
|
|
126
|
+
|
|
127
|
+
for (let i = 0; i < width; i++) {
|
|
128
|
+
current.push(framework.computed(() => head.read() + 1));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let sum = framework.computed(() => current.map((x) => x.read()).reduce((a, b) => a + b, 0));
|
|
132
|
+
|
|
133
|
+
framework.effect(() => {
|
|
134
|
+
sum.read();
|
|
135
|
+
counter.count++;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return async () => {
|
|
139
|
+
await framework.withBatch(() => {
|
|
140
|
+
head.write(1);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
assert(sum.read() === 2 * width, 'kairo diamond: unexpected initial sum');
|
|
144
|
+
|
|
145
|
+
counter.count = 0;
|
|
146
|
+
|
|
147
|
+
for (let i = 0; i < 500; i++) {
|
|
148
|
+
await framework.withBatch(() => {
|
|
149
|
+
head.write(i);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
assert(sum.read() === (i + 1) * width, 'kairo diamond: unexpected sum');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
assert(counter.count === 500, 'kairo diamond: unexpected effect run count');
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function mux() {
|
|
160
|
+
let heads = new Array(100).fill(null).map(() => framework.signal(0));
|
|
161
|
+
|
|
162
|
+
let muxed = framework.computed(() => Object.fromEntries(heads.map((h) => h.read()).entries()));
|
|
163
|
+
|
|
164
|
+
let splited = heads
|
|
165
|
+
.map((_, index) => framework.computed(() => muxed.read()[index]))
|
|
166
|
+
.map((x) => framework.computed(() => x.read() + 1));
|
|
167
|
+
|
|
168
|
+
for (let i = 0, n = splited.length; i < n; i++) {
|
|
169
|
+
let x = splited[i];
|
|
170
|
+
|
|
171
|
+
framework.effect(() => x.read());
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return async () => {
|
|
175
|
+
for (let i = 0; i < 10; i++) {
|
|
176
|
+
await framework.withBatch(() => {
|
|
177
|
+
heads[i].write(i);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
assert(splited[i].read() === i + 1, 'kairo mux: unexpected split value');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (let i = 0; i < 10; i++) {
|
|
184
|
+
await framework.withBatch(() => {
|
|
185
|
+
heads[i].write(i * 2);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
assert(splited[i].read() === i * 2 + 1, 'kairo mux: unexpected split value');
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function repeatedObservers() {
|
|
194
|
+
let counter = { count: 0 },
|
|
195
|
+
head = framework.signal(0),
|
|
196
|
+
size = 30;
|
|
197
|
+
|
|
198
|
+
let current = framework.computed(() => {
|
|
199
|
+
let result = 0;
|
|
200
|
+
|
|
201
|
+
for (let i = 0; i < size; i++) {
|
|
202
|
+
result += head.read();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return result;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
framework.effect(() => {
|
|
209
|
+
current.read();
|
|
210
|
+
counter.count++;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return async () => {
|
|
214
|
+
await framework.withBatch(() => {
|
|
215
|
+
head.write(1);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
assert(current.read() === size, 'kairo repeated: unexpected initial value');
|
|
219
|
+
|
|
220
|
+
counter.count = 0;
|
|
221
|
+
|
|
222
|
+
for (let i = 0; i < 100; i++) {
|
|
223
|
+
await framework.withBatch(() => {
|
|
224
|
+
head.write(i);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
assert(current.read() === i * size, 'kairo repeated: unexpected value');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
assert(counter.count === 100, 'kairo repeated: unexpected effect run count');
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function triangle() {
|
|
235
|
+
let counter = { count: 0 },
|
|
236
|
+
head = framework.signal(0),
|
|
237
|
+
list: ReactiveComputed<number>[] = [],
|
|
238
|
+
width = 10,
|
|
239
|
+
current: ReactiveComputed<number> = head;
|
|
240
|
+
|
|
241
|
+
for (let i = 0; i < width; i++) {
|
|
242
|
+
let c = current;
|
|
243
|
+
|
|
244
|
+
list.push(current);
|
|
245
|
+
current = framework.computed(() => c.read() + 1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let sum = framework.computed(() => list.map((x) => x.read()).reduce((a, b) => a + b, 0));
|
|
249
|
+
|
|
250
|
+
framework.effect(() => {
|
|
251
|
+
sum.read();
|
|
252
|
+
counter.count++;
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return async () => {
|
|
256
|
+
let constant = (width * (width + 1)) / 2;
|
|
257
|
+
|
|
258
|
+
await framework.withBatch(() => {
|
|
259
|
+
head.write(1);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
assert(sum.read() === constant, 'kairo triangle: unexpected initial sum');
|
|
263
|
+
|
|
264
|
+
counter.count = 0;
|
|
265
|
+
|
|
266
|
+
for (let i = 0; i < 100; i++) {
|
|
267
|
+
await framework.withBatch(() => {
|
|
268
|
+
head.write(i);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
assert(sum.read() === constant - width + i * width, 'kairo triangle: unexpected sum');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
assert(counter.count === 100, 'kairo triangle: unexpected effect run count');
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function unstable() {
|
|
279
|
+
let counter = { count: 0 },
|
|
280
|
+
head = framework.signal(0);
|
|
281
|
+
|
|
282
|
+
let double = framework.computed(() => head.read() * 2);
|
|
283
|
+
|
|
284
|
+
let inverse = framework.computed(() => -head.read());
|
|
285
|
+
|
|
286
|
+
let current = framework.computed(() => {
|
|
287
|
+
let result = 0;
|
|
288
|
+
|
|
289
|
+
for (let i = 0; i < 20; i++) {
|
|
290
|
+
result += head.read() % 2 ? double.read() : inverse.read();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return result;
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Keepers: this system auto-disposes a computed on last-sub unlink, so the
|
|
297
|
+
// inactive branch would go permanently stale without a live subscriber
|
|
298
|
+
framework.effect(() => double.read());
|
|
299
|
+
|
|
300
|
+
framework.effect(() => inverse.read());
|
|
301
|
+
|
|
302
|
+
framework.effect(() => {
|
|
303
|
+
current.read();
|
|
304
|
+
counter.count++;
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
return async () => {
|
|
308
|
+
await framework.withBatch(() => {
|
|
309
|
+
head.write(1);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
assert(current.read() === 40, 'kairo unstable: unexpected initial value');
|
|
313
|
+
|
|
314
|
+
counter.count = 0;
|
|
315
|
+
|
|
316
|
+
for (let i = 0; i < 100; i++) {
|
|
317
|
+
await framework.withBatch(() => {
|
|
318
|
+
head.write(i);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
assert(counter.count === 100, 'kairo unstable: unexpected effect run count');
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
describe('kairo', () => {
|
|
328
|
+
let avoidableRun = framework.withBuild(avoidablePropagation),
|
|
329
|
+
broadRun = framework.withBuild(broadPropagation),
|
|
330
|
+
deepRun = framework.withBuild(deepPropagation),
|
|
331
|
+
diamondRun = framework.withBuild(diamond),
|
|
332
|
+
muxRun = framework.withBuild(mux),
|
|
333
|
+
repeatedRun = framework.withBuild(repeatedObservers),
|
|
334
|
+
triangleRun = framework.withBuild(triangle),
|
|
335
|
+
unstableRun = framework.withBuild(unstable);
|
|
336
|
+
|
|
337
|
+
bench('avoidablePropagation', async () => {
|
|
338
|
+
await avoidableRun();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
bench('broad', async () => {
|
|
342
|
+
await broadRun();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
bench('deep', async () => {
|
|
346
|
+
await deepRun();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
bench('diamond', async () => {
|
|
350
|
+
await diamondRun();
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
bench('mux', async () => {
|
|
354
|
+
await muxRun();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
bench('repeatedObservers', async () => {
|
|
358
|
+
await repeatedRun();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
bench('triangle', async () => {
|
|
362
|
+
await triangleRun();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
bench('unstable', async () => {
|
|
366
|
+
await unstableRun();
|
|
367
|
+
});
|
|
368
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { computed, effect, read, root, signal, write } from '~/system';
|
|
2
|
+
|
|
3
|
+
import type { Settled } from '~/types';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
interface ReactiveComputed<T> {
|
|
7
|
+
read: () => T;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ReactiveSignal<T> extends ReactiveComputed<T> {
|
|
11
|
+
write: (value: T) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
// Mirrors js-reactivity-benchmark's ReactiveFramework so a future external-package swap is mechanical
|
|
16
|
+
const framework = {
|
|
17
|
+
computed: <T>(fn: () => T): ReactiveComputed<Settled<T>> => {
|
|
18
|
+
let node = computed(fn);
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
read: () => read(node)
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
effect: (fn: () => void): VoidFunction => effect(fn),
|
|
25
|
+
signal: <T>(value: T): ReactiveSignal<T> => {
|
|
26
|
+
let node = signal(value);
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
read: () => read(node),
|
|
30
|
+
write: (v: T) => write(node, v)
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
withBatch: async (fn: () => void): Promise<void> => {
|
|
34
|
+
fn();
|
|
35
|
+
|
|
36
|
+
// Propagation is microtask-scheduled (schedule/stabilize) — two ticks cover a reschedule
|
|
37
|
+
await Promise.resolve();
|
|
38
|
+
await Promise.resolve();
|
|
39
|
+
},
|
|
40
|
+
withBuild: <T>(fn: () => T): T => root(fn)
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const assert = (condition: boolean, message: string): void => {
|
|
44
|
+
if (!condition) {
|
|
45
|
+
throw new Error(`bench: ${message}`);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
export { assert, framework };
|
|
51
|
+
export type { ReactiveComputed, ReactiveSignal };
|