@zeix/cause-effect 0.17.3 → 0.18.1
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/.ai-context.md +169 -227
- package/.cursorrules +41 -35
- package/.github/copilot-instructions.md +176 -116
- package/ARCHITECTURE.md +276 -0
- package/CHANGELOG.md +29 -0
- package/CLAUDE.md +201 -143
- package/GUIDE.md +298 -0
- package/README.md +246 -193
- package/REQUIREMENTS.md +100 -0
- package/bench/reactivity.bench.ts +577 -0
- package/context7.json +4 -0
- package/examples/events-sensor.ts +187 -0
- package/examples/selector-sensor.ts +173 -0
- package/index.dev.js +1390 -1008
- package/index.js +1 -1
- package/index.ts +60 -74
- package/package.json +5 -2
- package/skills/changelog-keeper/SKILL.md +59 -0
- package/skills/changelog-keeper/agents/openai.yaml +4 -0
- package/src/errors.ts +118 -74
- package/src/graph.ts +612 -0
- package/src/nodes/collection.ts +512 -0
- package/src/nodes/effect.ts +149 -0
- package/src/nodes/list.ts +589 -0
- package/src/nodes/memo.ts +148 -0
- package/src/nodes/sensor.ts +149 -0
- package/src/nodes/state.ts +135 -0
- package/src/nodes/store.ts +378 -0
- package/src/nodes/task.ts +174 -0
- package/src/signal.ts +112 -66
- package/src/util.ts +26 -57
- package/test/batch.test.ts +96 -62
- package/test/benchmark.test.ts +473 -487
- package/test/collection.test.ts +456 -707
- package/test/effect.test.ts +293 -696
- package/test/list.test.ts +335 -592
- package/test/memo.test.ts +574 -0
- package/test/regression.test.ts +156 -0
- package/test/scope.test.ts +191 -0
- package/test/sensor.test.ts +454 -0
- package/test/signal.test.ts +220 -213
- package/test/state.test.ts +217 -265
- package/test/store.test.ts +346 -446
- package/test/task.test.ts +529 -0
- package/test/untrack.test.ts +167 -0
- package/types/index.d.ts +13 -15
- package/types/src/errors.d.ts +73 -17
- package/types/src/graph.d.ts +218 -0
- package/types/src/nodes/collection.d.ts +69 -0
- package/types/src/nodes/effect.d.ts +48 -0
- package/types/src/nodes/list.d.ts +66 -0
- package/types/src/nodes/memo.d.ts +63 -0
- package/types/src/nodes/sensor.d.ts +81 -0
- package/types/src/nodes/state.d.ts +78 -0
- package/types/src/nodes/store.d.ts +51 -0
- package/types/src/nodes/task.d.ts +79 -0
- package/types/src/signal.d.ts +43 -29
- package/types/src/util.d.ts +9 -16
- package/archive/benchmark.ts +0 -683
- package/archive/collection.ts +0 -253
- package/archive/composite.ts +0 -85
- package/archive/computed.ts +0 -195
- package/archive/list.ts +0 -483
- package/archive/memo.ts +0 -139
- package/archive/state.ts +0 -90
- package/archive/store.ts +0 -298
- package/archive/task.ts +0 -189
- package/src/classes/collection.ts +0 -245
- package/src/classes/computed.ts +0 -349
- package/src/classes/list.ts +0 -343
- package/src/classes/ref.ts +0 -70
- package/src/classes/state.ts +0 -102
- package/src/classes/store.ts +0 -262
- package/src/diff.ts +0 -138
- package/src/effect.ts +0 -93
- package/src/match.ts +0 -45
- package/src/resolve.ts +0 -49
- package/src/system.ts +0 -257
- package/test/computed.test.ts +0 -1108
- package/test/diff.test.ts +0 -955
- package/test/match.test.ts +0 -388
- package/test/ref.test.ts +0 -353
- package/test/resolve.test.ts +0 -154
- package/types/src/classes/collection.d.ts +0 -45
- package/types/src/classes/computed.d.ts +0 -94
- package/types/src/classes/list.d.ts +0 -43
- package/types/src/classes/ref.d.ts +0 -35
- package/types/src/classes/state.d.ts +0 -49
- package/types/src/classes/store.d.ts +0 -52
- package/types/src/diff.d.ts +0 -28
- package/types/src/effect.d.ts +0 -15
- package/types/src/match.d.ts +0 -21
- package/types/src/resolve.d.ts +0 -29
- package/types/src/system.d.ts +0 -78
package/REQUIREMENTS.md
ADDED
|
@@ -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()
|
package/context7.json
ADDED