atom.io 0.1.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.
@@ -0,0 +1,109 @@
1
+ import { pipe } from "fp-ts/function"
2
+ import HAMT from "hamt_plus"
3
+
4
+ import type { Atom, ReadonlySelector, Selector } from "."
5
+ import type { Store } from "./store"
6
+ import { IMPLICIT } from "./store"
7
+ import type {
8
+ AtomToken,
9
+ ReadonlyValueToken,
10
+ SelectorToken,
11
+ StateToken,
12
+ } from ".."
13
+
14
+ export const getCachedState = <T>(
15
+ state: Atom<T> | ReadonlySelector<T> | Selector<T>,
16
+ store: Store = IMPLICIT.STORE
17
+ ): T => {
18
+ const path = []
19
+ if (`default` in state) {
20
+ const atomKey = state.key
21
+ store.selectorAtoms = pipe(store.selectorAtoms, (oldValue) => {
22
+ let newValue = oldValue
23
+ for (const selectorKey of path) {
24
+ newValue = newValue.set(selectorKey, atomKey)
25
+ }
26
+ return newValue
27
+ })
28
+ }
29
+ const value = HAMT.get(state.key, store.valueMap)
30
+ return value
31
+ }
32
+
33
+ export const getSelectorState = <T>(
34
+ selector: ReadonlySelector<T> | Selector<T>
35
+ ): T => selector.get()
36
+
37
+ export function lookup(
38
+ key: string,
39
+ store: Store
40
+ ): AtomToken<unknown> | ReadonlyValueToken<unknown> | SelectorToken<unknown> {
41
+ const type = HAMT.has(key, store.atoms)
42
+ ? `atom`
43
+ : HAMT.has(key, store.selectors)
44
+ ? `selector`
45
+ : `readonly_selector`
46
+ return { key, type }
47
+ }
48
+
49
+ export function withdraw<T>(token: AtomToken<T>, store: Store): Atom<T>
50
+ export function withdraw<T>(token: SelectorToken<T>, store: Store): Selector<T>
51
+ export function withdraw<T>(
52
+ token: StateToken<T>,
53
+ store: Store
54
+ ): Atom<T> | Selector<T>
55
+ export function withdraw<T>(
56
+ token: ReadonlyValueToken<T>,
57
+ store: Store
58
+ ): ReadonlySelector<T>
59
+ export function withdraw<T>(
60
+ token: ReadonlyValueToken<T> | StateToken<T>,
61
+ store: Store
62
+ ): Atom<T> | ReadonlySelector<T> | Selector<T>
63
+ export function withdraw<T>(
64
+ token: ReadonlyValueToken<T> | StateToken<T>,
65
+ store: Store
66
+ ): Atom<T> | ReadonlySelector<T> | Selector<T> {
67
+ return (
68
+ HAMT.get(token.key, store.atoms) ??
69
+ HAMT.get(token.key, store.selectors) ??
70
+ HAMT.get(token.key, store.readonlySelectors)
71
+ )
72
+ }
73
+
74
+ export function deposit<T>(state: Atom<T>): AtomToken<T>
75
+ export function deposit<T>(state: Selector<T>): SelectorToken<T>
76
+ export function deposit<T>(state: Atom<T> | Selector<T>): StateToken<T>
77
+ export function deposit<T>(state: ReadonlySelector<T>): ReadonlyValueToken<T>
78
+ export function deposit<T>(
79
+ state: Atom<T> | ReadonlySelector<T> | Selector<T>
80
+ ): ReadonlyValueToken<T> | StateToken<T>
81
+ export function deposit<T>(
82
+ state: Atom<T> | ReadonlySelector<T> | Selector<T>
83
+ ): ReadonlyValueToken<T> | StateToken<T> {
84
+ if (`get` in state) {
85
+ if (`set` in state) {
86
+ return { key: state.key, type: `selector` }
87
+ }
88
+ return { key: state.key, type: `readonly_selector` }
89
+ }
90
+ return { key: state.key, type: `atom` }
91
+ }
92
+
93
+ export const getState__INTERNAL = <T>(
94
+ state: Atom<T> | ReadonlySelector<T> | Selector<T>,
95
+ store: Store = IMPLICIT.STORE
96
+ ): T => {
97
+ if (HAMT.has(state.key, store.valueMap)) {
98
+ store.config.logger?.info(`>> read "${state.key}"`)
99
+ return getCachedState(state, store)
100
+ }
101
+ if (`get` in state) {
102
+ store.config.logger?.info(`-> calc "${state.key}"`)
103
+ return getSelectorState(state)
104
+ }
105
+ store.config.logger?.error(
106
+ `Attempted to get atom "${state.key}", which was never initialized in store "${store.config.name}".`
107
+ )
108
+ return state.default
109
+ }
@@ -0,0 +1,23 @@
1
+ import type * as Rx from "rxjs"
2
+
3
+ export * from "./get"
4
+ export * from "./set"
5
+ export * from "./is-default"
6
+ export * from "./selector-internal"
7
+ export * from "./store"
8
+ export * from "./subscribe-internal"
9
+ export * from "./operation"
10
+ export * from "./transaction-internal"
11
+
12
+ export type Atom<T> = {
13
+ key: string
14
+ subject: Rx.Subject<{ newValue: T; oldValue: T }>
15
+ default: T
16
+ }
17
+ export type Selector<T> = {
18
+ key: string
19
+ subject: Rx.Subject<{ newValue: T; oldValue: T }>
20
+ get: () => T
21
+ set: (newValue: T | ((oldValue: T) => T)) => void
22
+ }
23
+ export type ReadonlySelector<T> = Omit<Selector<T>, `set`>
@@ -0,0 +1,19 @@
1
+ import HAMT from "hamt_plus"
2
+
3
+ import type { Store } from "."
4
+ import { IMPLICIT, traceAllSelectorAtoms } from "."
5
+
6
+ export const isAtomDefault = (
7
+ key: string,
8
+ store: Store = IMPLICIT.STORE
9
+ ): boolean => {
10
+ return HAMT.get(key, store.atomsAreDefault)
11
+ }
12
+
13
+ export const isSelectorDefault = (
14
+ key: string,
15
+ store: Store = IMPLICIT.STORE
16
+ ): boolean => {
17
+ const roots = traceAllSelectorAtoms(key, store)
18
+ return roots.every((root) => isAtomDefault(root.key, store))
19
+ }
@@ -0,0 +1,49 @@
1
+ import HAMT from "hamt_plus"
2
+
3
+ import type { Atom, ReadonlySelector, Selector } from "."
4
+ import type { Store } from "./store"
5
+ import { IMPLICIT } from "./store"
6
+
7
+ export const startAction = (store: Store): void => {
8
+ store.operation = {
9
+ open: true,
10
+ done: new Set(),
11
+ prev: store.valueMap,
12
+ }
13
+ store.config.logger?.info(`⭕`, `operation start`)
14
+ }
15
+ export const finishAction = (store: Store): void => {
16
+ store.operation = { open: false }
17
+ store.config.logger?.info(`🔴`, `operation done`)
18
+ }
19
+
20
+ export const isDone = (key: string, store: Store = IMPLICIT.STORE): boolean => {
21
+ if (!store.operation.open) {
22
+ store.config.logger?.warn(
23
+ `isDone called outside of an action. This is probably a bug.`
24
+ )
25
+ return true
26
+ }
27
+ return store.operation.done.has(key)
28
+ }
29
+ export const markDone = (key: string, store: Store = IMPLICIT.STORE): void => {
30
+ if (!store.operation.open) {
31
+ store.config.logger?.warn(
32
+ `markDone called outside of an action. This is probably a bug.`
33
+ )
34
+ return
35
+ }
36
+ store.operation.done.add(key)
37
+ }
38
+ export const recallState = <T>(
39
+ state: Atom<T> | ReadonlySelector<T> | Selector<T>,
40
+ store: Store = IMPLICIT.STORE
41
+ ): T => {
42
+ if (!store.operation.open) {
43
+ store.config.logger?.warn(
44
+ `recall called outside of an action. This is probably a bug.`
45
+ )
46
+ return HAMT.get(state.key, store.valueMap)
47
+ }
48
+ return HAMT.get(state.key, store.operation.prev)
49
+ }
@@ -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
+ }