@livestore/livestore 0.0.12 → 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 (226) hide show
  1. package/README.md +25 -28
  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 +71 -0
  6. package/dist/QueryCache.js.map +1 -0
  7. package/dist/__tests__/react/fixture.d.ts +26 -0
  8. package/dist/__tests__/react/fixture.d.ts.map +1 -0
  9. package/dist/__tests__/react/fixture.js +60 -0
  10. package/dist/__tests__/react/fixture.js.map +1 -0
  11. package/dist/__tests__/react/useComponentState.test.d.ts +2 -0
  12. package/dist/__tests__/react/useComponentState.test.d.ts.map +1 -0
  13. package/dist/__tests__/react/useComponentState.test.js +68 -0
  14. package/dist/__tests__/react/useComponentState.test.js.map +1 -0
  15. package/dist/__tests__/react/useLQuery.test.d.ts +2 -0
  16. package/dist/__tests__/react/useLQuery.test.d.ts.map +1 -0
  17. package/dist/__tests__/react/useLQuery.test.js +38 -0
  18. package/dist/__tests__/react/useLQuery.test.js.map +1 -0
  19. package/dist/__tests__/react/useLiveStoreComponent.test.d.ts +2 -0
  20. package/dist/__tests__/react/useLiveStoreComponent.test.d.ts.map +1 -0
  21. package/dist/__tests__/react/useLiveStoreComponent.test.js +73 -0
  22. package/dist/__tests__/react/useLiveStoreComponent.test.js.map +1 -0
  23. package/dist/__tests__/react/useQuery.test.d.ts +2 -0
  24. package/dist/__tests__/react/useQuery.test.d.ts.map +1 -0
  25. package/dist/__tests__/react/useQuery.test.js +33 -0
  26. package/dist/__tests__/react/useQuery.test.js.map +1 -0
  27. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.d.ts +2 -0
  28. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.d.ts.map +1 -0
  29. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.js +38 -0
  30. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.js.map +1 -0
  31. package/dist/__tests__/reactive.test.d.ts +2 -0
  32. package/dist/__tests__/reactive.test.d.ts.map +1 -0
  33. package/dist/__tests__/reactive.test.js +271 -0
  34. package/dist/__tests__/reactive.test.js.map +1 -0
  35. package/dist/__tests__/reactiveQueries/sql.test.d.ts +2 -0
  36. package/dist/__tests__/reactiveQueries/sql.test.d.ts.map +1 -0
  37. package/dist/__tests__/reactiveQueries/sql.test.js +337 -0
  38. package/dist/__tests__/reactiveQueries/sql.test.js.map +1 -0
  39. package/dist/bounded-collections.d.ts +34 -0
  40. package/dist/bounded-collections.d.ts.map +1 -0
  41. package/dist/bounded-collections.js +103 -0
  42. package/dist/bounded-collections.js.map +1 -0
  43. package/dist/componentKey.d.ts +20 -0
  44. package/dist/componentKey.d.ts.map +1 -0
  45. package/dist/componentKey.js +3 -0
  46. package/dist/componentKey.js.map +1 -0
  47. package/dist/effect/LiveStore.d.ts +36 -0
  48. package/dist/effect/LiveStore.d.ts.map +1 -0
  49. package/dist/effect/LiveStore.js +41 -0
  50. package/dist/effect/LiveStore.js.map +1 -0
  51. package/dist/effect/index.d.ts +2 -0
  52. package/dist/effect/index.d.ts.map +1 -0
  53. package/dist/effect/index.js +2 -0
  54. package/dist/effect/index.js.map +1 -0
  55. package/dist/events.d.ts +7 -0
  56. package/dist/events.d.ts.map +1 -0
  57. package/dist/events.js +2 -0
  58. package/dist/events.js.map +1 -0
  59. package/dist/inMemoryDatabase.d.ts +56 -0
  60. package/dist/inMemoryDatabase.d.ts.map +1 -0
  61. package/dist/inMemoryDatabase.js +223 -0
  62. package/dist/inMemoryDatabase.js.map +1 -0
  63. package/dist/index.d.ts +22 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +13 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/migrations.d.ts +16 -0
  68. package/dist/migrations.d.ts.map +1 -0
  69. package/dist/migrations.js +67 -0
  70. package/dist/migrations.js.map +1 -0
  71. package/dist/otel.d.ts +4 -0
  72. package/dist/otel.d.ts.map +1 -0
  73. package/dist/otel.js +6 -0
  74. package/dist/otel.js.map +1 -0
  75. package/dist/react/LiveStoreContext.d.ts +11 -0
  76. package/dist/react/LiveStoreContext.d.ts.map +1 -0
  77. package/dist/react/LiveStoreContext.js +10 -0
  78. package/dist/react/LiveStoreContext.js.map +1 -0
  79. package/dist/react/LiveStoreProvider.d.ts +20 -0
  80. package/dist/react/LiveStoreProvider.d.ts.map +1 -0
  81. package/dist/react/LiveStoreProvider.js +52 -0
  82. package/dist/react/LiveStoreProvider.js.map +1 -0
  83. package/dist/react/index.d.ts +8 -0
  84. package/dist/react/index.d.ts.map +1 -0
  85. package/dist/react/index.js +6 -0
  86. package/dist/react/index.js.map +1 -0
  87. package/dist/react/useComponentState.d.ts +50 -0
  88. package/dist/react/useComponentState.d.ts.map +1 -0
  89. package/dist/react/useComponentState.js +248 -0
  90. package/dist/react/useComponentState.js.map +1 -0
  91. package/dist/react/useGlobalQuery.d.ts +3 -0
  92. package/dist/react/useGlobalQuery.d.ts.map +1 -0
  93. package/dist/react/useGlobalQuery.js +26 -0
  94. package/dist/react/useGlobalQuery.js.map +1 -0
  95. package/dist/react/useGraphQL.d.ts +13 -0
  96. package/dist/react/useGraphQL.d.ts.map +1 -0
  97. package/dist/react/useGraphQL.js +87 -0
  98. package/dist/react/useGraphQL.js.map +1 -0
  99. package/dist/react/useLiveStoreComponent.d.ts +75 -0
  100. package/dist/react/useLiveStoreComponent.d.ts.map +1 -0
  101. package/dist/react/useLiveStoreComponent.js +361 -0
  102. package/dist/react/useLiveStoreComponent.js.map +1 -0
  103. package/dist/react/useQuery.d.ts +3 -0
  104. package/dist/react/useQuery.d.ts.map +1 -0
  105. package/dist/react/useQuery.js +42 -0
  106. package/dist/react/useQuery.js.map +1 -0
  107. package/dist/react/useTemporaryQuery.d.ts +8 -0
  108. package/dist/react/useTemporaryQuery.d.ts.map +1 -0
  109. package/dist/react/useTemporaryQuery.js +17 -0
  110. package/dist/react/useTemporaryQuery.js.map +1 -0
  111. package/dist/react/utils/extractNamesFromStackTrace.d.ts +3 -0
  112. package/dist/react/utils/extractNamesFromStackTrace.d.ts.map +1 -0
  113. package/dist/react/utils/extractNamesFromStackTrace.js +40 -0
  114. package/dist/react/utils/extractNamesFromStackTrace.js.map +1 -0
  115. package/dist/react/utils/extractStackInfoFromStackTrace.d.ts +7 -0
  116. package/dist/react/utils/extractStackInfoFromStackTrace.d.ts.map +1 -0
  117. package/dist/react/utils/extractStackInfoFromStackTrace.js +40 -0
  118. package/dist/react/utils/extractStackInfoFromStackTrace.js.map +1 -0
  119. package/dist/react/utils/useStateRefWithReactiveInput.d.ts +13 -0
  120. package/dist/react/utils/useStateRefWithReactiveInput.d.ts.map +1 -0
  121. package/dist/react/utils/useStateRefWithReactiveInput.js +38 -0
  122. package/dist/react/utils/useStateRefWithReactiveInput.js.map +1 -0
  123. package/dist/reactive.d.ts +134 -0
  124. package/dist/reactive.d.ts.map +1 -0
  125. package/dist/reactive.js +409 -0
  126. package/dist/reactive.js.map +1 -0
  127. package/dist/reactiveQueries/base-class.d.ts +32 -0
  128. package/dist/reactiveQueries/base-class.d.ts.map +1 -0
  129. package/dist/reactiveQueries/base-class.js +30 -0
  130. package/dist/reactiveQueries/base-class.js.map +1 -0
  131. package/dist/reactiveQueries/graph.d.ts +10 -0
  132. package/dist/reactiveQueries/graph.d.ts.map +1 -0
  133. package/dist/reactiveQueries/graph.js +6 -0
  134. package/dist/reactiveQueries/graph.js.map +1 -0
  135. package/dist/reactiveQueries/graphql.d.ts +42 -0
  136. package/dist/reactiveQueries/graphql.d.ts.map +1 -0
  137. package/dist/reactiveQueries/graphql.js +99 -0
  138. package/dist/reactiveQueries/graphql.js.map +1 -0
  139. package/dist/reactiveQueries/js.d.ts +23 -0
  140. package/dist/reactiveQueries/js.d.ts.map +1 -0
  141. package/dist/reactiveQueries/js.js +36 -0
  142. package/dist/reactiveQueries/js.js.map +1 -0
  143. package/dist/reactiveQueries/sql.d.ts +35 -0
  144. package/dist/reactiveQueries/sql.d.ts.map +1 -0
  145. package/dist/reactiveQueries/sql.js +97 -0
  146. package/dist/reactiveQueries/sql.js.map +1 -0
  147. package/dist/schema.d.ts +81 -0
  148. package/dist/schema.d.ts.map +1 -0
  149. package/dist/schema.js +46 -0
  150. package/dist/schema.js.map +1 -0
  151. package/dist/storage/in-memory/index.d.ts +15 -0
  152. package/dist/storage/in-memory/index.d.ts.map +1 -0
  153. package/dist/storage/in-memory/index.js +14 -0
  154. package/dist/storage/in-memory/index.js.map +1 -0
  155. package/dist/storage/index.d.ts +14 -0
  156. package/dist/storage/index.d.ts.map +1 -0
  157. package/dist/storage/index.js +9 -0
  158. package/dist/storage/index.js.map +1 -0
  159. package/dist/storage/tauri/index.d.ts +19 -0
  160. package/dist/storage/tauri/index.d.ts.map +1 -0
  161. package/dist/storage/tauri/index.js +38 -0
  162. package/dist/storage/tauri/index.js.map +1 -0
  163. package/dist/storage/utils/idb.d.ts +10 -0
  164. package/dist/storage/utils/idb.d.ts.map +1 -0
  165. package/dist/storage/utils/idb.js +58 -0
  166. package/dist/storage/utils/idb.js.map +1 -0
  167. package/dist/storage/web-worker/index.d.ts +27 -0
  168. package/dist/storage/web-worker/index.d.ts.map +1 -0
  169. package/dist/storage/web-worker/index.js +74 -0
  170. package/dist/storage/web-worker/index.js.map +1 -0
  171. package/dist/storage/web-worker/worker.d.ts +13 -0
  172. package/dist/storage/web-worker/worker.d.ts.map +1 -0
  173. package/dist/storage/web-worker/worker.js +110 -0
  174. package/dist/storage/web-worker/worker.js.map +1 -0
  175. package/dist/store.d.ts +159 -0
  176. package/dist/store.d.ts.map +1 -0
  177. package/dist/store.js +626 -0
  178. package/dist/store.js.map +1 -0
  179. package/dist/util.d.ts +28 -0
  180. package/dist/util.d.ts.map +1 -0
  181. package/dist/util.js +55 -0
  182. package/dist/util.js.map +1 -0
  183. package/package.json +47 -19
  184. package/src/QueryCache.ts +1 -1
  185. package/src/__tests__/react/fixture.tsx +35 -39
  186. package/src/__tests__/react/{useLiveStoreComponent.test.tsx → useComponentState.test.tsx} +9 -20
  187. package/src/__tests__/react/useQuery.test.tsx +48 -0
  188. package/src/__tests__/react/utils/extractStackInfoFromStackTrace.test.ts +40 -0
  189. package/src/__tests__/reactive.test.ts +194 -142
  190. package/src/__tests__/reactiveQueries/sql.test.ts +372 -0
  191. package/src/effect/LiveStore.ts +22 -31
  192. package/src/events.ts +1 -1
  193. package/src/inMemoryDatabase.ts +117 -142
  194. package/src/index.ts +18 -22
  195. package/src/migrations.ts +119 -0
  196. package/src/otel.ts +0 -11
  197. package/src/react/LiveStoreProvider.tsx +24 -23
  198. package/src/react/index.ts +12 -7
  199. package/src/react/useComponentState.ts +409 -0
  200. package/src/react/useQuery.ts +58 -0
  201. package/src/react/useTemporaryQuery.ts +21 -0
  202. package/src/react/utils/extractStackInfoFromStackTrace.ts +47 -0
  203. package/src/reactive.ts +386 -267
  204. package/src/reactiveQueries/base-class.ts +61 -39
  205. package/src/reactiveQueries/graph.ts +15 -0
  206. package/src/reactiveQueries/graphql.ts +147 -31
  207. package/src/reactiveQueries/js.ts +54 -21
  208. package/src/reactiveQueries/sql.ts +128 -37
  209. package/src/schema.ts +69 -145
  210. package/src/storage/in-memory/index.ts +21 -0
  211. package/src/storage/index.ts +27 -0
  212. package/src/{backends/tauri.ts → storage/tauri/index.ts} +14 -28
  213. package/src/storage/web-worker/index.ts +116 -0
  214. package/src/{backends/web-worker.ts → storage/web-worker/worker.ts} +17 -52
  215. package/src/store.ts +466 -457
  216. package/src/util.ts +13 -3
  217. package/tsconfig.json +1 -3
  218. package/src/backends/base.ts +0 -67
  219. package/src/backends/index.ts +0 -98
  220. package/src/backends/noop.ts +0 -32
  221. package/src/backends/web-in-memory.ts +0 -65
  222. package/src/backends/web.ts +0 -97
  223. package/src/react/useGlobalQuery.ts +0 -40
  224. package/src/react/useGraphQL.ts +0 -112
  225. package/src/react/useLiveStoreComponent.ts +0 -483
  226. /package/src/{backends → storage}/utils/idb.ts +0 -0
@@ -1,49 +1,71 @@
1
- import * as otel from '@opentelemetry/api'
1
+ import type * as otel from '@opentelemetry/api'
2
2
 
3
- import type { ComponentKey } from '../componentKey.js'
4
- import type { Store } from '../store.js'
3
+ import type { StackInfo } from '../react/utils/extractStackInfoFromStackTrace.js'
4
+ import type { Atom, GetAtom, Thunk } from '../reactive.js'
5
+ import { type DbContext } from './graph.js'
6
+ import type { LiveStoreJSQuery } from './js.js'
5
7
 
6
8
  export type UnsubscribeQuery = () => void
7
9
 
8
- export abstract class LiveStoreQueryBase {
9
- /** The key for the associated component */
10
- componentKey: ComponentKey
11
- /** Human-readable label for the query for debugging */
10
+ let queryIdCounter = 0
11
+
12
+ export interface ILiveStoreQuery<TResult> {
13
+ id: number
14
+
15
+ /** A reactive thunk representing the query results */
16
+ results$: Thunk<TResult, DbContext>
17
+
12
18
  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
19
+
20
+ run: (otelContext?: otel.Context) => TResult
21
+
22
+ destroy(): void
23
+
24
+ activeSubscriptions: Set<SubscriberInfo>
25
+ }
26
+
27
+ export type SubscriberInfo = {
28
+ stack: StackInfo[]
29
+ }
30
+
31
+ export abstract class LiveStoreQueryBase<TResult> implements ILiveStoreQuery<TResult> {
32
+ id = queryIdCounter++
33
+
34
+ /** Human-readable label for the query for debugging */
35
+ abstract label: string
36
+
37
+ abstract results$: Thunk<TResult, DbContext>
38
+
39
+ activeSubscriptions: Set<SubscriberInfo> = new Set()
40
+
41
+ get runs() {
42
+ return this.results$.recomputations
36
43
  }
37
44
 
38
- destroy = () => {
39
- const span = otel.trace.getSpan(this.otelContext)!
40
- span.end()
45
+ abstract destroy: () => void
46
+
47
+ // subscribe = (
48
+ // onNewValue: (value: TResult) => void,
49
+ // onSubsubscribe?: () => void,
50
+ // options?: { label?: string } | undefined,
51
+ // ): (() => void) => this.store.subscribe(this as any, onNewValue as any, onSubsubscribe, options)
52
+
53
+ run = (otelContext?: otel.Context): TResult => this.results$.computeResult(otelContext)
41
54
 
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
- }
55
+ runAndDestroy = (otelContext?: otel.Context): TResult => {
56
+ const result = this.run(otelContext)
57
+ this.destroy()
58
+ return result
48
59
  }
49
60
  }
61
+
62
+ export type GetAtomResult = <T>(atom: Atom<T, any> | LiveStoreJSQuery<T>) => T
63
+
64
+ export const makeGetAtomResult = (get: GetAtom, otelContext: otel.Context) => {
65
+ const getAtom: GetAtomResult = (atom) => {
66
+ if (atom._tag === 'thunk' || atom._tag === 'ref') return get(atom, otelContext)
67
+ return get(atom.results$, otelContext)
68
+ }
69
+
70
+ return getAtom
71
+ }
@@ -0,0 +1,15 @@
1
+ import type * as otel from '@opentelemetry/api'
2
+ import ReactDOM from 'react-dom'
3
+
4
+ import { ReactiveGraph } from '../reactive.js'
5
+ import type { QueryDebugInfo, RefreshReason, Store } from '../store.js'
6
+
7
+ export type DbContext = {
8
+ store: Store
9
+ otelTracer: otel.Tracer
10
+ rootOtelContext: otel.Context
11
+ }
12
+
13
+ export const dbGraph = new ReactiveGraph<RefreshReason, QueryDebugInfo, DbContext>({
14
+ effectsWrapper: (run) => ReactDOM.unstable_batchedUpdates(() => run()),
15
+ })
@@ -1,54 +1,170 @@
1
1
  import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'
2
- import type * as otel from '@opentelemetry/api'
2
+ import { assertNever, shouldNeverHappen } from '@livestore/utils'
3
+ import * as otel from '@opentelemetry/api'
4
+ import * as graphql from 'graphql'
3
5
 
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'
6
+ import type { Thunk } from '../reactive.js'
7
+ import { type BaseGraphQLContext, type Store } from '../store.js'
8
+ import { type GetAtomResult, LiveStoreQueryBase, makeGetAtomResult } from './base-class.js'
9
+ import { type DbContext, dbGraph } from './graph.js'
10
+ import { LiveStoreJSQuery } from './js.js'
11
+
12
+ export const queryGraphQL = <TResult extends Record<string, any>, TVariableValues extends Record<string, any>>(
13
+ document: DocumentNode<TResult, TVariableValues>,
14
+ genVariableValues: TVariableValues | ((get: GetAtomResult) => TVariableValues),
15
+ { label }: { label?: string } = {},
16
+ ) => new LiveStoreGraphQLQuery({ document, genVariableValues, label })
9
17
 
10
18
  export class LiveStoreGraphQLQuery<
11
19
  TResult extends Record<string, any>,
12
- VariableValues extends Record<string, any>,
20
+ TVariableValues extends Record<string, any>,
13
21
  TContext extends BaseGraphQLContext,
14
- > extends LiveStoreQueryBase {
22
+ > extends LiveStoreQueryBase<TResult> {
15
23
  _tag: 'graphql' = 'graphql'
16
24
 
17
25
  /** The abstract GraphQL query */
18
- document: DocumentNode<TResult, VariableValues>
26
+ document: DocumentNode<TResult, TVariableValues>
19
27
 
20
28
  /** A reactive thunk representing the query results */
21
- results$: Thunk<TResult>
29
+ results$: Thunk<TResult, DbContext>
30
+
31
+ variableValues$: Thunk<TVariableValues, DbContext>
32
+
33
+ label: string
22
34
 
23
35
  constructor({
24
36
  document,
25
- results$,
26
- ...baseProps
37
+ label,
38
+ genVariableValues, // context,
27
39
  }: {
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
40
+ document: DocumentNode<TResult, TVariableValues>
41
+ genVariableValues: TVariableValues | ((get: GetAtomResult) => TVariableValues)
42
+ label?: string
35
43
  }) {
36
- super(baseProps)
44
+ super()
37
45
 
46
+ const labelWithDefault = label ?? graphql.getOperationAST(document)?.name?.value ?? 'graphql'
47
+
48
+ this.label = labelWithDefault
38
49
  this.document = document
39
- this.results$ = results$
40
- }
41
50
 
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)
51
+ // if (context === undefined) {
52
+ // return shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL context")
53
+ // }
54
+
55
+ // TODO don't even create a thunk if variables are static
56
+ const variableValues$ = dbGraph.makeThunk(
57
+ (get, _addDebugInfo, { rootOtelContext }, otelContext) => {
58
+ if (typeof genVariableValues === 'function') {
59
+ return genVariableValues(makeGetAtomResult(get, otelContext ?? rootOtelContext))
60
+ } else {
61
+ return genVariableValues
62
+ }
47
63
  },
48
- {
49
- componentKey: this.componentKey,
50
- label: `${this.label}:js`,
51
- otelContext: this.otelContext,
64
+ { label: `${labelWithDefault}:variableValues`, meta: { liveStoreThunkType: 'graphqlVariableValues' } },
65
+ )
66
+
67
+ this.variableValues$ = variableValues$
68
+
69
+ // const resultsLabel = `${labelWithDefault}:results` + (this.temporaryQueries ? ':temp' : '')
70
+ const resultsLabel = `${labelWithDefault}:results`
71
+ this.results$ = dbGraph.makeThunk<TResult>(
72
+ (get, addDebugInfo, { store, otelTracer, rootOtelContext }, otelContext) => {
73
+ const variableValues = get(variableValues$)
74
+ const { result, queriedTables } = this.queryOnce({
75
+ document,
76
+ variableValues,
77
+ otelContext: otelContext ?? rootOtelContext,
78
+ otelTracer,
79
+ store: store as Store<TContext>,
80
+ })
81
+
82
+ // Add dependencies on any tables that were used
83
+ for (const tableName of queriedTables) {
84
+ const tableRef = store.tableRefs[tableName]
85
+ assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
86
+ get(tableRef!)
87
+ }
88
+
89
+ addDebugInfo({ _tag: 'graphql', label: resultsLabel, query: graphql.print(document) })
90
+
91
+ return result
52
92
  },
93
+ { label: resultsLabel, meta: { liveStoreThunkType: 'graphqlResults' } },
94
+ // otelContext,
53
95
  )
96
+ }
97
+
98
+ /**
99
+ * Returns a new reactive query that contains the result of
100
+ * running an arbitrary JS computation on the results of this SQL query.
101
+ */
102
+ pipe = <U>(fn: (result: TResult, get: GetAtomResult) => U): LiveStoreJSQuery<U> =>
103
+ new LiveStoreJSQuery({
104
+ fn: (get) => {
105
+ const results = get(this.results$)
106
+ return fn(results, get)
107
+ },
108
+ label: `${this.label}:js`,
109
+ onDestroy: () => this.destroy(),
110
+ })
111
+
112
+ queryOnce = ({
113
+ document,
114
+ otelContext,
115
+ otelTracer,
116
+ variableValues,
117
+ store,
118
+ }: {
119
+ document: graphql.DocumentNode
120
+ otelContext: otel.Context
121
+ otelTracer: otel.Tracer
122
+ variableValues: TVariableValues
123
+ store: Store<TContext>
124
+ }) => {
125
+ // const schema = this.schema
126
+ // const context = this.context
127
+ const schema =
128
+ store.graphQLSchema ?? shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL schema")
129
+ const context =
130
+ store.graphQLContext ?? shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL context")
131
+
132
+ const operationName = graphql.getOperationAST(document)?.name?.value
133
+
134
+ return otelTracer.startActiveSpan(`executeGraphQLQuery: ${operationName}`, {}, otelContext, (span) => {
135
+ try {
136
+ span.setAttribute('graphql.variables', JSON.stringify(variableValues))
137
+ span.setAttribute('graphql.query', graphql.print(document))
138
+
139
+ context.queriedTables.clear()
140
+
141
+ context.otelContext = otel.trace.setSpan(otel.context.active(), span)
142
+
143
+ const res = graphql.executeSync({
144
+ document,
145
+ contextValue: context,
146
+ schema: schema,
147
+ variableValues,
148
+ })
149
+
150
+ // TODO track number of nested SQL queries via Otel + debug info
151
+
152
+ if (res.errors) {
153
+ span.setStatus({ code: otel.SpanStatusCode.ERROR, message: 'GraphQL error' })
154
+ span.setAttribute('graphql.error', res.errors.join('\n'))
155
+ span.setAttribute('graphql.error-detail', JSON.stringify(res.errors))
156
+ console.error(`graphql error (${operationName})`, res.errors)
157
+ }
158
+
159
+ return { result: res.data as unknown as TResult, queriedTables: Array.from(context.queriedTables.values()) }
160
+ } finally {
161
+ span.end()
162
+ }
163
+ })
164
+ }
165
+
166
+ destroy = () => {
167
+ dbGraph.destroy(this.variableValues$)
168
+ dbGraph.destroy(this.results$)
169
+ }
54
170
  }
@@ -1,36 +1,69 @@
1
- import type * as otel from '@opentelemetry/api'
1
+ import * as otel from '@opentelemetry/api'
2
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'
3
+ import type { Thunk } from '../reactive.js'
4
+ import { type GetAtomResult, LiveStoreQueryBase, makeGetAtomResult } from './base-class.js'
5
+ import type { DbContext } from './graph.js'
6
+ import { dbGraph } from './graph.js'
7
7
 
8
- export class LiveStoreJSQuery<TResult> extends LiveStoreQueryBase {
8
+ export const queryJS = <TResult>(fn: (get: GetAtomResult) => TResult, options: { label: string }) =>
9
+ new LiveStoreJSQuery<TResult>({ fn, label: options.label })
10
+
11
+ export class LiveStoreJSQuery<TResult> extends LiveStoreQueryBase<TResult> {
9
12
  _tag: 'js' = 'js'
13
+
10
14
  /** A reactive thunk representing the query results */
11
- results$: Thunk<TResult>
15
+ results$: Thunk<TResult, DbContext>
16
+
17
+ label: string
18
+
19
+ /** Currently only used for "nested destruction" of piped queries */
20
+ private onDestroy: (() => void) | undefined
12
21
 
13
22
  constructor({
14
- results$,
15
- ...baseProps
23
+ fn,
24
+ label,
25
+ onDestroy,
16
26
  }: {
17
- results$: Thunk<TResult>
18
- componentKey: ComponentKey
19
27
  label: string
20
- store: Store<any>
21
- otelContext: otel.Context
28
+ fn: (get: GetAtomResult) => TResult
29
+ /** Currently only used for "nested destruction" of piped queries */
30
+ onDestroy?: () => void
22
31
  }) {
23
- super(baseProps)
32
+ super()
33
+
34
+ this.onDestroy = onDestroy
35
+ this.label = label
36
+
37
+ const queryLabel = `${label}:results`
24
38
 
25
- this.results$ = results$
39
+ this.results$ = dbGraph.makeThunk(
40
+ (get, addDebugInfo, { otelTracer, rootOtelContext }, otelContext) =>
41
+ otelTracer.startActiveSpan(`js:${label}`, {}, otelContext ?? rootOtelContext, (span) => {
42
+ try {
43
+ addDebugInfo({ _tag: 'js', label, query: fn.toString() })
44
+
45
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
46
+ return fn(makeGetAtomResult(get, otelContext))
47
+ } finally {
48
+ span.end()
49
+ }
50
+ }),
51
+ { label: queryLabel, meta: { liveStoreThunkType: 'jsResults' } },
52
+ )
26
53
  }
27
54
 
28
- pipe = <U>(f: (x: TResult, get: GetAtom) => U): LiveStoreJSQuery<U> =>
29
- this.store.queryJS(
30
- (get) => {
55
+ pipe = <U>(fn: (result: TResult, get: GetAtomResult) => U): LiveStoreJSQuery<U> =>
56
+ new LiveStoreJSQuery({
57
+ fn: (get) => {
31
58
  const results = get(this.results$)
32
- return f(results, get)
59
+ return fn(results, get)
33
60
  },
34
- { componentKey: this.componentKey, label: `${this.label}:js`, otelContext: this.otelContext },
35
- )
61
+ label: `${this.label}:js`,
62
+ onDestroy: () => this.destroy(),
63
+ })
64
+
65
+ destroy = () => {
66
+ dbGraph.destroy(this.results$)
67
+ this.onDestroy?.()
68
+ }
36
69
  }
@@ -1,34 +1,121 @@
1
- import type * as otel from '@opentelemetry/api'
1
+ import { shouldNeverHappen } from '@livestore/utils'
2
+ import * as otel from '@opentelemetry/api'
2
3
 
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
- import type { LiveStoreJSQuery } from './js.js'
4
+ import type { Thunk } from '../reactive.js'
5
+ import type { Bindable } from '../util.js'
6
+ import { prepareBindValues } from '../util.js'
7
+ import { type GetAtomResult, LiveStoreQueryBase, makeGetAtomResult } from './base-class.js'
8
+ import type { DbContext } from './graph.js'
9
+ import { dbGraph } from './graph.js'
10
+ import { LiveStoreJSQuery } from './js.js'
11
+
12
+ export const querySQL = <Row>(
13
+ query: string | ((get: GetAtomResult) => string),
14
+ options: {
15
+ queriedTables: ReadonlyArray<string>
16
+ bindValues?: Bindable
17
+ label?: string
18
+ },
19
+ ) =>
20
+ new LiveStoreSQLQuery<Row>({
21
+ label: options.label,
22
+ genQueryString: query,
23
+ queriedTables: options.queriedTables,
24
+ bindValues: options.bindValues,
25
+ })
8
26
 
9
27
  /* An object encapsulating a reactive SQL query */
10
- export class LiveStoreSQLQuery<Row> extends LiveStoreQueryBase {
28
+ export class LiveStoreSQLQuery<Row> extends LiveStoreQueryBase<ReadonlyArray<Row>> {
11
29
  _tag: 'sql' = 'sql'
30
+
12
31
  /** A reactive thunk representing the query text */
13
- queryString$: Thunk<string>
32
+ queryString$: Thunk<string, DbContext>
33
+
14
34
  /** A reactive thunk representing the query results */
15
- results$: Thunk<Row[]>
35
+ results$: Thunk<ReadonlyArray<Row>, DbContext>
36
+
37
+ label: string
16
38
 
17
39
  constructor({
18
- queryString$,
19
- results$,
20
- ...baseProps
40
+ genQueryString,
41
+ queriedTables,
42
+ bindValues,
43
+ label,
21
44
  }: {
22
- queryString$: Thunk<string>
23
- results$: Thunk<Row[]>
24
- componentKey: ComponentKey
25
- label: string
26
- store: Store<any>
27
- otelContext: otel.Context
45
+ label?: string
46
+ genQueryString: string | ((get: GetAtomResult) => string)
47
+ queriedTables: ReadonlyArray<string>
48
+ bindValues?: Bindable
28
49
  }) {
29
- super(baseProps)
50
+ super()
51
+
52
+ // TODO don't even create a thunk if query string is static
53
+ const queryString$ = dbGraph.makeThunk(
54
+ (get, addDebugInfo, { rootOtelContext }, otelContext) => {
55
+ if (typeof genQueryString === 'function') {
56
+ const queryString = genQueryString(makeGetAtomResult(get, otelContext ?? rootOtelContext))
57
+ addDebugInfo({ _tag: 'js', label: `${label}:queryString`, query: queryString })
58
+ return queryString
59
+ } else {
60
+ return genQueryString
61
+ }
62
+ },
63
+ { label: `${label}:queryString`, meta: { liveStoreThunkType: 'sqlQueryString' } },
64
+ )
30
65
 
31
66
  this.queryString$ = queryString$
67
+
68
+ // TODO come up with different way to handle labels
69
+ // label = label ?? `sql(${queryString$.computeResult()})`
70
+
71
+ this.label = label ?? `sql(${genQueryString.toString()})`
72
+ // span.updateName(`querySQL:${label}`)
73
+
74
+ const queryLabel = `${label}:results`
75
+ // const queryLabel = `${label}:results` + (this.temporaryQueries ? ':temp' : '')
76
+
77
+ const results$ = dbGraph.makeThunk<ReadonlyArray<Row>>(
78
+ (get, addDebugInfo, { store, otelTracer, rootOtelContext }, otelContext) =>
79
+ otelTracer.startActiveSpan(
80
+ 'sql:', // NOTE span name will be overridden further down
81
+ {},
82
+ otelContext ?? rootOtelContext,
83
+ (span) => {
84
+ try {
85
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
86
+
87
+ // Establish a reactive dependency on the tables used in the query
88
+ for (const tableName of queriedTables) {
89
+ const tableRef = store.tableRefs[tableName] ?? shouldNeverHappen(`No table ref found for ${tableName}`)
90
+ get(tableRef, otelContext)
91
+ }
92
+ const sqlString = get(queryString$, otelContext)
93
+
94
+ span.setAttribute('sql.query', sqlString)
95
+ span.updateName(`sql:${sqlString.slice(0, 50)}`)
96
+
97
+ const results = store.inMemoryDB.select<Row>(sqlString, {
98
+ queriedTables,
99
+ bindValues: bindValues ? prepareBindValues(bindValues, sqlString) : undefined,
100
+ otelContext,
101
+ })
102
+
103
+ span.setAttribute('sql.rowsCount', results.length)
104
+ addDebugInfo({ _tag: 'sql', label: label ?? '', query: sqlString })
105
+
106
+ return results
107
+ } finally {
108
+ span.end()
109
+ }
110
+ },
111
+ ),
112
+ { label: queryLabel },
113
+ )
114
+
115
+ // this.queryString$ = queryString$
116
+ // this.results$ = results$
117
+ // this.payload = payload
118
+
32
119
  this.results$ = results$
33
120
  }
34
121
 
@@ -36,30 +123,34 @@ export class LiveStoreSQLQuery<Row> extends LiveStoreQueryBase {
36
123
  * Returns a new reactive query that contains the result of
37
124
  * running an arbitrary JS computation on the results of this SQL query.
38
125
  */
39
- pipe = <U>(f: (result: Row[], get: GetAtom) => U): LiveStoreJSQuery<U> =>
40
- this.store.queryJS(
41
- (get) => {
42
- const results = get(this.results$)
43
- return f(results, get)
44
- },
45
- {
46
- componentKey: this.componentKey,
47
- label: `${this.label}:js`,
48
- otelContext: this.otelContext,
126
+ pipe = <U>(fn: (result: ReadonlyArray<Row>, get: GetAtomResult) => U): LiveStoreJSQuery<U> =>
127
+ new LiveStoreJSQuery({
128
+ fn: (get) => {
129
+ const results = get(this.results$!)
130
+ return fn(results, get)
49
131
  },
50
- )
132
+ label: `${this.label}:js`,
133
+ onDestroy: () => this.destroy(),
134
+ })
51
135
 
52
136
  /** Returns a reactive query */
53
137
  getFirstRow = (args?: { defaultValue?: Row }) =>
54
- this.store.queryJS(
55
- (get) => {
56
- const results = get(this.results$)
138
+ new LiveStoreJSQuery({
139
+ fn: (get) => {
140
+ const results = get(this.results$!)
57
141
  if (results.length === 0 && args?.defaultValue === undefined) {
58
- const queryLabel = this._tag === 'sql' ? this.queryString$.result : this.label
142
+ // const queryLabel = this._tag === 'sql' ? this.queryString$!.computeResult(otelContext) : this.label
143
+ const queryLabel = this.label
59
144
  throw new Error(`Expected query ${queryLabel} to return at least one result`)
60
145
  }
61
- return (results[0] ?? args?.defaultValue) as Row
146
+ return results[0] ?? args!.defaultValue!
62
147
  },
63
- { componentKey: this.componentKey, label: `${this.label}:first`, otelContext: this.otelContext },
64
- )
148
+ label: `${this.label}:first`,
149
+ onDestroy: () => this.destroy(),
150
+ })
151
+
152
+ destroy = () => {
153
+ dbGraph.destroy(this.queryString$)
154
+ dbGraph.destroy(this.results$)
155
+ }
65
156
  }