@e280/strata 0.0.0-4 → 0.0.0-6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -39
- package/package.json +10 -3
- package/s/index.ts +3 -5
- package/s/signals/index.ts +5 -0
- package/s/signals/parts/computed.ts +53 -0
- package/s/signals/parts/effect.ts +23 -0
- package/s/signals/parts/signal.ts +66 -0
- package/s/signals/signals.test.ts +181 -0
- package/s/tests.test.ts +45 -286
- package/s/tracker/index.ts +3 -0
- package/s/tracker/tracker.test.ts +40 -0
- package/s/tracker/tracker.ts +73 -0
- package/s/tree/index.ts +7 -0
- package/s/{parts/substrata.ts → tree/parts/branch.ts} +15 -9
- package/s/{parts/chronstrata.ts → tree/parts/chronobranch.ts} +8 -8
- package/s/{parts/strata.ts → tree/parts/trunk.ts} +14 -11
- package/s/{parts → tree/parts}/types.ts +8 -8
- package/s/{parts → tree/parts}/utils/setup.ts +4 -4
- package/s/tree/tree.test.ts +307 -0
- package/x/index.d.ts +3 -5
- package/x/index.js +3 -5
- package/x/index.js.map +1 -1
- package/x/signals/index.d.ts +3 -0
- package/x/signals/index.js +4 -0
- package/x/signals/index.js.map +1 -0
- package/x/signals/parts/computed.d.ts +14 -0
- package/x/signals/parts/computed.js +41 -0
- package/x/signals/parts/computed.js.map +1 -0
- package/x/signals/parts/effect.d.ts +5 -0
- package/x/signals/parts/effect.js +17 -0
- package/x/signals/parts/effect.js.map +1 -0
- package/x/signals/parts/signal.d.ts +18 -0
- package/x/signals/parts/signal.js +47 -0
- package/x/signals/parts/signal.js.map +1 -0
- package/x/signals/signals.test.d.ts +18 -0
- package/x/signals/signals.test.js +144 -0
- package/x/signals/signals.test.js.map +1 -0
- package/x/tests.test.js +46 -265
- package/x/tests.test.js.map +1 -1
- package/x/tracker/index.d.ts +1 -0
- package/x/tracker/index.js +2 -0
- package/x/tracker/index.js.map +1 -0
- package/x/tracker/tracker.d.ts +29 -0
- package/x/tracker/tracker.js +62 -0
- package/x/tracker/tracker.js.map +1 -0
- package/x/tracker/tracker.test.d.ts +6 -0
- package/x/tracker/tracker.test.js +32 -0
- package/x/tracker/tracker.test.js.map +1 -0
- package/x/tree/index.d.ts +5 -0
- package/x/tree/index.js +6 -0
- package/x/tree/index.js.map +1 -0
- package/x/tree/parts/branch.d.ts +15 -0
- package/x/{parts/substrata.js → tree/parts/branch.js} +13 -8
- package/x/tree/parts/branch.js.map +1 -0
- package/x/{parts/chronstrata.d.ts → tree/parts/chronobranch.d.ts} +6 -6
- package/x/{parts/chronstrata.js → tree/parts/chronobranch.js} +6 -6
- package/x/tree/parts/chronobranch.js.map +1 -0
- package/x/tree/parts/persistence.js.map +1 -0
- package/x/tree/parts/trunk.d.ts +17 -0
- package/x/{parts/strata.js → tree/parts/trunk.js} +13 -10
- package/x/tree/parts/trunk.js.map +1 -0
- package/x/{parts → tree/parts}/types.d.ts +8 -8
- package/x/{parts → tree/parts}/types.js.map +1 -1
- package/x/tree/parts/utils/process-options.js.map +1 -0
- package/x/tree/parts/utils/setup.d.ts +8 -0
- package/x/{parts → tree/parts}/utils/setup.js +3 -3
- package/x/tree/parts/utils/setup.js.map +1 -0
- package/x/tree/tree.test.d.ts +37 -0
- package/x/tree/tree.test.js +271 -0
- package/x/tree/tree.test.js.map +1 -0
- package/x/parts/chronstrata.js.map +0 -1
- package/x/parts/persistence.js.map +0 -1
- package/x/parts/strata.d.ts +0 -17
- package/x/parts/strata.js.map +0 -1
- package/x/parts/substrata.d.ts +0 -15
- package/x/parts/substrata.js.map +0 -1
- package/x/parts/utils/process-options.js.map +0 -1
- package/x/parts/utils/setup.d.ts +0 -8
- package/x/parts/utils/setup.js.map +0 -1
- /package/s/{parts → tree/parts}/persistence.ts +0 -0
- /package/s/{parts → tree/parts}/utils/process-options.ts +0 -0
- /package/x/{parts → tree/parts}/persistence.d.ts +0 -0
- /package/x/{parts → tree/parts}/persistence.js +0 -0
- /package/x/{parts → tree/parts}/types.js +0 -0
- /package/x/{parts → tree/parts}/utils/process-options.d.ts +0 -0
- /package/x/{parts → tree/parts}/utils/process-options.js +0 -0
package/README.md
CHANGED
|
@@ -1,26 +1,100 @@
|
|
|
1
1
|
|
|
2
2
|

|
|
3
3
|
|
|
4
|
+
<br/>
|
|
5
|
+
|
|
4
6
|
# ⛏️ strata
|
|
7
|
+
📦 `npm install @e280/strata`
|
|
8
|
+
probably my tenth state management library, lol
|
|
5
9
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
<br/>
|
|
11
|
+
|
|
12
|
+
### get in loser, we're managing state
|
|
13
|
+
🚦 **signals** — ephemeral view-level state
|
|
14
|
+
🌳 **tree** — persistent app-level state
|
|
15
|
+
🪄 **tracker** — reactivity integration hub
|
|
16
|
+
|
|
17
|
+
<br/>
|
|
18
|
+
|
|
19
|
+
> [!TIP]
|
|
20
|
+
> incredibly, signals and trees are interoperable.
|
|
21
|
+
> that means, effects and computeds are responsive to changes in tree state.
|
|
13
22
|
|
|
14
23
|
<br/>
|
|
15
24
|
|
|
16
|
-
##
|
|
25
|
+
## 🚦 signals — *ephemeral view-level state*
|
|
26
|
+
- `@e280/strata/signals`
|
|
27
|
+
```ts
|
|
28
|
+
import {signal, effect, computed} from "@e280/strata"
|
|
29
|
+
```
|
|
30
|
+
- **signals are little bundles of joy**
|
|
31
|
+
```ts
|
|
32
|
+
const count = signal(0)
|
|
33
|
+
|
|
34
|
+
console.log(count.get())
|
|
35
|
+
// 0
|
|
17
36
|
|
|
18
|
-
|
|
37
|
+
count.set(1)
|
|
38
|
+
|
|
39
|
+
console.log(count.value)
|
|
40
|
+
// 1
|
|
41
|
+
```
|
|
42
|
+
- components/views will auto rerender when relevant signals change
|
|
43
|
+
(if your component/view library is cool an integrates with `tracker`)
|
|
44
|
+
- three ways to get it
|
|
45
|
+
```ts
|
|
46
|
+
count() // 1
|
|
47
|
+
count.get() // 1
|
|
48
|
+
count.value // 1
|
|
49
|
+
```
|
|
50
|
+
- three ways to set it
|
|
51
|
+
```ts
|
|
52
|
+
await count(2)
|
|
53
|
+
await count.set(2)
|
|
54
|
+
count.value = 2
|
|
55
|
+
```
|
|
56
|
+
- using `await` here allows you to wait for downstream effects to finish
|
|
57
|
+
- **effects are run when the relevant signals change**
|
|
58
|
+
```ts
|
|
59
|
+
effect(() => console.log(count()))
|
|
60
|
+
// 2
|
|
61
|
+
|
|
62
|
+
count.value++
|
|
63
|
+
// 3
|
|
64
|
+
```
|
|
65
|
+
- **computed signals are super lazy**
|
|
66
|
+
```ts
|
|
67
|
+
const tenly = computed(() => {
|
|
68
|
+
console.log("recomputed!")
|
|
69
|
+
return count() * 10
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
console.log(tenly())
|
|
73
|
+
// "recomputed!"
|
|
74
|
+
// 30
|
|
75
|
+
|
|
76
|
+
await count(4)
|
|
77
|
+
|
|
78
|
+
console.log(tenly.value)
|
|
79
|
+
// "recomputed!"
|
|
80
|
+
// 40
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
<br/>
|
|
84
|
+
|
|
85
|
+
## 🌳 tree — *persistent app-level state*
|
|
86
|
+
- `@e280/strata/tree`
|
|
87
|
+
- single-source-of-truth state tree
|
|
88
|
+
- immutable except for `mutate(fn)` calls
|
|
89
|
+
- undo/redo history, cross-tab sync, localStorage persistence
|
|
90
|
+
- no spooky-dookie proxy magic — just god's honest javascript
|
|
91
|
+
|
|
92
|
+
#### `Trunk` is your app's state tree root
|
|
19
93
|
- better stick to json-friendly serializable data
|
|
20
94
|
```ts
|
|
21
|
-
import {
|
|
95
|
+
import {Trunk} from "@e280/strata"
|
|
22
96
|
|
|
23
|
-
const
|
|
97
|
+
const trunk = new Trunk({
|
|
24
98
|
count: 0,
|
|
25
99
|
snacks: {
|
|
26
100
|
peanuts: 8,
|
|
@@ -28,26 +102,26 @@
|
|
|
28
102
|
},
|
|
29
103
|
})
|
|
30
104
|
|
|
31
|
-
|
|
32
|
-
|
|
105
|
+
trunk.state.count // 0
|
|
106
|
+
trunk.state.snacks.peanuts // 8
|
|
33
107
|
```
|
|
34
108
|
|
|
35
|
-
|
|
109
|
+
#### formal mutations to change state
|
|
36
110
|
- ⛔ informal mutations are denied
|
|
37
111
|
```ts
|
|
38
|
-
|
|
112
|
+
trunk.state.count++ // error is thrown
|
|
39
113
|
```
|
|
40
114
|
- ✅ formal mutations are allowed
|
|
41
115
|
```ts
|
|
42
|
-
await
|
|
116
|
+
await trunk.mutate(s => s.count++)
|
|
43
117
|
```
|
|
44
118
|
|
|
45
|
-
|
|
119
|
+
#### `Branch` is a view into a subtree
|
|
46
120
|
- it's a lens, make lots of them, pass 'em around your app
|
|
47
121
|
```ts
|
|
48
|
-
const snacks =
|
|
122
|
+
const snacks = trunk.branch(s => s.snacks)
|
|
49
123
|
```
|
|
50
|
-
- run
|
|
124
|
+
- run branch mutations
|
|
51
125
|
```ts
|
|
52
126
|
await snacks.mutate(s => s.peanuts++)
|
|
53
127
|
```
|
|
@@ -55,43 +129,42 @@
|
|
|
55
129
|
```ts
|
|
56
130
|
await snacks.mutate(s => s.bag.push("salt"))
|
|
57
131
|
```
|
|
58
|
-
- you can
|
|
132
|
+
- you can branch a branch
|
|
59
133
|
|
|
60
|
-
|
|
61
|
-
-
|
|
134
|
+
#### watch for mutations
|
|
135
|
+
- on the trunk, we can listen deeply for mutations within the whole tree
|
|
62
136
|
```ts
|
|
63
|
-
|
|
137
|
+
trunk.watch(s => console.log(s.count))
|
|
64
138
|
```
|
|
65
|
-
-
|
|
139
|
+
- whereas branch listeners don't care about changes outside their scope
|
|
66
140
|
```ts
|
|
67
141
|
snacks.watch(s => console.log(s.peanuts))
|
|
68
142
|
```
|
|
69
143
|
- watch returns a fn to stop listening
|
|
70
144
|
```ts
|
|
71
|
-
const stop =
|
|
145
|
+
const stop = trunk.watch(s => console.log(s.count))
|
|
72
146
|
stop() // stop listening
|
|
73
147
|
```
|
|
74
148
|
|
|
75
|
-
|
|
149
|
+
### only discerning high-class aristocrats are permitted beyond this point
|
|
76
150
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
### `Strata.setup` for localStorage persistence etc
|
|
151
|
+
#### `Trunk.setup` for localStorage persistence etc
|
|
80
152
|
- simple setup
|
|
81
153
|
```ts
|
|
82
|
-
const {
|
|
154
|
+
const {trunk} = await Trunk.setup({
|
|
83
155
|
version: 1, // 👈 bump whenever your change state schema!
|
|
84
156
|
initialState: {count: 0},
|
|
85
157
|
})
|
|
86
158
|
```
|
|
159
|
+
- uses localStorage by default
|
|
87
160
|
- it's compatible with [`@e280/kv`](https://github.com/e280/kv)
|
|
88
161
|
```ts
|
|
89
162
|
import {Kv, StorageDriver} from "@e280/kv"
|
|
90
163
|
|
|
91
164
|
const kv = new Kv(new StorageDriver())
|
|
92
|
-
const store = kv.store<any>("
|
|
165
|
+
const store = kv.store<any>("appState")
|
|
93
166
|
|
|
94
|
-
const {
|
|
167
|
+
const {trunk} = await Trunk.setup({
|
|
95
168
|
version: 1,
|
|
96
169
|
initialState: {count: 0},
|
|
97
170
|
persistence: {
|
|
@@ -101,21 +174,21 @@
|
|
|
101
174
|
})
|
|
102
175
|
```
|
|
103
176
|
|
|
104
|
-
|
|
177
|
+
#### `Chronobranch` for undo/redo history
|
|
105
178
|
- first, put a `Chronicle` into your state tree
|
|
106
179
|
```ts
|
|
107
|
-
const
|
|
180
|
+
const trunk = new Trunk({
|
|
108
181
|
count: 0,
|
|
109
|
-
snacks:
|
|
182
|
+
snacks: Trunk.chronicle({
|
|
110
183
|
peanuts: 8,
|
|
111
184
|
bag: ["popcorn", "butter"],
|
|
112
185
|
}),
|
|
113
186
|
})
|
|
114
187
|
```
|
|
115
188
|
- *big-brain moment:* the whole chronicle *itself* is stored in the state.. serializable.. think persistence — user can close their project, reopen, and their undo/redo history is still chillin' — *brat girl summer*
|
|
116
|
-
- second, make a `
|
|
189
|
+
- second, make a `Chronobranch` which is like a branch
|
|
117
190
|
```ts
|
|
118
|
-
const snacks =
|
|
191
|
+
const snacks = trunk.chronobranch(64, s => s.snacks)
|
|
119
192
|
// \
|
|
120
193
|
// how many past snapshots to store
|
|
121
194
|
```
|
|
@@ -134,8 +207,15 @@
|
|
|
134
207
|
snacks.undoable // 2
|
|
135
208
|
snacks.redoable // 1
|
|
136
209
|
```
|
|
137
|
-
-
|
|
138
|
-
- plz pinky-swear right now, that you won't create a
|
|
210
|
+
- chronobranch can have its own branch — all their mutations advance history
|
|
211
|
+
- plz pinky-swear right now, that you won't create a chronobranch under a branch under another chronobranch 💀
|
|
212
|
+
|
|
213
|
+
<br/>
|
|
214
|
+
|
|
215
|
+
## 🪄 tracker — integrations
|
|
216
|
+
- `@e280/strata/tracker`
|
|
217
|
+
- all reactivity is orchestrated by the `tracker`
|
|
218
|
+
- if you are integrating a new state object, or a new view layer that needs to react to state changes, just read [tracker.ts](./s/tracker/tracker.ts)
|
|
139
219
|
|
|
140
220
|
<br/>
|
|
141
221
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@e280/strata",
|
|
3
|
-
"version": "0.0.0-
|
|
3
|
+
"version": "0.0.0-6",
|
|
4
4
|
"description": "state management",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Chase Moskal <chasemoskal@gmail.com>",
|
|
@@ -10,6 +10,13 @@
|
|
|
10
10
|
"x",
|
|
11
11
|
"s"
|
|
12
12
|
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./x/index.js",
|
|
15
|
+
"./signals": "./x/signals/index.js",
|
|
16
|
+
"./tracker": "./x/tracker/index.js",
|
|
17
|
+
"./tree": "./x/tree/index.js",
|
|
18
|
+
"./*": "./*"
|
|
19
|
+
},
|
|
13
20
|
"scripts": {
|
|
14
21
|
"build": "run-s _clean _links _tsc",
|
|
15
22
|
"test": "node x/tests.test.js",
|
|
@@ -24,7 +31,7 @@
|
|
|
24
31
|
},
|
|
25
32
|
"devDependencies": {
|
|
26
33
|
"@e280/science": "^0.0.5",
|
|
27
|
-
"@types/node": "^24.0.
|
|
34
|
+
"@types/node": "^24.0.7",
|
|
28
35
|
"npm-run-all": "^4.1.5",
|
|
29
36
|
"typescript": "^5.8.3"
|
|
30
37
|
},
|
|
@@ -41,6 +48,6 @@
|
|
|
41
48
|
"url": "https://github.com/e280/strata/issues"
|
|
42
49
|
},
|
|
43
50
|
"dependencies": {
|
|
44
|
-
"@e280/stz": "^0.0.0-
|
|
51
|
+
"@e280/stz": "^0.0.0-27"
|
|
45
52
|
}
|
|
46
53
|
}
|
package/s/index.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
export * from "./
|
|
3
|
-
export * from "./
|
|
4
|
-
export * from "./
|
|
5
|
-
export * from "./parts/substrata.js"
|
|
6
|
-
export * from "./parts/types.js"
|
|
2
|
+
export * from "./signals/index.js"
|
|
3
|
+
export * from "./tracker/index.js"
|
|
4
|
+
export * from "./tree/index.js"
|
|
7
5
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
|
|
2
|
+
import {initEffect} from "./effect.js"
|
|
3
|
+
import {SignalCore} from "./signal.js"
|
|
4
|
+
|
|
5
|
+
export type Computed<V> = {(): V} & ComputedCore<V>
|
|
6
|
+
|
|
7
|
+
export function computed<V>(fn: () => V) {
|
|
8
|
+
const core = new ComputedCore<V>(fn)
|
|
9
|
+
|
|
10
|
+
function f(): V {
|
|
11
|
+
return (f as any).value
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
Object.setPrototypeOf(f, ComputedCore.prototype)
|
|
15
|
+
Object.assign(f, core)
|
|
16
|
+
|
|
17
|
+
return f as Computed<V>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class ComputedCore<V> extends SignalCore<V> {
|
|
21
|
+
_dirty = false
|
|
22
|
+
_formula: () => V
|
|
23
|
+
_effect: (() => void) | undefined
|
|
24
|
+
|
|
25
|
+
constructor(formula: () => V) {
|
|
26
|
+
super(undefined as any)
|
|
27
|
+
this._formula = formula
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get() {
|
|
31
|
+
if (!this._effect) {
|
|
32
|
+
const {result, dispose} = initEffect(this._formula, () => this._dirty = true)
|
|
33
|
+
this._effect = dispose
|
|
34
|
+
this.sneak = result
|
|
35
|
+
}
|
|
36
|
+
if (this._dirty) {
|
|
37
|
+
this._dirty = false
|
|
38
|
+
super.set(this._formula())
|
|
39
|
+
}
|
|
40
|
+
return super.get()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async set() {
|
|
44
|
+
throw new Error("computed is readonly")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
dispose() {
|
|
48
|
+
if (this._effect)
|
|
49
|
+
this._effect()
|
|
50
|
+
super.dispose()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
|
|
2
|
+
import {debounce} from "@e280/stz"
|
|
3
|
+
import {tracker} from "../../tracker/tracker.js"
|
|
4
|
+
|
|
5
|
+
export function effect<C = void>(collector: () => C, responder: () => void = collector) {
|
|
6
|
+
return initEffect<C>(collector, responder).dispose
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function initEffect<C = void>(collector: () => C, responder: () => void = collector) {
|
|
10
|
+
const {seen, result} = tracker.seen(collector)
|
|
11
|
+
const fn = debounce(0, responder)
|
|
12
|
+
|
|
13
|
+
const disposers: (() => void)[] = []
|
|
14
|
+
const dispose = () => disposers.forEach(d => d())
|
|
15
|
+
|
|
16
|
+
for (const saw of seen) {
|
|
17
|
+
const dispose = tracker.changed(saw, fn)
|
|
18
|
+
disposers.push(dispose)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {result, dispose}
|
|
22
|
+
}
|
|
23
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
|
|
2
|
+
import {sub} from "@e280/stz"
|
|
3
|
+
import {tracker} from "../../tracker/tracker.js"
|
|
4
|
+
|
|
5
|
+
export type Signal<V> = {
|
|
6
|
+
(): V
|
|
7
|
+
(v: V): Promise<void>
|
|
8
|
+
(v?: V): V | Promise<void>
|
|
9
|
+
} & SignalCore<V>
|
|
10
|
+
|
|
11
|
+
export function signal<V>(value: V) {
|
|
12
|
+
const core = new SignalCore(value)
|
|
13
|
+
|
|
14
|
+
function fn(): V
|
|
15
|
+
function fn(v: V): Promise<void>
|
|
16
|
+
function fn(v?: V): V | Promise<void> {
|
|
17
|
+
return v !== undefined
|
|
18
|
+
? (fn as any).set(v)
|
|
19
|
+
: (fn as any).get()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
Object.setPrototypeOf(fn, SignalCore.prototype)
|
|
23
|
+
Object.assign(fn, core)
|
|
24
|
+
|
|
25
|
+
return fn as Signal<V>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class SignalCore<V> {
|
|
29
|
+
on = sub<[V]>()
|
|
30
|
+
published: Promise<V>
|
|
31
|
+
|
|
32
|
+
constructor(public sneak: V) {
|
|
33
|
+
this.published = Promise.resolve(sneak)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get() {
|
|
37
|
+
tracker.see(this)
|
|
38
|
+
return this.sneak
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async set(v: V) {
|
|
42
|
+
if (v !== this.sneak)
|
|
43
|
+
await this.publish(v)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get value() {
|
|
47
|
+
return this.get()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
set value(v: V) {
|
|
51
|
+
this.set(v)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async publish(v = this.get()) {
|
|
55
|
+
this.sneak = v
|
|
56
|
+
await Promise.all([
|
|
57
|
+
tracker.change(this),
|
|
58
|
+
this.published = this.on.pub(v).then(() => v),
|
|
59
|
+
])
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
dispose() {
|
|
63
|
+
this.on.clear()
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
|
|
2
|
+
import {Science, test, expect} from "@e280/science"
|
|
3
|
+
|
|
4
|
+
import {effect} from "./parts/effect.js"
|
|
5
|
+
import {signal} from "./parts/signal.js"
|
|
6
|
+
import {computed} from "./parts/computed.js"
|
|
7
|
+
|
|
8
|
+
export default Science.suite({
|
|
9
|
+
"signal get/set value": test(async() => {
|
|
10
|
+
const count = signal(0)
|
|
11
|
+
expect(count.value).is(0)
|
|
12
|
+
|
|
13
|
+
count.value++
|
|
14
|
+
expect(count.value).is(1)
|
|
15
|
+
|
|
16
|
+
count.value = 5
|
|
17
|
+
expect(count.value).is(5)
|
|
18
|
+
}),
|
|
19
|
+
|
|
20
|
+
"signal fn syntax": test(async() => {
|
|
21
|
+
const count = signal(0)
|
|
22
|
+
expect(count()).is(0)
|
|
23
|
+
|
|
24
|
+
count(count() + 1)
|
|
25
|
+
expect(count()).is(1)
|
|
26
|
+
|
|
27
|
+
count(5)
|
|
28
|
+
expect(count()).is(5)
|
|
29
|
+
}),
|
|
30
|
+
|
|
31
|
+
"signal syntax interop": test(async() => {
|
|
32
|
+
const count = signal(0)
|
|
33
|
+
|
|
34
|
+
count.value = 1
|
|
35
|
+
expect(count()).is(1)
|
|
36
|
+
}),
|
|
37
|
+
|
|
38
|
+
"signal on is not debounced": test(async() => {
|
|
39
|
+
const count = signal(1)
|
|
40
|
+
let runs = 0
|
|
41
|
+
count.on(() => void runs++)
|
|
42
|
+
await count.set(2)
|
|
43
|
+
await count.set(3)
|
|
44
|
+
expect(runs).is(2)
|
|
45
|
+
}),
|
|
46
|
+
|
|
47
|
+
"signal on only fires on change": test(async() => {
|
|
48
|
+
const count = signal(1)
|
|
49
|
+
let runs = 0
|
|
50
|
+
count.on(() => void runs++)
|
|
51
|
+
await count.set(2)
|
|
52
|
+
await count.set(2)
|
|
53
|
+
expect(runs).is(1)
|
|
54
|
+
}),
|
|
55
|
+
|
|
56
|
+
"effect tracks signal changes": test(async() => {
|
|
57
|
+
const count = signal(1)
|
|
58
|
+
let doubled = 0
|
|
59
|
+
|
|
60
|
+
effect(() => doubled = count.value * 2)
|
|
61
|
+
expect(doubled).is(2)
|
|
62
|
+
|
|
63
|
+
await count.set(3)
|
|
64
|
+
expect(doubled).is(6)
|
|
65
|
+
}),
|
|
66
|
+
|
|
67
|
+
"effect is only called when signal actually changes": test(async() => {
|
|
68
|
+
const count = signal(1)
|
|
69
|
+
let runs = 0
|
|
70
|
+
effect(() => {
|
|
71
|
+
count.get()
|
|
72
|
+
runs++
|
|
73
|
+
})
|
|
74
|
+
expect(runs).is(1)
|
|
75
|
+
await count.set(999)
|
|
76
|
+
expect(runs).is(2)
|
|
77
|
+
await count.set(999)
|
|
78
|
+
expect(runs).is(2)
|
|
79
|
+
}),
|
|
80
|
+
|
|
81
|
+
"effects are debounced": test(async() => {
|
|
82
|
+
const count = signal(1)
|
|
83
|
+
let runs = 0
|
|
84
|
+
effect(() => {
|
|
85
|
+
count.get()
|
|
86
|
+
runs++
|
|
87
|
+
})
|
|
88
|
+
expect(runs).is(1)
|
|
89
|
+
count.value++
|
|
90
|
+
count.value++
|
|
91
|
+
await count.set(count.get() + 1)
|
|
92
|
+
expect(runs).is(2)
|
|
93
|
+
}),
|
|
94
|
+
|
|
95
|
+
"effects can be disposed": test(async() => {
|
|
96
|
+
const count = signal(1)
|
|
97
|
+
let doubled = 0
|
|
98
|
+
|
|
99
|
+
const dispose = effect(() => doubled = count.value * 2)
|
|
100
|
+
expect(doubled).is(2)
|
|
101
|
+
|
|
102
|
+
await count.set(3)
|
|
103
|
+
expect(doubled).is(6)
|
|
104
|
+
|
|
105
|
+
dispose()
|
|
106
|
+
await count.set(4)
|
|
107
|
+
expect(doubled).is(6) // old value
|
|
108
|
+
}),
|
|
109
|
+
|
|
110
|
+
"signal set promise waits for effects": test(async() => {
|
|
111
|
+
const count = signal(1)
|
|
112
|
+
let doubled = 0
|
|
113
|
+
|
|
114
|
+
effect(() => doubled = count.value * 2)
|
|
115
|
+
expect(doubled).is(2)
|
|
116
|
+
|
|
117
|
+
await count.set(3)
|
|
118
|
+
expect(doubled).is(6)
|
|
119
|
+
}),
|
|
120
|
+
|
|
121
|
+
"effect only runs on change": test(async() => {
|
|
122
|
+
const sig = signal("a")
|
|
123
|
+
let runs = 0
|
|
124
|
+
|
|
125
|
+
effect(() => {
|
|
126
|
+
sig.value
|
|
127
|
+
runs++
|
|
128
|
+
})
|
|
129
|
+
expect(runs).is(1)
|
|
130
|
+
|
|
131
|
+
await sig.set("a")
|
|
132
|
+
expect(runs).is(1)
|
|
133
|
+
|
|
134
|
+
await sig.set("b")
|
|
135
|
+
expect(runs).is(2)
|
|
136
|
+
}),
|
|
137
|
+
|
|
138
|
+
"computed values": test(async() => {
|
|
139
|
+
const a = signal(2)
|
|
140
|
+
const b = signal(3)
|
|
141
|
+
const sum = computed(() => a.value + b.value)
|
|
142
|
+
expect(sum.value).is(5)
|
|
143
|
+
|
|
144
|
+
await a.set(5)
|
|
145
|
+
expect(sum.value).is(8)
|
|
146
|
+
|
|
147
|
+
await b.set(7)
|
|
148
|
+
expect(sum.value).is(12)
|
|
149
|
+
}),
|
|
150
|
+
|
|
151
|
+
"computed is lazy": test(async() => {
|
|
152
|
+
const a = signal(1)
|
|
153
|
+
let runs = 0
|
|
154
|
+
|
|
155
|
+
const comp = computed(() => {
|
|
156
|
+
runs++
|
|
157
|
+
return a.value * 10
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
expect(runs).is(0)
|
|
161
|
+
expect(comp.value).is(10)
|
|
162
|
+
expect(runs).is(1)
|
|
163
|
+
|
|
164
|
+
await a.set(2)
|
|
165
|
+
expect(runs).is(1)
|
|
166
|
+
expect(comp.value).is(20)
|
|
167
|
+
expect(runs).is(2)
|
|
168
|
+
}),
|
|
169
|
+
|
|
170
|
+
"computed fn syntax": test(async() => {
|
|
171
|
+
const a = signal(2)
|
|
172
|
+
const b = signal(3)
|
|
173
|
+
const sum = computed(() => a.value + b.value)
|
|
174
|
+
expect(sum.value).is(5)
|
|
175
|
+
|
|
176
|
+
await a.set(5)
|
|
177
|
+
expect(sum.value).is(8)
|
|
178
|
+
expect(sum()).is(8)
|
|
179
|
+
}),
|
|
180
|
+
})
|
|
181
|
+
|