atom.io 0.6.9 โ†’ 0.7.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.
Files changed (169) hide show
  1. package/README.md +21 -2
  2. package/dist/index.d.mts +34 -421
  3. package/dist/index.d.ts +34 -421
  4. package/dist/index.js +248 -23
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +209 -4
  7. package/dist/index.mjs.map +1 -1
  8. package/internal/dist/index.d.mts +342 -0
  9. package/internal/dist/index.d.ts +342 -0
  10. package/internal/dist/index.js +1873 -0
  11. package/internal/dist/index.js.map +1 -0
  12. package/internal/dist/index.mjs +1798 -0
  13. package/internal/dist/index.mjs.map +1 -0
  14. package/internal/package.json +15 -0
  15. package/internal/src/atom/create-atom.ts +75 -0
  16. package/internal/src/atom/delete-atom.ts +10 -0
  17. package/internal/src/atom/index.ts +3 -0
  18. package/internal/src/atom/is-default.ts +37 -0
  19. package/internal/src/caching.ts +21 -0
  20. package/internal/src/families/create-atom-family.ts +59 -0
  21. package/internal/src/families/create-readonly-selector-family.ts +45 -0
  22. package/internal/src/families/create-selector-family.ts +67 -0
  23. package/internal/src/families/index.ts +3 -0
  24. package/internal/src/get-state-internal.ts +23 -0
  25. package/internal/src/index.ts +13 -0
  26. package/internal/src/mutable/create-mutable-atom-family.ts +25 -0
  27. package/internal/src/mutable/create-mutable-atom.ts +49 -0
  28. package/internal/src/mutable/get-json-token.ts +22 -0
  29. package/internal/src/mutable/get-update-token.ts +20 -0
  30. package/internal/src/mutable/index.ts +17 -0
  31. package/internal/src/mutable/is-atom-token-mutable.ts +7 -0
  32. package/internal/src/mutable/tracker-family.ts +61 -0
  33. package/internal/src/mutable/tracker.ts +164 -0
  34. package/internal/src/mutable/transceiver.ts +110 -0
  35. package/internal/src/operation.ts +68 -0
  36. package/internal/src/selector/create-read-write-selector.ts +65 -0
  37. package/internal/src/selector/create-readonly-selector.ts +49 -0
  38. package/internal/src/selector/create-selector.ts +65 -0
  39. package/internal/src/selector/index.ts +5 -0
  40. package/internal/src/selector/lookup-selector-sources.ts +20 -0
  41. package/internal/src/selector/register-selector.ts +61 -0
  42. package/internal/src/selector/trace-selector-atoms.ts +45 -0
  43. package/internal/src/selector/update-selector-atoms.ts +34 -0
  44. package/internal/src/set-state/become.ts +10 -0
  45. package/internal/src/set-state/copy-mutable-if-needed.ts +23 -0
  46. package/internal/src/set-state/copy-mutable-in-transaction.ts +59 -0
  47. package/internal/src/set-state/copy-mutable-into-new-store.ts +34 -0
  48. package/internal/src/set-state/emit-update.ts +23 -0
  49. package/internal/src/set-state/evict-downstream.ts +39 -0
  50. package/internal/src/set-state/index.ts +2 -0
  51. package/internal/src/set-state/set-atom-state.ts +38 -0
  52. package/internal/src/set-state/set-selector-state.ts +19 -0
  53. package/internal/src/set-state/set-state-internal.ts +18 -0
  54. package/internal/src/set-state/stow-update.ts +42 -0
  55. package/internal/src/store/deposit.ts +43 -0
  56. package/internal/src/store/index.ts +5 -0
  57. package/internal/src/store/lookup.ts +26 -0
  58. package/internal/src/store/store.ts +154 -0
  59. package/internal/src/store/withdraw-new-family-member.ts +53 -0
  60. package/internal/src/store/withdraw.ts +113 -0
  61. package/internal/src/subject.ts +21 -0
  62. package/internal/src/subscribe/index.ts +1 -0
  63. package/internal/src/subscribe/recall-state.ts +19 -0
  64. package/internal/src/subscribe/subscribe-to-root-atoms.ts +47 -0
  65. package/internal/src/timeline/add-atom-to-timeline.ts +189 -0
  66. package/internal/src/timeline/index.ts +3 -0
  67. package/internal/src/timeline/time-travel-internal.ts +91 -0
  68. package/internal/src/timeline/timeline-internal.ts +115 -0
  69. package/internal/src/transaction/abort-transaction.ts +12 -0
  70. package/internal/src/transaction/apply-transaction.ts +64 -0
  71. package/internal/src/transaction/build-transaction.ts +39 -0
  72. package/internal/src/transaction/index.ts +26 -0
  73. package/internal/src/transaction/redo-transaction.ts +22 -0
  74. package/internal/src/transaction/transaction-internal.ts +64 -0
  75. package/internal/src/transaction/undo-transaction.ts +22 -0
  76. package/introspection/dist/index.d.mts +3 -197
  77. package/introspection/dist/index.d.ts +3 -197
  78. package/introspection/dist/index.js +329 -4
  79. package/introspection/dist/index.js.map +1 -1
  80. package/introspection/dist/index.mjs +310 -4
  81. package/introspection/dist/index.mjs.map +1 -1
  82. package/introspection/src/attach-atom-index.ts +84 -0
  83. package/introspection/src/attach-introspection-states.ts +38 -0
  84. package/introspection/src/attach-selector-index.ts +90 -0
  85. package/introspection/src/attach-timeline-family.ts +59 -0
  86. package/introspection/src/attach-timeline-index.ts +38 -0
  87. package/introspection/src/attach-transaction-index.ts +40 -0
  88. package/introspection/src/attach-transaction-logs.ts +43 -0
  89. package/introspection/src/index.ts +20 -0
  90. package/json/dist/index.d.mts +10 -2
  91. package/json/dist/index.d.ts +10 -2
  92. package/json/dist/index.js +83 -26
  93. package/json/dist/index.js.map +1 -1
  94. package/json/dist/index.mjs +74 -3
  95. package/json/dist/index.mjs.map +1 -1
  96. package/json/src/index.ts +5 -0
  97. package/json/src/select-json-family.ts +35 -0
  98. package/json/src/select-json.ts +22 -0
  99. package/package.json +103 -63
  100. package/react/dist/index.d.mts +9 -17
  101. package/react/dist/index.d.ts +9 -17
  102. package/react/dist/index.js +44 -27
  103. package/react/dist/index.js.map +1 -1
  104. package/react/dist/index.mjs +24 -4
  105. package/react/dist/index.mjs.map +1 -1
  106. package/react/src/index.ts +2 -0
  107. package/react/src/store-context.tsx +12 -0
  108. package/react/src/store-hooks.ts +36 -0
  109. package/react-devtools/dist/index.css +50 -1
  110. package/react-devtools/dist/index.css.map +1 -1
  111. package/react-devtools/dist/index.d.mts +104 -71
  112. package/react-devtools/dist/index.d.ts +104 -71
  113. package/react-devtools/dist/index.js +2806 -44
  114. package/react-devtools/dist/index.js.map +1 -1
  115. package/react-devtools/dist/index.mjs +2775 -10
  116. package/react-devtools/dist/index.mjs.map +1 -1
  117. package/react-devtools/src/AtomIODevtools.tsx +109 -0
  118. package/react-devtools/src/Button.tsx +23 -0
  119. package/react-devtools/src/StateEditor.tsx +75 -0
  120. package/react-devtools/src/StateIndex.tsx +159 -0
  121. package/react-devtools/src/TimelineIndex.tsx +88 -0
  122. package/react-devtools/src/TransactionIndex.tsx +70 -0
  123. package/react-devtools/src/Updates.tsx +150 -0
  124. package/react-devtools/src/devtools.scss +310 -0
  125. package/react-devtools/src/index.ts +72 -0
  126. package/realtime-react/dist/index.d.mts +8 -22
  127. package/realtime-react/dist/index.d.ts +8 -22
  128. package/realtime-react/dist/index.js +87 -32
  129. package/realtime-react/dist/index.js.map +1 -1
  130. package/realtime-react/dist/index.mjs +62 -6
  131. package/realtime-react/dist/index.mjs.map +1 -1
  132. package/realtime-react/src/index.ts +7 -0
  133. package/realtime-react/src/realtime-context.tsx +29 -0
  134. package/realtime-react/src/use-pull-family-member.ts +15 -0
  135. package/realtime-react/src/use-pull-mutable-family-member.ts +20 -0
  136. package/realtime-react/src/use-pull-mutable.ts +17 -0
  137. package/realtime-react/src/use-pull.ts +15 -0
  138. package/realtime-react/src/use-push.ts +19 -0
  139. package/realtime-react/src/use-server-action.ts +18 -0
  140. package/realtime-testing/dist/index.d.mts +49 -0
  141. package/realtime-testing/dist/index.d.ts +49 -0
  142. package/realtime-testing/dist/index.js +147 -0
  143. package/realtime-testing/dist/index.js.map +1 -0
  144. package/realtime-testing/dist/index.mjs +116 -0
  145. package/realtime-testing/dist/index.mjs.map +1 -0
  146. package/realtime-testing/src/index.ts +1 -0
  147. package/realtime-testing/src/setup-realtime-test.tsx +161 -0
  148. package/src/atom.ts +64 -9
  149. package/src/index.ts +36 -32
  150. package/src/logger.ts +3 -3
  151. package/src/selector.ts +3 -3
  152. package/src/silo.ts +29 -20
  153. package/src/subscribe.ts +3 -3
  154. package/src/timeline.ts +2 -2
  155. package/transceivers/set-rtx/dist/index.d.mts +39 -0
  156. package/transceivers/set-rtx/dist/index.d.ts +39 -0
  157. package/transceivers/set-rtx/dist/index.js +213 -0
  158. package/transceivers/set-rtx/dist/index.js.map +1 -0
  159. package/transceivers/set-rtx/dist/index.mjs +211 -0
  160. package/transceivers/set-rtx/dist/index.mjs.map +1 -0
  161. package/{realtime โ†’ transceivers/set-rtx}/package.json +1 -1
  162. package/transceivers/set-rtx/src/index.ts +1 -0
  163. package/transceivers/set-rtx/src/set-rtx.ts +242 -0
  164. package/realtime/dist/index.d.mts +0 -23
  165. package/realtime/dist/index.d.ts +0 -23
  166. package/realtime/dist/index.js +0 -32
  167. package/realtime/dist/index.js.map +0 -1
  168. package/realtime/dist/index.mjs +0 -7
  169. package/realtime/dist/index.mjs.map +0 -1
@@ -0,0 +1,67 @@
1
+ import type {
2
+ FamilyMetadata,
3
+ ReadonlySelectorFamily,
4
+ ReadonlySelectorFamilyOptions,
5
+ SelectorFamily,
6
+ SelectorFamilyOptions,
7
+ SelectorToken,
8
+ } from "atom.io"
9
+ import type { Json } from "atom.io/json"
10
+ import { stringifyJson } from "atom.io/json"
11
+
12
+ import { createSelector } from "../selector"
13
+ import type { Store } from "../store"
14
+ import { IMPLICIT, deposit } from "../store"
15
+ import { Subject } from "../subject"
16
+ import { target } from "../transaction"
17
+ import { createReadonlySelectorFamily } from "./create-readonly-selector-family"
18
+
19
+ export function createSelectorFamily<T, K extends Json.Serializable>(
20
+ options: SelectorFamilyOptions<T, K>,
21
+ store?: Store,
22
+ ): SelectorFamily<T, K>
23
+ export function createSelectorFamily<T, K extends Json.Serializable>(
24
+ options: ReadonlySelectorFamilyOptions<T, K>,
25
+ store?: Store,
26
+ ): ReadonlySelectorFamily<T, K>
27
+ export function createSelectorFamily<T, K extends Json.Serializable>(
28
+ options: ReadonlySelectorFamilyOptions<T, K> | SelectorFamilyOptions<T, K>,
29
+ store: Store = IMPLICIT.STORE,
30
+ ): ReadonlySelectorFamily<T, K> | SelectorFamily<T, K> {
31
+ const isReadonly = !(`set` in options)
32
+
33
+ if (isReadonly) {
34
+ return createReadonlySelectorFamily(options, store)
35
+ }
36
+ const core = target(store)
37
+ const subject = new Subject<SelectorToken<T>>()
38
+
39
+ const selectorFamily = Object.assign(
40
+ (key: K): SelectorToken<T> => {
41
+ const subKey = stringifyJson(key)
42
+ const family: FamilyMetadata = { key: options.key, subKey }
43
+ const fullKey = `${options.key}(${subKey})`
44
+ const existing = core.selectors.get(fullKey)
45
+ if (existing) {
46
+ return deposit(existing)
47
+ }
48
+ const token = createSelector<T>(
49
+ {
50
+ key: fullKey,
51
+ get: options.get(key),
52
+ set: options.set(key),
53
+ },
54
+ family,
55
+ store,
56
+ )
57
+ subject.next(token)
58
+ return token
59
+ },
60
+ {
61
+ key: options.key,
62
+ type: `selector_family`,
63
+ } as const,
64
+ ) as SelectorFamily<T, K>
65
+ core.families.set(options.key, selectorFamily)
66
+ return selectorFamily
67
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./create-atom-family"
2
+ export * from "./create-readonly-selector-family"
3
+ export * from "./create-selector-family"
@@ -0,0 +1,23 @@
1
+ import type { Atom } from "./atom"
2
+ import { isValueCached, readCachedValue } from "./caching"
3
+ import type { ReadonlySelector, Selector } from "./selector"
4
+ import type { Store } from "./store"
5
+ import { IMPLICIT } from "./store"
6
+
7
+ export const getState__INTERNAL = <T>(
8
+ state: Atom<T> | ReadonlySelector<T> | Selector<T>,
9
+ store: Store = IMPLICIT.STORE,
10
+ ): T => {
11
+ if (isValueCached(state.key, store)) {
12
+ store.config.logger?.info(`>> read "${state.key}"`)
13
+ return readCachedValue(state.key, store)
14
+ }
15
+ if (state.type !== `atom`) {
16
+ store.config.logger?.info(`-> calc "${state.key}"`)
17
+ return state.get()
18
+ }
19
+ store.config.logger?.error(
20
+ `Attempted to get atom "${state.key}", which was never initialized in store "${store.config.name}".`,
21
+ )
22
+ return state.default
23
+ }
@@ -0,0 +1,13 @@
1
+ export * from "./atom"
2
+ export * from "./caching"
3
+ export * from "./families"
4
+ export * from "./get-state-internal"
5
+ export * from "./operation"
6
+ export * from "./mutable"
7
+ export * from "./selector"
8
+ export * from "./set-state"
9
+ export * from "./store"
10
+ export * from "./subject"
11
+ export * from "./subscribe"
12
+ export * from "./timeline"
13
+ export * from "./transaction"
@@ -0,0 +1,25 @@
1
+ import type * as AtomIO from "atom.io"
2
+ import type { Json } from "atom.io/json"
3
+ import { selectJsonFamily } from "atom.io/json"
4
+
5
+ import type { Store } from ".."
6
+ import { IMPLICIT, createAtomFamily } from ".."
7
+ import { FamilyTracker } from "./tracker-family"
8
+ import type { Transceiver } from "./transceiver"
9
+
10
+ export function createMutableAtomFamily<
11
+ Core extends Transceiver<any>,
12
+ SerializableCore extends Json.Serializable,
13
+ Key extends string,
14
+ >(
15
+ options: AtomIO.MutableAtomFamilyOptions<Core, SerializableCore, Key>,
16
+ store: Store = IMPLICIT.STORE,
17
+ ): AtomIO.MutableAtomFamily<Core, SerializableCore, Key> {
18
+ const coreFamily = Object.assign(
19
+ createAtomFamily<Core, Key>(options, store),
20
+ options,
21
+ ) as AtomIO.MutableAtomFamily<Core, SerializableCore, Key>
22
+ selectJsonFamily(coreFamily, options)
23
+ new FamilyTracker(coreFamily, store)
24
+ return coreFamily
25
+ }
@@ -0,0 +1,49 @@
1
+ import * as AtomIO from "atom.io"
2
+ import type { Json } from "atom.io/json"
3
+ import { selectJson } from "atom.io/json"
4
+
5
+ import { createAtom } from "../atom"
6
+ import type { Store } from "../store"
7
+ import { IMPLICIT } from "../store"
8
+ import { target } from "../transaction"
9
+ import { Tracker } from "./tracker"
10
+ import type { Transceiver } from "./transceiver"
11
+
12
+ export function createMutableAtom<
13
+ Core extends Transceiver<any>,
14
+ SerializableCore extends Json.Serializable,
15
+ >(
16
+ options: AtomIO.MutableAtomOptions<Core, SerializableCore>,
17
+ store: Store = IMPLICIT.STORE,
18
+ ): AtomIO.MutableAtomToken<Core, SerializableCore> {
19
+ store.config.logger?.info(
20
+ `๐Ÿ”ง creating mutable atom "${options.key}" in store "${store.config.name}"`,
21
+ )
22
+ const coreState = createAtom<Core>(options, undefined, store)
23
+ new Tracker(coreState, store)
24
+ const jsonState = selectJson(coreState, options, store)
25
+ AtomIO.subscribe(
26
+ jsonState,
27
+ () => {
28
+ store.config.logger?.info(
29
+ `๐Ÿ” tracker-initializer:${store?.config.name}:${
30
+ store.transactionStatus.phase === `idle`
31
+ ? `main`
32
+ : store.transactionStatus.key
33
+ }`,
34
+ `Initializing tracker for ${coreState.key}`,
35
+ )
36
+
37
+ const trackerHasBeenInitialized = target(store).trackers.has(coreState.key)
38
+ if (!trackerHasBeenInitialized) {
39
+ new Tracker(coreState, store)
40
+ }
41
+ },
42
+ `tracker-initializer:${store?.config.name}:${
43
+ store.transactionStatus.phase === `idle`
44
+ ? `main`
45
+ : store.transactionStatus.key
46
+ }`,
47
+ )
48
+ return coreState as AtomIO.MutableAtomToken<Core, SerializableCore>
49
+ }
@@ -0,0 +1,22 @@
1
+ import type { MutableAtomToken, SelectorToken } from "atom.io"
2
+ import type { Json } from "atom.io/json"
3
+ import type { Transceiver } from "./transceiver"
4
+
5
+ export const getJsonToken = <
6
+ Core extends Transceiver<Json.Serializable>,
7
+ SerializableCore extends Json.Serializable,
8
+ >(
9
+ mutableAtomToken: MutableAtomToken<Core, SerializableCore>,
10
+ ): SelectorToken<SerializableCore> => {
11
+ const key = mutableAtomToken.family
12
+ ? `${mutableAtomToken.family.key}:JSON(${mutableAtomToken.family.subKey})`
13
+ : `${mutableAtomToken.key}:JSON`
14
+ const jsonToken: SelectorToken<SerializableCore> = { type: `selector`, key }
15
+ if (mutableAtomToken.family) {
16
+ jsonToken.family = {
17
+ key: `${mutableAtomToken.family.key}:JSON`,
18
+ subKey: mutableAtomToken.family.subKey,
19
+ }
20
+ }
21
+ return jsonToken
22
+ }
@@ -0,0 +1,20 @@
1
+ import type { AtomToken, MutableAtomToken } from "atom.io"
2
+ import type { Json } from "atom.io/json"
3
+ import type { Signal, Transceiver } from "./transceiver"
4
+
5
+ export const getUpdateToken = <
6
+ Core extends Transceiver<Json.Serializable>,
7
+ SerializableCore extends Json.Serializable,
8
+ >(
9
+ mutableAtomToken: MutableAtomToken<Core, SerializableCore>,
10
+ ): AtomToken<Signal<Core>> => {
11
+ const key = `*${mutableAtomToken.key}`
12
+ const updateToken: AtomToken<Signal<Core>> = { type: `atom`, key }
13
+ if (mutableAtomToken.family) {
14
+ updateToken.family = {
15
+ key: `*${mutableAtomToken.family.key}`,
16
+ subKey: mutableAtomToken.family.subKey,
17
+ }
18
+ }
19
+ return updateToken
20
+ }
@@ -0,0 +1,17 @@
1
+ import type { Atom } from "../atom"
2
+
3
+ export * from "./create-mutable-atom"
4
+ export * from "./create-mutable-atom-family"
5
+ export * from "./get-json-token"
6
+ export * from "./get-update-token"
7
+ export * from "./is-atom-token-mutable"
8
+ export * from "./tracker"
9
+ export * from "./tracker-family"
10
+ export * from "./transceiver"
11
+
12
+ export interface MutableAtom<T> extends Atom<T> {
13
+ mutable: true
14
+ }
15
+
16
+ export const isAtomMutable = <T>(atom: Atom<T>): atom is MutableAtom<T> =>
17
+ `isMutable` in atom
@@ -0,0 +1,7 @@
1
+ import type * as AtomIO from "atom.io"
2
+
3
+ export function isAtomTokenMutable(
4
+ token: AtomIO.AtomToken<any>,
5
+ ): token is AtomIO.MutableAtomToken<any, any> {
6
+ return token.key.endsWith(`::mutable`)
7
+ }
@@ -0,0 +1,61 @@
1
+ import type * as AtomIO from "atom.io"
2
+ import type { Json } from "atom.io/json"
3
+ import { parseJson } from "atom.io/json"
4
+
5
+ import { createAtomFamily } from "../families"
6
+ import type { Store } from "../store"
7
+ import { IMPLICIT } from "../store"
8
+ import { Tracker } from "./tracker"
9
+ import type { Transceiver } from "./transceiver"
10
+
11
+ export class FamilyTracker<
12
+ Core extends Transceiver<any>,
13
+ FamilyMemberKey extends Json.Serializable,
14
+ > {
15
+ private readonly Update: Core extends Transceiver<infer Signal>
16
+ ? Signal
17
+ : never
18
+
19
+ public readonly findLatestUpdateState: AtomIO.AtomFamily<
20
+ typeof this.Update | null,
21
+ FamilyMemberKey
22
+ >
23
+ public readonly findMutableState: AtomIO.AtomFamily<Core, FamilyMemberKey>
24
+
25
+ public constructor(
26
+ findMutableState: AtomIO.AtomFamily<Core, FamilyMemberKey>,
27
+ store: Store = IMPLICIT.STORE,
28
+ ) {
29
+ this.findLatestUpdateState = createAtomFamily<
30
+ typeof this.Update | null,
31
+ FamilyMemberKey
32
+ >(
33
+ {
34
+ key: `*${findMutableState.key}`,
35
+ default: null,
36
+ },
37
+ store,
38
+ )
39
+ this.findMutableState = findMutableState
40
+ this.findMutableState.subject.subscribe(
41
+ `store=${store.config.name}::tracker-atom-family`,
42
+ (atomToken) => {
43
+ if (atomToken.family) {
44
+ const key = parseJson(atomToken.family.subKey) as FamilyMemberKey
45
+ this.findLatestUpdateState(key)
46
+ new Tracker<Core>(atomToken, store)
47
+ }
48
+ },
49
+ )
50
+ this.findLatestUpdateState.subject.subscribe(
51
+ `store=${store.config.name}::tracker-atom-family`,
52
+ (atomToken) => {
53
+ if (atomToken.family) {
54
+ const key = parseJson(atomToken.family.subKey) as FamilyMemberKey
55
+ const mutableAtomToken = this.findMutableState(key)
56
+ new Tracker<Core>(mutableAtomToken, store)
57
+ }
58
+ },
59
+ )
60
+ }
61
+ }
@@ -0,0 +1,164 @@
1
+ import * as AtomIO from "atom.io"
2
+ import type { Json } from "atom.io/json"
3
+
4
+ import type { Store } from ".."
5
+ import { IMPLICIT } from ".."
6
+ import { createAtom, deleteAtom } from "../atom"
7
+ import { target } from "../transaction"
8
+ import type { Transceiver } from "./transceiver"
9
+
10
+ /**
11
+ * @internal Give the tracker a transceiver state and a store, and it will
12
+ * subscribe to the transceiver's inner value. When the inner value changes,
13
+ * the tracker will update its own state to reflect the change.
14
+ */
15
+ export class Tracker<Mutable extends Transceiver<any>> {
16
+ private Update: Mutable extends Transceiver<infer Signal> ? Signal : never
17
+
18
+ private initializeState(
19
+ mutableState: AtomIO.MutableAtomToken<Mutable, Json.Serializable>,
20
+ store: Store = IMPLICIT.STORE,
21
+ ): AtomIO.AtomToken<typeof this.Update | null> {
22
+ const latestUpdateStateKey = `*${mutableState.key}`
23
+ deleteAtom(latestUpdateStateKey, target(store))
24
+ const familyMetaData: AtomIO.FamilyMetadata | undefined = mutableState.family
25
+ ? {
26
+ key: `*${mutableState.family.key}`,
27
+ subKey: mutableState.family.subKey,
28
+ }
29
+ : undefined
30
+ const latestUpdateState = createAtom<
31
+ (Mutable extends Transceiver<infer Signal> ? Signal : never) | null
32
+ >(
33
+ {
34
+ key: latestUpdateStateKey,
35
+ default: null,
36
+ },
37
+ familyMetaData,
38
+ store,
39
+ )
40
+
41
+ return latestUpdateState
42
+ }
43
+
44
+ private unsubscribeFromInnerValue: (() => void) | null = null
45
+ private observeCore(
46
+ mutableState: AtomIO.MutableAtomToken<Mutable, Json.Serializable>,
47
+ latestUpdateState: AtomIO.AtomToken<typeof this.Update | null>,
48
+ store: Store = IMPLICIT.STORE,
49
+ ): void {
50
+ const originalInnerValue = AtomIO.getState(mutableState, store)
51
+ this.unsubscribeFromInnerValue = originalInnerValue.subscribe(
52
+ `tracker:${store.config.name}:${
53
+ store.transactionStatus.phase === `idle`
54
+ ? `main`
55
+ : store.transactionStatus.key
56
+ }`,
57
+ (update) => {
58
+ const unsubscribe = store.subject.operationStatus.subscribe(
59
+ mutableState.key,
60
+ () => {
61
+ unsubscribe()
62
+ AtomIO.setState(latestUpdateState, update, store)
63
+ },
64
+ )
65
+ },
66
+ )
67
+ AtomIO.subscribe(
68
+ mutableState,
69
+ (update) => {
70
+ if (update.newValue !== update.oldValue) {
71
+ this.unsubscribeFromInnerValue?.()
72
+ this.unsubscribeFromInnerValue = update.newValue.subscribe(
73
+ `tracker:${store.config.name}:${
74
+ store.transactionStatus.phase === `idle`
75
+ ? `main`
76
+ : store.transactionStatus.key
77
+ }`,
78
+ (update) => {
79
+ const unsubscribe = store.subject.operationStatus.subscribe(
80
+ mutableState.key,
81
+ () => {
82
+ unsubscribe()
83
+ AtomIO.setState(latestUpdateState, update, store)
84
+ },
85
+ )
86
+ },
87
+ )
88
+ }
89
+ },
90
+ `${store.config.name}: tracker observing inner value`,
91
+ store,
92
+ )
93
+ }
94
+
95
+ private updateCore<Core extends Transceiver<any>>(
96
+ mutableState: AtomIO.MutableAtomToken<Core, Json.Serializable>,
97
+ latestUpdateState: AtomIO.AtomToken<typeof this.Update | null>,
98
+ store: Store = IMPLICIT.STORE,
99
+ ): void {
100
+ AtomIO.subscribe(
101
+ latestUpdateState,
102
+ ({ newValue, oldValue }) => {
103
+ const timelineId = store.timelineAtoms.getRelatedKey(
104
+ latestUpdateState.key,
105
+ )
106
+ if (timelineId) {
107
+ const timelineData = store.timelines.get(timelineId)
108
+ if (timelineData?.timeTraveling) {
109
+ const unsubscribe = AtomIO.subscribeToTimeline(
110
+ { key: timelineId, type: `timeline` },
111
+ (update) => {
112
+ unsubscribe()
113
+ AtomIO.setState(
114
+ mutableState,
115
+ (transceiver) => {
116
+ if (update === `redo` && newValue) {
117
+ transceiver.do(newValue)
118
+ } else if (update === `undo` && oldValue) {
119
+ transceiver.undo(oldValue)
120
+ }
121
+ return transceiver
122
+ },
123
+ store,
124
+ )
125
+ },
126
+ )
127
+ return
128
+ }
129
+ }
130
+
131
+ const unsubscribe = store.subject.operationStatus.subscribe(
132
+ latestUpdateState.key,
133
+ () => {
134
+ unsubscribe()
135
+ if (newValue) {
136
+ AtomIO.setState(
137
+ mutableState,
138
+ (transceiver) => (transceiver.do(newValue), transceiver),
139
+ store,
140
+ )
141
+ }
142
+ },
143
+ )
144
+ },
145
+ `${store.config.name}: tracker observing latest update`,
146
+ store,
147
+ )
148
+ }
149
+
150
+ public mutableState: AtomIO.MutableAtomToken<Mutable, Json.Serializable>
151
+ public latestUpdateState: AtomIO.AtomToken<typeof this.Update | null>
152
+
153
+ public constructor(
154
+ mutableState: AtomIO.MutableAtomToken<Mutable, Json.Serializable>,
155
+ store: Store = IMPLICIT.STORE,
156
+ ) {
157
+ this.mutableState = mutableState
158
+ this.latestUpdateState = this.initializeState(mutableState, store)
159
+ this.observeCore(mutableState, this.latestUpdateState, store)
160
+ this.updateCore(mutableState, this.latestUpdateState, store)
161
+ const core = target(store)
162
+ core.trackers.set(mutableState.key, this)
163
+ }
164
+ }
@@ -0,0 +1,110 @@
1
+ import type { Json } from "atom.io/json"
2
+
3
+ export interface Transceiver<Signal extends Json.Serializable> {
4
+ do: (update: Signal) => void
5
+ undo: (update: Signal) => void
6
+ subscribe: (key: string, fn: (update: Signal) => void) => () => void
7
+ }
8
+
9
+ export function isTransceiver(
10
+ value: unknown,
11
+ ): value is Transceiver<Json.Serializable> {
12
+ return (
13
+ typeof value === `object` &&
14
+ value !== null &&
15
+ `do` in value &&
16
+ `undo` in value &&
17
+ `subscribe` in value
18
+ )
19
+ }
20
+
21
+ export type TransceiverMode = `playback` | `record` | `transaction`
22
+
23
+ export type Signal<TVR extends Transceiver<any>> = TVR extends Transceiver<
24
+ infer Signal
25
+ >
26
+ ? Signal
27
+ : never
28
+
29
+ /*
30
+ A transceiver may also keep a list of updates that have been applied to it.
31
+ This is useful for undo/redo functionality, especially in the context of
32
+ revising history. It is a good idea to accept a cache limit in your
33
+ constructor, and overwrite old updates. Here's an example of how we
34
+ might set that up:
35
+
36
+ myTransceiver = Transceiver {
37
+ cacheUpdateNumber: number = 27
38
+ cacheIdx: number = 1
39
+ cacheLimit: number = 3
40
+ cache: Array<Update> = [
41
+ 26=add:"x"
42
+ 27=del:"x" (current)
43
+ 25=add:"y"
44
+ ]
45
+ }
46
+
47
+ CONFIRM/NO-OP
48
+ Update `27=del:"x"` is passed to myTransceiver.do:
49
+ - [updateNumber = 27, update = `del:"x"`]
50
+ - updateOffset = updateNumber - cacheUpdateNumber // 0
51
+ - eventOffset < 1 // true (we're validating the past)
52
+ - |eventOffset| < cacheLimit // true (we remember this update)
53
+ - eventIdx = cacheIdx + updateOffset // 1
54
+ - update === cache.get(eventIdx) // true
55
+ - return null // ๐Ÿ‘
56
+
57
+ EXPECTED UPDATE
58
+ Update `28=add:"x"` is passed to myTransceiver.do:
59
+ - [updateNumber = 28, update = `add:"x"`]
60
+ - updateOffset = updateNumber - cacheUpdateNumber // 1
61
+ - eventOffset < 1 // false (we're in the future)
62
+ - eventOffset === 1 // true (we're ready to apply this update)
63
+ - cacheIdx += eventOffset // 2
64
+ - cacheIdx %= cacheLimit // 2
65
+ - cache[cacheIdx] = update // cache = <{ 0 => add:"x" }>
66
+ - return null // ๐Ÿ‘
67
+
68
+ UNEXPECTED UPDATE
69
+ Update `29=del:"x"` is passed to myTransceiver.do:
70
+ - [updateNumber = 29, update = `del:"x"`]
71
+ - updateOffset = updateNumber - cacheUpdateNumber // 2
72
+ - eventOffset < 1 // false (we're in the future)
73
+ - eventOffset === 1 // false (we're NOT ready to apply this update)
74
+ - updateIdx := cacheIdx + updateOffset // 3
75
+ - updateIdx %= cacheLimit // 0
76
+ - cache[updateIdx] = update // cache = <{ 0 => del:"x" }>
77
+ - expectedUpdateNumber = cacheUpdateNumber + 1 // 28
78
+ - return expectedUpdateNumber // ๐Ÿคจ๐Ÿ‘‚
79
+
80
+ SUCCESSFUL ROLLBACK UPDATE
81
+ Update `25=add:"z"` is passed to myTransceiver.do:
82
+ - [updateNumber = 25, update = `add:"z"`]
83
+ - updateOffset = updateNumber - cacheUpdateNumber // -2
84
+ - eventOffset < 1 // true (we're validating the past)
85
+ - |eventOffset| < cacheLimit // true (we remember this update)
86
+ - eventIdx = cacheIdx + updateOffset // -1
87
+ - eventIdx %= cacheLimit // 2
88
+ - update === cache[eventIdx] // false (we're rolling back)
89
+ - done := false
90
+ - update := cache[cacheIdx] // update = `del:"x"`
91
+ - undo(update) // myTransceiver.undo(`del:"x"`)
92
+ - while (!done) {
93
+ - cacheIdx -= 1 // 0, -1
94
+ - cacheIdx %= cacheLimit // 0, 2
95
+ - update = cache[cacheIdx] // update = `add:"y"`, `add:"x"`
96
+ - undo(update) // myTransceiver.undo(`add:"y"`), myTransceiver.undo(`add:"x"`)
97
+ - done = cacheIdx === eventIdx // false, true
98
+ - }
99
+ - do(update) // myTransceiver.do(`add:"z"`)
100
+ - return null // ๐Ÿ‘
101
+
102
+ UNSUCCESSFUL ROLLBACK UPDATE
103
+ Update `24=add:"z"` is passed to myTransceiver.do:
104
+ - [updateNumber = 24, update = `add:"z"`]
105
+ - updateOffset = updateNumber - cacheUpdateNumber // -3
106
+ - eventOffset < 1 // true (we're validating the past)
107
+ - |eventOffset| < cacheLimit // 3 < 3 // false (we don't remember this update)
108
+ - return `OUT_OF_RANGE` // ๐Ÿ˜ตโ€๐Ÿ’ซ๐Ÿ‘‚
109
+
110
+ */
@@ -0,0 +1,68 @@
1
+ import type { StateToken } from "atom.io"
2
+
3
+ import type { Store } from "./store"
4
+ import { IMPLICIT } from "./store"
5
+ import { target } from "./transaction"
6
+
7
+ export type OperationProgress =
8
+ | {
9
+ open: false
10
+ }
11
+ | {
12
+ open: true
13
+ done: Set<string>
14
+ prev: Map<string, any>
15
+ time: number
16
+ token: StateToken<any>
17
+ }
18
+
19
+ export const openOperation = (token: StateToken<any>, store: Store): void => {
20
+ const core = target(store)
21
+ if (core.operation.open) {
22
+ store.config.logger?.error(
23
+ `โŒ failed to setState to "${token.key}" during a setState for "${core.operation.token.key}"`,
24
+ )
25
+ throw Symbol(`violation`)
26
+ }
27
+ core.operation = {
28
+ open: true,
29
+ done: new Set(),
30
+ prev: new Map(store.valueMap),
31
+ time: Date.now(),
32
+ token,
33
+ }
34
+ store.config.logger?.info(
35
+ `โญ• operation start from "${token.key}" in store "${store.config.name}"${
36
+ store.transactionStatus.phase === `idle`
37
+ ? ``
38
+ : ` ${store.transactionStatus.phase} "${store.transactionStatus.key}"`
39
+ }`,
40
+ )
41
+ }
42
+ export const closeOperation = (store: Store): void => {
43
+ const core = target(store)
44
+ core.operation = { open: false }
45
+ store.config.logger?.info(`๐Ÿ”ด operation done`)
46
+ store.subject.operationStatus.next(core.operation)
47
+ }
48
+
49
+ export const isDone = (key: string, store: Store = IMPLICIT.STORE): boolean => {
50
+ const core = target(store)
51
+ if (!core.operation.open) {
52
+ store.config.logger?.warn(
53
+ `isDone called outside of an operation. This is probably a bug.`,
54
+ )
55
+ return true
56
+ }
57
+ return core.operation.done.has(key)
58
+ }
59
+ export const markDone = (key: string, store: Store = IMPLICIT.STORE): void => {
60
+ const core = target(store)
61
+ if (!core.operation.open) {
62
+ store.config.logger?.warn(
63
+ `markDone called outside of an operation. This is probably a bug.`,
64
+ )
65
+ return
66
+ }
67
+ core.operation.done.add(key)
68
+ }