atom.io 0.1.0 → 0.3.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.
@@ -0,0 +1,132 @@
1
+ import type { Hamt } from "hamt_plus"
2
+ import HAMT from "hamt_plus"
3
+
4
+ import type { Atom, ReadonlySelector, Selector } from "."
5
+ import { target } from "."
6
+ import type { Store } from "./store"
7
+ import { IMPLICIT } from "./store"
8
+
9
+ export type OperationProgress =
10
+ | {
11
+ open: false
12
+ }
13
+ | {
14
+ open: true
15
+ done: Set<string>
16
+ prev: Hamt<any, string>
17
+ }
18
+
19
+ export const openOperation = (store: Store): void => {
20
+ const core = target(store)
21
+ core.operation = {
22
+ open: true,
23
+ done: new Set(),
24
+ prev: store.valueMap,
25
+ }
26
+ store.config.logger?.info(`⭕`, `operation start`)
27
+ }
28
+ export const closeOperation = (store: Store): void => {
29
+ const core = target(store)
30
+ core.operation = { open: false }
31
+ store.config.logger?.info(`🔴`, `operation done`)
32
+ }
33
+
34
+ export const isDone = (key: string, store: Store = IMPLICIT.STORE): boolean => {
35
+ const core = target(store)
36
+ if (!core.operation.open) {
37
+ store.config.logger?.warn(
38
+ `isDone called outside of an operation. This is probably a bug.`
39
+ )
40
+ return true
41
+ }
42
+ return core.operation.done.has(key)
43
+ }
44
+ export const markDone = (key: string, store: Store = IMPLICIT.STORE): void => {
45
+ const core = target(store)
46
+ if (!core.operation.open) {
47
+ store.config.logger?.warn(
48
+ `markDone called outside of an operation. This is probably a bug.`
49
+ )
50
+ return
51
+ }
52
+ core.operation.done.add(key)
53
+ }
54
+ export const recallState = <T>(
55
+ state: Atom<T> | ReadonlySelector<T> | Selector<T>,
56
+ store: Store = IMPLICIT.STORE
57
+ ): T => {
58
+ const core = target(store)
59
+ if (!core.operation.open) {
60
+ store.config.logger?.warn(
61
+ `recall called outside of an operation. This is probably a bug.`
62
+ )
63
+ return HAMT.get(state.key, core.valueMap)
64
+ }
65
+ return HAMT.get(state.key, core.operation.prev)
66
+ }
67
+
68
+ export const cacheValue = (
69
+ key: string,
70
+ value: unknown,
71
+ store: Store = IMPLICIT.STORE
72
+ ): void => {
73
+ const core = target(store)
74
+ core.valueMap = HAMT.set(key, value, core.valueMap)
75
+ }
76
+
77
+ export const evictCachedValue = (
78
+ key: string,
79
+ store: Store = IMPLICIT.STORE
80
+ ): void => {
81
+ const core = target(store)
82
+ core.valueMap = HAMT.remove(key, core.valueMap)
83
+ }
84
+ export const readCachedValue = <T>(
85
+ key: string,
86
+ store: Store = IMPLICIT.STORE
87
+ ): T => HAMT.get(key, target(store).valueMap)
88
+
89
+ export const isValueCached = (
90
+ key: string,
91
+ store: Store = IMPLICIT.STORE
92
+ ): boolean => HAMT.has(key, target(store).valueMap)
93
+
94
+ export const storeAtom = (
95
+ atom: Atom<any>,
96
+ store: Store = IMPLICIT.STORE
97
+ ): void => {
98
+ const core = target(store)
99
+ core.atoms = HAMT.set(atom.key, atom, core.atoms)
100
+ }
101
+
102
+ export const storeSelector = (
103
+ selector: Selector<any>,
104
+ store: Store = IMPLICIT.STORE
105
+ ): void => {
106
+ const core = target(store)
107
+ core.selectors = HAMT.set(selector.key, selector, core.selectors)
108
+ }
109
+
110
+ export const storeReadonlySelector = (
111
+ selector: ReadonlySelector<any>,
112
+ store: Store = IMPLICIT.STORE
113
+ ): void => {
114
+ const core = target(store)
115
+ core.readonlySelectors = HAMT.set(
116
+ selector.key,
117
+ selector,
118
+ core.readonlySelectors
119
+ )
120
+ }
121
+
122
+ export const hasKeyBeenUsed = (
123
+ key: string,
124
+ store: Store = IMPLICIT.STORE
125
+ ): boolean => {
126
+ const core = target(store)
127
+ return (
128
+ HAMT.has(key, core.atoms) ||
129
+ HAMT.has(key, core.selectors) ||
130
+ HAMT.has(key, core.readonlySelectors)
131
+ )
132
+ }
@@ -0,0 +1,227 @@
1
+ import HAMT from "hamt_plus"
2
+ import * as Rx from "rxjs"
3
+
4
+ import { become } from "~/packages/anvl/src/function"
5
+
6
+ import type { Store } from "."
7
+ import {
8
+ target,
9
+ cacheValue,
10
+ markDone,
11
+ lookup,
12
+ IMPLICIT,
13
+ getState__INTERNAL,
14
+ setState__INTERNAL,
15
+ withdraw,
16
+ } from "."
17
+ import type {
18
+ AtomToken,
19
+ FamilyMetadata,
20
+ ReadonlySelectorOptions,
21
+ ReadonlyValueToken,
22
+ SelectorOptions,
23
+ SelectorToken,
24
+ StateToken,
25
+ } from ".."
26
+ import type { Transactors } from "../transaction"
27
+
28
+ export type Selector<T> = {
29
+ key: string
30
+ type: `selector`
31
+ family?: FamilyMetadata
32
+ subject: Rx.Subject<{ newValue: T; oldValue: T }>
33
+ get: () => T
34
+ set: (newValue: T | ((oldValue: T) => T)) => void
35
+ }
36
+ export type ReadonlySelector<T> = {
37
+ key: string
38
+ type: `readonly_selector`
39
+ family?: FamilyMetadata
40
+ subject: Rx.Subject<{ newValue: T; oldValue: T }>
41
+ get: () => T
42
+ }
43
+
44
+ export const lookupSelectorSources = (
45
+ key: string,
46
+ store: Store
47
+ ): (
48
+ | AtomToken<unknown>
49
+ | ReadonlyValueToken<unknown>
50
+ | SelectorToken<unknown>
51
+ )[] =>
52
+ target(store)
53
+ .selectorGraph.getRelations(key)
54
+ .filter(({ source }) => source !== key)
55
+ .map(({ source }) => lookup(source, store))
56
+
57
+ export const traceSelectorAtoms = (
58
+ selectorKey: string,
59
+ dependency: ReadonlyValueToken<unknown> | StateToken<unknown>,
60
+ store: Store
61
+ ): AtomToken<unknown>[] => {
62
+ const roots: AtomToken<unknown>[] = []
63
+
64
+ const sources = lookupSelectorSources(dependency.key, store)
65
+ let depth = 0
66
+ while (sources.length > 0) {
67
+ /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
68
+ const source = sources.shift()!
69
+ ++depth
70
+ if (depth > 999) {
71
+ throw new Error(
72
+ `Maximum selector dependency depth exceeded in selector "${selectorKey}".`
73
+ )
74
+ }
75
+
76
+ if (source.type !== `atom`) {
77
+ sources.push(...lookupSelectorSources(source.key, store))
78
+ } else {
79
+ roots.push(source)
80
+ }
81
+ }
82
+
83
+ return roots
84
+ }
85
+
86
+ export const traceAllSelectorAtoms = (
87
+ selectorKey: string,
88
+ store: Store
89
+ ): AtomToken<unknown>[] => {
90
+ const sources = lookupSelectorSources(selectorKey, store)
91
+ return sources.flatMap((source) =>
92
+ source.type === `atom`
93
+ ? source
94
+ : traceSelectorAtoms(selectorKey, source, store)
95
+ )
96
+ }
97
+
98
+ export const updateSelectorAtoms = (
99
+ selectorKey: string,
100
+ dependency: ReadonlyValueToken<unknown> | StateToken<unknown>,
101
+ store: Store
102
+ ): void => {
103
+ const core = target(store)
104
+ if (dependency.type === `atom`) {
105
+ core.selectorAtoms = core.selectorAtoms.set(selectorKey, dependency.key)
106
+ store.config.logger?.info(
107
+ ` || adding root for "${selectorKey}": ${dependency.key}`
108
+ )
109
+ return
110
+ }
111
+ const roots = traceSelectorAtoms(selectorKey, dependency, store)
112
+ store.config.logger?.info(
113
+ ` || adding roots for "${selectorKey}":`,
114
+ roots.map((r) => r.key)
115
+ )
116
+ for (const root of roots) {
117
+ core.selectorAtoms = core.selectorAtoms.set(selectorKey, root.key)
118
+ }
119
+ }
120
+
121
+ export const registerSelector = (
122
+ selectorKey: string,
123
+ store: Store = IMPLICIT.STORE
124
+ ): Transactors => ({
125
+ get: (dependency) => {
126
+ const core = target(store)
127
+ const alreadyRegistered = core.selectorGraph
128
+ .getRelations(selectorKey)
129
+ .some(({ source }) => source === dependency.key)
130
+
131
+ const dependencyState = withdraw(dependency, store)
132
+ const dependencyValue = getState__INTERNAL(dependencyState, store)
133
+
134
+ if (alreadyRegistered) {
135
+ store.config.logger?.info(
136
+ ` || ${selectorKey} <- ${dependency.key} =`,
137
+ dependencyValue
138
+ )
139
+ } else {
140
+ store.config.logger?.info(
141
+ `🔌 registerSelector "${selectorKey}" <- "${dependency.key}" =`,
142
+ dependencyValue
143
+ )
144
+ core.selectorGraph = core.selectorGraph.set(selectorKey, dependency.key, {
145
+ source: dependency.key,
146
+ })
147
+ }
148
+ updateSelectorAtoms(selectorKey, dependency, store)
149
+ return dependencyValue
150
+ },
151
+ set: (stateToken, newValue) => {
152
+ const state = withdraw(stateToken, store)
153
+ setState__INTERNAL(state, newValue, store)
154
+ },
155
+ })
156
+
157
+ export function selector__INTERNAL<T>(
158
+ options: SelectorOptions<T>,
159
+ family?: FamilyMetadata,
160
+ store?: Store
161
+ ): SelectorToken<T>
162
+ export function selector__INTERNAL<T>(
163
+ options: ReadonlySelectorOptions<T>,
164
+ family?: FamilyMetadata,
165
+ store?: Store
166
+ ): ReadonlyValueToken<T>
167
+ export function selector__INTERNAL<T>(
168
+ options: ReadonlySelectorOptions<T> | SelectorOptions<T>,
169
+ family?: FamilyMetadata,
170
+ store: Store = IMPLICIT.STORE
171
+ ): ReadonlyValueToken<T> | SelectorToken<T> {
172
+ const core = target(store)
173
+ if (HAMT.has(options.key, core.selectors)) {
174
+ store.config.logger?.error(
175
+ `Key "${options.key}" already exists in the store.`
176
+ )
177
+ }
178
+
179
+ const subject = new Rx.Subject<{ newValue: T; oldValue: T }>()
180
+
181
+ const { get, set } = registerSelector(options.key, store)
182
+ const getSelf = () => {
183
+ const value = options.get({ get })
184
+ cacheValue(options.key, value, store)
185
+ return value
186
+ }
187
+ if (!(`set` in options)) {
188
+ const readonlySelector: ReadonlySelector<T> = {
189
+ ...options,
190
+ subject,
191
+ get: getSelf,
192
+ type: `readonly_selector`,
193
+ ...(family && { family }),
194
+ }
195
+ core.readonlySelectors = HAMT.set(
196
+ options.key,
197
+ readonlySelector,
198
+ core.readonlySelectors
199
+ )
200
+ const initialValue = getSelf()
201
+ store.config.logger?.info(` ✨ "${options.key}" =`, initialValue)
202
+ return { ...readonlySelector, type: `readonly_selector` }
203
+ }
204
+ const setSelf = (next: T | ((oldValue: T) => T)): void => {
205
+ store.config.logger?.info(` <- "${options.key}" became`, next)
206
+ const oldValue = getSelf()
207
+ const newValue = become(next)(oldValue)
208
+ cacheValue(options.key, newValue, store)
209
+ markDone(options.key, store)
210
+ if (store.transactionStatus.phase === `idle`) {
211
+ subject.next({ newValue, oldValue })
212
+ }
213
+ options.set({ get, set }, newValue)
214
+ }
215
+ const mySelector: Selector<T> = {
216
+ ...options,
217
+ subject,
218
+ get: getSelf,
219
+ set: setSelf,
220
+ type: `selector`,
221
+ ...(family && { family }),
222
+ }
223
+ core.selectors = HAMT.set(options.key, mySelector, core.selectors)
224
+ const initialValue = getSelf()
225
+ store.config.logger?.info(` ✨ "${options.key}" =`, initialValue)
226
+ return { ...mySelector, type: `selector` }
227
+ }
@@ -0,0 +1,102 @@
1
+ import HAMT from "hamt_plus"
2
+
3
+ import { become } from "~/packages/anvl/src/function"
4
+
5
+ import type { Atom, Selector, Store } from "."
6
+ import {
7
+ IMPLICIT,
8
+ cacheValue,
9
+ emitUpdate,
10
+ evictCachedValue,
11
+ getState__INTERNAL,
12
+ isAtomDefault,
13
+ isDone,
14
+ markAtomAsNotDefault,
15
+ markDone,
16
+ stowUpdate,
17
+ target,
18
+ } from "."
19
+
20
+ export const evictDownStream = <T>(
21
+ state: Atom<T>,
22
+ store: Store = IMPLICIT.STORE
23
+ ): void => {
24
+ const core = target(store)
25
+ const downstream = core.selectorAtoms.getRelations(state.key)
26
+ const downstreamKeys = downstream.map(({ id }) => id)
27
+ store.config.logger?.info(
28
+ ` || ${downstreamKeys.length} downstream:`,
29
+ downstreamKeys
30
+ )
31
+ if (core.operation.open) {
32
+ store.config.logger?.info(` ||`, [...core.operation.done], `already done`)
33
+ }
34
+ downstream.forEach(({ id: stateKey }) => {
35
+ if (isDone(stateKey, store)) {
36
+ store.config.logger?.info(` || ${stateKey} already done`)
37
+ return
38
+ }
39
+ const state =
40
+ HAMT.get(stateKey, core.selectors) ??
41
+ HAMT.get(stateKey, core.readonlySelectors)
42
+ if (!state) {
43
+ store.config.logger?.info(
44
+ ` || ${stateKey} is an atom, and can't be downstream`
45
+ )
46
+ return
47
+ }
48
+ evictCachedValue(stateKey, store)
49
+ store.config.logger?.info(` xx evicted "${stateKey}"`)
50
+
51
+ markDone(stateKey, store)
52
+ })
53
+ }
54
+
55
+ export const setAtomState = <T>(
56
+ atom: Atom<T>,
57
+ next: T | ((oldValue: T) => T),
58
+ store: Store = IMPLICIT.STORE
59
+ ): void => {
60
+ const oldValue = getState__INTERNAL(atom, store)
61
+ const newValue = become(next)(oldValue)
62
+ store.config.logger?.info(`<< setting atom "${atom.key}" to`, newValue)
63
+ cacheValue(atom.key, newValue, store)
64
+ if (isAtomDefault(atom.key)) {
65
+ markAtomAsNotDefault(atom.key, store)
66
+ }
67
+ markDone(atom.key, store)
68
+ store.config.logger?.info(
69
+ ` || evicting caches downstream from "${atom.key}"`
70
+ )
71
+ evictDownStream(atom, store)
72
+ const update = { oldValue, newValue }
73
+ if (store.transactionStatus.phase !== `building`) {
74
+ emitUpdate(atom, update, store)
75
+ } else {
76
+ stowUpdate(atom, update, store)
77
+ }
78
+ }
79
+ export const setSelectorState = <T>(
80
+ selector: Selector<T>,
81
+ next: T | ((oldValue: T) => T),
82
+ store: Store = IMPLICIT.STORE
83
+ ): void => {
84
+ const oldValue = getState__INTERNAL(selector, store)
85
+ const newValue = become(next)(oldValue)
86
+
87
+ store.config.logger?.info(`<< setting selector "${selector.key}" to`, newValue)
88
+ store.config.logger?.info(` || propagating change made to "${selector.key}"`)
89
+
90
+ selector.set(newValue)
91
+ }
92
+ export const setState__INTERNAL = <T>(
93
+ state: Atom<T> | Selector<T>,
94
+ value: T | ((oldValue: T) => T),
95
+ store: Store = IMPLICIT.STORE
96
+ ): void => {
97
+ if (`set` in state) {
98
+ setSelectorState(state, value, store)
99
+ } else {
100
+ setAtomState(state, value, store)
101
+ }
102
+ }
@@ -0,0 +1,97 @@
1
+ import type { Hamt } from "hamt_plus"
2
+ import HAMT from "hamt_plus"
3
+
4
+ import { doNothing } from "~/packages/anvl/src/function"
5
+ import { Join } from "~/packages/anvl/src/join"
6
+
7
+ import type {
8
+ Atom,
9
+ OperationProgress,
10
+ ReadonlySelector,
11
+ Selector,
12
+ TransactionStatus,
13
+ Logger,
14
+ Timeline,
15
+ TimelineData,
16
+ } from "."
17
+ import type { Transaction, ƒn } from ".."
18
+
19
+ export type StoreCore = Pick<
20
+ Store,
21
+ | `atoms`
22
+ | `atomsThatAreDefault`
23
+ | `operation`
24
+ | `readonlySelectors`
25
+ | `selectorAtoms`
26
+ | `selectorGraph`
27
+ | `selectors`
28
+ | `timelineAtoms`
29
+ | `timelines`
30
+ | `transactions`
31
+ | `valueMap`
32
+ >
33
+
34
+ export interface Store {
35
+ atoms: Hamt<Atom<any>, string>
36
+ atomsThatAreDefault: Set<string>
37
+ readonlySelectors: Hamt<ReadonlySelector<any>, string>
38
+ selectorAtoms: Join
39
+ selectorGraph: Join<{ source: string }>
40
+ selectors: Hamt<Selector<any>, string>
41
+ timelines: Hamt<Timeline, string>
42
+ timelineAtoms: Join
43
+ timelineStore: Hamt<TimelineData, string>
44
+ transactions: Hamt<Transaction<any>, string>
45
+ valueMap: Hamt<any, string>
46
+
47
+ operation: OperationProgress
48
+ transactionStatus: TransactionStatus<ƒn>
49
+ config: {
50
+ name: string
51
+ logger: Logger | null
52
+ logger__INTERNAL: Logger
53
+ }
54
+ }
55
+
56
+ export const createStore = (name: string): Store =>
57
+ ({
58
+ atoms: HAMT.make<Atom<any>, string>(),
59
+ atomsThatAreDefault: new Set(),
60
+ readonlySelectors: HAMT.make<ReadonlySelector<any>, string>(),
61
+ selectorAtoms: new Join({ relationType: `n:n` }),
62
+ selectorGraph: new Join({ relationType: `n:n` }),
63
+ selectors: HAMT.make<Selector<any>, string>(),
64
+ timelines: HAMT.make<Timeline, string>(),
65
+ timelineAtoms: new Join({ relationType: `1:n` }),
66
+ timelineStore: HAMT.make<TimelineData, string>(),
67
+ transactions: HAMT.make<Transaction<any>, string>(),
68
+ valueMap: HAMT.make<any, string>(),
69
+
70
+ operation: {
71
+ open: false,
72
+ },
73
+ transactionStatus: {
74
+ phase: `idle`,
75
+ },
76
+ config: {
77
+ name,
78
+ logger: {
79
+ ...console,
80
+ info: doNothing,
81
+ },
82
+ logger__INTERNAL: console,
83
+ },
84
+ } satisfies Store)
85
+
86
+ export const IMPLICIT = {
87
+ STORE_INTERNAL: undefined as Store | undefined,
88
+ get STORE(): Store {
89
+ return this.STORE_INTERNAL ?? (this.STORE_INTERNAL = createStore(`DEFAULT`))
90
+ },
91
+ }
92
+
93
+ export const clearStore = (store: Store = IMPLICIT.STORE): void => {
94
+ const { config } = store
95
+ Object.assign(store, createStore(config.name))
96
+ store.config = config
97
+ }
@@ -0,0 +1,77 @@
1
+ import {
2
+ getState__INTERNAL,
3
+ withdraw,
4
+ recallState,
5
+ traceAllSelectorAtoms,
6
+ } from "."
7
+ import type { Atom, ReadonlySelector, Selector, Store } from "."
8
+ import type { StateUpdate } from ".."
9
+
10
+ export const prepareUpdate = <T>(
11
+ state: Atom<T> | ReadonlySelector<T> | Selector<T>,
12
+ store: Store
13
+ ): StateUpdate<T> => {
14
+ const oldValue = recallState(state, store)
15
+ const newValue = getState__INTERNAL(state, store)
16
+ return { newValue, oldValue }
17
+ }
18
+
19
+ export const stowUpdate = <T>(
20
+ state: Atom<T>,
21
+ update: StateUpdate<T>,
22
+ store: Store
23
+ ): void => {
24
+ const { key } = state
25
+ const { logger } = store.config
26
+ if (store.transactionStatus.phase !== `building`) {
27
+ store.config.logger?.warn(
28
+ `stowUpdate called outside of a transaction. This is probably a bug.`
29
+ )
30
+ return
31
+ }
32
+ store.transactionStatus.atomUpdates.push({ key, ...update })
33
+ logger?.info(`📝 ${key} stowed (`, update.oldValue, `->`, update.newValue, `)`)
34
+ }
35
+
36
+ export const emitUpdate = <T>(
37
+ state: Atom<T> | ReadonlySelector<T> | Selector<T>,
38
+ update: StateUpdate<T>,
39
+ store: Store
40
+ ): void => {
41
+ const { key } = state
42
+ const { logger } = store.config
43
+ logger?.info(
44
+ `📢 ${state.type} "${key}" went (`,
45
+ update.oldValue,
46
+ `->`,
47
+ update.newValue,
48
+ `)`
49
+ )
50
+ state.subject.next(update)
51
+ }
52
+
53
+ export const subscribeToRootAtoms = <T>(
54
+ state: ReadonlySelector<T> | Selector<T>,
55
+ store: Store
56
+ ): { unsubscribe: () => void }[] | null => {
57
+ const dependencySubscriptions =
58
+ `default` in state
59
+ ? null
60
+ : traceAllSelectorAtoms(state.key, store).map((atomToken) => {
61
+ const atom = withdraw(atomToken, store)
62
+ return atom.subject.subscribe((atomChange) => {
63
+ store.config.logger?.info(
64
+ `📢 selector "${state.key}" saw root "${atomToken.key}" go (`,
65
+ atomChange.oldValue,
66
+ `->`,
67
+ atomChange.newValue,
68
+ `)`
69
+ )
70
+ const oldValue = recallState(state, store)
71
+ const newValue = getState__INTERNAL(state, store)
72
+ store.config.logger?.info(` <- ${state.key} became`, newValue)
73
+ state.subject.next({ newValue, oldValue })
74
+ })
75
+ })
76
+ return dependencySubscriptions
77
+ }