@e280/strata 0.2.1 → 0.2.3
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 +132 -131
- package/package.json +7 -5
- package/s/index.ts +1 -0
- package/s/prism/chrono/chronicle.ts +11 -0
- package/s/prism/chrono/chrono.ts +91 -0
- package/s/prism/chrono/types.ts +12 -0
- package/s/prism/index.ts +11 -0
- package/s/prism/lens.ts +54 -0
- package/s/prism/prism.test.ts +330 -0
- package/s/prism/prism.ts +41 -0
- package/s/prism/types.ts +27 -0
- package/s/prism/utils/cache-cell.ts +22 -0
- package/s/prism/utils/immute.ts +8 -0
- package/s/prism/utils/optic-symbol.ts +3 -0
- package/s/prism/vault/local-store.ts +31 -0
- package/s/prism/vault/types.ts +19 -0
- package/s/prism/vault/vault.ts +19 -0
- package/s/signals/core/effect.ts +2 -2
- package/s/tests.test.ts +4 -1
- package/s/tree/parts/branch.ts +28 -14
- package/s/tree/parts/chronobranch.ts +4 -2
- package/s/tree/parts/trunk.ts +18 -22
- package/s/tree/parts/types.ts +36 -31
- package/s/tree/parts/utils/immute.ts +43 -0
- package/s/tree/parts/utils/process-options.ts +2 -2
- package/s/tree/tree.test.ts +69 -19
- package/x/index.d.ts +1 -0
- package/x/index.js +1 -0
- package/x/index.js.map +1 -1
- package/x/prism/chrono/chronicle.d.ts +2 -0
- package/x/prism/chrono/chronicle.js +8 -0
- package/x/prism/chrono/chronicle.js.map +1 -0
- package/x/prism/chrono/chrono.d.ts +26 -0
- package/x/prism/chrono/chrono.js +79 -0
- package/x/prism/chrono/chrono.js.map +1 -0
- package/x/prism/chrono/types.d.ts +5 -0
- package/x/prism/chrono/types.js +2 -0
- package/x/prism/chrono/types.js.map +1 -0
- package/x/prism/index.d.ts +9 -0
- package/x/prism/index.js +10 -0
- package/x/prism/index.js.map +1 -0
- package/x/prism/lens.d.ts +13 -0
- package/x/prism/lens.js +45 -0
- package/x/prism/lens.js.map +1 -0
- package/x/prism/prism.d.ts +10 -0
- package/x/prism/prism.js +34 -0
- package/x/prism/prism.js.map +1 -0
- package/x/prism/prism.test.d.ts +35 -0
- package/x/prism/prism.test.js +286 -0
- package/x/prism/prism.test.js.map +1 -0
- package/x/prism/types.d.ts +17 -0
- package/x/prism/types.js +2 -0
- package/x/prism/types.js.map +1 -0
- package/x/prism/utils/cache-cell.d.ts +7 -0
- package/x/prism/utils/cache-cell.js +20 -0
- package/x/prism/utils/cache-cell.js.map +1 -0
- package/x/prism/utils/immute.d.ts +2 -0
- package/x/prism/utils/immute.js +5 -0
- package/x/prism/utils/immute.js.map +1 -0
- package/x/prism/utils/optic-symbol.d.ts +1 -0
- package/x/prism/utils/optic-symbol.js +2 -0
- package/x/prism/utils/optic-symbol.js.map +1 -0
- package/x/prism/vault/local-store.d.ts +9 -0
- package/x/prism/vault/local-store.js +27 -0
- package/x/prism/vault/local-store.js.map +1 -0
- package/x/prism/vault/types.d.ts +14 -0
- package/x/prism/vault/types.js +2 -0
- package/x/prism/vault/types.js.map +1 -0
- package/x/prism/vault/vault.d.ts +7 -0
- package/x/prism/vault/vault.js +17 -0
- package/x/prism/vault/vault.js.map +1 -0
- package/x/signals/core/effect.js +2 -2
- package/x/signals/core/effect.js.map +1 -1
- package/x/tests.test.js +3 -1
- package/x/tests.test.js.map +1 -1
- package/x/tree/parts/branch.d.ts +4 -2
- package/x/tree/parts/branch.js +20 -9
- package/x/tree/parts/branch.js.map +1 -1
- package/x/tree/parts/chronobranch.d.ts +5 -3
- package/x/tree/parts/chronobranch.js +1 -0
- package/x/tree/parts/chronobranch.js.map +1 -1
- package/x/tree/parts/trunk.d.ts +8 -6
- package/x/tree/parts/trunk.js +14 -14
- package/x/tree/parts/trunk.js.map +1 -1
- package/x/tree/parts/types.d.ts +5 -20
- package/x/tree/parts/utils/immute.d.ts +11 -0
- package/x/tree/parts/utils/immute.js +33 -0
- package/x/tree/parts/utils/immute.js.map +1 -0
- package/x/tree/parts/utils/process-options.d.ts +2 -2
- package/x/tree/parts/utils/process-options.js.map +1 -1
- package/x/tree/tree.test.d.ts +6 -3
- package/x/tree/tree.test.js +64 -18
- package/x/tree/tree.test.js.map +1 -1
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
|
|
2
|
+
import {suite, test, expect} from "@e280/science"
|
|
3
|
+
|
|
4
|
+
import {Prism} from "./prism.js"
|
|
5
|
+
import {Chrono} from "./chrono/chrono.js"
|
|
6
|
+
import {chronicle} from "./chrono/chronicle.js"
|
|
7
|
+
import {effect} from "../signals/core/effect.js"
|
|
8
|
+
|
|
9
|
+
export default suite({
|
|
10
|
+
"prism": suite({
|
|
11
|
+
"get and set state": test(async() => {
|
|
12
|
+
const prism = new Prism({count: 1})
|
|
13
|
+
expect(prism.get().count).is(1)
|
|
14
|
+
await prism.set({count: 2})
|
|
15
|
+
expect(prism.get().count).is(2)
|
|
16
|
+
}),
|
|
17
|
+
}),
|
|
18
|
+
|
|
19
|
+
"lens": suite({
|
|
20
|
+
"get state": test(async() => {
|
|
21
|
+
const prism = new Prism({data: {count: 1}})
|
|
22
|
+
const lens = prism.lens(s => s.data)
|
|
23
|
+
expect(lens.state.count).is(1)
|
|
24
|
+
}),
|
|
25
|
+
|
|
26
|
+
"state is immutable": test(async() => {
|
|
27
|
+
const prism = new Prism({data: {count: 1}})
|
|
28
|
+
const lens = prism.lens(s => s.data)
|
|
29
|
+
expect(() => (lens.state as any).count++).throws()
|
|
30
|
+
}),
|
|
31
|
+
|
|
32
|
+
"proper mutation": test(async() => {
|
|
33
|
+
const prism = new Prism({data: {count: 1}})
|
|
34
|
+
const lens = prism.lens(s => s.data)
|
|
35
|
+
await lens.mutate(s => s.count++)
|
|
36
|
+
expect(lens.state.count).is(2)
|
|
37
|
+
await lens.mutate(s => s.count++)
|
|
38
|
+
expect(lens.state.count).is(3)
|
|
39
|
+
}),
|
|
40
|
+
|
|
41
|
+
"state after mutation is frozen": test(async() => {
|
|
42
|
+
const prism = new Prism({data: {count: 1}})
|
|
43
|
+
const lens = prism.lens(s => s)
|
|
44
|
+
await lens.mutate(s => s.data = {count: 2})
|
|
45
|
+
expect(lens.state.data.count).is(2)
|
|
46
|
+
expect(() => (lens.state.data as any).count++).throws()
|
|
47
|
+
}),
|
|
48
|
+
|
|
49
|
+
"effect reacts": test(async() => {
|
|
50
|
+
const prism = new Prism({data: {count: 1}})
|
|
51
|
+
const lens = prism.lens(s => s.data)
|
|
52
|
+
let happenings = 0
|
|
53
|
+
const stop = effect(() => {
|
|
54
|
+
void lens.state.count
|
|
55
|
+
happenings++
|
|
56
|
+
})
|
|
57
|
+
await lens.mutate(s => s.count++)
|
|
58
|
+
expect(happenings).is(2)
|
|
59
|
+
stop()
|
|
60
|
+
}),
|
|
61
|
+
|
|
62
|
+
"lens.on is debounced": test(async() => {
|
|
63
|
+
const prism = new Prism({data: {count: 1}})
|
|
64
|
+
const lens = prism.lens(s => s.data)
|
|
65
|
+
let happenings = 0
|
|
66
|
+
const stop = lens.on(() => void happenings++)
|
|
67
|
+
await Promise.all([
|
|
68
|
+
lens.mutate(s => s.count++),
|
|
69
|
+
lens.mutate(s => s.count++),
|
|
70
|
+
])
|
|
71
|
+
expect(happenings).is(1)
|
|
72
|
+
stop()
|
|
73
|
+
}),
|
|
74
|
+
|
|
75
|
+
"array pushes are reactive": test(async() => {
|
|
76
|
+
const prism = new Prism({data: {array: ["lol"]}})
|
|
77
|
+
const lens = prism.lens(s => s.data)
|
|
78
|
+
let happenings = 0
|
|
79
|
+
const stop = lens.on(() => void happenings++)
|
|
80
|
+
await lens.mutate(s => s.array.push("lmao"))
|
|
81
|
+
expect(happenings).is(1)
|
|
82
|
+
expect(lens.state.array.length).is(2)
|
|
83
|
+
stop()
|
|
84
|
+
}),
|
|
85
|
+
|
|
86
|
+
"sync coherence": test(async() => {
|
|
87
|
+
const prism = new Prism({data: {count: 1}})
|
|
88
|
+
const lens = prism.lens(s => s.data)
|
|
89
|
+
const p1 = lens.mutate(s => s.count++)
|
|
90
|
+
expect(lens.state.count).is(2)
|
|
91
|
+
const p2 = lens.mutate(s => s.count++)
|
|
92
|
+
expect(lens.state.count).is(3)
|
|
93
|
+
await p1
|
|
94
|
+
await p2
|
|
95
|
+
}),
|
|
96
|
+
|
|
97
|
+
"nullable selector": test(async() => {
|
|
98
|
+
type S = {a?: {b: {count: number}}}
|
|
99
|
+
const prism = new Prism<S>({a: {b: {count: 1}}})
|
|
100
|
+
const lens = prism.lens(s => s.a?.b)
|
|
101
|
+
expect(lens.state?.count).is(1)
|
|
102
|
+
await prism.set({a: undefined})
|
|
103
|
+
expect(lens.state?.count).is(undefined)
|
|
104
|
+
}),
|
|
105
|
+
|
|
106
|
+
"deep composition": test(async() => {
|
|
107
|
+
const prism = new Prism({a: {b: {count: 1}}})
|
|
108
|
+
const lensA = prism.lens(s => s.a)
|
|
109
|
+
const lensB = lensA.lens(s => s.b)
|
|
110
|
+
expect(prism.get().a.b.count).is(1)
|
|
111
|
+
expect(lensA.state.b.count).is(1)
|
|
112
|
+
expect(lensB.state.count).is(1)
|
|
113
|
+
}),
|
|
114
|
+
|
|
115
|
+
"deep mutations": test(async() => {
|
|
116
|
+
const prism = new Prism({a: {b: {count: 1}}})
|
|
117
|
+
const lensA = prism.lens(s => s.a)
|
|
118
|
+
const lensB = lensA.lens(s => s.b)
|
|
119
|
+
await lensB.mutate(s => s.count++)
|
|
120
|
+
expect(prism.get().a.b.count).is(2)
|
|
121
|
+
expect(lensA.state.b.count).is(2)
|
|
122
|
+
expect(lensB.state.count).is(2)
|
|
123
|
+
await lensA.mutate(s => s.b = {count: 3})
|
|
124
|
+
expect(prism.get().a.b.count).is(3)
|
|
125
|
+
expect(lensA.state.b.count).is(3)
|
|
126
|
+
expect(lensB.state.count).is(3)
|
|
127
|
+
}),
|
|
128
|
+
|
|
129
|
+
"outside mutations ignored": test(async() => {
|
|
130
|
+
const prism = new Prism({a: {count: 1}, b: {count: 101}})
|
|
131
|
+
const lensA = prism.lens(s => s.a)
|
|
132
|
+
const lensB = prism.lens(s => s.b)
|
|
133
|
+
let happeningsA = 0
|
|
134
|
+
let happeningsB = 0
|
|
135
|
+
const stopA = lensA.on(() => void happeningsA++)
|
|
136
|
+
const stopB = lensB.on(() => void happeningsA++)
|
|
137
|
+
await lensA.mutate(s => s.count++)
|
|
138
|
+
expect(happeningsA).is(1)
|
|
139
|
+
expect(happeningsB).is(0)
|
|
140
|
+
stopA()
|
|
141
|
+
stopB()
|
|
142
|
+
}),
|
|
143
|
+
|
|
144
|
+
"outside mutations ignored for effects": test(async() => {
|
|
145
|
+
const prism = new Prism({a: {count: 1}, b: {count: 101}})
|
|
146
|
+
const lensA = prism.lens(s => s.a)
|
|
147
|
+
const lensB = prism.lens(s => s.b)
|
|
148
|
+
let happeningsA = 0
|
|
149
|
+
let happeningsB = 0
|
|
150
|
+
const stopA = effect(() => {
|
|
151
|
+
void lensA.state.count
|
|
152
|
+
happeningsA++
|
|
153
|
+
})
|
|
154
|
+
const stopB = effect(() => {
|
|
155
|
+
void lensB.state.count
|
|
156
|
+
happeningsB++
|
|
157
|
+
})
|
|
158
|
+
await lensA.mutate(s => s.count++)
|
|
159
|
+
expect(happeningsA).is(2)
|
|
160
|
+
expect(happeningsB).is(1)
|
|
161
|
+
stopA()
|
|
162
|
+
stopB()
|
|
163
|
+
}),
|
|
164
|
+
}),
|
|
165
|
+
|
|
166
|
+
"chrono": suite({
|
|
167
|
+
"get present state": test(async() => {
|
|
168
|
+
const prism = new Prism({data: chronicle({count: 1})})
|
|
169
|
+
const lens = prism.lens(s => s.data)
|
|
170
|
+
const chrono = new Chrono(64, lens)
|
|
171
|
+
expect(chrono.state.count).is(1)
|
|
172
|
+
}),
|
|
173
|
+
|
|
174
|
+
"mutation": test(async() => {
|
|
175
|
+
const prism = new Prism({data: chronicle({count: 1})})
|
|
176
|
+
const lens = prism.lens(s => s.data)
|
|
177
|
+
const chrono = new Chrono(64, lens)
|
|
178
|
+
await chrono.mutate(s => s.count++)
|
|
179
|
+
expect(chrono.state.count).is(2)
|
|
180
|
+
await chrono.mutate(s => s.count++)
|
|
181
|
+
expect(chrono.state.count).is(3)
|
|
182
|
+
}),
|
|
183
|
+
|
|
184
|
+
"undoable/redoable": test(async() => {
|
|
185
|
+
const prism = new Prism({data: chronicle({count: 1})})
|
|
186
|
+
const lens = prism.lens(s => s.data)
|
|
187
|
+
const chrono = new Chrono(64, lens)
|
|
188
|
+
expect(chrono.undoable).is(0)
|
|
189
|
+
expect(chrono.redoable).is(0)
|
|
190
|
+
await chrono.mutate(s => s.count++)
|
|
191
|
+
expect(chrono.undoable).is(1)
|
|
192
|
+
await chrono.mutate(s => s.count++)
|
|
193
|
+
expect(chrono.undoable).is(2)
|
|
194
|
+
await chrono.undo()
|
|
195
|
+
expect(chrono.undoable).is(1)
|
|
196
|
+
expect(chrono.redoable).is(1)
|
|
197
|
+
await chrono.undo()
|
|
198
|
+
expect(chrono.undoable).is(0)
|
|
199
|
+
expect(chrono.redoable).is(2)
|
|
200
|
+
await chrono.redo()
|
|
201
|
+
expect(chrono.undoable).is(1)
|
|
202
|
+
expect(chrono.redoable).is(1)
|
|
203
|
+
}),
|
|
204
|
+
|
|
205
|
+
"undo": test(async() => {
|
|
206
|
+
const prism = new Prism({data: chronicle({count: 1})})
|
|
207
|
+
const lens = prism.lens(s => s.data)
|
|
208
|
+
const chrono = new Chrono(64, lens)
|
|
209
|
+
|
|
210
|
+
await chrono.mutate(s => s.count++)
|
|
211
|
+
expect(chrono.state.count).is(2)
|
|
212
|
+
|
|
213
|
+
await chrono.undo()
|
|
214
|
+
expect(chrono.state.count).is(1)
|
|
215
|
+
}),
|
|
216
|
+
|
|
217
|
+
"sync undo": test(async() => {
|
|
218
|
+
const prism = new Prism({data: chronicle({count: 1})})
|
|
219
|
+
const lens = prism.lens(s => s.data)
|
|
220
|
+
const chrono = new Chrono(64, lens)
|
|
221
|
+
|
|
222
|
+
chrono.mutate(s => s.count++)
|
|
223
|
+
expect(chrono.state.count).is(2)
|
|
224
|
+
|
|
225
|
+
chrono.undo()
|
|
226
|
+
expect(chrono.state.count).is(1)
|
|
227
|
+
}),
|
|
228
|
+
|
|
229
|
+
"redo": test(async() => {
|
|
230
|
+
const prism = new Prism({data: chronicle({count: 1})})
|
|
231
|
+
const lens = prism.lens(s => s.data)
|
|
232
|
+
const chrono = new Chrono(64, lens)
|
|
233
|
+
|
|
234
|
+
await chrono.mutate(s => s.count++)
|
|
235
|
+
expect(chrono.state.count).is(2)
|
|
236
|
+
|
|
237
|
+
await chrono.undo()
|
|
238
|
+
expect(chrono.state.count).is(1)
|
|
239
|
+
|
|
240
|
+
await chrono.redo()
|
|
241
|
+
expect(chrono.state.count).is(2)
|
|
242
|
+
}),
|
|
243
|
+
|
|
244
|
+
"undo/redo is orderly": test(async() => {
|
|
245
|
+
const prism = new Prism({data: chronicle({count: 1})})
|
|
246
|
+
const lens = prism.lens(s => s.data)
|
|
247
|
+
const chrono = new Chrono(64, lens)
|
|
248
|
+
|
|
249
|
+
await chrono.mutate(s => s.count++)
|
|
250
|
+
await chrono.mutate(s => s.count++)
|
|
251
|
+
await chrono.mutate(s => s.count++)
|
|
252
|
+
expect(chrono.state.count).is(4)
|
|
253
|
+
|
|
254
|
+
await chrono.undo()
|
|
255
|
+
expect(chrono.state.count).is(3)
|
|
256
|
+
|
|
257
|
+
await chrono.undo()
|
|
258
|
+
expect(chrono.state.count).is(2)
|
|
259
|
+
|
|
260
|
+
await chrono.redo()
|
|
261
|
+
expect(chrono.state.count).is(3)
|
|
262
|
+
|
|
263
|
+
await chrono.redo()
|
|
264
|
+
expect(chrono.state.count).is(4)
|
|
265
|
+
|
|
266
|
+
await chrono.undo()
|
|
267
|
+
expect(chrono.state.count).is(3)
|
|
268
|
+
|
|
269
|
+
await chrono.undo()
|
|
270
|
+
expect(chrono.state.count).is(2)
|
|
271
|
+
|
|
272
|
+
await chrono.undo()
|
|
273
|
+
expect(chrono.state.count).is(1)
|
|
274
|
+
}),
|
|
275
|
+
|
|
276
|
+
"undo nothing does nothing": test(async() => {
|
|
277
|
+
const prism = new Prism({data: chronicle({count: 1})})
|
|
278
|
+
const lens = prism.lens(s => s.data)
|
|
279
|
+
const chrono = new Chrono(64, lens)
|
|
280
|
+
await chrono.undo()
|
|
281
|
+
expect(chrono.state.count).is(1)
|
|
282
|
+
}),
|
|
283
|
+
|
|
284
|
+
"redo nothing does nothing": test(async() => {
|
|
285
|
+
const prism = new Prism({data: chronicle({count: 1})})
|
|
286
|
+
const lens = prism.lens(s => s.data)
|
|
287
|
+
const chrono = new Chrono(64, lens)
|
|
288
|
+
await chrono.redo()
|
|
289
|
+
expect(chrono.state.count).is(1)
|
|
290
|
+
}),
|
|
291
|
+
|
|
292
|
+
"undo 2x": test(async() => {
|
|
293
|
+
const prism = new Prism({data: chronicle({count: 1})})
|
|
294
|
+
const lens = prism.lens(s => s.data)
|
|
295
|
+
const chrono = new Chrono(64, lens)
|
|
296
|
+
await chrono.mutate(s => s.count++)
|
|
297
|
+
await chrono.mutate(s => s.count++)
|
|
298
|
+
await chrono.mutate(s => s.count++)
|
|
299
|
+
expect(chrono.state.count).is(4)
|
|
300
|
+
await chrono.undo(2)
|
|
301
|
+
expect(chrono.state.count).is(2)
|
|
302
|
+
}),
|
|
303
|
+
|
|
304
|
+
"redo 2x": test(async() => {
|
|
305
|
+
const prism = new Prism({data: chronicle({count: 1})})
|
|
306
|
+
const lens = prism.lens(s => s.data)
|
|
307
|
+
const chrono = new Chrono(64, lens)
|
|
308
|
+
await chrono.mutate(s => s.count++)
|
|
309
|
+
await chrono.mutate(s => s.count++)
|
|
310
|
+
await chrono.mutate(s => s.count++)
|
|
311
|
+
expect(chrono.state.count).is(4)
|
|
312
|
+
await chrono.undo(2)
|
|
313
|
+
expect(chrono.state.count).is(2)
|
|
314
|
+
await chrono.redo(2)
|
|
315
|
+
expect(chrono.state.count).is(4)
|
|
316
|
+
}),
|
|
317
|
+
|
|
318
|
+
"sublens mutations are undoable": test(async() => {
|
|
319
|
+
const prism = new Prism({data: chronicle({a: {count: 1}})})
|
|
320
|
+
const chrono = new Chrono(64, prism.lens(s => s.data))
|
|
321
|
+
const sublens = chrono.lens(s => s.a)
|
|
322
|
+
expect(sublens.state.count).is(1)
|
|
323
|
+
await sublens.mutate(s => s.count++)
|
|
324
|
+
expect(sublens.state.count).is(2)
|
|
325
|
+
await chrono.undo()
|
|
326
|
+
expect(sublens.state.count).is(1)
|
|
327
|
+
}),
|
|
328
|
+
}),
|
|
329
|
+
})
|
|
330
|
+
|
package/s/prism/prism.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
|
|
2
|
+
import {microbounce, sub} from "@e280/stz"
|
|
3
|
+
import {Lens} from "./lens.js"
|
|
4
|
+
|
|
5
|
+
/** state mangagement source-of-truth */
|
|
6
|
+
export class Prism<State> {
|
|
7
|
+
#state: State
|
|
8
|
+
#lenses = new Set<Lens<any>>()
|
|
9
|
+
|
|
10
|
+
on = sub<[state: State]>()
|
|
11
|
+
#onPublishDebounced = microbounce(() => this.on.publish(this.#state))
|
|
12
|
+
|
|
13
|
+
constructor(state: State) {
|
|
14
|
+
this.#state = state
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get() {
|
|
18
|
+
return this.#state
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async set(state: State) {
|
|
22
|
+
this.#state = state
|
|
23
|
+
await Promise.all([...this.#lenses].map(lens => lens.update()))
|
|
24
|
+
await this.#onPublishDebounced()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
lens<State2>(selector: (state: State) => State2) {
|
|
28
|
+
const lens = new Lens<State2>({
|
|
29
|
+
getState: () => selector(this.#state),
|
|
30
|
+
mutate: async fn => {
|
|
31
|
+
const result = fn(selector(this.#state))
|
|
32
|
+
await this.set(this.#state)
|
|
33
|
+
return result
|
|
34
|
+
},
|
|
35
|
+
registerLens: lens => this.#lenses.add(lens),
|
|
36
|
+
})
|
|
37
|
+
this.#lenses.add(lens)
|
|
38
|
+
return lens
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
package/s/prism/types.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
|
|
2
|
+
import {Lens} from "./lens.js"
|
|
3
|
+
|
|
4
|
+
export type LensLike<State> = {
|
|
5
|
+
readonly state: Immutable<State>
|
|
6
|
+
mutate<R>(fn: (state: State) => R): Promise<R>
|
|
7
|
+
lens<S2>(selector: (state: State) => S2): Lens<S2>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type Optic<State> = {
|
|
11
|
+
getState: () => State
|
|
12
|
+
mutate: <R>(fn: (state: State) => R) => Promise<R>
|
|
13
|
+
registerLens: (lens: Lens<any>) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type Immutable<T> =
|
|
17
|
+
T extends (...args: any[]) => any ? T :
|
|
18
|
+
T extends readonly any[] ? ReadonlyArray<Immutable<T[number]>> :
|
|
19
|
+
T extends object ? { readonly [K in keyof T]: Immutable<T[K]> } :
|
|
20
|
+
T
|
|
21
|
+
|
|
22
|
+
export type Mutable<T> =
|
|
23
|
+
T extends (...args: any[]) => any ? T :
|
|
24
|
+
T extends ReadonlyArray<infer U> ? Mutable<U>[] :
|
|
25
|
+
T extends object ? { -readonly [K in keyof T]: Mutable<T[K]> } :
|
|
26
|
+
T
|
|
27
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
export class CacheCell<R> {
|
|
3
|
+
#dirty = false
|
|
4
|
+
#value: R
|
|
5
|
+
|
|
6
|
+
constructor(private calculate: () => R) {
|
|
7
|
+
this.#value = calculate()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
get() {
|
|
11
|
+
if (this.#dirty) {
|
|
12
|
+
this.#dirty = false
|
|
13
|
+
this.#value = this.calculate()
|
|
14
|
+
}
|
|
15
|
+
return this.#value
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
invalidate() {
|
|
19
|
+
this.#dirty = true
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
|
|
2
|
+
import {EzStore} from "./types.js"
|
|
3
|
+
|
|
4
|
+
export class LocalStore<X> implements EzStore<X> {
|
|
5
|
+
constructor(
|
|
6
|
+
private key: string,
|
|
7
|
+
private storage: Storage = window.localStorage,
|
|
8
|
+
) {}
|
|
9
|
+
|
|
10
|
+
async get() {
|
|
11
|
+
const json = this.storage.getItem(this.key)
|
|
12
|
+
return json
|
|
13
|
+
? JSON.parse(json)
|
|
14
|
+
: undefined
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async set(data: X) {
|
|
18
|
+
const json = JSON.stringify(data)
|
|
19
|
+
this.storage.setItem(this.key, json)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
onStorageEvent(fn: () => void) {
|
|
23
|
+
const listener = (event: StorageEvent) => {
|
|
24
|
+
if (event.storageArea === this.storage && event.key === this.key)
|
|
25
|
+
fn()
|
|
26
|
+
}
|
|
27
|
+
window.addEventListener("storage", listener)
|
|
28
|
+
return () => window.removeEventListener("storage", listener)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
|
|
2
|
+
import {Prism} from "../prism.js"
|
|
3
|
+
|
|
4
|
+
export type Versioned<State> = {
|
|
5
|
+
state: State
|
|
6
|
+
version: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type EzStore<X> = {
|
|
10
|
+
get(): Promise<X | undefined>
|
|
11
|
+
set(data: X | undefined): Promise<void>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type VaultOptions<State> = {
|
|
15
|
+
version: number
|
|
16
|
+
prism: Prism<State>
|
|
17
|
+
store: EzStore<Versioned<State>>
|
|
18
|
+
}
|
|
19
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
|
|
2
|
+
import {VaultOptions} from "./types.js"
|
|
3
|
+
|
|
4
|
+
export class Vault<State> {
|
|
5
|
+
constructor(private options: VaultOptions<State>) {}
|
|
6
|
+
|
|
7
|
+
load = async() => {
|
|
8
|
+
const {store, version, prism} = this.options
|
|
9
|
+
const pickle = await store.get()
|
|
10
|
+
if (pickle && pickle.version === version)
|
|
11
|
+
await prism.set(pickle.state)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
save = async() => {
|
|
15
|
+
const {store, version, prism} = this.options
|
|
16
|
+
await store.set({version, state: prism.get()})
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
package/s/signals/core/effect.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import {
|
|
2
|
+
import {microbounce} from "@e280/stz"
|
|
3
3
|
import {tracker} from "../../tracker/tracker.js"
|
|
4
4
|
|
|
5
5
|
export function effect(
|
|
@@ -16,7 +16,7 @@ export function collectorEffect<C = void>(
|
|
|
16
16
|
) {
|
|
17
17
|
|
|
18
18
|
const {seen, result} = tracker.observe(collector)
|
|
19
|
-
const fn =
|
|
19
|
+
const fn = microbounce(responder)
|
|
20
20
|
|
|
21
21
|
const disposers: (() => void)[] = []
|
|
22
22
|
const dispose = () => disposers.forEach(d => d())
|
package/s/tests.test.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {expect, Science, test} from "@e280/science"
|
|
3
3
|
|
|
4
4
|
import tree from "./tree/tree.test.js"
|
|
5
|
+
import prism from "./prism/prism.test.js"
|
|
5
6
|
import signals from "./signals/signals.test.js"
|
|
6
7
|
import tracker from "./tracker/tracker.test.js"
|
|
7
8
|
|
|
@@ -11,10 +12,12 @@ import {Trunk} from "./tree/parts/trunk.js"
|
|
|
11
12
|
|
|
12
13
|
await Science.run({
|
|
13
14
|
tree,
|
|
15
|
+
prism,
|
|
16
|
+
|
|
14
17
|
signals,
|
|
15
18
|
tracker,
|
|
16
19
|
|
|
17
|
-
interop: Science.suite({
|
|
20
|
+
interop: Science.suite.skip({
|
|
18
21
|
"effect responds to trunk change": test(async() => {
|
|
19
22
|
const trunk = new Trunk({count: 1})
|
|
20
23
|
|
package/s/tree/parts/branch.ts
CHANGED
|
@@ -1,37 +1,51 @@
|
|
|
1
1
|
|
|
2
2
|
import {deep} from "@e280/stz"
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import {Immutable} from "../../prism/types.js"
|
|
4
|
+
import {SignalFn} from "../../signals/types.js"
|
|
5
|
+
import {signal} from "../../signals/porcelain.js"
|
|
6
|
+
import {Branchstate, Mutator, TreeOptions, Selector, Tree} from "./types.js"
|
|
6
7
|
|
|
8
|
+
/** @deprecated tree stuff has been replaced by prism/lens stuff */
|
|
7
9
|
export class Branch<S extends Branchstate, ParentState extends Branchstate = any> implements Tree<S> {
|
|
8
|
-
#
|
|
10
|
+
#previous: Immutable<S>
|
|
11
|
+
#$data: SignalFn<Immutable<S>>
|
|
9
12
|
|
|
10
13
|
constructor(
|
|
11
14
|
private parent: Tree<ParentState>,
|
|
12
15
|
private selector: Selector<S, ParentState>,
|
|
13
|
-
private options:
|
|
16
|
+
private options: TreeOptions,
|
|
14
17
|
) {
|
|
15
18
|
|
|
16
|
-
this
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
this.#$data = signal(this.#pull())
|
|
20
|
+
this.#previous = this.state
|
|
21
|
+
|
|
22
|
+
this.parent.on(() => {
|
|
23
|
+
const oldState = this.#previous
|
|
24
|
+
const newState = this.#pull()
|
|
25
|
+
if (!deep.equal(newState, oldState)) {
|
|
26
|
+
this.#previous = newState
|
|
27
|
+
this.#$data.set(newState, true)
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#pull() {
|
|
33
|
+
return this.selector(this.parent.state as ParentState) as Immutable<S>
|
|
20
34
|
}
|
|
21
35
|
|
|
22
36
|
get state() {
|
|
23
|
-
return this
|
|
37
|
+
return this.#$data.get()
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
get on() {
|
|
27
|
-
return this
|
|
41
|
+
return this.#$data.on
|
|
28
42
|
}
|
|
29
43
|
|
|
30
44
|
async mutate(mutator: Mutator<S>) {
|
|
31
|
-
await this.parent.mutate(
|
|
32
|
-
mutator(this.selector(parentState))
|
|
45
|
+
await this.parent.mutate(
|
|
46
|
+
parentState => mutator(this.selector(parentState))
|
|
33
47
|
)
|
|
34
|
-
return this
|
|
48
|
+
return this.state
|
|
35
49
|
}
|
|
36
50
|
|
|
37
51
|
branch<Sub extends Branchstate>(selector: Selector<Sub, S>): Branch<Sub, S> {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
|
|
2
|
+
import {Chronicle, Immutable} from "../../prism/index.js"
|
|
2
3
|
import {Branch} from "./branch.js"
|
|
3
|
-
import {Branchstate,
|
|
4
|
+
import {Branchstate, Mutator, TreeOptions, Selector, Tree} from "./types.js"
|
|
4
5
|
|
|
6
|
+
/** @deprecated tree stuff has been replaced by prism/lens stuff */
|
|
5
7
|
export class Chronobranch<S extends Branchstate, ParentState extends Branchstate = any> implements Tree<S> {
|
|
6
8
|
#branch: Branch<Chronicle<S>, ParentState>
|
|
7
9
|
|
|
@@ -9,7 +11,7 @@ export class Chronobranch<S extends Branchstate, ParentState extends Branchstate
|
|
|
9
11
|
public limit: number,
|
|
10
12
|
public parent: Tree<ParentState>,
|
|
11
13
|
public selector: Selector<Chronicle<S>, ParentState>,
|
|
12
|
-
public options:
|
|
14
|
+
public options: TreeOptions,
|
|
13
15
|
) {
|
|
14
16
|
this.#branch = parent.branch(selector)
|
|
15
17
|
}
|