@e280/strata 0.0.0-5 → 0.0.0-7
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 +140 -39
- package/package.json +9 -2
- 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} +11 -10
- package/s/{parts/chronstrata.ts → tree/parts/chronobranch.ts} +8 -8
- package/s/{parts/strata.ts → tree/parts/trunk.ts} +14 -13
- package/s/{parts → tree/parts}/types.ts +8 -8
- package/s/{parts → tree/parts}/utils/setup.ts +9 -9
- 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} +10 -9
- 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 -12
- 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 +8 -8
- 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,119 @@
|
|
|
1
1
|
|
|
2
2
|

|
|
3
3
|
|
|
4
|
+
<br/>
|
|
5
|
+
|
|
4
6
|
# ⛏️ strata
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
### get in loser, we're managing state
|
|
9
|
+
📦 `npm install @e280/strata`
|
|
10
|
+
🧙♂️ probably my tenth state management library, lol
|
|
11
|
+
|
|
12
|
+
🚦 **signals** — ephemeral view-level state
|
|
13
|
+
🌳 **tree** — persistent app-level state
|
|
14
|
+
🪄 **tracker** — reactivity integration hub
|
|
13
15
|
|
|
14
16
|
<br/>
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
> [!TIP]
|
|
19
|
+
> incredibly, signals and trees are interoperable.
|
|
20
|
+
> that means, effects and computeds are responsive to changes in tree state.
|
|
21
|
+
|
|
22
|
+
<br/>
|
|
23
|
+
|
|
24
|
+
## 🚦 signals — *ephemeral view-level state*
|
|
25
|
+
```ts
|
|
26
|
+
import {signal, effect, computed} from "@e280/strata"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### each signal holds a value
|
|
30
|
+
- **create a signal**
|
|
31
|
+
```ts
|
|
32
|
+
const count = signal(0)
|
|
33
|
+
```
|
|
34
|
+
- **read a signal**
|
|
35
|
+
```ts
|
|
36
|
+
count() // 0
|
|
37
|
+
```
|
|
38
|
+
- **set a signal**
|
|
39
|
+
```ts
|
|
40
|
+
count(1)
|
|
41
|
+
```
|
|
42
|
+
- **set a signal, and await effect propagation**
|
|
43
|
+
```ts
|
|
44
|
+
await count(2)
|
|
45
|
+
```
|
|
46
|
+
- **signals are for auto rerendering your ui.**
|
|
47
|
+
components/views will auto rerender when relevant signals change
|
|
48
|
+
— well only if your ui lib is cool and integrates `tracker`.
|
|
17
49
|
|
|
18
|
-
###
|
|
50
|
+
### pick your poison
|
|
51
|
+
- **signals hipster fn syntax**
|
|
52
|
+
```ts
|
|
53
|
+
count() // get
|
|
54
|
+
await count(2) // set
|
|
55
|
+
```
|
|
56
|
+
- **signals get/set syntax**
|
|
57
|
+
```ts
|
|
58
|
+
count.get() // get
|
|
59
|
+
await count.set(2) // set
|
|
60
|
+
```
|
|
61
|
+
- **signals .value accessor syntax**
|
|
62
|
+
```ts
|
|
63
|
+
count.value // get
|
|
64
|
+
count.value = 2 // set
|
|
65
|
+
```
|
|
66
|
+
value pattern is nice for this vibe
|
|
67
|
+
```ts
|
|
68
|
+
count.value++
|
|
69
|
+
count.value += 1
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### effects
|
|
73
|
+
- **effects run when the relevant signals change**
|
|
74
|
+
```ts
|
|
75
|
+
effect(() => console.log(count()))
|
|
76
|
+
// 1
|
|
77
|
+
// the system detects 'count' is relevant
|
|
78
|
+
|
|
79
|
+
count.value++
|
|
80
|
+
// 2
|
|
81
|
+
// when count is changed, the effect fn is run
|
|
82
|
+
```
|
|
83
|
+
- **computed signals are super lazy**
|
|
84
|
+
they only run if and when you get the value
|
|
85
|
+
```ts
|
|
86
|
+
const tenly = computed(() => {
|
|
87
|
+
console.log("recomputed!")
|
|
88
|
+
return count() * 10
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
console.log(tenly())
|
|
92
|
+
// "recomputed!"
|
|
93
|
+
// 20
|
|
94
|
+
|
|
95
|
+
await count(3)
|
|
96
|
+
|
|
97
|
+
console.log(tenly.value)
|
|
98
|
+
// "recomputed!"
|
|
99
|
+
// 30
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
<br/>
|
|
103
|
+
|
|
104
|
+
## 🌳 tree — *persistent app-level state*
|
|
105
|
+
- single-source-of-truth state tree
|
|
106
|
+
- immutable except for `mutate(fn)` calls
|
|
107
|
+
- undo/redo history, cross-tab sync, localStorage persistence
|
|
108
|
+
- no spooky-dookie proxy magic — just god's honest javascript
|
|
109
|
+
- separate but compatible with signals
|
|
110
|
+
|
|
111
|
+
#### `Trunk` is your app's state tree root
|
|
19
112
|
- better stick to json-friendly serializable data
|
|
20
113
|
```ts
|
|
21
|
-
import {
|
|
114
|
+
import {Trunk} from "@e280/strata"
|
|
22
115
|
|
|
23
|
-
const
|
|
116
|
+
const trunk = new Trunk({
|
|
24
117
|
count: 0,
|
|
25
118
|
snacks: {
|
|
26
119
|
peanuts: 8,
|
|
@@ -28,26 +121,26 @@
|
|
|
28
121
|
},
|
|
29
122
|
})
|
|
30
123
|
|
|
31
|
-
|
|
32
|
-
|
|
124
|
+
trunk.state.count // 0
|
|
125
|
+
trunk.state.snacks.peanuts // 8
|
|
33
126
|
```
|
|
34
127
|
|
|
35
|
-
|
|
128
|
+
#### formal mutations to change state
|
|
36
129
|
- ⛔ informal mutations are denied
|
|
37
130
|
```ts
|
|
38
|
-
|
|
131
|
+
trunk.state.count++ // error is thrown
|
|
39
132
|
```
|
|
40
133
|
- ✅ formal mutations are allowed
|
|
41
134
|
```ts
|
|
42
|
-
await
|
|
135
|
+
await trunk.mutate(s => s.count++)
|
|
43
136
|
```
|
|
44
137
|
|
|
45
|
-
|
|
138
|
+
#### `Branch` is a view into a subtree
|
|
46
139
|
- it's a lens, make lots of them, pass 'em around your app
|
|
47
140
|
```ts
|
|
48
|
-
const snacks =
|
|
141
|
+
const snacks = trunk.branch(s => s.snacks)
|
|
49
142
|
```
|
|
50
|
-
- run
|
|
143
|
+
- run branch mutations
|
|
51
144
|
```ts
|
|
52
145
|
await snacks.mutate(s => s.peanuts++)
|
|
53
146
|
```
|
|
@@ -55,43 +148,42 @@
|
|
|
55
148
|
```ts
|
|
56
149
|
await snacks.mutate(s => s.bag.push("salt"))
|
|
57
150
|
```
|
|
58
|
-
- you can
|
|
151
|
+
- you can branch a branch
|
|
59
152
|
|
|
60
|
-
|
|
61
|
-
-
|
|
153
|
+
#### watch for mutations
|
|
154
|
+
- on the trunk, we can listen deeply for mutations within the whole tree
|
|
62
155
|
```ts
|
|
63
|
-
|
|
156
|
+
trunk.watch(s => console.log(s.count))
|
|
64
157
|
```
|
|
65
|
-
-
|
|
158
|
+
- whereas branch listeners don't care about changes outside their scope
|
|
66
159
|
```ts
|
|
67
160
|
snacks.watch(s => console.log(s.peanuts))
|
|
68
161
|
```
|
|
69
162
|
- watch returns a fn to stop listening
|
|
70
163
|
```ts
|
|
71
|
-
const stop =
|
|
164
|
+
const stop = trunk.watch(s => console.log(s.count))
|
|
72
165
|
stop() // stop listening
|
|
73
166
|
```
|
|
74
167
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
## only high-class discerning aristocrats permitted beyond this point
|
|
168
|
+
### only discerning high-class aristocrats are permitted beyond this point
|
|
78
169
|
|
|
79
|
-
|
|
170
|
+
#### `Trunk.setup` for localStorage persistence etc
|
|
80
171
|
- simple setup
|
|
81
172
|
```ts
|
|
82
|
-
const {
|
|
173
|
+
const {trunk} = await Trunk.setup({
|
|
83
174
|
version: 1, // 👈 bump whenever your change state schema!
|
|
84
175
|
initialState: {count: 0},
|
|
85
176
|
})
|
|
86
177
|
```
|
|
178
|
+
- uses localStorage by default
|
|
87
179
|
- it's compatible with [`@e280/kv`](https://github.com/e280/kv)
|
|
88
180
|
```ts
|
|
89
181
|
import {Kv, StorageDriver} from "@e280/kv"
|
|
90
182
|
|
|
91
183
|
const kv = new Kv(new StorageDriver())
|
|
92
|
-
const store = kv.store<any>("
|
|
184
|
+
const store = kv.store<any>("appState")
|
|
93
185
|
|
|
94
|
-
const {
|
|
186
|
+
const {trunk} = await Trunk.setup({
|
|
95
187
|
version: 1,
|
|
96
188
|
initialState: {count: 0},
|
|
97
189
|
persistence: {
|
|
@@ -101,21 +193,21 @@
|
|
|
101
193
|
})
|
|
102
194
|
```
|
|
103
195
|
|
|
104
|
-
|
|
196
|
+
#### `Chronobranch` for undo/redo history
|
|
105
197
|
- first, put a `Chronicle` into your state tree
|
|
106
198
|
```ts
|
|
107
|
-
const
|
|
199
|
+
const trunk = new Trunk({
|
|
108
200
|
count: 0,
|
|
109
|
-
snacks:
|
|
201
|
+
snacks: Trunk.chronicle({
|
|
110
202
|
peanuts: 8,
|
|
111
203
|
bag: ["popcorn", "butter"],
|
|
112
204
|
}),
|
|
113
205
|
})
|
|
114
206
|
```
|
|
115
207
|
- *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 `
|
|
208
|
+
- second, make a `Chronobranch` which is like a branch, but is concerned with history
|
|
117
209
|
```ts
|
|
118
|
-
const snacks =
|
|
210
|
+
const snacks = trunk.chronobranch(64, s => s.snacks)
|
|
119
211
|
// \
|
|
120
212
|
// how many past snapshots to store
|
|
121
213
|
```
|
|
@@ -134,8 +226,17 @@
|
|
|
134
226
|
snacks.undoable // 2
|
|
135
227
|
snacks.redoable // 1
|
|
136
228
|
```
|
|
137
|
-
-
|
|
138
|
-
- plz pinky-swear right now, that you won't create a
|
|
229
|
+
- chronobranch can have its own branches — all their mutations advance history
|
|
230
|
+
- plz pinky-swear right now, that you won't create a chronobranch under a branch under another chronobranch 💀
|
|
231
|
+
|
|
232
|
+
<br/>
|
|
233
|
+
|
|
234
|
+
## 🪄 tracker — integrations
|
|
235
|
+
- ```ts
|
|
236
|
+
import {tracker} from "@e280/strata/tracker"
|
|
237
|
+
```
|
|
238
|
+
- all reactivity is orchestrated by the `tracker`
|
|
239
|
+
- 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
240
|
|
|
140
241
|
<br/>
|
|
141
242
|
|
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-7",
|
|
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
|
},
|
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
|
+
|