atom.io 0.3.0 → 0.3.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atom.io",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Reactive state graph for React, Preact, and vanilla",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -48,7 +48,7 @@
48
48
  "dependencies": {
49
49
  "fp-ts": "2.14.0",
50
50
  "hamt_plus": "1.0.2",
51
- "rxjs": "7.8.0"
51
+ "rxjs": "7.8.1"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@testing-library/preact": "3.2.3",
package/src/index.ts CHANGED
@@ -7,18 +7,17 @@ import {
7
7
  isAtomDefault,
8
8
  isSelectorDefault,
9
9
  withdraw,
10
- setLogLevel,
11
- useLogger,
12
10
  } from "./internal"
13
11
  import * as __INTERNAL__ from "./internal"
14
12
  import type { Store } from "./internal/store"
15
13
 
16
14
  export * from "./atom"
15
+ export * from "./logger"
17
16
  export * from "./selector"
17
+ export * from "./subscribe"
18
18
  export * from "./timeline"
19
19
  export * from "./transaction"
20
- export * from "./subscribe"
21
- export { __INTERNAL__, setLogLevel, useLogger }
20
+ export { __INTERNAL__ }
22
21
  export type { Serializable } from "~/packages/anvl/src/json"
23
22
 
24
23
  export type AtomToken<_> = {
@@ -62,7 +61,14 @@ export const setState = <T, New extends T>(
62
61
  value: New | ((oldValue: T) => New),
63
62
  store: Store = IMPLICIT.STORE
64
63
  ): void => {
65
- openOperation(store)
64
+ try {
65
+ openOperation(token, store)
66
+ } catch (thrown) {
67
+ if (!(typeof thrown === `symbol`)) {
68
+ throw thrown
69
+ }
70
+ return
71
+ }
66
72
  const state = withdraw(token, store)
67
73
  setState__INTERNAL(state, value, store)
68
74
  closeOperation(store)
@@ -34,7 +34,7 @@ export function atomFamily__INTERNAL<T, K extends Serializable>(
34
34
  (key: K): AtomToken<T> => {
35
35
  const subKey = stringifyJson(key)
36
36
  const family: FamilyMetadata = { key: options.key, subKey }
37
- const fullKey = `${options.key}__${subKey}`
37
+ const fullKey = `${options.key}(${subKey})`
38
38
  const existing = withdraw({ key: fullKey, type: `atom` }, store)
39
39
  const token = existing
40
40
  ? deposit(existing)
@@ -71,7 +71,7 @@ export function readonlySelectorFamily__INTERNAL<T, K extends Serializable>(
71
71
  (key: K): ReadonlyValueToken<T> => {
72
72
  const subKey = stringifyJson(key)
73
73
  const family: FamilyMetadata = { key: options.key, subKey }
74
- const fullKey = `${options.key}__${subKey}`
74
+ const fullKey = `${options.key}(${subKey})`
75
75
  const existing = core.readonlySelectors.get(fullKey)
76
76
  if (existing) {
77
77
  return deposit(existing)
@@ -117,7 +117,7 @@ export function selectorFamily__INTERNAL<T, K extends Serializable>(
117
117
  (key: K): SelectorToken<T> => {
118
118
  const subKey = stringifyJson(key)
119
119
  const family: FamilyMetadata = { key: options.key, subKey }
120
- const fullKey = `${options.key}__${subKey}`
120
+ const fullKey = `${options.key}(${subKey})`
121
121
  const existing = core.selectors.get(fullKey)
122
122
  if (existing) {
123
123
  return deposit(existing)
@@ -96,7 +96,7 @@ export const getState__INTERNAL = <T>(
96
96
  store.config.logger?.info(`>> read "${state.key}"`)
97
97
  return readCachedValue(state.key, store)
98
98
  }
99
- if (`get` in state) {
99
+ if (state.type !== `atom`) {
100
100
  store.config.logger?.info(`-> calc "${state.key}"`)
101
101
  return computeSelectorState(state)
102
102
  }
@@ -2,7 +2,6 @@ export * from "./atom-internal"
2
2
  export * from "./families-internal"
3
3
  export * from "./get"
4
4
  export * from "./is-default"
5
- export * from "./logger"
6
5
  export * from "./operation"
7
6
  export * from "./selector-internal"
8
7
  export * from "./set"
@@ -5,6 +5,7 @@ import type { Atom, ReadonlySelector, Selector } from "."
5
5
  import { target } from "."
6
6
  import type { Store } from "./store"
7
7
  import { IMPLICIT } from "./store"
8
+ import type { StateToken } from ".."
8
9
 
9
10
  export type OperationProgress =
10
11
  | {
@@ -14,21 +15,32 @@ export type OperationProgress =
14
15
  open: true
15
16
  done: Set<string>
16
17
  prev: Hamt<any, string>
18
+ time: number
19
+ token: StateToken<any>
17
20
  }
18
21
 
19
- export const openOperation = (store: Store): void => {
22
+ export const openOperation = (token: StateToken<any>, store: Store): void => {
20
23
  const core = target(store)
24
+ if (core.operation.open) {
25
+ console.warn(core.operation.open)
26
+ store.config.logger?.error(
27
+ `❌ failed to setState to "${token.key}" during a setState for "${core.operation.token.key}"`
28
+ )
29
+ throw Symbol(`violation`)
30
+ }
21
31
  core.operation = {
22
32
  open: true,
23
33
  done: new Set(),
24
34
  prev: store.valueMap,
35
+ time: Date.now(),
36
+ token,
25
37
  }
26
- store.config.logger?.info(`⭕`, `operation start`)
38
+ store.config.logger?.info(`⭕ operation start from "${token.key}"`)
27
39
  }
28
40
  export const closeOperation = (store: Store): void => {
29
41
  const core = target(store)
30
42
  core.operation = { open: false }
31
- store.config.logger?.info(`🔴`, `operation done`)
43
+ store.config.logger?.info(`🔴 operation done`)
32
44
  }
33
45
 
34
46
  export const isDone = (key: string, store: Store = IMPLICIT.STORE): boolean => {
@@ -138,8 +138,9 @@ export const registerSelector = (
138
138
  )
139
139
  } else {
140
140
  store.config.logger?.info(
141
- `🔌 registerSelector "${selectorKey}" <- "${dependency.key}" =`,
142
- dependencyValue
141
+ `🔌 registerSelector "${selectorKey}" <- ( "${dependency.key}" =`,
142
+ dependencyValue,
143
+ `)`
143
144
  )
144
145
  core.selectorGraph = core.selectorGraph.set(selectorKey, dependency.key, {
145
146
  source: dependency.key,
@@ -10,11 +10,10 @@ import type {
10
10
  ReadonlySelector,
11
11
  Selector,
12
12
  TransactionStatus,
13
- Logger,
14
13
  Timeline,
15
14
  TimelineData,
16
15
  } from "."
17
- import type { Transaction, ƒn } from ".."
16
+ import type { Logger, Transaction, ƒn } from ".."
18
17
 
19
18
  export type StoreCore = Pick<
20
19
  Store,
@@ -1,7 +1,9 @@
1
+ /* eslint-disable max-lines */
2
+
1
3
  import HAMT from "hamt_plus"
2
4
 
3
5
  import type { KeyedStateUpdate, TransactionUpdate, Store } from "."
4
- import { IMPLICIT, withdraw } from "."
6
+ import { target, IMPLICIT, withdraw } from "."
5
7
  import { setState } from ".."
6
8
  import type { AtomToken, TimelineOptions, TimelineToken, ƒn } from ".."
7
9
 
@@ -12,8 +14,13 @@ export type Timeline = {
12
14
  prev: () => void
13
15
  }
14
16
 
15
- export type TimelineStateUpdate = KeyedStateUpdate<unknown> & {
16
- type: `state_update`
17
+ export type TimelineAtomUpdate = KeyedStateUpdate<unknown> & {
18
+ type: `atom_update`
19
+ }
20
+ export type TimelineSelectorUpdate = {
21
+ key: string
22
+ type: `selector_update`
23
+ atomUpdates: TimelineAtomUpdate[]
17
24
  }
18
25
  export type TimelineTransactionUpdate = TransactionUpdate<ƒn> & {
19
26
  type: `transaction_update`
@@ -22,13 +29,19 @@ export type TimelineTransactionUpdate = TransactionUpdate<ƒn> & {
22
29
  export type TimelineData = {
23
30
  at: number
24
31
  timeTraveling: boolean
25
- history: (TimelineStateUpdate | TimelineTransactionUpdate)[]
32
+ history: (
33
+ | TimelineAtomUpdate
34
+ | TimelineSelectorUpdate
35
+ | TimelineTransactionUpdate
36
+ )[]
26
37
  }
27
38
 
28
39
  export function timeline__INTERNAL(
29
40
  options: TimelineOptions,
30
41
  store: Store = IMPLICIT.STORE
31
42
  ): TimelineToken {
43
+ let incompleteSelectorTime: number | null = null
44
+ // let selectorAtomUpdates: TimelineAtomUpdate[] = []
32
45
  let incompleteTransactionKey: string | null = null
33
46
  const timelineData: TimelineData = {
34
47
  at: 0,
@@ -39,6 +52,15 @@ export function timeline__INTERNAL(
39
52
  const subscribeToAtom = (token: AtomToken<any>) => {
40
53
  const state = withdraw(token, store)
41
54
  state.subject.subscribe((update) => {
55
+ const storeCurrentSelectorKey =
56
+ store.operation.open && store.operation.token.type === `selector`
57
+ ? store.operation.token.key
58
+ : null
59
+ const storeCurrentSelectorTime =
60
+ store.operation.open && store.operation.token.type === `selector`
61
+ ? store.operation.time
62
+ : null
63
+
42
64
  const storeCurrentTransactionKey =
43
65
  store.transactionStatus.phase === `applying`
44
66
  ? store.transactionStatus.key
@@ -70,6 +92,9 @@ export function timeline__INTERNAL(
70
92
  incompleteTransactionKey = storeCurrentTransactionKey
71
93
  const subscription = currentTransaction.subject.subscribe((update) => {
72
94
  if (timelineData.timeTraveling === false) {
95
+ if (timelineData.at !== timelineData.history.length) {
96
+ timelineData.history.splice(timelineData.at)
97
+ }
73
98
  timelineData.history.push({
74
99
  type: `transaction_update`,
75
100
  ...update,
@@ -82,39 +107,100 @@ export function timeline__INTERNAL(
82
107
  subscription.unsubscribe()
83
108
  incompleteTransactionKey = null
84
109
  store.config.logger?.info(
85
- `⌛ timeline "${options.key}" pushed a transaction_update from "${update.key}"`
110
+ `⌛ timeline "${options.key}" got a transaction_update "${update.key}"`
86
111
  )
87
112
  })
88
113
  }
114
+ } else if (storeCurrentSelectorKey) {
115
+ if (timelineData.timeTraveling === false) {
116
+ if (storeCurrentSelectorTime !== incompleteSelectorTime) {
117
+ const newSelectorUpdate: TimelineSelectorUpdate = {
118
+ type: `selector_update`,
119
+ key: storeCurrentSelectorKey,
120
+ atomUpdates: [],
121
+ }
122
+ newSelectorUpdate.atomUpdates.push({
123
+ key: token.key,
124
+ type: `atom_update`,
125
+ ...update,
126
+ })
127
+ if (timelineData.at !== timelineData.history.length) {
128
+ timelineData.history.splice(timelineData.at)
129
+ }
130
+ timelineData.history.push(newSelectorUpdate)
131
+
132
+ store.config.logger?.info(
133
+ `⌛ timeline "${options.key}" got a selector_update "${storeCurrentSelectorKey}" with`,
134
+ newSelectorUpdate.atomUpdates.map((atomUpdate) => atomUpdate.key)
135
+ )
136
+ timelineData.at = timelineData.history.length
137
+ incompleteSelectorTime = storeCurrentSelectorTime
138
+ } else {
139
+ const latestUpdate = timelineData.history.at(-1)
140
+ if (latestUpdate?.type === `selector_update`) {
141
+ latestUpdate.atomUpdates.push({
142
+ key: token.key,
143
+ type: `atom_update`,
144
+ ...update,
145
+ })
146
+ store.config.logger?.info(
147
+ ` ⌛ timeline "${options.key}" set selector_update "${storeCurrentSelectorKey}" to`,
148
+ latestUpdate?.atomUpdates.map((atomUpdate) => atomUpdate.key)
149
+ )
150
+ }
151
+ }
152
+ }
89
153
  } else {
90
154
  if (timelineData.timeTraveling === false) {
155
+ incompleteSelectorTime = null
156
+ if (timelineData.at !== timelineData.history.length) {
157
+ timelineData.history.splice(timelineData.at)
158
+ }
91
159
  timelineData.history.push({
92
- type: `state_update`,
160
+ type: `atom_update`,
93
161
  key: token.key,
94
162
  oldValue: update.oldValue,
95
163
  newValue: update.newValue,
96
164
  })
97
165
  store.config.logger?.info(
98
- `⌛ timeline "${options.key}" pushed a state_update to "${token.key}"`
166
+ `⌛ timeline "${options.key}" got a state_update to "${token.key}"`
99
167
  )
100
168
  timelineData.at = timelineData.history.length
101
169
  }
102
170
  }
103
171
  })
104
172
  }
105
-
173
+ const core = target(store)
106
174
  for (const tokenOrFamily of options.atoms) {
175
+ const timelineKey = core.timelineAtoms.getRelatedId(tokenOrFamily.key)
176
+ if (timelineKey) {
177
+ store.config.logger?.error(
178
+ `❌ Failed to add atom "${tokenOrFamily.key}" to timeline "${options.key}" because it belongs to timeline "${timelineKey}"`
179
+ )
180
+ continue
181
+ }
107
182
  if (tokenOrFamily.type === `atom_family`) {
108
183
  const family = tokenOrFamily
109
184
  family.subject.subscribe((token) => subscribeToAtom(token))
110
185
  } else {
111
186
  const token = tokenOrFamily
187
+ if (`family` in token && token.family) {
188
+ const familyTimelineKey = core.timelineAtoms.getRelatedId(
189
+ token.family.key
190
+ )
191
+ if (familyTimelineKey) {
192
+ store.config.logger?.error(
193
+ `❌ Failed to add atom "${token.key}" to timeline "${options.key}" because its family "${token.family.key}" belongs to timeline "${familyTimelineKey}"`
194
+ )
195
+ continue
196
+ }
197
+ }
112
198
  subscribeToAtom(token)
113
199
  }
200
+ core.timelineAtoms = core.timelineAtoms.set(tokenOrFamily.key, options.key)
114
201
  }
115
202
 
116
203
  store.timelineStore = HAMT.set(options.key, timelineData, store.timelineStore)
117
-
118
204
  return {
119
205
  key: options.key,
120
206
  type: `timeline`,
@@ -125,27 +211,29 @@ export const redo__INTERNAL = (
125
211
  token: TimelineToken,
126
212
  store: Store = IMPLICIT.STORE
127
213
  ): void => {
214
+ store.config.logger?.info(`⏩ redo "${token.key}"`)
128
215
  const timelineData = store.timelineStore.get(token.key)
129
216
  if (!timelineData) {
130
217
  store.config.logger?.error(
131
- `Tried to redo on timeline "${token.key}" has not been initialized.`
218
+ `Failed to redo on timeline "${token.key}". This timeline has not been initialized.`
132
219
  )
133
220
  return
134
221
  }
135
222
  if (timelineData.at === timelineData.history.length) {
136
223
  store.config.logger?.warn(
137
- `Tried to redo on timeline "${token.key}" but there is nothing to redo.`
224
+ `Failed to redo at the end of timeline "${token.key}". There is nothing to redo.`
138
225
  )
139
226
  return
140
227
  }
141
228
  timelineData.timeTraveling = true
142
229
  const update = timelineData.history[timelineData.at]
143
230
  switch (update.type) {
144
- case `state_update`: {
231
+ case `atom_update`: {
145
232
  const { key, newValue } = update
146
233
  setState({ key, type: `atom` }, newValue)
147
234
  break
148
235
  }
236
+ case `selector_update`:
149
237
  case `transaction_update`: {
150
238
  for (const atomUpdate of update.atomUpdates) {
151
239
  const { key, newValue } = atomUpdate
@@ -156,34 +244,40 @@ export const redo__INTERNAL = (
156
244
  }
157
245
  ++timelineData.at
158
246
  timelineData.timeTraveling = false
247
+ store.config.logger?.info(
248
+ `⏹️ "${token.key}" is now at ${timelineData.at} / ${timelineData.history.length}`
249
+ )
159
250
  }
160
251
 
161
252
  export const undo__INTERNAL = (
162
253
  token: TimelineToken,
163
254
  store: Store = IMPLICIT.STORE
164
255
  ): void => {
256
+ store.config.logger?.info(`⏪ undo "${token.key}"`)
165
257
  const timelineData = store.timelineStore.get(token.key)
166
258
  if (!timelineData) {
167
259
  store.config.logger?.error(
168
- `Tried to undo on timeline "${token.key}" has not been initialized.`
260
+ `Failed to undo on timeline "${token.key}". This timeline has not been initialized.`
169
261
  )
170
262
  return
171
263
  }
172
264
  if (timelineData.at === 0) {
173
265
  store.config.logger?.warn(
174
- `Tried to undo on timeline "${token.key}" but there is nothing to undo.`
266
+ `Failed to undo at the beginning of timeline "${token.key}". There is nothing to undo.`
175
267
  )
176
268
  return
177
269
  }
178
270
  timelineData.timeTraveling = true
271
+
179
272
  --timelineData.at
180
273
  const update = timelineData.history[timelineData.at]
181
274
  switch (update.type) {
182
- case `state_update`: {
275
+ case `atom_update`: {
183
276
  const { key, oldValue } = update
184
277
  setState({ key, type: `atom` }, oldValue)
185
278
  break
186
279
  }
280
+ case `selector_update`:
187
281
  case `transaction_update`: {
188
282
  for (const atomUpdate of update.atomUpdates) {
189
283
  const { key, oldValue } = atomUpdate
@@ -193,4 +287,7 @@ export const undo__INTERNAL = (
193
287
  }
194
288
  }
195
289
  timelineData.timeTraveling = false
290
+ store.config.logger?.info(
291
+ `⏹️ "${token.key}" is now at ${timelineData.at} / ${timelineData.history.length}`
292
+ )
196
293
  }
@@ -1,7 +1,7 @@
1
1
  import { doNothing } from "~/packages/anvl/src/function"
2
2
 
3
- import type { Store } from "./store"
4
- import { IMPLICIT } from "./store"
3
+ import type { Store } from "./internal/store"
4
+ import { IMPLICIT } from "./internal/store"
5
5
 
6
6
  export type Logger = Pick<Console, `error` | `info` | `warn`>
7
7
  export const LOG_LEVELS: ReadonlyArray<keyof Logger> = [
package/src/selector.ts CHANGED
@@ -4,12 +4,12 @@ import type { Serializable } from "~/packages/anvl/src/json"
4
4
 
5
5
  import type { ReadonlyValueToken, SelectorToken } from "."
6
6
  import { selectorFamily__INTERNAL, selector__INTERNAL } from "./internal"
7
- import type { ReadonlyTransactors, Transactors } from "./transaction"
7
+ import type { Read, Write } from "./transaction"
8
8
 
9
9
  export type SelectorOptions<T> = {
10
10
  key: string
11
- get: (readonlyTransactors: ReadonlyTransactors) => T
12
- set: (transactors: Transactors, newValue: T) => void
11
+ get: Read<() => T>
12
+ set: Write<(newValue: T) => void>
13
13
  }
14
14
  export type ReadonlySelectorOptions<T> = Omit<SelectorOptions<T>, `set`>
15
15
 
@@ -25,8 +25,8 @@ export function selector<T>(
25
25
 
26
26
  export type SelectorFamilyOptions<T, K extends Serializable> = {
27
27
  key: string
28
- get: (key: K) => (readonlyTransactors: ReadonlyTransactors) => T
29
- set: (key: K) => (transactors: Transactors, newValue: T) => void
28
+ get: (key: K) => Read<() => T>
29
+ set: (key: K) => Write<(newValue: T) => void>
30
30
  }
31
31
  export type ReadonlySelectorFamilyOptions<T, K extends Serializable> = Omit<
32
32
  SelectorFamilyOptions<T, K>,
package/src/subscribe.ts CHANGED
@@ -14,7 +14,7 @@ export const subscribe = <T>(
14
14
  const subscription = state.subject.subscribe(handleUpdate)
15
15
  store.config.logger?.info(`👀 subscribe to "${state.key}"`)
16
16
  const dependencySubscriptions =
17
- `get` in state ? subscribeToRootAtoms(state, store) : null
17
+ state.type !== `atom` ? subscribeToRootAtoms(state, store) : null
18
18
 
19
19
  const unsubscribe =
20
20
  dependencySubscriptions === null
package/src/timeline.ts CHANGED
@@ -1,10 +1,5 @@
1
- import HAMT from "hamt_plus"
2
- import type * as Rx from "rxjs"
3
-
4
- import type { AtomFamily, AtomToken, ƒn } from "."
5
- import { setState } from "."
6
- import type { Store, KeyedStateUpdate, TransactionUpdate } from "./internal"
7
- import { target, IMPLICIT, withdraw } from "./internal"
1
+ import type { AtomFamily, AtomToken } from "."
2
+ import { IMPLICIT } from "./internal"
8
3
  import {
9
4
  redo__INTERNAL,
10
5
  timeline__INTERNAL,
@@ -12,14 +12,19 @@ export type Transactors = {
12
12
  }
13
13
  export type ReadonlyTransactors = Pick<Transactors, `get`>
14
14
 
15
- export type Action<ƒ extends ƒn> = (
15
+ export type Read<ƒ extends ƒn> = (
16
+ transactors: ReadonlyTransactors,
17
+ ...parameters: Parameters<ƒ>
18
+ ) => ReturnType<ƒ>
19
+
20
+ export type Write<ƒ extends ƒn> = (
16
21
  transactors: Transactors,
17
22
  ...parameters: Parameters<ƒ>
18
23
  ) => ReturnType<ƒ>
19
24
 
20
25
  export type TransactionOptions<ƒ extends ƒn> = {
21
26
  key: string
22
- do: Action<ƒ>
27
+ do: Write<ƒ>
23
28
  }
24
29
 
25
30
  export type Transaction<ƒ extends ƒn> = {