@e280/strata 0.0.0-0 → 0.0.0-2

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 CHANGED
@@ -1,72 +1,129 @@
1
1
 
2
+ ![](https://i.imgur.com/h7FohWa.jpeg)
3
+
2
4
  # ⛏️ strata
3
5
 
4
- **my 10th state management library, probably**
6
+ **strata isn't just state management. *it's pure sex.***
5
7
  - 📦 `npm install @e280/strata`
6
8
  - single-source-of-truth state tree
7
9
  - immutable except for `mutate(fn)` calls
10
+ - no spooky-dookie proxy magic — just god's honest javascript
8
11
  - undo/redo history, cross-tab sync, localStorage persistence
12
+ - probably my 10th state management library, lol
13
+
14
+ <br/>
9
15
 
10
- ## good state management
16
+ ## get in loser, we're managing state
11
17
 
12
- ### establish a strata with some state
18
+ ### `Strata` is your app's state tree root
13
19
  - better stick to json-friendly serializable data
14
20
  ```ts
15
21
  import {Strata} from "@e280/strata"
16
22
 
17
23
  const strata = new Strata({
18
24
  count: 0,
19
- stuff: {
25
+ snacks: {
20
26
  peanuts: 8,
21
- items: ["hello", "world"],
27
+ bag: ["popcorn", "butter"],
22
28
  },
23
29
  })
24
30
 
25
31
  strata.state.count // 0
26
- strata.state.stuff.peanuts // 8
32
+ strata.state.snacks.peanuts // 8
27
33
  ```
28
34
 
29
- ### how mutations work
35
+ ### formal mutations to change state
30
36
  - ⛔ informal mutations are denied
31
37
  ```ts
32
38
  strata.state.count++ // error is thrown
33
39
  ```
34
- - ✅ formal mutation is allowed
40
+ - ✅ formal mutations are allowed
35
41
  ```ts
36
42
  await strata.mutate(s => s.count++)
37
43
  ```
38
44
 
39
- ### substrata and selectors
40
- - a substrata is a view into a subset of the state tree
45
+ ### `Substrata` is a view into a subtree
46
+ - it's a lens, make lots of them, pass 'em around your app
41
47
  ```ts
42
- const stuff = strata.substrata(s => s.stuff)
48
+ const snacks = strata.substrata(s => s.snacks)
43
49
  ```
44
50
  - run substrata mutations
45
51
  ```ts
46
- await stuff.mutate(s => s.peanuts++)
52
+ await snacks.mutate(s => s.peanuts++)
47
53
  ```
48
- - array mutations are cool, actually
54
+ - array mutations are unironically based, actually
49
55
  ```ts
50
- await stuff.mutate(s => s.items.push("lol"))
56
+ await snacks.mutate(s => s.bag.push("salt"))
51
57
  ```
58
+ - you can make a substrata of another substrata
52
59
 
53
- ### onMutation events
60
+ ### watch mutations
54
61
  - you can listen to global mutations on the strata
55
62
  ```ts
56
- strata.onMutation(s => console.log(s.count))
63
+ strata.watch(s => console.log(s.count))
57
64
  ```
58
-
59
65
  - substrata listeners don't care about outside changes
60
66
  ```ts
61
- stuff.onMutation(s => console.log(s.peanuts))
67
+ snacks.watch(s => console.log(s.peanuts))
62
68
  ```
63
-
64
- - onMutation returns a fn to stop listening
69
+ - watch returns a fn to stop listening
65
70
  ```ts
66
- const stop = strata.onMutation(s => console.log(s.count))
71
+ const stop = strata.watch(s => console.log(s.count))
67
72
  stop() // stop listening
68
73
  ```
69
74
 
75
+ <br/>
76
+
77
+ ## only high-class discerning aristocrats permitted beyond this point
78
+
79
+ ### `Chronstrata` for undo/redo history
80
+ - first, put a `Chronicle` into your state tree
81
+ ```ts
82
+ const strata = new Strata({
83
+ count: 0,
84
+ snacks: Strata.chronicle({
85
+ peanuts: 8,
86
+ bag: ["popcorn", "butter"],
87
+ }),
88
+ })
89
+ ```
90
+ - *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*
91
+ - second, make a `Chronstrata` which is like a substrata
92
+ ```ts
93
+ const snacks = strata.chronstrata(64, s => s.snacks)
94
+ // \
95
+ // how many past snapshots to store
96
+ ```
97
+ - mutations will advance history (undoable/redoable)
98
+ ```ts
99
+ await snacks.mutate(s => s.peanuts = 101)
100
+
101
+ await snacks.undo()
102
+ // back to 8 peanuts
103
+
104
+ await snacks.redo()
105
+ // forward to 101 peanuts
106
+ ```
107
+ - you can check how many undoable or redoable steps are available
108
+ ```ts
109
+ snacks.undoable // 0
110
+
111
+ await snacks.mutate(s => s.peanuts = 101)
112
+ await snacks.mutate(s => s.peanuts = 102)
113
+ await snacks.mutate(s => s.peanuts = 103)
114
+
115
+ snacks.undoable // 3
116
+
117
+ await snacks.undo()
118
+
119
+ snacks.undoable // 2
120
+ snacks.redoable // 1
121
+ ```
122
+ - chronstrata can have its own substrata — all their mutations advance history
123
+ - plz pinky-swear right now, that you won't create a chronstrata under a substrata under a chronstrata
124
+
125
+ <br/>
126
+
70
127
  ## a buildercore e280 project
71
128
  free and open source by https://e280.org/
72
129
  join us if you're cool and good at dev
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e280/strata",
3
- "version": "0.0.0-0",
3
+ "version": "0.0.0-2",
4
4
  "description": "state management",
5
5
  "license": "MIT",
6
6
  "author": "Chase Moskal <chasemoskal@gmail.com>",
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "devDependencies": {
26
26
  "@e280/science": "^0.0.5",
27
- "@types/node": "^24.0.3",
27
+ "@types/node": "^24.0.4",
28
28
  "npm-run-all": "^4.1.5",
29
29
  "typescript": "^5.8.3"
30
30
  },
package/s/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
 
2
- export {Strata} from "./parts/strata.js"
3
- export {Substrata} from "./parts/substrata.js"
4
- export {Options, Mutator, Selector} from "./parts/types.js"
2
+ export * from "./parts/chronstrata.js"
3
+ export * from "./parts/strata.js"
4
+ export * from "./parts/substrata.js"
5
+ export * from "./parts/types.js"
5
6
 
@@ -0,0 +1,84 @@
1
+
2
+ import {Substrata} from "./substrata.js"
3
+ import {Chronicle, Mutator, Options, Selector, Stratum, Substate} from "./types.js"
4
+
5
+ export class Chronstrata<ParentState extends Substate, S extends Substate> implements Stratum<S> {
6
+ #substrata: Substrata<ParentState, Chronicle<S>>
7
+
8
+ constructor(
9
+ public limit: number,
10
+ public parent: Stratum<ParentState>,
11
+ public selector: Selector<ParentState, Chronicle<S>>,
12
+ public options: Options,
13
+ ) {
14
+ this.#substrata = parent.substrata(selector)
15
+ }
16
+
17
+ get state() {
18
+ return this.#substrata.state.present
19
+ }
20
+
21
+ get undoable() {
22
+ return this.#substrata.state.past.length
23
+ }
24
+
25
+ get redoable() {
26
+ return this.#substrata.state.future.length
27
+ }
28
+
29
+ watch(fn: (state: S) => void) {
30
+ return this.#substrata.watch(chronicle => fn(chronicle.present))
31
+ }
32
+
33
+ /** progress forwards in history */
34
+ async mutate(mutator: Mutator<S>) {
35
+ const limit = Math.max(0, this.limit)
36
+ const snapshot = this.options.clone(this.#substrata.state.present)
37
+ await this.#substrata.mutate(chronicle => {
38
+ mutator(chronicle.present)
39
+ chronicle.past.push(snapshot)
40
+ chronicle.past = chronicle.past.slice(-limit)
41
+ chronicle.future = []
42
+ })
43
+ return this.state
44
+ }
45
+
46
+ /** step backwards into the past, by n steps */
47
+ async undo(n = 1) {
48
+ await this.#substrata.mutate(chronicle => {
49
+ const snapshots = chronicle.past.slice(-n)
50
+ if (snapshots.length >= n) {
51
+ const oldPresent = chronicle.present
52
+ chronicle.present = snapshots.shift()!
53
+ chronicle.past = chronicle.past.slice(0, -n)
54
+ chronicle.future.unshift(oldPresent, ...snapshots)
55
+ }
56
+ })
57
+ }
58
+
59
+ /** step forwards into the future, by n steps */
60
+ async redo(n = 1) {
61
+ await this.#substrata.mutate(chronicle => {
62
+ const snapshots = chronicle.future.slice(0, n)
63
+ if (snapshots.length >= n) {
64
+ const oldPresent = chronicle.present
65
+ chronicle.present = snapshots.shift()!
66
+ chronicle.past.push(oldPresent, ...snapshots)
67
+ chronicle.future = chronicle.future.slice(n)
68
+ }
69
+ })
70
+ }
71
+
72
+ /** wipe past and future snapshots */
73
+ async wipe() {
74
+ await this.#substrata.mutate(chronicle => {
75
+ chronicle.past = []
76
+ chronicle.future = []
77
+ })
78
+ }
79
+
80
+ substrata<Sub extends Substate>(selector: Selector<S, Sub>): Substrata<S, Sub> {
81
+ return new Substrata(this, selector, this.options)
82
+ }
83
+ }
84
+
package/s/parts/strata.ts CHANGED
@@ -2,39 +2,51 @@
2
2
  import {debounce, deep, sub} from "@e280/stz"
3
3
 
4
4
  import {Substrata} from "./substrata.js"
5
+ import {Chronstrata} from "./chronstrata.js"
5
6
  import {processOptions} from "./utils/process-options.js"
6
- import {Mutator, Options, Selector, State, Substate} from "./types.js"
7
+ import {Chronicle, Mutator, Options, Selector, State, Stratum, Substate} from "./types.js"
7
8
 
8
- export class Strata<S extends State> {
9
- onMutation = sub<[state: S]>()
9
+ export class Strata<S extends State> implements Stratum<S> {
10
+ static chronicle = <S extends Substate>(state: S): Chronicle<S> => ({
11
+ present: state,
12
+ past: [],
13
+ future: [],
14
+ })
15
+
16
+ options: Options
17
+ watch = sub<[state: S]>()
10
18
 
11
- #options: Options
12
19
  #mutable: S
13
20
  #immutable: S
14
- #dispatchMutation = debounce(0, (state: S) => this.onMutation.pub(state))
21
+ #mutationLock = 0
22
+ #dispatchMutation = debounce(0, async(state: S) => {
23
+ this.#mutationLock++
24
+ try { await this.watch.pub(state) }
25
+ finally { this.#mutationLock-- }
26
+ })
15
27
 
16
28
  constructor(state: S, options: Partial<Options> = {}) {
17
- this.#options = processOptions(options)
29
+ this.options = processOptions(options)
18
30
  this.#mutable = state
19
- this.#immutable = deep.freeze(this.#options.clone(state))
31
+ this.#immutable = deep.freeze(this.options.clone(state))
20
32
  }
21
33
 
22
34
  #updateState(state: S) {
23
35
  this.#mutable = state
24
- this.#immutable = deep.freeze(this.#options.clone(state))
36
+ this.#immutable = deep.freeze(this.options.clone(state))
25
37
  }
26
38
 
27
39
  get state() {
28
40
  return this.#immutable
29
41
  }
30
42
 
31
- substrata<Sub extends Substate>(selector: Selector<S, Sub>) {
32
- return new Substrata(this, selector, this.#options)
33
- }
34
-
35
43
  async mutate(mutator: Mutator<S>) {
36
- const oldState = this.#options.clone(this.#mutable)
37
- mutator(this.#mutable)
44
+ const oldState = this.options.clone(this.#mutable)
45
+ if (this.#mutationLock > 0)
46
+ throw new Error("nested mutations are forbidden")
47
+ this.#mutationLock++
48
+ try { mutator(this.#mutable) }
49
+ finally { this.#mutationLock-- }
38
50
  const newState = this.#mutable
39
51
  const isChanged = !deep.equal(newState, oldState)
40
52
  if (isChanged) {
@@ -44,5 +56,16 @@ export class Strata<S extends State> {
44
56
  }
45
57
  return this.#immutable
46
58
  }
59
+
60
+ substrata<Sub extends Substate>(selector: Selector<S, Sub>): Substrata<S, Sub> {
61
+ return new Substrata(this, selector, this.options)
62
+ }
63
+
64
+ chronstrata<Sub extends Substate>(
65
+ limit: number,
66
+ selector: Selector<S, Chronicle<Sub>>,
67
+ ) {
68
+ return new Chronstrata(limit, this, selector, this.options)
69
+ }
47
70
  }
48
71
 
@@ -1,29 +1,25 @@
1
1
 
2
2
  import {debounce, deep, sub} from "@e280/stz"
3
+ import {Chronstrata} from "./chronstrata.js"
4
+ import {Chronicle, Mutator, Options, Selector, Stratum, Substate} from "./types.js"
3
5
 
4
- import {Strata} from "./strata.js"
5
- import {processOptions} from "./utils/process-options.js"
6
- import {Mutator, Options, Selector, State, Substate} from "./types.js"
7
-
8
- export class Substrata<ParentState extends State, S extends Substate> {
6
+ export class Substrata<ParentState extends Substate, S extends Substate> implements Stratum<S> {
9
7
  dispose: () => void
10
- onMutation = sub<[state: S]>()
8
+ watch = sub<[state: S]>()
11
9
 
12
- #options: Options
13
10
  #immutable: S
14
- #dispatchMutation = debounce(0, (state: S) => this.onMutation.pub(state))
11
+ #dispatchMutation = debounce(0, (state: S) => this.watch.pub(state))
15
12
 
16
13
  constructor(
17
- private strata: Strata<ParentState>,
14
+ private parent: Stratum<ParentState>,
18
15
  private selector: Selector<ParentState, S>,
19
- options: Partial<Options> = {},
16
+ private options: Options,
20
17
  ) {
21
18
 
22
- this.#options = processOptions(options)
23
- const state = this.selector(this.strata.state)
24
- this.#immutable = deep.freeze(this.#options.clone(state))
19
+ const state = this.selector(this.parent.state)
20
+ this.#immutable = deep.freeze(this.options.clone(state))
25
21
 
26
- this.dispose = this.strata.onMutation(async parentState => {
22
+ this.dispose = this.parent.watch(async parentState => {
27
23
  const oldState = this.#immutable
28
24
  const newState = this.selector(parentState)
29
25
  const isChanged = !deep.equal(newState, oldState)
@@ -36,7 +32,7 @@ export class Substrata<ParentState extends State, S extends Substate> {
36
32
  }
37
33
 
38
34
  #updateState(state: S) {
39
- this.#immutable = deep.freeze(this.#options.clone(state))
35
+ this.#immutable = deep.freeze(this.options.clone(state))
40
36
  }
41
37
 
42
38
  get state(): S {
@@ -44,12 +40,19 @@ export class Substrata<ParentState extends State, S extends Substate> {
44
40
  }
45
41
 
46
42
  async mutate(mutator: Mutator<S>) {
47
- await this.strata.mutate(parentState => mutator(this.selector(parentState)))
43
+ await this.parent.mutate(parentState => mutator(this.selector(parentState)))
48
44
  return this.#immutable
49
45
  }
50
46
 
51
- substrata<Sub extends Substate>(selector: Selector<S, Sub>) {
52
- return this.strata.substrata(parentState => selector(this.selector(parentState)))
47
+ substrata<Sub extends Substate>(selector: Selector<S, Sub>): Substrata<S, Sub> {
48
+ return new Substrata(this, selector, this.options)
49
+ }
50
+
51
+ chronstrata<Sub extends Substate>(
52
+ limit: number,
53
+ selector: Selector<S, Chronicle<Sub>>,
54
+ ) {
55
+ return new Chronstrata(limit, this, selector, this.options)
53
56
  }
54
57
  }
55
58
 
package/s/parts/types.ts CHANGED
@@ -1,4 +1,6 @@
1
1
 
2
+ import {Substrata} from "./substrata.js"
3
+
2
4
  export type Options = {
3
5
  clone: <X>(x: X) => X
4
6
  }
@@ -9,3 +11,21 @@ export type Mutator<S> = (state: S) => void
9
11
  export type State = {}
10
12
  export type Substate = {} | null | undefined
11
13
 
14
+ export type Stratum<S extends Substate> = {
15
+ readonly state: S
16
+ watch(fn: (s: S) => void): () => void
17
+ mutate(mutator: Mutator<S>): Promise<S>
18
+ substrata<Sub extends Substate>(selector: Selector<S, Sub>): Substrata<S, Sub>
19
+ }
20
+
21
+ export type Chronicle<S extends Substate> = {
22
+ // [abc] d [efg]
23
+ // \ \ \
24
+ // \ \ future
25
+ // \ present
26
+ // past
27
+ past: S[]
28
+ present: S
29
+ future: S[]
30
+ }
31
+
package/s/tests.test.ts CHANGED
@@ -23,38 +23,64 @@ await Science.run({
23
23
  expect(strata.state.count).is(2)
24
24
  }),
25
25
 
26
+ "forbidden mutation nesting": Science.test(async() => {
27
+ const strata = new Strata({count: 0})
28
+ await expect(async() => {
29
+ let promise!: Promise<any>
30
+ await strata.mutate(() => {
31
+ promise = strata.mutate(() => {})
32
+ })
33
+ await promise
34
+ }).throwsAsync()
35
+ }),
36
+
26
37
  "state after mutation is frozen": Science.test(async () => {
27
38
  const strata = new Strata({x: 1})
28
39
  await strata.mutate(s => { s.x = 2 })
29
40
  expect(() => strata.state.x = 3).throws()
30
41
  }),
31
42
 
32
- "onMutation is published": Science.test(async() => {
43
+ "watch is published": Science.test(async() => {
33
44
  const strata = new Strata({count: 0})
34
45
  let mutationCount = 0
35
- strata.onMutation.sub(() => {mutationCount++})
46
+ strata.watch.sub(() => {mutationCount++})
36
47
  await strata.mutate(state => state.count++)
37
48
  expect(mutationCount).is(1)
38
49
  }),
39
50
 
40
- "onMutation is debounced": Science.test(async() => {
51
+ "watch is debounced": Science.test(async() => {
41
52
  const strata = new Strata({count: 0})
42
53
  let mutationCount = 0
43
- strata.onMutation.sub(() => {mutationCount++})
54
+ strata.watch.sub(() => {mutationCount++})
44
55
  const promise = strata.mutate(state => state.count++)
45
56
  expect(mutationCount).is(0)
46
57
  await promise
47
58
  expect(mutationCount).is(1)
48
59
  }),
49
60
 
50
- "onMutation is fired when array item is pushed": Science.test(async() => {
61
+ "watch is fired when array item is pushed": Science.test(async() => {
51
62
  const strata = new Strata({items: ["hello", "world"]})
52
63
  let mutationCount = 0
53
- strata.onMutation.sub(() => {mutationCount++})
64
+ strata.watch.sub(() => {mutationCount++})
54
65
  await strata.mutate(state => state.items.push("lol"))
55
66
  expect(mutationCount).is(1)
56
67
  expect(strata.state.items.length).is(3)
57
68
  }),
69
+
70
+ "prevent mutation loops": Science.test(async() => {
71
+ const strata = new Strata({count: 0})
72
+ let mutationCount = 0
73
+ strata.watch.sub(async() => {
74
+ mutationCount++
75
+ if (mutationCount > 100)
76
+ return
77
+ await strata.mutate(s => s.count++)
78
+ })
79
+ await expect(async() => {
80
+ await strata.mutate(state => state.count++)
81
+ }).throwsAsync()
82
+ expect(mutationCount).is(1)
83
+ }),
58
84
  }),
59
85
 
60
86
  "substrata": Science.suite({
@@ -105,15 +131,177 @@ await Science.run({
105
131
  expect(b.state.c).is(103)
106
132
  }),
107
133
 
108
- "onMutation ignores outside mutations": Science.test(async() => {
134
+ "watch ignores outside mutations": Science.test(async() => {
109
135
  const strata = new Strata({a: {x: 0}, b: {x: 0}})
110
136
  const a = strata.substrata(s => s.a)
111
137
  const b = strata.substrata(s => s.b)
112
138
  let counted = 0
113
- b.onMutation.sub(() => {counted++})
139
+ b.watch.sub(() => {counted++})
114
140
  await a.mutate(a => a.x = 1)
115
141
  expect(counted).is(0)
116
142
  }),
143
+
144
+ "forbid submutation in mutation": Science.test(async() => {
145
+ const strata = new Strata({a: {b: 0}})
146
+ const a = strata.substrata(s => s.a)
147
+ await expect(async() => {
148
+ let promise!: Promise<any>
149
+ await strata.mutate(() => {
150
+ promise = a.mutate(() => {})
151
+ })
152
+ await promise
153
+ }).throwsAsync()
154
+ }),
155
+
156
+ "forbid mutation in submutation": Science.test(async() => {
157
+ const strata = new Strata({a: {b: 0}})
158
+ const a = strata.substrata(s => s.a)
159
+ await expect(async() => {
160
+ let promise!: Promise<any>
161
+ await a.mutate(() => {
162
+ promise = strata.mutate(() => {})
163
+ })
164
+ await promise
165
+ }).throwsAsync()
166
+ }),
117
167
  }),
168
+
169
+ "chronstrata": (() => {
170
+ const setup = () => {
171
+ const strata = new Strata({
172
+ chron: Strata.chronicle({count: 0}),
173
+ })
174
+ const chron = strata.chronstrata(64, s => s.chron)
175
+ return {strata, chron}
176
+ }
177
+
178
+ return Science.suite({
179
+ "get state": Science.test(async() => {
180
+ const {chron} = setup()
181
+ expect(chron.state.count).is(0)
182
+ }),
183
+
184
+ "mutate": Science.test(async() => {
185
+ const {chron} = setup()
186
+ expect(chron.state.count).is(0)
187
+ await chron.mutate(s => s.count++)
188
+ expect(chron.state.count).is(1)
189
+ await chron.mutate(s => s.count++)
190
+ expect(chron.state.count).is(2)
191
+ }),
192
+
193
+ "undoable/redoable": Science.test(async() => {
194
+ const {chron} = setup()
195
+ expect(chron.undoable).is(0)
196
+ expect(chron.redoable).is(0)
197
+ expect(chron.state.count).is(0)
198
+ await chron.mutate(s => s.count++)
199
+ expect(chron.undoable).is(1)
200
+ await chron.mutate(s => s.count++)
201
+ expect(chron.undoable).is(2)
202
+ await chron.undo()
203
+ expect(chron.undoable).is(1)
204
+ expect(chron.redoable).is(1)
205
+ await chron.undo()
206
+ expect(chron.undoable).is(0)
207
+ expect(chron.redoable).is(2)
208
+ await chron.redo()
209
+ expect(chron.undoable).is(1)
210
+ expect(chron.redoable).is(1)
211
+ }),
212
+
213
+ "undo": Science.test(async() => {
214
+ const {chron} = setup()
215
+ await chron.mutate(s => s.count++)
216
+ await chron.undo()
217
+ expect(chron.state.count).is(0)
218
+ }),
219
+
220
+ "redo": Science.test(async() => {
221
+ const {chron} = setup()
222
+ await chron.mutate(s => s.count++)
223
+ await chron.undo()
224
+ expect(chron.state.count).is(0)
225
+ await chron.redo()
226
+ expect(chron.state.count).is(1)
227
+ }),
228
+
229
+ "undo/redo well ordered": Science.test(async() => {
230
+ const {chron} = setup()
231
+ await chron.mutate(s => s.count++)
232
+ await chron.mutate(s => s.count++)
233
+ await chron.mutate(s => s.count++)
234
+ expect(chron.state.count).is(3)
235
+
236
+ await chron.undo()
237
+ expect(chron.state.count).is(2)
238
+
239
+ await chron.undo()
240
+ expect(chron.state.count).is(1)
241
+
242
+ await chron.redo()
243
+ expect(chron.state.count).is(2)
244
+
245
+ await chron.redo()
246
+ expect(chron.state.count).is(3)
247
+
248
+ await chron.undo()
249
+ expect(chron.state.count).is(2)
250
+
251
+ await chron.undo()
252
+ expect(chron.state.count).is(1)
253
+
254
+ await chron.undo()
255
+ expect(chron.state.count).is(0)
256
+ }),
257
+
258
+ "undo nothing does nothing": Science.test(async() => {
259
+ const {chron} = setup()
260
+ await chron.undo()
261
+ expect(chron.state.count).is(0)
262
+ }),
263
+
264
+ "redo nothing does nothing": Science.test(async() => {
265
+ const {chron} = setup()
266
+ await chron.redo()
267
+ expect(chron.state.count).is(0)
268
+ }),
269
+
270
+ "undo 2x": Science.test(async() => {
271
+ const {chron} = setup()
272
+ await chron.mutate(s => s.count++)
273
+ await chron.mutate(s => s.count++)
274
+ expect(chron.state.count).is(2)
275
+ await chron.undo(2)
276
+ expect(chron.state.count).is(0)
277
+ }),
278
+
279
+ "redo 2x": Science.test(async() => {
280
+ const {chron} = setup()
281
+ await chron.mutate(s => s.count++)
282
+ await chron.mutate(s => s.count++)
283
+ expect(chron.state.count).is(2)
284
+ await chron.undo(2)
285
+ expect(chron.state.count).is(0)
286
+ await chron.redo(2)
287
+ expect(chron.state.count).is(2)
288
+ }),
289
+
290
+ "substrata mutations are tracked": Science.test(async() => {
291
+ const strata = new Strata({
292
+ chron: Strata.chronicle({
293
+ group: {count: 0},
294
+ }),
295
+ })
296
+ const chron = strata.chronstrata(64, s => s.chron)
297
+ const group = chron.substrata(s => s.group)
298
+ expect(group.state.count).is(0)
299
+ await group.mutate(g => g.count = 101)
300
+ expect(group.state.count).is(101)
301
+ await chron.undo()
302
+ expect(group.state.count).is(0)
303
+ }),
304
+ })
305
+ })(),
118
306
  })
119
307