atom.io 0.3.0 → 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 (55) hide show
  1. package/README.md +14 -8
  2. package/dist/index.d.ts +117 -62
  3. package/dist/index.js +577 -287
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +574 -285
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +16 -6
  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 +14 -8
  22. package/src/internal/atom-internal.ts +10 -5
  23. package/src/internal/families-internal.ts +7 -7
  24. package/src/internal/get.ts +9 -9
  25. package/src/internal/index.ts +2 -1
  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 +14 -3
  32. package/src/internal/selector-internal.ts +37 -15
  33. package/src/internal/store.ts +35 -6
  34. package/src/internal/time-travel-internal.ts +89 -0
  35. package/src/internal/timeline-internal.ts +110 -93
  36. package/src/internal/transaction-internal.ts +14 -5
  37. package/src/{internal/logger.ts → logger.ts} +2 -2
  38. package/src/react/index.ts +28 -46
  39. package/src/react-devtools/AtomIODevtools.tsx +107 -0
  40. package/src/react-devtools/StateEditor.tsx +73 -0
  41. package/src/react-devtools/TokenList.tsx +49 -0
  42. package/src/react-devtools/devtools.scss +130 -0
  43. package/src/react-devtools/index.ts +1 -0
  44. package/src/react-explorer/AtomIOExplorer.tsx +208 -0
  45. package/src/react-explorer/explorer-effects.ts +20 -0
  46. package/src/react-explorer/explorer-states.ts +224 -0
  47. package/src/react-explorer/index.ts +23 -0
  48. package/src/react-explorer/space-states.ts +73 -0
  49. package/src/react-explorer/view-states.ts +43 -0
  50. package/src/selector.ts +11 -11
  51. package/src/subscribe.ts +3 -3
  52. package/src/timeline.ts +3 -12
  53. package/src/transaction.ts +9 -4
  54. package/src/web-effects/index.ts +1 -0
  55. package/src/web-effects/storage.ts +30 -0
@@ -0,0 +1,130 @@
1
+ main.atom_io_devtools {
2
+ --fg-color: #eee;
3
+ --bg-color: #111;
4
+ @media (prefers-color-scheme: light) {
5
+ --fg-color: #222;
6
+ --bg-color: #ccc;
7
+ }
8
+ box-sizing: border-box;
9
+ color: var(--fg-color);
10
+ background-color: var(--bg-color);
11
+ border: 2px solid var(--fg-color);
12
+ position: fixed;
13
+ right: 0;
14
+ bottom: 0;
15
+ height: 100%;
16
+ display: flex;
17
+ flex-flow: column;
18
+ max-height: 800px;
19
+ width: 100%;
20
+ max-width: 460px;
21
+ overflow-y: scroll;
22
+ padding: 5px;
23
+ header {
24
+ display: flex;
25
+ justify-content: space-between;
26
+ h1 {
27
+ font-size: inherit;
28
+ margin: 0;
29
+ }
30
+ }
31
+ main {
32
+ overflow-y: scroll;
33
+ flex-grow: 1;
34
+ section {
35
+ margin-top: 30px;
36
+ h2 {
37
+ font-size: inherit;
38
+ margin: 0;
39
+ }
40
+ .node {
41
+ border: 1px solid var(--fg-color);
42
+ padding: 5px;
43
+ margin: 5px;
44
+ overflow-x: scroll;
45
+ }
46
+ }
47
+ }
48
+ footer {
49
+ display: flex;
50
+ justify-content: flex-end;
51
+ button {
52
+ cursor: pointer;
53
+ background: none;
54
+ border: none;
55
+ padding: none;
56
+ position: absolute;
57
+ right: 0;
58
+ bottom: 0;
59
+ }
60
+ }
61
+
62
+ .json_editor {
63
+ input {
64
+ font-size: 20px;
65
+ font-family: theia;
66
+ border: none;
67
+ border-bottom: 1px solid;
68
+ background: none;
69
+ &:disabled {
70
+ border: none;
71
+ }
72
+ }
73
+ button {
74
+ background: none;
75
+ margin-left: auto;
76
+ color: #777;
77
+ border: none;
78
+ font-family: theia;
79
+ font-size: 14px;
80
+ margin: none;
81
+ padding: 4px;
82
+ padding-bottom: 6px;
83
+ cursor: pointer;
84
+ &:hover {
85
+ color: #333;
86
+ background-color: #aaa;
87
+ }
88
+ }
89
+ select {
90
+ font-family: theia;
91
+ font-size: 14px;
92
+ background: none;
93
+ border: none;
94
+ color: #777;
95
+ @media (prefers-color-scheme: light) {
96
+ color: #999;
97
+ }
98
+ }
99
+ .json_editor_unofficial {
100
+ background-color: #777;
101
+ button {
102
+ color: #333;
103
+ }
104
+ }
105
+ .json_editor_missing {
106
+ background-color: #f055;
107
+ }
108
+ .json_editor_key {
109
+ input {
110
+ color: #999;
111
+ @media (prefers-color-scheme: light) {
112
+ color: #777;
113
+ }
114
+ }
115
+ }
116
+ .json_editor_object {
117
+ border-left: 2px solid #333;
118
+ padding-left: 20px;
119
+ @media (prefers-color-scheme: light) {
120
+ border-color: #ccc;
121
+ }
122
+ .json_editor_properties {
123
+ > * {
124
+ border-bottom: 2px solid #333;
125
+ margin-bottom: 2px;
126
+ }
127
+ }
128
+ }
129
+ }
130
+ }
@@ -0,0 +1 @@
1
+ export * from "./AtomIODevtools"
@@ -0,0 +1,208 @@
1
+ import type { FC, ReactNode } from "react"
2
+ import { useEffect } from "react"
3
+
4
+ import { Link, MemoryRouter, useLocation } from "react-router-dom"
5
+
6
+ import type { composeStoreHooks } from "~/packages/atom.io/src/react"
7
+ import { ErrorBoundary } from "~/packages/hamr/src/react-error-boundary"
8
+ import type { WC } from "~/packages/hamr/src/react-json-editor"
9
+
10
+ import { attachExplorerState } from "./explorer-states"
11
+ import { setState } from ".."
12
+ import { runTransaction } from "../transaction"
13
+
14
+ export type ExplorerOptions = {
15
+ key: string
16
+ Components?: {
17
+ SpaceWrapper: WC
18
+ CloseSpaceButton: FC<{ onClick: () => void }>
19
+ }
20
+ storeHooks: ReturnType<typeof composeStoreHooks>
21
+ }
22
+
23
+ const DEFAULT_COMPONENTS: ExplorerOptions[`Components`] = {
24
+ SpaceWrapper: ({ children }) => <div>{children}</div>,
25
+ CloseSpaceButton: ({ onClick }) => <button onClick={onClick}>X</button>,
26
+ }
27
+
28
+ export const composeExplorer = ({
29
+ key,
30
+ Components,
31
+ storeHooks: { useO, useIO },
32
+ }: ExplorerOptions): ReturnType<typeof attachExplorerState> & {
33
+ Explorer: FC<{ children: ReactNode }>
34
+ useSetTitle: (viewId: string) => void
35
+ } => {
36
+ const { SpaceWrapper, CloseSpaceButton } = {
37
+ ...DEFAULT_COMPONENTS,
38
+ ...Components,
39
+ }
40
+
41
+ const state = attachExplorerState(key)
42
+
43
+ const {
44
+ addSpace,
45
+ addView,
46
+ allViewsState,
47
+ findSpaceFocusedViewState,
48
+ findSpaceLayoutNode,
49
+ findSpaceViewsState,
50
+ findViewFocusedState,
51
+ findViewState,
52
+ removeSpace,
53
+ removeView,
54
+ spaceLayoutState,
55
+ viewIndexState,
56
+ } = state
57
+
58
+ const View: FC<{
59
+ children: ReactNode
60
+ viewId: string
61
+ }> = ({ children, viewId }) => {
62
+ const location = useLocation()
63
+ const viewState = findViewState(viewId)
64
+ const [view, setView] = useIO(viewState)
65
+ useEffect(() => {
66
+ setView((view) => ({ ...view, location }))
67
+ }, [location.key])
68
+ return (
69
+ <div className="view">
70
+ <header>
71
+ <h1>{view.title}</h1>
72
+ <CloseSpaceButton onClick={() => runTransaction(removeView)(viewId)} />
73
+ </header>
74
+ <main>{children}</main>
75
+ <footer>
76
+ <nav>
77
+ {location.pathname.split(`/`).map((pathPiece, idx, array) =>
78
+ pathPiece === `` && idx === 1 ? null : (
79
+ <Link
80
+ to={array.slice(0, idx + 1).join(`/`)}
81
+ key={`${pathPiece}_${viewId}`}
82
+ >
83
+ {idx === 0 ? `home` : pathPiece}/
84
+ </Link>
85
+ )
86
+ )}
87
+ </nav>
88
+ </footer>
89
+ </div>
90
+ )
91
+ }
92
+
93
+ const Tab: FC<{ viewId: string; spaceId: string }> = ({ viewId, spaceId }) => {
94
+ const view = useO(findViewState(viewId))
95
+ const [spaceFocusedView, setSpaceFocusedView] = useIO(
96
+ findSpaceFocusedViewState(spaceId)
97
+ )
98
+ return (
99
+ <div
100
+ className={`tab ${spaceFocusedView === viewId ? `focused` : ``}`}
101
+ onClick={() => setSpaceFocusedView(viewId)}
102
+ >
103
+ {view.title}
104
+ </div>
105
+ )
106
+ }
107
+
108
+ const TabBar: FC<{
109
+ spaceId: string
110
+ viewIds: string[]
111
+ }> = ({ spaceId, viewIds }) => {
112
+ return (
113
+ <nav className="tab-bar">
114
+ {viewIds.map((viewId) => (
115
+ <Tab key={viewId} viewId={viewId} spaceId={spaceId} />
116
+ ))}
117
+ </nav>
118
+ )
119
+ }
120
+
121
+ const Space: FC<{
122
+ children: ReactNode
123
+ focusedViewId: string
124
+ spaceId: string
125
+ viewIds: string[]
126
+ }> = ({ children, focusedViewId, spaceId, viewIds }) => {
127
+ const view = useO(findViewState(focusedViewId))
128
+ return (
129
+ <div className="space">
130
+ <ErrorBoundary>
131
+ <MemoryRouter
132
+ initialEntries={view.location ? [view.location.pathname] : []}
133
+ >
134
+ <TabBar spaceId={spaceId} viewIds={viewIds} />
135
+ <View viewId={focusedViewId}>{children}</View>
136
+ </MemoryRouter>
137
+ </ErrorBoundary>
138
+ </div>
139
+ )
140
+ }
141
+
142
+ const Spaces: FC<{ children: ReactNode; spaceId?: string }> = ({
143
+ children,
144
+ spaceId = `root`,
145
+ }) => {
146
+ const spaceLayout = useO(findSpaceLayoutNode(spaceId))
147
+ const viewIds = useO(findSpaceViewsState(spaceId))
148
+ const focusedViewId = useO(findSpaceFocusedViewState(spaceId))
149
+ console.log({ spaceLayout, viewIds, focusedViewId })
150
+ return (
151
+ <div className="spaces">
152
+ {spaceLayout.childSpaceIds.length === 0 ? (
153
+ focusedViewId ? (
154
+ <Space
155
+ focusedViewId={focusedViewId}
156
+ spaceId={spaceId}
157
+ viewIds={viewIds}
158
+ >
159
+ {children}
160
+ </Space>
161
+ ) : (
162
+ `no view`
163
+ )
164
+ ) : (
165
+ spaceLayout.childSpaceIds.map((childSpaceId) => (
166
+ <Spaces key={childSpaceId} spaceId={childSpaceId}>
167
+ {children}
168
+ </Spaces>
169
+ ))
170
+ )}
171
+ <button onClick={() => runTransaction(addView)({ spaceId })}>
172
+ + View
173
+ </button>
174
+ <button onClick={() => runTransaction(addSpace)({ parentId: spaceId })}>
175
+ + Space
176
+ </button>
177
+ </div>
178
+ )
179
+ }
180
+
181
+ const Explorer: FC<{ children: ReactNode }> = ({ children }) => {
182
+ return <Spaces>{children}</Spaces>
183
+ }
184
+
185
+ const useSetTitle = (title: string): void => {
186
+ let location: ReturnType<typeof useLocation>
187
+ try {
188
+ location = useLocation()
189
+ } catch (thrown) {
190
+ console.warn(
191
+ `Failed to set title to "${title}"; useSetTitle must be called within the children of Explorer`
192
+ )
193
+ return
194
+ }
195
+ const views = useO(allViewsState)
196
+ const locationView = views.find(
197
+ ([, view]) => view.location.key === location.key
198
+ )
199
+ const viewId = locationView?.[0] ?? null
200
+ useEffect(() => {
201
+ if (viewId) {
202
+ setState(findViewState(viewId), (v) => ({ ...v, title }))
203
+ }
204
+ }, [viewId])
205
+ }
206
+
207
+ return { Explorer, useSetTitle, ...state }
208
+ }
@@ -0,0 +1,20 @@
1
+ import { isString } from "fp-ts/lib/string"
2
+
3
+ import { isArray } from "~/packages/anvl/src/array"
4
+ import { parseJson, stringifyJson } from "~/packages/anvl/src/json"
5
+
6
+ import { persistAtom } from "../web-effects"
7
+
8
+ export const persistStringSetAtom = persistAtom<Set<string>>(localStorage)({
9
+ stringify: (set) => stringifyJson([...set]),
10
+ parse: (string) => {
11
+ try {
12
+ const json = parseJson(string)
13
+ const array = isArray(isString)(json) ? json : []
14
+ return new Set(array)
15
+ } catch (thrown) {
16
+ console.error(`Error parsing spaceIndexState from localStorage`)
17
+ return new Set()
18
+ }
19
+ },
20
+ })
@@ -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
+ })