@e280/strata 0.2.2 → 0.2.4

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.
Files changed (87) hide show
  1. package/README.md +123 -128
  2. package/package.json +6 -4
  3. package/s/index.ts +1 -0
  4. package/s/prism/chrono/chronicle.ts +11 -0
  5. package/s/prism/chrono/chrono.ts +91 -0
  6. package/s/prism/chrono/types.ts +12 -0
  7. package/s/prism/index.ts +11 -0
  8. package/s/prism/lens.ts +54 -0
  9. package/s/prism/prism.test.ts +342 -0
  10. package/s/prism/prism.ts +44 -0
  11. package/s/prism/types.ts +27 -0
  12. package/s/prism/utils/cache-cell.ts +22 -0
  13. package/s/prism/utils/immute.ts +8 -0
  14. package/s/prism/utils/optic-symbol.ts +3 -0
  15. package/s/prism/vault/local-store.ts +31 -0
  16. package/s/prism/vault/types.ts +19 -0
  17. package/s/prism/vault/vault.ts +19 -0
  18. package/s/tests.test.ts +4 -1
  19. package/s/tree/parts/branch.ts +25 -12
  20. package/s/tree/parts/chronobranch.ts +3 -1
  21. package/s/tree/parts/trunk.ts +3 -1
  22. package/s/tree/parts/types.ts +32 -30
  23. package/s/tree/parts/utils/immute.ts +4 -4
  24. package/s/tree/tree.test.ts +30 -2
  25. package/x/index.d.ts +1 -0
  26. package/x/index.js +1 -0
  27. package/x/index.js.map +1 -1
  28. package/x/prism/chrono/chronicle.d.ts +2 -0
  29. package/x/prism/chrono/chronicle.js +8 -0
  30. package/x/prism/chrono/chronicle.js.map +1 -0
  31. package/x/prism/chrono/chrono.d.ts +26 -0
  32. package/x/prism/chrono/chrono.js +79 -0
  33. package/x/prism/chrono/chrono.js.map +1 -0
  34. package/x/prism/chrono/types.d.ts +5 -0
  35. package/x/prism/chrono/types.js +2 -0
  36. package/x/prism/chrono/types.js.map +1 -0
  37. package/x/prism/index.d.ts +9 -0
  38. package/x/prism/index.js +10 -0
  39. package/x/prism/index.js.map +1 -0
  40. package/x/prism/lens.d.ts +13 -0
  41. package/x/prism/lens.js +45 -0
  42. package/x/prism/lens.js.map +1 -0
  43. package/x/prism/prism.d.ts +10 -0
  44. package/x/prism/prism.js +37 -0
  45. package/x/prism/prism.js.map +1 -0
  46. package/x/prism/prism.test.d.ts +36 -0
  47. package/x/prism/prism.test.js +298 -0
  48. package/x/prism/prism.test.js.map +1 -0
  49. package/x/prism/types.d.ts +17 -0
  50. package/x/prism/types.js +2 -0
  51. package/x/prism/types.js.map +1 -0
  52. package/x/prism/utils/cache-cell.d.ts +7 -0
  53. package/x/prism/utils/cache-cell.js +20 -0
  54. package/x/prism/utils/cache-cell.js.map +1 -0
  55. package/x/prism/utils/immute.d.ts +2 -0
  56. package/x/prism/utils/immute.js +5 -0
  57. package/x/prism/utils/immute.js.map +1 -0
  58. package/x/prism/utils/optic-symbol.d.ts +1 -0
  59. package/x/prism/utils/optic-symbol.js +2 -0
  60. package/x/prism/utils/optic-symbol.js.map +1 -0
  61. package/x/prism/vault/local-store.d.ts +9 -0
  62. package/x/prism/vault/local-store.js +27 -0
  63. package/x/prism/vault/local-store.js.map +1 -0
  64. package/x/prism/vault/types.d.ts +14 -0
  65. package/x/prism/vault/types.js +2 -0
  66. package/x/prism/vault/types.js.map +1 -0
  67. package/x/prism/vault/vault.d.ts +7 -0
  68. package/x/prism/vault/vault.js +17 -0
  69. package/x/prism/vault/vault.js.map +1 -0
  70. package/x/tests.test.js +3 -1
  71. package/x/tests.test.js.map +1 -1
  72. package/x/tree/parts/branch.d.ts +4 -2
  73. package/x/tree/parts/branch.js +17 -9
  74. package/x/tree/parts/branch.js.map +1 -1
  75. package/x/tree/parts/chronobranch.d.ts +3 -1
  76. package/x/tree/parts/chronobranch.js +1 -0
  77. package/x/tree/parts/chronobranch.js.map +1 -1
  78. package/x/tree/parts/trunk.d.ts +6 -4
  79. package/x/tree/parts/trunk.js +1 -0
  80. package/x/tree/parts/trunk.js.map +1 -1
  81. package/x/tree/parts/types.d.ts +2 -19
  82. package/x/tree/parts/utils/immute.d.ts +2 -1
  83. package/x/tree/parts/utils/immute.js +2 -3
  84. package/x/tree/parts/utils/immute.js.map +1 -1
  85. package/x/tree/tree.test.d.ts +4 -2
  86. package/x/tree/tree.test.js +28 -2
  87. package/x/tree/tree.test.js.map +1 -1
package/README.md CHANGED
@@ -9,13 +9,13 @@
9
9
 
10
10
  ### get in loser, we're managing state
11
11
  📦 `npm install @e280/strata`
12
+ ✨ it's all about automagically rerendering ui when data changes
13
+ 🦝 powers auto-reactivity in our view library [@e280/sly](https://github.com/e280/sly)
12
14
  🧙‍♂️ probably my tenth state management library, lol
13
- 💁 it's all about rerendering ui when data changes
14
- 🦝 powers reactivity in our view library [@e280/sly](https://github.com/e280/sly)
15
- 🧑‍💻 a project by https://e280.org/
15
+ 🧑‍💻 a project by https://e280.org/
16
16
 
17
17
  🚦 [**signals**](#signals) — ephemeral view-level state
18
- 🌳 [**tree**](#tree) — persistent app-level state
18
+ 🔮 [**prism**](#prism) — app-level state tree
19
19
  🪄 [**tracker**](#tracker) — reactivity integration hub
20
20
 
21
21
 
@@ -24,7 +24,7 @@
24
24
 
25
25
  <a id="signals"></a>
26
26
 
27
- ## 🚦 strata signals
27
+ ## 🍋 strata signals
28
28
  > *ephemeral view-level state*
29
29
 
30
30
  ```ts
@@ -152,140 +152,135 @@ import {signal, effect} from "@e280/strata"
152
152
 
153
153
  <br/><br/>
154
154
 
155
- <a id="tree"></a>
155
+ <a id="prism"></a>
156
156
 
157
- ## 🌳 strata tree
157
+ ## 🍋 strata prism
158
158
  > *persistent app-level state*
159
159
 
160
- ```ts
161
- import {Trunk} from "@e280/strata"
162
- ```
163
-
164
160
  - single-source-of-truth state tree
165
- - immutable except for `mutate(fn)` calls
166
- - localStorage persistence, cross-tab sync, undo/redo history
167
161
  - no spooky-dookie proxy magic — just god's honest javascript
162
+ - immutable except for `mutate(fn)` calls
163
+ - use many lenses, efficient reactivity
164
+ - chrono provides undo/redo history
165
+ - persistence, localstorage, cross-tab sync
168
166
 
169
- #### 🌳 `Trunk` is your app's state tree root
170
- - better stick to json-friendly serializable data
171
- ```ts
172
- const trunk = new Trunk({
173
- count: 0,
174
- snacks: {
175
- peanuts: 8,
176
- bag: ["popcorn", "butter"],
177
- },
178
- })
179
-
180
- trunk.state.count // 0
181
- trunk.state.snacks.peanuts // 8
182
- ```
183
-
184
- #### 🌳 formal mutations to change state
185
- - ⛔ informal mutations are denied
186
- ```ts
187
- trunk.state.count++ // error is thrown
188
- ```
189
- - formal mutations are allowed
190
- ```ts
191
- await trunk.mutate(s => s.count++)
192
- ```
193
-
194
- #### 🌳 `Branch` is a view into a subtree
195
- - it's a lens, make lots of them, pass 'em around your app
196
- ```ts
197
- const snacks = trunk.branch(s => s.snacks)
198
- ```
199
- - run branch mutations
200
- ```ts
201
- await snacks.mutate(s => s.peanuts++)
202
- ```
203
- - array mutations are unironically based, actually
204
- ```ts
205
- await snacks.mutate(s => s.bag.push("salt"))
206
- ```
207
- - you can branch a branch
167
+ ### 🔮 prism and lenses
168
+ - **import prism**
169
+ ```ts
170
+ import {Prism} from "@e280/strata"
171
+ ```
172
+ - **prism is a state tree**
173
+ ```ts
174
+ const prism = new Prism({
175
+ snacks: {
176
+ peanuts: 8,
177
+ bag: ["popcorn", "butter"],
178
+ person: {
179
+ name: "chase",
180
+ incredi: true,
181
+ },
182
+ },
183
+ })
184
+ ```
185
+ - **create lenses, which are views into state subtrees**
186
+ ```ts
187
+ const snacks = prism.lens(state => state.snacks)
188
+ const person = snacks.lens(state => state.person)
189
+ ```
190
+ - you can lens another lens
191
+ - **lenses provide immutable access to state**
192
+ ```ts
193
+ snacks.state.peanuts // 8
194
+ person.state.name // "chase"
195
+ ```
196
+ - **only formal mutations can change state**
197
+ ```ts
198
+ snacks.state.peanuts++
199
+ // error: casual mutations forbidden
200
+ ```
201
+ ```ts
202
+ snacks.mutate(state => state.peanuts++)
203
+ // only proper mutations can make state changes
208
204
 
209
- #### 🌳 `on` to watch for mutations
210
- - on the trunk, we can listen deeply for mutations within the whole tree
211
- ```ts
212
- trunk.on(s => console.log(s.count))
213
- ```
214
- - whereas branch listeners don't care about changes outside their scope
215
- ```ts
216
- snacks.on(s => console.log(s.peanuts))
217
- ```
218
- - on returns a fn to stop listening
219
- ```ts
220
- const stop = trunk.on(s => console.log(s.count))
221
- stop() // stop listening
222
- ```
205
+ snacks.state.peanuts // 9
206
+ ```
207
+ - **array mutations are unironically based, actually**
208
+ ```ts
209
+ await snacks.mutate(state => state.bag.push("salt"))
210
+ ```
223
211
 
224
- ### 🌳 fancy advanced usage
225
- > *only discerning high-class aristocrats are permitted beyond this point*
212
+ ### 🔮 chrono for time travel
213
+ - **import stuff**
214
+ ```ts
215
+ import {Chrono, chronicle} from "@e280/strata"
216
+ ```
217
+ - **create a chronicle in your state**
218
+ ```ts
219
+ const prism = new Prism({
220
+
221
+ // chronicle stores history
222
+ // 👇
223
+ snacks: chronicle({
224
+ peanuts: 8,
225
+ bag: ["popcorn", "butter"],
226
+ person: {
227
+ name: "chase",
228
+ incredi: true,
229
+ },
230
+ }),
231
+ })
232
+ ```
233
+ - *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*
234
+ - **create a chrono-wrapped lens to interact with your chronicle**
235
+ ```ts
236
+ const snacks = new Chrono(64, prism.lens(state => state.snacks))
237
+ // 👆
238
+ // how many past snapshots to store
239
+ ```
240
+ - **mutations will advance history,** and undo/redo works
241
+ ```ts
242
+ snacks.mutate(s => s.peanuts = 101)
226
243
 
227
- #### 🌳 `Trunk.setup` for localStorage persistence etc
228
- - it automatically handles persistence to localStorage and cross-tab synchronization
229
- - simple setup
230
- ```ts
231
- const {trunk} = await Trunk.setup({
232
- version: 1, // 👈 bump whenever you change state schema!
233
- initialState: {count: 0},
234
- })
235
- ```
236
- - uses localStorage by default
237
- - it's compatible with [`@e280/kv`](https://github.com/e280/kv)
238
- ```ts
239
- import {Kv, StorageDriver} from "@e280/kv"
244
+ snacks.undo()
245
+ // back to 8 peanuts
240
246
 
241
- const kv = new Kv(new StorageDriver())
242
- const store = kv.store<any>("appState")
247
+ snacks.redo()
248
+ // forward to 101 peanuts
249
+ ```
250
+ - **check how many undoable or redoable steps are available**
251
+ ```ts
252
+ snacks.undoable // 1
253
+ snacks.redoable // 0
254
+ ```
255
+ - **you can make sub-lenses of a chrono,** all their mutations advance history too
256
+ - **plz pinky-swear right now,** that you won't create a chrono under a lens under another chrono 💀
243
257
 
244
- const {trunk} = await Trunk.setup({
245
- version: 1,
246
- initialState: {count: 0},
247
- persistence: {
258
+ ### 🔮 persistence to localStorage
259
+ - **import prism**
260
+ ```ts
261
+ import {Vault, LocalStore} from "@e280/strata"
262
+ ```
263
+ - **create a local storage store**
264
+ ```ts
265
+ const store = new LocalStore("myAppState")
266
+ ```
267
+ - **make a vault for your prism**
268
+ ```ts
269
+ const vault = new Vault({
270
+ prism,
248
271
  store,
249
- onChange: StorageDriver.onStorageEvent,
250
- },
251
- })
252
- ```
253
-
254
- #### 🌳 `Chronobranch` for undo/redo history
255
- - first, put a `Chronicle` into your state tree
256
- ```ts
257
- const trunk = new Trunk({
258
- count: 0,
259
- snacks: Trunk.chronicle({
260
- peanuts: 8,
261
- bag: ["popcorn", "butter"],
262
- }),
263
- })
264
- ```
265
- - *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*
266
- - second, make a `Chronobranch` which is like a branch, but is concerned with history
267
- ```ts
268
- const snacks = trunk.chronobranch(64, s => s.snacks)
269
- // \
270
- // how many past snapshots to store
271
- ```
272
- - mutations will advance history (undoable/redoable)
273
- ```ts
274
- await snacks.mutate(s => s.peanuts = 101)
275
-
276
- await snacks.undo()
277
- // back to 8 peanuts
278
-
279
- await snacks.redo()
280
- // forward to 101 peanuts
281
- ```
282
- - you can check how many undoable or redoable steps are available
283
- ```ts
284
- snacks.undoable // 2
285
- snacks.redoable // 1
286
- ```
287
- - chronobranch can have its own branches — all their mutations advance history
288
- - plz pinky-swear right now, that you won't create a chronobranch under a branch under another chronobranch 💀
272
+ version: 1, // 👈 bump this when you break your state schema!
273
+ })
274
+ ```
275
+ - `store` type is compatible with [`@e280/kv`](https://github.com/e280/kv)
276
+ - **cross-tab sync (load on storage events)**
277
+ ```ts
278
+ store.onStorageEvent(vault.load)
279
+ ```
280
+ - **initial load**
281
+ ```ts
282
+ await vault.load()
283
+ ```
289
284
 
290
285
 
291
286
 
@@ -293,7 +288,7 @@ import {Trunk} from "@e280/strata"
293
288
 
294
289
  <a id="tracker"></a>
295
290
 
296
- ## 🪄 strata tracker
291
+ ## 🍋 strata tracker
297
292
  > *reactivity integration hub*
298
293
 
299
294
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e280/strata",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "state management",
5
5
  "license": "MIT",
6
6
  "author": "Chase Moskal <chasemoskal@gmail.com>",
@@ -12,6 +12,7 @@
12
12
  ],
13
13
  "exports": {
14
14
  ".": "./x/index.js",
15
+ "./prism": "./x/prism/index.js",
15
16
  "./signals": "./x/signals/index.js",
16
17
  "./tracker": "./x/tracker/index.js",
17
18
  "./tree": "./x/tree/index.js"
@@ -22,7 +23,7 @@
22
23
  "test-watch": "node --watch x/tests.test.js",
23
24
  "test-inspect": "node inspect x/tests.test.js",
24
25
  "count": "find s -path '*/_archive' -prune -o -name '*.ts' -exec wc -l {} +",
25
- "watch": "run-p _tscw test-watch",
26
+ "start": "octo 'npm run _tscw -s' 'npm run test-watch -s'",
26
27
  "_clean": "rm -rf x && mkdir x",
27
28
  "_links": "ln -s \"$(realpath node_modules)\" x/node_modules",
28
29
  "_tsc": "tsc",
@@ -32,8 +33,9 @@
32
33
  "@e280/stz": "^0.2.14"
33
34
  },
34
35
  "devDependencies": {
35
- "@e280/science": "^0.1.3",
36
- "@types/node": "^24.9.1",
36
+ "@e280/science": "^0.1.4",
37
+ "@e280/scute": "^0.1.1",
38
+ "@types/node": "^24.10.0",
37
39
  "npm-run-all": "^4.1.5",
38
40
  "typescript": "^5.9.3"
39
41
  },
package/s/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
 
2
+ export * from "./prism/index.js"
2
3
  export * from "./signals/index.js"
3
4
  export * from "./tracker/index.js"
4
5
  export * from "./tree/index.js"
@@ -0,0 +1,11 @@
1
+
2
+ import {Chronicle} from "./types.js"
3
+
4
+ export function chronicle<State>(state: State): Chronicle<State> {
5
+ return {
6
+ past: [],
7
+ present: state,
8
+ future: [],
9
+ }
10
+ }
11
+
@@ -0,0 +1,91 @@
1
+
2
+ import {deep} from "@e280/stz"
3
+ import {Lens} from "../lens.js"
4
+ import {Chronicle} from "./types.js"
5
+ import {LensLike} from "../types.js"
6
+ import {_optic} from "../utils/optic-symbol.js"
7
+
8
+ export class Chrono<State> implements LensLike<State> {
9
+ constructor(
10
+ public limit: number,
11
+ private basis: Lens<Chronicle<State>>,
12
+ ) {}
13
+
14
+ get chronicle() {
15
+ return this.basis.state
16
+ }
17
+
18
+ get state() {
19
+ return this.basis.state.present
20
+ }
21
+
22
+ get undoable() {
23
+ return this.chronicle.past.length
24
+ }
25
+
26
+ get redoable() {
27
+ return this.chronicle.future.length
28
+ }
29
+
30
+ #mut<R>(chronicle: Chronicle<State>, fn: (state: State) => R) {
31
+ const limit = Math.max(0, this.limit)
32
+ const snapshot = deep.clone(this.chronicle.present) as State
33
+ const result = fn(chronicle.present)
34
+ chronicle.past.push(snapshot)
35
+ chronicle.past = chronicle.past.slice(-limit)
36
+ chronicle.future = []
37
+ return result
38
+ }
39
+
40
+ /** progress forwards, depositing history into the past */
41
+ async mutate<R>(fn: (state: State) => R): Promise<R> {
42
+ return this.basis.mutate(chronicle => this.#mut(chronicle, fn))
43
+ }
44
+
45
+ /** step backwards into the past, by n steps */
46
+ async undo(n = 1) {
47
+ await this.basis.mutate(chronicle => {
48
+ const snapshots = chronicle.past.slice(-n)
49
+ if (snapshots.length >= n) {
50
+ const oldPresent = chronicle.present
51
+ chronicle.present = snapshots.shift()!
52
+ chronicle.past = chronicle.past.slice(0, -n)
53
+ chronicle.future.unshift(oldPresent, ...snapshots)
54
+ }
55
+ })
56
+ }
57
+
58
+ /** step forwards into the future, by n steps */
59
+ async redo(n = 1) {
60
+ await this.basis.mutate(chronicle => {
61
+ const snapshots = chronicle.future.slice(0, n)
62
+ if (snapshots.length >= n) {
63
+ const oldPresent = chronicle.present
64
+ chronicle.present = snapshots.shift()!
65
+ chronicle.past.push(oldPresent, ...snapshots)
66
+ chronicle.future = chronicle.future.slice(n)
67
+ }
68
+ })
69
+ }
70
+
71
+ /** wipe past and future snapshots */
72
+ async wipe() {
73
+ await this.basis.mutate(chronicle => {
74
+ chronicle.past = []
75
+ chronicle.future = []
76
+ })
77
+ }
78
+
79
+ lens<State2>(selector: (state: State) => State2) {
80
+ const lens = new Lens<State2>({
81
+ registerLens: this.basis[_optic].registerLens,
82
+ getState: () => selector(this.basis[_optic].getState().present),
83
+ mutate: fn => this.basis[_optic].mutate(chronicle => {
84
+ return this.#mut(chronicle, state => fn(selector(state)))
85
+ }),
86
+ })
87
+ this.basis[_optic].registerLens(lens)
88
+ return lens
89
+ }
90
+ }
91
+
@@ -0,0 +1,12 @@
1
+
2
+ export type Chronicle<State> = {
3
+ // [abc] d [efg]
4
+ // \ \ \
5
+ // \ \ future
6
+ // \ present
7
+ // past
8
+ past: State[]
9
+ present: State
10
+ future: State[]
11
+ }
12
+
@@ -0,0 +1,11 @@
1
+
2
+ export * from "./chrono/chronicle.js"
3
+ export * from "./chrono/chrono.js"
4
+ export * from "./chrono/types.js"
5
+ export * from "./vault/local-store.js"
6
+ export * from "./vault/vault.js"
7
+ export * from "./vault/types.js"
8
+ export * from "./lens.js"
9
+ export * from "./prism.js"
10
+ export * from "./types.js"
11
+
@@ -0,0 +1,54 @@
1
+
2
+ import {deep, microbounce, sub} from "@e280/stz"
3
+ import {immute} from "./utils/immute.js"
4
+ import {tracker} from "../tracker/tracker.js"
5
+ import {_optic} from "./utils/optic-symbol.js"
6
+ import {CacheCell} from "./utils/cache-cell.js"
7
+ import {Immutable, LensLike, Optic} from "./types.js"
8
+
9
+ /** reactive view into a state prism, with formalized mutations */
10
+ export class Lens<State> implements LensLike<State> {
11
+ on = sub<[state: Immutable<State>]>()
12
+
13
+ ;[_optic]: Optic<State>
14
+ #previous: State
15
+ #immutable: CacheCell<Immutable<State>>
16
+ #onPublishDebounced = microbounce(() => this.on.publish(this.state))
17
+
18
+ constructor(optic: Optic<State>) {
19
+ this[_optic] = optic
20
+ this.#previous = deep.clone(optic.getState())
21
+ this.#immutable = new CacheCell(() => immute(optic.getState()))
22
+ }
23
+
24
+ async update() {
25
+ const state = this[_optic].getState()
26
+ const isChanged = !deep.equal(state, this.#previous)
27
+ if (isChanged) {
28
+ this.#immutable.invalidate()
29
+ this.#previous = deep.clone(state)
30
+ this.#onPublishDebounced()
31
+ await tracker.notifyWrite(this)
32
+ }
33
+ }
34
+
35
+ get state() {
36
+ tracker.notifyRead(this)
37
+ return this.#immutable.get()
38
+ }
39
+
40
+ async mutate<R>(fn: (state: State) => R) {
41
+ return this[_optic].mutate(fn)
42
+ }
43
+
44
+ lens<State2>(selector: (state: State) => State2) {
45
+ const lens = new Lens<State2>({
46
+ getState: () => selector(this[_optic].getState()),
47
+ mutate: fn => this[_optic].mutate(state => fn(selector(state))),
48
+ registerLens: this[_optic].registerLens,
49
+ })
50
+ this[_optic].registerLens(lens)
51
+ return lens
52
+ }
53
+ }
54
+