@livestore/livestore 0.0.19 → 0.0.21

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 (126) hide show
  1. package/README.md +18 -21
  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 +5 -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__/reactive.test.js +167 -93
  29. package/dist/__tests__/reactive.test.js.map +1 -1
  30. package/dist/__tests__/reactiveQueries/sql.test.d.ts +2 -0
  31. package/dist/__tests__/reactiveQueries/sql.test.d.ts.map +1 -0
  32. package/dist/__tests__/reactiveQueries/sql.test.js +337 -0
  33. package/dist/__tests__/reactiveQueries/sql.test.js.map +1 -0
  34. package/dist/inMemoryDatabase.d.ts +2 -2
  35. package/dist/inMemoryDatabase.d.ts.map +1 -1
  36. package/dist/index.d.ts +7 -5
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +4 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/react/index.d.ts +3 -3
  41. package/dist/react/index.d.ts.map +1 -1
  42. package/dist/react/index.js +2 -2
  43. package/dist/react/index.js.map +1 -1
  44. package/dist/react/useComponentState.d.ts +50 -0
  45. package/dist/react/useComponentState.d.ts.map +1 -0
  46. package/dist/react/useComponentState.js +248 -0
  47. package/dist/react/useComponentState.js.map +1 -0
  48. package/dist/react/useGlobalQuery.d.ts +3 -0
  49. package/dist/react/useGlobalQuery.d.ts.map +1 -0
  50. package/dist/react/useGlobalQuery.js +26 -0
  51. package/dist/react/useGlobalQuery.js.map +1 -0
  52. package/dist/react/useGraphQL.d.ts +3 -3
  53. package/dist/react/useGraphQL.d.ts.map +1 -1
  54. package/dist/react/useGraphQL.js +10 -8
  55. package/dist/react/useGraphQL.js.map +1 -1
  56. package/dist/react/useLiveStoreComponent.d.ts +6 -6
  57. package/dist/react/useLiveStoreComponent.d.ts.map +1 -1
  58. package/dist/react/useLiveStoreComponent.js +143 -99
  59. package/dist/react/useLiveStoreComponent.js.map +1 -1
  60. package/dist/react/useQuery.d.ts +2 -2
  61. package/dist/react/useQuery.d.ts.map +1 -1
  62. package/dist/react/useQuery.js +26 -22
  63. package/dist/react/useQuery.js.map +1 -1
  64. package/dist/react/useTemporaryQuery.d.ts +8 -0
  65. package/dist/react/useTemporaryQuery.d.ts.map +1 -0
  66. package/dist/react/useTemporaryQuery.js +17 -0
  67. package/dist/react/useTemporaryQuery.js.map +1 -0
  68. package/dist/react/utils/extractNamesFromStackTrace.d.ts +3 -0
  69. package/dist/react/utils/extractNamesFromStackTrace.d.ts.map +1 -0
  70. package/dist/react/utils/extractNamesFromStackTrace.js +40 -0
  71. package/dist/react/utils/extractNamesFromStackTrace.js.map +1 -0
  72. package/dist/react/utils/extractStackInfoFromStackTrace.d.ts +7 -0
  73. package/dist/react/utils/extractStackInfoFromStackTrace.d.ts.map +1 -0
  74. package/dist/react/utils/extractStackInfoFromStackTrace.js +40 -0
  75. package/dist/react/utils/extractStackInfoFromStackTrace.js.map +1 -0
  76. package/dist/reactive.d.ts +42 -48
  77. package/dist/reactive.d.ts.map +1 -1
  78. package/dist/reactive.js +293 -186
  79. package/dist/reactive.js.map +1 -1
  80. package/dist/reactiveQueries/base-class.d.ts +28 -23
  81. package/dist/reactiveQueries/base-class.d.ts.map +1 -1
  82. package/dist/reactiveQueries/base-class.js +25 -18
  83. package/dist/reactiveQueries/base-class.js.map +1 -1
  84. package/dist/reactiveQueries/graph.d.ts +10 -0
  85. package/dist/reactiveQueries/graph.d.ts.map +1 -0
  86. package/dist/reactiveQueries/graph.js +6 -0
  87. package/dist/reactiveQueries/graph.js.map +1 -0
  88. package/dist/reactiveQueries/graphql.d.ts +34 -17
  89. package/dist/reactiveQueries/graphql.d.ts.map +1 -1
  90. package/dist/reactiveQueries/graphql.js +91 -10
  91. package/dist/reactiveQueries/graphql.js.map +1 -1
  92. package/dist/reactiveQueries/js.d.ts +16 -12
  93. package/dist/reactiveQueries/js.d.ts.map +1 -1
  94. package/dist/reactiveQueries/js.js +31 -8
  95. package/dist/reactiveQueries/js.js.map +1 -1
  96. package/dist/reactiveQueries/sql.d.ts +22 -18
  97. package/dist/reactiveQueries/sql.d.ts.map +1 -1
  98. package/dist/reactiveQueries/sql.js +82 -16
  99. package/dist/reactiveQueries/sql.js.map +1 -1
  100. package/dist/store.d.ts +12 -52
  101. package/dist/store.d.ts.map +1 -1
  102. package/dist/store.js +283 -264
  103. package/dist/store.js.map +1 -1
  104. package/package.json +4 -3
  105. package/src/QueryCache.ts +1 -1
  106. package/src/__tests__/react/fixture.tsx +12 -7
  107. package/src/__tests__/react/{useLiveStoreComponent.test.tsx → useComponentState.test.tsx} +9 -20
  108. package/src/__tests__/react/useQuery.test.tsx +48 -0
  109. package/src/__tests__/react/utils/extractStackInfoFromStackTrace.test.ts +40 -0
  110. package/src/__tests__/reactive.test.ts +193 -140
  111. package/src/__tests__/reactiveQueries/sql.test.ts +372 -0
  112. package/src/inMemoryDatabase.ts +2 -2
  113. package/src/index.ts +7 -11
  114. package/src/react/index.ts +3 -7
  115. package/src/react/{useLiveStoreComponent.ts → useComponentState.ts} +89 -247
  116. package/src/react/useQuery.ts +29 -27
  117. package/src/react/useTemporaryQuery.ts +21 -0
  118. package/src/react/utils/extractStackInfoFromStackTrace.ts +47 -0
  119. package/src/reactive.ts +385 -268
  120. package/src/reactiveQueries/base-class.ts +60 -44
  121. package/src/reactiveQueries/graph.ts +15 -0
  122. package/src/reactiveQueries/graphql.ts +145 -29
  123. package/src/reactiveQueries/js.ts +53 -20
  124. package/src/reactiveQueries/sql.ts +129 -36
  125. package/src/store.ts +338 -408
  126. 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.
@@ -24,68 +24,69 @@
24
24
  /* eslint-disable prefer-arrow/prefer-arrow-functions */
25
25
 
26
26
  import type { PrettifyFlat } from '@livestore/utils'
27
- import { pick } from '@livestore/utils'
27
+ import { pick, shouldNeverHappen } from '@livestore/utils'
28
28
  import type * as otel from '@opentelemetry/api'
29
29
  import { isEqual, max, 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>, otelContext?: otel.Context) => T
37
38
 
38
39
  export type Ref<T> = {
39
40
  _tag: 'ref'
40
41
  id: string
41
- result: T
42
+ isDirty: false
43
+ previousResult: T
42
44
  height: 0
43
- getResult: () => T
44
- sub: Set<Atom<any>> // always empty
45
- super: Set<Atom<any> | Effect>
45
+ computeResult: () => T
46
+ sub: Set<Atom<any, TODO>> // always empty
47
+ super: Set<Atom<any, TODO> | Effect>
46
48
  label?: string
47
49
  /** Container for meta information (e.g. the LiveStore Store) */
48
50
  meta?: any
49
51
  equal: (a: T, b: T) => boolean
50
52
  }
51
53
 
52
- type BaseThunk<T> = {
54
+ type BaseThunk<TResult, TContext> = {
53
55
  _tag: 'thunk'
54
56
  id: string
57
+ isDirty: boolean
55
58
  height: number
56
- getResult: (get: GetAtom, addDebugInfo: (debugInfo: any) => void) => T
57
- sub: Set<Atom<any>>
58
- super: Set<Atom<any> | Effect>
59
+ computeResult: (otelContext?: otel.Context) => TResult
60
+ previousResult: TResult | NOT_REFRESHED_YET
61
+ sub: Set<Atom<any, TContext>>
62
+ super: Set<Atom<any, TContext> | Effect>
59
63
  label?: string
60
64
  /** Container for meta information (e.g. the LiveStore Store) */
61
65
  meta?: any
62
- equal: (a: T, b: T) => boolean
66
+ equal: (a: TResult, b: TResult) => boolean
67
+ recomputations: number
68
+
69
+ __getResult: any
63
70
  }
64
71
 
65
- type UnevaluatedThunk<T> = BaseThunk<T> & { result: NOT_REFRESHED_YET }
66
- export type Thunk<T> = BaseThunk<T> & { result: T }
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 }
67
76
 
68
- export type Atom<T> = Ref<T> | Thunk<T>
77
+ export type Atom<T, TContext> = Ref<T> | Thunk<T, TContext>
69
78
 
70
79
  export type Effect = {
71
80
  _tag: 'effect'
72
81
  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
- }
82
+ doEffect: (otelContext?: otel.Context) => void
83
+ sub: Set<Atom<any, TODO>>
82
84
  }
83
85
 
84
86
  export type Taggable<T extends string = string> = { _tag: T }
85
87
 
86
88
  export type ReactiveGraphOptions = {
87
89
  effectsWrapper?: (runEffects: () => void) => void
88
- otelTracer: otel.Tracer
89
90
  }
90
91
 
91
92
  export type AtomDebugInfo<TDebugThunkInfo extends Taggable> = {
@@ -120,13 +121,13 @@ export type RefreshReasonWithGenericReasons<T extends Taggable> =
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, TODO>, '_tag' | 'height' | 'id' | 'label' | 'meta'> & {
130
131
  sub: string[]
131
132
  super: string[]
132
133
  }
@@ -137,45 +138,44 @@ 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, TODO>): SerializedAtom => ({
150
+ ...pick(atom, ['_tag', 'height', '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<TDebugRefreshReason extends Taggable, TDebugThunkInfo extends Taggable, TContext = {}> {
158
+ readonly atoms: Set<Atom<any, TContext>> = new Set()
161
159
  effectsWrapper: (runEffects: () => void) => void
162
160
 
161
+ context: TContext | undefined
162
+
163
163
  debugRefreshInfos: BoundArray<
164
164
  RefreshDebugInfo<RefreshReasonWithGenericReasons<TDebugRefreshReason>, TDebugThunkInfo>
165
165
  > = new BoundArray(5000)
166
166
 
167
167
  constructor(options: ReactiveGraphOptions) {
168
168
  this.effectsWrapper = options?.effectsWrapper ?? ((runEffects: () => void) => runEffects())
169
- this.otelTracer = options.otelTracer
170
169
  }
171
170
 
172
171
  makeRef<T>(val: T, options?: { label?: string; meta?: unknown; equal?: (a: T, b: T) => boolean }): Ref<T> {
173
172
  const ref: Ref<T> = {
174
173
  _tag: 'ref',
175
174
  id: uniqueNodeId(),
176
- result: val,
175
+ isDirty: false,
176
+ previousResult: val,
177
177
  height: 0,
178
- getResult: () => ref.result,
178
+ computeResult: () => ref.previousResult,
179
179
  sub: new Set(),
180
180
  super: new Set(),
181
181
  label: options?.label,
@@ -189,8 +189,13 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
189
189
  }
190
190
 
191
191
  makeThunk<T>(
192
- getResult: (get: GetAtom, addDebugInfo: (debugInfo: TDebugThunkInfo) => void) => T,
193
- options:
192
+ getResult_: (
193
+ get: GetAtom,
194
+ addDebugInfo: (debugInfo: TDebugThunkInfo) => void,
195
+ ctx: TContext,
196
+ otelContext: otel.Context | undefined,
197
+ ) => T,
198
+ options?:
194
199
  | {
195
200
  label?: string
196
201
  meta?: any
@@ -199,36 +204,98 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
199
204
  debugRefreshReason?: RefreshReasonWithGenericReasons<TDebugRefreshReason>
200
205
  }
201
206
  | undefined,
202
- otelContext: otel.Context,
203
- ): Thunk<T> {
204
- const thunk: UnevaluatedThunk<T> = {
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> = {
205
242
  _tag: 'thunk',
206
243
  id: uniqueNodeId(),
207
- result: NOT_REFRESHED_YET,
244
+ previousResult: NOT_REFRESHED_YET,
245
+ isDirty: true,
208
246
  height: 0,
209
- getResult,
247
+ computeResult: (otelContext) => {
248
+ if (thunk.isDirty) {
249
+ // Reset previous subcomputations as we're about to re-add them as part of the `doEffect` call below
250
+ thunk.sub = new Set()
251
+
252
+ const compute_ = (atom: Atom<T, unknown>, otelContext: otel.Context) => {
253
+ this.addEdge(thunk, atom)
254
+ return compute(atom, otelContext)
255
+ }
256
+ const result = getResult_(
257
+ compute_ as GetAtom,
258
+ addDebugInfo,
259
+ this.context ?? shouldNeverHappen('No store context set yet'),
260
+ otelContext,
261
+ )
262
+ thunk.isDirty = false
263
+ thunk.previousResult = result
264
+ thunk.recomputations++
265
+ return result
266
+ } else {
267
+ return thunk.previousResult as T
268
+ }
269
+ },
210
270
  sub: new Set(),
211
271
  super: new Set(),
272
+ recomputations: 0,
212
273
  label: options?.label,
213
274
  meta: options?.meta,
214
275
  equal: options?.equal ?? isEqual,
276
+ __getResult: getResult_,
215
277
  }
216
278
 
217
279
  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
- )
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)
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 as unknown as Thunk<T, TContext>
229
296
  }
230
297
 
231
- destroy(node: Atom<any> | Effect) {
298
+ destroy(node: Atom<any, TContext> | 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,31 +308,51 @@ 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?:
319
+ | {
320
+ label?: string
321
+ debugRefreshReason?: RefreshReasonWithGenericReasons<TDebugRefreshReason>
322
+ }
323
+ | undefined,
255
324
  ): Effect {
256
325
  const effect: Effect = {
257
326
  _tag: 'effect',
258
327
  id: uniqueNodeId(),
259
- doEffect,
328
+ doEffect: (otelContext) => {
329
+ // Reset previous subcomputations as we're about to re-add them as part of the `doEffect` call below
330
+ effect.sub = new Set()
331
+
332
+ const getAtom = (atom: Atom<any, unknown>, otelContext: otel.Context) => {
333
+ this.addEdge(effect, atom)
334
+ return compute(atom, otelContext)
335
+ }
336
+ doEffect(getAtom as GetAtom, otelContext)
337
+ },
260
338
  sub: new Set(),
261
339
  }
262
340
 
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
- )
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)
269
356
 
270
357
  return effect
271
358
  }
@@ -273,88 +360,88 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
273
360
  setRef<T>(
274
361
  ref: Ref<T>,
275
362
  val: T,
276
- options:
363
+ options?:
277
364
  | {
278
- otelHint?: string
279
- skipRefresh?: boolean
280
365
  debugRefreshReason?: TDebugRefreshReason
366
+ otelContext?: otel.Context
281
367
  }
282
368
  | undefined,
283
- otelContext: otel.Context,
284
369
  ) {
285
- const { otelHint, skipRefresh, debugRefreshReason } = options ?? {}
286
- ref.result = val
287
- this.dirtyNodes.add(ref)
288
-
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(),
370
+ const { debugRefreshReason } = options ?? {}
371
+ ref.previousResult = val
372
+
373
+ const effectsToRefresh = new Set<Effect>()
374
+ markSuperCompDirtyRec(ref, effectsToRefresh)
375
+
376
+ this.effectsWrapper(() => {
377
+ for (const effect of effectsToRefresh) {
378
+ effect.doEffect(options?.otelContext)
298
379
  }
299
- this.debugRefreshInfos.push(refreshDebugInfo)
300
- return
301
- }
380
+ })
302
381
 
303
- this.refresh({ otelHint, debugRefreshReason }, otelContext)
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)
304
392
  }
305
393
 
306
394
  setRefs<T>(
307
395
  refs: [Ref<T>, T][],
308
- options:
396
+ options?:
309
397
  | {
310
- otelHint?: string
311
- skipRefresh?: boolean
312
398
  debugRefreshReason?: TDebugRefreshReason
399
+ otelContext?: otel.Context
313
400
  }
314
401
  | undefined,
315
- otelContext: otel.Context,
316
402
  ) {
317
- const otelHint = options?.otelHint ?? ''
318
- const skipRefresh = options?.skipRefresh ?? false
319
403
  const debugRefreshReason = options?.debugRefreshReason
404
+ const effectsToRefresh = new Set<Effect>()
320
405
  for (const [ref, val] of refs) {
321
- ref.result = val
322
- this.dirtyNodes.add(ref)
406
+ ref.previousResult = val
407
+
408
+ markSuperCompDirtyRec(ref, effectsToRefresh)
323
409
  }
324
410
 
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(),
411
+ this.effectsWrapper(() => {
412
+ for (const effect of effectsToRefresh) {
413
+ effect.doEffect(options?.otelContext)
334
414
  }
335
- this.debugRefreshInfos.push(refreshDebugInfo)
336
- return
337
- }
415
+ })
338
416
 
339
- this.refresh({ otelHint, debugRefreshReason }, otelContext)
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)
340
427
  }
341
428
 
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)
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)
346
433
 
347
- const dependencyMightBeStale = context._tag !== 'effect' && context.height <= atom.height
348
- const dependencyNotRefreshedYet = atom.result === NOT_REFRESHED_YET
434
+ // const dependencyMightBeStale = context._tag !== 'effect' && context.height <= atom.height
435
+ // const dependencyNotRefreshedYet = atom.result === NOT_REFRESHED_YET
349
436
 
350
- if (dependencyMightBeStale || dependencyNotRefreshedYet) {
351
- throw new DependencyNotReadyError(
352
- `${this.label(context)} referenced dependency ${this.label(atom)} which isn't ready`,
353
- )
354
- }
437
+ // if (dependencyMightBeStale || dependencyNotRefreshedYet) {
438
+ // throw new DependencyNotReadyError(
439
+ // `${this.label(context)} referenced dependency ${this.label(atom)} which isn't ready`,
440
+ // )
441
+ // }
355
442
 
356
- return atom.result
357
- }
443
+ // return atom.result
444
+ // }
358
445
 
359
446
  /**
360
447
  * Update the graph to be consistent with the current values of the root atoms.
@@ -363,138 +450,138 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
363
450
  *
364
451
  * @param roots Root atoms to start the refresh from
365
452
  */
366
- refresh(
367
- options:
368
- | {
369
- otelHint?: string
370
- debugRefreshReason?: RefreshReasonWithGenericReasons<TDebugRefreshReason>
371
- }
372
- | 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
- }
430
-
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
- }
466
- }
467
-
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
479
-
480
- const refreshDebugInfo: RefreshDebugInfo<
481
- RefreshReasonWithGenericReasons<TDebugRefreshReason>,
482
- TDebugThunkInfo
483
- > = {
484
- id: uniqueRefreshInfoId(),
485
- reason: debugRefreshReason ?? unknownRefreshReason(),
486
- refreshedAtoms: debugInfoForRefreshedAtoms,
487
- skippedRefresh: false,
488
- durationMs: spanDurationMs,
489
- completedTimestamp: Date.now(),
490
- graphSnapshot: this.getSnapshot(),
491
- }
492
-
493
- this.debugRefreshInfos.push(refreshDebugInfo)
494
- })
495
- }
496
-
497
- label(atom: Atom<any> | Effect) {
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) {
498
585
  if (atom._tag === 'effect') {
499
586
  return `unknown effect`
500
587
  } else {
@@ -502,19 +589,19 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
502
589
  }
503
590
  }
504
591
 
505
- addEdge(superComp: Atom<any> | Effect, subComp: Atom<any>) {
592
+ addEdge(superComp: Atom<any, TContext> | Effect, subComp: Atom<any, TContext>) {
506
593
  superComp.sub.add(subComp)
507
594
  subComp.super.add(superComp)
508
595
  this.updateAtomHeight(superComp)
509
596
  }
510
597
 
511
- removeEdge(superComp: Atom<any> | Effect, subComp: Atom<any>) {
598
+ removeEdge(superComp: Atom<any, TContext> | Effect, subComp: Atom<any, TContext>) {
512
599
  superComp.sub.delete(subComp)
513
600
  subComp.super.delete(superComp)
514
601
  this.updateAtomHeight(superComp)
515
602
  }
516
603
 
517
- updateAtomHeight(atom: Atom<any> | Effect) {
604
+ updateAtomHeight(atom: Atom<any, TContext> | Effect) {
518
605
  switch (atom._tag) {
519
606
  case 'ref': {
520
607
  atom.height = 0
@@ -532,10 +619,40 @@ export class ReactiveGraph<TDebugRefreshReason extends Taggable, TDebugThunkInfo
532
619
 
533
620
  private getSnapshot = (): ReactiveGraphSnapshot => ({
534
621
  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),
622
+ // effects: Array.from(this.effects).map(serializeEffect),
623
+ // dirtyNodes: Array.from(this.dirtyNodes).map((a) => a.id),
537
624
  })
625
+
626
+ get atomsCount() {
627
+ return this.atoms.size
628
+ }
629
+ }
630
+
631
+ // const isAtom = <T, TContext>(a: Atom<T, TContext> | Effect): a is Atom<T, TContext> =>
632
+ // a._tag === 'ref' || a._tag === 'thunk'
633
+ // const isEffect = <T, TContext>(a: Atom<T, TContext> | Effect): a is Effect => a._tag === 'effect'
634
+
635
+ const compute = <T>(atom: Atom<T, any>, otelContext: otel.Context): T => {
636
+ // const __getResult = atom._tag === 'thunk' ? atom.__getResult.toString() : ''
637
+ if (atom.isDirty) {
638
+ // console.log('atom is dirty', atom.id, atom.label ?? '', atom._tag, __getResult)
639
+ const result = atom.computeResult(otelContext)
640
+ atom.isDirty = false
641
+ atom.previousResult = result
642
+ return result
643
+ } else {
644
+ // console.log('atom is clean', atom.id, atom.label ?? '', atom._tag, __getResult)
645
+ return atom.previousResult as T
646
+ }
538
647
  }
539
648
 
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'
649
+ const markSuperCompDirtyRec = <T>(atom: Atom<T, any>, effectsToRefresh: Set<Effect>) => {
650
+ for (const superComp of atom.super) {
651
+ if (superComp._tag === 'thunk' || superComp._tag === 'ref') {
652
+ superComp.isDirty = true
653
+ markSuperCompDirtyRec(superComp, effectsToRefresh)
654
+ } else {
655
+ effectsToRefresh.add(superComp)
656
+ }
657
+ }
658
+ }