@livestore/livestore 0.0.0-snapshot-909cdd1ac2fd591945c2be2b0f53e14d87f3c9d4

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 (131) hide show
  1. package/README.md +1 -0
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/QueryCache.d.ts +20 -0
  4. package/dist/QueryCache.d.ts.map +1 -0
  5. package/dist/QueryCache.js +61 -0
  6. package/dist/QueryCache.js.map +1 -0
  7. package/dist/SynchronousDatabaseWrapper.d.ts +36 -0
  8. package/dist/SynchronousDatabaseWrapper.d.ts.map +1 -0
  9. package/dist/SynchronousDatabaseWrapper.js +176 -0
  10. package/dist/SynchronousDatabaseWrapper.js.map +1 -0
  11. package/dist/effect/LiveStore.d.ts +38 -0
  12. package/dist/effect/LiveStore.d.ts.map +1 -0
  13. package/dist/effect/LiveStore.js +38 -0
  14. package/dist/effect/LiveStore.js.map +1 -0
  15. package/dist/effect/index.d.ts +2 -0
  16. package/dist/effect/index.d.ts.map +1 -0
  17. package/dist/effect/index.js +2 -0
  18. package/dist/effect/index.js.map +1 -0
  19. package/dist/global-state.d.ts +14 -0
  20. package/dist/global-state.d.ts.map +1 -0
  21. package/dist/global-state.js +16 -0
  22. package/dist/global-state.js.map +1 -0
  23. package/dist/index.d.ts +19 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +15 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/reactive.d.ts +163 -0
  28. package/dist/reactive.d.ts.map +1 -0
  29. package/dist/reactive.js +382 -0
  30. package/dist/reactive.js.map +1 -0
  31. package/dist/reactive.test.d.ts +2 -0
  32. package/dist/reactive.test.d.ts.map +1 -0
  33. package/dist/reactive.test.js +345 -0
  34. package/dist/reactive.test.js.map +1 -0
  35. package/dist/reactiveQueries/base-class.d.ts +59 -0
  36. package/dist/reactiveQueries/base-class.d.ts.map +1 -0
  37. package/dist/reactiveQueries/base-class.js +29 -0
  38. package/dist/reactiveQueries/base-class.js.map +1 -0
  39. package/dist/reactiveQueries/graphql.d.ts +52 -0
  40. package/dist/reactiveQueries/graphql.d.ts.map +1 -0
  41. package/dist/reactiveQueries/graphql.js +136 -0
  42. package/dist/reactiveQueries/graphql.js.map +1 -0
  43. package/dist/reactiveQueries/js.d.ts +35 -0
  44. package/dist/reactiveQueries/js.d.ts.map +1 -0
  45. package/dist/reactiveQueries/js.js +57 -0
  46. package/dist/reactiveQueries/js.js.map +1 -0
  47. package/dist/reactiveQueries/sql.d.ts +49 -0
  48. package/dist/reactiveQueries/sql.d.ts.map +1 -0
  49. package/dist/reactiveQueries/sql.js +130 -0
  50. package/dist/reactiveQueries/sql.js.map +1 -0
  51. package/dist/reactiveQueries/sql.test.d.ts +2 -0
  52. package/dist/reactiveQueries/sql.test.d.ts.map +1 -0
  53. package/dist/reactiveQueries/sql.test.js +284 -0
  54. package/dist/reactiveQueries/sql.test.js.map +1 -0
  55. package/dist/row-query.d.ts +33 -0
  56. package/dist/row-query.d.ts.map +1 -0
  57. package/dist/row-query.js +84 -0
  58. package/dist/row-query.js.map +1 -0
  59. package/dist/store-context.d.ts +26 -0
  60. package/dist/store-context.d.ts.map +1 -0
  61. package/dist/store-context.js +6 -0
  62. package/dist/store-context.js.map +1 -0
  63. package/dist/store-devtools.d.ts +19 -0
  64. package/dist/store-devtools.d.ts.map +1 -0
  65. package/dist/store-devtools.js +141 -0
  66. package/dist/store-devtools.js.map +1 -0
  67. package/dist/store.d.ts +175 -0
  68. package/dist/store.d.ts.map +1 -0
  69. package/dist/store.js +507 -0
  70. package/dist/store.js.map +1 -0
  71. package/dist/utils/data-structures.d.ts +10 -0
  72. package/dist/utils/data-structures.d.ts.map +1 -0
  73. package/dist/utils/data-structures.js +32 -0
  74. package/dist/utils/data-structures.js.map +1 -0
  75. package/dist/utils/dev.d.ts +3 -0
  76. package/dist/utils/dev.d.ts.map +1 -0
  77. package/dist/utils/dev.js +17 -0
  78. package/dist/utils/dev.js.map +1 -0
  79. package/dist/utils/otel.d.ts +4 -0
  80. package/dist/utils/otel.d.ts.map +1 -0
  81. package/dist/utils/otel.js +6 -0
  82. package/dist/utils/otel.js.map +1 -0
  83. package/dist/utils/stack-info.d.ts +10 -0
  84. package/dist/utils/stack-info.d.ts.map +1 -0
  85. package/dist/utils/stack-info.js +41 -0
  86. package/dist/utils/stack-info.js.map +1 -0
  87. package/dist/utils/stack-info.test.d.ts +2 -0
  88. package/dist/utils/stack-info.test.d.ts.map +1 -0
  89. package/dist/utils/stack-info.test.js +75 -0
  90. package/dist/utils/stack-info.test.js.map +1 -0
  91. package/dist/utils/tests/fixture.d.ts +259 -0
  92. package/dist/utils/tests/fixture.d.ts.map +1 -0
  93. package/dist/utils/tests/fixture.js +33 -0
  94. package/dist/utils/tests/fixture.js.map +1 -0
  95. package/dist/utils/tests/mod.d.ts +3 -0
  96. package/dist/utils/tests/mod.d.ts.map +1 -0
  97. package/dist/utils/tests/mod.js +3 -0
  98. package/dist/utils/tests/mod.js.map +1 -0
  99. package/dist/utils/tests/otel.d.ts +10 -0
  100. package/dist/utils/tests/otel.d.ts.map +1 -0
  101. package/dist/utils/tests/otel.js +42 -0
  102. package/dist/utils/tests/otel.js.map +1 -0
  103. package/package.json +60 -0
  104. package/src/QueryCache.ts +81 -0
  105. package/src/SynchronousDatabaseWrapper.ts +256 -0
  106. package/src/ambient.d.ts +10 -0
  107. package/src/effect/LiveStore.ts +112 -0
  108. package/src/effect/index.ts +8 -0
  109. package/src/global-state.ts +20 -0
  110. package/src/index.ts +64 -0
  111. package/src/reactive.test.ts +426 -0
  112. package/src/reactive.ts +661 -0
  113. package/src/reactiveQueries/base-class.ts +115 -0
  114. package/src/reactiveQueries/graphql.ts +233 -0
  115. package/src/reactiveQueries/js.ts +108 -0
  116. package/src/reactiveQueries/sql.test.ts +308 -0
  117. package/src/reactiveQueries/sql.ts +226 -0
  118. package/src/row-query.ts +200 -0
  119. package/src/store-context.ts +23 -0
  120. package/src/store-devtools.ts +217 -0
  121. package/src/store.ts +920 -0
  122. package/src/utils/data-structures.ts +36 -0
  123. package/src/utils/dev.ts +24 -0
  124. package/src/utils/otel.ts +9 -0
  125. package/src/utils/stack-info.test.ts +79 -0
  126. package/src/utils/stack-info.ts +54 -0
  127. package/src/utils/tests/fixture.ts +77 -0
  128. package/src/utils/tests/mod.ts +2 -0
  129. package/src/utils/tests/otel.ts +61 -0
  130. package/tsconfig.json +18 -0
  131. package/vitest.config.js +9 -0
@@ -0,0 +1,661 @@
1
+ // This is a simple implementation of a reactive dependency graph.
2
+
3
+ // Key Terminology:
4
+ // Ref: a mutable cell where values can be set
5
+ // Thunk: a pure computation that depends on other values
6
+ // Effect: a side effect that runs when a value changes; return value is ignored
7
+ // Atom: a node returning a value that can be depended on: Ref | Thunk
8
+
9
+ // Super computation: Nodes that depend on a given node ("downstream")
10
+ // Sub computation: Nodes that a given node depends on ("upstream")
11
+
12
+ // This vocabulary comes from the MiniAdapton paper linked below, although
13
+ // we don't actually implement the MiniAdapton algorithm because we don't need lazy recomputation.
14
+ // https://arxiv.org/abs/1609.05337
15
+
16
+ // Features:
17
+ // - Dependencies are tracked automatically in thunk computations by using a getter function
18
+ // to reference other atoms.
19
+ // - Whenever a ref is updated, the graph is eagerly refreshed to be consistent with the new values.
20
+ // - We minimize recomputation by refreshing the graph in topological sort order. (The topological height
21
+ // is maintained eagerly as edges are added and removed.)
22
+ // - At every thunk we check value equality with the previous value and cutoff propagation if possible.
23
+
24
+ /* eslint-disable prefer-arrow/prefer-arrow-functions */
25
+
26
+ import { BoundArray } from '@livestore/common'
27
+ import type { PrettifyFlat } from '@livestore/utils'
28
+ import { deepEqual, shouldNeverHappen } from '@livestore/utils'
29
+ import type * as otel from '@opentelemetry/api'
30
+ // import { getDurationMsFromSpan } from './otel.js'
31
+
32
+ export const NOT_REFRESHED_YET = Symbol.for('NOT_REFRESHED_YET')
33
+ export type NOT_REFRESHED_YET = typeof NOT_REFRESHED_YET
34
+
35
+ export type GetAtom = <T>(atom: Atom<T, any, any>, otelContext?: otel.Context) => T
36
+
37
+ export type Ref<T, TContext, TDebugRefreshReason extends DebugRefreshReason> = {
38
+ _tag: 'ref'
39
+ id: string
40
+ isDirty: false
41
+ isDestroyed: boolean
42
+ previousResult: T
43
+ computeResult: () => T
44
+ sub: Set<Atom<any, TContext, TDebugRefreshReason>> // always empty
45
+ super: Set<Thunk<any, TContext, TDebugRefreshReason> | Effect>
46
+ label?: string
47
+ /** Container for meta information (e.g. the LiveStore Store) */
48
+ meta?: any
49
+ equal: (a: T, b: T) => boolean
50
+ refreshes: number
51
+ }
52
+
53
+ export type Thunk<TResult, TContext, TDebugRefreshReason extends DebugRefreshReason> = {
54
+ _tag: 'thunk'
55
+ id: string
56
+ isDirty: boolean
57
+ isDestroyed: boolean
58
+ computeResult: (otelContext?: otel.Context, debugRefreshReason?: TDebugRefreshReason) => TResult
59
+ previousResult: TResult | NOT_REFRESHED_YET
60
+ sub: Set<Atom<any, TContext, TDebugRefreshReason>>
61
+ super: Set<Thunk<any, TContext, TDebugRefreshReason> | Effect>
62
+ label?: string
63
+ /** Container for meta information (e.g. the LiveStore Store) */
64
+ meta?: any
65
+ equal: (a: TResult, b: TResult) => boolean
66
+ recomputations: number
67
+
68
+ __getResult: any
69
+ }
70
+
71
+ export type Atom<T, TContext, TDebugRefreshReason extends DebugRefreshReason> =
72
+ | Ref<T, TContext, TDebugRefreshReason>
73
+ | Thunk<T, TContext, TDebugRefreshReason>
74
+
75
+ export type Effect = {
76
+ _tag: 'effect'
77
+ id: string
78
+ isDestroyed: boolean
79
+ doEffect: (otelContext?: otel.Context) => void
80
+ sub: Set<Atom<any, TODO, TODO>>
81
+ label?: string
82
+ invocations: number
83
+ }
84
+
85
+ export type Node<T, TContext, TDebugRefreshReason extends DebugRefreshReason> =
86
+ | Atom<T, TContext, TDebugRefreshReason>
87
+ | Effect
88
+
89
+ export const isThunk = <T, TContext, TDebugRefreshReason extends DebugRefreshReason>(
90
+ obj: unknown,
91
+ ): obj is Thunk<T, TContext, TDebugRefreshReason> => {
92
+ return typeof obj === 'object' && obj !== null && '_tag' in obj && (obj as any)._tag === 'thunk'
93
+ }
94
+
95
+ export type DebugThunkInfo<T extends string = string> = {
96
+ _tag: T
97
+ durationMs: number
98
+ }
99
+
100
+ export type DebugRefreshReasonBase =
101
+ /** Usually in response to some `mutate` calls with `skipRefresh: true` */
102
+ | {
103
+ _tag: 'runDeferredEffects'
104
+ originalRefreshReasons?: ReadonlyArray<DebugRefreshReasonBase>
105
+ manualRefreshReason?: DebugRefreshReasonBase
106
+ }
107
+ | { _tag: 'makeThunk'; label?: string }
108
+ | { _tag: 'unknown' }
109
+
110
+ export type DebugRefreshReason<T extends string = string> = DebugRefreshReasonBase | { _tag: T }
111
+
112
+ export type AtomDebugInfo<TDebugThunkInfo extends DebugThunkInfo> = {
113
+ atom: SerializedAtom
114
+ resultChanged: boolean
115
+ debugInfo: TDebugThunkInfo
116
+ }
117
+
118
+ // TODO possibly find a better name for "refresh"
119
+ export type RefreshDebugInfo<TDebugRefreshReason extends DebugRefreshReason, TDebugThunkInfo extends DebugThunkInfo> = {
120
+ /** Currently only used for easier handling in React (e.g. as key) */
121
+ id: string
122
+ reason: TDebugRefreshReason
123
+ refreshedAtoms: AtomDebugInfo<TDebugThunkInfo>[]
124
+ skippedRefresh: boolean
125
+ durationMs: number
126
+ /** Note we're using a regular `Date.now()` timestamp here as it's faster to produce and we don't need the fine accuracy */
127
+ completedTimestamp: number
128
+ graphSnapshot: ReactiveGraphSnapshot
129
+ }
130
+
131
+ const unknownRefreshReason = () => {
132
+ // debugger
133
+ return { _tag: 'unknown' as const }
134
+ }
135
+
136
+ export type EncodedOption<A> = { _tag: 'Some'; value?: A } | { _tag: 'None' }
137
+ const encodedOptionSome = <A>(value: A): EncodedOption<A> => ({ _tag: 'Some', value })
138
+ const encodedOptionNone = <A>(): EncodedOption<A> => ({ _tag: 'None' })
139
+
140
+ export type SerializedAtom = SerializedRef | SerializedThunk
141
+
142
+ export type SerializedRef = Readonly<
143
+ PrettifyFlat<
144
+ Pick<Ref<unknown, unknown, any>, '_tag' | 'id' | 'label' | 'meta' | 'isDirty' | 'isDestroyed' | 'refreshes'> & {
145
+ /** Is `None` if `getSnapshot` was called with `includeResults: false` which is the default */
146
+ previousResult: EncodedOption<string>
147
+ sub: ReadonlyArray<string>
148
+ super: ReadonlyArray<string>
149
+ }
150
+ >
151
+ >
152
+
153
+ export type SerializedThunk = Readonly<
154
+ PrettifyFlat<
155
+ Pick<
156
+ Thunk<unknown, unknown, any>,
157
+ '_tag' | 'id' | 'label' | 'meta' | 'isDirty' | 'isDestroyed' | 'recomputations'
158
+ > & {
159
+ /** Is `None` if `getSnapshot` was called with `includeResults: false` which is the default */
160
+ previousResult: EncodedOption<string>
161
+ sub: ReadonlyArray<string>
162
+ super: ReadonlyArray<string>
163
+ }
164
+ >
165
+ >
166
+
167
+ export type SerializedEffect = Readonly<
168
+ PrettifyFlat<
169
+ Pick<Effect, '_tag' | 'id' | 'label' | 'invocations' | 'isDestroyed'> & {
170
+ sub: ReadonlyArray<string>
171
+ }
172
+ >
173
+ >
174
+
175
+ export type ReactiveGraphSnapshot = {
176
+ readonly atoms: ReadonlyArray<SerializedAtom>
177
+ readonly effects: ReadonlyArray<SerializedEffect>
178
+ /** IDs of deferred effects */
179
+ readonly deferredEffects: ReadonlyArray<string>
180
+ }
181
+
182
+ let nodeIdCounter = 0
183
+ const uniqueNodeId = () => `node-${++nodeIdCounter}`
184
+ let refreshInfoIdCounter = 0
185
+ const uniqueRefreshInfoId = () => `refresh-info-${++refreshInfoIdCounter}`
186
+
187
+ let globalGraphIdCounter = 0
188
+ const uniqueGraphId = () => `graph-${++globalGraphIdCounter}`
189
+
190
+ export class ReactiveGraph<
191
+ TDebugRefreshReason extends DebugRefreshReason,
192
+ TDebugThunkInfo extends DebugThunkInfo,
193
+ TContext extends { effectsWrapper?: (runEffects: () => void) => void } = {},
194
+ > {
195
+ id = uniqueGraphId()
196
+
197
+ readonly atoms: Set<Atom<any, TContext, TDebugRefreshReason>> = new Set()
198
+ readonly effects: Set<Effect> = new Set()
199
+
200
+ context: TContext | undefined
201
+
202
+ debugRefreshInfos: BoundArray<RefreshDebugInfo<TDebugRefreshReason, TDebugThunkInfo>> = new BoundArray(5000)
203
+
204
+ private currentDebugRefresh:
205
+ | { refreshedAtoms: AtomDebugInfo<TDebugThunkInfo>[]; startMs: DOMHighResTimeStamp }
206
+ | undefined
207
+
208
+ private deferredEffects: Map<Effect, Set<TDebugRefreshReason>> = new Map()
209
+
210
+ private refreshCallbacks: Set<() => void> = new Set()
211
+
212
+ makeRef<T>(
213
+ val: T,
214
+ options?: { label?: string; meta?: unknown; equal?: (a: T, b: T) => boolean },
215
+ ): Ref<T, TContext, TDebugRefreshReason> {
216
+ const ref: Ref<T, TContext, TDebugRefreshReason> = {
217
+ _tag: 'ref',
218
+ id: uniqueNodeId(),
219
+ isDirty: false,
220
+ isDestroyed: false,
221
+ previousResult: val,
222
+ computeResult: () => ref.previousResult,
223
+ sub: new Set(),
224
+ super: new Set(),
225
+ label: options?.label,
226
+ meta: options?.meta,
227
+ equal: options?.equal ?? deepEqual,
228
+ refreshes: 0,
229
+ }
230
+
231
+ this.atoms.add(ref)
232
+
233
+ return ref
234
+ }
235
+
236
+ makeThunk<T>(
237
+ getResult: (
238
+ get: GetAtom,
239
+ setDebugInfo: (debugInfo: TDebugThunkInfo) => void,
240
+ ctx: TContext,
241
+ otelContext: otel.Context | undefined,
242
+ ) => T,
243
+ options?:
244
+ | {
245
+ label?: string
246
+ meta?: any
247
+ equal?: (a: T, b: T) => boolean
248
+ }
249
+ | undefined,
250
+ ): Thunk<T, TContext, TDebugRefreshReason> {
251
+ const thunk: Thunk<T, TContext, TDebugRefreshReason> = {
252
+ _tag: 'thunk',
253
+ id: uniqueNodeId(),
254
+ previousResult: NOT_REFRESHED_YET,
255
+ isDirty: true,
256
+ isDestroyed: false,
257
+ computeResult: (otelContext, debugRefreshReason) => {
258
+ if (thunk.isDirty) {
259
+ const neededCurrentRefresh = this.currentDebugRefresh === undefined
260
+ if (neededCurrentRefresh) {
261
+ this.currentDebugRefresh = { refreshedAtoms: [], startMs: performance.now() }
262
+ }
263
+
264
+ // Reset previous subcomputations as we're about to re-add them as part of the `doEffect` call below
265
+ thunk.sub = new Set()
266
+
267
+ const getAtom = (atom: Atom<T, TContext, TDebugRefreshReason>, otelContext: otel.Context) => {
268
+ this.addEdge(thunk, atom)
269
+ return compute(atom, otelContext)
270
+ }
271
+
272
+ let debugInfo: TDebugThunkInfo | undefined = undefined
273
+ const setDebugInfo = (debugInfo_: TDebugThunkInfo) => {
274
+ debugInfo = debugInfo_
275
+ }
276
+
277
+ const result = getResult(
278
+ getAtom as GetAtom,
279
+ setDebugInfo,
280
+ this.context ?? throwContextNotSetError(this),
281
+ otelContext,
282
+ )
283
+
284
+ const resultChanged = thunk.equal(thunk.previousResult as T, result) === false
285
+
286
+ const debugInfoForAtom = {
287
+ atom: serializeAtom(thunk, false),
288
+ resultChanged,
289
+ debugInfo: debugInfo ?? (unknownRefreshReason() as TDebugThunkInfo),
290
+ } satisfies AtomDebugInfo<TDebugThunkInfo>
291
+
292
+ this.currentDebugRefresh!.refreshedAtoms.push(debugInfoForAtom)
293
+
294
+ thunk.isDirty = false
295
+ thunk.previousResult = result
296
+ thunk.recomputations++
297
+
298
+ if (neededCurrentRefresh) {
299
+ const refreshedAtoms = this.currentDebugRefresh!.refreshedAtoms
300
+ const durationMs = performance.now() - this.currentDebugRefresh!.startMs
301
+ this.currentDebugRefresh = undefined
302
+
303
+ this.debugRefreshInfos.push({
304
+ id: uniqueRefreshInfoId(),
305
+ reason: debugRefreshReason ?? ({ _tag: 'makeThunk', label: options?.label } as TDebugRefreshReason),
306
+ skippedRefresh: false,
307
+ refreshedAtoms,
308
+ durationMs,
309
+ completedTimestamp: Date.now(),
310
+ graphSnapshot: this.getSnapshot({ includeResults: false }),
311
+ })
312
+ }
313
+
314
+ return result
315
+ } else {
316
+ return thunk.previousResult as T
317
+ }
318
+ },
319
+ sub: new Set(),
320
+ super: new Set(),
321
+ recomputations: 0,
322
+ label: options?.label,
323
+ meta: options?.meta,
324
+ equal: options?.equal ?? deepEqual,
325
+ __getResult: getResult,
326
+ }
327
+
328
+ this.atoms.add(thunk)
329
+
330
+ return thunk
331
+ }
332
+
333
+ destroyNode(node: Node<any, TContext, TDebugRefreshReason>) {
334
+ // console.debug(`destroying node (${node._tag})`, node.id, node.label)
335
+
336
+ // Recursively destroy any supercomputations
337
+ if (node._tag === 'ref' || node._tag === 'thunk') {
338
+ for (const superComp of node.super) {
339
+ this.destroyNode(superComp)
340
+ }
341
+ }
342
+
343
+ // Destroy this node
344
+ if (node._tag !== 'ref') {
345
+ for (const subComp of node.sub) {
346
+ this.removeEdge(node, subComp)
347
+ }
348
+ }
349
+
350
+ if (node._tag === 'effect') {
351
+ this.deferredEffects.delete(node)
352
+ this.effects.delete(node)
353
+ } else {
354
+ this.atoms.delete(node)
355
+ }
356
+
357
+ node.isDestroyed = true
358
+ }
359
+
360
+ destroy() {
361
+ // NOTE we don't need to sort the atoms first, as `destroyNode` will recursively destroy all supercomputations
362
+ for (const node of this.atoms) {
363
+ this.destroyNode(node)
364
+ }
365
+ }
366
+
367
+ makeEffect(
368
+ doEffect: (get: GetAtom, otelContext?: otel.Context) => void,
369
+ options?: { label?: string } | undefined,
370
+ ): Effect {
371
+ const effect: Effect = {
372
+ _tag: 'effect',
373
+ id: uniqueNodeId(),
374
+ isDestroyed: false,
375
+ doEffect: (otelContext) => {
376
+ effect.invocations++
377
+
378
+ // NOTE we're not tracking any debug refresh info for effects as they're tracked by the thunks they depend on
379
+
380
+ // Reset previous subcomputations as we're about to re-add them as part of the `doEffect` call below
381
+ effect.sub = new Set()
382
+
383
+ const getAtom = (atom: Atom<any, TContext, TDebugRefreshReason>, otelContext: otel.Context) => {
384
+ this.addEdge(effect, atom)
385
+ return compute(atom, otelContext)
386
+ }
387
+
388
+ doEffect(getAtom as GetAtom, otelContext)
389
+ },
390
+ sub: new Set(),
391
+ label: options?.label,
392
+ invocations: 0,
393
+ }
394
+
395
+ this.effects.add(effect)
396
+
397
+ return effect
398
+ }
399
+
400
+ setRef<T>(
401
+ ref: Ref<T, TContext, TDebugRefreshReason>,
402
+ val: T,
403
+ options?:
404
+ | {
405
+ skipRefresh?: boolean
406
+ debugRefreshReason?: TDebugRefreshReason
407
+ otelContext?: otel.Context
408
+ }
409
+ | undefined,
410
+ ) {
411
+ this.setRefs([[ref, val]], options)
412
+ }
413
+
414
+ setRefs<T>(
415
+ refs: [Ref<T, TContext, TDebugRefreshReason>, T][],
416
+ options?:
417
+ | {
418
+ skipRefresh?: boolean
419
+ debugRefreshReason?: TDebugRefreshReason
420
+ otelContext?: otel.Context
421
+ }
422
+ | undefined,
423
+ ) {
424
+ const effectsToRefresh = new Set<Effect>()
425
+ for (const [ref, val] of refs) {
426
+ ref.previousResult = val
427
+ ref.refreshes++
428
+
429
+ markSuperCompDirtyRec(ref, effectsToRefresh)
430
+ }
431
+
432
+ if (options?.skipRefresh) {
433
+ for (const effect of effectsToRefresh) {
434
+ if (this.deferredEffects.has(effect) === false) {
435
+ this.deferredEffects.set(effect, new Set())
436
+ }
437
+
438
+ if (options?.debugRefreshReason !== undefined) {
439
+ this.deferredEffects.get(effect)!.add(options.debugRefreshReason)
440
+ }
441
+ }
442
+ } else {
443
+ this.runEffects(effectsToRefresh, {
444
+ debugRefreshReason: options?.debugRefreshReason ?? (unknownRefreshReason() as TDebugRefreshReason),
445
+ otelContext: options?.otelContext,
446
+ })
447
+ }
448
+ }
449
+
450
+ private runEffects = (
451
+ effectsToRefresh: Set<Effect>,
452
+ options: {
453
+ debugRefreshReason: TDebugRefreshReason
454
+ otelContext?: otel.Context
455
+ },
456
+ ) => {
457
+ const effectsWrapper = this.context?.effectsWrapper ?? ((runEffects: () => void) => runEffects())
458
+ effectsWrapper(() => {
459
+ this.currentDebugRefresh = { refreshedAtoms: [], startMs: performance.now() }
460
+
461
+ for (const effect of effectsToRefresh) {
462
+ effect.doEffect(options?.otelContext)
463
+ }
464
+
465
+ const refreshedAtoms = this.currentDebugRefresh.refreshedAtoms
466
+ const durationMs = performance.now() - this.currentDebugRefresh.startMs
467
+ this.currentDebugRefresh = undefined
468
+
469
+ const refreshDebugInfo: RefreshDebugInfo<TDebugRefreshReason, TDebugThunkInfo> = {
470
+ id: uniqueRefreshInfoId(),
471
+ reason: options.debugRefreshReason,
472
+ skippedRefresh: false,
473
+ refreshedAtoms,
474
+ durationMs,
475
+ completedTimestamp: Date.now(),
476
+ graphSnapshot: this.getSnapshot({ includeResults: false }),
477
+ }
478
+ this.debugRefreshInfos.push(refreshDebugInfo)
479
+
480
+ this.runRefreshCallbacks()
481
+ })
482
+ }
483
+
484
+ runDeferredEffects = (options?: { debugRefreshReason?: TDebugRefreshReason; otelContext?: otel.Context }) => {
485
+ // TODO improve how refresh reasons are propagated for deferred effect execution
486
+ // TODO also improve "batching" of running deferred effects (i.e. in a single `this.runEffects` call)
487
+ // but need to be careful to not overwhelm the main thread
488
+ for (const [effect, debugRefreshReasons] of this.deferredEffects) {
489
+ this.runEffects(new Set([effect]), {
490
+ debugRefreshReason: {
491
+ _tag: 'runDeferredEffects',
492
+ originalRefreshReasons: Array.from(debugRefreshReasons) as ReadonlyArray<DebugRefreshReasonBase>,
493
+ manualRefreshReason: options?.debugRefreshReason,
494
+ } as TDebugRefreshReason,
495
+ otelContext: options?.otelContext,
496
+ })
497
+ }
498
+ }
499
+
500
+ runRefreshCallbacks = () => {
501
+ for (const cb of this.refreshCallbacks) {
502
+ cb()
503
+ }
504
+ }
505
+
506
+ addEdge(
507
+ superComp: Thunk<any, TContext, TDebugRefreshReason> | Effect,
508
+ subComp: Atom<any, TContext, TDebugRefreshReason>,
509
+ ) {
510
+ superComp.sub.add(subComp)
511
+ subComp.super.add(superComp)
512
+
513
+ if (this.currentDebugRefresh === undefined) {
514
+ this.runRefreshCallbacks()
515
+ }
516
+ }
517
+
518
+ removeEdge(
519
+ superComp: Thunk<any, TContext, TDebugRefreshReason> | Effect,
520
+ subComp: Atom<any, TContext, TDebugRefreshReason>,
521
+ ) {
522
+ superComp.sub.delete(subComp)
523
+ const effectsToRefresh = new Set<Effect>()
524
+ markSuperCompDirtyRec(subComp, effectsToRefresh)
525
+
526
+ for (const effect of effectsToRefresh) {
527
+ this.deferredEffects.set(effect, new Set())
528
+ }
529
+
530
+ subComp.super.delete(superComp)
531
+
532
+ if (this.currentDebugRefresh === undefined) {
533
+ this.runRefreshCallbacks()
534
+ }
535
+ }
536
+
537
+ // NOTE This function is performance-optimized (i.e. not using `Array.from`)
538
+ getSnapshot = (opts?: { includeResults: boolean }): ReactiveGraphSnapshot => {
539
+ const { includeResults = false } = opts ?? {}
540
+ const atoms: SerializedAtom[] = []
541
+ for (const atom of this.atoms) {
542
+ atoms.push(serializeAtom(atom, includeResults))
543
+ }
544
+
545
+ const effects: SerializedEffect[] = []
546
+ for (const effect of this.effects) {
547
+ effects.push(serializeEffect(effect))
548
+ }
549
+
550
+ const deferredEffects: string[] = []
551
+ for (const [effect] of this.deferredEffects) {
552
+ deferredEffects.push(effect.id)
553
+ }
554
+
555
+ return { atoms, effects, deferredEffects }
556
+ }
557
+
558
+ subscribeToRefresh = (cb: () => void) => {
559
+ this.refreshCallbacks.add(cb)
560
+ return () => {
561
+ this.refreshCallbacks.delete(cb)
562
+ }
563
+ }
564
+ }
565
+
566
+ const compute = <T>(atom: Atom<T, unknown, any>, otelContext: otel.Context): T => {
567
+ // const __getResult = atom._tag === 'thunk' ? atom.__getResult.toString() : ''
568
+ if (atom.isDestroyed) {
569
+ shouldNeverHappen(`LiveStore Error: Attempted to compute destroyed ${atom._tag} (${atom.id}): ${atom.label ?? ''}`)
570
+ }
571
+
572
+ if (atom.isDirty) {
573
+ // console.log('atom is dirty', atom.id, atom.label ?? '', atom._tag, __getResult)
574
+ const result = atom.computeResult(otelContext)
575
+ atom.isDirty = false
576
+ atom.previousResult = result
577
+ return result
578
+ } else {
579
+ // console.log('atom is clean', atom.id, atom.label ?? '', atom._tag, __getResult)
580
+ return atom.previousResult as T
581
+ }
582
+ }
583
+
584
+ const markSuperCompDirtyRec = <T>(atom: Atom<T, unknown, any>, effectsToRefresh: Set<Effect>) => {
585
+ for (const superComp of atom.super) {
586
+ if (superComp._tag === 'thunk') {
587
+ superComp.isDirty = true
588
+ markSuperCompDirtyRec(superComp, effectsToRefresh)
589
+ } else {
590
+ effectsToRefresh.add(superComp)
591
+ }
592
+ }
593
+ }
594
+
595
+ export const throwContextNotSetError = (graph: ReactiveGraph<any, any, any>): never => {
596
+ throw new Error(`LiveStore Error: \`context\` not set on ReactiveGraph (${graph.id})`)
597
+ }
598
+
599
+ // NOTE This function is performance-optimized (i.e. not using `pick` and `Array.from`)
600
+ const serializeAtom = (atom: Atom<any, unknown, any>, includeResult: boolean): SerializedAtom => {
601
+ const sub: string[] = []
602
+ for (const a of atom.sub) {
603
+ sub.push(a.id)
604
+ }
605
+
606
+ const super_: string[] = []
607
+ for (const a of atom.super) {
608
+ super_.push(a.id)
609
+ }
610
+
611
+ const previousResult: EncodedOption<string> = includeResult
612
+ ? encodedOptionSome(
613
+ atom.previousResult === NOT_REFRESHED_YET ? '"SYMBOL_NOT_REFRESHED_YET"' : JSON.stringify(atom.previousResult),
614
+ )
615
+ : encodedOptionNone()
616
+
617
+ if (atom._tag === 'ref') {
618
+ return {
619
+ _tag: atom._tag,
620
+ id: atom.id,
621
+ label: atom.label,
622
+ meta: atom.meta,
623
+ isDirty: atom.isDirty,
624
+ sub,
625
+ super: super_,
626
+ isDestroyed: atom.isDestroyed,
627
+ refreshes: atom.refreshes,
628
+ previousResult,
629
+ }
630
+ }
631
+
632
+ return {
633
+ _tag: 'thunk',
634
+ id: atom.id,
635
+ label: atom.label,
636
+ meta: atom.meta,
637
+ isDirty: atom.isDirty,
638
+ sub,
639
+ super: super_,
640
+ isDestroyed: atom.isDestroyed,
641
+ recomputations: atom.recomputations,
642
+ previousResult,
643
+ }
644
+ }
645
+
646
+ // NOTE This function is performance-optimized (i.e. not using `pick` and `Array.from`)
647
+ const serializeEffect = (effect: Effect): SerializedEffect => {
648
+ const sub: string[] = []
649
+ for (const a of effect.sub) {
650
+ sub.push(a.id)
651
+ }
652
+
653
+ return {
654
+ _tag: effect._tag,
655
+ id: effect.id,
656
+ label: effect.label,
657
+ sub,
658
+ invocations: effect.invocations,
659
+ isDestroyed: effect.isDestroyed,
660
+ }
661
+ }