atom.io 0.39.1 → 0.40.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.
@@ -1,4 +1,4 @@
1
- import type { ReadableToken } from "atom.io"
1
+ import type { AtomUpdateEvent, ReadableToken, StateCreationEvent } from "atom.io"
2
2
 
3
3
  import type { Store } from "./store"
4
4
  import { isChildStore } from "./transaction/is-root-store"
@@ -14,6 +14,7 @@ export type OpenOperation<R extends ReadableToken<any> = ReadableToken<any>> = {
14
14
  done: Set<string>
15
15
  prev: Map<string, any>
16
16
  timestamp: number
17
+ subEvents: (AtomUpdateEvent<any> | StateCreationEvent<any>)[]
17
18
  }
18
19
 
19
20
  export function openOperation(
@@ -36,6 +37,7 @@ export function openOperation(
36
37
  prev: new Map(),
37
38
  timestamp: Date.now(),
38
39
  token,
40
+ subEvents: [],
39
41
  }
40
42
  store.logger.info(
41
43
  `⭕`,
@@ -1,12 +1,12 @@
1
- import type { AtomUpdateEvent, StateCreationEvent, StateUpdate } from "atom.io"
1
+ import type {
2
+ AtomUpdateEvent,
3
+ StateCreationEvent,
4
+ StateUpdate,
5
+ TimelineEvent,
6
+ } from "atom.io"
2
7
 
3
- import {
4
- type MutableAtom,
5
- newest,
6
- type Subject,
7
- type WritableFamily,
8
- type WritableState,
9
- } from ".."
8
+ import type { MutableAtom, Subject, WritableFamily, WritableState } from ".."
9
+ import { newest } from ".."
10
10
  import { hasRole } from "../atom"
11
11
  import { readOrComputeValue } from "../get-state"
12
12
  import type { Transceiver } from "../mutable"
@@ -27,13 +27,15 @@ export function dispatchOrDeferStateUpdate<T>(
27
27
  const token = deposit(state)
28
28
  if (stateIsNewlyCreated && family) {
29
29
  state.subject.next({ newValue })
30
- const stateCreationEvent: StateCreationEvent<any> = {
30
+ const stateCreationEvent: StateCreationEvent<any> & TimelineEvent<any> = {
31
+ write: true,
31
32
  type: `state_creation`,
32
33
  subType: `writable`,
33
34
  token,
34
35
  timestamp: Date.now(),
35
36
  value: newValue,
36
37
  }
38
+ target.operation.subEvents.push(stateCreationEvent)
37
39
  const familySubject = family.subject as Subject<StateCreationEvent<any>>
38
40
  familySubject.next(stateCreationEvent)
39
41
  const innerTarget = newest(target)
@@ -75,7 +77,7 @@ export function dispatchOrDeferStateUpdate<T>(
75
77
  `is now (`,
76
78
  newValue,
77
79
  `) subscribers:`,
78
- subject.subscribers,
80
+ subject.subscribers.keys(),
79
81
  )
80
82
  break
81
83
  case `atom`:
@@ -90,7 +92,7 @@ export function dispatchOrDeferStateUpdate<T>(
90
92
  `->`,
91
93
  newValue,
92
94
  `) subscribers:`,
93
- subject.subscribers,
95
+ subject.subscribers.keys(),
94
96
  )
95
97
  }
96
98
  subject.next(update)
@@ -4,13 +4,16 @@ import type {
4
4
  AtomUpdateEvent,
5
5
  StateCreationEvent,
6
6
  StateDisposalEvent,
7
+ StateUpdate,
7
8
  TimelineEvent,
8
9
  TimelineManageable,
9
10
  TimelineOptions,
11
+ TimelineSelectorUpdateEvent,
10
12
  TimelineToken,
11
13
  TransactionOutcomeEvent,
12
14
  TransactionSubEvent,
13
15
  TransactionToken,
16
+ WritablePureSelectorToken,
14
17
  } from "atom.io"
15
18
 
16
19
  import { reduceReference } from "../get-state/reduce-reference"
@@ -24,10 +27,6 @@ export type Timeline<ManagedAtom extends TimelineManageable> = {
24
27
  type: `timeline`
25
28
  key: string
26
29
  at: number
27
- shouldCapture?: (
28
- update: TimelineEvent<ManagedAtom>,
29
- timeline: Timeline<ManagedAtom>,
30
- ) => boolean
31
30
  timeTraveling: `into_future` | `into_past` | null
32
31
  history: TimelineEvent<ManagedAtom>[]
33
32
  selectorTime: number | null
@@ -46,7 +45,6 @@ export function createTimeline<ManagedAtom extends TimelineManageable>(
46
45
  type: `timeline`,
47
46
  key: options.key,
48
47
  at: 0,
49
-
50
48
  timeTraveling: null,
51
49
  selectorTime: null,
52
50
  transactionKey: null,
@@ -56,9 +54,7 @@ export function createTimeline<ManagedAtom extends TimelineManageable>(
56
54
  subject: new Subject(),
57
55
  subscriptions: new Map(),
58
56
  }
59
- if (options.shouldCapture) {
60
- tl.shouldCapture = options.shouldCapture
61
- }
57
+
62
58
  const timelineKey = options.key
63
59
  const target = newest(store)
64
60
  for (const initialTopic of options.scope) {
@@ -182,93 +178,32 @@ function addAtomToTimeline(
182
178
  if (txUpdateInProgress) {
183
179
  joinTransaction(store, tl, txUpdateInProgress)
184
180
  } else if (currentSelectorToken && currentSelectorTime) {
185
- let latestUpdate: TimelineEvent<any> | undefined = tl.history.at(-1)
186
-
187
- if (currentSelectorTime !== tl.selectorTime) {
188
- latestUpdate = {
189
- type: `selector_update`,
190
- timestamp: currentSelectorTime,
191
- // key: currentSelectorKey,
192
- token: currentSelectorToken,
193
- atomUpdates: [],
194
- }
195
- latestUpdate.atomUpdates.push({
196
- type: `atom_update`,
197
- token: atomToken,
198
- update,
199
- timestamp: Date.now(), // 👺 use store operation
200
- })
201
- if (tl.at !== tl.history.length) {
202
- tl.history.splice(tl.at)
203
- }
204
-
205
- tl.history.push(latestUpdate)
206
-
207
- store.logger.info(
208
- `⌛`,
209
- `timeline`,
210
- tl.key,
211
- `got a selector_update "${currentSelectorToken.key}" with`,
212
- latestUpdate.atomUpdates.map(
213
- (atomUpdate) => atomUpdate.token.key,
214
- ),
215
- )
216
-
217
- tl.at = tl.history.length
218
- tl.selectorTime = currentSelectorTime
219
- } else {
220
- if (latestUpdate?.type === `selector_update`) {
221
- latestUpdate.atomUpdates.push({
222
- type: `atom_update`,
223
- token: atomToken,
224
- update,
225
- timestamp: Date.now(), // 👺 use store operation
226
- })
227
- store.logger.info(
228
- `⌛`,
229
- `timeline`,
230
- tl.key,
231
- `set selector_update "${currentSelectorToken.key}" to`,
232
- latestUpdate?.atomUpdates.map(
233
- (atomUpdate) => atomUpdate.token.key,
234
- ),
235
- )
236
- }
237
- }
238
- if (latestUpdate) {
239
- const willCaptureSelectorUpdate =
240
- tl.shouldCapture?.(latestUpdate, tl) ?? true
241
- if (willCaptureSelectorUpdate) {
242
- tl.subject.next(latestUpdate)
243
- } else {
244
- tl.history.pop()
245
- tl.at = tl.history.length
246
- }
247
- }
181
+ buildSelectorUpdate(
182
+ store,
183
+ tl,
184
+ atomToken,
185
+ update,
186
+ currentSelectorToken,
187
+ currentSelectorTime,
188
+ )
248
189
  } else {
249
190
  const timestamp = Date.now()
250
191
  tl.selectorTime = null
251
- if (tl.at !== tl.history.length) {
252
- tl.history.splice(tl.at)
253
- }
254
- const atomUpdate: AtomUpdateEvent<any> = {
192
+
193
+ const atomUpdate: AtomUpdateEvent<any> & TimelineEvent<any> = {
194
+ write: true,
255
195
  type: `atom_update`,
256
196
  token: deposit(atom),
257
197
  update,
258
198
  timestamp,
259
199
  }
260
- const willCapture = tl.shouldCapture?.(atomUpdate, tl) ?? true
261
200
  store.logger.info(
262
201
  `⌛`,
263
202
  `timeline`,
264
203
  tl.key,
265
204
  `got an atom_update to "${atom.key}"`,
266
205
  )
267
- if (willCapture) {
268
- tl.history.push(atomUpdate)
269
- tl.at = tl.history.length
270
- tl.subject.next(atomUpdate)
271
- }
206
+ addToHistory(tl, atomUpdate)
272
207
  }
273
208
  }
274
209
  },
@@ -322,10 +257,6 @@ function joinTransaction(
322
257
  unsubscribe()
323
258
  tl.transactionKey = null
324
259
  if (tl.timeTraveling === null && currentTxInstanceId) {
325
- if (tl.at !== tl.history.length) {
326
- tl.history.splice(tl.at)
327
- }
328
-
329
260
  // biome-ignore lint/style/noNonNullAssertion: we are in the context of this timeline
330
261
  const timelineTopics = store.timelineTopics.getRelatedKeys(tl.key)!
331
262
 
@@ -334,25 +265,99 @@ function joinTransaction(
334
265
  timelineTopics,
335
266
  )
336
267
 
337
- const timelineTransactionUpdate: TransactionOutcomeEvent<
338
- TransactionToken<any>
339
- > = {
268
+ const timelineTransactionUpdate: TimelineEvent<any> &
269
+ TransactionOutcomeEvent<TransactionToken<any>> = {
270
+ write: true,
340
271
  ...transactionUpdate,
341
272
  subEvents: subEventsFiltered,
342
273
  }
343
- const willCapture =
344
- tl.shouldCapture?.(timelineTransactionUpdate, tl) ?? true
345
- if (willCapture) {
346
- tl.history.push(timelineTransactionUpdate)
347
- tl.at = tl.history.length
348
- tl.subject.next(timelineTransactionUpdate)
349
- }
274
+
275
+ addToHistory(tl, timelineTransactionUpdate)
350
276
  }
351
277
  },
352
278
  )
353
279
  }
354
280
  }
355
281
 
282
+ function buildSelectorUpdate(
283
+ store: Store,
284
+ tl: Timeline<any>,
285
+ atomToken: AtomToken<any>,
286
+ eventOrUpdate: StateCreationEvent<any> | StateUpdate<any>,
287
+ currentSelectorToken: WritablePureSelectorToken<any>,
288
+ currentSelectorTime: number,
289
+ ) {
290
+ let latestUpdate: TimelineEvent<any> | undefined = tl.history.at(-1)
291
+ if (currentSelectorTime !== tl.selectorTime) {
292
+ const selectorUpdate: TimelineEvent<any> & TimelineSelectorUpdateEvent<any> =
293
+ (latestUpdate = {
294
+ write: true,
295
+ type: `selector_update`,
296
+ timestamp: currentSelectorTime,
297
+ token: currentSelectorToken,
298
+ subEvents: [],
299
+ })
300
+ if (`type` in eventOrUpdate) {
301
+ latestUpdate.subEvents.push(eventOrUpdate)
302
+ } else {
303
+ latestUpdate.subEvents.push({
304
+ type: `atom_update`,
305
+ token: atomToken,
306
+ update: eventOrUpdate,
307
+ timestamp: Date.now(), // 👺 use store operation
308
+ })
309
+ }
310
+
311
+ addToHistory(tl, latestUpdate)
312
+ tl.selectorTime = currentSelectorTime
313
+
314
+ store.logger.info(
315
+ `⌛`,
316
+ `timeline`,
317
+ tl.key,
318
+ `got a selector_update "${currentSelectorToken.key}" with`,
319
+ latestUpdate.subEvents.map((event) => event.token.key),
320
+ )
321
+
322
+ const operation = store.operation
323
+ const unsub = store.on.operationClose.subscribe(
324
+ `timeline:${tl.key} (needs to gather nested selector creations)`,
325
+ () => {
326
+ unsub()
327
+ if (operation.open) {
328
+ selectorUpdate.subEvents = [
329
+ ...operation.subEvents,
330
+ ...selectorUpdate.subEvents,
331
+ ]
332
+ }
333
+ },
334
+ )
335
+ } else {
336
+ if (latestUpdate?.type === `selector_update`) {
337
+ if (`type` in eventOrUpdate) {
338
+ latestUpdate.subEvents.push(eventOrUpdate)
339
+ } else {
340
+ latestUpdate.subEvents.push({
341
+ type: `atom_update`,
342
+ token: atomToken,
343
+ update: eventOrUpdate,
344
+ timestamp: Date.now(), // 👺 use store operation
345
+ })
346
+ }
347
+ store.logger.info(
348
+ `⌛`,
349
+ `timeline`,
350
+ tl.key,
351
+ `set selector_update "${currentSelectorToken.key}" to`,
352
+ latestUpdate?.subEvents.map((event) => event.token.key),
353
+ )
354
+ }
355
+ }
356
+ if (latestUpdate) {
357
+ tl.subject.next(latestUpdate)
358
+ }
359
+ }
360
+
356
361
  function filterTransactionSubEvents(
357
362
  updates: TransactionSubEvent[],
358
363
  timelineTopics: Set<string>,
@@ -402,6 +407,16 @@ function handleStateLifecycleEvent(
402
407
  event: StateCreationEvent<any> | StateDisposalEvent<any>,
403
408
  tl: Timeline<any>,
404
409
  ): void {
410
+ const currentSelectorToken =
411
+ store.operation.open &&
412
+ store.operation.token.type === `writable_pure_selector`
413
+ ? store.operation.token
414
+ : null
415
+ const currentSelectorTime =
416
+ store.operation.open &&
417
+ store.operation.token.type === `writable_pure_selector`
418
+ ? store.operation.timestamp
419
+ : null
405
420
  if (!tl.timeTraveling) {
406
421
  const target = newest(store)
407
422
  if (isChildStore(target)) {
@@ -410,10 +425,21 @@ function handleStateLifecycleEvent(
410
425
  const txUpdateInProgress = target.on.transactionApplying.state
411
426
  if (txUpdateInProgress) {
412
427
  joinTransaction(store, tl, txUpdateInProgress.update)
428
+ } else if (
429
+ currentSelectorToken &&
430
+ currentSelectorTime &&
431
+ event.type === `state_creation`
432
+ ) {
433
+ buildSelectorUpdate(
434
+ store,
435
+ tl,
436
+ event.token,
437
+ event,
438
+ currentSelectorToken,
439
+ currentSelectorTime,
440
+ )
413
441
  } else {
414
- tl.history.push(event)
415
- tl.at = tl.history.length
416
- tl.subject.next(event)
442
+ addToHistory(tl, event)
417
443
  }
418
444
  }
419
445
  }
@@ -427,3 +453,12 @@ function handleStateLifecycleEvent(
427
453
  break
428
454
  }
429
455
  }
456
+
457
+ function addToHistory(tl: Timeline<any>, event: TimelineEvent<any>): void {
458
+ if (tl.at !== tl.history.length) {
459
+ tl.history.splice(tl.at)
460
+ }
461
+ tl.history.push(event)
462
+ tl.at = tl.history.length
463
+ tl.subject.next(event)
464
+ }
@@ -1,4 +1,4 @@
1
- import type { TimelineToken } from "atom.io"
1
+ import type { TimelineEvent, TimelineToken } from "atom.io"
2
2
 
3
3
  import {
4
4
  ingestAtomUpdateEvent,
@@ -30,6 +30,7 @@ export const timeTravel = (
30
30
  )
31
31
  return
32
32
  }
33
+
33
34
  if (
34
35
  (action === `redo` && timelineData.at === timelineData.history.length) ||
35
36
  (action === `undo` && timelineData.at === 0)
@@ -46,46 +47,56 @@ export const timeTravel = (
46
47
  }
47
48
 
48
49
  timelineData.timeTraveling = action === `redo` ? `into_future` : `into_past`
49
- if (action === `undo`) {
50
- --timelineData.at
50
+ let nextIndex = timelineData.at
51
+ let events: TimelineEvent<any>[]
52
+ switch (action) {
53
+ case `undo`:
54
+ --nextIndex
55
+ while (nextIndex !== 0 && timelineData.history[nextIndex].write !== true) {
56
+ --nextIndex
57
+ }
58
+ events = timelineData.history.slice(nextIndex, timelineData.at).reverse()
59
+
60
+ break
61
+ case `redo`:
62
+ ++nextIndex
63
+ events = timelineData.history.slice(timelineData.at, nextIndex)
51
64
  }
65
+ timelineData.at = nextIndex
52
66
 
53
- const event = timelineData.history[timelineData.at]
54
67
  const applying = action === `redo` ? `newValue` : `oldValue`
55
68
 
56
- switch (event.type) {
57
- case `atom_update`: {
58
- ingestAtomUpdateEvent(store, event, applying)
59
- break
60
- }
61
- case `selector_update`: {
62
- ingestSelectorUpdateEvent(store, event, applying)
63
- break
64
- }
65
- case `transaction_outcome`: {
66
- ingestTransactionOutcomeEvent(store, event, applying)
67
- break
68
- }
69
- case `state_creation`: {
70
- ingestCreationEvent(store, event, applying)
71
- break
69
+ for (const event of events) {
70
+ switch (event.type) {
71
+ case `atom_update`: {
72
+ ingestAtomUpdateEvent(store, event, applying)
73
+ break
74
+ }
75
+ case `selector_update`: {
76
+ ingestSelectorUpdateEvent(store, event, applying)
77
+ break
78
+ }
79
+ case `transaction_outcome`: {
80
+ ingestTransactionOutcomeEvent(store, event, applying)
81
+ break
82
+ }
83
+ case `state_creation`: {
84
+ ingestCreationEvent(store, event, applying)
85
+ break
86
+ }
87
+ case `state_disposal`: {
88
+ ingestDisposalEvent(store, event, applying)
89
+ break
90
+ }
91
+ case `molecule_creation`:
92
+ case `molecule_disposal`:
72
93
  }
73
- case `state_disposal`: {
74
- ingestDisposalEvent(store, event, applying)
75
- break
76
- }
77
- case `molecule_creation`:
78
- case `molecule_disposal`:
79
- }
80
-
81
- if (action === `redo`) {
82
- ++timelineData.at
83
94
  }
84
95
 
85
96
  timelineData.subject.next(action)
86
97
  timelineData.timeTraveling = null
87
98
  store.logger.info(
88
- `⏹️`,
99
+ `⏸️`,
89
100
  `timeline`,
90
101
  token.key,
91
102
  `"${token.key}" is now at ${timelineData.at} / ${timelineData.history.length}`,
@@ -34,7 +34,7 @@ export function applyTransaction<F extends Fn>(
34
34
  `🛄`,
35
35
  `transaction`,
36
36
  child.transactionMeta.update.token.key,
37
- `Applying transaction with ${updates.length} updates:`,
37
+ `applying ${updates.length} subEvents:`,
38
38
  updates,
39
39
  )
40
40
 
@@ -55,7 +55,7 @@ export function applyTransaction<F extends Fn>(
55
55
  `🛬`,
56
56
  `transaction`,
57
57
  child.transactionMeta.update.token.key,
58
- `Finished applying transaction.`,
58
+ `applied`,
59
59
  )
60
60
  } else if (isChildStore(parent)) {
61
61
  parent.transactionMeta.update.subEvents.push(child.transactionMeta.update)
@@ -97,7 +97,7 @@ export const buildTransaction = (
97
97
  `🛫`,
98
98
  `transaction`,
99
99
  token.key,
100
- `Building transaction with params:`,
100
+ `building with params:`,
101
101
  params,
102
102
  )
103
103
  return child
@@ -23,10 +23,13 @@ export type AtomUpdateEvent<A extends AtomToken<any>> = {
23
23
  timestamp: number
24
24
  }
25
25
 
26
+ export type SelectorUpdateSubEvent<A extends AtomToken<any>> =
27
+ | AtomUpdateEvent<A>
28
+ | StateCreationEvent<any>
26
29
  export type TimelineSelectorUpdateEvent<A extends TimelineManageable> = {
27
30
  type: `selector_update`
28
31
  token: SelectorToken<any>
29
- atomUpdates: AtomUpdateEvent<AtomOnly<A>>[]
32
+ subEvents: SelectorUpdateSubEvent<AtomOnly<A>>[]
30
33
  timestamp: number
31
34
  }
32
35
 
@@ -110,7 +113,9 @@ export type TransactionOutcomeEvent<T extends TransactionToken<any>> = {
110
113
  output: ReturnType<TokenType<T>>
111
114
  }
112
115
 
113
- export type TimelineEvent<ManagedAtom extends TimelineManageable> =
116
+ export type TimelineEvent<ManagedAtom extends TimelineManageable> = {
117
+ write?: true
118
+ } & (
114
119
  | AtomUpdateEvent<AtomOnly<ManagedAtom>>
115
120
  | MoleculeCreationEvent
116
121
  | MoleculeDisposalEvent
@@ -118,3 +123,4 @@ export type TimelineEvent<ManagedAtom extends TimelineManageable> =
118
123
  | StateDisposalEvent<AtomOnly<ManagedAtom>>
119
124
  | TimelineSelectorUpdateEvent<ManagedAtom>
120
125
  | TransactionOutcomeEvent<TransactionToken<any>>
126
+ )
@@ -37,7 +37,7 @@ const LOGGER_ICON_DICTIONARY = {
37
37
  "⏭️": `Transaction redo`,
38
38
  "⏮️": `Transaction undo`,
39
39
  "⏳": `Timeline event partially captured`,
40
- "⏹️": `Time-travel complete`,
40
+ "⏸️": `Time-travel complete`,
41
41
  // Problems
42
42
  "💣": `Dangerous action likely to cause bad errors down the line`,
43
43
  "❗": `Dangerous action unless in development mode`,
@@ -1,7 +1,6 @@
1
- import type { Timeline } from "atom.io/internal"
2
1
  import { createTimeline, IMPLICIT, timeTravel } from "atom.io/internal"
3
2
 
4
- import type { AtomFamilyToken, AtomToken, TimelineEvent, TimelineToken } from "."
3
+ import type { AtomFamilyToken, AtomToken, TimelineToken } from "."
5
4
 
6
5
  export type TimelineManageable = AtomFamilyToken<any, any> | AtomToken<any>
7
6
  export type AtomOnly<M extends TimelineManageable> = M extends AtomFamilyToken<
@@ -33,11 +32,6 @@ export type TimelineOptions<ManagedAtom extends TimelineManageable> = {
33
32
  key: string
34
33
  /** The managed atoms (and families of atoms) to record */
35
34
  scope: ManagedAtom[]
36
- /** A function that determines whether a given update should be recorded */
37
- shouldCapture?: (
38
- update: TimelineEvent<ManagedAtom>,
39
- timeline: Timeline<TimelineManageable>,
40
- ) => boolean
41
35
  }
42
36
 
43
37
  /**
@@ -192,18 +192,23 @@ export const TimelineUpdateFC: React.FC<{
192
192
  }
193
193
  })
194
194
  ) : timelineUpdate.type === `selector_update` ? (
195
- timelineUpdate.atomUpdates
195
+ timelineUpdate.subEvents
196
196
  .filter(
197
197
  (atomUpdateEvent) => !atomUpdateEvent.token.key.startsWith(`👁‍🗨`),
198
198
  )
199
- .map((atomUpdate, index) => {
200
- return (
201
- <article.AtomUpdate
202
- key={`${timelineUpdate.token.key}:${index}:${atomUpdate.token.key}`}
203
- serialNumber={index}
204
- atomUpdate={atomUpdate}
205
- />
206
- )
199
+ .map((event, index) => {
200
+ switch (event.type) {
201
+ case `atom_update`:
202
+ return (
203
+ <article.AtomUpdate
204
+ key={`${timelineUpdate.token.key}:${index}:${event.token.key}`}
205
+ serialNumber={index}
206
+ atomUpdate={event}
207
+ />
208
+ )
209
+ case `state_creation`:
210
+ return null
211
+ }
207
212
  })
208
213
  ) : timelineUpdate.type === `atom_update` ? (
209
214
  <article.AtomUpdate