@livestore/livestore 0.0.16 → 0.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +18 -21
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/QueryCache.d.ts +1 -1
  4. package/dist/QueryCache.d.ts.map +1 -1
  5. package/dist/QueryCache.js.map +1 -1
  6. package/dist/__tests__/react/fixture.d.ts +5 -4
  7. package/dist/__tests__/react/fixture.d.ts.map +1 -1
  8. package/dist/__tests__/react/fixture.js +5 -5
  9. package/dist/__tests__/react/fixture.js.map +1 -1
  10. package/dist/__tests__/react/useComponentState.test.d.ts +2 -0
  11. package/dist/__tests__/react/useComponentState.test.d.ts.map +1 -0
  12. package/dist/__tests__/react/useComponentState.test.js +68 -0
  13. package/dist/__tests__/react/useComponentState.test.js.map +1 -0
  14. package/dist/__tests__/react/useLQuery.test.d.ts +2 -0
  15. package/dist/__tests__/react/useLQuery.test.d.ts.map +1 -0
  16. package/dist/__tests__/react/useLQuery.test.js +38 -0
  17. package/dist/__tests__/react/useLQuery.test.js.map +1 -0
  18. package/dist/__tests__/react/useLiveStoreComponent.test.js +4 -9
  19. package/dist/__tests__/react/useLiveStoreComponent.test.js.map +1 -1
  20. package/dist/__tests__/react/useQuery.test.d.ts +2 -0
  21. package/dist/__tests__/react/useQuery.test.d.ts.map +1 -0
  22. package/dist/__tests__/react/useQuery.test.js +33 -0
  23. package/dist/__tests__/react/useQuery.test.js.map +1 -0
  24. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.d.ts +2 -0
  25. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.d.ts.map +1 -0
  26. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.js +38 -0
  27. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.js.map +1 -0
  28. package/dist/__tests__/reactive.test.js +167 -93
  29. package/dist/__tests__/reactive.test.js.map +1 -1
  30. package/dist/__tests__/reactiveQueries/sql.test.d.ts +2 -0
  31. package/dist/__tests__/reactiveQueries/sql.test.d.ts.map +1 -0
  32. package/dist/__tests__/reactiveQueries/sql.test.js +337 -0
  33. package/dist/__tests__/reactiveQueries/sql.test.js.map +1 -0
  34. package/dist/inMemoryDatabase.d.ts +2 -2
  35. package/dist/inMemoryDatabase.d.ts.map +1 -1
  36. package/dist/index.d.ts +7 -5
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +4 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/react/index.d.ts +3 -3
  41. package/dist/react/index.d.ts.map +1 -1
  42. package/dist/react/index.js +2 -2
  43. package/dist/react/index.js.map +1 -1
  44. package/dist/react/useComponentState.d.ts +50 -0
  45. package/dist/react/useComponentState.d.ts.map +1 -0
  46. package/dist/react/useComponentState.js +248 -0
  47. package/dist/react/useComponentState.js.map +1 -0
  48. package/dist/react/useGlobalQuery.d.ts +3 -0
  49. package/dist/react/useGlobalQuery.d.ts.map +1 -0
  50. package/dist/react/useGlobalQuery.js +26 -0
  51. package/dist/react/useGlobalQuery.js.map +1 -0
  52. package/dist/react/useGraphQL.d.ts +3 -3
  53. package/dist/react/useGraphQL.d.ts.map +1 -1
  54. package/dist/react/useGraphQL.js +10 -8
  55. package/dist/react/useGraphQL.js.map +1 -1
  56. package/dist/react/useLiveStoreComponent.d.ts +6 -6
  57. package/dist/react/useLiveStoreComponent.d.ts.map +1 -1
  58. package/dist/react/useLiveStoreComponent.js +143 -99
  59. package/dist/react/useLiveStoreComponent.js.map +1 -1
  60. package/dist/react/useQuery.d.ts +2 -2
  61. package/dist/react/useQuery.d.ts.map +1 -1
  62. package/dist/react/useQuery.js +26 -22
  63. package/dist/react/useQuery.js.map +1 -1
  64. package/dist/react/useTemporaryQuery.d.ts +8 -0
  65. package/dist/react/useTemporaryQuery.d.ts.map +1 -0
  66. package/dist/react/useTemporaryQuery.js +17 -0
  67. package/dist/react/useTemporaryQuery.js.map +1 -0
  68. package/dist/react/utils/extractNamesFromStackTrace.d.ts +3 -0
  69. package/dist/react/utils/extractNamesFromStackTrace.d.ts.map +1 -0
  70. package/dist/react/utils/extractNamesFromStackTrace.js +40 -0
  71. package/dist/react/utils/extractNamesFromStackTrace.js.map +1 -0
  72. package/dist/react/utils/extractStackInfoFromStackTrace.d.ts +7 -0
  73. package/dist/react/utils/extractStackInfoFromStackTrace.d.ts.map +1 -0
  74. package/dist/react/utils/extractStackInfoFromStackTrace.js +40 -0
  75. package/dist/react/utils/extractStackInfoFromStackTrace.js.map +1 -0
  76. package/dist/reactive.d.ts +42 -48
  77. package/dist/reactive.d.ts.map +1 -1
  78. package/dist/reactive.js +293 -186
  79. package/dist/reactive.js.map +1 -1
  80. package/dist/reactiveQueries/base-class.d.ts +28 -23
  81. package/dist/reactiveQueries/base-class.d.ts.map +1 -1
  82. package/dist/reactiveQueries/base-class.js +25 -18
  83. package/dist/reactiveQueries/base-class.js.map +1 -1
  84. package/dist/reactiveQueries/graph.d.ts +10 -0
  85. package/dist/reactiveQueries/graph.d.ts.map +1 -0
  86. package/dist/reactiveQueries/graph.js +6 -0
  87. package/dist/reactiveQueries/graph.js.map +1 -0
  88. package/dist/reactiveQueries/graphql.d.ts +34 -17
  89. package/dist/reactiveQueries/graphql.d.ts.map +1 -1
  90. package/dist/reactiveQueries/graphql.js +91 -10
  91. package/dist/reactiveQueries/graphql.js.map +1 -1
  92. package/dist/reactiveQueries/js.d.ts +16 -12
  93. package/dist/reactiveQueries/js.d.ts.map +1 -1
  94. package/dist/reactiveQueries/js.js +31 -8
  95. package/dist/reactiveQueries/js.js.map +1 -1
  96. package/dist/reactiveQueries/sql.d.ts +22 -18
  97. package/dist/reactiveQueries/sql.d.ts.map +1 -1
  98. package/dist/reactiveQueries/sql.js +82 -16
  99. package/dist/reactiveQueries/sql.js.map +1 -1
  100. package/dist/store.d.ts +12 -52
  101. package/dist/store.d.ts.map +1 -1
  102. package/dist/store.js +283 -264
  103. package/dist/store.js.map +1 -1
  104. package/package.json +10 -9
  105. package/src/QueryCache.ts +1 -1
  106. package/src/__tests__/react/fixture.tsx +12 -7
  107. package/src/__tests__/react/{useLiveStoreComponent.test.tsx → useComponentState.test.tsx} +9 -20
  108. package/src/__tests__/react/useQuery.test.tsx +48 -0
  109. package/src/__tests__/react/utils/extractStackInfoFromStackTrace.test.ts +40 -0
  110. package/src/__tests__/reactive.test.ts +193 -140
  111. package/src/__tests__/reactiveQueries/sql.test.ts +372 -0
  112. package/src/inMemoryDatabase.ts +2 -2
  113. package/src/index.ts +7 -11
  114. package/src/react/index.ts +3 -7
  115. package/src/react/{useLiveStoreComponent.ts → useComponentState.ts} +89 -247
  116. package/src/react/useQuery.ts +29 -27
  117. package/src/react/useTemporaryQuery.ts +21 -0
  118. package/src/react/utils/extractStackInfoFromStackTrace.ts +47 -0
  119. package/src/reactive.ts +385 -268
  120. package/src/reactiveQueries/base-class.ts +60 -44
  121. package/src/reactiveQueries/graph.ts +15 -0
  122. package/src/reactiveQueries/graphql.ts +145 -29
  123. package/src/reactiveQueries/js.ts +53 -20
  124. package/src/reactiveQueries/sql.ts +129 -36
  125. package/src/store.ts +338 -408
  126. package/src/react/useGraphQL.ts +0 -138
@@ -1,55 +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<TResult> {
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
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
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)
54
+
55
+ runAndDestroy = (otelContext?: otel.Context): TResult => {
56
+ const result = this.run(otelContext)
57
+ this.destroy()
58
+ return result
59
+ }
60
+ }
61
+
62
+ export type GetAtomResult = <T>(atom: Atom<T, any> | LiveStoreJSQuery<T>) => T
41
63
 
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
- }
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)
48
68
  }
49
69
 
50
- subscribe = (
51
- onNewValue: (value: TResult) => void,
52
- onSubsubscribe?: () => void,
53
- options?: { label?: string } | undefined,
54
- ): (() => void) => this.store.subscribe(this as any, onNewValue as any, onSubsubscribe, options)
70
+ return getAtom
55
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
6
  import type { Thunk } from '../reactive.js'
6
- import type { BaseGraphQLContext, GetAtomResult, Store } from '../store.js'
7
- import { LiveStoreQueryBase } from './base-class.js'
8
- import type { LiveStoreJSQuery } from './js.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
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: GetAtomResult) => 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
+
8
+ export const queryJS = <TResult>(fn: (get: GetAtomResult) => TResult, options: { label: string }) =>
9
+ new LiveStoreJSQuery<TResult>({ fn, label: options.label })
7
10
 
8
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
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,123 @@
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<Row> {
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<ReadonlyArray<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: label_,
21
44
  }: {
22
- queryString$: Thunk<string>
23
- results$: Thunk<ReadonlyArray<Row>>
24
- componentKey: ComponentKey
25
- label: string
26
- store: Store
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
+ const label = label_ ?? genQueryString.toString()
53
+
54
+ // TODO don't even create a thunk if query string is static
55
+ const queryString$ = dbGraph.makeThunk(
56
+ (get, addDebugInfo, { rootOtelContext }, otelContext) => {
57
+ if (typeof genQueryString === 'function') {
58
+ const queryString = genQueryString(makeGetAtomResult(get, otelContext ?? rootOtelContext))
59
+ addDebugInfo({ _tag: 'js', label: `${label}:queryString`, query: queryString })
60
+ return queryString
61
+ } else {
62
+ return genQueryString
63
+ }
64
+ },
65
+ { label: `${label}:queryString`, meta: { liveStoreThunkType: 'sqlQueryString' } },
66
+ )
30
67
 
31
68
  this.queryString$ = queryString$
69
+
70
+ // TODO come up with different way to handle labels
71
+ // label = label ?? `sql(${queryString$.computeResult()})`
72
+
73
+ this.label = `sql(${label})`
74
+ // span.updateName(`querySQL:${label}`)
75
+
76
+ const queryLabel = `${label}:results`
77
+ // const queryLabel = `${label}:results` + (this.temporaryQueries ? ':temp' : '')
78
+
79
+ const results$ = dbGraph.makeThunk<ReadonlyArray<Row>>(
80
+ (get, addDebugInfo, { store, otelTracer, rootOtelContext }, otelContext) =>
81
+ otelTracer.startActiveSpan(
82
+ 'sql:', // NOTE span name will be overridden further down
83
+ {},
84
+ otelContext ?? rootOtelContext,
85
+ (span) => {
86
+ try {
87
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
88
+
89
+ // Establish a reactive dependency on the tables used in the query
90
+ for (const tableName of queriedTables) {
91
+ const tableRef = store.tableRefs[tableName] ?? shouldNeverHappen(`No table ref found for ${tableName}`)
92
+ get(tableRef, otelContext)
93
+ }
94
+ const sqlString = get(queryString$, otelContext)
95
+
96
+ span.setAttribute('sql.query', sqlString)
97
+ span.updateName(`sql:${sqlString.slice(0, 50)}`)
98
+
99
+ const results = store.inMemoryDB.select<Row>(sqlString, {
100
+ queriedTables,
101
+ bindValues: bindValues ? prepareBindValues(bindValues, sqlString) : undefined,
102
+ otelContext,
103
+ })
104
+
105
+ span.setAttribute('sql.rowsCount', results.length)
106
+ addDebugInfo({ _tag: 'sql', label: label ?? '', query: sqlString })
107
+
108
+ return results
109
+ } finally {
110
+ span.end()
111
+ }
112
+ },
113
+ ),
114
+ { label: queryLabel },
115
+ )
116
+
117
+ // this.queryString$ = queryString$
118
+ // this.results$ = results$
119
+ // this.payload = payload
120
+
32
121
  this.results$ = results$
33
122
  }
34
123
 
@@ -36,30 +125,34 @@ export class LiveStoreSQLQuery<Row> extends LiveStoreQueryBase<Row> {
36
125
  * Returns a new reactive query that contains the result of
37
126
  * running an arbitrary JS computation on the results of this SQL query.
38
127
  */
39
- pipe = <U>(fn: (result: ReadonlyArray<Row>, get: GetAtom) => U): LiveStoreJSQuery<U> =>
40
- this.store.queryJS(
41
- (get) => {
42
- const results = get(this.results$)
128
+ pipe = <U>(fn: (result: ReadonlyArray<Row>, get: GetAtomResult) => U): LiveStoreJSQuery<U> =>
129
+ new LiveStoreJSQuery({
130
+ fn: (get) => {
131
+ const results = get(this.results$!)
43
132
  return fn(results, get)
44
133
  },
45
- {
46
- componentKey: this.componentKey,
47
- label: `${this.label}:js`,
48
- otelContext: this.otelContext,
49
- },
50
- )
134
+ label: `${this.label}:js`,
135
+ onDestroy: () => this.destroy(),
136
+ })
51
137
 
52
138
  /** Returns a reactive query */
53
139
  getFirstRow = (args?: { defaultValue?: Row }) =>
54
- this.store.queryJS(
55
- (get) => {
56
- const results = get(this.results$)
140
+ new LiveStoreJSQuery({
141
+ fn: (get) => {
142
+ const results = get(this.results$!)
57
143
  if (results.length === 0 && args?.defaultValue === undefined) {
58
- const queryLabel = this._tag === 'sql' ? this.queryString$.result : this.label
144
+ // const queryLabel = this._tag === 'sql' ? this.queryString$!.computeResult(otelContext) : this.label
145
+ const queryLabel = this.label
59
146
  throw new Error(`Expected query ${queryLabel} to return at least one result`)
60
147
  }
61
- return (results[0] ?? args?.defaultValue) as Row
148
+ return results[0] ?? args!.defaultValue!
62
149
  },
63
- { componentKey: this.componentKey, label: `${this.label}:first`, otelContext: this.otelContext },
64
- )
150
+ label: `${this.label}:first`,
151
+ onDestroy: () => this.destroy(),
152
+ })
153
+
154
+ destroy = () => {
155
+ dbGraph.destroy(this.queryString$)
156
+ dbGraph.destroy(this.results$)
157
+ }
65
158
  }