@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 +78 -21
- package/package.json +2 -2
- package/s/index.ts +4 -3
- package/s/parts/chronstrata.ts +84 -0
- package/s/parts/strata.ts +37 -14
- package/s/parts/substrata.ts +21 -18
- package/s/parts/types.ts +20 -0
- package/s/tests.test.ts +196 -8
- package/x/index.d.ts +4 -3
- package/x/index.js +4 -2
- package/x/index.js.map +1 -1
- package/x/parts/chronstrata.d.ts +23 -0
- package/x/parts/chronstrata.js +74 -0
- package/x/parts/chronstrata.js.map +1 -0
- package/x/parts/strata.d.ts +8 -4
- package/x/parts/strata.js +37 -11
- package/x/parts/strata.js.map +1 -1
- package/x/parts/substrata.d.ts +9 -7
- package/x/parts/substrata.js +17 -14
- package/x/parts/substrata.js.map +1 -1
- package/x/parts/types.d.ts +12 -0
- package/x/tests.test.js +173 -8
- package/x/tests.test.js.map +1 -1
package/README.md
CHANGED
|
@@ -1,72 +1,129 @@
|
|
|
1
1
|
|
|
2
|
+

|
|
3
|
+
|
|
2
4
|
# ⛏️ strata
|
|
3
5
|
|
|
4
|
-
**
|
|
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
|
-
##
|
|
16
|
+
## get in loser, we're managing state
|
|
11
17
|
|
|
12
|
-
###
|
|
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
|
-
|
|
25
|
+
snacks: {
|
|
20
26
|
peanuts: 8,
|
|
21
|
-
|
|
27
|
+
bag: ["popcorn", "butter"],
|
|
22
28
|
},
|
|
23
29
|
})
|
|
24
30
|
|
|
25
31
|
strata.state.count // 0
|
|
26
|
-
strata.state.
|
|
32
|
+
strata.state.snacks.peanuts // 8
|
|
27
33
|
```
|
|
28
34
|
|
|
29
|
-
###
|
|
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
|
|
40
|
+
- ✅ formal mutations are allowed
|
|
35
41
|
```ts
|
|
36
42
|
await strata.mutate(s => s.count++)
|
|
37
43
|
```
|
|
38
44
|
|
|
39
|
-
###
|
|
40
|
-
- a
|
|
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
|
|
48
|
+
const snacks = strata.substrata(s => s.snacks)
|
|
43
49
|
```
|
|
44
50
|
- run substrata mutations
|
|
45
51
|
```ts
|
|
46
|
-
await
|
|
52
|
+
await snacks.mutate(s => s.peanuts++)
|
|
47
53
|
```
|
|
48
|
-
- array mutations are
|
|
54
|
+
- array mutations are unironically based, actually
|
|
49
55
|
```ts
|
|
50
|
-
await
|
|
56
|
+
await snacks.mutate(s => s.bag.push("salt"))
|
|
51
57
|
```
|
|
58
|
+
- you can make a substrata of another substrata
|
|
52
59
|
|
|
53
|
-
###
|
|
60
|
+
### watch mutations
|
|
54
61
|
- you can listen to global mutations on the strata
|
|
55
62
|
```ts
|
|
56
|
-
strata.
|
|
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
|
-
|
|
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.
|
|
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-
|
|
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.
|
|
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
|
|
3
|
-
export
|
|
4
|
-
export
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
|
29
|
+
this.options = processOptions(options)
|
|
18
30
|
this.#mutable = state
|
|
19
|
-
this.#immutable = deep.freeze(this
|
|
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
|
|
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
|
|
37
|
-
|
|
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
|
|
package/s/parts/substrata.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
8
|
+
watch = sub<[state: S]>()
|
|
11
9
|
|
|
12
|
-
#options: Options
|
|
13
10
|
#immutable: S
|
|
14
|
-
#dispatchMutation = debounce(0, (state: S) => this.
|
|
11
|
+
#dispatchMutation = debounce(0, (state: S) => this.watch.pub(state))
|
|
15
12
|
|
|
16
13
|
constructor(
|
|
17
|
-
private
|
|
14
|
+
private parent: Stratum<ParentState>,
|
|
18
15
|
private selector: Selector<ParentState, S>,
|
|
19
|
-
options:
|
|
16
|
+
private options: Options,
|
|
20
17
|
) {
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
"
|
|
43
|
+
"watch is published": Science.test(async() => {
|
|
33
44
|
const strata = new Strata({count: 0})
|
|
34
45
|
let mutationCount = 0
|
|
35
|
-
strata.
|
|
46
|
+
strata.watch.sub(() => {mutationCount++})
|
|
36
47
|
await strata.mutate(state => state.count++)
|
|
37
48
|
expect(mutationCount).is(1)
|
|
38
49
|
}),
|
|
39
50
|
|
|
40
|
-
"
|
|
51
|
+
"watch is debounced": Science.test(async() => {
|
|
41
52
|
const strata = new Strata({count: 0})
|
|
42
53
|
let mutationCount = 0
|
|
43
|
-
strata.
|
|
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
|
-
"
|
|
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.
|
|
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
|
-
"
|
|
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.
|
|
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
|
|