@livestore/livestore 0.0.21 → 0.0.22

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 (78) hide show
  1. package/README.md +14 -4
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/react/fixture.d.ts.map +1 -1
  4. package/dist/__tests__/react/fixture.js +0 -2
  5. package/dist/__tests__/react/fixture.js.map +1 -1
  6. package/dist/__tests__/react/useQuery.test.js +1 -1
  7. package/dist/__tests__/react/useQuery.test.js.map +1 -1
  8. package/dist/__tests__/react/utils/stack-info.test.d.ts +2 -0
  9. package/dist/__tests__/react/utils/stack-info.test.d.ts.map +1 -0
  10. package/dist/__tests__/react/utils/stack-info.test.js +43 -0
  11. package/dist/__tests__/react/utils/stack-info.test.js.map +1 -0
  12. package/dist/__tests__/reactive.test.js +13 -1
  13. package/dist/__tests__/reactive.test.js.map +1 -1
  14. package/dist/__tests__/reactiveQueries/sql.test.js +3 -3
  15. package/dist/__tests__/reactiveQueries/sql.test.js.map +1 -1
  16. package/dist/inMemoryDatabase.d.ts +2 -1
  17. package/dist/inMemoryDatabase.d.ts.map +1 -1
  18. package/dist/inMemoryDatabase.js +3 -2
  19. package/dist/inMemoryDatabase.js.map +1 -1
  20. package/dist/react/index.d.ts +1 -0
  21. package/dist/react/index.d.ts.map +1 -1
  22. package/dist/react/index.js +1 -0
  23. package/dist/react/index.js.map +1 -1
  24. package/dist/react/useComponentState.d.ts.map +1 -1
  25. package/dist/react/useComponentState.js +19 -27
  26. package/dist/react/useComponentState.js.map +1 -1
  27. package/dist/react/useQuery.d.ts.map +1 -1
  28. package/dist/react/useQuery.js +46 -26
  29. package/dist/react/useQuery.js.map +1 -1
  30. package/dist/react/useTemporaryQuery.d.ts.map +1 -1
  31. package/dist/react/useTemporaryQuery.js +2 -0
  32. package/dist/react/useTemporaryQuery.js.map +1 -1
  33. package/dist/react/utils/stack-info.d.ts +11 -0
  34. package/dist/react/utils/stack-info.d.ts.map +1 -0
  35. package/dist/react/utils/stack-info.js +49 -0
  36. package/dist/react/utils/stack-info.js.map +1 -0
  37. package/dist/reactive.d.ts +33 -43
  38. package/dist/reactive.d.ts.map +1 -1
  39. package/dist/reactive.js +66 -255
  40. package/dist/reactive.js.map +1 -1
  41. package/dist/reactiveQueries/base-class.d.ts +15 -13
  42. package/dist/reactiveQueries/base-class.d.ts.map +1 -1
  43. package/dist/reactiveQueries/base-class.js +5 -8
  44. package/dist/reactiveQueries/base-class.js.map +1 -1
  45. package/dist/reactiveQueries/graphql.d.ts +4 -3
  46. package/dist/reactiveQueries/graphql.d.ts.map +1 -1
  47. package/dist/reactiveQueries/graphql.js +29 -34
  48. package/dist/reactiveQueries/graphql.js.map +1 -1
  49. package/dist/reactiveQueries/js.d.ts +2 -1
  50. package/dist/reactiveQueries/js.d.ts.map +1 -1
  51. package/dist/reactiveQueries/js.js +8 -9
  52. package/dist/reactiveQueries/js.js.map +1 -1
  53. package/dist/reactiveQueries/sql.d.ts +11 -5
  54. package/dist/reactiveQueries/sql.d.ts.map +1 -1
  55. package/dist/reactiveQueries/sql.js +31 -34
  56. package/dist/reactiveQueries/sql.js.map +1 -1
  57. package/dist/store.d.ts +26 -12
  58. package/dist/store.d.ts.map +1 -1
  59. package/dist/store.js +41 -255
  60. package/dist/store.js.map +1 -1
  61. package/package.json +3 -3
  62. package/src/__tests__/react/fixture.tsx +0 -3
  63. package/src/__tests__/react/useQuery.test.tsx +1 -1
  64. package/src/__tests__/react/utils/{extractStackInfoFromStackTrace.test.ts → stack-info.test.ts} +25 -20
  65. package/src/__tests__/reactive.test.ts +20 -1
  66. package/src/__tests__/reactiveQueries/sql.test.ts +3 -3
  67. package/src/inMemoryDatabase.ts +9 -6
  68. package/src/react/index.ts +1 -0
  69. package/src/react/useComponentState.ts +25 -30
  70. package/src/react/useQuery.ts +66 -34
  71. package/src/react/useTemporaryQuery.ts +2 -0
  72. package/src/react/utils/{extractStackInfoFromStackTrace.ts → stack-info.ts} +21 -5
  73. package/src/reactive.ts +148 -339
  74. package/src/reactiveQueries/base-class.ts +23 -22
  75. package/src/reactiveQueries/graphql.ts +34 -36
  76. package/src/reactiveQueries/js.ts +14 -10
  77. package/src/reactiveQueries/sql.ts +55 -48
  78. package/src/store.ts +70 -305
package/src/reactive.ts CHANGED
@@ -24,9 +24,9 @@
24
24
  /* eslint-disable prefer-arrow/prefer-arrow-functions */
25
25
 
26
26
  import type { PrettifyFlat } from '@livestore/utils'
27
- import { pick, shouldNeverHappen } from '@livestore/utils'
27
+ import { pick } from '@livestore/utils'
28
28
  import type * as otel from '@opentelemetry/api'
29
- import { isEqual, max, uniqueId } from 'lodash-es'
29
+ import { isEqual, uniqueId } from 'lodash-es'
30
30
 
31
31
  import { BoundArray } from './bounded-collections.js'
32
32
  // import { getDurationMsFromSpan } from './otel.js'
@@ -34,32 +34,33 @@ import { BoundArray } from './bounded-collections.js'
34
34
  export const NOT_REFRESHED_YET = Symbol.for('NOT_REFRESHED_YET')
35
35
  export type NOT_REFRESHED_YET = typeof NOT_REFRESHED_YET
36
36
 
37
- export type GetAtom = <T>(atom: Atom<T, any>, otelContext?: otel.Context) => T
37
+ export type GetAtom = <T>(atom: Atom<T, any, any>, otelContext?: otel.Context) => T
38
38
 
39
- export type Ref<T> = {
39
+ export type Ref<T, TContext, TDebugRefreshReason extends Taggable> = {
40
40
  _tag: 'ref'
41
41
  id: string
42
42
  isDirty: false
43
43
  previousResult: T
44
- height: 0
45
44
  computeResult: () => T
46
- sub: Set<Atom<any, TODO>> // always empty
47
- super: Set<Atom<any, TODO> | Effect>
45
+ sub: Set<Atom<any, TContext, TDebugRefreshReason>> // always empty
46
+ super: Set<Atom<any, TContext, TDebugRefreshReason> | Effect>
48
47
  label?: string
49
48
  /** Container for meta information (e.g. the LiveStore Store) */
50
49
  meta?: any
51
50
  equal: (a: T, b: T) => boolean
52
51
  }
53
52
 
54
- type BaseThunk<TResult, TContext> = {
53
+ export type Thunk<TResult, TContext, TDebugRefreshReason extends Taggable> = {
55
54
  _tag: 'thunk'
56
55
  id: string
57
56
  isDirty: boolean
58
- height: number
59
- computeResult: (otelContext?: otel.Context) => TResult
57
+ computeResult: (
58
+ otelContext?: otel.Context,
59
+ debugRefreshReason?: RefreshReasonWithGenericReasons<TDebugRefreshReason>,
60
+ ) => TResult
60
61
  previousResult: TResult | NOT_REFRESHED_YET
61
- sub: Set<Atom<any, TContext>>
62
- super: Set<Atom<any, TContext> | Effect>
62
+ sub: Set<Atom<any, TContext, TDebugRefreshReason>>
63
+ super: Set<Atom<any, TContext, TDebugRefreshReason> | Effect>
63
64
  label?: string
64
65
  /** Container for meta information (e.g. the LiveStore Store) */
65
66
  meta?: any
@@ -69,34 +70,37 @@ type BaseThunk<TResult, TContext> = {
69
70
  __getResult: any
70
71
  }
71
72
 
72
- type UnevaluatedThunk<T, TContext> = BaseThunk<T, TContext>
73
- // & { result: NOT_REFRESHED_YET }
74
- export type Thunk<T, TContext> = BaseThunk<T, TContext>
75
- // & { result: T }
76
-
77
- export type Atom<T, TContext> = Ref<T> | Thunk<T, TContext>
73
+ export type Atom<T, TContext, TDebugRefreshReason extends Taggable> =
74
+ | Ref<T, TContext, TDebugRefreshReason>
75
+ | Thunk<T, TContext, TDebugRefreshReason>
78
76
 
79
77
  export type Effect = {
80
78
  _tag: 'effect'
81
79
  id: string
82
80
  doEffect: (otelContext?: otel.Context) => void
83
- sub: Set<Atom<any, TODO>>
81
+ sub: Set<Atom<any, TODO, TODO>>
82
+ label?: string
84
83
  }
85
84
 
86
85
  export type Taggable<T extends string = string> = { _tag: T }
87
86
 
87
+ export type DebugThunkInfo<T extends string = string> = {
88
+ _tag: T
89
+ durationMs: number
90
+ }
91
+
88
92
  export type ReactiveGraphOptions = {
89
93
  effectsWrapper?: (runEffects: () => void) => void
90
94
  }
91
95
 
92
- export type AtomDebugInfo<TDebugThunkInfo extends Taggable> = {
96
+ export type AtomDebugInfo<TDebugThunkInfo extends DebugThunkInfo> = {
93
97
  atom: SerializedAtom
94
98
  resultChanged: boolean
95
- durationMs: number
96
99
  debugInfo: TDebugThunkInfo
97
100
  }
98
101
 
99
- export type RefreshDebugInfo<TDebugRefreshReason extends Taggable, TDebugThunkInfo extends Taggable> = {
102
+ // TODO possibly find a better name for "refresh"
103
+ export type RefreshDebugInfo<TDebugRefreshReason extends Taggable, TDebugThunkInfo extends DebugThunkInfo> = {
100
104
  /** Currently only used for easier handling in React (e.g. as key) */
101
105
  id: string
102
106
  reason: TDebugRefreshReason
@@ -114,10 +118,6 @@ export type RefreshReasonWithGenericReasons<T extends Taggable> =
114
118
  _tag: 'makeThunk'
115
119
  label?: string
116
120
  }
117
- | {
118
- _tag: 'makeEffect'
119
- label?: string
120
- }
121
121
  | { _tag: 'unknown' }
122
122
 
123
123
  export const unknownRefreshReason = () => {
@@ -127,7 +127,7 @@ export const unknownRefreshReason = () => {
127
127
 
128
128
  export type SerializedAtom = Readonly<
129
129
  PrettifyFlat<
130
- Pick<Atom<unknown, TODO>, '_tag' | 'height' | 'id' | 'label' | 'meta'> & {
130
+ Pick<Atom<unknown, unknown, any>, '_tag' | 'id' | 'label' | 'meta'> & {
131
131
  sub: string[]
132
132
  super: string[]
133
133
  }
@@ -146,16 +146,20 @@ type ReactiveGraphSnapshot = {
146
146
  const uniqueNodeId = () => uniqueId('node-')
147
147
  const uniqueRefreshInfoId = () => uniqueId('refresh-info-')
148
148
 
149
- const serializeAtom = (atom: Atom<any, TODO>): SerializedAtom => ({
150
- ...pick(atom, ['_tag', 'height', 'id', 'label', 'meta']),
149
+ const serializeAtom = (atom: Atom<any, unknown, any>): SerializedAtom => ({
150
+ ...pick(atom, ['_tag', 'id', 'label', 'meta']),
151
151
  sub: Array.from(atom.sub).map((a) => a.id),
152
152
  super: Array.from(atom.super).map((a) => a.id),
153
153
  })
154
154
 
155
155
  // const serializeEffect = (effect: Effect): SerializedEffect => pick(effect, ['_tag', 'id'])
156
156
 
157
- export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo extends Taggable, TContext = {}> {
158
- readonly atoms: Set<Atom<any, TContext>> = new Set()
157
+ export class ReactiveGraph<
158
+ TDebugRefreshReason extends Taggable,
159
+ TDebugThunkInfo extends DebugThunkInfo,
160
+ TContext = {},
161
+ > {
162
+ readonly atoms: Set<Atom<any, TContext, TDebugRefreshReason>> = new Set()
159
163
  effectsWrapper: (runEffects: () => void) => void
160
164
 
161
165
  context: TContext | undefined
@@ -164,17 +168,21 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
164
168
  RefreshDebugInfo<RefreshReasonWithGenericReasons<TDebugRefreshReason>, TDebugThunkInfo>
165
169
  > = new BoundArray(5000)
166
170
 
171
+ currentDebugRefresh: { refreshedAtoms: AtomDebugInfo<TDebugThunkInfo>[]; startMs: DOMHighResTimeStamp } | undefined
172
+
167
173
  constructor(options: ReactiveGraphOptions) {
168
174
  this.effectsWrapper = options?.effectsWrapper ?? ((runEffects: () => void) => runEffects())
169
175
  }
170
176
 
171
- makeRef<T>(val: T, options?: { label?: string; meta?: unknown; equal?: (a: T, b: T) => boolean }): Ref<T> {
172
- const ref: Ref<T> = {
177
+ makeRef<T>(
178
+ val: T,
179
+ options?: { label?: string; meta?: unknown; equal?: (a: T, b: T) => boolean },
180
+ ): Ref<T, TContext, TDebugRefreshReason> {
181
+ const ref: Ref<T, TContext, TDebugRefreshReason> = {
173
182
  _tag: 'ref',
174
183
  id: uniqueNodeId(),
175
184
  isDirty: false,
176
185
  previousResult: val,
177
- height: 0,
178
186
  computeResult: () => ref.previousResult,
179
187
  sub: new Set(),
180
188
  super: new Set(),
@@ -189,9 +197,9 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
189
197
  }
190
198
 
191
199
  makeThunk<T>(
192
- getResult_: (
200
+ getResult: (
193
201
  get: GetAtom,
194
- addDebugInfo: (debugInfo: TDebugThunkInfo) => void,
202
+ setDebugInfo: (debugInfo: TDebugThunkInfo) => void,
195
203
  ctx: TContext,
196
204
  otelContext: otel.Context | undefined,
197
205
  ) => T,
@@ -201,67 +209,73 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
201
209
  meta?: any
202
210
  equal?: (a: T, b: T) => boolean
203
211
  /** Debug info for initializing the thunk (i.e. running it the first time) */
204
- debugRefreshReason?: RefreshReasonWithGenericReasons<TDebugRefreshReason>
212
+ // debugRefreshReason?: RefreshReasonWithGenericReasons<TDebugRefreshReason>
205
213
  }
206
214
  | undefined,
207
- ): Thunk<T, TContext> {
208
- // const computeResult = (): T => {
209
- // const getAtom = (atom: Atom<T, any>): T => {
210
- // const __getResult = atom._tag === 'thunk' ? atom.__getResult.toString() : ''
211
- // if (atom.isDirty) {
212
- // console.log('atom is dirty', atom.id, atom.label ?? '', atom._tag, __getResult)
213
- // const result = atom.computeResult()
214
- // atom.isDirty = false
215
- // atom.previousResult = result
216
- // return result
217
- // } else {
218
- // console.log('atom is clean', atom.id, atom.label ?? '', atom._tag, __getResult)
219
- // return atom.previousResult as T
220
- // }
221
- // }
222
-
223
- // let resultChanged = false
224
- // const debugInfoForAtom = {
225
- // atom: serializeAtom(null as TODO),
226
- // resultChanged,
227
- // // debugInfo: unknownRefreshReason() as TDebugThunkInfo,
228
- // debugInfo: { _tag: 'unknown' } as TDebugThunkInfo,
229
- // durationMs: 0,
230
- // } satisfies AtomDebugInfo<TDebugThunkInfo>
231
-
232
- const addDebugInfo = (_debugInfo: TDebugThunkInfo) => {
233
- // debugInfoForAtom.debugInfo = debugInfo
234
- }
235
-
236
- // debugInfoForRefreshedAtoms.push(debugInfoForAtom)
237
-
238
- // return getResult_(getAtom as GetAtom, addDebugInfo, this.context!)
239
- // }
240
-
241
- const thunk: UnevaluatedThunk<T, TContext> = {
215
+ ): Thunk<T, TContext, TDebugRefreshReason> {
216
+ const thunk: Thunk<T, TContext, TDebugRefreshReason> = {
242
217
  _tag: 'thunk',
243
218
  id: uniqueNodeId(),
244
219
  previousResult: NOT_REFRESHED_YET,
245
220
  isDirty: true,
246
- height: 0,
247
- computeResult: (otelContext) => {
221
+ computeResult: (otelContext, debugRefreshReason) => {
248
222
  if (thunk.isDirty) {
223
+ const neededCurrentRefresh = this.currentDebugRefresh === undefined
224
+ if (neededCurrentRefresh) {
225
+ this.currentDebugRefresh = { refreshedAtoms: [], startMs: performance.now() }
226
+ }
227
+
249
228
  // Reset previous subcomputations as we're about to re-add them as part of the `doEffect` call below
250
229
  thunk.sub = new Set()
251
230
 
252
- const compute_ = (atom: Atom<T, unknown>, otelContext: otel.Context) => {
231
+ const getAtom = (atom: Atom<T, TContext, TDebugRefreshReason>, otelContext: otel.Context) => {
253
232
  this.addEdge(thunk, atom)
254
233
  return compute(atom, otelContext)
255
234
  }
256
- const result = getResult_(
257
- compute_ as GetAtom,
258
- addDebugInfo,
259
- this.context ?? shouldNeverHappen('No store context set yet'),
235
+
236
+ let debugInfo: TDebugThunkInfo | undefined = undefined
237
+ const setDebugInfo = (debugInfo_: TDebugThunkInfo) => {
238
+ debugInfo = debugInfo_
239
+ }
240
+
241
+ const result = getResult(
242
+ getAtom as GetAtom,
243
+ setDebugInfo,
244
+ this.context ?? throwContextNotSetError(),
260
245
  otelContext,
261
246
  )
247
+
248
+ const resultChanged = thunk.equal(thunk.previousResult as T, result) === false
249
+
250
+ const debugInfoForAtom = {
251
+ atom: serializeAtom(thunk),
252
+ resultChanged,
253
+ debugInfo: debugInfo ?? (unknownRefreshReason() as TDebugThunkInfo),
254
+ } satisfies AtomDebugInfo<TDebugThunkInfo>
255
+
256
+ this.currentDebugRefresh!.refreshedAtoms.push(debugInfoForAtom)
257
+
262
258
  thunk.isDirty = false
263
259
  thunk.previousResult = result
264
260
  thunk.recomputations++
261
+
262
+ if (neededCurrentRefresh) {
263
+ const refreshedAtoms = this.currentDebugRefresh!.refreshedAtoms
264
+ const durationMs = performance.now() - this.currentDebugRefresh!.startMs
265
+ this.currentDebugRefresh = undefined
266
+
267
+ const refreshDebugInfo = {
268
+ id: uniqueRefreshInfoId(),
269
+ reason: debugRefreshReason ?? { _tag: 'makeThunk', label: options?.label },
270
+ skippedRefresh: false,
271
+ refreshedAtoms,
272
+ durationMs,
273
+ completedTimestamp: Date.now(),
274
+ graphSnapshot: this.getSnapshot(),
275
+ }
276
+ this.debugRefreshInfos.push(refreshDebugInfo)
277
+ }
278
+
265
279
  return result
266
280
  } else {
267
281
  return thunk.previousResult as T
@@ -273,29 +287,15 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
273
287
  label: options?.label,
274
288
  meta: options?.meta,
275
289
  equal: options?.equal ?? isEqual,
276
- __getResult: getResult_,
290
+ __getResult: getResult,
277
291
  }
278
292
 
279
293
  this.atoms.add(thunk)
280
- // this.dirtyNodes.add(thunk)
281
-
282
- const debugRefreshReason = options?.debugRefreshReason ?? { _tag: 'makeThunk', label: options?.label }
283
-
284
- const refreshDebugInfo = {
285
- id: uniqueRefreshInfoId(),
286
- reason: debugRefreshReason,
287
- skippedRefresh: true,
288
- refreshedAtoms: [],
289
- durationMs: 0,
290
- completedTimestamp: Date.now(),
291
- graphSnapshot: this.getSnapshot(),
292
- }
293
- this.debugRefreshInfos.push(refreshDebugInfo)
294
294
 
295
- return thunk as unknown as Thunk<T, TContext>
295
+ return thunk
296
296
  }
297
297
 
298
- destroy(node: Atom<any, TContext> | Effect) {
298
+ destroy(node: Atom<any, TContext, TDebugRefreshReason> | Effect) {
299
299
  // Recursively destroy any supercomputations
300
300
  if (node._tag === 'ref' || node._tag === 'thunk') {
301
301
  for (const superComp of node.super) {
@@ -315,50 +315,33 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
315
315
 
316
316
  makeEffect(
317
317
  doEffect: (get: GetAtom, otelContext?: otel.Context) => void,
318
- options?:
319
- | {
320
- label?: string
321
- debugRefreshReason?: RefreshReasonWithGenericReasons<TDebugRefreshReason>
322
- }
323
- | undefined,
318
+ options?: { label?: string } | undefined,
324
319
  ): Effect {
325
320
  const effect: Effect = {
326
321
  _tag: 'effect',
327
322
  id: uniqueNodeId(),
328
323
  doEffect: (otelContext) => {
324
+ // NOTE we're not tracking any debug refresh info for effects as they're tracked by the thunks they depend on
325
+
329
326
  // Reset previous subcomputations as we're about to re-add them as part of the `doEffect` call below
330
327
  effect.sub = new Set()
331
328
 
332
- const getAtom = (atom: Atom<any, unknown>, otelContext: otel.Context) => {
329
+ const getAtom = (atom: Atom<any, TContext, TDebugRefreshReason>, otelContext: otel.Context) => {
333
330
  this.addEdge(effect, atom)
334
331
  return compute(atom, otelContext)
335
332
  }
333
+
336
334
  doEffect(getAtom as GetAtom, otelContext)
337
335
  },
338
336
  sub: new Set(),
337
+ label: options?.label,
339
338
  }
340
339
 
341
- // this.effects.add(effect)
342
- // this.dirtyNodes.add(effect)
343
-
344
- const debugRefreshReason = options?.debugRefreshReason ?? { _tag: 'makeEffect', label: options?.label }
345
-
346
- const refreshDebugInfo = {
347
- id: uniqueRefreshInfoId(),
348
- reason: debugRefreshReason ?? (unknownRefreshReason() as TDebugRefreshReason),
349
- skippedRefresh: true,
350
- refreshedAtoms: [],
351
- durationMs: 0,
352
- completedTimestamp: Date.now(),
353
- graphSnapshot: this.getSnapshot(),
354
- }
355
- this.debugRefreshInfos.push(refreshDebugInfo)
356
-
357
340
  return effect
358
341
  }
359
342
 
360
343
  setRef<T>(
361
- ref: Ref<T>,
344
+ ref: Ref<T, TContext, TDebugRefreshReason>,
362
345
  val: T,
363
346
  options?:
364
347
  | {
@@ -367,32 +350,16 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
367
350
  }
368
351
  | undefined,
369
352
  ) {
370
- const { debugRefreshReason } = options ?? {}
371
353
  ref.previousResult = val
372
354
 
373
355
  const effectsToRefresh = new Set<Effect>()
374
356
  markSuperCompDirtyRec(ref, effectsToRefresh)
375
357
 
376
- this.effectsWrapper(() => {
377
- for (const effect of effectsToRefresh) {
378
- effect.doEffect(options?.otelContext)
379
- }
380
- })
381
-
382
- const refreshDebugInfo: RefreshDebugInfo<TDebugRefreshReason, TDebugThunkInfo> = {
383
- id: uniqueRefreshInfoId(),
384
- reason: debugRefreshReason ?? (unknownRefreshReason() as TDebugRefreshReason),
385
- skippedRefresh: true,
386
- refreshedAtoms: [],
387
- durationMs: 0,
388
- completedTimestamp: Date.now(),
389
- graphSnapshot: this.getSnapshot(),
390
- }
391
- this.debugRefreshInfos.push(refreshDebugInfo)
358
+ this.runEffects(effectsToRefresh, options)
392
359
  }
393
360
 
394
361
  setRefs<T>(
395
- refs: [Ref<T>, T][],
362
+ refs: [Ref<T, TContext, TDebugRefreshReason>, T][],
396
363
  options?:
397
364
  | {
398
365
  debugRefreshReason?: TDebugRefreshReason
@@ -400,7 +367,6 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
400
367
  }
401
368
  | undefined,
402
369
  ) {
403
- const debugRefreshReason = options?.debugRefreshReason
404
370
  const effectsToRefresh = new Set<Effect>()
405
371
  for (const [ref, val] of refs) {
406
372
  ref.previousResult = val
@@ -408,213 +374,56 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
408
374
  markSuperCompDirtyRec(ref, effectsToRefresh)
409
375
  }
410
376
 
377
+ this.runEffects(effectsToRefresh, options)
378
+ }
379
+
380
+ private runEffects = (
381
+ effectsToRefresh: Set<Effect>,
382
+ options?:
383
+ | {
384
+ debugRefreshReason?: TDebugRefreshReason
385
+ otelContext?: otel.Context
386
+ }
387
+ | undefined,
388
+ ) => {
411
389
  this.effectsWrapper(() => {
390
+ this.currentDebugRefresh = { refreshedAtoms: [], startMs: performance.now() }
391
+
412
392
  for (const effect of effectsToRefresh) {
413
393
  effect.doEffect(options?.otelContext)
414
394
  }
415
- })
416
395
 
417
- const refreshDebugInfo: RefreshDebugInfo<TDebugRefreshReason, TDebugThunkInfo> = {
418
- id: uniqueRefreshInfoId(),
419
- reason: debugRefreshReason ?? (unknownRefreshReason() as TDebugRefreshReason),
420
- skippedRefresh: true,
421
- refreshedAtoms: [],
422
- durationMs: 0,
423
- completedTimestamp: Date.now(),
424
- graphSnapshot: this.getSnapshot(),
425
- }
426
- this.debugRefreshInfos.push(refreshDebugInfo)
427
- }
428
-
429
- // get<T>(atom: Atom<T, TContext>, context: Atom<any, TContext> | Effect): T {
430
- // // Autotracking: if we're getting the value of an atom,
431
- // // that means it's a subcomputation for the currently refreshing atom.
432
- // this.addEdge(context, atom)
433
-
434
- // const dependencyMightBeStale = context._tag !== 'effect' && context.height <= atom.height
435
- // const dependencyNotRefreshedYet = atom.result === NOT_REFRESHED_YET
436
-
437
- // if (dependencyMightBeStale || dependencyNotRefreshedYet) {
438
- // throw new DependencyNotReadyError(
439
- // `${this.label(context)} referenced dependency ${this.label(atom)} which isn't ready`,
440
- // )
441
- // }
442
-
443
- // return atom.result
444
- // }
445
-
446
- /**
447
- * Update the graph to be consistent with the current values of the root atoms.
448
- * Generally we run this after a ref is updated.
449
- * At the end of the refresh, we run any effects that were scheduled.
450
- *
451
- * @param roots Root atoms to start the refresh from
452
- */
453
- // refresh(
454
- // options?:
455
- // | {
456
- // otelHint?: string
457
- // debugRefreshReason?: RefreshReasonWithGenericReasons<TDebugRefreshReason>
458
- // }
459
- // | undefined,
460
- // otelContext: otel.Context = otel.context.active(),
461
- // ): void {
462
- // const otelHint = options?.otelHint ?? ''
463
- // const debugRefreshReason = options?.debugRefreshReason
464
-
465
- // const roots = [...this.dirtyNodes]
466
-
467
- // const debugInfoForRefreshedAtoms: AtomDebugInfo<TDebugThunkInfo>[] = []
468
-
469
- // // if (otelHint.includes('tableName')) {
470
- // // console.log('refresh', otelHint, { shouldTrace })
471
- // // }
472
-
473
- // this.otelTracer.startActiveSpan(`LiveStore.refresh:${otelHint}`, {}, otelContext, (span) => {
474
- // const atomsToRefresh = roots.filter(isAtom)
475
- // const effectsToRun = new Set(roots.filter(isEffect))
476
-
477
- // span.setAttribute('livestore.hint', otelHint)
478
- // span.setAttribute('livestore.rootsCount', roots.length)
479
- // // span.setAttribute('sstack', new Error().stack!)
480
-
481
- // // Sort in topological order, starting with minimum height
482
- // while (atomsToRefresh.length > 0) {
483
- // atomsToRefresh.sort((a, b) => a.height - b.height)
484
- // const atomToRefresh = atomsToRefresh.shift()!
485
-
486
- // // Recompute the value
487
- // let resultChanged = false
488
- // const debugInfoForAtom = {
489
- // atom: serializeAtom(atomToRefresh),
490
- // resultChanged,
491
- // // debugInfo: unknownRefreshReason() as TDebugThunkInfo,
492
- // debugInfo: { _tag: 'unknown' } as TDebugThunkInfo,
493
- // durationMs: 0,
494
- // } satisfies AtomDebugInfo<TDebugThunkInfo>
495
- // try {
496
- // atomToRefresh.sub = new Set()
497
- // const beforeTimestamp = performance.now()
498
- // const newResult = atomToRefresh.getResult(
499
- // (atom) => this.get(atom, atomToRefresh),
500
- // (debugInfo) => {
501
- // debugInfoForAtom.debugInfo = debugInfo
502
- // },
503
- // this.context ?? shouldNeverHappen(`No context provided yet for ReactiveGraph`),
504
- // )
505
- // const afterTimestamp = performance.now()
506
- // debugInfoForAtom.durationMs = afterTimestamp - beforeTimestamp
507
-
508
- // // Determine if the result changed to do early cutoff and avoid further unnecessary updates.
509
- // // Refs never depend on anything, so if a ref is being refreshed it definitely changed.
510
- // // For thunks, we use a deep equality check.
511
- // resultChanged =
512
- // atomToRefresh._tag === 'ref' ||
513
- // (atomToRefresh._tag === 'thunk' && !atomToRefresh.equal(atomToRefresh.result, newResult))
514
-
515
- // if (resultChanged) {
516
- // atomToRefresh.result = newResult
517
- // }
518
-
519
- // this.dirtyNodes.delete(atomToRefresh)
520
- // } catch (e) {
521
- // if (e instanceof DependencyNotReadyError) {
522
- // // If we hit a dependency that wasn't ready yet,
523
- // // abort this recomputation and try again later.
524
- // if (!atomsToRefresh.includes(atomToRefresh)) {
525
- // atomsToRefresh.push(atomToRefresh)
526
- // }
527
- // } else {
528
- // throw e
529
- // }
530
- // }
531
-
532
- // debugInfoForRefreshedAtoms.push(debugInfoForAtom)
533
-
534
- // if (!resultChanged) {
535
- // continue
536
- // }
537
-
538
- // // Schedule supercomputations
539
- // for (const superComp of atomToRefresh.super) {
540
- // switch (superComp._tag) {
541
- // case 'ref':
542
- // case 'thunk': {
543
- // if (!atomsToRefresh.includes(superComp)) {
544
- // atomsToRefresh.push(superComp)
545
- // }
546
- // break
547
- // }
548
- // case 'effect': {
549
- // effectsToRun.add(superComp)
550
- // break
551
- // }
552
- // }
553
- // }
554
- // }
555
-
556
- // this.effectsWrapper(() => {
557
- // for (const effect of effectsToRun) {
558
- // effect.doEffect((atom: Atom<any, TContext>) => this.get(atom, effect))
559
- // this.dirtyNodes.delete(effect)
560
- // }
561
- // })
562
-
563
- // span.end()
564
-
565
- // const spanDurationMs = getDurationMsFromSpan(span)
566
-
567
- // const refreshDebugInfo: RefreshDebugInfo<
568
- // RefreshReasonWithGenericReasons<TDebugRefreshReason>,
569
- // TDebugThunkInfo
570
- // > = {
571
- // id: uniqueRefreshInfoId(),
572
- // reason: debugRefreshReason ?? unknownRefreshReason(),
573
- // refreshedAtoms: debugInfoForRefreshedAtoms,
574
- // skippedRefresh: false,
575
- // durationMs: spanDurationMs,
576
- // completedTimestamp: Date.now(),
577
- // graphSnapshot: this.getSnapshot(),
578
- // }
579
-
580
- // this.debugRefreshInfos.push(refreshDebugInfo)
581
- // })
582
- // }
583
-
584
- label(atom: Atom<any, TContext> | Effect) {
585
- if (atom._tag === 'effect') {
586
- return `unknown effect`
587
- } else {
588
- return atom.label ?? `unknown ${atom._tag}`
589
- }
396
+ const refreshedAtoms = this.currentDebugRefresh.refreshedAtoms
397
+ const durationMs = performance.now() - this.currentDebugRefresh.startMs
398
+ this.currentDebugRefresh = undefined
399
+
400
+ const refreshDebugInfo: RefreshDebugInfo<TDebugRefreshReason, TDebugThunkInfo> = {
401
+ id: uniqueRefreshInfoId(),
402
+ reason: options?.debugRefreshReason ?? (unknownRefreshReason() as TDebugRefreshReason),
403
+ skippedRefresh: false,
404
+ refreshedAtoms,
405
+ durationMs,
406
+ completedTimestamp: Date.now(),
407
+ graphSnapshot: this.getSnapshot(),
408
+ }
409
+ this.debugRefreshInfos.push(refreshDebugInfo)
410
+ })
590
411
  }
591
412
 
592
- addEdge(superComp: Atom<any, TContext> | Effect, subComp: Atom<any, TContext>) {
413
+ addEdge(
414
+ superComp: Atom<any, TContext, TDebugRefreshReason> | Effect,
415
+ subComp: Atom<any, TContext, TDebugRefreshReason>,
416
+ ) {
593
417
  superComp.sub.add(subComp)
594
418
  subComp.super.add(superComp)
595
- this.updateAtomHeight(superComp)
596
419
  }
597
420
 
598
- removeEdge(superComp: Atom<any, TContext> | Effect, subComp: Atom<any, TContext>) {
421
+ removeEdge(
422
+ superComp: Atom<any, TContext, TDebugRefreshReason> | Effect,
423
+ subComp: Atom<any, TContext, TDebugRefreshReason>,
424
+ ) {
599
425
  superComp.sub.delete(subComp)
600
426
  subComp.super.delete(superComp)
601
- this.updateAtomHeight(superComp)
602
- }
603
-
604
- updateAtomHeight(atom: Atom<any, TContext> | Effect) {
605
- switch (atom._tag) {
606
- case 'ref': {
607
- atom.height = 0
608
- break
609
- }
610
- case 'thunk': {
611
- atom.height = (max([...atom.sub].map((atom) => atom.height)) || 0) + 1
612
- break
613
- }
614
- case 'effect': {
615
- break
616
- }
617
- }
618
427
  }
619
428
 
620
429
  private getSnapshot = (): ReactiveGraphSnapshot => ({
@@ -622,17 +431,13 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
622
431
  // effects: Array.from(this.effects).map(serializeEffect),
623
432
  // dirtyNodes: Array.from(this.dirtyNodes).map((a) => a.id),
624
433
  })
625
-
626
- get atomsCount() {
627
- return this.atoms.size
628
- }
629
434
  }
630
435
 
631
436
  // const isAtom = <T, TContext>(a: Atom<T, TContext> | Effect): a is Atom<T, TContext> =>
632
437
  // a._tag === 'ref' || a._tag === 'thunk'
633
438
  // const isEffect = <T, TContext>(a: Atom<T, TContext> | Effect): a is Effect => a._tag === 'effect'
634
439
 
635
- const compute = <T>(atom: Atom<T, any>, otelContext: otel.Context): T => {
440
+ const compute = <T>(atom: Atom<T, unknown, any>, otelContext: otel.Context): T => {
636
441
  // const __getResult = atom._tag === 'thunk' ? atom.__getResult.toString() : ''
637
442
  if (atom.isDirty) {
638
443
  // console.log('atom is dirty', atom.id, atom.label ?? '', atom._tag, __getResult)
@@ -646,7 +451,7 @@ const compute = <T>(atom: Atom<T, any>, otelContext: otel.Context): T => {
646
451
  }
647
452
  }
648
453
 
649
- const markSuperCompDirtyRec = <T>(atom: Atom<T, any>, effectsToRefresh: Set<Effect>) => {
454
+ const markSuperCompDirtyRec = <T>(atom: Atom<T, unknown, any>, effectsToRefresh: Set<Effect>) => {
650
455
  for (const superComp of atom.super) {
651
456
  if (superComp._tag === 'thunk' || superComp._tag === 'ref') {
652
457
  superComp.isDirty = true
@@ -656,3 +461,7 @@ const markSuperCompDirtyRec = <T>(atom: Atom<T, any>, effectsToRefresh: Set<Effe
656
461
  }
657
462
  }
658
463
  }
464
+
465
+ const throwContextNotSetError = (): never => {
466
+ throw new Error(`LiveStore Error: \`context\` not set on ReactiveGraph`)
467
+ }