@e280/strata 0.0.0-1 → 0.0.0-10
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 +165 -45
- package/package.json +14 -8
- package/s/index.ts +3 -4
- package/s/signals/index.ts +6 -0
- package/s/signals/parts/derive.ts +29 -0
- package/s/signals/parts/effect.ts +23 -0
- package/s/signals/parts/lazy.ts +27 -0
- package/s/signals/parts/signal.ts +44 -0
- package/s/signals/parts/types.ts +11 -0
- package/s/signals/parts/units.ts +152 -0
- package/s/signals/signals.test.ts +285 -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/tree/parts/branch.ts +41 -0
- package/s/{parts/chronstrata.ts → tree/parts/chronobranch.ts} +19 -19
- package/s/tree/parts/persistence.ts +31 -0
- package/s/tree/parts/trunk.ts +72 -0
- package/s/tree/parts/types.ts +65 -0
- package/s/{parts → tree/parts}/utils/process-options.ts +1 -1
- package/s/tree/parts/utils/setup.ts +40 -0
- package/s/tree/tree.test.ts +316 -0
- package/x/index.d.ts +3 -4
- package/x/index.js +3 -4
- package/x/index.js.map +1 -1
- package/x/signals/index.d.ts +4 -0
- package/x/signals/index.js +5 -0
- package/x/signals/index.js.map +1 -0
- package/x/signals/parts/derive.d.ts +12 -0
- package/x/signals/parts/derive.js +12 -0
- package/x/signals/parts/derive.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/lazy.d.ts +10 -0
- package/x/signals/parts/lazy.js +12 -0
- package/x/signals/parts/lazy.js.map +1 -0
- package/x/signals/parts/signal.d.ts +21 -0
- package/x/signals/parts/signal.js +18 -0
- package/x/signals/parts/signal.js.map +1 -0
- package/x/signals/parts/types.d.ts +7 -0
- package/x/signals/parts/types.js.map +1 -0
- package/x/signals/parts/units.d.ts +43 -0
- package/x/signals/parts/units.js +133 -0
- package/x/signals/parts/units.js.map +1 -0
- package/x/signals/signals.test.d.ts +24 -0
- package/x/signals/signals.test.js +230 -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 +12 -0
- package/x/tree/parts/branch.js +31 -0
- package/x/tree/parts/branch.js.map +1 -0
- package/x/tree/parts/chronobranch.d.ts +23 -0
- package/x/{parts/chronstrata.js → tree/parts/chronobranch.js} +17 -17
- package/x/tree/parts/chronobranch.js.map +1 -0
- package/x/tree/parts/persistence.d.ts +2 -0
- package/x/tree/parts/persistence.js +23 -0
- package/x/tree/parts/persistence.js.map +1 -0
- package/x/tree/parts/trunk.d.ts +17 -0
- package/x/tree/parts/trunk.js +56 -0
- package/x/tree/parts/trunk.js.map +1 -0
- package/x/tree/parts/types.d.ts +43 -0
- package/x/tree/parts/types.js +2 -0
- package/x/{parts → tree/parts}/types.js.map +1 -1
- package/x/tree/parts/utils/process-options.js +4 -0
- package/x/tree/parts/utils/process-options.js.map +1 -0
- package/x/tree/parts/utils/setup.d.ts +8 -0
- package/x/tree/parts/utils/setup.js +24 -0
- 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 +279 -0
- package/x/tree/tree.test.js.map +1 -0
- package/s/parts/strata.ts +0 -71
- package/s/parts/substrata.ts +0 -58
- package/s/parts/types.ts +0 -31
- package/x/parts/chronstrata.d.ts +0 -23
- package/x/parts/chronstrata.js.map +0 -1
- package/x/parts/strata.d.ts +0 -14
- package/x/parts/strata.js +0 -64
- package/x/parts/strata.js.map +0 -1
- package/x/parts/substrata.d.ts +0 -15
- package/x/parts/substrata.js +0 -45
- package/x/parts/substrata.js.map +0 -1
- package/x/parts/types.d.ts +0 -19
- package/x/parts/utils/process-options.js +0 -4
- package/x/parts/utils/process-options.js.map +0 -1
- /package/x/{parts → signals/parts}/types.js +0 -0
- /package/x/{parts → tree/parts}/utils/process-options.d.ts +0 -0
package/README.md
CHANGED
|
@@ -1,25 +1,121 @@
|
|
|
1
1
|
|
|
2
2
|

|
|
3
3
|
|
|
4
|
+
<br/>
|
|
5
|
+
|
|
4
6
|
# ⛏️ strata
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
### get in loser, we're managing state
|
|
9
|
+
📦 `npm install @e280/strata`
|
|
10
|
+
🧙♂️ probably my tenth state management library, lol
|
|
11
|
+
💁 it's all about rerendering ui when data changes
|
|
12
|
+
|
|
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.
|
|
12
22
|
|
|
13
23
|
<br/>
|
|
14
24
|
|
|
15
|
-
##
|
|
25
|
+
## 🚦 signals — *ephemeral view-level state*
|
|
26
|
+
```ts
|
|
27
|
+
import {signal, effect, computed} from "@e280/strata"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### each signal holds a value
|
|
31
|
+
- **create a signal**
|
|
32
|
+
```ts
|
|
33
|
+
const count = signal(0)
|
|
34
|
+
```
|
|
35
|
+
- **read a signal**
|
|
36
|
+
```ts
|
|
37
|
+
count() // 0
|
|
38
|
+
```
|
|
39
|
+
- **set a signal**
|
|
40
|
+
```ts
|
|
41
|
+
count(1)
|
|
42
|
+
```
|
|
43
|
+
- **set a signal, and await effect propagation**
|
|
44
|
+
```ts
|
|
45
|
+
await count(2)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### pick your poison
|
|
49
|
+
- **signals hipster fn syntax**
|
|
50
|
+
```ts
|
|
51
|
+
count() // get
|
|
52
|
+
await count(2) // set
|
|
53
|
+
```
|
|
54
|
+
> to achieve this hipster syntax i had to make the implementation so damn cursed, lol 💀
|
|
55
|
+
- **signals get/set syntax**
|
|
56
|
+
```ts
|
|
57
|
+
count.get() // get
|
|
58
|
+
await count.set(2) // set
|
|
59
|
+
```
|
|
60
|
+
- **signals .value accessor syntax**
|
|
61
|
+
```ts
|
|
62
|
+
count.value // get
|
|
63
|
+
count.value = 2 // set
|
|
64
|
+
```
|
|
65
|
+
value pattern is nice for this vibe
|
|
66
|
+
```ts
|
|
67
|
+
count.value++
|
|
68
|
+
count.value += 1
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### effects
|
|
72
|
+
- **effects run when the relevant signals change**
|
|
73
|
+
```ts
|
|
74
|
+
effect(() => console.log(count()))
|
|
75
|
+
// 1
|
|
76
|
+
// the system detects 'count' is relevant
|
|
77
|
+
|
|
78
|
+
count.value++
|
|
79
|
+
// 2
|
|
80
|
+
// when count is changed, the effect fn is run
|
|
81
|
+
```
|
|
16
82
|
|
|
17
|
-
### `
|
|
83
|
+
### `signal.derive` and `signal.lazy` are computed signals
|
|
84
|
+
- **signal.derive**
|
|
85
|
+
is for combining signals
|
|
86
|
+
```ts
|
|
87
|
+
const a = signal(1)
|
|
88
|
+
const b = signal(10)
|
|
89
|
+
const product = signal.derive(() => a() * b())
|
|
90
|
+
|
|
91
|
+
product() // 10
|
|
92
|
+
|
|
93
|
+
// change a dependency,
|
|
94
|
+
// and the derived signal is automatically updated
|
|
95
|
+
await a.set(2)
|
|
96
|
+
|
|
97
|
+
product() // 20
|
|
98
|
+
```
|
|
99
|
+
- **signal.lazy**
|
|
100
|
+
is for making special optimizations.
|
|
101
|
+
it's like derive, except it cannot trigger effects,
|
|
102
|
+
because it's so lazy it only computes the value on read, and only when necessary.
|
|
103
|
+
> ⚠️ *i repeat: lazy signals cannot trigger effects!*
|
|
104
|
+
|
|
105
|
+
<br/>
|
|
106
|
+
|
|
107
|
+
## 🌳 tree — *persistent app-level state*
|
|
108
|
+
- single-source-of-truth state tree
|
|
109
|
+
- immutable except for `mutate(fn)` calls
|
|
110
|
+
- localStorage persistence, cross-tab sync, undo/redo history
|
|
111
|
+
- no spooky-dookie proxy magic — just god's honest javascript
|
|
112
|
+
|
|
113
|
+
#### `Trunk` is your app's state tree root
|
|
18
114
|
- better stick to json-friendly serializable data
|
|
19
115
|
```ts
|
|
20
|
-
import {
|
|
116
|
+
import {Trunk} from "@e280/strata"
|
|
21
117
|
|
|
22
|
-
const
|
|
118
|
+
const trunk = new Trunk({
|
|
23
119
|
count: 0,
|
|
24
120
|
snacks: {
|
|
25
121
|
peanuts: 8,
|
|
@@ -27,26 +123,26 @@
|
|
|
27
123
|
},
|
|
28
124
|
})
|
|
29
125
|
|
|
30
|
-
|
|
31
|
-
|
|
126
|
+
trunk.state.count // 0
|
|
127
|
+
trunk.state.snacks.peanuts // 8
|
|
32
128
|
```
|
|
33
129
|
|
|
34
|
-
|
|
130
|
+
#### formal mutations to change state
|
|
35
131
|
- ⛔ informal mutations are denied
|
|
36
132
|
```ts
|
|
37
|
-
|
|
133
|
+
trunk.state.count++ // error is thrown
|
|
38
134
|
```
|
|
39
135
|
- ✅ formal mutations are allowed
|
|
40
136
|
```ts
|
|
41
|
-
await
|
|
137
|
+
await trunk.mutate(s => s.count++)
|
|
42
138
|
```
|
|
43
139
|
|
|
44
|
-
|
|
140
|
+
#### `Branch` is a view into a subtree
|
|
45
141
|
- it's a lens, make lots of them, pass 'em around your app
|
|
46
142
|
```ts
|
|
47
|
-
const snacks =
|
|
143
|
+
const snacks = trunk.branch(s => s.snacks)
|
|
48
144
|
```
|
|
49
|
-
- run
|
|
145
|
+
- run branch mutations
|
|
50
146
|
```ts
|
|
51
147
|
await snacks.mutate(s => s.peanuts++)
|
|
52
148
|
```
|
|
@@ -54,42 +150,67 @@
|
|
|
54
150
|
```ts
|
|
55
151
|
await snacks.mutate(s => s.bag.push("salt"))
|
|
56
152
|
```
|
|
57
|
-
- you can
|
|
153
|
+
- you can branch a branch
|
|
58
154
|
|
|
59
|
-
|
|
60
|
-
-
|
|
155
|
+
#### `on` to watch for mutations
|
|
156
|
+
- on the trunk, we can listen deeply for mutations within the whole tree
|
|
61
157
|
```ts
|
|
62
|
-
|
|
158
|
+
trunk.on(s => console.log(s.count))
|
|
63
159
|
```
|
|
64
|
-
-
|
|
160
|
+
- whereas branch listeners don't care about changes outside their scope
|
|
65
161
|
```ts
|
|
66
|
-
snacks.
|
|
162
|
+
snacks.on(s => console.log(s.peanuts))
|
|
67
163
|
```
|
|
68
|
-
-
|
|
164
|
+
- on returns a fn to stop listening
|
|
69
165
|
```ts
|
|
70
|
-
const stop =
|
|
166
|
+
const stop = trunk.on(s => console.log(s.count))
|
|
71
167
|
stop() // stop listening
|
|
72
168
|
```
|
|
73
169
|
|
|
74
|
-
|
|
170
|
+
### only discerning high-class aristocrats are permitted beyond this point
|
|
75
171
|
|
|
76
|
-
|
|
172
|
+
#### `Trunk.setup` for localStorage persistence etc
|
|
173
|
+
- it automatically handles persistence to localStorage and cross-tab synchronization
|
|
174
|
+
- simple setup
|
|
175
|
+
```ts
|
|
176
|
+
const {trunk} = await Trunk.setup({
|
|
177
|
+
version: 1, // 👈 bump whenever you change state schema!
|
|
178
|
+
initialState: {count: 0},
|
|
179
|
+
})
|
|
180
|
+
```
|
|
181
|
+
- uses localStorage by default
|
|
182
|
+
- it's compatible with [`@e280/kv`](https://github.com/e280/kv)
|
|
183
|
+
```ts
|
|
184
|
+
import {Kv, StorageDriver} from "@e280/kv"
|
|
77
185
|
|
|
78
|
-
|
|
186
|
+
const kv = new Kv(new StorageDriver())
|
|
187
|
+
const store = kv.store<any>("appState")
|
|
188
|
+
|
|
189
|
+
const {trunk} = await Trunk.setup({
|
|
190
|
+
version: 1,
|
|
191
|
+
initialState: {count: 0},
|
|
192
|
+
persistence: {
|
|
193
|
+
store,
|
|
194
|
+
onChange: StorageDriver.onStorageEvent,
|
|
195
|
+
},
|
|
196
|
+
})
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
#### `Chronobranch` for undo/redo history
|
|
79
200
|
- first, put a `Chronicle` into your state tree
|
|
80
201
|
```ts
|
|
81
|
-
const
|
|
202
|
+
const trunk = new Trunk({
|
|
82
203
|
count: 0,
|
|
83
|
-
snacks:
|
|
204
|
+
snacks: Trunk.chronicle({
|
|
84
205
|
peanuts: 8,
|
|
85
206
|
bag: ["popcorn", "butter"],
|
|
86
207
|
}),
|
|
87
208
|
})
|
|
88
209
|
```
|
|
89
210
|
- *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*
|
|
90
|
-
- second, make a `
|
|
211
|
+
- second, make a `Chronobranch` which is like a branch, but is concerned with history
|
|
91
212
|
```ts
|
|
92
|
-
const snacks =
|
|
213
|
+
const snacks = trunk.chronobranch(64, s => s.snacks)
|
|
93
214
|
// \
|
|
94
215
|
// how many past snapshots to store
|
|
95
216
|
```
|
|
@@ -105,21 +226,20 @@
|
|
|
105
226
|
```
|
|
106
227
|
- you can check how many undoable or redoable steps are available
|
|
107
228
|
```ts
|
|
108
|
-
snacks.undoable // 0
|
|
109
|
-
|
|
110
|
-
await snacks.mutate(s => s.peanuts = 101)
|
|
111
|
-
await snacks.mutate(s => s.peanuts = 102)
|
|
112
|
-
await snacks.mutate(s => s.peanuts = 103)
|
|
113
|
-
|
|
114
|
-
snacks.undoable // 3
|
|
115
|
-
|
|
116
|
-
await snacks.undo()
|
|
117
|
-
|
|
118
229
|
snacks.undoable // 2
|
|
119
230
|
snacks.redoable // 1
|
|
120
231
|
```
|
|
121
|
-
-
|
|
122
|
-
- plz pinky-swear right now, that you won't create a
|
|
232
|
+
- chronobranch can have its own branches — all their mutations advance history
|
|
233
|
+
- plz pinky-swear right now, that you won't create a chronobranch under a branch under another chronobranch 💀
|
|
234
|
+
|
|
235
|
+
<br/>
|
|
236
|
+
|
|
237
|
+
## 🪄 tracker — integrations
|
|
238
|
+
- ```ts
|
|
239
|
+
import {tracker} from "@e280/strata/tracker"
|
|
240
|
+
```
|
|
241
|
+
- all reactivity is orchestrated by the `tracker`
|
|
242
|
+
- 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)
|
|
123
243
|
|
|
124
244
|
<br/>
|
|
125
245
|
|
package/package.json
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@e280/strata",
|
|
3
|
-
"version": "0.0.0-
|
|
3
|
+
"version": "0.0.0-10",
|
|
4
4
|
"description": "state management",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Chase Moskal <chasemoskal@gmail.com>",
|
|
7
7
|
"type": "module",
|
|
8
|
-
"main": "x/index.js",
|
|
8
|
+
"main": "./x/index.js",
|
|
9
9
|
"files": [
|
|
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
|
+
},
|
|
13
19
|
"scripts": {
|
|
14
20
|
"build": "run-s _clean _links _tsc",
|
|
15
21
|
"test": "node x/tests.test.js",
|
|
@@ -22,11 +28,14 @@
|
|
|
22
28
|
"_tsc": "tsc",
|
|
23
29
|
"_tscw": "tsc -w"
|
|
24
30
|
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@e280/stz": "^0.0.0-35"
|
|
33
|
+
},
|
|
25
34
|
"devDependencies": {
|
|
26
|
-
"@e280/science": "^0.0.
|
|
27
|
-
"@types/node": "^24.0
|
|
35
|
+
"@e280/science": "^0.0.6",
|
|
36
|
+
"@types/node": "^24.3.0",
|
|
28
37
|
"npm-run-all": "^4.1.5",
|
|
29
|
-
"typescript": "^5.
|
|
38
|
+
"typescript": "^5.9.2"
|
|
30
39
|
},
|
|
31
40
|
"keywords": [
|
|
32
41
|
"state",
|
|
@@ -39,8 +48,5 @@
|
|
|
39
48
|
},
|
|
40
49
|
"bugs": {
|
|
41
50
|
"url": "https://github.com/e280/strata/issues"
|
|
42
|
-
},
|
|
43
|
-
"dependencies": {
|
|
44
|
-
"@e280/stz": "^0.0.0-24"
|
|
45
51
|
}
|
|
46
52
|
}
|
package/s/index.ts
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
|
|
2
|
+
import {Sub} from "@e280/stz"
|
|
3
|
+
import {SignalOptions} from "./types.js"
|
|
4
|
+
import {DerivedCore, processSignalOptions} from "./units.js"
|
|
5
|
+
|
|
6
|
+
export type DerivedSignal<V> = {
|
|
7
|
+
(): V
|
|
8
|
+
kind: "derived"
|
|
9
|
+
|
|
10
|
+
sneak: V
|
|
11
|
+
on: Sub<[V]>
|
|
12
|
+
get(): V
|
|
13
|
+
get value(): V
|
|
14
|
+
dispose(): void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function derive<V>(formula: () => V, options: Partial<SignalOptions> = {}) {
|
|
18
|
+
function fn(): V {
|
|
19
|
+
return (fn as any).value
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const o = processSignalOptions(options)
|
|
23
|
+
const core = DerivedCore.make<V>(fn as any, formula, o)
|
|
24
|
+
Object.setPrototypeOf(fn, DerivedCore.prototype)
|
|
25
|
+
Object.assign(fn, core)
|
|
26
|
+
|
|
27
|
+
return fn as DerivedSignal<V>
|
|
28
|
+
}
|
|
29
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
|
|
2
|
+
import {debounce} from "@e280/stz"
|
|
3
|
+
import {tracker} from "../../tracker/tracker.js"
|
|
4
|
+
|
|
5
|
+
export function effect(collector: () => void, responder: () => void = collector) {
|
|
6
|
+
return collectorEffect(collector, responder).dispose
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function collectorEffect<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,27 @@
|
|
|
1
|
+
|
|
2
|
+
import {SignalOptions} from "./types.js"
|
|
3
|
+
import {LazyCore, processSignalOptions} from "./units.js"
|
|
4
|
+
|
|
5
|
+
export type LazySignal<V> = {
|
|
6
|
+
(): V
|
|
7
|
+
kind: "lazy"
|
|
8
|
+
|
|
9
|
+
sneak: V
|
|
10
|
+
get(): V
|
|
11
|
+
get value(): V
|
|
12
|
+
dispose(): void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function lazy<V>(formula: () => V, options: Partial<SignalOptions> = {}) {
|
|
16
|
+
function fn(): V {
|
|
17
|
+
return (fn as any).value
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const o = processSignalOptions(options)
|
|
21
|
+
const core = new LazyCore<V>(formula, o)
|
|
22
|
+
Object.setPrototypeOf(fn, LazyCore.prototype)
|
|
23
|
+
Object.assign(fn, core)
|
|
24
|
+
|
|
25
|
+
return fn as LazySignal<V>
|
|
26
|
+
}
|
|
27
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
|
|
2
|
+
import {Sub} from "@e280/stz"
|
|
3
|
+
|
|
4
|
+
import {lazy} from "./lazy.js"
|
|
5
|
+
import {derive} from "./derive.js"
|
|
6
|
+
import {SignalOptions} from "./types.js"
|
|
7
|
+
import {processSignalOptions, SignalCore} from "./units.js"
|
|
8
|
+
|
|
9
|
+
export type Signal<V> = {
|
|
10
|
+
(): V
|
|
11
|
+
(v: V): Promise<V>
|
|
12
|
+
(v?: V): V | Promise<V>
|
|
13
|
+
|
|
14
|
+
kind: "signal"
|
|
15
|
+
|
|
16
|
+
sneak: V
|
|
17
|
+
value: V
|
|
18
|
+
on: Sub<[V]>
|
|
19
|
+
get(): V
|
|
20
|
+
set(v: V): Promise<V>
|
|
21
|
+
publish(v?: V): Promise<V>
|
|
22
|
+
dispose(): void
|
|
23
|
+
} & SignalCore<V>
|
|
24
|
+
|
|
25
|
+
export function signal<V>(value: V, options: Partial<SignalOptions> = {}) {
|
|
26
|
+
function fn(): V
|
|
27
|
+
function fn(v: V): Promise<void>
|
|
28
|
+
function fn(v?: V): V | Promise<void> {
|
|
29
|
+
return v !== undefined
|
|
30
|
+
? (fn as any).set(v)
|
|
31
|
+
: (fn as any).get()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const o = processSignalOptions(options)
|
|
35
|
+
const core = new SignalCore(value, o)
|
|
36
|
+
Object.setPrototypeOf(fn, SignalCore.prototype)
|
|
37
|
+
Object.assign(fn, core)
|
|
38
|
+
|
|
39
|
+
return fn as Signal<V>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
signal.lazy = lazy
|
|
43
|
+
signal.derive = derive
|
|
44
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
|
|
2
|
+
import {Signal} from "./signal.js"
|
|
3
|
+
import {LazySignal} from "./lazy.js"
|
|
4
|
+
import {DerivedSignal} from "./derive.js"
|
|
5
|
+
|
|
6
|
+
export type Signaloid<V> = Signal<V> | DerivedSignal<V> | LazySignal<V>
|
|
7
|
+
|
|
8
|
+
export type SignalOptions = {
|
|
9
|
+
compare: (a: any, b: any) => boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
|
|
2
|
+
import {sub} from "@e280/stz"
|
|
3
|
+
import {SignalOptions} from "./types.js"
|
|
4
|
+
import {collectorEffect} from "./effect.js"
|
|
5
|
+
import {tracker} from "../../tracker/tracker.js"
|
|
6
|
+
|
|
7
|
+
const defaultSignalOptions: SignalOptions = {
|
|
8
|
+
compare: (a, b) => a === b
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function processSignalOptions(options: Partial<SignalOptions> = {}) {
|
|
12
|
+
return {...defaultSignalOptions, ...options}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class ReadableSignal<V> {
|
|
16
|
+
constructor(public sneak: V) {}
|
|
17
|
+
|
|
18
|
+
get() {
|
|
19
|
+
tracker.see(this)
|
|
20
|
+
return this.sneak
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get value() {
|
|
24
|
+
return this.get()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class ReactiveSignal<V> extends ReadableSignal<V> {
|
|
29
|
+
on = sub<[V]>()
|
|
30
|
+
|
|
31
|
+
dispose() {
|
|
32
|
+
this.on.clear()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class SignalCore<V> extends ReactiveSignal<V> {
|
|
37
|
+
kind: "signal" = "signal"
|
|
38
|
+
_lock = false
|
|
39
|
+
|
|
40
|
+
constructor(sneak: V, public _options: SignalOptions) {
|
|
41
|
+
super(sneak)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async set(v: V) {
|
|
45
|
+
const isChanged = !this._options.compare(this.sneak, v)
|
|
46
|
+
if (isChanged)
|
|
47
|
+
await this.publish(v)
|
|
48
|
+
return v
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get value() {
|
|
52
|
+
return this.get()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
set value(v: V) {
|
|
56
|
+
this.set(v)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async publish(v = this.get()) {
|
|
60
|
+
if (this._lock)
|
|
61
|
+
throw new Error("forbid circularity")
|
|
62
|
+
|
|
63
|
+
let promise = Promise.resolve()
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
this._lock = true
|
|
67
|
+
this.sneak = v
|
|
68
|
+
promise = Promise.all([
|
|
69
|
+
tracker.change(this),
|
|
70
|
+
this.on.pub(v),
|
|
71
|
+
]) as any
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
this._lock = false
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
await promise
|
|
78
|
+
return v
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class LazyCore<V> extends ReadableSignal<V> {
|
|
83
|
+
kind: "lazy" = "lazy"
|
|
84
|
+
|
|
85
|
+
_dirty = false
|
|
86
|
+
_effect: (() => void) | undefined
|
|
87
|
+
|
|
88
|
+
constructor(public _formula: () => V, public _options: SignalOptions) {
|
|
89
|
+
super(undefined as any)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get() {
|
|
93
|
+
if (!this._effect) {
|
|
94
|
+
const {result, dispose} = collectorEffect(this._formula, () => this._dirty = true)
|
|
95
|
+
this._effect = dispose
|
|
96
|
+
this.sneak = result
|
|
97
|
+
}
|
|
98
|
+
if (this._dirty) {
|
|
99
|
+
this._dirty = false
|
|
100
|
+
|
|
101
|
+
const v = this._formula()
|
|
102
|
+
const isChanged = !this._options.compare(this.sneak, v)
|
|
103
|
+
if (isChanged) {
|
|
104
|
+
this.sneak = v
|
|
105
|
+
tracker.change(this)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return super.get()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
get value() {
|
|
112
|
+
return this.get()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
dispose() {
|
|
116
|
+
if (this._effect)
|
|
117
|
+
this._effect()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export class DerivedCore<V> extends ReactiveSignal<V> {
|
|
122
|
+
static make<V>(that: DerivedCore<V>, formula: () => V, options: SignalOptions) {
|
|
123
|
+
const {result, dispose} = collectorEffect(formula, async() => {
|
|
124
|
+
const value = formula()
|
|
125
|
+
const isChanged = !options.compare(that.sneak, value)
|
|
126
|
+
if (isChanged) {
|
|
127
|
+
that.sneak = value
|
|
128
|
+
await Promise.all([
|
|
129
|
+
tracker.change(that),
|
|
130
|
+
that.on.pub(value),
|
|
131
|
+
])
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
return new this(result, dispose)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
kind: "derived" = "derived"
|
|
138
|
+
|
|
139
|
+
constructor(initialValue: V, public _effect: () => void) {
|
|
140
|
+
super(initialValue)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
get value() {
|
|
144
|
+
return this.get()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
dispose() {
|
|
148
|
+
super.dispose()
|
|
149
|
+
this._effect()
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|