@livestore/livestore 0.0.19 → 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 (136) hide show
  1. package/README.md +29 -22
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/QueryCache.d.ts +1 -1
  4. package/dist/QueryCache.d.ts.map +1 -1
  5. package/dist/QueryCache.js.map +1 -1
  6. package/dist/__tests__/react/fixture.d.ts +5 -4
  7. package/dist/__tests__/react/fixture.d.ts.map +1 -1
  8. package/dist/__tests__/react/fixture.js +3 -5
  9. package/dist/__tests__/react/fixture.js.map +1 -1
  10. package/dist/__tests__/react/useComponentState.test.d.ts +2 -0
  11. package/dist/__tests__/react/useComponentState.test.d.ts.map +1 -0
  12. package/dist/__tests__/react/useComponentState.test.js +68 -0
  13. package/dist/__tests__/react/useComponentState.test.js.map +1 -0
  14. package/dist/__tests__/react/useLQuery.test.d.ts +2 -0
  15. package/dist/__tests__/react/useLQuery.test.d.ts.map +1 -0
  16. package/dist/__tests__/react/useLQuery.test.js +38 -0
  17. package/dist/__tests__/react/useLQuery.test.js.map +1 -0
  18. package/dist/__tests__/react/useLiveStoreComponent.test.js +4 -9
  19. package/dist/__tests__/react/useLiveStoreComponent.test.js.map +1 -1
  20. package/dist/__tests__/react/useQuery.test.d.ts +2 -0
  21. package/dist/__tests__/react/useQuery.test.d.ts.map +1 -0
  22. package/dist/__tests__/react/useQuery.test.js +33 -0
  23. package/dist/__tests__/react/useQuery.test.js.map +1 -0
  24. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.d.ts +2 -0
  25. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.d.ts.map +1 -0
  26. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.js +38 -0
  27. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.js.map +1 -0
  28. package/dist/__tests__/react/utils/stack-info.test.d.ts +2 -0
  29. package/dist/__tests__/react/utils/stack-info.test.d.ts.map +1 -0
  30. package/dist/__tests__/react/utils/stack-info.test.js +43 -0
  31. package/dist/__tests__/react/utils/stack-info.test.js.map +1 -0
  32. package/dist/__tests__/reactive.test.js +179 -93
  33. package/dist/__tests__/reactive.test.js.map +1 -1
  34. package/dist/__tests__/reactiveQueries/sql.test.d.ts +2 -0
  35. package/dist/__tests__/reactiveQueries/sql.test.d.ts.map +1 -0
  36. package/dist/__tests__/reactiveQueries/sql.test.js +337 -0
  37. package/dist/__tests__/reactiveQueries/sql.test.js.map +1 -0
  38. package/dist/inMemoryDatabase.d.ts +4 -3
  39. package/dist/inMemoryDatabase.d.ts.map +1 -1
  40. package/dist/inMemoryDatabase.js +3 -2
  41. package/dist/inMemoryDatabase.js.map +1 -1
  42. package/dist/index.d.ts +7 -5
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +4 -0
  45. package/dist/index.js.map +1 -1
  46. package/dist/react/index.d.ts +4 -3
  47. package/dist/react/index.d.ts.map +1 -1
  48. package/dist/react/index.js +3 -2
  49. package/dist/react/index.js.map +1 -1
  50. package/dist/react/useComponentState.d.ts +50 -0
  51. package/dist/react/useComponentState.d.ts.map +1 -0
  52. package/dist/react/useComponentState.js +240 -0
  53. package/dist/react/useComponentState.js.map +1 -0
  54. package/dist/react/useGlobalQuery.d.ts +3 -0
  55. package/dist/react/useGlobalQuery.d.ts.map +1 -0
  56. package/dist/react/useGlobalQuery.js +26 -0
  57. package/dist/react/useGlobalQuery.js.map +1 -0
  58. package/dist/react/useGraphQL.d.ts +3 -3
  59. package/dist/react/useGraphQL.d.ts.map +1 -1
  60. package/dist/react/useGraphQL.js +10 -8
  61. package/dist/react/useGraphQL.js.map +1 -1
  62. package/dist/react/useLiveStoreComponent.d.ts +6 -6
  63. package/dist/react/useLiveStoreComponent.d.ts.map +1 -1
  64. package/dist/react/useLiveStoreComponent.js +143 -99
  65. package/dist/react/useLiveStoreComponent.js.map +1 -1
  66. package/dist/react/useQuery.d.ts +2 -2
  67. package/dist/react/useQuery.d.ts.map +1 -1
  68. package/dist/react/useQuery.js +54 -30
  69. package/dist/react/useQuery.js.map +1 -1
  70. package/dist/react/useTemporaryQuery.d.ts +8 -0
  71. package/dist/react/useTemporaryQuery.d.ts.map +1 -0
  72. package/dist/react/useTemporaryQuery.js +19 -0
  73. package/dist/react/useTemporaryQuery.js.map +1 -0
  74. package/dist/react/utils/extractNamesFromStackTrace.d.ts +3 -0
  75. package/dist/react/utils/extractNamesFromStackTrace.d.ts.map +1 -0
  76. package/dist/react/utils/extractNamesFromStackTrace.js +40 -0
  77. package/dist/react/utils/extractNamesFromStackTrace.js.map +1 -0
  78. package/dist/react/utils/extractStackInfoFromStackTrace.d.ts +7 -0
  79. package/dist/react/utils/extractStackInfoFromStackTrace.d.ts.map +1 -0
  80. package/dist/react/utils/extractStackInfoFromStackTrace.js +40 -0
  81. package/dist/react/utils/extractStackInfoFromStackTrace.js.map +1 -0
  82. package/dist/react/utils/stack-info.d.ts +11 -0
  83. package/dist/react/utils/stack-info.d.ts.map +1 -0
  84. package/dist/react/utils/stack-info.js +49 -0
  85. package/dist/react/utils/stack-info.js.map +1 -0
  86. package/dist/reactive.d.ts +51 -67
  87. package/dist/reactive.d.ts.map +1 -1
  88. package/dist/reactive.js +138 -220
  89. package/dist/reactive.js.map +1 -1
  90. package/dist/reactiveQueries/base-class.d.ts +28 -21
  91. package/dist/reactiveQueries/base-class.d.ts.map +1 -1
  92. package/dist/reactiveQueries/base-class.js +22 -18
  93. package/dist/reactiveQueries/base-class.js.map +1 -1
  94. package/dist/reactiveQueries/graph.d.ts +10 -0
  95. package/dist/reactiveQueries/graph.d.ts.map +1 -0
  96. package/dist/reactiveQueries/graph.js +6 -0
  97. package/dist/reactiveQueries/graph.js.map +1 -0
  98. package/dist/reactiveQueries/graphql.d.ts +35 -17
  99. package/dist/reactiveQueries/graphql.d.ts.map +1 -1
  100. package/dist/reactiveQueries/graphql.js +86 -10
  101. package/dist/reactiveQueries/graphql.js.map +1 -1
  102. package/dist/reactiveQueries/js.d.ts +17 -12
  103. package/dist/reactiveQueries/js.d.ts.map +1 -1
  104. package/dist/reactiveQueries/js.js +30 -8
  105. package/dist/reactiveQueries/js.js.map +1 -1
  106. package/dist/reactiveQueries/sql.d.ts +28 -18
  107. package/dist/reactiveQueries/sql.d.ts.map +1 -1
  108. package/dist/reactiveQueries/sql.js +79 -16
  109. package/dist/reactiveQueries/sql.js.map +1 -1
  110. package/dist/store.d.ts +35 -61
  111. package/dist/store.d.ts.map +1 -1
  112. package/dist/store.js +77 -272
  113. package/dist/store.js.map +1 -1
  114. package/package.json +4 -3
  115. package/src/QueryCache.ts +1 -1
  116. package/src/__tests__/react/fixture.tsx +10 -8
  117. package/src/__tests__/react/{useLiveStoreComponent.test.tsx → useComponentState.test.tsx} +9 -20
  118. package/src/__tests__/react/useQuery.test.tsx +48 -0
  119. package/src/__tests__/react/utils/stack-info.test.ts +45 -0
  120. package/src/__tests__/reactive.test.ts +212 -140
  121. package/src/__tests__/reactiveQueries/sql.test.ts +372 -0
  122. package/src/inMemoryDatabase.ts +11 -8
  123. package/src/index.ts +7 -11
  124. package/src/react/index.ts +4 -7
  125. package/src/react/{useLiveStoreComponent.ts → useComponentState.ts} +90 -253
  126. package/src/react/useQuery.ts +74 -40
  127. package/src/react/useTemporaryQuery.ts +23 -0
  128. package/src/react/utils/stack-info.ts +63 -0
  129. package/src/reactive.ts +234 -308
  130. package/src/reactiveQueries/base-class.ts +59 -42
  131. package/src/reactiveQueries/graph.ts +15 -0
  132. package/src/reactiveQueries/graphql.ts +143 -29
  133. package/src/reactiveQueries/js.ts +57 -20
  134. package/src/reactiveQueries/sql.ts +136 -36
  135. package/src/store.ts +121 -426
  136. package/src/react/useGraphQL.ts +0 -138
package/src/reactive.ts CHANGED
@@ -6,8 +6,8 @@
6
6
  // Effect: a side effect that runs when a value changes; return value is ignored
7
7
  // Atom: a node returning a value that can be depended on: Ref | Thunk
8
8
 
9
- // Super computation: Nodes that depend on a given node
10
- // Sub computation: Nodes that a given node depends on
9
+ // Super computation: Nodes that depend on a given node ("downstream")
10
+ // Sub computation: Nodes that a given node depends on ("upstream")
11
11
 
12
12
  // This vocabulary comes from the MiniAdapton paper linked below, although
13
13
  // we don't actually implement the MiniAdapton algorithm because we don't need lazy recomputation.
@@ -26,76 +26,81 @@
26
26
  import type { PrettifyFlat } from '@livestore/utils'
27
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
+ // import { getDurationMsFromSpan } from './otel.js'
32
33
 
33
- const NOT_REFRESHED_YET = Symbol.for('NOT_REFRESHED_YET')
34
- type NOT_REFRESHED_YET = typeof NOT_REFRESHED_YET
34
+ export const NOT_REFRESHED_YET = Symbol.for('NOT_REFRESHED_YET')
35
+ export type NOT_REFRESHED_YET = typeof NOT_REFRESHED_YET
35
36
 
36
- export type GetAtom = <T>(atom: Atom<T>) => T
37
+ export type GetAtom = <T>(atom: Atom<T, any, any>, otelContext?: otel.Context) => T
37
38
 
38
- export type Ref<T> = {
39
+ export type Ref<T, TContext, TDebugRefreshReason extends Taggable> = {
39
40
  _tag: 'ref'
40
41
  id: string
41
- result: T
42
- height: 0
43
- getResult: () => T
44
- sub: Set<Atom<any>> // always empty
45
- super: Set<Atom<any> | Effect>
42
+ isDirty: false
43
+ previousResult: T
44
+ computeResult: () => T
45
+ sub: Set<Atom<any, TContext, TDebugRefreshReason>> // always empty
46
+ super: Set<Atom<any, TContext, TDebugRefreshReason> | Effect>
46
47
  label?: string
47
48
  /** Container for meta information (e.g. the LiveStore Store) */
48
49
  meta?: any
49
50
  equal: (a: T, b: T) => boolean
50
51
  }
51
52
 
52
- type BaseThunk<T> = {
53
+ export type Thunk<TResult, TContext, TDebugRefreshReason extends Taggable> = {
53
54
  _tag: 'thunk'
54
55
  id: string
55
- height: number
56
- getResult: (get: GetAtom, addDebugInfo: (debugInfo: any) => void) => T
57
- sub: Set<Atom<any>>
58
- super: Set<Atom<any> | Effect>
56
+ isDirty: boolean
57
+ computeResult: (
58
+ otelContext?: otel.Context,
59
+ debugRefreshReason?: RefreshReasonWithGenericReasons<TDebugRefreshReason>,
60
+ ) => TResult
61
+ previousResult: TResult | NOT_REFRESHED_YET
62
+ sub: Set<Atom<any, TContext, TDebugRefreshReason>>
63
+ super: Set<Atom<any, TContext, TDebugRefreshReason> | Effect>
59
64
  label?: string
60
65
  /** Container for meta information (e.g. the LiveStore Store) */
61
66
  meta?: any
62
- equal: (a: T, b: T) => boolean
63
- }
67
+ equal: (a: TResult, b: TResult) => boolean
68
+ recomputations: number
64
69
 
65
- type UnevaluatedThunk<T> = BaseThunk<T> & { result: NOT_REFRESHED_YET }
66
- export type Thunk<T> = BaseThunk<T> & { result: T }
70
+ __getResult: any
71
+ }
67
72
 
68
- export type Atom<T> = Ref<T> | Thunk<T>
73
+ export type Atom<T, TContext, TDebugRefreshReason extends Taggable> =
74
+ | Ref<T, TContext, TDebugRefreshReason>
75
+ | Thunk<T, TContext, TDebugRefreshReason>
69
76
 
70
77
  export type Effect = {
71
78
  _tag: 'effect'
72
79
  id: string
73
- doEffect: (get: GetAtom) => void
74
- sub: Set<Atom<any>>
75
- }
76
-
77
- class DependencyNotReadyError extends Error {
78
- constructor(message: string) {
79
- super(message)
80
- this.name = 'DependencyNotReadyError'
81
- }
80
+ doEffect: (otelContext?: otel.Context) => void
81
+ sub: Set<Atom<any, TODO, TODO>>
82
+ label?: string
82
83
  }
83
84
 
84
85
  export type Taggable<T extends string = string> = { _tag: T }
85
86
 
87
+ export type DebugThunkInfo<T extends string = string> = {
88
+ _tag: T
89
+ durationMs: number
90
+ }
91
+
86
92
  export type ReactiveGraphOptions = {
87
93
  effectsWrapper?: (runEffects: () => void) => void
88
- otelTracer: otel.Tracer
89
94
  }
90
95
 
91
- export type AtomDebugInfo<TDebugThunkInfo extends Taggable> = {
96
+ export type AtomDebugInfo<TDebugThunkInfo extends DebugThunkInfo> = {
92
97
  atom: SerializedAtom
93
98
  resultChanged: boolean
94
- durationMs: number
95
99
  debugInfo: TDebugThunkInfo
96
100
  }
97
101
 
98
- 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> = {
99
104
  /** Currently only used for easier handling in React (e.g. as key) */
100
105
  id: string
101
106
  reason: TDebugRefreshReason
@@ -113,20 +118,16 @@ export type RefreshReasonWithGenericReasons<T extends Taggable> =
113
118
  _tag: 'makeThunk'
114
119
  label?: string
115
120
  }
116
- | {
117
- _tag: 'makeEffect'
118
- label?: string
119
- }
120
121
  | { _tag: 'unknown' }
121
122
 
122
123
  export const unknownRefreshReason = () => {
123
- debugger
124
+ // debugger
124
125
  return { _tag: 'unknown' as const }
125
126
  }
126
127
 
127
128
  export type SerializedAtom = Readonly<
128
129
  PrettifyFlat<
129
- Pick<Atom<unknown>, '_tag' | 'height' | 'id' | 'label' | 'meta' | 'result'> & {
130
+ Pick<Atom<unknown, unknown, any>, '_tag' | 'id' | 'label' | 'meta'> & {
130
131
  sub: string[]
131
132
  super: string[]
132
133
  }
@@ -137,45 +138,52 @@ export type SerializedEffect = Readonly<PrettifyFlat<Pick<Effect, '_tag' | 'id'>
137
138
 
138
139
  type ReactiveGraphSnapshot = {
139
140
  readonly atoms: SerializedAtom[]
140
- readonly effects: SerializedEffect[]
141
+ // readonly effects: SerializedEffect[]
141
142
  /** IDs of atoms and effects that are dirty */
142
- readonly dirtyNodes: string[]
143
+ // readonly dirtyNodes: string[]
143
144
  }
144
145
 
145
146
  const uniqueNodeId = () => uniqueId('node-')
146
147
  const uniqueRefreshInfoId = () => uniqueId('refresh-info-')
147
148
 
148
- const serializeAtom = (atom: Atom<any>): SerializedAtom => ({
149
- ...pick(atom, ['_tag', 'height', 'id', 'label', 'meta', 'result']),
149
+ const serializeAtom = (atom: Atom<any, unknown, any>): SerializedAtom => ({
150
+ ...pick(atom, ['_tag', 'id', 'label', 'meta']),
150
151
  sub: Array.from(atom.sub).map((a) => a.id),
151
152
  super: Array.from(atom.super).map((a) => a.id),
152
153
  })
153
154
 
154
- const serializeEffect = (effect: Effect): SerializedEffect => pick(effect, ['_tag', 'id'])
155
+ // const serializeEffect = (effect: Effect): SerializedEffect => pick(effect, ['_tag', 'id'])
155
156
 
156
- export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo extends Taggable> {
157
- private atoms: Set<Atom<any>> = new Set()
158
- private effects: Set<Effect> = new Set()
159
- private otelTracer: otel.Tracer
160
- readonly dirtyNodes: Set<Atom<any> | Effect> = 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()
161
163
  effectsWrapper: (runEffects: () => void) => void
162
164
 
165
+ context: TContext | undefined
166
+
163
167
  debugRefreshInfos: BoundArray<
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
- this.otelTracer = options.otelTracer
170
175
  }
171
176
 
172
- makeRef<T>(val: T, options?: { label?: string; meta?: unknown; equal?: (a: T, b: T) => boolean }): Ref<T> {
173
- 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> = {
174
182
  _tag: 'ref',
175
183
  id: uniqueNodeId(),
176
- result: val,
177
- height: 0,
178
- getResult: () => ref.result,
184
+ isDirty: false,
185
+ previousResult: val,
186
+ computeResult: () => ref.previousResult,
179
187
  sub: new Set(),
180
188
  super: new Set(),
181
189
  label: options?.label,
@@ -189,46 +197,105 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
189
197
  }
190
198
 
191
199
  makeThunk<T>(
192
- getResult: (get: GetAtom, addDebugInfo: (debugInfo: TDebugThunkInfo) => void) => T,
193
- options:
200
+ getResult: (
201
+ get: GetAtom,
202
+ setDebugInfo: (debugInfo: TDebugThunkInfo) => void,
203
+ ctx: TContext,
204
+ otelContext: otel.Context | undefined,
205
+ ) => T,
206
+ options?:
194
207
  | {
195
208
  label?: string
196
209
  meta?: any
197
210
  equal?: (a: T, b: T) => boolean
198
211
  /** Debug info for initializing the thunk (i.e. running it the first time) */
199
- debugRefreshReason?: RefreshReasonWithGenericReasons<TDebugRefreshReason>
212
+ // debugRefreshReason?: RefreshReasonWithGenericReasons<TDebugRefreshReason>
200
213
  }
201
214
  | undefined,
202
- otelContext: otel.Context,
203
- ): Thunk<T> {
204
- const thunk: UnevaluatedThunk<T> = {
215
+ ): Thunk<T, TContext, TDebugRefreshReason> {
216
+ const thunk: Thunk<T, TContext, TDebugRefreshReason> = {
205
217
  _tag: 'thunk',
206
218
  id: uniqueNodeId(),
207
- result: NOT_REFRESHED_YET,
208
- height: 0,
209
- getResult,
219
+ previousResult: NOT_REFRESHED_YET,
220
+ isDirty: true,
221
+ computeResult: (otelContext, debugRefreshReason) => {
222
+ if (thunk.isDirty) {
223
+ const neededCurrentRefresh = this.currentDebugRefresh === undefined
224
+ if (neededCurrentRefresh) {
225
+ this.currentDebugRefresh = { refreshedAtoms: [], startMs: performance.now() }
226
+ }
227
+
228
+ // Reset previous subcomputations as we're about to re-add them as part of the `doEffect` call below
229
+ thunk.sub = new Set()
230
+
231
+ const getAtom = (atom: Atom<T, TContext, TDebugRefreshReason>, otelContext: otel.Context) => {
232
+ this.addEdge(thunk, atom)
233
+ return compute(atom, otelContext)
234
+ }
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(),
245
+ otelContext,
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
+
258
+ thunk.isDirty = false
259
+ thunk.previousResult = result
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
+
279
+ return result
280
+ } else {
281
+ return thunk.previousResult as T
282
+ }
283
+ },
210
284
  sub: new Set(),
211
285
  super: new Set(),
286
+ recomputations: 0,
212
287
  label: options?.label,
213
288
  meta: options?.meta,
214
289
  equal: options?.equal ?? isEqual,
290
+ __getResult: getResult,
215
291
  }
216
292
 
217
293
  this.atoms.add(thunk)
218
- this.dirtyNodes.add(thunk)
219
- this.refresh(
220
- {
221
- otelHint: options?.label ?? 'makeThunk',
222
- debugRefreshReason: options?.debugRefreshReason ?? { _tag: 'makeThunk', label: options?.label },
223
- },
224
- otelContext,
225
- )
226
294
 
227
- // Manually tell the typesystem this thunk is guaranteed to have a result at this point
228
- return thunk as unknown as Thunk<T>
295
+ return thunk
229
296
  }
230
297
 
231
- destroy(node: Atom<any> | Effect) {
298
+ destroy(node: Atom<any, TContext, TDebugRefreshReason> | Effect) {
232
299
  // Recursively destroy any supercomputations
233
300
  if (node._tag === 'ref' || node._tag === 'thunk') {
234
301
  for (const superComp of node.super) {
@@ -241,301 +308,160 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
241
308
  this.removeEdge(node, subComp)
242
309
  }
243
310
 
244
- if (node._tag === 'effect') {
245
- this.effects.delete(node)
246
- } else {
311
+ if (node._tag !== 'effect') {
247
312
  this.atoms.delete(node)
248
313
  }
249
314
  }
250
315
 
251
316
  makeEffect(
252
- doEffect: (get: GetAtom) => void,
253
- options: { label?: string } | undefined,
254
- otelContext: otel.Context,
317
+ doEffect: (get: GetAtom, otelContext?: otel.Context) => void,
318
+ options?: { label?: string } | undefined,
255
319
  ): Effect {
256
320
  const effect: Effect = {
257
321
  _tag: 'effect',
258
322
  id: uniqueNodeId(),
259
- doEffect,
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
+
326
+ // Reset previous subcomputations as we're about to re-add them as part of the `doEffect` call below
327
+ effect.sub = new Set()
328
+
329
+ const getAtom = (atom: Atom<any, TContext, TDebugRefreshReason>, otelContext: otel.Context) => {
330
+ this.addEdge(effect, atom)
331
+ return compute(atom, otelContext)
332
+ }
333
+
334
+ doEffect(getAtom as GetAtom, otelContext)
335
+ },
260
336
  sub: new Set(),
337
+ label: options?.label,
261
338
  }
262
339
 
263
- this.effects.add(effect)
264
- this.dirtyNodes.add(effect)
265
- this.refresh(
266
- { otelHint: 'makeEffect', debugRefreshReason: { _tag: 'makeEffect', label: options?.label } },
267
- otelContext,
268
- )
269
-
270
340
  return effect
271
341
  }
272
342
 
273
343
  setRef<T>(
274
- ref: Ref<T>,
344
+ ref: Ref<T, TContext, TDebugRefreshReason>,
275
345
  val: T,
276
- options:
346
+ options?:
277
347
  | {
278
- otelHint?: string
279
- skipRefresh?: boolean
280
348
  debugRefreshReason?: TDebugRefreshReason
349
+ otelContext?: otel.Context
281
350
  }
282
351
  | undefined,
283
- otelContext: otel.Context,
284
352
  ) {
285
- const { otelHint, skipRefresh, debugRefreshReason } = options ?? {}
286
- ref.result = val
287
- this.dirtyNodes.add(ref)
353
+ ref.previousResult = val
288
354
 
289
- if (skipRefresh) {
290
- const refreshDebugInfo: RefreshDebugInfo<TDebugRefreshReason, TDebugThunkInfo> = {
291
- id: uniqueRefreshInfoId(),
292
- reason: debugRefreshReason ?? (unknownRefreshReason() as TDebugRefreshReason),
293
- skippedRefresh: true,
294
- refreshedAtoms: [],
295
- durationMs: 0,
296
- completedTimestamp: Date.now(),
297
- graphSnapshot: this.getSnapshot(),
298
- }
299
- this.debugRefreshInfos.push(refreshDebugInfo)
300
- return
301
- }
355
+ const effectsToRefresh = new Set<Effect>()
356
+ markSuperCompDirtyRec(ref, effectsToRefresh)
302
357
 
303
- this.refresh({ otelHint, debugRefreshReason }, otelContext)
358
+ this.runEffects(effectsToRefresh, options)
304
359
  }
305
360
 
306
361
  setRefs<T>(
307
- refs: [Ref<T>, T][],
308
- options:
362
+ refs: [Ref<T, TContext, TDebugRefreshReason>, T][],
363
+ options?:
309
364
  | {
310
- otelHint?: string
311
- skipRefresh?: boolean
312
365
  debugRefreshReason?: TDebugRefreshReason
366
+ otelContext?: otel.Context
313
367
  }
314
368
  | undefined,
315
- otelContext: otel.Context,
316
369
  ) {
317
- const otelHint = options?.otelHint ?? ''
318
- const skipRefresh = options?.skipRefresh ?? false
319
- const debugRefreshReason = options?.debugRefreshReason
370
+ const effectsToRefresh = new Set<Effect>()
320
371
  for (const [ref, val] of refs) {
321
- ref.result = val
322
- this.dirtyNodes.add(ref)
323
- }
324
-
325
- if (skipRefresh) {
326
- const refreshDebugInfo: RefreshDebugInfo<TDebugRefreshReason, TDebugThunkInfo> = {
327
- id: uniqueRefreshInfoId(),
328
- reason: debugRefreshReason ?? (unknownRefreshReason() as TDebugRefreshReason),
329
- skippedRefresh: true,
330
- refreshedAtoms: [],
331
- durationMs: 0,
332
- completedTimestamp: Date.now(),
333
- graphSnapshot: this.getSnapshot(),
334
- }
335
- this.debugRefreshInfos.push(refreshDebugInfo)
336
- return
337
- }
372
+ ref.previousResult = val
338
373
 
339
- this.refresh({ otelHint, debugRefreshReason }, otelContext)
340
- }
341
-
342
- get<T>(atom: Atom<T>, context: Atom<any> | Effect): T {
343
- // Autotracking: if we're getting the value of an atom,
344
- // that means it's a subcomputation for the currently refreshing atom.
345
- this.addEdge(context, atom)
346
-
347
- const dependencyMightBeStale = context._tag !== 'effect' && context.height <= atom.height
348
- const dependencyNotRefreshedYet = atom.result === NOT_REFRESHED_YET
349
-
350
- if (dependencyMightBeStale || dependencyNotRefreshedYet) {
351
- throw new DependencyNotReadyError(
352
- `${this.label(context)} referenced dependency ${this.label(atom)} which isn't ready`,
353
- )
374
+ markSuperCompDirtyRec(ref, effectsToRefresh)
354
375
  }
355
376
 
356
- return atom.result
377
+ this.runEffects(effectsToRefresh, options)
357
378
  }
358
379
 
359
- /**
360
- * Update the graph to be consistent with the current values of the root atoms.
361
- * Generally we run this after a ref is updated.
362
- * At the end of the refresh, we run any effects that were scheduled.
363
- *
364
- * @param roots Root atoms to start the refresh from
365
- */
366
- refresh(
367
- options:
380
+ private runEffects = (
381
+ effectsToRefresh: Set<Effect>,
382
+ options?:
368
383
  | {
369
- otelHint?: string
370
- debugRefreshReason?: RefreshReasonWithGenericReasons<TDebugRefreshReason>
384
+ debugRefreshReason?: TDebugRefreshReason
385
+ otelContext?: otel.Context
371
386
  }
372
387
  | undefined,
373
- otelContext: otel.Context,
374
- ): void {
375
- const otelHint = options?.otelHint ?? ''
376
- const debugRefreshReason = options?.debugRefreshReason
377
-
378
- const roots = [...this.dirtyNodes]
379
-
380
- const debugInfoForRefreshedAtoms: AtomDebugInfo<TDebugThunkInfo>[] = []
381
-
382
- // if (otelHint.includes('tableName')) {
383
- // console.log('refresh', otelHint, { shouldTrace })
384
- // }
385
-
386
- this.otelTracer.startActiveSpan(`LiveStore.refresh:${otelHint}`, {}, otelContext, (span) => {
387
- const atomsToRefresh = roots.filter(isAtom)
388
- const effectsToRun = new Set(roots.filter(isEffect))
389
-
390
- span.setAttribute('livestore.hint', otelHint)
391
- span.setAttribute('livestore.rootsCount', roots.length)
392
- // span.setAttribute('sstack', new Error().stack!)
393
-
394
- // Sort in topological order, starting with minimum height
395
- while (atomsToRefresh.length > 0) {
396
- atomsToRefresh.sort((a, b) => a.height - b.height)
397
- const atomToRefresh = atomsToRefresh.shift()!
398
-
399
- // Recompute the value
400
- let resultChanged = false
401
- const debugInfoForAtom = {
402
- atom: serializeAtom(atomToRefresh),
403
- resultChanged,
404
- // debugInfo: unknownRefreshReason() as TDebugThunkInfo,
405
- debugInfo: { _tag: 'unknown' } as TDebugThunkInfo,
406
- durationMs: 0,
407
- } satisfies AtomDebugInfo<TDebugThunkInfo>
408
- try {
409
- atomToRefresh.sub = new Set()
410
- const beforeTimestamp = performance.now()
411
- const newResult = atomToRefresh.getResult(
412
- (atom) => this.get(atom, atomToRefresh),
413
- (debugInfo) => {
414
- debugInfoForAtom.debugInfo = debugInfo
415
- },
416
- )
417
- const afterTimestamp = performance.now()
418
- debugInfoForAtom.durationMs = afterTimestamp - beforeTimestamp
419
-
420
- // Determine if the result changed to do early cutoff and avoid further unnecessary updates.
421
- // Refs never depend on anything, so if a ref is being refreshed it definitely changed.
422
- // For thunks, we use a deep equality check.
423
- resultChanged =
424
- atomToRefresh._tag === 'ref' ||
425
- (atomToRefresh._tag === 'thunk' && !atomToRefresh.equal(atomToRefresh.result, newResult))
426
-
427
- if (resultChanged) {
428
- atomToRefresh.result = newResult
429
- }
388
+ ) => {
389
+ this.effectsWrapper(() => {
390
+ this.currentDebugRefresh = { refreshedAtoms: [], startMs: performance.now() }
430
391
 
431
- this.dirtyNodes.delete(atomToRefresh)
432
- } catch (e) {
433
- if (e instanceof DependencyNotReadyError) {
434
- // If we hit a dependency that wasn't ready yet,
435
- // abort this recomputation and try again later.
436
- if (!atomsToRefresh.includes(atomToRefresh)) {
437
- atomsToRefresh.push(atomToRefresh)
438
- }
439
- } else {
440
- throw e
441
- }
442
- }
443
-
444
- debugInfoForRefreshedAtoms.push(debugInfoForAtom)
445
-
446
- if (!resultChanged) {
447
- continue
448
- }
449
-
450
- // Schedule supercomputations
451
- for (const superComp of atomToRefresh.super) {
452
- switch (superComp._tag) {
453
- case 'ref':
454
- case 'thunk': {
455
- if (!atomsToRefresh.includes(superComp)) {
456
- atomsToRefresh.push(superComp)
457
- }
458
- break
459
- }
460
- case 'effect': {
461
- effectsToRun.add(superComp)
462
- break
463
- }
464
- }
465
- }
392
+ for (const effect of effectsToRefresh) {
393
+ effect.doEffect(options?.otelContext)
466
394
  }
467
395
 
468
- this.effectsWrapper(() => {
469
- for (const effect of effectsToRun) {
470
- effect.doEffect((atom: Atom<any>) => this.get(atom, effect))
471
- this.dirtyNodes.delete(effect)
472
- }
473
- })
474
-
475
- span.end()
476
-
477
- const spanDurationHr = (span as any)._duration
478
- const spanDurationMs = spanDurationHr[0] * 1000 + spanDurationHr[1] / 1_000_000
396
+ const refreshedAtoms = this.currentDebugRefresh.refreshedAtoms
397
+ const durationMs = performance.now() - this.currentDebugRefresh.startMs
398
+ this.currentDebugRefresh = undefined
479
399
 
480
- const refreshDebugInfo: RefreshDebugInfo<
481
- RefreshReasonWithGenericReasons<TDebugRefreshReason>,
482
- TDebugThunkInfo
483
- > = {
400
+ const refreshDebugInfo: RefreshDebugInfo<TDebugRefreshReason, TDebugThunkInfo> = {
484
401
  id: uniqueRefreshInfoId(),
485
- reason: debugRefreshReason ?? unknownRefreshReason(),
486
- refreshedAtoms: debugInfoForRefreshedAtoms,
402
+ reason: options?.debugRefreshReason ?? (unknownRefreshReason() as TDebugRefreshReason),
487
403
  skippedRefresh: false,
488
- durationMs: spanDurationMs,
404
+ refreshedAtoms,
405
+ durationMs,
489
406
  completedTimestamp: Date.now(),
490
407
  graphSnapshot: this.getSnapshot(),
491
408
  }
492
-
493
409
  this.debugRefreshInfos.push(refreshDebugInfo)
494
410
  })
495
411
  }
496
412
 
497
- label(atom: Atom<any> | Effect) {
498
- if (atom._tag === 'effect') {
499
- return `unknown effect`
500
- } else {
501
- return atom.label ?? `unknown ${atom._tag}`
502
- }
503
- }
504
-
505
- addEdge(superComp: Atom<any> | Effect, subComp: Atom<any>) {
413
+ addEdge(
414
+ superComp: Atom<any, TContext, TDebugRefreshReason> | Effect,
415
+ subComp: Atom<any, TContext, TDebugRefreshReason>,
416
+ ) {
506
417
  superComp.sub.add(subComp)
507
418
  subComp.super.add(superComp)
508
- this.updateAtomHeight(superComp)
509
419
  }
510
420
 
511
- removeEdge(superComp: Atom<any> | Effect, subComp: Atom<any>) {
421
+ removeEdge(
422
+ superComp: Atom<any, TContext, TDebugRefreshReason> | Effect,
423
+ subComp: Atom<any, TContext, TDebugRefreshReason>,
424
+ ) {
512
425
  superComp.sub.delete(subComp)
513
426
  subComp.super.delete(superComp)
514
- this.updateAtomHeight(superComp)
515
- }
516
-
517
- updateAtomHeight(atom: Atom<any> | Effect) {
518
- switch (atom._tag) {
519
- case 'ref': {
520
- atom.height = 0
521
- break
522
- }
523
- case 'thunk': {
524
- atom.height = (max([...atom.sub].map((atom) => atom.height)) || 0) + 1
525
- break
526
- }
527
- case 'effect': {
528
- break
529
- }
530
- }
531
427
  }
532
428
 
533
429
  private getSnapshot = (): ReactiveGraphSnapshot => ({
534
430
  atoms: Array.from(this.atoms).map(serializeAtom),
535
- effects: Array.from(this.effects).map(serializeEffect),
536
- dirtyNodes: Array.from(this.dirtyNodes).map((a) => a.id),
431
+ // effects: Array.from(this.effects).map(serializeEffect),
432
+ // dirtyNodes: Array.from(this.dirtyNodes).map((a) => a.id),
537
433
  })
538
434
  }
539
435
 
540
- const isAtom = <T>(a: Atom<T> | Effect): a is Atom<T> => a._tag === 'ref' || a._tag === 'thunk'
541
- const isEffect = <T>(a: Atom<T> | Effect): a is Effect => a._tag === 'effect'
436
+ // const isAtom = <T, TContext>(a: Atom<T, TContext> | Effect): a is Atom<T, TContext> =>
437
+ // a._tag === 'ref' || a._tag === 'thunk'
438
+ // const isEffect = <T, TContext>(a: Atom<T, TContext> | Effect): a is Effect => a._tag === 'effect'
439
+
440
+ const compute = <T>(atom: Atom<T, unknown, any>, otelContext: otel.Context): T => {
441
+ // const __getResult = atom._tag === 'thunk' ? atom.__getResult.toString() : ''
442
+ if (atom.isDirty) {
443
+ // console.log('atom is dirty', atom.id, atom.label ?? '', atom._tag, __getResult)
444
+ const result = atom.computeResult(otelContext)
445
+ atom.isDirty = false
446
+ atom.previousResult = result
447
+ return result
448
+ } else {
449
+ // console.log('atom is clean', atom.id, atom.label ?? '', atom._tag, __getResult)
450
+ return atom.previousResult as T
451
+ }
452
+ }
453
+
454
+ const markSuperCompDirtyRec = <T>(atom: Atom<T, unknown, any>, effectsToRefresh: Set<Effect>) => {
455
+ for (const superComp of atom.super) {
456
+ if (superComp._tag === 'thunk' || superComp._tag === 'ref') {
457
+ superComp.isDirty = true
458
+ markSuperCompDirtyRec(superComp, effectsToRefresh)
459
+ } else {
460
+ effectsToRefresh.add(superComp)
461
+ }
462
+ }
463
+ }
464
+
465
+ const throwContextNotSetError = (): never => {
466
+ throw new Error(`LiveStore Error: \`context\` not set on ReactiveGraph`)
467
+ }