@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.
Files changed (93) hide show
  1. package/README.md +132 -131
  2. package/package.json +7 -5
  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 +330 -0
  10. package/s/prism/prism.ts +41 -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/signals/core/effect.ts +2 -2
  19. package/s/tests.test.ts +4 -1
  20. package/s/tree/parts/branch.ts +28 -14
  21. package/s/tree/parts/chronobranch.ts +4 -2
  22. package/s/tree/parts/trunk.ts +18 -22
  23. package/s/tree/parts/types.ts +36 -31
  24. package/s/tree/parts/utils/immute.ts +43 -0
  25. package/s/tree/parts/utils/process-options.ts +2 -2
  26. package/s/tree/tree.test.ts +69 -19
  27. package/x/index.d.ts +1 -0
  28. package/x/index.js +1 -0
  29. package/x/index.js.map +1 -1
  30. package/x/prism/chrono/chronicle.d.ts +2 -0
  31. package/x/prism/chrono/chronicle.js +8 -0
  32. package/x/prism/chrono/chronicle.js.map +1 -0
  33. package/x/prism/chrono/chrono.d.ts +26 -0
  34. package/x/prism/chrono/chrono.js +79 -0
  35. package/x/prism/chrono/chrono.js.map +1 -0
  36. package/x/prism/chrono/types.d.ts +5 -0
  37. package/x/prism/chrono/types.js +2 -0
  38. package/x/prism/chrono/types.js.map +1 -0
  39. package/x/prism/index.d.ts +9 -0
  40. package/x/prism/index.js +10 -0
  41. package/x/prism/index.js.map +1 -0
  42. package/x/prism/lens.d.ts +13 -0
  43. package/x/prism/lens.js +45 -0
  44. package/x/prism/lens.js.map +1 -0
  45. package/x/prism/prism.d.ts +10 -0
  46. package/x/prism/prism.js +34 -0
  47. package/x/prism/prism.js.map +1 -0
  48. package/x/prism/prism.test.d.ts +35 -0
  49. package/x/prism/prism.test.js +286 -0
  50. package/x/prism/prism.test.js.map +1 -0
  51. package/x/prism/types.d.ts +17 -0
  52. package/x/prism/types.js +2 -0
  53. package/x/prism/types.js.map +1 -0
  54. package/x/prism/utils/cache-cell.d.ts +7 -0
  55. package/x/prism/utils/cache-cell.js +20 -0
  56. package/x/prism/utils/cache-cell.js.map +1 -0
  57. package/x/prism/utils/immute.d.ts +2 -0
  58. package/x/prism/utils/immute.js +5 -0
  59. package/x/prism/utils/immute.js.map +1 -0
  60. package/x/prism/utils/optic-symbol.d.ts +1 -0
  61. package/x/prism/utils/optic-symbol.js +2 -0
  62. package/x/prism/utils/optic-symbol.js.map +1 -0
  63. package/x/prism/vault/local-store.d.ts +9 -0
  64. package/x/prism/vault/local-store.js +27 -0
  65. package/x/prism/vault/local-store.js.map +1 -0
  66. package/x/prism/vault/types.d.ts +14 -0
  67. package/x/prism/vault/types.js +2 -0
  68. package/x/prism/vault/types.js.map +1 -0
  69. package/x/prism/vault/vault.d.ts +7 -0
  70. package/x/prism/vault/vault.js +17 -0
  71. package/x/prism/vault/vault.js.map +1 -0
  72. package/x/signals/core/effect.js +2 -2
  73. package/x/signals/core/effect.js.map +1 -1
  74. package/x/tests.test.js +3 -1
  75. package/x/tests.test.js.map +1 -1
  76. package/x/tree/parts/branch.d.ts +4 -2
  77. package/x/tree/parts/branch.js +20 -9
  78. package/x/tree/parts/branch.js.map +1 -1
  79. package/x/tree/parts/chronobranch.d.ts +5 -3
  80. package/x/tree/parts/chronobranch.js +1 -0
  81. package/x/tree/parts/chronobranch.js.map +1 -1
  82. package/x/tree/parts/trunk.d.ts +8 -6
  83. package/x/tree/parts/trunk.js +14 -14
  84. package/x/tree/parts/trunk.js.map +1 -1
  85. package/x/tree/parts/types.d.ts +5 -20
  86. package/x/tree/parts/utils/immute.d.ts +11 -0
  87. package/x/tree/parts/utils/immute.js +33 -0
  88. package/x/tree/parts/utils/immute.js.map +1 -0
  89. package/x/tree/parts/utils/process-options.d.ts +2 -2
  90. package/x/tree/parts/utils/process-options.js.map +1 -1
  91. package/x/tree/tree.test.d.ts +6 -3
  92. package/x/tree/tree.test.js +64 -18
  93. package/x/tree/tree.test.js.map +1 -1
package/README.md CHANGED
@@ -9,20 +9,22 @@
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
- 🚦 **signals** — ephemeral view-level state
18
- 🌳 **tree** — persistent app-level state
19
- 🪄 **tracker** — reactivity integration hub
17
+ 🚦 [**signals**](#signals) — ephemeral view-level state
18
+ 🔮 [**prism**](#prism) — app-level state tree
19
+ 🪄 [**tracker**](#tracker) — reactivity integration hub
20
20
 
21
21
 
22
22
 
23
23
  <br/><br/>
24
24
 
25
- ## 🚦 strata signals
25
+ <a id="signals"></a>
26
+
27
+ ## 🍋 strata signals
26
28
  > *ephemeral view-level state*
27
29
 
28
30
  ```ts
@@ -150,144 +152,143 @@ import {signal, effect} from "@e280/strata"
150
152
 
151
153
  <br/><br/>
152
154
 
153
- ## 🌳 strata trees
154
- > *persistent app-level state*
155
+ <a id="prism"></a>
155
156
 
156
- ```ts
157
- import {Trunk} from "@e280/strata"
158
- ```
157
+ ## 🍋 strata prism
158
+ > *persistent app-level state*
159
159
 
160
160
  - single-source-of-truth state tree
161
- - immutable except for `mutate(fn)` calls
162
- - localStorage persistence, cross-tab sync, undo/redo history
163
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
164
166
 
165
- #### 🌳 `Trunk` is your app's state tree root
166
- - better stick to json-friendly serializable data
167
- ```ts
168
- const trunk = new Trunk({
169
- count: 0,
170
- snacks: {
171
- peanuts: 8,
172
- bag: ["popcorn", "butter"],
173
- },
174
- })
175
-
176
- trunk.state.count // 0
177
- trunk.state.snacks.peanuts // 8
178
- ```
179
-
180
- #### 🌳 formal mutations to change state
181
- - ⛔ informal mutations are denied
182
- ```ts
183
- trunk.state.count++ // error is thrown
184
- ```
185
- - formal mutations are allowed
186
- ```ts
187
- await trunk.mutate(s => s.count++)
188
- ```
189
-
190
- #### 🌳 `Branch` is a view into a subtree
191
- - it's a lens, make lots of them, pass 'em around your app
192
- ```ts
193
- const snacks = trunk.branch(s => s.snacks)
194
- ```
195
- - run branch mutations
196
- ```ts
197
- await snacks.mutate(s => s.peanuts++)
198
- ```
199
- - array mutations are unironically based, actually
200
- ```ts
201
- await snacks.mutate(s => s.bag.push("salt"))
202
- ```
203
- - 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
204
204
 
205
- #### 🌳 `on` to watch for mutations
206
- - on the trunk, we can listen deeply for mutations within the whole tree
207
- ```ts
208
- trunk.on(s => console.log(s.count))
209
- ```
210
- - whereas branch listeners don't care about changes outside their scope
211
- ```ts
212
- snacks.on(s => console.log(s.peanuts))
213
- ```
214
- - on returns a fn to stop listening
215
- ```ts
216
- const stop = trunk.on(s => console.log(s.count))
217
- stop() // stop listening
218
- ```
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
+ ```
219
211
 
220
- ### 🌳 fancy advanced usage
221
- > *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)
222
243
 
223
- #### 🌳 `Trunk.setup` for localStorage persistence etc
224
- - it automatically handles persistence to localStorage and cross-tab synchronization
225
- - simple setup
226
- ```ts
227
- const {trunk} = await Trunk.setup({
228
- version: 1, // 👈 bump whenever you change state schema!
229
- initialState: {count: 0},
230
- })
231
- ```
232
- - uses localStorage by default
233
- - it's compatible with [`@e280/kv`](https://github.com/e280/kv)
234
- ```ts
235
- import {Kv, StorageDriver} from "@e280/kv"
244
+ snacks.undo()
245
+ // back to 8 peanuts
236
246
 
237
- const kv = new Kv(new StorageDriver())
238
- 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 💀
239
257
 
240
- const {trunk} = await Trunk.setup({
241
- version: 1,
242
- initialState: {count: 0},
243
- 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,
244
271
  store,
245
- onChange: StorageDriver.onStorageEvent,
246
- },
247
- })
248
- ```
249
-
250
- #### 🌳 `Chronobranch` for undo/redo history
251
- - first, put a `Chronicle` into your state tree
252
- ```ts
253
- const trunk = new Trunk({
254
- count: 0,
255
- snacks: Trunk.chronicle({
256
- peanuts: 8,
257
- bag: ["popcorn", "butter"],
258
- }),
259
- })
260
- ```
261
- - *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*
262
- - second, make a `Chronobranch` which is like a branch, but is concerned with history
263
- ```ts
264
- const snacks = trunk.chronobranch(64, s => s.snacks)
265
- // \
266
- // how many past snapshots to store
267
- ```
268
- - mutations will advance history (undoable/redoable)
269
- ```ts
270
- await snacks.mutate(s => s.peanuts = 101)
271
-
272
- await snacks.undo()
273
- // back to 8 peanuts
274
-
275
- await snacks.redo()
276
- // forward to 101 peanuts
277
- ```
278
- - you can check how many undoable or redoable steps are available
279
- ```ts
280
- snacks.undoable // 2
281
- snacks.redoable // 1
282
- ```
283
- - chronobranch can have its own branches — all their mutations advance history
284
- - 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
+ ```
285
284
 
286
285
 
287
286
 
288
287
  <br/><br/>
289
288
 
290
- ## 🪄 strata tracker
289
+ <a id="tracker"></a>
290
+
291
+ ## 🍋 strata tracker
291
292
  > *reactivity integration hub*
292
293
 
293
294
  ```ts
@@ -318,8 +319,8 @@ note, the *items* that the tracker tracks can be any object, or symbol.. the tra
318
319
  ```
319
320
  - it's a good idea to debounce your rerender fn
320
321
  ```ts
321
- import {debounce} from "@e280/stz"
322
- const myDebouncedRerenderFn = debounce(0, myRerenderFn)
322
+ import {microbounce} from "@e280/stz"
323
+ const myDebouncedRerenderFn = microbounce(myRerenderFn)
323
324
  ```
324
325
  - `tracker.subscribe` to respond to changes
325
326
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e280/strata",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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,18 +23,19 @@
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",
29
30
  "_tscw": "tsc -w"
30
31
  },
31
32
  "dependencies": {
32
- "@e280/stz": "^0.2.11"
33
+ "@e280/stz": "^0.2.14"
33
34
  },
34
35
  "devDependencies": {
35
- "@e280/science": "^0.1.2",
36
- "@types/node": "^24.7.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
+