atom.io 0.3.1 → 0.4.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 (54) hide show
  1. package/README.md +11 -3
  2. package/dist/index.d.ts +83 -30
  3. package/dist/index.js +427 -230
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +427 -230
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +15 -5
  8. package/react/dist/index.d.ts +12 -17
  9. package/react/dist/index.js +25 -34
  10. package/react/dist/index.js.map +1 -1
  11. package/react/dist/index.mjs +21 -34
  12. package/react/dist/index.mjs.map +1 -1
  13. package/react-devtools/dist/index.css +26 -0
  14. package/react-devtools/dist/index.css.map +1 -0
  15. package/react-devtools/dist/index.d.ts +15 -0
  16. package/react-devtools/dist/index.js +1579 -0
  17. package/react-devtools/dist/index.js.map +1 -0
  18. package/react-devtools/dist/index.mjs +1551 -0
  19. package/react-devtools/dist/index.mjs.map +1 -0
  20. package/react-devtools/package.json +15 -0
  21. package/src/index.ts +3 -3
  22. package/src/internal/atom-internal.ts +10 -5
  23. package/src/internal/families-internal.ts +4 -4
  24. package/src/internal/get.ts +8 -8
  25. package/src/internal/index.ts +2 -0
  26. package/src/internal/meta/attach-meta.ts +17 -0
  27. package/src/internal/meta/index.ts +4 -0
  28. package/src/internal/meta/meta-state.ts +135 -0
  29. package/src/internal/meta/meta-timelines.ts +1 -0
  30. package/src/internal/meta/meta-transactions.ts +1 -0
  31. package/src/internal/operation.ts +0 -1
  32. package/src/internal/selector-internal.ts +34 -13
  33. package/src/internal/store.ts +35 -5
  34. package/src/internal/time-travel-internal.ts +89 -0
  35. package/src/internal/timeline-internal.ts +23 -103
  36. package/src/internal/transaction-internal.ts +14 -5
  37. package/src/react/index.ts +28 -46
  38. package/src/react-devtools/AtomIODevtools.tsx +107 -0
  39. package/src/react-devtools/StateEditor.tsx +73 -0
  40. package/src/react-devtools/TokenList.tsx +49 -0
  41. package/src/react-devtools/devtools.scss +130 -0
  42. package/src/react-devtools/index.ts +1 -0
  43. package/src/react-explorer/AtomIOExplorer.tsx +208 -0
  44. package/src/react-explorer/explorer-effects.ts +20 -0
  45. package/src/react-explorer/explorer-states.ts +224 -0
  46. package/src/react-explorer/index.ts +23 -0
  47. package/src/react-explorer/space-states.ts +73 -0
  48. package/src/react-explorer/view-states.ts +43 -0
  49. package/src/selector.ts +6 -6
  50. package/src/subscribe.ts +2 -2
  51. package/src/timeline.ts +1 -5
  52. package/src/transaction.ts +2 -2
  53. package/src/web-effects/index.ts +1 -0
  54. package/src/web-effects/storage.ts +30 -0
@@ -0,0 +1,224 @@
1
+ import { lastOf } from "~/packages/anvl/src/array"
2
+ import { now } from "~/packages/anvl/src/id"
3
+ import { Join } from "~/packages/anvl/src/join"
4
+ import type { Entries } from "~/packages/anvl/src/object"
5
+ import { cannotExist } from "~/packages/anvl/src/refinement"
6
+
7
+ import { addToIndex, removeFromIndex } from "."
8
+ import {
9
+ makeSpaceLayoutNodeFamily,
10
+ makeSpaceFamily,
11
+ makeSpaceIndex,
12
+ makeSpaceLayoutState,
13
+ } from "./space-states"
14
+ import type { View } from "./view-states"
15
+ import {
16
+ makeViewFocusedFamily,
17
+ makeViewFamily,
18
+ makeViewIndex,
19
+ } from "./view-states"
20
+ import type {
21
+ AtomFamily,
22
+ AtomToken,
23
+ ReadonlySelectorFamily,
24
+ ReadonlySelectorToken,
25
+ SelectorFamily,
26
+ TransactionToken,
27
+ Write,
28
+ } from ".."
29
+ import { selectorFamily, selector, transaction, atom } from ".."
30
+ import { persistAtom } from "../web-effects"
31
+
32
+ export const makeViewsPerSpaceState = (
33
+ key: string
34
+ ): AtomToken<Join<null, `viewId`, `spaceId`>> =>
35
+ atom<Join<null, `viewId`, `spaceId`>>({
36
+ key: `${key}:views_per_space`,
37
+ default: new Join({ relationType: `1:n` }),
38
+ effects: [
39
+ persistAtom<Join<null, `viewId`, `spaceId`>>(localStorage)({
40
+ stringify: (index) => JSON.stringify(index.toJSON()),
41
+ parse: (json) =>
42
+ Join.fromJSON(JSON.parse(json), cannotExist, `viewId`, `spaceId`),
43
+ })(`${key}:views_per_space`),
44
+ ],
45
+ })
46
+
47
+ export const makeSpaceViewsFamily = (
48
+ key: string,
49
+ viewsPerSpaceState: AtomToken<Join>
50
+ ): ReadonlySelectorFamily<string[], string> =>
51
+ selectorFamily<string[], string>({
52
+ key: `${key}:space_views`,
53
+ get:
54
+ (spaceId) =>
55
+ ({ get }) => {
56
+ const join = get(viewsPerSpaceState)
57
+ const viewIds = join.getRelatedIds(spaceId)
58
+ return viewIds
59
+ },
60
+ })
61
+
62
+ export const makeSpaceFocusedViewFamily = (
63
+ key: string,
64
+ findSpaceViewsState: ReadonlySelectorFamily<string[], string>,
65
+ findViewFocusedState: AtomFamily<number, string>
66
+ ): SelectorFamily<string | null, string> =>
67
+ selectorFamily<string | null, string>({
68
+ key: `${key}:space_focused_view`,
69
+ get:
70
+ (spaceKey) =>
71
+ ({ get }) => {
72
+ const views = get(findSpaceViewsState(spaceKey))
73
+ const viewsLastFocused = views.map((viewKey): [string, number] => [
74
+ viewKey,
75
+ get(findViewFocusedState(viewKey)),
76
+ ])
77
+ const lastFocused = lastOf(viewsLastFocused.sort((a, b) => b[1] - a[1]))
78
+ return lastFocused ? lastFocused[0] : null
79
+ },
80
+ set:
81
+ (spaceKey) =>
82
+ ({ get, set }, viewKey) => {
83
+ if (viewKey === null) {
84
+ return
85
+ }
86
+ const views = get(findSpaceViewsState(spaceKey))
87
+ if (views.includes(viewKey)) {
88
+ set(findViewFocusedState(viewKey), Date.now())
89
+ } else {
90
+ console.warn(`View ${viewKey} not found in space ${spaceKey}`)
91
+ }
92
+ },
93
+ })
94
+
95
+ type AddViewOptions = { spaceId?: string; path?: string }
96
+ type SplitSpaceOptions = { parentId?: string }
97
+
98
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
99
+ export const attachExplorerState = (key: string) => {
100
+ const findSpaceState = makeSpaceFamily(key)
101
+ const findViewState = makeViewFamily(key)
102
+ const findViewFocusedState = makeViewFocusedFamily(key)
103
+ const spaceIndexState = makeSpaceIndex(key)
104
+ const spaceLayoutState = makeSpaceLayoutState(key)
105
+ const viewIndexState = makeViewIndex(key)
106
+ const viewsPerSpaceState = makeViewsPerSpaceState(key)
107
+
108
+ const findSpaceLayoutNode = makeSpaceLayoutNodeFamily(key, spaceLayoutState)
109
+ const findSpaceViewsState = makeSpaceViewsFamily(key, viewsPerSpaceState)
110
+ const findSpaceFocusedViewState = makeSpaceFocusedViewFamily(
111
+ key,
112
+ findSpaceViewsState,
113
+ findViewFocusedState
114
+ )
115
+
116
+ const allViewsState = selector<Entries<string, View>>({
117
+ key: `${key}:all_views`,
118
+ get: ({ get }) => {
119
+ const viewIndex = get(viewIndexState)
120
+ return [...viewIndex].map((id) => [id, get(findViewState(id))])
121
+ },
122
+ })
123
+
124
+ const writeOperationAddSpace: Write<
125
+ (options?: SplitSpaceOptions) => string
126
+ > = (transactors, { parentId = `root` } = {}) => {
127
+ const { set } = transactors
128
+ const key = `s-${now()}`
129
+ addToIndex(transactors, { indexAtom: spaceIndexState, id: key })
130
+ set(spaceLayoutState, (current) =>
131
+ current.set({ parent: `parent:${parentId}`, child: key }, { size: 1 })
132
+ )
133
+ set(findSpaceState(key), 1)
134
+ return key
135
+ }
136
+
137
+ const writeOperationRemoveSpace: Write<(id: string) => void> = (
138
+ transactors,
139
+ id
140
+ ) => {
141
+ removeFromIndex(transactors, { indexAtom: spaceIndexState, id })
142
+ transactors.set(findSpaceState(id), null)
143
+ }
144
+
145
+ const writeOperationAddView: Write<(options?: AddViewOptions) => void> = (
146
+ transactors,
147
+ { spaceId: maybeSpaceId, path } = {}
148
+ ) => {
149
+ const { get, set } = transactors
150
+ const id = `v-${now()}`
151
+
152
+ addToIndex(transactors, { indexAtom: viewIndexState, id })
153
+ set(
154
+ findViewState(id),
155
+ (current): View => ({
156
+ ...current,
157
+ location: {
158
+ ...current.location,
159
+ pathname: path ?? `/`,
160
+ },
161
+ })
162
+ )
163
+ const spaceId =
164
+ maybeSpaceId ??
165
+ lastOf([...get(spaceIndexState)]) ??
166
+ writeOperationAddSpace(transactors)
167
+ set(findViewFocusedState(id), Date.now())
168
+
169
+ set(viewsPerSpaceState, (current) => current.set({ spaceId, viewId: id }))
170
+ set(findViewFocusedState(id), Date.now())
171
+ }
172
+
173
+ const writeOperationRemoveView: Write<(viewId: string) => void> = (
174
+ transactors,
175
+ viewId
176
+ ) => {
177
+ const { set } = transactors
178
+ removeFromIndex(transactors, { indexAtom: viewIndexState, id: viewId })
179
+ set(viewsPerSpaceState, (current) => current.remove({ viewId }))
180
+ set(findViewState(viewId), null)
181
+ }
182
+
183
+ const addView = transaction<(options?: AddViewOptions) => void>({
184
+ key: `${key}:add_view`,
185
+ do: writeOperationAddView,
186
+ })
187
+
188
+ const removeView = transaction({
189
+ key: `${key}:remove_view`,
190
+ do: writeOperationRemoveView,
191
+ })
192
+
193
+ const addSpace = transaction({
194
+ key: `${key}:add_space`,
195
+ do: writeOperationAddSpace,
196
+ })
197
+
198
+ const removeSpace = transaction({
199
+ key: `${key}:remove_space`,
200
+ do: writeOperationRemoveSpace,
201
+ })
202
+
203
+ return {
204
+ addSpace,
205
+ addView,
206
+ allViewsState,
207
+ findSpaceLayoutNode,
208
+ findSpaceFocusedViewState,
209
+ findSpaceState,
210
+ findSpaceViewsState,
211
+ findViewState,
212
+ findViewFocusedState,
213
+ removeSpace,
214
+ removeView,
215
+ spaceIndexState,
216
+ spaceLayoutState,
217
+ viewIndexState,
218
+ viewsPerSpaceState,
219
+ writeOperationAddSpace,
220
+ writeOperationAddView,
221
+ writeOperationRemoveSpace,
222
+ writeOperationRemoveView,
223
+ }
224
+ }
@@ -0,0 +1,23 @@
1
+ import type { AtomToken, Write } from ".."
2
+
3
+ export * from "./AtomIOExplorer"
4
+
5
+ export type AtomicIndexOptions = {
6
+ indexAtom: AtomToken<Set<string>>
7
+ id: string
8
+ }
9
+
10
+ export const addToIndex: Write<(options: AtomicIndexOptions) => void> = (
11
+ { set },
12
+ { indexAtom, id }
13
+ ): void => set(indexAtom, (currentSet) => new Set(currentSet).add(id))
14
+
15
+ export const removeFromIndex: Write<(options: AtomicIndexOptions) => void> = (
16
+ { set },
17
+ { indexAtom, id }
18
+ ): void =>
19
+ set(indexAtom, (currentSet) => {
20
+ const newSet = new Set(currentSet)
21
+ newSet.delete(id)
22
+ return newSet
23
+ })
@@ -0,0 +1,73 @@
1
+ import { isNumber } from "fp-ts/lib/number"
2
+
3
+ import { Join } from "~/packages/anvl/src/join"
4
+ import { parseJson, stringifyJson } from "~/packages/anvl/src/json"
5
+ import { hasExactProperties } from "~/packages/anvl/src/object"
6
+
7
+ import { persistStringSetAtom } from "./explorer-effects"
8
+ import type { AtomToken, ReadonlySelectorFamily, SelectorFamily } from ".."
9
+ import { selectorFamily } from ".."
10
+ import type { AtomFamily } from "../atom"
11
+ import { atom, atomFamily } from "../atom"
12
+ import { lazyLocalStorageEffect, persistAtom } from "../web-effects"
13
+
14
+ export const makeSpaceIndex = (key: string): AtomToken<Set<string>> =>
15
+ atom<Set<string>>({
16
+ key: `${key}:space_index`,
17
+ default: new Set([`root`]),
18
+ effects: [persistStringSetAtom(`${key}:space_index`)],
19
+ })
20
+
21
+ export const makeSpaceLayoutState = (
22
+ key: string
23
+ ): AtomToken<Join<{ size: number }, `parent`, `child`>> =>
24
+ atom({
25
+ key: `${key}:space_layout`,
26
+ default: new Join({ relationType: `1:n` }),
27
+ effects: [
28
+ persistAtom<Join<{ size: number }, `parent`, `child`>>(localStorage)({
29
+ stringify: (join) => stringifyJson(join.toJSON()),
30
+ parse: (string) => {
31
+ try {
32
+ const json = parseJson(string)
33
+ const join = Join.fromJSON(
34
+ json,
35
+ hasExactProperties({ size: isNumber }),
36
+ `parent`,
37
+ `child`
38
+ )
39
+ return join
40
+ } catch (thrown) {
41
+ console.error(`Error parsing spaceLayoutState from localStorage`)
42
+ return new Join({ relationType: `1:n` })
43
+ }
44
+ },
45
+ })(`${key}:space_layout`),
46
+ ],
47
+ })
48
+
49
+ export const makeSpaceLayoutNodeFamily = (
50
+ key: string,
51
+ spaceLayoutState: AtomToken<Join<{ size: number }>>
52
+ ): ReadonlySelectorFamily<{ childSpaceIds: string[]; size: number }, string> =>
53
+ selectorFamily<{ childSpaceIds: string[]; size: number }, string>({
54
+ key: `${key}:explorer_space`,
55
+ get:
56
+ (me) =>
57
+ ({ get }) => {
58
+ const join = get(spaceLayoutState)
59
+ const myFollowers = join.getRelatedIds(`parent:${me}`)
60
+ const myLeader = join.getRelatedId(me)
61
+ const { size } = myLeader
62
+ ? join.getContent(myLeader, me) ?? { size: NaN }
63
+ : { size: NaN }
64
+ return { childSpaceIds: myFollowers, size }
65
+ },
66
+ })
67
+
68
+ export const makeSpaceFamily = (key: string): AtomFamily<number, string> =>
69
+ atomFamily<number, string>({
70
+ key: `${key}:space`,
71
+ default: 1,
72
+ effects: (subKey) => [lazyLocalStorageEffect(`${key}:${subKey}`)],
73
+ })
@@ -0,0 +1,43 @@
1
+ import type { Location } from "react-router-dom"
2
+
3
+ import { key } from "~/packages/anvl/src/object"
4
+
5
+ import { persistStringSetAtom } from "./explorer-effects"
6
+ import type { AtomToken } from ".."
7
+ import type { AtomFamily } from "../atom"
8
+ import { atom, atomFamily } from "../atom"
9
+ import { lazyLocalStorageEffect } from "../web-effects"
10
+
11
+ export type View = {
12
+ title: string
13
+ location: Omit<Location, `state`>
14
+ }
15
+
16
+ export const makeViewFamily = (key: string): AtomFamily<View, string> =>
17
+ atomFamily<View, string>({
18
+ key: `${key}:view`,
19
+ default: {
20
+ title: ``,
21
+ location: {
22
+ pathname: ``,
23
+ search: ``,
24
+ hash: ``,
25
+ key: ``,
26
+ },
27
+ },
28
+ effects: (subKey) => [lazyLocalStorageEffect(`${key}:${subKey}`)],
29
+ })
30
+
31
+ export const makeViewIndex = (key: string): AtomToken<Set<string>> =>
32
+ atom<Set<string>>({
33
+ key: `${key}:view_index`,
34
+ default: new Set(),
35
+ effects: [persistStringSetAtom(`${key}:view_index`)],
36
+ })
37
+
38
+ export const makeViewFocusedFamily = (key: string): AtomFamily<number, string> =>
39
+ atomFamily<number, string>({
40
+ key: `${key}:view_focused`,
41
+ default: 0,
42
+ effects: (subKey) => [lazyLocalStorageEffect(`${key}:${subKey}`)],
43
+ })
package/src/selector.ts CHANGED
@@ -2,7 +2,7 @@ import type * as Rx from "rxjs"
2
2
 
3
3
  import type { Serializable } from "~/packages/anvl/src/json"
4
4
 
5
- import type { ReadonlyValueToken, SelectorToken } from "."
5
+ import type { ReadonlySelectorToken, SelectorToken } from "."
6
6
  import { selectorFamily__INTERNAL, selector__INTERNAL } from "./internal"
7
7
  import type { Read, Write } from "./transaction"
8
8
 
@@ -13,13 +13,13 @@ export type SelectorOptions<T> = {
13
13
  }
14
14
  export type ReadonlySelectorOptions<T> = Omit<SelectorOptions<T>, `set`>
15
15
 
16
- export function selector<T>(options: SelectorOptions<T>): SelectorToken<T>
17
16
  export function selector<T>(
18
17
  options: ReadonlySelectorOptions<T>
19
- ): ReadonlyValueToken<T>
18
+ ): ReadonlySelectorToken<T>
19
+ export function selector<T>(options: SelectorOptions<T>): SelectorToken<T>
20
20
  export function selector<T>(
21
21
  options: ReadonlySelectorOptions<T> | SelectorOptions<T>
22
- ): ReadonlyValueToken<T> | SelectorToken<T> {
22
+ ): ReadonlySelectorToken<T> | SelectorToken<T> {
23
23
  return selector__INTERNAL(options)
24
24
  }
25
25
 
@@ -43,10 +43,10 @@ export type SelectorFamily<T, K extends Serializable = Serializable> = ((
43
43
 
44
44
  export type ReadonlySelectorFamily<T, K extends Serializable = Serializable> = ((
45
45
  key: K
46
- ) => ReadonlyValueToken<T>) & {
46
+ ) => ReadonlySelectorToken<T>) & {
47
47
  key: string
48
48
  type: `readonly_selector_family`
49
- subject: Rx.Subject<ReadonlyValueToken<T>>
49
+ subject: Rx.Subject<ReadonlySelectorToken<T>>
50
50
  }
51
51
 
52
52
  export function selectorFamily<T, K extends Serializable>(
package/src/subscribe.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ReadonlyValueToken, StateToken, TransactionToken, ƒn } from "."
1
+ import type { ReadonlySelectorToken, StateToken, TransactionToken, ƒn } from "."
2
2
  import type { Store, TransactionUpdate } from "./internal"
3
3
  import { IMPLICIT, subscribeToRootAtoms, withdraw } from "./internal"
4
4
 
@@ -6,7 +6,7 @@ export type StateUpdate<T> = { newValue: T; oldValue: T }
6
6
  export type UpdateHandler<T> = (update: StateUpdate<T>) => void
7
7
 
8
8
  export const subscribe = <T>(
9
- token: ReadonlyValueToken<T> | StateToken<T>,
9
+ token: ReadonlySelectorToken<T> | StateToken<T>,
10
10
  handleUpdate: UpdateHandler<T>,
11
11
  store: Store = IMPLICIT.STORE
12
12
  ): (() => void) => {
package/src/timeline.ts CHANGED
@@ -1,10 +1,6 @@
1
1
  import type { AtomFamily, AtomToken } from "."
2
2
  import { IMPLICIT } from "./internal"
3
- import {
4
- redo__INTERNAL,
5
- timeline__INTERNAL,
6
- undo__INTERNAL,
7
- } from "./internal/timeline-internal"
3
+ import { redo__INTERNAL, timeline__INTERNAL, undo__INTERNAL } from "./internal/"
8
4
 
9
5
  export type TimelineToken = {
10
6
  key: string
@@ -1,13 +1,13 @@
1
1
  import type * as Rx from "rxjs"
2
2
 
3
- import type { ReadonlyValueToken, StateToken, TransactionToken } from "."
3
+ import type { ReadonlySelectorToken, StateToken, TransactionToken } from "."
4
4
  import type { Store, TransactionUpdate } from "./internal"
5
5
  import { IMPLICIT, transaction__INTERNAL, withdraw } from "./internal"
6
6
 
7
7
  export type ƒn = (...parameters: any[]) => any
8
8
 
9
9
  export type Transactors = {
10
- get: <S>(state: ReadonlyValueToken<S> | StateToken<S>) => S
10
+ get: <S>(state: ReadonlySelectorToken<S> | StateToken<S>) => S
11
11
  set: <S>(state: StateToken<S>, newValue: S | ((oldValue: S) => S)) => void
12
12
  }
13
13
  export type ReadonlyTransactors = Pick<Transactors, `get`>
@@ -0,0 +1 @@
1
+ export * from "./storage"
@@ -0,0 +1,30 @@
1
+ import type { Json } from "~/packages/anvl/src/json"
2
+
3
+ import type { AtomEffect } from "../index"
4
+
5
+ export type StringInterface<T> = {
6
+ stringify: (t: T) => string
7
+ parse: (s: string) => T
8
+ }
9
+
10
+ export const persistAtom =
11
+ <T>(storage: Storage) =>
12
+ ({ stringify, parse }: StringInterface<T>) =>
13
+ (key: string): AtomEffect<T> =>
14
+ ({ setSelf, onSet }) => {
15
+ const savedValue = storage.getItem(key)
16
+
17
+ if (savedValue != null) setSelf(parse(savedValue))
18
+
19
+ onSet(({ newValue }) => {
20
+ if (newValue == null) {
21
+ storage.removeItem(key)
22
+ return
23
+ }
24
+ storage.setItem(key, stringify(newValue))
25
+ })
26
+ }
27
+
28
+ export const lazyLocalStorageEffect: <J extends Json>(
29
+ key: string
30
+ ) => AtomEffect<J> = persistAtom(localStorage)(JSON)