@zeix/cause-effect 0.17.3 → 0.18.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.
Files changed (89) hide show
  1. package/.ai-context.md +163 -232
  2. package/.cursorrules +41 -35
  3. package/.github/copilot-instructions.md +166 -116
  4. package/ARCHITECTURE.md +274 -0
  5. package/CLAUDE.md +199 -143
  6. package/COLLECTION_REFACTORING.md +161 -0
  7. package/GUIDE.md +298 -0
  8. package/README.md +232 -197
  9. package/REQUIREMENTS.md +100 -0
  10. package/bench/reactivity.bench.ts +577 -0
  11. package/index.dev.js +1325 -997
  12. package/index.js +1 -1
  13. package/index.ts +58 -74
  14. package/package.json +4 -1
  15. package/src/errors.ts +118 -74
  16. package/src/graph.ts +601 -0
  17. package/src/nodes/collection.ts +474 -0
  18. package/src/nodes/effect.ts +149 -0
  19. package/src/nodes/list.ts +588 -0
  20. package/src/nodes/memo.ts +120 -0
  21. package/src/nodes/sensor.ts +139 -0
  22. package/src/nodes/state.ts +135 -0
  23. package/src/nodes/store.ts +383 -0
  24. package/src/nodes/task.ts +146 -0
  25. package/src/signal.ts +112 -66
  26. package/src/util.ts +26 -57
  27. package/test/batch.test.ts +96 -62
  28. package/test/benchmark.test.ts +473 -487
  29. package/test/collection.test.ts +466 -706
  30. package/test/effect.test.ts +293 -696
  31. package/test/list.test.ts +335 -592
  32. package/test/memo.test.ts +380 -0
  33. package/test/regression.test.ts +156 -0
  34. package/test/scope.test.ts +191 -0
  35. package/test/sensor.test.ts +454 -0
  36. package/test/signal.test.ts +220 -213
  37. package/test/state.test.ts +217 -265
  38. package/test/store.test.ts +346 -446
  39. package/test/task.test.ts +395 -0
  40. package/test/untrack.test.ts +167 -0
  41. package/types/index.d.ts +13 -15
  42. package/types/src/errors.d.ts +73 -17
  43. package/types/src/graph.d.ts +208 -0
  44. package/types/src/nodes/collection.d.ts +64 -0
  45. package/types/src/nodes/effect.d.ts +48 -0
  46. package/types/src/nodes/list.d.ts +65 -0
  47. package/types/src/nodes/memo.d.ts +57 -0
  48. package/types/src/nodes/sensor.d.ts +75 -0
  49. package/types/src/nodes/state.d.ts +78 -0
  50. package/types/src/nodes/store.d.ts +51 -0
  51. package/types/src/nodes/task.d.ts +73 -0
  52. package/types/src/signal.d.ts +43 -29
  53. package/types/src/util.d.ts +9 -16
  54. package/archive/benchmark.ts +0 -683
  55. package/archive/collection.ts +0 -253
  56. package/archive/composite.ts +0 -85
  57. package/archive/computed.ts +0 -195
  58. package/archive/list.ts +0 -483
  59. package/archive/memo.ts +0 -139
  60. package/archive/state.ts +0 -90
  61. package/archive/store.ts +0 -298
  62. package/archive/task.ts +0 -189
  63. package/src/classes/collection.ts +0 -245
  64. package/src/classes/computed.ts +0 -349
  65. package/src/classes/list.ts +0 -343
  66. package/src/classes/ref.ts +0 -70
  67. package/src/classes/state.ts +0 -102
  68. package/src/classes/store.ts +0 -262
  69. package/src/diff.ts +0 -138
  70. package/src/effect.ts +0 -93
  71. package/src/match.ts +0 -45
  72. package/src/resolve.ts +0 -49
  73. package/src/system.ts +0 -257
  74. package/test/computed.test.ts +0 -1108
  75. package/test/diff.test.ts +0 -955
  76. package/test/match.test.ts +0 -388
  77. package/test/ref.test.ts +0 -353
  78. package/test/resolve.test.ts +0 -154
  79. package/types/src/classes/collection.d.ts +0 -45
  80. package/types/src/classes/computed.d.ts +0 -94
  81. package/types/src/classes/list.d.ts +0 -43
  82. package/types/src/classes/ref.d.ts +0 -35
  83. package/types/src/classes/state.d.ts +0 -49
  84. package/types/src/classes/store.d.ts +0 -52
  85. package/types/src/diff.d.ts +0 -28
  86. package/types/src/effect.d.ts +0 -15
  87. package/types/src/match.d.ts +0 -21
  88. package/types/src/resolve.d.ts +0 -29
  89. package/types/src/system.d.ts +0 -78
@@ -0,0 +1,100 @@
1
+ # Cause & Effect - Requirements
2
+
3
+ This document captures the vision, audience, constraints, and boundaries of the library. It is intended to survive version bumps and guide decisions about what belongs in the library and what does not.
4
+
5
+ ## Vision
6
+
7
+ Cause & Effect is a **primitives-only reactive state management library** for TypeScript. It provides the foundational building blocks that library authors and experienced developers need to manage complex, dynamic, composite, and asynchronous state — correctly and performantly — in a unified signal graph.
8
+
9
+ The library is deliberately **not a framework**. It has no opinions about rendering, persistence, or application architecture. It is a thin, trustworthy layer over JavaScript that provides the comfort and guarantees of fine-grained reactivity while avoiding the common pitfalls of imperative code.
10
+
11
+ ## Audience
12
+
13
+ ### Primary: Library Authors
14
+
15
+ TypeScript library authors — frontend or backend — who need a solid reactive foundation to build on. The library is designed so that consuming libraries should not have to implement their own reactive primitives. The extensive set of signal types exists precisely so that patterns like external data feeds, async derivations, and keyed collections are handled correctly within a unified graph rather than bolted on as ad-hoc extensions.
16
+
17
+ Cause & Effect is open source, built to power **Le Truc**, a Web Component library by Zeix AG.
18
+
19
+ ### Secondary: Experienced Developers
20
+
21
+ Developers who want to write framework-agnostic web applications with a thin layer over JavaScript. They value explicit dependencies, predictable updates, and type safety over the convenience of a full framework. They are comfortable composing their own rendering and application layers on top of reactive primitives.
22
+
23
+ ## Design Principles
24
+
25
+ ### Explicit Reactivity
26
+ Dependencies are automatically tracked through `.get()` calls, but relationships remain clear and predictable. There is no hidden magic — the graph always reflects the true dependency structure.
27
+
28
+ ### Non-Nullable Types
29
+ All signals enforce `T extends {}`, excluding `null` and `undefined` at the type level. This is a deliberate design decision: developers should be able to trust returned types and never have to do null checks after a value enters the signal graph.
30
+
31
+ ### Unified Graph
32
+ Every signal type participates in the same dependency graph with the same propagation, batching, and cleanup semantics. Composite signals (Store, List, Collection) and async signals (Task) are first-class citizens, not afterthoughts. The goal is that all state which is derivable can be derived.
33
+
34
+ ### Minimal Surface, Maximum Coverage
35
+ The library ships 8 signal types — each justified by a distinct role in the graph and a distinct data structure it manages:
36
+
37
+ | Type | Role | Data Structure |
38
+ |------|------|----------------|
39
+ | **State** | Mutable source | Single value |
40
+ | **Sensor** | External input source | Single value (lazy lifecycle) |
41
+ | **Memo** | Synchronous derivation | Single value (memoized) |
42
+ | **Task** | Asynchronous derivation | Single value (memoized, cancellable) |
43
+ | **Effect** | Side-effect sink | None (terminal) |
44
+ | **Store** | Reactive object | Keyed properties (proxy-based) |
45
+ | **List** | Reactive array | Keyed items (stable identity) |
46
+ | **Collection** | Reactive collection (external source or derived) | Keyed items (lazy lifecycle, item-level memoization) |
47
+
48
+ This set is considered **complete**. The principle for inclusion is: does this type represent a fundamentally different data structure or role in the graph that cannot be correctly or performantly expressed as a composition of existing types?
49
+
50
+ ## Runtime Environments
51
+
52
+ - All evergreen browsers
53
+ - Bun
54
+ - Modern Node.js (with ES module support)
55
+ - Deno
56
+
57
+ The library uses no browser-specific APIs in its core. Environment-specific behavior (DOM events, network connections) is the responsibility of user-provided callbacks (Sensor start functions, Collection start callbacks, watched callbacks).
58
+
59
+ ## Size and Performance Constraints
60
+
61
+ ### Bundle Size
62
+
63
+ | Usage | Target |
64
+ |-------|--------|
65
+ | Core signals only (State, Memo, Task, Effect) | Below 5 kB gzipped |
66
+ | Full library (all 8 signal types + utilities) | Below 10 kB gzipped |
67
+
68
+ The library must remain tree-shakable: importing only what you use should not pull in unrelated signal types.
69
+
70
+ ### Performance
71
+
72
+ The synchronous path (State, Memo, Effect propagation) must be competitive with current leaders in fine-grained reactivity (Preact Signals, Solid, Alien Signals). The library's differentiator is not being the absolute fastest on micro-benchmarks, but seamlessly integrating async (Task), external observers (Sensor, Collection), and composite signals (Store, List, Collection) without sacrificing sync-path performance.
73
+
74
+ ## Non-Goals
75
+
76
+ The following are explicitly out of scope and will not be added to the library:
77
+
78
+ - **Rendering**: No DOM manipulation, no virtual DOM, no component model, no template system. Rendering is the responsibility of consuming libraries or application code.
79
+ - **Persistence**: No serialization, no local storage, no database integration. State enters and leaves the graph through signals; how it is stored is not this library's concern.
80
+ - **Framework-specific bindings**: No React hooks, no Vue composables, no Angular decorators. Consuming libraries build their own integrations.
81
+ - **DevTools protocol**: Debugging is straightforward by design — attaching an effect to any signal reveals its current value and update behavior. A dedicated debugging protocol adds complexity without proportional value.
82
+ - **Additional signal types**: The 8 signal types are considered complete. New types would only be considered if major Web Platform changes shift the optimal way to achieve the library's existing goals.
83
+
84
+ ## Stability
85
+
86
+ Version 0.18 is the last pre-release before 1.0. The API surface — how signals are created and consumed — is considered stable. From 1.0 onward:
87
+
88
+ - **Breaking changes** are expected only if major new features of the Web Platform shift the optimal way to achieve the goals this library already does.
89
+ - **New features** are not expected. The signal type set is complete.
90
+ - **Backward compatibility** becomes a concern at 1.0. Prior to that, all known consumers (Le Truc and one other library) are maintained by Zeix AG and can adapt to changes.
91
+
92
+ ## Success Criteria
93
+
94
+ The library succeeds when:
95
+
96
+ 1. Consuming libraries (Le Truc and others) do not need to implement their own reactive primitives for patterns the signal graph already covers.
97
+ 2. The mental model is understandable: developers can predict how changes propagate by understanding the graph structure.
98
+ 3. The type system catches errors at compile time that would otherwise surface as runtime null checks or stale state bugs.
99
+ 4. Performance remains competitive on standard reactivity benchmarks without special-casing for benchmarks.
100
+ 5. The library remains small enough that it does not meaningfully contribute to bundle size concerns in production applications.
@@ -0,0 +1,577 @@
1
+ import { bench, group, run } from 'mitata'
2
+ import {
3
+ batch,
4
+ createEffect,
5
+ createList,
6
+ createMemo,
7
+ createSensor,
8
+ createState,
9
+ createStore,
10
+ createTask,
11
+ SKIP_EQUALITY,
12
+ } from '../index.ts'
13
+ import type { ReactiveFramework } from '../test/util/reactive-framework'
14
+
15
+ /* === Framework Adapter === */
16
+
17
+ const framework: ReactiveFramework = {
18
+ name: 'cause-effect',
19
+ // @ts-expect-error ReactiveFramework doesn't have non-nullable signals
20
+ signal: <T extends {}>(initialValue: T) => {
21
+ const s = createState(initialValue)
22
+ return { write: s.set, read: s.get }
23
+ },
24
+ // @ts-expect-error ReactiveFramework doesn't have non-nullable signals
25
+ computed: <T extends {}>(fn: () => T) => {
26
+ const c = createMemo(fn)
27
+ return { read: c.get }
28
+ },
29
+ effect: (fn: () => undefined) => {
30
+ createEffect(() => fn())
31
+ },
32
+ withBatch: fn => batch(fn),
33
+ withBuild: <T>(fn: () => T) => fn(),
34
+ }
35
+
36
+ /* === Kairo Benchmarks === */
37
+
38
+ function setupDeep(fw: ReactiveFramework) {
39
+ const len = 50
40
+ const head = fw.signal(0)
41
+ let current = head as { read: () => number }
42
+ for (let i = 0; i < len; i++) {
43
+ const c = current
44
+ current = fw.computed(() => c.read() + 1)
45
+ }
46
+ fw.effect(() => {
47
+ current.read()
48
+ })
49
+ let i = 0
50
+ return () => {
51
+ fw.withBatch(() => {
52
+ head.write(++i)
53
+ })
54
+ }
55
+ }
56
+
57
+ function setupBroad(fw: ReactiveFramework) {
58
+ const head = fw.signal(0)
59
+ for (let i = 0; i < 50; i++) {
60
+ const current = fw.computed(() => head.read() + i)
61
+ const current2 = fw.computed(() => current.read() + 1)
62
+ fw.effect(() => {
63
+ current2.read()
64
+ })
65
+ }
66
+ let i = 0
67
+ return () => {
68
+ fw.withBatch(() => {
69
+ head.write(++i)
70
+ })
71
+ }
72
+ }
73
+
74
+ function setupDiamond(fw: ReactiveFramework) {
75
+ const width = 5
76
+ const head = fw.signal(0)
77
+ const branches: { read(): number }[] = []
78
+ for (let i = 0; i < width; i++) {
79
+ branches.push(fw.computed(() => head.read() + 1))
80
+ }
81
+ const sum = fw.computed(() =>
82
+ branches.map(x => x.read()).reduce((a, b) => a + b, 0),
83
+ )
84
+ fw.effect(() => {
85
+ sum.read()
86
+ })
87
+ let i = 0
88
+ return () => {
89
+ fw.withBatch(() => {
90
+ head.write(++i)
91
+ })
92
+ }
93
+ }
94
+
95
+ function setupTriangle(fw: ReactiveFramework) {
96
+ const width = 10
97
+ const head = fw.signal(0)
98
+ let current = head as { read: () => number }
99
+ const list: { read: () => number }[] = []
100
+ for (let i = 0; i < width; i++) {
101
+ const c = current
102
+ list.push(current)
103
+ current = fw.computed(() => c.read() + 1)
104
+ }
105
+ const sum = fw.computed(() =>
106
+ list.map(x => x.read()).reduce((a, b) => a + b, 0),
107
+ )
108
+ fw.effect(() => {
109
+ sum.read()
110
+ })
111
+ let i = 0
112
+ return () => {
113
+ fw.withBatch(() => {
114
+ head.write(++i)
115
+ })
116
+ }
117
+ }
118
+
119
+ function setupMux(fw: ReactiveFramework) {
120
+ const heads = new Array(100).fill(null).map(_ => fw.signal(0))
121
+ const mux = fw.computed(() =>
122
+ Object.fromEntries(heads.map(h => h.read()).entries()),
123
+ )
124
+ const splited = heads
125
+ .map((_, index) => fw.computed(() => mux.read()[index]))
126
+ .map(x => fw.computed(() => x.read() + 1))
127
+ for (const x of splited) {
128
+ fw.effect(() => {
129
+ x.read()
130
+ })
131
+ }
132
+ let i = 0
133
+ return () => {
134
+ const idx = i % heads.length
135
+ fw.withBatch(() => {
136
+ heads[idx].write(++i)
137
+ })
138
+ }
139
+ }
140
+
141
+ function setupUnstable(fw: ReactiveFramework) {
142
+ const head = fw.signal(0)
143
+ const double = fw.computed(() => head.read() * 2)
144
+ const inverse = fw.computed(() => -head.read())
145
+ const current = fw.computed(() => {
146
+ let result = 0
147
+ for (let i = 0; i < 20; i++) {
148
+ result += head.read() % 2 ? double.read() : inverse.read()
149
+ }
150
+ return result
151
+ })
152
+ fw.effect(() => {
153
+ current.read()
154
+ })
155
+ let i = 0
156
+ return () => {
157
+ fw.withBatch(() => {
158
+ head.write(++i)
159
+ })
160
+ }
161
+ }
162
+
163
+ function setupAvoidable(fw: ReactiveFramework) {
164
+ const head = fw.signal(0)
165
+ const computed1 = fw.computed(() => head.read())
166
+ const computed2 = fw.computed(() => {
167
+ computed1.read()
168
+ return 0
169
+ })
170
+ const computed3 = fw.computed(() => computed2.read() + 1)
171
+ const computed4 = fw.computed(() => computed3.read() + 2)
172
+ const computed5 = fw.computed(() => computed4.read() + 3)
173
+ fw.effect(() => {
174
+ computed5.read()
175
+ })
176
+ let i = 0
177
+ return () => {
178
+ fw.withBatch(() => {
179
+ head.write(++i)
180
+ })
181
+ }
182
+ }
183
+
184
+ function setupRepeatedObservers(fw: ReactiveFramework) {
185
+ const size = 30
186
+ const head = fw.signal(0)
187
+ const current = fw.computed(() => {
188
+ let result = 0
189
+ for (let i = 0; i < size; i++) {
190
+ result += head.read()
191
+ }
192
+ return result
193
+ })
194
+ fw.effect(() => {
195
+ current.read()
196
+ })
197
+ let i = 0
198
+ return () => {
199
+ fw.withBatch(() => {
200
+ head.write(++i)
201
+ })
202
+ }
203
+ }
204
+
205
+ /* === CellX Benchmark === */
206
+
207
+ function setupCellx(fw: ReactiveFramework, layers: number) {
208
+ const start = {
209
+ prop1: fw.signal(1),
210
+ prop2: fw.signal(2),
211
+ prop3: fw.signal(3),
212
+ prop4: fw.signal(4),
213
+ }
214
+ let layer: Record<string, { read(): number }> = start
215
+
216
+ for (let i = layers; i > 0; i--) {
217
+ const m = layer
218
+ const s = {
219
+ prop1: fw.computed(() => m.prop2.read()),
220
+ prop2: fw.computed(() => m.prop1.read() - m.prop3.read()),
221
+ prop3: fw.computed(() => m.prop2.read() + m.prop4.read()),
222
+ prop4: fw.computed(() => m.prop3.read()),
223
+ }
224
+
225
+ fw.effect(() => {
226
+ s.prop1.read()
227
+ })
228
+ fw.effect(() => {
229
+ s.prop2.read()
230
+ })
231
+ fw.effect(() => {
232
+ s.prop3.read()
233
+ })
234
+ fw.effect(() => {
235
+ s.prop4.read()
236
+ })
237
+ fw.effect(() => {
238
+ s.prop1.read()
239
+ })
240
+ fw.effect(() => {
241
+ s.prop2.read()
242
+ })
243
+ fw.effect(() => {
244
+ s.prop3.read()
245
+ })
246
+ fw.effect(() => {
247
+ s.prop4.read()
248
+ })
249
+
250
+ layer = s
251
+ }
252
+
253
+ const end = layer
254
+ let toggle = false
255
+ return () => {
256
+ toggle = !toggle
257
+ fw.withBatch(() => {
258
+ start.prop1.write(toggle ? 4 : 1)
259
+ start.prop2.write(toggle ? 3 : 2)
260
+ start.prop3.write(toggle ? 2 : 3)
261
+ start.prop4.write(toggle ? 1 : 4)
262
+ })
263
+ end.prop1.read()
264
+ end.prop2.read()
265
+ end.prop3.read()
266
+ end.prop4.read()
267
+ }
268
+ }
269
+
270
+ /* === $mol_wire Benchmark === */
271
+
272
+ function setupMolWire(fw: ReactiveFramework) {
273
+ const fib = (n: number): number => {
274
+ if (n < 2) return 1
275
+ return fib(n - 1) + fib(n - 2)
276
+ }
277
+ const hard = (n: number, _log: string) => n + fib(16)
278
+ const numbers = Array.from({ length: 5 }, (_, i) => i)
279
+
280
+ const A = fw.signal(0)
281
+ const B = fw.signal(0)
282
+ const C = fw.computed(() => (A.read() % 2) + (B.read() % 2))
283
+ const D = fw.computed(() =>
284
+ numbers.map(i => ({ x: i + (A.read() % 2) - (B.read() % 2) })),
285
+ )
286
+ const E = fw.computed(() => hard(C.read() + A.read() + D.read()[0].x, 'E'))
287
+ const F = fw.computed(() => hard(D.read()[2].x || B.read(), 'F'))
288
+ const G = fw.computed(
289
+ () => C.read() + (C.read() || E.read() % 2) + D.read()[4].x + F.read(),
290
+ )
291
+ fw.effect(() => {
292
+ hard(G.read(), 'H')
293
+ })
294
+ fw.effect(() => {
295
+ G.read()
296
+ })
297
+ fw.effect(() => {
298
+ hard(F.read(), 'J')
299
+ })
300
+
301
+ let i = 0
302
+ return () => {
303
+ i++
304
+ fw.withBatch(() => {
305
+ B.write(1)
306
+ A.write(1 + i * 2)
307
+ })
308
+ fw.withBatch(() => {
309
+ A.write(2 + i * 2)
310
+ B.write(2)
311
+ })
312
+ }
313
+ }
314
+
315
+ /* === Signal Creation Benchmark === */
316
+
317
+ function benchCreateSignals(fw: ReactiveFramework, count: number) {
318
+ return () => {
319
+ for (let i = 0; i < count; i++) {
320
+ fw.signal(i)
321
+ }
322
+ }
323
+ }
324
+
325
+ function benchCreateComputations(fw: ReactiveFramework, count: number) {
326
+ const src = fw.signal(0)
327
+ return () => {
328
+ for (let i = 0; i < count; i++) {
329
+ fw.computed(() => src.read())
330
+ }
331
+ }
332
+ }
333
+
334
+ /* === Run Benchmarks === */
335
+
336
+ // Kairo benchmarks
337
+ const kairoBenchmarks = [
338
+ ['deep propagation', setupDeep],
339
+ ['broad propagation', setupBroad],
340
+ ['diamond', setupDiamond],
341
+ ['triangle', setupTriangle],
342
+ ['mux', setupMux],
343
+ ['unstable', setupUnstable],
344
+ ['avoidable propagation', setupAvoidable],
345
+ ['repeated observers', setupRepeatedObservers],
346
+ ] as const
347
+
348
+ for (const [name, setup] of kairoBenchmarks) {
349
+ group(`Kairo: ${name}`, () => {
350
+ bench('cause-effect', setup(framework))
351
+ })
352
+ }
353
+
354
+ // CellX benchmarks
355
+ for (const layers of [10]) {
356
+ group(`CellX ${layers} layers`, () => {
357
+ bench('cause-effect', setupCellx(framework, layers))
358
+ })
359
+ }
360
+
361
+ // $mol_wire benchmark
362
+ group('$mol_wire', () => {
363
+ bench('cause-effect', setupMolWire(framework))
364
+ })
365
+
366
+ // Creation benchmarks
367
+ group('Create 1k signals', () => {
368
+ bench('cause-effect', benchCreateSignals(framework, 1_000))
369
+ })
370
+
371
+ group('Create 1k computations', () => {
372
+ bench('cause-effect', benchCreateComputations(framework, 1_000))
373
+ })
374
+
375
+ /* === Task Benchmarks === */
376
+
377
+ group('Create 100 tasks', () => {
378
+ bench('cause-effect', () => {
379
+ const src = createState(0)
380
+ for (let i = 0; i < 100; i++) {
381
+ createTask(async () => src.get() + 1)
382
+ }
383
+ })
384
+ })
385
+
386
+ group('Task: resolve propagation', () => {
387
+ const wait = () => new Promise<void>(r => setTimeout(r, 0))
388
+
389
+ const src = createState(1)
390
+ const task = createTask(async () => src.get() * 2, {
391
+ value: 0,
392
+ })
393
+ createEffect(() => {
394
+ task.get()
395
+ })
396
+
397
+ let i = 1
398
+ bench('cause-effect', async () => {
399
+ batch(() => src.set(++i))
400
+ await wait()
401
+ })
402
+ })
403
+
404
+ /* === Sensor Benchmarks === */
405
+
406
+ group('Sensor: create + update (with equality)', () => {
407
+ bench('cause-effect', () => {
408
+ let setFn: (v: number) => void
409
+ const sensor = createSensor<number>(set => {
410
+ setFn = set
411
+ set(0)
412
+ return () => {}
413
+ })
414
+ createEffect(() => {
415
+ sensor.get()
416
+ })
417
+ for (let i = 0; i < 10; i++) {
418
+ // biome-ignore lint/style/noNonNullAssertion: assigned in start callback
419
+ setFn!(i)
420
+ }
421
+ })
422
+ })
423
+
424
+ group('Sensor: create + update (SKIP_EQUALITY)', () => {
425
+ bench('cause-effect', () => {
426
+ const obj = { x: 0 }
427
+ let setFn: (v: typeof obj) => void
428
+ const sensor = createSensor<typeof obj>(
429
+ set => {
430
+ setFn = set
431
+ set(obj)
432
+ return () => {}
433
+ },
434
+ { value: obj, equals: SKIP_EQUALITY },
435
+ )
436
+ createEffect(() => {
437
+ sensor.get()
438
+ })
439
+ for (let i = 0; i < 10; i++) {
440
+ obj.x = i
441
+ // biome-ignore lint/style/noNonNullAssertion: assigned in start callback
442
+ setFn!(obj)
443
+ }
444
+ })
445
+ })
446
+
447
+ /* === List Benchmarks === */
448
+
449
+ group('List: create 100 items', () => {
450
+ const items = Array.from({ length: 100 }, (_, i) => i + 1)
451
+ bench('cause-effect', () => {
452
+ createList(items)
453
+ })
454
+ })
455
+
456
+ group('List: add + remove 10 items', () => {
457
+ bench('cause-effect', () => {
458
+ const list = createList<number>([1, 2, 3])
459
+ for (let i = 0; i < 10; i++) list.add(i + 10)
460
+ for (let i = 0; i < 10; i++) list.remove(0)
461
+ })
462
+ })
463
+
464
+ group('List: sort 50 items', () => {
465
+ bench('cause-effect', () => {
466
+ const list = createList(
467
+ Array.from({ length: 50 }, () => Math.random() * 100),
468
+ )
469
+ list.sort((a, b) => a - b)
470
+ })
471
+ })
472
+
473
+ group('List: set (diff) 50 items', () => {
474
+ const initial = Array.from({ length: 50 }, (_, i) => i)
475
+ const updated = Array.from({ length: 50 }, (_, i) => i * 2)
476
+ bench('cause-effect', () => {
477
+ const list = createList(initial.slice())
478
+ list.set(updated)
479
+ })
480
+ })
481
+
482
+ group('List: reactive propagation', () => {
483
+ const list = createList([1, 2, 3])
484
+ const memo = createMemo(() => list.get().reduce((a, b) => a + b, 0))
485
+ createEffect(() => {
486
+ memo.get()
487
+ })
488
+
489
+ let i = 0
490
+ bench('cause-effect', () => {
491
+ list.set([++i, 2, 3])
492
+ })
493
+ })
494
+
495
+ /* === Collection Benchmarks === */
496
+
497
+ group('Collection: derive 50 items (sync)', () => {
498
+ bench('cause-effect', () => {
499
+ const list = createList(Array.from({ length: 50 }, (_, i) => i + 1))
500
+ const col = list.deriveCollection((v: number) => v * 2)
501
+ col.get()
502
+ })
503
+ })
504
+
505
+ group('Collection: chain 2 derivations', () => {
506
+ bench('cause-effect', () => {
507
+ const list = createList(Array.from({ length: 20 }, (_, i) => i + 1))
508
+ const col1 = list.deriveCollection((v: number) => v * 2)
509
+ const col2 = col1.deriveCollection((v: number) => v + 1)
510
+ col2.get()
511
+ })
512
+ })
513
+
514
+ group('Collection: reactive update', () => {
515
+ const list = createList([1, 2, 3, 4, 5])
516
+ const col = list.deriveCollection((v: number) => v * 10)
517
+ createEffect(() => {
518
+ col.get()
519
+ })
520
+
521
+ let i = 0
522
+ bench('cause-effect', () => {
523
+ list.set([++i, 2, 3, 4, 5])
524
+ })
525
+ })
526
+
527
+ /* === Store Benchmarks === */
528
+
529
+ group('Store: create with 10 properties', () => {
530
+ const obj = Object.fromEntries(
531
+ Array.from({ length: 10 }, (_, i) => [`key${i}`, i]),
532
+ )
533
+ bench('cause-effect', () => {
534
+ createStore(obj)
535
+ })
536
+ })
537
+
538
+ group('Store: property access + set', () => {
539
+ const store = createStore({ a: 1, b: 2, c: 3 })
540
+ createEffect(() => {
541
+ store.a.get()
542
+ })
543
+
544
+ let i = 1
545
+ bench('cause-effect', () => {
546
+ store.a.set(++i)
547
+ })
548
+ })
549
+
550
+ group('Store: set (diff) entire object', () => {
551
+ const store = createStore({ x: 0, y: 0, z: 0 })
552
+ createEffect(() => {
553
+ store.get()
554
+ })
555
+
556
+ let i = 0
557
+ bench('cause-effect', () => {
558
+ store.set({ x: ++i, y: i * 2, z: i * 3 })
559
+ })
560
+ })
561
+
562
+ group('Store: nested store propagation', () => {
563
+ const nested = createStore({
564
+ user: { name: 'Alice', prefs: { theme: 'light' } },
565
+ })
566
+ createEffect(() => {
567
+ nested.get()
568
+ })
569
+
570
+ let toggle = false
571
+ bench('cause-effect', () => {
572
+ toggle = !toggle
573
+ nested.user.prefs.theme.set(toggle ? 'dark' : 'light')
574
+ })
575
+ })
576
+
577
+ await run()