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.
@@ -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
+ }
@@ -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
+ )