@livestore/livestore 0.0.13 → 0.0.14

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