@livestore/livestore 0.0.0

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 +108 -0
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/LiveRiffleStore.d.ts +42 -0
  4. package/dist/LiveRiffleStore.d.ts.map +1 -0
  5. package/dist/LiveRiffleStore.js +36 -0
  6. package/dist/LiveRiffleStore.js.map +1 -0
  7. package/dist/QueryCache.d.ts +20 -0
  8. package/dist/QueryCache.d.ts.map +1 -0
  9. package/dist/QueryCache.js +71 -0
  10. package/dist/QueryCache.js.map +1 -0
  11. package/dist/__tests__/react/fixture.d.ts +141 -0
  12. package/dist/__tests__/react/fixture.d.ts.map +1 -0
  13. package/dist/__tests__/react/fixture.js +72 -0
  14. package/dist/__tests__/react/fixture.js.map +1 -0
  15. package/dist/__tests__/react/useLiveStoreComponent.test.d.ts +2 -0
  16. package/dist/__tests__/react/useLiveStoreComponent.test.d.ts.map +1 -0
  17. package/dist/__tests__/react/useLiveStoreComponent.test.js +78 -0
  18. package/dist/__tests__/react/useLiveStoreComponent.test.js.map +1 -0
  19. package/dist/__tests__/react/useRiffleComponent.test.d.ts +2 -0
  20. package/dist/__tests__/react/useRiffleComponent.test.d.ts.map +1 -0
  21. package/dist/__tests__/react/useRiffleComponent.test.js +78 -0
  22. package/dist/__tests__/react/useRiffleComponent.test.js.map +1 -0
  23. package/dist/__tests__/reactive.test.d.ts +2 -0
  24. package/dist/__tests__/reactive.test.d.ts.map +1 -0
  25. package/dist/__tests__/reactive.test.js +167 -0
  26. package/dist/__tests__/reactive.test.js.map +1 -0
  27. package/dist/backends/base.d.ts +13 -0
  28. package/dist/backends/base.d.ts.map +1 -0
  29. package/dist/backends/base.js +53 -0
  30. package/dist/backends/base.js.map +1 -0
  31. package/dist/backends/index.d.ts +41 -0
  32. package/dist/backends/index.d.ts.map +1 -0
  33. package/dist/backends/index.js +38 -0
  34. package/dist/backends/index.js.map +1 -0
  35. package/dist/backends/noop.d.ts +18 -0
  36. package/dist/backends/noop.d.ts.map +1 -0
  37. package/dist/backends/noop.js +21 -0
  38. package/dist/backends/noop.js.map +1 -0
  39. package/dist/backends/tauri.d.ts +24 -0
  40. package/dist/backends/tauri.d.ts.map +1 -0
  41. package/dist/backends/tauri.js +48 -0
  42. package/dist/backends/tauri.js.map +1 -0
  43. package/dist/backends/utils/idb.d.ts +10 -0
  44. package/dist/backends/utils/idb.d.ts.map +1 -0
  45. package/dist/backends/utils/idb.js +58 -0
  46. package/dist/backends/utils/idb.js.map +1 -0
  47. package/dist/backends/web-in-memory.d.ts +24 -0
  48. package/dist/backends/web-in-memory.d.ts.map +1 -0
  49. package/dist/backends/web-in-memory.js +46 -0
  50. package/dist/backends/web-in-memory.js.map +1 -0
  51. package/dist/backends/web-worker.d.ts +17 -0
  52. package/dist/backends/web-worker.d.ts.map +1 -0
  53. package/dist/backends/web-worker.js +139 -0
  54. package/dist/backends/web-worker.js.map +1 -0
  55. package/dist/backends/web.d.ts +28 -0
  56. package/dist/backends/web.d.ts.map +1 -0
  57. package/dist/backends/web.js +64 -0
  58. package/dist/backends/web.js.map +1 -0
  59. package/dist/bounded-collections.d.ts +34 -0
  60. package/dist/bounded-collections.d.ts.map +1 -0
  61. package/dist/bounded-collections.js +103 -0
  62. package/dist/bounded-collections.js.map +1 -0
  63. package/dist/componentKey.d.ts +20 -0
  64. package/dist/componentKey.d.ts.map +1 -0
  65. package/dist/componentKey.js +3 -0
  66. package/dist/componentKey.js.map +1 -0
  67. package/dist/effect/LiveStore.d.ts +42 -0
  68. package/dist/effect/LiveStore.d.ts.map +1 -0
  69. package/dist/effect/LiveStore.js +36 -0
  70. package/dist/effect/LiveStore.js.map +1 -0
  71. package/dist/effect/index.d.ts +2 -0
  72. package/dist/effect/index.d.ts.map +1 -0
  73. package/dist/effect/index.js +2 -0
  74. package/dist/effect/index.js.map +1 -0
  75. package/dist/events.d.ts +7 -0
  76. package/dist/events.d.ts.map +1 -0
  77. package/dist/events.js +2 -0
  78. package/dist/events.js.map +1 -0
  79. package/dist/inMemoryDatabase.d.ts +65 -0
  80. package/dist/inMemoryDatabase.d.ts.map +1 -0
  81. package/dist/inMemoryDatabase.js +241 -0
  82. package/dist/inMemoryDatabase.js.map +1 -0
  83. package/dist/index.d.ts +20 -0
  84. package/dist/index.d.ts.map +1 -0
  85. package/dist/index.js +10 -0
  86. package/dist/index.js.map +1 -0
  87. package/dist/otel.d.ts +5 -0
  88. package/dist/otel.d.ts.map +1 -0
  89. package/dist/otel.js +17 -0
  90. package/dist/otel.js.map +1 -0
  91. package/dist/react/LiveStoreContext.d.ts +11 -0
  92. package/dist/react/LiveStoreContext.d.ts.map +1 -0
  93. package/dist/react/LiveStoreContext.js +10 -0
  94. package/dist/react/LiveStoreContext.js.map +1 -0
  95. package/dist/react/LiveStoreProvider.d.ts +21 -0
  96. package/dist/react/LiveStoreProvider.d.ts.map +1 -0
  97. package/dist/react/LiveStoreProvider.js +48 -0
  98. package/dist/react/LiveStoreProvider.js.map +1 -0
  99. package/dist/react/RiffleProvider.d.ts +21 -0
  100. package/dist/react/RiffleProvider.d.ts.map +1 -0
  101. package/dist/react/RiffleProvider.js +48 -0
  102. package/dist/react/RiffleProvider.js.map +1 -0
  103. package/dist/react/StoreContext.d.ts +11 -0
  104. package/dist/react/StoreContext.d.ts.map +1 -0
  105. package/dist/react/StoreContext.js +10 -0
  106. package/dist/react/StoreContext.js.map +1 -0
  107. package/dist/react/index.d.ts +7 -0
  108. package/dist/react/index.d.ts.map +1 -0
  109. package/dist/react/index.js +6 -0
  110. package/dist/react/index.js.map +1 -0
  111. package/dist/react/useGlobalQuery.d.ts +3 -0
  112. package/dist/react/useGlobalQuery.d.ts.map +1 -0
  113. package/dist/react/useGlobalQuery.js +25 -0
  114. package/dist/react/useGlobalQuery.js.map +1 -0
  115. package/dist/react/useGraphQL.d.ts +11 -0
  116. package/dist/react/useGraphQL.d.ts.map +1 -0
  117. package/dist/react/useGraphQL.js +68 -0
  118. package/dist/react/useGraphQL.js.map +1 -0
  119. package/dist/react/useLiveStoreComponent.d.ts +70 -0
  120. package/dist/react/useLiveStoreComponent.d.ts.map +1 -0
  121. package/dist/react/useLiveStoreComponent.js +261 -0
  122. package/dist/react/useLiveStoreComponent.js.map +1 -0
  123. package/dist/react/useRiffleComponent.d.ts +70 -0
  124. package/dist/react/useRiffleComponent.d.ts.map +1 -0
  125. package/dist/react/useRiffleComponent.js +261 -0
  126. package/dist/react/useRiffleComponent.js.map +1 -0
  127. package/dist/react/useRiffleJsonHook.d.ts +4 -0
  128. package/dist/react/useRiffleJsonHook.d.ts.map +1 -0
  129. package/dist/react/useRiffleJsonHook.js +21 -0
  130. package/dist/react/useRiffleJsonHook.js.map +1 -0
  131. package/dist/react/utils/useStateRefWithReactiveInput.d.ts +13 -0
  132. package/dist/react/utils/useStateRefWithReactiveInput.d.ts.map +1 -0
  133. package/dist/react/utils/useStateRefWithReactiveInput.js +38 -0
  134. package/dist/react/utils/useStateRefWithReactiveInput.js.map +1 -0
  135. package/dist/reactive.d.ts +140 -0
  136. package/dist/reactive.d.ts.map +1 -0
  137. package/dist/reactive.js +301 -0
  138. package/dist/reactive.js.map +1 -0
  139. package/dist/reactiveQueries/base-class.d.ts +24 -0
  140. package/dist/reactiveQueries/base-class.d.ts.map +1 -0
  141. package/dist/reactiveQueries/base-class.js +22 -0
  142. package/dist/reactiveQueries/base-class.js.map +1 -0
  143. package/dist/reactiveQueries/graphql.d.ts +25 -0
  144. package/dist/reactiveQueries/graphql.d.ts.map +1 -0
  145. package/dist/reactiveQueries/graphql.js +14 -0
  146. package/dist/reactiveQueries/graphql.js.map +1 -0
  147. package/dist/reactiveQueries/js.d.ts +19 -0
  148. package/dist/reactiveQueries/js.d.ts.map +1 -0
  149. package/dist/reactiveQueries/js.js +13 -0
  150. package/dist/reactiveQueries/js.js.map +1 -0
  151. package/dist/reactiveQueries/sql.d.ts +31 -0
  152. package/dist/reactiveQueries/sql.d.ts.map +1 -0
  153. package/dist/reactiveQueries/sql.js +28 -0
  154. package/dist/reactiveQueries/sql.js.map +1 -0
  155. package/dist/schema.d.ts +163 -0
  156. package/dist/schema.d.ts.map +1 -0
  157. package/dist/schema.js +92 -0
  158. package/dist/schema.js.map +1 -0
  159. package/dist/store.d.ts +175 -0
  160. package/dist/store.d.ts.map +1 -0
  161. package/dist/store.js +546 -0
  162. package/dist/store.js.map +1 -0
  163. package/dist/util.d.ts +24 -0
  164. package/dist/util.d.ts.map +1 -0
  165. package/dist/util.js +51 -0
  166. package/dist/util.js.map +1 -0
  167. package/package.json +52 -0
  168. package/src/QueryCache.ts +81 -0
  169. package/src/__tests__/react/fixture.tsx +106 -0
  170. package/src/__tests__/react/useLiveStoreComponent.test.tsx +111 -0
  171. package/src/__tests__/reactive.test.ts +227 -0
  172. package/src/ambient.d.ts +7 -0
  173. package/src/backends/base.ts +67 -0
  174. package/src/backends/index.ts +94 -0
  175. package/src/backends/noop.ts +32 -0
  176. package/src/backends/tauri.ts +74 -0
  177. package/src/backends/utils/idb.ts +71 -0
  178. package/src/backends/web-in-memory.ts +65 -0
  179. package/src/backends/web-worker.ts +176 -0
  180. package/src/backends/web.ts +96 -0
  181. package/src/bounded-collections.ts +112 -0
  182. package/src/componentKey.ts +9 -0
  183. package/src/effect/LiveStore.ts +123 -0
  184. package/src/effect/index.ts +7 -0
  185. package/src/events.ts +8 -0
  186. package/src/inMemoryDatabase.ts +347 -0
  187. package/src/index.ts +47 -0
  188. package/src/otel.ts +20 -0
  189. package/src/react/LiveStoreContext.ts +23 -0
  190. package/src/react/LiveStoreProvider.tsx +93 -0
  191. package/src/react/index.ts +11 -0
  192. package/src/react/useGlobalQuery.ts +40 -0
  193. package/src/react/useGraphQL.ts +113 -0
  194. package/src/react/useLiveStoreComponent.ts +493 -0
  195. package/src/react/utils/useStateRefWithReactiveInput.ts +51 -0
  196. package/src/reactive.ts +538 -0
  197. package/src/reactiveQueries/base-class.ts +49 -0
  198. package/src/reactiveQueries/graphql.ts +52 -0
  199. package/src/reactiveQueries/js.ts +38 -0
  200. package/src/reactiveQueries/sql.ts +65 -0
  201. package/src/schema.ts +219 -0
  202. package/src/store.ts +889 -0
  203. package/src/util.ts +59 -0
  204. package/tsconfig.json +15 -0
  205. package/vitest.config.js +13 -0
@@ -0,0 +1,538 @@
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
10
+ // Sub computation: Nodes that a given node depends on
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 type { PrettifyFlat } from '@livestore/utils'
27
+ import { pick } from '@livestore/utils'
28
+ import type * as otel from '@opentelemetry/api'
29
+ import { isEqual, max, uniqueId } from 'lodash-es'
30
+
31
+ import { BoundArray } from './bounded-collections.js'
32
+
33
+ export type GetAtom = <T>(atom: Atom<T>) => T
34
+
35
+ export type Ref<T> = {
36
+ _tag: 'ref'
37
+ id: string
38
+ result: T
39
+ height: 0
40
+ getResult: () => T
41
+ sub: Set<Atom<any>> // always empty
42
+ super: Set<Atom<any> | Effect>
43
+ label?: string
44
+ /** Container for meta information (e.g. the LiveStore Store) */
45
+ meta?: any
46
+ equal: (a: T, b: T) => boolean
47
+ }
48
+
49
+ type BaseThunk<T> = {
50
+ _tag: 'thunk'
51
+ id: string
52
+ height: number
53
+ getResult: (get: GetAtom, addDebugInfo: (debugInfo: any) => void) => T
54
+ sub: Set<Atom<any>>
55
+ super: Set<Atom<any> | Effect>
56
+ label?: string
57
+ /** Container for meta information (e.g. the LiveStore Store) */
58
+ meta?: any
59
+ equal: (a: T, b: T) => boolean
60
+ }
61
+
62
+ type UnevaluatedThunk<T> = BaseThunk<T> & { result: undefined }
63
+ export type Thunk<T> = BaseThunk<T> & { result: T }
64
+
65
+ export type Atom<T> = Ref<T> | Thunk<T>
66
+
67
+ export type Effect = {
68
+ _tag: 'effect'
69
+ 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
+ }
79
+ }
80
+
81
+ export type Taggable<T extends string = string> = { _tag: T }
82
+
83
+ export type ReactiveGraphOptions = {
84
+ effectsWrapper?: (runEffects: () => void) => void
85
+ otelTracer: otel.Tracer
86
+ }
87
+
88
+ export type AtomDebugInfo<TDebugThunkInfo extends Taggable> = {
89
+ atom: SerializedAtom
90
+ resultChanged: boolean
91
+ durationMs: number
92
+ debugInfo: TDebugThunkInfo
93
+ }
94
+
95
+ export type RefreshDebugInfo<TDebugRefreshReason extends Taggable, TDebugThunkInfo extends Taggable> = {
96
+ /** Currently only used for easier handling in React (e.g. as key) */
97
+ id: string
98
+ reason: TDebugRefreshReason
99
+ refreshedAtoms: AtomDebugInfo<TDebugThunkInfo>[]
100
+ skippedRefresh: boolean
101
+ durationMs: number
102
+ /** Note we're using a regular `Date.now()` timestamp here as it's faster to produce and we don't need the fine accuracy */
103
+ completedTimestamp: number
104
+ graphSnapshot: ReactiveGraphSnapshot
105
+ }
106
+
107
+ export type RefreshReasonWithGenericReasons<T extends Taggable> =
108
+ | T
109
+ | {
110
+ _tag: 'makeThunk'
111
+ label?: string
112
+ }
113
+ | {
114
+ _tag: 'makeEffect'
115
+ label?: string
116
+ }
117
+ | { _tag: 'unknown' }
118
+
119
+ export const unknownRefreshReason = () => {
120
+ debugger
121
+ return { _tag: 'unknown' as const }
122
+ }
123
+
124
+ export type SerializedAtom = Readonly<
125
+ PrettifyFlat<
126
+ Pick<Atom<unknown>, '_tag' | 'height' | 'id' | 'label' | 'meta' | 'result'> & {
127
+ sub: string[]
128
+ super: string[]
129
+ }
130
+ >
131
+ >
132
+
133
+ export type SerializedEffect = Readonly<PrettifyFlat<Pick<Effect, '_tag' | 'id'>>>
134
+
135
+ type ReactiveGraphSnapshot = {
136
+ readonly atoms: SerializedAtom[]
137
+ readonly effects: SerializedEffect[]
138
+ /** IDs of atoms and effects that are dirty */
139
+ readonly dirtyNodes: string[]
140
+ }
141
+
142
+ const uniqueNodeId = () => uniqueId('node-')
143
+ const uniqueRefreshInfoId = () => uniqueId('refresh-info-')
144
+
145
+ const serializeAtom = (atom: Atom<any>): SerializedAtom => ({
146
+ ...pick(atom, ['_tag', 'height', 'id', 'label', 'meta', 'result']),
147
+ sub: Array.from(atom.sub).map((a) => a.id),
148
+ super: Array.from(atom.super).map((a) => a.id),
149
+ })
150
+
151
+ const serializeEffect = (effect: Effect): SerializedEffect => pick(effect, ['_tag', 'id'])
152
+
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()
158
+ effectsWrapper: (runEffects: () => void) => void
159
+
160
+ debugRefreshInfos: BoundArray<
161
+ RefreshDebugInfo<RefreshReasonWithGenericReasons<TDebugRefreshReason>, TDebugThunkInfo>
162
+ > = new BoundArray(5000)
163
+
164
+ constructor(options: ReactiveGraphOptions) {
165
+ this.effectsWrapper = options?.effectsWrapper ?? ((runEffects: () => void) => runEffects())
166
+ this.otelTracer = options.otelTracer
167
+ }
168
+
169
+ makeRef<T>(val: T, options?: { label?: string; meta?: unknown; equal?: (a: T, b: T) => boolean }): Ref<T> {
170
+ const ref: Ref<T> = {
171
+ _tag: 'ref',
172
+ id: uniqueNodeId(),
173
+ result: val,
174
+ height: 0,
175
+ getResult: () => ref.result,
176
+ sub: new Set(),
177
+ super: new Set(),
178
+ label: options?.label,
179
+ meta: options?.meta,
180
+ equal: options?.equal ?? isEqual,
181
+ }
182
+
183
+ this.atoms.add(ref)
184
+
185
+ return ref
186
+ }
187
+
188
+ makeThunk<T>(
189
+ getResult: (get: GetAtom, addDebugInfo: (debugInfo: TDebugThunkInfo) => void) => T,
190
+ options:
191
+ | {
192
+ label?: string
193
+ meta?: any
194
+ equal?: (a: T, b: T) => boolean
195
+ /** Debug info for initializing the thunk (i.e. running it the first time) */
196
+ debugRefreshReason?: RefreshReasonWithGenericReasons<TDebugRefreshReason>
197
+ }
198
+ | undefined,
199
+ parentSpanContext: otel.Context,
200
+ ): Thunk<T> {
201
+ const thunk: UnevaluatedThunk<T> = {
202
+ _tag: 'thunk',
203
+ id: uniqueNodeId(),
204
+ result: undefined,
205
+ height: 0,
206
+ getResult,
207
+ sub: new Set(),
208
+ super: new Set(),
209
+ label: options?.label,
210
+ meta: options?.meta,
211
+ equal: options?.equal ?? isEqual,
212
+ }
213
+
214
+ 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
+ parentSpanContext,
222
+ )
223
+
224
+ // Manually tell the typesystem this thunk is guaranteed to have a result at this point
225
+ return thunk as unknown as Thunk<T>
226
+ }
227
+
228
+ destroy(node: Atom<any> | Effect) {
229
+ // Recursively destroy any supercomputations
230
+ if (node._tag === 'ref' || node._tag === 'thunk') {
231
+ for (const superComp of node.super) {
232
+ this.destroy(superComp)
233
+ }
234
+ }
235
+
236
+ // Destroy this node
237
+ for (const subComp of node.sub) {
238
+ this.removeEdge(node, subComp)
239
+ }
240
+
241
+ if (node._tag === 'effect') {
242
+ this.effects.delete(node)
243
+ } else {
244
+ this.atoms.delete(node)
245
+ }
246
+ }
247
+
248
+ makeEffect(
249
+ doEffect: (get: GetAtom) => void,
250
+ options: { label?: string } | undefined,
251
+ parentSpanContext: otel.Context,
252
+ ): Effect {
253
+ const effect: Effect = {
254
+ _tag: 'effect',
255
+ id: uniqueNodeId(),
256
+ doEffect,
257
+ sub: new Set(),
258
+ }
259
+
260
+ this.effects.add(effect)
261
+ this.dirtyNodes.add(effect)
262
+ this.refresh(
263
+ { otelHint: 'makeEffect', debugRefreshReason: { _tag: 'makeEffect', label: options?.label } },
264
+ parentSpanContext,
265
+ )
266
+
267
+ return effect
268
+ }
269
+
270
+ setRef<T>(
271
+ ref: Ref<T>,
272
+ val: T,
273
+ options:
274
+ | {
275
+ otelHint?: string
276
+ skipRefresh?: boolean
277
+ debugRefreshReason?: TDebugRefreshReason
278
+ }
279
+ | undefined,
280
+ parentSpanContext: otel.Context,
281
+ ) {
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(),
295
+ }
296
+ this.debugRefreshInfos.push(refreshDebugInfo)
297
+ return
298
+ }
299
+
300
+ this.refresh({ otelHint, debugRefreshReason }, parentSpanContext)
301
+ }
302
+
303
+ setRefs<T>(
304
+ refs: [Ref<T>, T][],
305
+ options:
306
+ | {
307
+ otelHint?: string
308
+ skipRefresh?: boolean
309
+ debugRefreshReason?: TDebugRefreshReason
310
+ }
311
+ | undefined,
312
+ parentSpanContext: otel.Context,
313
+ ) {
314
+ const otelHint = options?.otelHint ?? ''
315
+ const skipRefresh = options?.skipRefresh ?? false
316
+ const debugRefreshReason = options?.debugRefreshReason
317
+ for (const [ref, val] of refs) {
318
+ ref.result = val
319
+ this.dirtyNodes.add(ref)
320
+ }
321
+
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(),
331
+ }
332
+ this.debugRefreshInfos.push(refreshDebugInfo)
333
+ return
334
+ }
335
+
336
+ this.refresh({ otelHint, debugRefreshReason }, parentSpanContext)
337
+ }
338
+
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)
343
+
344
+ const dependencyMightBeStale = context._tag !== 'effect' && context.height <= atom.height
345
+ const dependencyNotRefreshedYet = atom.result === undefined
346
+
347
+ if (dependencyMightBeStale || dependencyNotRefreshedYet) {
348
+ throw new DependencyNotReadyError(
349
+ `${this.label(context)} referenced dependency ${this.label(atom)} which isn't ready`,
350
+ )
351
+ }
352
+
353
+ return atom.result!
354
+ }
355
+
356
+ /**
357
+ * Update the graph to be consistent with the current values of the root atoms.
358
+ * Generally we run this after a ref is updated.
359
+ * At the end of the refresh, we run any effects that were scheduled.
360
+ *
361
+ * @param roots Root atoms to start the refresh from
362
+ */
363
+ refresh(
364
+ options:
365
+ | {
366
+ otelHint?: string
367
+ debugRefreshReason?: RefreshReasonWithGenericReasons<TDebugRefreshReason>
368
+ }
369
+ | undefined,
370
+ parentSpanContext: otel.Context,
371
+ ): void {
372
+ const otelHint = options?.otelHint ?? ''
373
+ const debugRefreshReason = options?.debugRefreshReason
374
+
375
+ const roots = [...this.dirtyNodes]
376
+
377
+ const debugInfoForRefreshedAtoms: AtomDebugInfo<TDebugThunkInfo>[] = []
378
+
379
+ // if (otelHint.includes('tableName')) {
380
+ // console.log('refresh', otelHint, { shouldTrace })
381
+ // }
382
+
383
+ this.otelTracer.startActiveSpan(`LiveStore.refresh:${otelHint}`, {}, parentSpanContext, (span) => {
384
+ const atomsToRefresh = roots.filter(isAtom)
385
+ const effectsToRun = new Set(roots.filter(isEffect))
386
+
387
+ span.setAttribute('livestore.hint', otelHint)
388
+ span.setAttribute('livestore.rootsCount', roots.length)
389
+ // span.setAttribute('sstack', new Error().stack!)
390
+
391
+ // Sort in topological order, starting with minimum height
392
+ while (atomsToRefresh.length > 0) {
393
+ atomsToRefresh.sort((a, b) => a.height - b.height)
394
+ const atomToRefresh = atomsToRefresh.shift()!
395
+
396
+ // Recompute the value
397
+ let resultChanged = false
398
+ const debugInfoForAtom = {
399
+ atom: serializeAtom(atomToRefresh),
400
+ resultChanged,
401
+ // debugInfo: unknownRefreshReason() as TDebugThunkInfo,
402
+ debugInfo: { _tag: 'unknown' } as TDebugThunkInfo,
403
+ durationMs: 0,
404
+ } satisfies AtomDebugInfo<TDebugThunkInfo>
405
+ try {
406
+ atomToRefresh.sub = new Set()
407
+ const beforeTimestamp = performance.now()
408
+ const newResult = atomToRefresh.getResult(
409
+ (atom) => this.get(atom, atomToRefresh),
410
+ (debugInfo) => {
411
+ debugInfoForAtom.debugInfo = debugInfo
412
+ },
413
+ )
414
+ const afterTimestamp = performance.now()
415
+ debugInfoForAtom.durationMs = afterTimestamp - beforeTimestamp
416
+
417
+ // Determine if the result changed to do early cutoff and avoid further unnecessary updates.
418
+ // Refs never depend on anything, so if a ref is being refreshed it definitely changed.
419
+ // For thunks, we use a deep equality check.
420
+ resultChanged =
421
+ atomToRefresh._tag === 'ref' ||
422
+ (atomToRefresh._tag === 'thunk' && !atomToRefresh.equal(atomToRefresh.result, newResult))
423
+
424
+ if (resultChanged) {
425
+ atomToRefresh.result = newResult
426
+ }
427
+
428
+ this.dirtyNodes.delete(atomToRefresh)
429
+ } catch (e) {
430
+ if (e instanceof DependencyNotReadyError) {
431
+ // If we hit a dependency that wasn't ready yet,
432
+ // abort this recomputation and try again later.
433
+ if (!atomsToRefresh.includes(atomToRefresh)) {
434
+ atomsToRefresh.push(atomToRefresh)
435
+ }
436
+ } else {
437
+ throw e
438
+ }
439
+ }
440
+
441
+ debugInfoForRefreshedAtoms.push(debugInfoForAtom)
442
+
443
+ if (!resultChanged) {
444
+ continue
445
+ }
446
+
447
+ // Schedule supercomputations
448
+ for (const superComp of atomToRefresh.super) {
449
+ switch (superComp._tag) {
450
+ case 'ref':
451
+ case 'thunk': {
452
+ if (!atomsToRefresh.includes(superComp)) {
453
+ atomsToRefresh.push(superComp)
454
+ }
455
+ break
456
+ }
457
+ case 'effect': {
458
+ effectsToRun.add(superComp)
459
+ break
460
+ }
461
+ }
462
+ }
463
+ }
464
+
465
+ this.effectsWrapper(() => {
466
+ for (const effect of effectsToRun) {
467
+ effect.doEffect((atom: Atom<any>) => this.get(atom, effect))
468
+ this.dirtyNodes.delete(effect)
469
+ }
470
+ })
471
+
472
+ span.end()
473
+
474
+ const spanDurationHr = (span as any)._duration
475
+ const spanDurationMs = spanDurationHr[0] * 1000 + spanDurationHr[1] / 1_000_000
476
+
477
+ const refreshDebugInfo: RefreshDebugInfo<
478
+ RefreshReasonWithGenericReasons<TDebugRefreshReason>,
479
+ TDebugThunkInfo
480
+ > = {
481
+ id: uniqueRefreshInfoId(),
482
+ reason: debugRefreshReason ?? unknownRefreshReason(),
483
+ refreshedAtoms: debugInfoForRefreshedAtoms,
484
+ skippedRefresh: false,
485
+ durationMs: spanDurationMs,
486
+ completedTimestamp: Date.now(),
487
+ graphSnapshot: this.getSnapshot(),
488
+ }
489
+
490
+ this.debugRefreshInfos.push(refreshDebugInfo)
491
+ })
492
+ }
493
+
494
+ label(atom: Atom<any> | Effect) {
495
+ if (atom._tag === 'effect') {
496
+ return `unknown effect`
497
+ } else {
498
+ return atom.label ?? `unknown ${atom._tag}`
499
+ }
500
+ }
501
+
502
+ addEdge(superComp: Atom<any> | Effect, subComp: Atom<any>) {
503
+ superComp.sub.add(subComp)
504
+ subComp.super.add(superComp)
505
+ this.updateAtomHeight(superComp)
506
+ }
507
+
508
+ removeEdge(superComp: Atom<any> | Effect, subComp: Atom<any>) {
509
+ superComp.sub.delete(subComp)
510
+ subComp.super.delete(superComp)
511
+ this.updateAtomHeight(superComp)
512
+ }
513
+
514
+ updateAtomHeight(atom: Atom<any> | Effect) {
515
+ switch (atom._tag) {
516
+ case 'ref': {
517
+ atom.height = 0
518
+ break
519
+ }
520
+ case 'thunk': {
521
+ atom.height = (max([...atom.sub].map((atom) => atom.height)) || 0) + 1
522
+ break
523
+ }
524
+ case 'effect': {
525
+ break
526
+ }
527
+ }
528
+ }
529
+
530
+ private getSnapshot = (): ReactiveGraphSnapshot => ({
531
+ atoms: Array.from(this.atoms).map(serializeAtom),
532
+ effects: Array.from(this.effects).map(serializeEffect),
533
+ dirtyNodes: Array.from(this.dirtyNodes).map((a) => a.id),
534
+ })
535
+ }
536
+
537
+ const isAtom = <T>(a: Atom<T> | Effect): a is Atom<T> => a._tag === 'ref' || a._tag === 'thunk'
538
+ const isEffect = <T>(a: Atom<T> | Effect): a is Effect => a._tag === 'effect'
@@ -0,0 +1,49 @@
1
+ import * as otel from '@opentelemetry/api'
2
+
3
+ import type { ComponentKey } from '../componentKey.js'
4
+ import type { Store } from '../store.js'
5
+
6
+ export type UnsubscribeQuery = () => void
7
+
8
+ export abstract class LiveStoreQueryBase {
9
+ /** The key for the associated component */
10
+ componentKey: ComponentKey
11
+ /** Human-readable label for the query for debugging */
12
+ label: string
13
+ /** A pointer back to the store containing this query */
14
+ store: Store<any>
15
+ /** Otel Span is started in LiveStore store but ended in this query */
16
+ otelContext: otel.Context
17
+
18
+ /** The string key is used to identify a subscription from "outside" */
19
+ activeSubscriptions: Map<string, UnsubscribeQuery> = new Map()
20
+
21
+ constructor({
22
+ componentKey,
23
+ label,
24
+ store,
25
+ otelContext,
26
+ }: {
27
+ componentKey: ComponentKey
28
+ label: string
29
+ store: Store<any>
30
+ otelContext: otel.Context
31
+ }) {
32
+ this.componentKey = componentKey
33
+ this.label = label
34
+ this.store = store
35
+ this.otelContext = otelContext
36
+ }
37
+
38
+ destroy = () => {
39
+ const span = otel.trace.getSpan(this.otelContext)!
40
+ span.end()
41
+
42
+ // NOTE usually the `unsubscribe` function is called by `useLiveStoreComponent` but this code path
43
+ // is used for manual store destruction, so we need to manually unsubscribe here
44
+ for (const [_key, unsubscribe] of this.activeSubscriptions) {
45
+ // unsubscribe from the query
46
+ unsubscribe()
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,52 @@
1
+ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'
2
+ import type * as otel from '@opentelemetry/api'
3
+
4
+ import type { ComponentKey } from '../componentKey.js'
5
+ import type { GetAtom, Thunk } from '../reactive.js'
6
+ import type { BaseGraphQLContext, Store } from '../store.js'
7
+ import { LiveStoreQueryBase } from './base-class.js'
8
+ import type { LiveStoreJSQuery } from './js.js'
9
+
10
+ export class LiveStoreGraphQLQuery<
11
+ TResult extends Record<string, any>,
12
+ VariableValues extends Record<string, any>,
13
+ TContext extends BaseGraphQLContext,
14
+ > extends LiveStoreQueryBase {
15
+ _tag: 'graphql' = 'graphql'
16
+
17
+ /** The abstract GraphQL query */
18
+ document: DocumentNode<TResult, VariableValues>
19
+
20
+ /** A reactive thunk representing the query results */
21
+ results$: Thunk<TResult>
22
+
23
+ constructor({
24
+ document,
25
+ results$,
26
+ ...baseProps
27
+ }: {
28
+ document: DocumentNode<TResult, VariableValues>
29
+ context: TContext
30
+ results$: Thunk<TResult>
31
+ componentKey: ComponentKey
32
+ label: string
33
+ store: Store<TContext>
34
+ otelContext: otel.Context
35
+ }) {
36
+ super(baseProps)
37
+
38
+ this.document = document
39
+ this.results$ = results$
40
+ }
41
+
42
+ pipe = <U>(f: (x: TResult, get: GetAtom) => U): LiveStoreJSQuery<U> =>
43
+ this.store.queryJS(
44
+ (get) => {
45
+ const results = get(this.results$)
46
+ return f(results, get)
47
+ },
48
+ this.componentKey,
49
+ `${this.label}:js`,
50
+ this.otelContext,
51
+ )
52
+ }
@@ -0,0 +1,38 @@
1
+ import type * as otel from '@opentelemetry/api'
2
+
3
+ import type { ComponentKey } from '../componentKey.js'
4
+ import type { GetAtom, Thunk } from '../reactive.js'
5
+ import type { Store } from '../store.js'
6
+ import { LiveStoreQueryBase } from './base-class.js'
7
+
8
+ export class LiveStoreJSQuery<TResult> extends LiveStoreQueryBase {
9
+ _tag: 'js' = 'js'
10
+ /** A reactive thunk representing the query results */
11
+ results$: Thunk<TResult>
12
+
13
+ constructor({
14
+ results$,
15
+ ...baseProps
16
+ }: {
17
+ results$: Thunk<TResult>
18
+ componentKey: ComponentKey
19
+ label: string
20
+ store: Store<any>
21
+ otelContext: otel.Context
22
+ }) {
23
+ super(baseProps)
24
+
25
+ this.results$ = results$
26
+ }
27
+
28
+ pipe = <U>(f: (x: TResult, get: GetAtom) => U): LiveStoreJSQuery<U> =>
29
+ this.store.queryJS(
30
+ (get) => {
31
+ const results = get(this.results$)
32
+ return f(results, get)
33
+ },
34
+ this.componentKey,
35
+ `${this.label}:js`,
36
+ this.otelContext,
37
+ )
38
+ }