atom.io 0.0.0 → 0.2.0
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 +32 -13
- package/dist/index-9d9f5a05.d.ts +293 -0
- package/dist/index.d.ts +4 -257
- package/dist/index.js +218 -100
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +217 -99
- package/dist/index.mjs.map +1 -0
- package/dist/react/index.d.ts +25 -0
- package/dist/react/index.js +909 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/index.mjs +880 -0
- package/dist/react/index.mjs.map +1 -0
- package/package.json +24 -9
- package/react/package.json +6 -0
- package/src/atom.ts +78 -0
- package/src/index.ts +91 -0
- package/src/internal/get.ts +109 -0
- package/src/internal/index.ts +23 -0
- package/src/internal/is-default.ts +19 -0
- package/src/internal/operation.ts +49 -0
- package/src/internal/selector-internal.ts +127 -0
- package/src/internal/set.ts +88 -0
- package/src/internal/store.ts +84 -0
- package/src/internal/subscribe-internal.ts +33 -0
- package/src/internal/transaction-internal.ts +34 -0
- package/src/react/index.ts +65 -0
- package/src/selector.ts +132 -0
- package/src/transaction.ts +53 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { Store } from "."
|
|
2
|
+
import {
|
|
3
|
+
lookup,
|
|
4
|
+
IMPLICIT,
|
|
5
|
+
getState__INTERNAL,
|
|
6
|
+
setState__INTERNAL,
|
|
7
|
+
withdraw,
|
|
8
|
+
} from "."
|
|
9
|
+
import type {
|
|
10
|
+
AtomToken,
|
|
11
|
+
ReadonlyValueToken,
|
|
12
|
+
SelectorToken,
|
|
13
|
+
StateToken,
|
|
14
|
+
} from ".."
|
|
15
|
+
import type { Transactors } from "../transaction"
|
|
16
|
+
|
|
17
|
+
export const lookupSelectorSources = (
|
|
18
|
+
key: string,
|
|
19
|
+
store: Store
|
|
20
|
+
): (
|
|
21
|
+
| AtomToken<unknown>
|
|
22
|
+
| ReadonlyValueToken<unknown>
|
|
23
|
+
| SelectorToken<unknown>
|
|
24
|
+
)[] =>
|
|
25
|
+
store.selectorGraph
|
|
26
|
+
.getRelations(key)
|
|
27
|
+
.filter(({ source }) => source !== key)
|
|
28
|
+
.map(({ source }) => lookup(source, store))
|
|
29
|
+
|
|
30
|
+
export const traceSelectorAtoms = (
|
|
31
|
+
selectorKey: string,
|
|
32
|
+
dependency: ReadonlyValueToken<unknown> | StateToken<unknown>,
|
|
33
|
+
store: Store
|
|
34
|
+
): AtomToken<unknown>[] => {
|
|
35
|
+
const roots: AtomToken<unknown>[] = []
|
|
36
|
+
|
|
37
|
+
const sources = lookupSelectorSources(dependency.key, store)
|
|
38
|
+
let depth = 0
|
|
39
|
+
while (sources.length > 0) {
|
|
40
|
+
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
|
|
41
|
+
const source = sources.shift()!
|
|
42
|
+
++depth
|
|
43
|
+
if (depth > 999) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Maximum selector dependency depth exceeded in selector "${selectorKey}".`
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (source.type !== `atom`) {
|
|
50
|
+
sources.push(...lookupSelectorSources(source.key, store))
|
|
51
|
+
} else {
|
|
52
|
+
roots.push(source)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return roots
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const traceAllSelectorAtoms = (
|
|
60
|
+
selectorKey: string,
|
|
61
|
+
store: Store
|
|
62
|
+
): AtomToken<unknown>[] => {
|
|
63
|
+
const sources = lookupSelectorSources(selectorKey, store)
|
|
64
|
+
return sources.flatMap((source) =>
|
|
65
|
+
source.type === `atom`
|
|
66
|
+
? source
|
|
67
|
+
: traceSelectorAtoms(selectorKey, source, store)
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const updateSelectorAtoms = (
|
|
72
|
+
selectorKey: string,
|
|
73
|
+
dependency: ReadonlyValueToken<unknown> | StateToken<unknown>,
|
|
74
|
+
store: Store
|
|
75
|
+
): void => {
|
|
76
|
+
if (dependency.type === `atom`) {
|
|
77
|
+
store.selectorAtoms = store.selectorAtoms.set(selectorKey, dependency.key)
|
|
78
|
+
store.config.logger?.info(
|
|
79
|
+
` || adding root for "${selectorKey}": ${dependency.key}`
|
|
80
|
+
)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
const roots = traceSelectorAtoms(selectorKey, dependency, store)
|
|
84
|
+
store.config.logger?.info(` || adding roots for "${selectorKey}":`, roots)
|
|
85
|
+
for (const root of roots) {
|
|
86
|
+
store.selectorAtoms = store.selectorAtoms.set(selectorKey, root.key)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const registerSelector = (
|
|
91
|
+
selectorKey: string,
|
|
92
|
+
store: Store = IMPLICIT.STORE
|
|
93
|
+
): Transactors => ({
|
|
94
|
+
get: (dependency) => {
|
|
95
|
+
const alreadyRegistered = store.selectorGraph
|
|
96
|
+
.getRelations(selectorKey)
|
|
97
|
+
.some(({ source }) => source === dependency.key)
|
|
98
|
+
|
|
99
|
+
const dependencyState = withdraw(dependency, store)
|
|
100
|
+
const dependencyValue = getState__INTERNAL(dependencyState, store)
|
|
101
|
+
|
|
102
|
+
if (alreadyRegistered) {
|
|
103
|
+
store.config.logger?.info(
|
|
104
|
+
` || ${selectorKey} <- ${dependency.key} =`,
|
|
105
|
+
dependencyValue
|
|
106
|
+
)
|
|
107
|
+
} else {
|
|
108
|
+
store.config.logger?.info(
|
|
109
|
+
`🔌 registerSelector "${selectorKey}" <- "${dependency.key}" =`,
|
|
110
|
+
dependencyValue
|
|
111
|
+
)
|
|
112
|
+
store.selectorGraph = store.selectorGraph.set(
|
|
113
|
+
selectorKey,
|
|
114
|
+
dependency.key,
|
|
115
|
+
{
|
|
116
|
+
source: dependency.key,
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
updateSelectorAtoms(selectorKey, dependency, store)
|
|
121
|
+
return dependencyValue
|
|
122
|
+
},
|
|
123
|
+
set: (stateToken, newValue) => {
|
|
124
|
+
const state = withdraw(stateToken, store)
|
|
125
|
+
setState__INTERNAL(state, newValue, store)
|
|
126
|
+
},
|
|
127
|
+
})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import HAMT from "hamt_plus"
|
|
2
|
+
|
|
3
|
+
import { become } from "~/packages/anvl/src/function"
|
|
4
|
+
|
|
5
|
+
import type { Atom, Selector } from "."
|
|
6
|
+
import { isAtomDefault } from "."
|
|
7
|
+
import { getState__INTERNAL } from "./get"
|
|
8
|
+
import { isDone, markDone } from "./operation"
|
|
9
|
+
import type { Store } from "./store"
|
|
10
|
+
import { IMPLICIT } from "./store"
|
|
11
|
+
|
|
12
|
+
export const evictDownStream = <T>(
|
|
13
|
+
state: Atom<T>,
|
|
14
|
+
store: Store = IMPLICIT.STORE
|
|
15
|
+
): void => {
|
|
16
|
+
const downstream = store.selectorAtoms.getRelations(state.key)
|
|
17
|
+
const downstreamKeys = downstream.map(({ id }) => id)
|
|
18
|
+
store.config.logger?.info(
|
|
19
|
+
` || ${downstreamKeys.length} downstream:`,
|
|
20
|
+
downstreamKeys
|
|
21
|
+
)
|
|
22
|
+
if (store.operation.open) {
|
|
23
|
+
store.config.logger?.info(` ||`, [...store.operation.done], `already done`)
|
|
24
|
+
}
|
|
25
|
+
downstream.forEach(({ id: stateKey }) => {
|
|
26
|
+
if (isDone(stateKey, store)) {
|
|
27
|
+
store.config.logger?.info(` || ${stateKey} already done`)
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
const state =
|
|
31
|
+
HAMT.get(stateKey, store.selectors) ??
|
|
32
|
+
HAMT.get(stateKey, store.readonlySelectors)
|
|
33
|
+
if (!state) {
|
|
34
|
+
store.config.logger?.info(
|
|
35
|
+
` || ${stateKey} is an atom, and can't be downstream`
|
|
36
|
+
)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
store.valueMap = HAMT.remove(stateKey, store.valueMap)
|
|
40
|
+
store.config.logger?.info(` xx evicted "${stateKey}"`)
|
|
41
|
+
|
|
42
|
+
markDone(stateKey, store)
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const setAtomState = <T>(
|
|
47
|
+
atom: Atom<T>,
|
|
48
|
+
next: T | ((oldValue: T) => T),
|
|
49
|
+
store: Store = IMPLICIT.STORE
|
|
50
|
+
): void => {
|
|
51
|
+
const oldValue = getState__INTERNAL(atom, store)
|
|
52
|
+
const newValue = become(next)(oldValue)
|
|
53
|
+
store.config.logger?.info(`-> setting atom "${atom.key}" to`, newValue)
|
|
54
|
+
store.valueMap = HAMT.set(atom.key, newValue, store.valueMap)
|
|
55
|
+
if (isAtomDefault(atom.key)) {
|
|
56
|
+
store.atomsAreDefault = HAMT.set(atom.key, false, store.atomsAreDefault)
|
|
57
|
+
}
|
|
58
|
+
markDone(atom.key, store)
|
|
59
|
+
store.config.logger?.info(
|
|
60
|
+
` || evicting caches downstream from "${atom.key}"`
|
|
61
|
+
)
|
|
62
|
+
evictDownStream(atom, store)
|
|
63
|
+
atom.subject.next({ newValue, oldValue })
|
|
64
|
+
}
|
|
65
|
+
export const setSelectorState = <T>(
|
|
66
|
+
selector: Selector<T>,
|
|
67
|
+
next: T | ((oldValue: T) => T),
|
|
68
|
+
store: Store = IMPLICIT.STORE
|
|
69
|
+
): void => {
|
|
70
|
+
const oldValue = getState__INTERNAL(selector, store)
|
|
71
|
+
const newValue = become(next)(oldValue)
|
|
72
|
+
|
|
73
|
+
store.config.logger?.info(`-> setting selector "${selector.key}" to`, newValue)
|
|
74
|
+
store.config.logger?.info(` || propagating change made to "${selector.key}"`)
|
|
75
|
+
|
|
76
|
+
selector.set(newValue)
|
|
77
|
+
}
|
|
78
|
+
export const setState__INTERNAL = <T>(
|
|
79
|
+
state: Atom<T> | Selector<T>,
|
|
80
|
+
value: T | ((oldValue: T) => T),
|
|
81
|
+
store: Store = IMPLICIT.STORE
|
|
82
|
+
): void => {
|
|
83
|
+
if (`set` in state) {
|
|
84
|
+
setSelectorState(state, value, store)
|
|
85
|
+
} else {
|
|
86
|
+
setAtomState(state, value, store)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Hamt } from "hamt_plus"
|
|
2
|
+
import HAMT from "hamt_plus"
|
|
3
|
+
|
|
4
|
+
import { Join } from "~/packages/anvl/src/join"
|
|
5
|
+
|
|
6
|
+
import type { Atom, ReadonlySelector, Selector } from "."
|
|
7
|
+
|
|
8
|
+
export interface Store {
|
|
9
|
+
valueMap: Hamt<any, string>
|
|
10
|
+
selectorGraph: Join<{ source: string }>
|
|
11
|
+
selectorAtoms: Join
|
|
12
|
+
atoms: Hamt<Atom<any>, string>
|
|
13
|
+
atomsAreDefault: Hamt<boolean, string>
|
|
14
|
+
selectors: Hamt<Selector<any>, string>
|
|
15
|
+
readonlySelectors: Hamt<ReadonlySelector<any>, string>
|
|
16
|
+
operation:
|
|
17
|
+
| {
|
|
18
|
+
open: false
|
|
19
|
+
}
|
|
20
|
+
| {
|
|
21
|
+
open: true
|
|
22
|
+
done: Set<string>
|
|
23
|
+
prev: Hamt<any, string>
|
|
24
|
+
}
|
|
25
|
+
transaction:
|
|
26
|
+
| {
|
|
27
|
+
open: false
|
|
28
|
+
}
|
|
29
|
+
| {
|
|
30
|
+
open: true
|
|
31
|
+
prev: Pick<
|
|
32
|
+
Store,
|
|
33
|
+
| `atoms`
|
|
34
|
+
| `readonlySelectors`
|
|
35
|
+
| `selectorGraph`
|
|
36
|
+
| `selectors`
|
|
37
|
+
| `valueMap`
|
|
38
|
+
>
|
|
39
|
+
}
|
|
40
|
+
config: {
|
|
41
|
+
name: string
|
|
42
|
+
logger: Pick<Console, `error` | `info` | `warn`> | null
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const createStore = (name: string): Store =>
|
|
47
|
+
({
|
|
48
|
+
valueMap: HAMT.make<any, string>(),
|
|
49
|
+
selectorGraph: new Join({ relationType: `n:n` }),
|
|
50
|
+
selectorAtoms: new Join({ relationType: `n:n` }),
|
|
51
|
+
atoms: HAMT.make<Atom<any>, string>(),
|
|
52
|
+
atomsAreDefault: HAMT.make<boolean, string>(),
|
|
53
|
+
selectors: HAMT.make<Selector<any>, string>(),
|
|
54
|
+
readonlySelectors: HAMT.make<ReadonlySelector<any>, string>(),
|
|
55
|
+
operation: {
|
|
56
|
+
open: false,
|
|
57
|
+
},
|
|
58
|
+
transaction: {
|
|
59
|
+
open: false,
|
|
60
|
+
},
|
|
61
|
+
config: {
|
|
62
|
+
name,
|
|
63
|
+
logger: null,
|
|
64
|
+
},
|
|
65
|
+
} satisfies Store)
|
|
66
|
+
|
|
67
|
+
export const IMPLICIT = {
|
|
68
|
+
STORE_INTERNAL: undefined as Store | undefined,
|
|
69
|
+
get STORE(): Store {
|
|
70
|
+
return this.STORE_INTERNAL ?? (this.STORE_INTERNAL = createStore(`DEFAULT`))
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
export const configure = (
|
|
74
|
+
config: Partial<Store[`config`]>,
|
|
75
|
+
store: Store = IMPLICIT.STORE
|
|
76
|
+
): void => {
|
|
77
|
+
Object.assign(store.config, config)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const clearStore = (store: Store = IMPLICIT.STORE): void => {
|
|
81
|
+
const { config } = store
|
|
82
|
+
Object.assign(store, createStore(config.name))
|
|
83
|
+
store.config = config
|
|
84
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Atom, ReadonlySelector, Selector } from "."
|
|
2
|
+
import { getState__INTERNAL, withdraw } from "./get"
|
|
3
|
+
import { recallState } from "./operation"
|
|
4
|
+
import { traceAllSelectorAtoms } from "./selector-internal"
|
|
5
|
+
import type { Store } from "./store"
|
|
6
|
+
import { IMPLICIT } from "./store"
|
|
7
|
+
import { __INTERNAL__ } from ".."
|
|
8
|
+
|
|
9
|
+
export const subscribeToRootAtoms = <T>(
|
|
10
|
+
state: Atom<T> | ReadonlySelector<T> | Selector<T>,
|
|
11
|
+
store: Store = IMPLICIT.STORE
|
|
12
|
+
): { unsubscribe: () => void }[] | null => {
|
|
13
|
+
const dependencySubscriptions =
|
|
14
|
+
`default` in state
|
|
15
|
+
? null
|
|
16
|
+
: traceAllSelectorAtoms(state.key, store).map((atomToken) => {
|
|
17
|
+
const atom = withdraw(atomToken, store)
|
|
18
|
+
return atom.subject.subscribe((atomChange) => {
|
|
19
|
+
store.config.logger?.info(
|
|
20
|
+
`📢 atom changed: "${atomToken.key}" (`,
|
|
21
|
+
atomChange.oldValue,
|
|
22
|
+
`->`,
|
|
23
|
+
atomChange.newValue,
|
|
24
|
+
`) re-evaluating "${state.key}"`
|
|
25
|
+
)
|
|
26
|
+
const oldValue = recallState(state, store)
|
|
27
|
+
const newValue = getState__INTERNAL(state, store)
|
|
28
|
+
store.config.logger?.info(` <- ${state.key} became`, newValue)
|
|
29
|
+
state.subject.next({ newValue, oldValue })
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
return dependencySubscriptions
|
|
33
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Store } from "./store"
|
|
2
|
+
|
|
3
|
+
export const finishTransaction = (store: Store): void => {
|
|
4
|
+
store.transaction = { open: false }
|
|
5
|
+
store.config.logger?.info(`🛬`, `transaction done`)
|
|
6
|
+
}
|
|
7
|
+
export const startTransaction = (store: Store): void => {
|
|
8
|
+
store.transaction = {
|
|
9
|
+
open: true,
|
|
10
|
+
prev: {
|
|
11
|
+
atoms: store.atoms,
|
|
12
|
+
readonlySelectors: store.readonlySelectors,
|
|
13
|
+
selectorGraph: store.selectorGraph,
|
|
14
|
+
selectors: store.selectors,
|
|
15
|
+
valueMap: store.valueMap,
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
store.config.logger?.info(`🛫`, `transaction start`)
|
|
19
|
+
}
|
|
20
|
+
export const abortTransaction = (store: Store): void => {
|
|
21
|
+
if (!store.transaction.open) {
|
|
22
|
+
store.config.logger?.warn(
|
|
23
|
+
`abortTransaction called outside of a transaction. This is probably a bug.`
|
|
24
|
+
)
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
store.atoms = store.transaction.prev.atoms
|
|
28
|
+
store.readonlySelectors = store.transaction.prev.readonlySelectors
|
|
29
|
+
store.selectorGraph = store.transaction.prev.selectorGraph
|
|
30
|
+
store.selectors = store.transaction.prev.selectors
|
|
31
|
+
store.valueMap = store.transaction.prev.valueMap
|
|
32
|
+
store.transaction = { open: false }
|
|
33
|
+
store.config.logger?.info(`🪂`, `transaction fail`)
|
|
34
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type Preact from "preact/hooks"
|
|
2
|
+
|
|
3
|
+
import type React from "react"
|
|
4
|
+
|
|
5
|
+
import type { Modifier } from "~/packages/anvl/src/function"
|
|
6
|
+
|
|
7
|
+
import type { ReadonlyValueToken, StateToken } from ".."
|
|
8
|
+
import { subscribe, setState, __INTERNAL__ } from ".."
|
|
9
|
+
import { withdraw } from "../internal"
|
|
10
|
+
|
|
11
|
+
export type AtomStoreReactConfig = {
|
|
12
|
+
useState: typeof Preact.useState | typeof React.useState
|
|
13
|
+
useEffect: typeof Preact.useEffect | typeof React.useEffect
|
|
14
|
+
store?: __INTERNAL__.Store
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
|
18
|
+
export const composeStoreHooks = ({
|
|
19
|
+
useState,
|
|
20
|
+
useEffect,
|
|
21
|
+
store = __INTERNAL__.IMPLICIT.STORE,
|
|
22
|
+
}: AtomStoreReactConfig) => {
|
|
23
|
+
function useI<T>(token: StateToken<T>): (next: Modifier<T> | T) => void {
|
|
24
|
+
const updateState = (next: Modifier<T> | T) => setState(token, next, store)
|
|
25
|
+
return updateState
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function useO<T>(token: ReadonlyValueToken<T> | StateToken<T>): T {
|
|
29
|
+
const state = withdraw(token, store)
|
|
30
|
+
const initialValue = __INTERNAL__.getState__INTERNAL(state, store)
|
|
31
|
+
const [current, dispatch] = useState(initialValue)
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const unsubscribe = subscribe(
|
|
34
|
+
token,
|
|
35
|
+
({ newValue, oldValue }) => {
|
|
36
|
+
if (oldValue !== newValue) {
|
|
37
|
+
dispatch(newValue)
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
store
|
|
41
|
+
)
|
|
42
|
+
return unsubscribe
|
|
43
|
+
}, [current, dispatch])
|
|
44
|
+
|
|
45
|
+
return current
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function useIO<T>(token: StateToken<T>): [T, (next: Modifier<T> | T) => void] {
|
|
49
|
+
return [useO(token), useI(token)]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function useStore<T>(
|
|
53
|
+
token: StateToken<T>
|
|
54
|
+
): [T, (next: Modifier<T> | T) => void]
|
|
55
|
+
function useStore<T>(token: ReadonlyValueToken<T>): T
|
|
56
|
+
function useStore<T>(
|
|
57
|
+
token: ReadonlyValueToken<T> | StateToken<T>
|
|
58
|
+
): T | [T, (next: Modifier<T> | T) => void] {
|
|
59
|
+
if (token.type === `readonly_selector`) {
|
|
60
|
+
return useO(token)
|
|
61
|
+
}
|
|
62
|
+
return useIO(token)
|
|
63
|
+
}
|
|
64
|
+
return { useI, useO, useIO, useStore }
|
|
65
|
+
}
|
package/src/selector.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import HAMT from "hamt_plus"
|
|
2
|
+
import * as Rx from "rxjs"
|
|
3
|
+
|
|
4
|
+
import { become } from "~/packages/anvl/src/function"
|
|
5
|
+
import type { Serializable } from "~/packages/anvl/src/json"
|
|
6
|
+
import { stringifyJson } from "~/packages/anvl/src/json"
|
|
7
|
+
|
|
8
|
+
import type { ReadonlyValueToken, SelectorToken } from "."
|
|
9
|
+
import type { Selector, Store } from "./internal"
|
|
10
|
+
import { IMPLICIT, markDone, deposit, registerSelector } from "./internal"
|
|
11
|
+
import type { ReadonlyTransactors, Transactors } from "./transaction"
|
|
12
|
+
|
|
13
|
+
export type SelectorOptions<T> = {
|
|
14
|
+
key: string
|
|
15
|
+
get: (readonlyTransactors: ReadonlyTransactors) => T
|
|
16
|
+
set: (transactors: Transactors, newValue: T) => void
|
|
17
|
+
}
|
|
18
|
+
export type ReadonlySelectorOptions<T> = Omit<SelectorOptions<T>, `set`>
|
|
19
|
+
|
|
20
|
+
export function selector<T>(
|
|
21
|
+
options: SelectorOptions<T>,
|
|
22
|
+
store?: Store
|
|
23
|
+
): SelectorToken<T>
|
|
24
|
+
export function selector<T>(
|
|
25
|
+
options: ReadonlySelectorOptions<T>,
|
|
26
|
+
store?: Store
|
|
27
|
+
): ReadonlyValueToken<T>
|
|
28
|
+
export function selector<T>(
|
|
29
|
+
options: ReadonlySelectorOptions<T> | SelectorOptions<T>,
|
|
30
|
+
store: Store = IMPLICIT.STORE
|
|
31
|
+
): ReadonlyValueToken<T> | SelectorToken<T> {
|
|
32
|
+
if (HAMT.has(options.key, store.selectors)) {
|
|
33
|
+
throw new Error(`Key "${options.key}" already exists in the store.`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const subject = new Rx.Subject<{ newValue: T; oldValue: T }>()
|
|
37
|
+
|
|
38
|
+
const { get, set } = registerSelector(options.key, store)
|
|
39
|
+
const getSelf = () => {
|
|
40
|
+
const value = options.get({ get })
|
|
41
|
+
store.valueMap = HAMT.set(options.key, value, store.valueMap)
|
|
42
|
+
return value
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!(`set` in options)) {
|
|
46
|
+
const readonlySelector = {
|
|
47
|
+
...options,
|
|
48
|
+
subject,
|
|
49
|
+
get: getSelf,
|
|
50
|
+
}
|
|
51
|
+
store.readonlySelectors = HAMT.set(
|
|
52
|
+
options.key,
|
|
53
|
+
readonlySelector,
|
|
54
|
+
store.readonlySelectors
|
|
55
|
+
)
|
|
56
|
+
const initialValue = getSelf()
|
|
57
|
+
store.config.logger?.info(` ✨ "${options.key}" =`, initialValue)
|
|
58
|
+
return { ...readonlySelector, type: `readonly_selector` }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const setSelf = (next: T | ((oldValue: T) => T)): void => {
|
|
62
|
+
store.config.logger?.info(` <- "${options.key}" became`, next)
|
|
63
|
+
const oldValue = getSelf()
|
|
64
|
+
const newValue = become(next)(oldValue)
|
|
65
|
+
store.valueMap = HAMT.set(options.key, newValue, store.valueMap)
|
|
66
|
+
markDone(options.key, store)
|
|
67
|
+
subject.next({ newValue, oldValue })
|
|
68
|
+
options.set({ get, set }, newValue)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const mySelector: Selector<T> = {
|
|
72
|
+
...options,
|
|
73
|
+
subject,
|
|
74
|
+
get: getSelf,
|
|
75
|
+
set: setSelf,
|
|
76
|
+
}
|
|
77
|
+
store.selectors = HAMT.set(options.key, mySelector, store.selectors)
|
|
78
|
+
const initialValue = getSelf()
|
|
79
|
+
store.config.logger?.info(` ✨ "${options.key}" =`, initialValue)
|
|
80
|
+
return { ...mySelector, type: `selector` }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type SelectorFamilyOptions<T, K extends Serializable> = {
|
|
84
|
+
key: string
|
|
85
|
+
get: (key: K) => (readonlyTransactors: ReadonlyTransactors) => T
|
|
86
|
+
set: (key: K) => (transactors: Transactors, newValue: T) => void
|
|
87
|
+
}
|
|
88
|
+
export type ReadonlySelectorFamilyOptions<T, K extends Serializable> = Omit<
|
|
89
|
+
SelectorFamilyOptions<T, K>,
|
|
90
|
+
`set`
|
|
91
|
+
>
|
|
92
|
+
|
|
93
|
+
export function selectorFamily<T, K extends Serializable>(
|
|
94
|
+
options: SelectorFamilyOptions<T, K>,
|
|
95
|
+
store?: Store
|
|
96
|
+
): (key: K) => SelectorToken<T>
|
|
97
|
+
export function selectorFamily<T, K extends Serializable>(
|
|
98
|
+
options: ReadonlySelectorFamilyOptions<T, K>,
|
|
99
|
+
store?: Store
|
|
100
|
+
): (key: K) => ReadonlyValueToken<T>
|
|
101
|
+
export function selectorFamily<T, K extends Serializable>(
|
|
102
|
+
options: ReadonlySelectorFamilyOptions<T, K> | SelectorFamilyOptions<T, K>,
|
|
103
|
+
store: Store = IMPLICIT.STORE
|
|
104
|
+
): (key: K) => ReadonlyValueToken<T> | SelectorToken<T> {
|
|
105
|
+
return (key: K): ReadonlyValueToken<T> | SelectorToken<T> => {
|
|
106
|
+
const fullKey = `${options.key}__${stringifyJson(key)}`
|
|
107
|
+
const existing =
|
|
108
|
+
store.selectors.get(fullKey) ?? store.readonlySelectors.get(fullKey)
|
|
109
|
+
if (existing) {
|
|
110
|
+
return deposit(existing)
|
|
111
|
+
}
|
|
112
|
+
const readonlySelectorOptions: ReadonlySelectorOptions<T> = {
|
|
113
|
+
key: fullKey,
|
|
114
|
+
get: options.get(key),
|
|
115
|
+
}
|
|
116
|
+
if (!(`set` in options)) {
|
|
117
|
+
return selector<T>(
|
|
118
|
+
{
|
|
119
|
+
...readonlySelectorOptions,
|
|
120
|
+
},
|
|
121
|
+
store
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
return selector<T>(
|
|
125
|
+
{
|
|
126
|
+
...readonlySelectorOptions,
|
|
127
|
+
set: options.set(key),
|
|
128
|
+
},
|
|
129
|
+
store
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ReadonlyValueToken, StateToken } from "."
|
|
2
|
+
import { getState, setState } from "."
|
|
3
|
+
import type { Store } from "./internal"
|
|
4
|
+
import { IMPLICIT } from "./internal"
|
|
5
|
+
import {
|
|
6
|
+
abortTransaction,
|
|
7
|
+
finishTransaction,
|
|
8
|
+
startTransaction,
|
|
9
|
+
} from "./internal/transaction-internal"
|
|
10
|
+
|
|
11
|
+
export type ƒn = (...parameters: any[]) => any
|
|
12
|
+
|
|
13
|
+
export type Transactors = {
|
|
14
|
+
get: <S>(state: ReadonlyValueToken<S> | StateToken<S>) => S
|
|
15
|
+
set: <S>(state: StateToken<S>, newValue: S | ((oldValue: S) => S)) => void
|
|
16
|
+
}
|
|
17
|
+
export type ReadonlyTransactors = Pick<Transactors, `get`>
|
|
18
|
+
|
|
19
|
+
export type Action<ƒ extends ƒn> = (
|
|
20
|
+
transactors: Transactors,
|
|
21
|
+
...parameters: Parameters<ƒ>
|
|
22
|
+
) => ReturnType<ƒ>
|
|
23
|
+
|
|
24
|
+
export type TransactionOptions<ƒ extends ƒn> = {
|
|
25
|
+
key: string
|
|
26
|
+
do: Action<ƒ>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const transaction = <ƒ extends ƒn>(
|
|
30
|
+
options: TransactionOptions<ƒ>,
|
|
31
|
+
store: Store = IMPLICIT.STORE
|
|
32
|
+
): ((...parameters: Parameters<ƒ>) => ReturnType<ƒ>) & { key: string } =>
|
|
33
|
+
Object.assign(
|
|
34
|
+
(...parameters: Parameters<ƒ>) => {
|
|
35
|
+
startTransaction(store)
|
|
36
|
+
try {
|
|
37
|
+
const result = options.do(
|
|
38
|
+
{
|
|
39
|
+
get: (token) => getState(token, store),
|
|
40
|
+
set: (token, value) => setState(token, value, store),
|
|
41
|
+
},
|
|
42
|
+
...parameters
|
|
43
|
+
)
|
|
44
|
+
finishTransaction(store)
|
|
45
|
+
return result
|
|
46
|
+
} catch (thrown) {
|
|
47
|
+
abortTransaction(store)
|
|
48
|
+
store.config.logger?.error(`Transaction ${options.key} failed`, thrown)
|
|
49
|
+
throw thrown
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
{ key: options.key }
|
|
53
|
+
)
|