@e280/strata 0.0.0-0 → 0.0.0-10
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 +207 -31
- package/package.json +14 -8
- package/s/index.ts +3 -3
- package/s/signals/index.ts +6 -0
- package/s/signals/parts/derive.ts +29 -0
- package/s/signals/parts/effect.ts +23 -0
- package/s/signals/parts/lazy.ts +27 -0
- package/s/signals/parts/signal.ts +44 -0
- package/s/signals/parts/types.ts +11 -0
- package/s/signals/parts/units.ts +152 -0
- package/s/signals/signals.test.ts +285 -0
- package/s/tests.test.ts +46 -99
- package/s/tracker/index.ts +3 -0
- package/s/tracker/tracker.test.ts +40 -0
- package/s/tracker/tracker.ts +73 -0
- package/s/tree/index.ts +7 -0
- package/s/tree/parts/branch.ts +41 -0
- package/s/tree/parts/chronobranch.ts +84 -0
- package/s/tree/parts/persistence.ts +31 -0
- package/s/tree/parts/trunk.ts +72 -0
- package/s/tree/parts/types.ts +65 -0
- package/s/{parts → tree/parts}/utils/process-options.ts +1 -1
- package/s/tree/parts/utils/setup.ts +40 -0
- package/s/tree/tree.test.ts +316 -0
- package/x/index.d.ts +3 -3
- package/x/index.js +3 -2
- package/x/index.js.map +1 -1
- package/x/signals/index.d.ts +4 -0
- package/x/signals/index.js +5 -0
- package/x/signals/index.js.map +1 -0
- package/x/signals/parts/derive.d.ts +12 -0
- package/x/signals/parts/derive.js +12 -0
- package/x/signals/parts/derive.js.map +1 -0
- package/x/signals/parts/effect.d.ts +5 -0
- package/x/signals/parts/effect.js +17 -0
- package/x/signals/parts/effect.js.map +1 -0
- package/x/signals/parts/lazy.d.ts +10 -0
- package/x/signals/parts/lazy.js +12 -0
- package/x/signals/parts/lazy.js.map +1 -0
- package/x/signals/parts/signal.d.ts +21 -0
- package/x/signals/parts/signal.js +18 -0
- package/x/signals/parts/signal.js.map +1 -0
- package/x/signals/parts/types.d.ts +7 -0
- package/x/signals/parts/types.js.map +1 -0
- package/x/signals/parts/units.d.ts +43 -0
- package/x/signals/parts/units.js +133 -0
- package/x/signals/parts/units.js.map +1 -0
- package/x/signals/signals.test.d.ts +24 -0
- package/x/signals/signals.test.js +230 -0
- package/x/signals/signals.test.js.map +1 -0
- package/x/tests.test.js +46 -100
- package/x/tests.test.js.map +1 -1
- package/x/tracker/index.d.ts +1 -0
- package/x/tracker/index.js +2 -0
- package/x/tracker/index.js.map +1 -0
- package/x/tracker/tracker.d.ts +29 -0
- package/x/tracker/tracker.js +62 -0
- package/x/tracker/tracker.js.map +1 -0
- package/x/tracker/tracker.test.d.ts +6 -0
- package/x/tracker/tracker.test.js +32 -0
- package/x/tracker/tracker.test.js.map +1 -0
- package/x/tree/index.d.ts +5 -0
- package/x/tree/index.js +6 -0
- package/x/tree/index.js.map +1 -0
- package/x/tree/parts/branch.d.ts +12 -0
- package/x/tree/parts/branch.js +31 -0
- package/x/tree/parts/branch.js.map +1 -0
- package/x/tree/parts/chronobranch.d.ts +23 -0
- package/x/tree/parts/chronobranch.js +74 -0
- package/x/tree/parts/chronobranch.js.map +1 -0
- package/x/tree/parts/persistence.d.ts +2 -0
- package/x/tree/parts/persistence.js +23 -0
- package/x/tree/parts/persistence.js.map +1 -0
- package/x/tree/parts/trunk.d.ts +17 -0
- package/x/tree/parts/trunk.js +56 -0
- package/x/tree/parts/trunk.js.map +1 -0
- package/x/tree/parts/types.d.ts +43 -0
- package/x/tree/parts/types.js +2 -0
- package/x/{parts → tree/parts}/types.js.map +1 -1
- package/x/tree/parts/utils/process-options.js +4 -0
- package/x/tree/parts/utils/process-options.js.map +1 -0
- package/x/tree/parts/utils/setup.d.ts +8 -0
- package/x/tree/parts/utils/setup.js +24 -0
- package/x/tree/parts/utils/setup.js.map +1 -0
- package/x/tree/tree.test.d.ts +37 -0
- package/x/tree/tree.test.js +279 -0
- package/x/tree/tree.test.js.map +1 -0
- package/s/parts/strata.ts +0 -48
- package/s/parts/substrata.ts +0 -55
- package/s/parts/types.ts +0 -11
- package/x/parts/strata.d.ts +0 -10
- package/x/parts/strata.js +0 -38
- package/x/parts/strata.js.map +0 -1
- package/x/parts/substrata.d.ts +0 -13
- package/x/parts/substrata.js +0 -42
- package/x/parts/substrata.js.map +0 -1
- package/x/parts/types.d.ts +0 -7
- package/x/parts/utils/process-options.js +0 -4
- package/x/parts/utils/process-options.js.map +0 -1
- /package/x/{parts → signals/parts}/types.js +0 -0
- /package/x/{parts → tree/parts}/utils/process-options.d.ts +0 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
|
|
2
|
+
import {Persistence} from "./types.js"
|
|
3
|
+
|
|
4
|
+
export const localPersistence = <X>(
|
|
5
|
+
key: string,
|
|
6
|
+
storage: Storage = window.localStorage,
|
|
7
|
+
): Persistence<X> => ({
|
|
8
|
+
|
|
9
|
+
store: {
|
|
10
|
+
async get() {
|
|
11
|
+
const json = storage.getItem(key)
|
|
12
|
+
return json
|
|
13
|
+
? JSON.parse(json)
|
|
14
|
+
: undefined
|
|
15
|
+
},
|
|
16
|
+
async set(state) {
|
|
17
|
+
const json = JSON.stringify(state)
|
|
18
|
+
storage.setItem(key, json)
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
onChange: (fn: () => void) => {
|
|
23
|
+
const listener = (event: StorageEvent) => {
|
|
24
|
+
if (event.storageArea === storage && event.key === key)
|
|
25
|
+
fn()
|
|
26
|
+
}
|
|
27
|
+
window.addEventListener("storage", listener)
|
|
28
|
+
return () => window.removeEventListener("storage", listener)
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
|
|
2
|
+
import {deep} from "@e280/stz"
|
|
3
|
+
import {Branch} from "./branch.js"
|
|
4
|
+
import {trunkSetup} from "./utils/setup.js"
|
|
5
|
+
import {Chronobranch} from "./chronobranch.js"
|
|
6
|
+
import {processOptions} from "./utils/process-options.js"
|
|
7
|
+
import {DerivedSignal} from "../../signals/parts/derive.js"
|
|
8
|
+
import {signal, Signal} from "../../signals/parts/signal.js"
|
|
9
|
+
import {Branchstate, Chronicle, Immutable, Mutator, Options, Selector, Tree, Trunkstate} from "./types.js"
|
|
10
|
+
|
|
11
|
+
export class Trunk<S extends Trunkstate> implements Tree<S> {
|
|
12
|
+
static setup = trunkSetup
|
|
13
|
+
static chronicle = <S extends Branchstate>(state: S): Chronicle<S> => ({
|
|
14
|
+
present: state,
|
|
15
|
+
past: [],
|
|
16
|
+
future: [],
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
options: Options
|
|
20
|
+
|
|
21
|
+
#immutable: DerivedSignal<Immutable<S>>
|
|
22
|
+
#mutable: Signal<S>
|
|
23
|
+
#mutationLock = 0
|
|
24
|
+
|
|
25
|
+
constructor(state: S, options: Partial<Options> = {}) {
|
|
26
|
+
this.options = processOptions(options)
|
|
27
|
+
this.#mutable = signal(state)
|
|
28
|
+
this.#immutable = signal.derive(() =>
|
|
29
|
+
deep.freeze(this.options.clone(this.#mutable.get())) as Immutable<S>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get state() {
|
|
34
|
+
return this.#immutable.get()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get on() {
|
|
38
|
+
return this.#immutable.on
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async mutate(mutator: Mutator<S>) {
|
|
42
|
+
const oldState = this.options.clone(this.#mutable.get())
|
|
43
|
+
if (this.#mutationLock > 0)
|
|
44
|
+
throw new Error("nested mutations are forbidden")
|
|
45
|
+
try {
|
|
46
|
+
this.#mutationLock++
|
|
47
|
+
mutator(this.#mutable())
|
|
48
|
+
const newState = this.#mutable.get()
|
|
49
|
+
const isChanged = !deep.equal(newState, oldState)
|
|
50
|
+
if (isChanged)
|
|
51
|
+
await this.overwrite(newState)
|
|
52
|
+
}
|
|
53
|
+
finally { this.#mutationLock-- }
|
|
54
|
+
return this.#immutable.get()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async overwrite(state: S) {
|
|
58
|
+
await this.#mutable.publish(state)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
branch<Sub extends Branchstate>(selector: Selector<Sub, S>): Branch<Sub, S> {
|
|
62
|
+
return new Branch(this, selector, this.options)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
chronobranch<Sub extends Branchstate>(
|
|
66
|
+
limit: number,
|
|
67
|
+
selector: Selector<Chronicle<Sub>, S>,
|
|
68
|
+
) {
|
|
69
|
+
return new Chronobranch(limit, this, selector, this.options)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
|
|
2
|
+
import {Branch} from "./branch.js"
|
|
3
|
+
|
|
4
|
+
export type Options = {
|
|
5
|
+
clone: <X>(x: X) => X
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type Selector<Sub, S> = (state: S) => Sub
|
|
9
|
+
export type Mutator<S> = (state: S) => void
|
|
10
|
+
|
|
11
|
+
export type Trunkstate = {}
|
|
12
|
+
export type Branchstate = {} | null | undefined
|
|
13
|
+
|
|
14
|
+
export type Versioned<S extends Trunkstate> = {
|
|
15
|
+
state: S
|
|
16
|
+
version: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type Immutable<T> =
|
|
20
|
+
T extends (...args: any[]) => any ? T :
|
|
21
|
+
T extends readonly any[] ? ReadonlyArray<Immutable<T[number]>> :
|
|
22
|
+
T extends object ? { readonly [K in keyof T]: Immutable<T[K]> } :
|
|
23
|
+
T
|
|
24
|
+
|
|
25
|
+
export type Mutable<T> =
|
|
26
|
+
T extends (...args: any[]) => any ? T :
|
|
27
|
+
T extends ReadonlyArray<infer U> ? Mutable<U>[] :
|
|
28
|
+
T extends object ? { -readonly [K in keyof T]: Mutable<T[K]> } :
|
|
29
|
+
T
|
|
30
|
+
|
|
31
|
+
export type Tree<S extends Branchstate> = {
|
|
32
|
+
get state(): Immutable<S>
|
|
33
|
+
on(fn: (state: Immutable<S>) => void): () => void
|
|
34
|
+
mutate(mutator: Mutator<S>): Promise<Immutable<S>>
|
|
35
|
+
branch<Sub extends Branchstate>(selector: Selector<Sub, S>): Branch<Sub, S>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type SetupOptions<S extends Trunkstate> = {
|
|
39
|
+
version: number
|
|
40
|
+
initialState: S
|
|
41
|
+
saveDebounceTime?: number
|
|
42
|
+
persistence?: Persistence<Versioned<S>>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type Chronicle<S extends Branchstate> = {
|
|
46
|
+
// [abc] d [efg]
|
|
47
|
+
// \ \ \
|
|
48
|
+
// \ \ future
|
|
49
|
+
// \ present
|
|
50
|
+
// past
|
|
51
|
+
past: S[]
|
|
52
|
+
present: S
|
|
53
|
+
future: S[]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type EzStore<X> = {
|
|
57
|
+
get(): Promise<X | undefined>
|
|
58
|
+
set(state: X | undefined): Promise<void>
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type Persistence<X> = {
|
|
62
|
+
store: EzStore<X>
|
|
63
|
+
onChange: (fn: () => void) => (() => void)
|
|
64
|
+
}
|
|
65
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
|
|
2
|
+
import {debounce} from "@e280/stz"
|
|
3
|
+
|
|
4
|
+
import {Trunk} from "../trunk.js"
|
|
5
|
+
import {localPersistence} from "../persistence.js"
|
|
6
|
+
import {SetupOptions, Trunkstate} from "../types.js"
|
|
7
|
+
|
|
8
|
+
export async function trunkSetup<S extends Trunkstate>(options: SetupOptions<S>) {
|
|
9
|
+
const {
|
|
10
|
+
version,
|
|
11
|
+
initialState,
|
|
12
|
+
saveDebounceTime = 500,
|
|
13
|
+
persistence = localPersistence("strataTree"),
|
|
14
|
+
} = options
|
|
15
|
+
|
|
16
|
+
const trunk = new Trunk<S>(initialState)
|
|
17
|
+
|
|
18
|
+
async function load() {
|
|
19
|
+
const pickle = await persistence.store.get()
|
|
20
|
+
if (pickle && pickle.version === version)
|
|
21
|
+
await trunk.overwrite(pickle.state)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const save = debounce(saveDebounceTime, async() => persistence.store.set({
|
|
25
|
+
version,
|
|
26
|
+
state: trunk.state as any,
|
|
27
|
+
}))
|
|
28
|
+
|
|
29
|
+
// persistence: initial load from store
|
|
30
|
+
await load()
|
|
31
|
+
|
|
32
|
+
// persistence: save to store
|
|
33
|
+
trunk.on(save)
|
|
34
|
+
|
|
35
|
+
// cross-tab sync
|
|
36
|
+
const dispose = persistence.onChange(load)
|
|
37
|
+
|
|
38
|
+
return {trunk, load, save, dispose}
|
|
39
|
+
}
|
|
40
|
+
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
|
|
2
|
+
import {nap} from "@e280/stz"
|
|
3
|
+
import {Science, expect} from "@e280/science"
|
|
4
|
+
|
|
5
|
+
import {Trunk} from "./parts/trunk.js"
|
|
6
|
+
import {effect} from "../signals/parts/effect.js"
|
|
7
|
+
|
|
8
|
+
export default Science.suite({
|
|
9
|
+
"trunk": Science.suite({
|
|
10
|
+
"get state": Science.test(async() => {
|
|
11
|
+
const trunk = new Trunk({count: 0})
|
|
12
|
+
expect(trunk.state.count).is(0)
|
|
13
|
+
}),
|
|
14
|
+
|
|
15
|
+
"state is immutable": Science.test(async() => {
|
|
16
|
+
const trunk = new Trunk({count: 0})
|
|
17
|
+
expect(() => (trunk.state as any).count++).throws()
|
|
18
|
+
}),
|
|
19
|
+
|
|
20
|
+
"run a proper mutation": Science.test(async() => {
|
|
21
|
+
const trunk = new Trunk({count: 0})
|
|
22
|
+
expect(trunk.state.count).is(0)
|
|
23
|
+
await trunk.mutate(state => state.count++)
|
|
24
|
+
expect(trunk.state.count).is(1)
|
|
25
|
+
await trunk.mutate(state => state.count++)
|
|
26
|
+
expect(trunk.state.count).is(2)
|
|
27
|
+
}),
|
|
28
|
+
|
|
29
|
+
"forbidden mutation nesting": Science.test(async() => {
|
|
30
|
+
const trunk = new Trunk({count: 0})
|
|
31
|
+
await expect(async() => {
|
|
32
|
+
let promise!: Promise<any>
|
|
33
|
+
await trunk.mutate(() => {
|
|
34
|
+
promise = trunk.mutate(() => {})
|
|
35
|
+
})
|
|
36
|
+
await promise
|
|
37
|
+
}).throwsAsync()
|
|
38
|
+
}),
|
|
39
|
+
|
|
40
|
+
"state after mutation is frozen": Science.test(async () => {
|
|
41
|
+
const trunk = new Trunk({x: 1})
|
|
42
|
+
await trunk.mutate(s => { s.x = 2 })
|
|
43
|
+
expect(() => (trunk.state as any).x = 3).throws()
|
|
44
|
+
}),
|
|
45
|
+
|
|
46
|
+
"effect reacts to trunk mutation": Science.test(async() => {
|
|
47
|
+
const trunk = new Trunk({count: 0})
|
|
48
|
+
await nap(10)
|
|
49
|
+
let mutationCount = 0
|
|
50
|
+
effect(() => {
|
|
51
|
+
void trunk.state.count
|
|
52
|
+
mutationCount++
|
|
53
|
+
})
|
|
54
|
+
expect(mutationCount).is(1)
|
|
55
|
+
await trunk.mutate(state => state.count++)
|
|
56
|
+
expect(mutationCount).is(2)
|
|
57
|
+
}),
|
|
58
|
+
|
|
59
|
+
"signal.on is debounced": Science.test(async() => {
|
|
60
|
+
const trunk = new Trunk({count: 0})
|
|
61
|
+
let mutationCount = 0
|
|
62
|
+
trunk.on.sub(() => {mutationCount++})
|
|
63
|
+
const promise = trunk.mutate(state => state.count++)
|
|
64
|
+
expect(mutationCount).is(0)
|
|
65
|
+
await promise
|
|
66
|
+
expect(mutationCount).is(1)
|
|
67
|
+
}),
|
|
68
|
+
|
|
69
|
+
"listeners are fired when array item is pushed": Science.test(async() => {
|
|
70
|
+
const trunk = new Trunk({items: ["hello", "world"]})
|
|
71
|
+
let mutationCount = 0
|
|
72
|
+
trunk.on.sub(() => {mutationCount++})
|
|
73
|
+
await trunk.mutate(state => state.items.push("lol"))
|
|
74
|
+
expect(mutationCount).is(1)
|
|
75
|
+
expect(trunk.state.items.length).is(3)
|
|
76
|
+
}),
|
|
77
|
+
|
|
78
|
+
"prevent mutation loops": Science.test(async() => {
|
|
79
|
+
const trunk = new Trunk({count: 0})
|
|
80
|
+
let mutationCount = 0
|
|
81
|
+
trunk.on.sub(async() => {
|
|
82
|
+
mutationCount++
|
|
83
|
+
if (mutationCount > 100)
|
|
84
|
+
return
|
|
85
|
+
await trunk.mutate(s => s.count++)
|
|
86
|
+
})
|
|
87
|
+
await expect(async() => {
|
|
88
|
+
await trunk.mutate(state => state.count++)
|
|
89
|
+
}).throwsAsync()
|
|
90
|
+
expect(mutationCount).is(1)
|
|
91
|
+
}),
|
|
92
|
+
}),
|
|
93
|
+
|
|
94
|
+
"branch": Science.suite({
|
|
95
|
+
"get state": Science.test(async() => {
|
|
96
|
+
const trunk = new Trunk({count: 0, sub: {rofls: 0}})
|
|
97
|
+
const branch = trunk.branch(s => s.sub)
|
|
98
|
+
expect(branch.state.rofls).is(0)
|
|
99
|
+
}),
|
|
100
|
+
|
|
101
|
+
"nullable selector": Science.test(async () => {
|
|
102
|
+
const trunk = new Trunk({
|
|
103
|
+
a: {b: 0} as (null | {b: number}),
|
|
104
|
+
})
|
|
105
|
+
const a = trunk.branch(s => s.a)
|
|
106
|
+
expect(trunk.state.a?.b).is(0)
|
|
107
|
+
expect(a.state?.b).is(0)
|
|
108
|
+
await a.mutate(a => { a!.b = 1 })
|
|
109
|
+
expect(trunk.state.a?.b).is(1)
|
|
110
|
+
expect(a.state?.b).is(1)
|
|
111
|
+
await trunk.mutate(s => s.a = null)
|
|
112
|
+
expect(trunk.state.a?.b).is(undefined)
|
|
113
|
+
expect(a.state?.b).is(undefined)
|
|
114
|
+
}),
|
|
115
|
+
|
|
116
|
+
"composition": Science.test(async () => {
|
|
117
|
+
const trunk = new Trunk({a: {b: {c: 0}}})
|
|
118
|
+
const a = trunk.branch(s => s.a)
|
|
119
|
+
const b = a.branch(s => s.b)
|
|
120
|
+
expect(trunk.state.a.b.c).is(0)
|
|
121
|
+
expect(b.state.c).is(0)
|
|
122
|
+
}),
|
|
123
|
+
|
|
124
|
+
"deep mutations": Science.test(async () => {
|
|
125
|
+
const trunk = new Trunk({a: {b: {c: 0}}})
|
|
126
|
+
const a = trunk.branch(s => s.a)
|
|
127
|
+
const b = a.branch(s => s.b)
|
|
128
|
+
await b.mutate(b => { b.c = 101 })
|
|
129
|
+
expect(trunk.state.a.b.c).is(101)
|
|
130
|
+
expect(a.state.b.c).is(101)
|
|
131
|
+
expect(b.state.c).is(101)
|
|
132
|
+
await a.mutate(a => { a.b = {c: 102} })
|
|
133
|
+
expect(trunk.state.a.b.c).is(102)
|
|
134
|
+
expect(a.state.b.c).is(102)
|
|
135
|
+
expect(b.state.c).is(102)
|
|
136
|
+
await trunk.mutate(s => { s.a = {b: {c: 103}} })
|
|
137
|
+
expect(trunk.state.a.b.c).is(103)
|
|
138
|
+
expect(a.state.b.c).is(103)
|
|
139
|
+
expect(b.state.c).is(103)
|
|
140
|
+
}),
|
|
141
|
+
|
|
142
|
+
"signal.on ignores outside mutations": Science.test(async() => {
|
|
143
|
+
const trunk = new Trunk({a: {x: 0}, b: {x: 0}})
|
|
144
|
+
const a = trunk.branch(s => s.a)
|
|
145
|
+
const b = trunk.branch(s => s.b)
|
|
146
|
+
let counted = 0
|
|
147
|
+
b.on.sub(() => {counted++})
|
|
148
|
+
expect(counted).is(0)
|
|
149
|
+
await a.mutate(a => a.x = 1)
|
|
150
|
+
expect(counted).is(0)
|
|
151
|
+
}),
|
|
152
|
+
|
|
153
|
+
"forbid submutation in mutation": Science.test(async() => {
|
|
154
|
+
const trunk = new Trunk({a: {b: 0}})
|
|
155
|
+
const a = trunk.branch(s => s.a)
|
|
156
|
+
await expect(async() => {
|
|
157
|
+
let promise!: Promise<any>
|
|
158
|
+
await trunk.mutate(() => {
|
|
159
|
+
promise = a.mutate(() => {})
|
|
160
|
+
})
|
|
161
|
+
await promise
|
|
162
|
+
}).throwsAsync()
|
|
163
|
+
}),
|
|
164
|
+
|
|
165
|
+
"forbid mutation in submutation": Science.test(async() => {
|
|
166
|
+
const trunk = new Trunk({a: {b: 0}})
|
|
167
|
+
const a = trunk.branch(s => s.a)
|
|
168
|
+
await expect(async() => {
|
|
169
|
+
let promise!: Promise<any>
|
|
170
|
+
await a.mutate(() => {
|
|
171
|
+
promise = trunk.mutate(() => {})
|
|
172
|
+
})
|
|
173
|
+
await promise
|
|
174
|
+
}).throwsAsync()
|
|
175
|
+
}),
|
|
176
|
+
}),
|
|
177
|
+
|
|
178
|
+
"chronobranch": (() => {
|
|
179
|
+
const setup = () => {
|
|
180
|
+
const trunk = new Trunk({
|
|
181
|
+
chron: Trunk.chronicle({count: 0}),
|
|
182
|
+
})
|
|
183
|
+
const chron = trunk.chronobranch(64, s => s.chron)
|
|
184
|
+
return {trunk, chron}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return Science.suite({
|
|
188
|
+
"get state": Science.test(async() => {
|
|
189
|
+
const {chron} = setup()
|
|
190
|
+
expect(chron.state.count).is(0)
|
|
191
|
+
}),
|
|
192
|
+
|
|
193
|
+
"mutate": Science.test(async() => {
|
|
194
|
+
const {chron} = setup()
|
|
195
|
+
expect(chron.state.count).is(0)
|
|
196
|
+
await chron.mutate(s => s.count++)
|
|
197
|
+
expect(chron.state.count).is(1)
|
|
198
|
+
await chron.mutate(s => s.count++)
|
|
199
|
+
expect(chron.state.count).is(2)
|
|
200
|
+
}),
|
|
201
|
+
|
|
202
|
+
"undoable/redoable": Science.test(async() => {
|
|
203
|
+
const {chron} = setup()
|
|
204
|
+
expect(chron.undoable).is(0)
|
|
205
|
+
expect(chron.redoable).is(0)
|
|
206
|
+
expect(chron.state.count).is(0)
|
|
207
|
+
await chron.mutate(s => s.count++)
|
|
208
|
+
expect(chron.undoable).is(1)
|
|
209
|
+
await chron.mutate(s => s.count++)
|
|
210
|
+
expect(chron.undoable).is(2)
|
|
211
|
+
await chron.undo()
|
|
212
|
+
expect(chron.undoable).is(1)
|
|
213
|
+
expect(chron.redoable).is(1)
|
|
214
|
+
await chron.undo()
|
|
215
|
+
expect(chron.undoable).is(0)
|
|
216
|
+
expect(chron.redoable).is(2)
|
|
217
|
+
await chron.redo()
|
|
218
|
+
expect(chron.undoable).is(1)
|
|
219
|
+
expect(chron.redoable).is(1)
|
|
220
|
+
}),
|
|
221
|
+
|
|
222
|
+
"undo": Science.test(async() => {
|
|
223
|
+
const {chron} = setup()
|
|
224
|
+
await chron.mutate(s => s.count++)
|
|
225
|
+
await chron.undo()
|
|
226
|
+
expect(chron.state.count).is(0)
|
|
227
|
+
}),
|
|
228
|
+
|
|
229
|
+
"redo": Science.test(async() => {
|
|
230
|
+
const {chron} = setup()
|
|
231
|
+
await chron.mutate(s => s.count++)
|
|
232
|
+
await chron.undo()
|
|
233
|
+
expect(chron.state.count).is(0)
|
|
234
|
+
await chron.redo()
|
|
235
|
+
expect(chron.state.count).is(1)
|
|
236
|
+
}),
|
|
237
|
+
|
|
238
|
+
"undo/redo well ordered": Science.test(async() => {
|
|
239
|
+
const {chron} = setup()
|
|
240
|
+
await chron.mutate(s => s.count++)
|
|
241
|
+
await chron.mutate(s => s.count++)
|
|
242
|
+
await chron.mutate(s => s.count++)
|
|
243
|
+
expect(chron.state.count).is(3)
|
|
244
|
+
|
|
245
|
+
await chron.undo()
|
|
246
|
+
expect(chron.state.count).is(2)
|
|
247
|
+
|
|
248
|
+
await chron.undo()
|
|
249
|
+
expect(chron.state.count).is(1)
|
|
250
|
+
|
|
251
|
+
await chron.redo()
|
|
252
|
+
expect(chron.state.count).is(2)
|
|
253
|
+
|
|
254
|
+
await chron.redo()
|
|
255
|
+
expect(chron.state.count).is(3)
|
|
256
|
+
|
|
257
|
+
await chron.undo()
|
|
258
|
+
expect(chron.state.count).is(2)
|
|
259
|
+
|
|
260
|
+
await chron.undo()
|
|
261
|
+
expect(chron.state.count).is(1)
|
|
262
|
+
|
|
263
|
+
await chron.undo()
|
|
264
|
+
expect(chron.state.count).is(0)
|
|
265
|
+
}),
|
|
266
|
+
|
|
267
|
+
"undo nothing does nothing": Science.test(async() => {
|
|
268
|
+
const {chron} = setup()
|
|
269
|
+
await chron.undo()
|
|
270
|
+
expect(chron.state.count).is(0)
|
|
271
|
+
}),
|
|
272
|
+
|
|
273
|
+
"redo nothing does nothing": Science.test(async() => {
|
|
274
|
+
const {chron} = setup()
|
|
275
|
+
await chron.redo()
|
|
276
|
+
expect(chron.state.count).is(0)
|
|
277
|
+
}),
|
|
278
|
+
|
|
279
|
+
"undo 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
|
+
}),
|
|
287
|
+
|
|
288
|
+
"redo 2x": Science.test(async() => {
|
|
289
|
+
const {chron} = setup()
|
|
290
|
+
await chron.mutate(s => s.count++)
|
|
291
|
+
await chron.mutate(s => s.count++)
|
|
292
|
+
expect(chron.state.count).is(2)
|
|
293
|
+
await chron.undo(2)
|
|
294
|
+
expect(chron.state.count).is(0)
|
|
295
|
+
await chron.redo(2)
|
|
296
|
+
expect(chron.state.count).is(2)
|
|
297
|
+
}),
|
|
298
|
+
|
|
299
|
+
"substrata mutations are tracked": Science.test(async() => {
|
|
300
|
+
const strata = new Trunk({
|
|
301
|
+
chron: Trunk.chronicle({
|
|
302
|
+
group: {count: 0},
|
|
303
|
+
}),
|
|
304
|
+
})
|
|
305
|
+
const chron = strata.chronobranch(64, s => s.chron)
|
|
306
|
+
const group = chron.branch(s => s.group)
|
|
307
|
+
expect(group.state.count).is(0)
|
|
308
|
+
await group.mutate(g => g.count = 101)
|
|
309
|
+
expect(group.state.count).is(101)
|
|
310
|
+
await chron.undo()
|
|
311
|
+
expect(group.state.count).is(0)
|
|
312
|
+
}),
|
|
313
|
+
})
|
|
314
|
+
})(),
|
|
315
|
+
})
|
|
316
|
+
|
package/x/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
1
|
+
export * from "./signals/index.js";
|
|
2
|
+
export * from "./tracker/index.js";
|
|
3
|
+
export * from "./tree/index.js";
|
package/x/index.js
CHANGED
package/x/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../s/index.ts"],"names":[],"mappings":"AACA,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../s/index.ts"],"names":[],"mappings":"AACA,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../s/signals/index.ts"],"names":[],"mappings":"AACA,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA;AACjC,cAAc,mBAAmB,CAAA;AACjC,cAAc,kBAAkB,CAAA"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Sub } from "@e280/stz";
|
|
2
|
+
import { SignalOptions } from "./types.js";
|
|
3
|
+
export type DerivedSignal<V> = {
|
|
4
|
+
(): V;
|
|
5
|
+
kind: "derived";
|
|
6
|
+
sneak: V;
|
|
7
|
+
on: Sub<[V]>;
|
|
8
|
+
get(): V;
|
|
9
|
+
get value(): V;
|
|
10
|
+
dispose(): void;
|
|
11
|
+
};
|
|
12
|
+
export declare function derive<V>(formula: () => V, options?: Partial<SignalOptions>): DerivedSignal<V>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DerivedCore, processSignalOptions } from "./units.js";
|
|
2
|
+
export function derive(formula, options = {}) {
|
|
3
|
+
function fn() {
|
|
4
|
+
return fn.value;
|
|
5
|
+
}
|
|
6
|
+
const o = processSignalOptions(options);
|
|
7
|
+
const core = DerivedCore.make(fn, formula, o);
|
|
8
|
+
Object.setPrototypeOf(fn, DerivedCore.prototype);
|
|
9
|
+
Object.assign(fn, core);
|
|
10
|
+
return fn;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=derive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derive.js","sourceRoot":"","sources":["../../../s/signals/parts/derive.ts"],"names":[],"mappings":"AAGA,OAAO,EAAC,WAAW,EAAE,oBAAoB,EAAC,MAAM,YAAY,CAAA;AAa5D,MAAM,UAAU,MAAM,CAAI,OAAgB,EAAE,UAAkC,EAAE;IAC/E,SAAS,EAAE;QACV,OAAQ,EAAU,CAAC,KAAK,CAAA;IACzB,CAAC;IAED,MAAM,CAAC,GAAG,oBAAoB,CAAC,OAAO,CAAC,CAAA;IACvC,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAI,EAAS,EAAE,OAAO,EAAE,CAAC,CAAC,CAAA;IACvD,MAAM,CAAC,cAAc,CAAC,EAAE,EAAE,WAAW,CAAC,SAAS,CAAC,CAAA;IAChD,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,CAAA;IAEvB,OAAO,EAAsB,CAAA;AAC9B,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { debounce } from "@e280/stz";
|
|
2
|
+
import { tracker } from "../../tracker/tracker.js";
|
|
3
|
+
export function effect(collector, responder = collector) {
|
|
4
|
+
return collectorEffect(collector, responder).dispose;
|
|
5
|
+
}
|
|
6
|
+
export function collectorEffect(collector, responder = collector) {
|
|
7
|
+
const { seen, result } = tracker.seen(collector);
|
|
8
|
+
const fn = debounce(0, responder);
|
|
9
|
+
const disposers = [];
|
|
10
|
+
const dispose = () => disposers.forEach(d => d());
|
|
11
|
+
for (const saw of seen) {
|
|
12
|
+
const dispose = tracker.changed(saw, fn);
|
|
13
|
+
disposers.push(dispose);
|
|
14
|
+
}
|
|
15
|
+
return { result, dispose };
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=effect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"effect.js","sourceRoot":"","sources":["../../../s/signals/parts/effect.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,QAAQ,EAAC,MAAM,WAAW,CAAA;AAClC,OAAO,EAAC,OAAO,EAAC,MAAM,0BAA0B,CAAA;AAEhD,MAAM,UAAU,MAAM,CAAC,SAAqB,EAAE,YAAwB,SAAS;IAC9E,OAAO,eAAe,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,OAAO,CAAA;AACrD,CAAC;AAED,MAAM,UAAU,eAAe,CAAW,SAAkB,EAAE,YAAwB,SAAS;IAC9F,MAAM,EAAC,IAAI,EAAE,MAAM,EAAC,GAAG,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IAC9C,MAAM,EAAE,GAAG,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAA;IAEjC,MAAM,SAAS,GAAmB,EAAE,CAAA;IACpC,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAA;IAEjD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACxB,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;QACxC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IACxB,CAAC;IAED,OAAO,EAAC,MAAM,EAAE,OAAO,EAAC,CAAA;AACzB,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { SignalOptions } from "./types.js";
|
|
2
|
+
export type LazySignal<V> = {
|
|
3
|
+
(): V;
|
|
4
|
+
kind: "lazy";
|
|
5
|
+
sneak: V;
|
|
6
|
+
get(): V;
|
|
7
|
+
get value(): V;
|
|
8
|
+
dispose(): void;
|
|
9
|
+
};
|
|
10
|
+
export declare function lazy<V>(formula: () => V, options?: Partial<SignalOptions>): LazySignal<V>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { LazyCore, processSignalOptions } from "./units.js";
|
|
2
|
+
export function lazy(formula, options = {}) {
|
|
3
|
+
function fn() {
|
|
4
|
+
return fn.value;
|
|
5
|
+
}
|
|
6
|
+
const o = processSignalOptions(options);
|
|
7
|
+
const core = new LazyCore(formula, o);
|
|
8
|
+
Object.setPrototypeOf(fn, LazyCore.prototype);
|
|
9
|
+
Object.assign(fn, core);
|
|
10
|
+
return fn;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=lazy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lazy.js","sourceRoot":"","sources":["../../../s/signals/parts/lazy.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,QAAQ,EAAE,oBAAoB,EAAC,MAAM,YAAY,CAAA;AAYzD,MAAM,UAAU,IAAI,CAAI,OAAgB,EAAE,UAAkC,EAAE;IAC7E,SAAS,EAAE;QACV,OAAQ,EAAU,CAAC,KAAK,CAAA;IACzB,CAAC;IAED,MAAM,CAAC,GAAG,oBAAoB,CAAC,OAAO,CAAC,CAAA;IACvC,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAI,OAAO,EAAE,CAAC,CAAC,CAAA;IACxC,MAAM,CAAC,cAAc,CAAC,EAAE,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAA;IAC7C,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,CAAA;IAEvB,OAAO,EAAmB,CAAA;AAC3B,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Sub } from "@e280/stz";
|
|
2
|
+
import { SignalOptions } from "./types.js";
|
|
3
|
+
import { SignalCore } from "./units.js";
|
|
4
|
+
export type Signal<V> = {
|
|
5
|
+
(): V;
|
|
6
|
+
(v: V): Promise<V>;
|
|
7
|
+
(v?: V): V | Promise<V>;
|
|
8
|
+
kind: "signal";
|
|
9
|
+
sneak: V;
|
|
10
|
+
value: V;
|
|
11
|
+
on: Sub<[V]>;
|
|
12
|
+
get(): V;
|
|
13
|
+
set(v: V): Promise<V>;
|
|
14
|
+
publish(v?: V): Promise<V>;
|
|
15
|
+
dispose(): void;
|
|
16
|
+
} & SignalCore<V>;
|
|
17
|
+
export declare function signal<V>(value: V, options?: Partial<SignalOptions>): Signal<V>;
|
|
18
|
+
export declare namespace signal {
|
|
19
|
+
var lazy: typeof import("./lazy.js").lazy;
|
|
20
|
+
var derive: typeof import("./derive.js").derive;
|
|
21
|
+
}
|