atom.io 0.24.5 → 0.24.7

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,11 +1,13 @@
1
1
  import type {
2
2
  AtomFamilyToken,
3
+ AtomToken,
3
4
  FamilyMetadata,
4
5
  Flat,
5
6
  Func,
6
7
  MoleculeConstructor,
7
8
  MoleculeCreation,
8
9
  MoleculeDisposal,
10
+ MoleculeFamilyToken,
9
11
  ReadableToken,
10
12
  StateCreation,
11
13
  StateDisposal,
@@ -15,15 +17,16 @@ import type {
15
17
  TimelineToken,
16
18
  TimelineUpdate,
17
19
  TokenType,
20
+ TransactionToken,
18
21
  TransactionUpdate,
22
+ TransactionUpdateContent,
19
23
  } from "atom.io"
20
24
  import { stringifyJson } from "atom.io/json"
21
25
 
22
26
  import { newest } from "../lineage"
23
- import { getUpdateToken, isMutable } from "../mutable"
27
+ import { getUpdateToken } from "../mutable"
24
28
  import { type Store, withdraw } from "../store"
25
29
  import { Subject } from "../subject"
26
- import { addAtomToTimeline } from "./add-atom-to-timeline"
27
30
 
28
31
  export type TimelineAtomUpdate<ManagedAtom extends TimelineManageable> = Flat<
29
32
  StateUpdate<TokenType<ManagedAtom>> & {
@@ -100,56 +103,27 @@ export function createTimeline<ManagedAtom extends TimelineManageable>(
100
103
  }
101
104
  const timelineKey = options.key
102
105
  const target = newest(store)
103
- for (const tokenOrFamily of options.scope) {
104
- let atomKey = tokenOrFamily.key
105
- switch (tokenOrFamily.type) {
106
- case `atom_family`:
107
- case `mutable_atom_family`:
108
- {
109
- const familyToken: AtomFamilyToken<any> = tokenOrFamily
110
- const family = withdraw(familyToken, store)
111
- const familyKey = family.key
112
- target.timelineAtoms.set({ atomKey: familyKey, timelineKey })
113
- tl.subscriptions.set(
114
- family.key,
115
- family.subject.subscribe(
116
- `timeline:${options.key}`,
117
- (creationOrDisposal) => {
118
- handleStateLifecycleEvent(creationOrDisposal, tl, store)
119
- },
120
- ),
121
- )
122
- for (const atom of target.atoms.values()) {
123
- if (atom.family?.key === familyKey) {
124
- addAtomToTimeline(atom, tl, store)
125
- }
126
- }
127
- }
128
- break
106
+ for (const initialTopic of options.scope) {
107
+ switch (initialTopic.type) {
129
108
  case `atom`:
130
109
  case `mutable_atom`:
131
110
  {
132
- let atom = withdraw(tokenOrFamily, store)
133
- if (isMutable(atom)) {
134
- const updateAtom = withdraw(getUpdateToken(atom), store)
135
- atom = updateAtom
136
- atomKey = atom.key
137
- }
138
- if (`family` in atom) {
139
- const familyTimelineKey = target.timelineAtoms.getRelatedKey(
140
- atom.family.key,
141
- )
142
- if (familyTimelineKey) {
111
+ const atomToken: AtomToken<ManagedAtom> = initialTopic
112
+ const atomKey = atomToken.key
113
+ let existingTimelineKey = target.timelineTopics.getRelatedKey(atomKey)
114
+ if (`family` in atomToken) {
115
+ const familyKey = atomToken.family.key
116
+ existingTimelineKey = target.timelineTopics.getRelatedKey(familyKey)
117
+ if (existingTimelineKey) {
143
118
  store.logger.error(
144
119
  `❌`,
145
120
  `timeline`,
146
121
  options.key,
147
- `Failed to add atom "${atom.key}" because its family "${atom.family.key}" already belongs to timeline "${familyTimelineKey}"`,
122
+ `Failed to add atom "${atomKey}" because its family "${familyKey}" already belongs to timeline "${existingTimelineKey}"`,
148
123
  )
149
124
  continue
150
125
  }
151
126
  }
152
- const existingTimelineKey = target.timelineAtoms.getRelatedKey(atomKey)
153
127
  if (existingTimelineKey) {
154
128
  store.logger.error(
155
129
  `❌`,
@@ -159,72 +133,46 @@ export function createTimeline<ManagedAtom extends TimelineManageable>(
159
133
  )
160
134
  continue
161
135
  }
162
- addAtomToTimeline(atom, tl, store)
136
+ addAtomToTimeline(atomToken, tl, store)
163
137
  }
164
138
  break
165
- case `molecule_family`:
139
+
140
+ case `atom_family`:
141
+ case `mutable_atom_family`:
166
142
  {
167
- const family = store.moleculeFamilies.get(tokenOrFamily.key)
168
- if (family) {
169
- tl.subscriptions.set(
170
- tokenOrFamily.key,
171
- family.subject.subscribe(
172
- `timeline:${options.key}`,
173
- (creationOrDisposal) => {
174
- switch (creationOrDisposal.type) {
175
- case `molecule_creation`:
176
- {
177
- const molecule = store.molecules.get(
178
- stringifyJson(creationOrDisposal.token.key),
179
- )
180
- if (molecule) {
181
- const event = Object.assign(creationOrDisposal, {
182
- timestamp: Date.now(),
183
- })
184
- tl.history.push(event)
185
- tl.at = tl.history.length
186
- tl.subject.next(event)
143
+ const familyToken: AtomFamilyToken<any, any> = initialTopic
144
+ const familyKey = familyToken.key
145
+ const existingTimelineKey =
146
+ target.timelineTopics.getRelatedKey(familyKey)
147
+ if (existingTimelineKey) {
148
+ store.logger.error(
149
+ `❌`,
150
+ `timeline`,
151
+ options.key,
152
+ `Failed to add atom family "${familyKey}" because it already belongs to timeline "${existingTimelineKey}"`,
153
+ )
154
+ continue
155
+ }
156
+ addAtomFamilyToTimeline(familyToken, tl, store)
157
+ }
158
+ break
187
159
 
188
- for (const token of molecule.tokens.values()) {
189
- switch (token.type) {
190
- case `atom`:
191
- case `mutable_atom`:
192
- addAtomToTimeline(token, tl, store)
193
- break
194
- }
195
- }
196
- tl.subscriptions.set(
197
- molecule.key,
198
- molecule.subject.subscribe(
199
- `timeline:${options.key}`,
200
- (stateCreationOrDisposal) => {
201
- handleStateLifecycleEvent(
202
- stateCreationOrDisposal,
203
- tl,
204
- store,
205
- )
206
- },
207
- ),
208
- )
209
- }
210
- }
211
- break
212
- case `molecule_disposal`:
213
- tl.subscriptions.get(creationOrDisposal.token.key)?.()
214
- tl.subscriptions.delete(creationOrDisposal.token.key)
215
- for (const familyKey of creationOrDisposal.familyKeys) {
216
- const stateKey = `${familyKey}(${stringifyJson(
217
- creationOrDisposal.token.key,
218
- )})`
219
- tl.subscriptions.get(stateKey)?.()
220
- tl.subscriptions.delete(stateKey)
221
- }
222
- break
223
- }
224
- },
225
- ),
160
+ case `molecule_family`:
161
+ {
162
+ const familyToken: MoleculeFamilyToken<any> = initialTopic
163
+ const familyKey = familyToken.key
164
+ const existingTimelineKey =
165
+ target.timelineTopics.getRelatedKey(familyKey)
166
+ if (existingTimelineKey) {
167
+ store.logger.error(
168
+ `❌`,
169
+ `timeline`,
170
+ options.key,
171
+ `Failed to add molecule family "${familyKey}" because it already belongs to timeline "${existingTimelineKey}"`,
226
172
  )
173
+ continue
227
174
  }
175
+ addMoleculeFamilyToTimeline(familyToken, tl, store)
228
176
  }
229
177
  break
230
178
  }
@@ -239,6 +187,353 @@ export function createTimeline<ManagedAtom extends TimelineManageable>(
239
187
  return token
240
188
  }
241
189
 
190
+ function addAtomToTimeline(
191
+ atomToken: AtomToken<any>,
192
+ tl: Timeline<any>,
193
+ store: Store,
194
+ ): void {
195
+ let maybeAtom = withdraw(atomToken, store)
196
+ if (maybeAtom.type === `mutable_atom`) {
197
+ const updateToken = getUpdateToken(maybeAtom)
198
+ maybeAtom = withdraw(updateToken, store)
199
+ }
200
+ const atom = maybeAtom
201
+ store.timelineTopics.set(
202
+ { topicKey: atom.key, timelineKey: tl.key },
203
+ { topicType: `atom` },
204
+ )
205
+
206
+ tl.subscriptions.set(
207
+ atom.key,
208
+ atom.subject.subscribe(`timeline`, (update) => {
209
+ const target = newest(store)
210
+ const currentSelectorKey =
211
+ store.operation.open && store.operation.token.type === `selector`
212
+ ? store.operation.token.key
213
+ : null
214
+ const currentSelectorTime =
215
+ store.operation.open && store.operation.token.type === `selector`
216
+ ? store.operation.time
217
+ : null
218
+
219
+ const txUpdateInProgress = target.on.transactionApplying.state?.update
220
+
221
+ store.logger.info(
222
+ `⏳`,
223
+ `timeline`,
224
+ tl.key,
225
+ `atom`,
226
+ atomToken.key,
227
+ `went`,
228
+ update.oldValue,
229
+ `->`,
230
+ update.newValue,
231
+ txUpdateInProgress
232
+ ? `in transaction "${txUpdateInProgress.key}"`
233
+ : currentSelectorKey
234
+ ? `in selector "${currentSelectorKey}"`
235
+ : ``,
236
+ )
237
+ if (tl.timeTraveling === null) {
238
+ if (txUpdateInProgress) {
239
+ joinTransaction(tl, txUpdateInProgress, store)
240
+ } else if (currentSelectorKey && currentSelectorTime) {
241
+ let latestUpdate: TimelineUpdate<any> | undefined = tl.history.at(-1)
242
+
243
+ if (currentSelectorTime !== tl.selectorTime) {
244
+ latestUpdate = {
245
+ type: `selector_update`,
246
+ timestamp: currentSelectorTime,
247
+ key: currentSelectorKey,
248
+ atomUpdates: [],
249
+ }
250
+ latestUpdate.atomUpdates.push({
251
+ key: atom.key,
252
+ type: `atom_update`,
253
+ ...update,
254
+ })
255
+ if (tl.at !== tl.history.length) {
256
+ tl.history.splice(tl.at)
257
+ }
258
+
259
+ tl.history.push(latestUpdate)
260
+
261
+ store.logger.info(
262
+ `⌛`,
263
+ `timeline`,
264
+ tl.key,
265
+ `got a selector_update "${currentSelectorKey}" with`,
266
+ latestUpdate.atomUpdates.map((atomUpdate) => atomUpdate.key),
267
+ )
268
+
269
+ tl.at = tl.history.length
270
+ tl.selectorTime = currentSelectorTime
271
+ } else {
272
+ if (latestUpdate?.type === `selector_update`) {
273
+ latestUpdate.atomUpdates.push({
274
+ key: atom.key,
275
+ type: `atom_update`,
276
+ ...update,
277
+ })
278
+ store.logger.info(
279
+ `⌛`,
280
+ `timeline`,
281
+ tl.key,
282
+ `set selector_update "${currentSelectorKey}" to`,
283
+ latestUpdate?.atomUpdates.map((atomUpdate) => atomUpdate.key),
284
+ )
285
+ }
286
+ }
287
+ if (latestUpdate) {
288
+ const willCaptureSelectorUpdate =
289
+ tl.shouldCapture?.(latestUpdate, tl) ?? true
290
+ if (willCaptureSelectorUpdate) {
291
+ tl.subject.next(latestUpdate)
292
+ } else {
293
+ tl.history.pop()
294
+ tl.at = tl.history.length
295
+ }
296
+ }
297
+ } else {
298
+ const timestamp = Date.now()
299
+ tl.selectorTime = null
300
+ if (tl.at !== tl.history.length) {
301
+ tl.history.splice(tl.at)
302
+ }
303
+ const atomUpdate: TimelineAtomUpdate<any> = {
304
+ type: `atom_update`,
305
+ timestamp,
306
+ key: atom.key,
307
+ oldValue: update.oldValue,
308
+ newValue: update.newValue,
309
+ }
310
+ if (atom.family) {
311
+ atomUpdate.family = atom.family
312
+ }
313
+ const willCapture = tl.shouldCapture?.(atomUpdate, tl) ?? true
314
+ store.logger.info(
315
+ `⌛`,
316
+ `timeline`,
317
+ tl.key,
318
+ `got an atom_update to "${atom.key}"`,
319
+ )
320
+ if (willCapture) {
321
+ tl.history.push(atomUpdate)
322
+ tl.at = tl.history.length
323
+ tl.subject.next(atomUpdate)
324
+ }
325
+ }
326
+ }
327
+ }),
328
+ )
329
+ }
330
+
331
+ function addAtomFamilyToTimeline(
332
+ atomFamilyToken: AtomFamilyToken<any, any>,
333
+ tl: Timeline<any>,
334
+ store: Store,
335
+ ): void {
336
+ const family = withdraw(atomFamilyToken, store)
337
+ store.timelineTopics.set(
338
+ { topicKey: family.key, timelineKey: tl.key },
339
+ { topicType: `atom_family` },
340
+ )
341
+ tl.subscriptions.set(
342
+ family.key,
343
+ family.subject.subscribe(`timeline`, (creationOrDisposal) => {
344
+ handleStateLifecycleEvent(creationOrDisposal, tl, store)
345
+ }),
346
+ )
347
+ for (const atom of store.atoms.values()) {
348
+ if (atom.family?.key === family.key) {
349
+ addAtomToTimeline(atom, tl, store)
350
+ }
351
+ }
352
+ }
353
+
354
+ function addMoleculeFamilyToTimeline(
355
+ familyToken: MoleculeFamilyToken<any>,
356
+ tl: Timeline<any>,
357
+ store: Store,
358
+ ): void {
359
+ store.timelineTopics.set(
360
+ { topicKey: familyToken.key, timelineKey: tl.key },
361
+ { topicType: `molecule_family` },
362
+ )
363
+ const family = store.moleculeFamilies.get(familyToken.key)
364
+ if (family) {
365
+ tl.subscriptions.set(
366
+ familyToken.key,
367
+ family.subject.subscribe(`timeline:${tl.key}`, (creationOrDisposal) => {
368
+ store.logger.info(
369
+ `🐞`,
370
+ `timeline`,
371
+ tl.key,
372
+ `got a molecule creation or disposal`,
373
+ creationOrDisposal,
374
+ )
375
+ switch (creationOrDisposal.type) {
376
+ case `molecule_creation`:
377
+ {
378
+ store.timelineTopics.set(
379
+ {
380
+ topicKey: creationOrDisposal.token.key,
381
+ timelineKey: tl.key,
382
+ },
383
+ { topicType: `molecule` },
384
+ )
385
+ const txUpdateInProgress =
386
+ newest(store).on.transactionApplying.state?.update
387
+ if (txUpdateInProgress) {
388
+ joinTransaction(tl, txUpdateInProgress, store)
389
+ } else if (tl.timeTraveling === null) {
390
+ const event = Object.assign(creationOrDisposal, {
391
+ timestamp: Date.now(),
392
+ })
393
+ tl.history.push(event)
394
+ tl.at = tl.history.length
395
+ tl.subject.next(event)
396
+ }
397
+ const molecule = withdraw(creationOrDisposal.token, store)
398
+
399
+ for (const token of molecule.tokens.values()) {
400
+ switch (token.type) {
401
+ case `atom`:
402
+ case `mutable_atom`:
403
+ addAtomToTimeline(token, tl, store)
404
+ break
405
+ }
406
+ }
407
+ tl.subscriptions.set(
408
+ molecule.key,
409
+ molecule.subject.subscribe(
410
+ `timeline:${tl.key}`,
411
+ (stateCreationOrDisposal) => {
412
+ handleStateLifecycleEvent(stateCreationOrDisposal, tl, store)
413
+ },
414
+ ),
415
+ )
416
+ }
417
+ break
418
+ case `molecule_disposal`:
419
+ {
420
+ const txUpdateInProgress =
421
+ newest(store).on.transactionApplying.state?.update
422
+ if (txUpdateInProgress) {
423
+ joinTransaction(tl, txUpdateInProgress, store)
424
+ } else if (tl.timeTraveling === null) {
425
+ const event = Object.assign(creationOrDisposal, {
426
+ timestamp: Date.now(),
427
+ })
428
+ tl.history.push(event)
429
+ tl.at = tl.history.length
430
+ tl.subject.next(event)
431
+ }
432
+ const moleculeKey = creationOrDisposal.token.key
433
+ tl.subscriptions.get(moleculeKey)?.()
434
+ tl.subscriptions.delete(moleculeKey)
435
+ for (const [familyKey] of creationOrDisposal.values) {
436
+ const stateKey = `${familyKey}(${stringifyJson(moleculeKey)})`
437
+ tl.subscriptions.get(stateKey)?.()
438
+ tl.subscriptions.delete(stateKey)
439
+ store.timelineTopics.delete(stateKey)
440
+ }
441
+ }
442
+ break
443
+ }
444
+ }),
445
+ )
446
+ }
447
+ }
448
+
449
+ function joinTransaction(
450
+ tl: Timeline<any>,
451
+ txUpdateInProgress: TransactionUpdate<Func>,
452
+ store: Store,
453
+ ) {
454
+ const currentTxKey = txUpdateInProgress.key
455
+ const currentTxInstanceId = txUpdateInProgress.id
456
+ const currentTxToken: TransactionToken<any> = {
457
+ key: currentTxKey,
458
+ type: `transaction`,
459
+ }
460
+ const currentTransaction = withdraw(currentTxToken, store)
461
+ if (currentTxKey && tl.transactionKey === null) {
462
+ tl.transactionKey = currentTxKey
463
+ const unsubscribe = currentTransaction.subject.subscribe(
464
+ `timeline:${tl.key}`,
465
+ (transactionUpdate) => {
466
+ unsubscribe()
467
+ tl.transactionKey = null
468
+ if (tl.timeTraveling === null && currentTxInstanceId) {
469
+ if (tl.at !== tl.history.length) {
470
+ tl.history.splice(tl.at)
471
+ }
472
+
473
+ // biome-ignore lint/style/noNonNullAssertion: we are in the context of this timeline
474
+ const timelineTopics = store.timelineTopics.getRelatedKeys(tl.key)!
475
+
476
+ const updates = filterTransactionUpdates(
477
+ transactionUpdate.updates,
478
+ timelineTopics,
479
+ )
480
+
481
+ const timelineTransactionUpdate: TimelineTransactionUpdate = {
482
+ timestamp: Date.now(),
483
+ ...transactionUpdate,
484
+ updates,
485
+ }
486
+ const willCapture =
487
+ tl.shouldCapture?.(timelineTransactionUpdate, tl) ?? true
488
+ if (willCapture) {
489
+ tl.history.push(timelineTransactionUpdate)
490
+ tl.at = tl.history.length
491
+ tl.subject.next(timelineTransactionUpdate)
492
+ }
493
+ }
494
+ },
495
+ )
496
+ }
497
+ }
498
+
499
+ function filterTransactionUpdates(
500
+ updates: TransactionUpdateContent[],
501
+ timelineTopics: Set<string>,
502
+ ): TransactionUpdateContent[] {
503
+ return updates
504
+ .filter((updateFromTx) => {
505
+ if (updateFromTx.type === `transaction_update`) {
506
+ return true
507
+ }
508
+
509
+ let key: string
510
+ switch (updateFromTx.type) {
511
+ case `state_creation`:
512
+ case `state_disposal`:
513
+ case `molecule_creation`:
514
+ case `molecule_disposal`:
515
+ key = updateFromTx.token.key
516
+ break
517
+ default:
518
+ key = updateFromTx.key
519
+ break
520
+ }
521
+ return timelineTopics.has(key)
522
+ })
523
+ .map((updateFromTx) => {
524
+ if (`updates` in updateFromTx) {
525
+ return {
526
+ ...updateFromTx,
527
+ updates: filterTransactionUpdates(
528
+ updateFromTx.updates,
529
+ timelineTopics,
530
+ ),
531
+ }
532
+ }
533
+ return updateFromTx
534
+ })
535
+ }
536
+
242
537
  function handleStateLifecycleEvent(
243
538
  event: StateCreation<any> | StateDisposal<any>,
244
539
  tl: Timeline<any>,
@@ -249,9 +544,14 @@ function handleStateLifecycleEvent(
249
544
  timestamp,
250
545
  }) as TimelineUpdate<any>
251
546
  if (!tl.timeTraveling) {
252
- tl.history.push(timelineEvent)
253
- tl.at = tl.history.length
254
- tl.subject.next(timelineEvent)
547
+ const txUpdateInProgress = newest(store).on.transactionApplying.state?.update
548
+ if (txUpdateInProgress) {
549
+ joinTransaction(tl, txUpdateInProgress, store)
550
+ } else {
551
+ tl.history.push(timelineEvent)
552
+ tl.at = tl.history.length
553
+ tl.subject.next(timelineEvent)
554
+ }
255
555
  }
256
556
  switch (event.type) {
257
557
  case `state_creation`:
@@ -1,3 +1,2 @@
1
- export * from "./add-atom-to-timeline"
2
1
  export * from "./create-timeline"
3
2
  export * from "./time-travel"
@@ -38,7 +38,7 @@ export const buildTransaction = (
38
38
  operation: { open: false },
39
39
  readonlySelectors: new LazyMap(parent.readonlySelectors),
40
40
  timelines: new LazyMap(parent.timelines),
41
- timelineAtoms: new Junction(parent.timelineAtoms.toJSON()),
41
+ timelineTopics: new Junction(parent.timelineTopics.toJSON()),
42
42
  trackers: new Map(),
43
43
  transactions: new LazyMap(parent.transactions),
44
44
  selectorAtoms: new Junction(parent.selectorAtoms.toJSON()),
@@ -49,6 +49,7 @@ export const buildTransaction = (
49
49
  valueMap: new LazyMap(parent.valueMap),
50
50
  molecules: new LazyMap(parent.molecules),
51
51
  moleculeFamilies: new LazyMap(parent.moleculeFamilies),
52
+ moleculeInProgress: parent.moleculeInProgress,
52
53
  miscResources: new LazyMap(parent.miscResources),
53
54
  }
54
55
  const epoch = getEpochNumberOfAction(key, store)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atom.io",
3
- "version": "0.24.5",
3
+ "version": "0.24.7",
4
4
  "description": "Composable and testable reactive data library.",
5
5
  "homepage": "https://atom.io.fyi",
6
6
  "sideEffects": false,
@@ -56,8 +56,8 @@
56
56
  "@types/npmlog": "7.0.0",
57
57
  "@types/react": "18.3.3",
58
58
  "@types/tmp": "0.2.6",
59
- "@typescript-eslint/parser": "7.13.1",
60
- "@typescript-eslint/rule-tester": "7.13.1",
59
+ "@typescript-eslint/parser": "7.14.1",
60
+ "@typescript-eslint/rule-tester": "7.14.1",
61
61
  "@vitest/coverage-v8": "1.6.0",
62
62
  "@vitest/ui": "1.6.0",
63
63
  "concurrently": "8.2.2",
@@ -73,7 +73,7 @@
73
73
  "preact": "10.22.0",
74
74
  "react": "18.3.1",
75
75
  "react-dom": "18.3.1",
76
- "react-router-dom": "6.23.1",
76
+ "react-router-dom": "6.24.0",
77
77
  "socket.io": "4.7.5",
78
78
  "socket.io-client": "4.7.5",
79
79
  "tmp": "0.2.3",
@@ -57,7 +57,7 @@ export type MoleculeDisposal = {
57
57
  token: MoleculeToken<any>
58
58
  family: MoleculeFamilyToken<any>
59
59
  context: MoleculeToken<any>[]
60
- familyKeys: string[]
60
+ values: [key: string, value: any][]
61
61
  }
62
62
 
63
63
  export type TransactionUpdateContent =