@livestore/livestore 0.0.19 → 0.0.22

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 (136) hide show
  1. package/README.md +29 -22
  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 +3 -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__/react/utils/stack-info.test.d.ts +2 -0
  29. package/dist/__tests__/react/utils/stack-info.test.d.ts.map +1 -0
  30. package/dist/__tests__/react/utils/stack-info.test.js +43 -0
  31. package/dist/__tests__/react/utils/stack-info.test.js.map +1 -0
  32. package/dist/__tests__/reactive.test.js +179 -93
  33. package/dist/__tests__/reactive.test.js.map +1 -1
  34. package/dist/__tests__/reactiveQueries/sql.test.d.ts +2 -0
  35. package/dist/__tests__/reactiveQueries/sql.test.d.ts.map +1 -0
  36. package/dist/__tests__/reactiveQueries/sql.test.js +337 -0
  37. package/dist/__tests__/reactiveQueries/sql.test.js.map +1 -0
  38. package/dist/inMemoryDatabase.d.ts +4 -3
  39. package/dist/inMemoryDatabase.d.ts.map +1 -1
  40. package/dist/inMemoryDatabase.js +3 -2
  41. package/dist/inMemoryDatabase.js.map +1 -1
  42. package/dist/index.d.ts +7 -5
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +4 -0
  45. package/dist/index.js.map +1 -1
  46. package/dist/react/index.d.ts +4 -3
  47. package/dist/react/index.d.ts.map +1 -1
  48. package/dist/react/index.js +3 -2
  49. package/dist/react/index.js.map +1 -1
  50. package/dist/react/useComponentState.d.ts +50 -0
  51. package/dist/react/useComponentState.d.ts.map +1 -0
  52. package/dist/react/useComponentState.js +240 -0
  53. package/dist/react/useComponentState.js.map +1 -0
  54. package/dist/react/useGlobalQuery.d.ts +3 -0
  55. package/dist/react/useGlobalQuery.d.ts.map +1 -0
  56. package/dist/react/useGlobalQuery.js +26 -0
  57. package/dist/react/useGlobalQuery.js.map +1 -0
  58. package/dist/react/useGraphQL.d.ts +3 -3
  59. package/dist/react/useGraphQL.d.ts.map +1 -1
  60. package/dist/react/useGraphQL.js +10 -8
  61. package/dist/react/useGraphQL.js.map +1 -1
  62. package/dist/react/useLiveStoreComponent.d.ts +6 -6
  63. package/dist/react/useLiveStoreComponent.d.ts.map +1 -1
  64. package/dist/react/useLiveStoreComponent.js +143 -99
  65. package/dist/react/useLiveStoreComponent.js.map +1 -1
  66. package/dist/react/useQuery.d.ts +2 -2
  67. package/dist/react/useQuery.d.ts.map +1 -1
  68. package/dist/react/useQuery.js +54 -30
  69. package/dist/react/useQuery.js.map +1 -1
  70. package/dist/react/useTemporaryQuery.d.ts +8 -0
  71. package/dist/react/useTemporaryQuery.d.ts.map +1 -0
  72. package/dist/react/useTemporaryQuery.js +19 -0
  73. package/dist/react/useTemporaryQuery.js.map +1 -0
  74. package/dist/react/utils/extractNamesFromStackTrace.d.ts +3 -0
  75. package/dist/react/utils/extractNamesFromStackTrace.d.ts.map +1 -0
  76. package/dist/react/utils/extractNamesFromStackTrace.js +40 -0
  77. package/dist/react/utils/extractNamesFromStackTrace.js.map +1 -0
  78. package/dist/react/utils/extractStackInfoFromStackTrace.d.ts +7 -0
  79. package/dist/react/utils/extractStackInfoFromStackTrace.d.ts.map +1 -0
  80. package/dist/react/utils/extractStackInfoFromStackTrace.js +40 -0
  81. package/dist/react/utils/extractStackInfoFromStackTrace.js.map +1 -0
  82. package/dist/react/utils/stack-info.d.ts +11 -0
  83. package/dist/react/utils/stack-info.d.ts.map +1 -0
  84. package/dist/react/utils/stack-info.js +49 -0
  85. package/dist/react/utils/stack-info.js.map +1 -0
  86. package/dist/reactive.d.ts +51 -67
  87. package/dist/reactive.d.ts.map +1 -1
  88. package/dist/reactive.js +138 -220
  89. package/dist/reactive.js.map +1 -1
  90. package/dist/reactiveQueries/base-class.d.ts +28 -21
  91. package/dist/reactiveQueries/base-class.d.ts.map +1 -1
  92. package/dist/reactiveQueries/base-class.js +22 -18
  93. package/dist/reactiveQueries/base-class.js.map +1 -1
  94. package/dist/reactiveQueries/graph.d.ts +10 -0
  95. package/dist/reactiveQueries/graph.d.ts.map +1 -0
  96. package/dist/reactiveQueries/graph.js +6 -0
  97. package/dist/reactiveQueries/graph.js.map +1 -0
  98. package/dist/reactiveQueries/graphql.d.ts +35 -17
  99. package/dist/reactiveQueries/graphql.d.ts.map +1 -1
  100. package/dist/reactiveQueries/graphql.js +86 -10
  101. package/dist/reactiveQueries/graphql.js.map +1 -1
  102. package/dist/reactiveQueries/js.d.ts +17 -12
  103. package/dist/reactiveQueries/js.d.ts.map +1 -1
  104. package/dist/reactiveQueries/js.js +30 -8
  105. package/dist/reactiveQueries/js.js.map +1 -1
  106. package/dist/reactiveQueries/sql.d.ts +28 -18
  107. package/dist/reactiveQueries/sql.d.ts.map +1 -1
  108. package/dist/reactiveQueries/sql.js +79 -16
  109. package/dist/reactiveQueries/sql.js.map +1 -1
  110. package/dist/store.d.ts +35 -61
  111. package/dist/store.d.ts.map +1 -1
  112. package/dist/store.js +77 -272
  113. package/dist/store.js.map +1 -1
  114. package/package.json +4 -3
  115. package/src/QueryCache.ts +1 -1
  116. package/src/__tests__/react/fixture.tsx +10 -8
  117. package/src/__tests__/react/{useLiveStoreComponent.test.tsx → useComponentState.test.tsx} +9 -20
  118. package/src/__tests__/react/useQuery.test.tsx +48 -0
  119. package/src/__tests__/react/utils/stack-info.test.ts +45 -0
  120. package/src/__tests__/reactive.test.ts +212 -140
  121. package/src/__tests__/reactiveQueries/sql.test.ts +372 -0
  122. package/src/inMemoryDatabase.ts +11 -8
  123. package/src/index.ts +7 -11
  124. package/src/react/index.ts +4 -7
  125. package/src/react/{useLiveStoreComponent.ts → useComponentState.ts} +90 -253
  126. package/src/react/useQuery.ts +74 -40
  127. package/src/react/useTemporaryQuery.ts +23 -0
  128. package/src/react/utils/stack-info.ts +63 -0
  129. package/src/reactive.ts +234 -308
  130. package/src/reactiveQueries/base-class.ts +59 -42
  131. package/src/reactiveQueries/graph.ts +15 -0
  132. package/src/reactiveQueries/graphql.ts +143 -29
  133. package/src/reactiveQueries/js.ts +57 -20
  134. package/src/reactiveQueries/sql.ts +136 -36
  135. package/src/store.ts +121 -426
  136. package/src/react/useGraphQL.ts +0 -138
@@ -1,55 +1,72 @@
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/stack-info.js'
4
+ import type { Atom, GetAtom, RefreshReasonWithGenericReasons, Thunk } from '../reactive.js'
5
+ import type { RefreshReason } from '../store.js'
6
+ import { type DbContext, dbGraph } from './graph.js'
7
+ import type { LiveStoreJSQuery } from './js.js'
5
8
 
6
9
  export type UnsubscribeQuery = () => void
7
10
 
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 */
11
+ let queryIdCounter = 0
12
+
13
+ export interface ILiveStoreQuery<TResult> {
14
+ id: number
15
+
16
+ /** A reactive thunk representing the query results */
17
+ results$: Thunk<TResult, DbContext, RefreshReason>
18
+
12
19
  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
20
+
21
+ run: (otelContext?: otel.Context, debugRefreshReason?: RefreshReasonWithGenericReasons<RefreshReason>) => TResult
22
+
23
+ destroy(): void
24
+
25
+ activeSubscriptions: Set<StackInfo>
26
+ }
27
+
28
+ export abstract class LiveStoreQueryBase<TResult> implements ILiveStoreQuery<TResult> {
29
+ id = queryIdCounter++
30
+
31
+ /** Human-readable label for the query for debugging */
32
+ abstract label: string
33
+
34
+ abstract results$: Thunk<TResult, DbContext, RefreshReason>
35
+
36
+ activeSubscriptions: Set<StackInfo> = new Set()
37
+
38
+ get runs() {
39
+ return this.results$.recomputations
36
40
  }
37
41
 
38
- destroy = () => {
39
- const span = otel.trace.getSpan(this.otelContext)!
40
- span.end()
42
+ abstract destroy: () => void
43
+
44
+ run = (otelContext?: otel.Context, debugRefreshReason?: RefreshReasonWithGenericReasons<RefreshReason>): TResult =>
45
+ this.results$.computeResult(otelContext, debugRefreshReason)
41
46
 
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
- }
47
+ runAndDestroy = (
48
+ otelContext?: otel.Context,
49
+ debugRefreshReason?: RefreshReasonWithGenericReasons<RefreshReason>,
50
+ ): TResult => {
51
+ const result = this.run(otelContext, debugRefreshReason)
52
+ this.destroy()
53
+ return result
48
54
  }
49
55
 
50
56
  subscribe = (
51
57
  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)
58
+ onUnsubsubscribe?: () => void,
59
+ options?: { label?: string; otelContext?: otel.Context } | undefined,
60
+ ): (() => void) => dbGraph.context!.store.subscribe(this, onNewValue, onUnsubsubscribe, options)
61
+ }
62
+
63
+ export type GetAtomResult = <T>(atom: Atom<T, any, RefreshReason> | LiveStoreJSQuery<T>) => T
64
+
65
+ export const makeGetAtomResult = (get: GetAtom, otelContext: otel.Context) => {
66
+ const getAtom: GetAtomResult = (atom) => {
67
+ if (atom._tag === 'thunk' || atom._tag === 'ref') return get(atom, otelContext)
68
+ return get(atom.results$, otelContext)
69
+ }
70
+
71
+ return getAtom
55
72
  }
@@ -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,168 @@
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'
6
+ import { getDurationMsFromSpan } from '../otel.js'
5
7
  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'
8
+ import type { BaseGraphQLContext, RefreshReason, Store } from '../store.js'
9
+ import { type GetAtomResult, LiveStoreQueryBase, makeGetAtomResult } from './base-class.js'
10
+ import { type DbContext, dbGraph } from './graph.js'
11
+ import { LiveStoreJSQuery } from './js.js'
12
+
13
+ export const queryGraphQL = <TResult extends Record<string, any>, TVariableValues extends Record<string, any>>(
14
+ document: DocumentNode<TResult, TVariableValues>,
15
+ genVariableValues: TVariableValues | ((get: GetAtomResult) => TVariableValues),
16
+ { label }: { label?: string } = {},
17
+ ) => new LiveStoreGraphQLQuery({ document, genVariableValues, label })
9
18
 
10
19
  export class LiveStoreGraphQLQuery<
11
20
  TResult extends Record<string, any>,
12
- VariableValues extends Record<string, any>,
21
+ TVariableValues extends Record<string, any>,
13
22
  TContext extends BaseGraphQLContext,
14
23
  > extends LiveStoreQueryBase<TResult> {
15
24
  _tag: 'graphql' = 'graphql'
16
25
 
17
26
  /** The abstract GraphQL query */
18
- document: DocumentNode<TResult, VariableValues>
27
+ document: DocumentNode<TResult, TVariableValues>
19
28
 
20
29
  /** A reactive thunk representing the query results */
21
- results$: Thunk<TResult>
30
+ results$: Thunk<TResult, DbContext, RefreshReason>
31
+
32
+ variableValues$: Thunk<TVariableValues, DbContext, RefreshReason>
33
+
34
+ label: string
22
35
 
23
36
  constructor({
24
37
  document,
25
- results$,
26
- ...baseProps
38
+ label,
39
+ genVariableValues,
27
40
  }: {
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
41
+ document: DocumentNode<TResult, TVariableValues>
42
+ genVariableValues: TVariableValues | ((get: GetAtomResult) => TVariableValues)
43
+ label?: string
35
44
  }) {
36
- super(baseProps)
45
+ super()
37
46
 
47
+ const labelWithDefault = label ?? graphql.getOperationAST(document)?.name?.value ?? 'graphql'
48
+
49
+ this.label = labelWithDefault
38
50
  this.document = document
39
- this.results$ = results$
40
- }
41
51
 
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)
52
+ // TODO don't even create a thunk if variables are static
53
+ const variableValues$ = dbGraph.makeThunk(
54
+ (get, _setDebugInfo, { rootOtelContext }, otelContext) => {
55
+ if (typeof genVariableValues === 'function') {
56
+ return genVariableValues(makeGetAtomResult(get, otelContext ?? rootOtelContext))
57
+ } else {
58
+ return genVariableValues
59
+ }
47
60
  },
48
- {
49
- componentKey: this.componentKey,
50
- label: `${this.label}:js`,
51
- otelContext: this.otelContext,
61
+ { label: `${labelWithDefault}:variableValues`, meta: { liveStoreThunkType: 'graphqlVariableValues' } },
62
+ )
63
+
64
+ this.variableValues$ = variableValues$
65
+
66
+ const resultsLabel = `${labelWithDefault}:results`
67
+ this.results$ = dbGraph.makeThunk<TResult>(
68
+ (get, setDebugInfo, { store, otelTracer, rootOtelContext }, otelContext) => {
69
+ const variableValues = get(variableValues$)
70
+ const { result, queriedTables, durationMs } = this.queryOnce({
71
+ document,
72
+ variableValues,
73
+ otelContext: otelContext ?? rootOtelContext,
74
+ otelTracer,
75
+ store: store as Store<TContext>,
76
+ })
77
+
78
+ // Add dependencies on any tables that were used
79
+ for (const tableName of queriedTables) {
80
+ const tableRef = store.tableRefs[tableName]
81
+ assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
82
+ get(tableRef!)
83
+ }
84
+
85
+ setDebugInfo({ _tag: 'graphql', label: resultsLabel, query: graphql.print(document), durationMs })
86
+
87
+ return result
52
88
  },
89
+ { label: resultsLabel, meta: { liveStoreThunkType: 'graphqlResults' } },
90
+ // otelContext,
53
91
  )
92
+ }
93
+
94
+ /**
95
+ * Returns a new reactive query that contains the result of
96
+ * running an arbitrary JS computation on the results of this SQL query.
97
+ */
98
+ pipe = <U>(fn: (result: TResult, get: GetAtomResult) => U): LiveStoreJSQuery<U> =>
99
+ new LiveStoreJSQuery({
100
+ fn: (get) => {
101
+ const results = get(this.results$)
102
+ return fn(results, get)
103
+ },
104
+ label: `${this.label}:js`,
105
+ onDestroy: () => this.destroy(),
106
+ })
107
+
108
+ queryOnce = ({
109
+ document,
110
+ otelContext,
111
+ otelTracer,
112
+ variableValues,
113
+ store,
114
+ }: {
115
+ document: graphql.DocumentNode
116
+ otelContext: otel.Context
117
+ otelTracer: otel.Tracer
118
+ variableValues: TVariableValues
119
+ store: Store<TContext>
120
+ }) => {
121
+ const schema =
122
+ store.graphQLSchema ?? shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL schema")
123
+ const context =
124
+ store.graphQLContext ?? shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL context")
125
+
126
+ const operationName = graphql.getOperationAST(document)?.name?.value
127
+
128
+ return otelTracer.startActiveSpan(`executeGraphQLQuery: ${operationName}`, {}, otelContext, (span) => {
129
+ span.setAttribute('graphql.variables', JSON.stringify(variableValues))
130
+ span.setAttribute('graphql.query', graphql.print(document))
131
+
132
+ context.queriedTables.clear()
133
+
134
+ context.otelContext = otel.trace.setSpan(otel.context.active(), span)
135
+
136
+ const res = graphql.executeSync({
137
+ document,
138
+ contextValue: context,
139
+ schema: schema,
140
+ variableValues,
141
+ })
142
+
143
+ // TODO track number of nested SQL queries via Otel + debug info
144
+
145
+ if (res.errors) {
146
+ span.setStatus({ code: otel.SpanStatusCode.ERROR, message: 'GraphQL error' })
147
+ span.setAttribute('graphql.error', res.errors.join('\n'))
148
+ span.setAttribute('graphql.error-detail', JSON.stringify(res.errors))
149
+ console.error(`graphql error (${operationName})`, res.errors)
150
+ }
151
+
152
+ span.end()
153
+
154
+ const durationMs = getDurationMsFromSpan(span)
155
+
156
+ return {
157
+ result: res.data as unknown as TResult,
158
+ queriedTables: Array.from(context.queriedTables.values()),
159
+ durationMs,
160
+ }
161
+ })
162
+ }
163
+
164
+ destroy = () => {
165
+ dbGraph.destroy(this.variableValues$)
166
+ dbGraph.destroy(this.results$)
167
+ }
54
168
  }
@@ -1,36 +1,73 @@
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 { getDurationMsFromSpan } from '../otel.js'
4
+ import type { Thunk } from '../reactive.js'
5
+ import type { RefreshReason } from '../store.js'
6
+ import { type GetAtomResult, LiveStoreQueryBase, makeGetAtomResult } from './base-class.js'
7
+ import type { DbContext } from './graph.js'
8
+ import { dbGraph } from './graph.js'
9
+
10
+ export const queryJS = <TResult>(fn: (get: GetAtomResult) => TResult, options: { label: string }) =>
11
+ new LiveStoreJSQuery<TResult>({ fn, label: options.label })
7
12
 
8
13
  export class LiveStoreJSQuery<TResult> extends LiveStoreQueryBase<TResult> {
9
14
  _tag: 'js' = 'js'
15
+
10
16
  /** A reactive thunk representing the query results */
11
- results$: Thunk<TResult>
17
+ results$: Thunk<TResult, DbContext, RefreshReason>
18
+
19
+ label: string
20
+
21
+ /** Currently only used for "nested destruction" of piped queries */
22
+ private onDestroy: (() => void) | undefined
12
23
 
13
24
  constructor({
14
- results$,
15
- ...baseProps
25
+ fn,
26
+ label,
27
+ onDestroy,
16
28
  }: {
17
- results$: Thunk<TResult>
18
- componentKey: ComponentKey
19
29
  label: string
20
- store: Store
21
- otelContext: otel.Context
30
+ fn: (get: GetAtomResult) => TResult
31
+ /** Currently only used for "nested destruction" of piped queries */
32
+ onDestroy?: () => void
22
33
  }) {
23
- super(baseProps)
34
+ super()
35
+
36
+ this.onDestroy = onDestroy
37
+ this.label = label
38
+
39
+ const queryLabel = `${label}:results`
40
+
41
+ this.results$ = dbGraph.makeThunk(
42
+ (get, setDebugInfo, { otelTracer, rootOtelContext }, otelContext) =>
43
+ otelTracer.startActiveSpan(`js:${label}`, {}, otelContext ?? rootOtelContext, (span) => {
44
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
45
+ const res = fn(makeGetAtomResult(get, otelContext))
24
46
 
25
- this.results$ = results$
47
+ span.end()
48
+
49
+ const durationMs = getDurationMsFromSpan(span)
50
+
51
+ setDebugInfo({ _tag: 'js', label, query: fn.toString(), durationMs })
52
+
53
+ return res
54
+ }),
55
+ { label: queryLabel, meta: { liveStoreThunkType: 'jsResults' } },
56
+ )
26
57
  }
27
58
 
28
- pipe = <U>(f: (x: TResult, get: GetAtom) => U): LiveStoreJSQuery<U> =>
29
- this.store.queryJS(
30
- (get) => {
59
+ pipe = <U>(fn: (result: TResult, get: GetAtomResult) => U): LiveStoreJSQuery<U> =>
60
+ new LiveStoreJSQuery({
61
+ fn: (get) => {
31
62
  const results = get(this.results$)
32
- return f(results, get)
63
+ return fn(results, get)
33
64
  },
34
- { componentKey: this.componentKey, label: `${this.label}:js`, otelContext: this.otelContext },
35
- )
65
+ label: `${this.label}:js`,
66
+ onDestroy: () => this.destroy(),
67
+ })
68
+
69
+ destroy = () => {
70
+ dbGraph.destroy(this.results$)
71
+ this.onDestroy?.()
72
+ }
36
73
  }
@@ -1,34 +1,130 @@
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 { getDurationMsFromSpan } from '../otel.js'
5
+ import type { Thunk } from '../reactive.js'
6
+ import type { RefreshReason } from '../store.js'
7
+ import type { Bindable } from '../util.js'
8
+ import { prepareBindValues } from '../util.js'
9
+ import { type GetAtomResult, LiveStoreQueryBase, makeGetAtomResult } from './base-class.js'
10
+ import type { DbContext } from './graph.js'
11
+ import { dbGraph } from './graph.js'
12
+ import { LiveStoreJSQuery } from './js.js'
13
+
14
+ export const querySQL = <Row>(
15
+ query: string | ((get: GetAtomResult) => string),
16
+ options?: {
17
+ /**
18
+ * Can be provided explicitly to slightly speed up initial query performance
19
+ *
20
+ * NOTE In the future we want to do this automatically at build time
21
+ */
22
+ queriedTables?: ReadonlyArray<string>
23
+ bindValues?: Bindable
24
+ label?: string
25
+ },
26
+ ) =>
27
+ new LiveStoreSQLQuery<Row>({
28
+ label: options?.label,
29
+ genQueryString: query,
30
+ queriedTables: options?.queriedTables,
31
+ bindValues: options?.bindValues,
32
+ })
8
33
 
9
34
  /* An object encapsulating a reactive SQL query */
10
- export class LiveStoreSQLQuery<Row> extends LiveStoreQueryBase<Row> {
35
+ export class LiveStoreSQLQuery<Row> extends LiveStoreQueryBase<ReadonlyArray<Row>> {
11
36
  _tag: 'sql' = 'sql'
37
+
12
38
  /** A reactive thunk representing the query text */
13
- queryString$: Thunk<string>
39
+ queryString$: Thunk<string, DbContext, RefreshReason>
40
+
14
41
  /** A reactive thunk representing the query results */
15
- results$: Thunk<ReadonlyArray<Row>>
42
+ results$: Thunk<ReadonlyArray<Row>, DbContext, RefreshReason>
43
+
44
+ label: string
16
45
 
17
46
  constructor({
18
- queryString$,
19
- results$,
20
- ...baseProps
47
+ genQueryString,
48
+ queriedTables,
49
+ bindValues,
50
+ label: label_,
21
51
  }: {
22
- queryString$: Thunk<string>
23
- results$: Thunk<ReadonlyArray<Row>>
24
- componentKey: ComponentKey
25
- label: string
26
- store: Store
27
- otelContext: otel.Context
52
+ label?: string
53
+ genQueryString: string | ((get: GetAtomResult) => string)
54
+ queriedTables?: ReadonlyArray<string>
55
+ bindValues?: Bindable
28
56
  }) {
29
- super(baseProps)
57
+ super()
58
+
59
+ const label = label_ ?? genQueryString.toString()
60
+ this.label = `sql(${label})`
61
+
62
+ // TODO don't even create a thunk if query string is static
63
+ const queryString$ = dbGraph.makeThunk(
64
+ (get, setDebugInfo, { rootOtelContext }, otelContext) => {
65
+ if (typeof genQueryString === 'function') {
66
+ const startMs = performance.now()
67
+ const queryString = genQueryString(makeGetAtomResult(get, otelContext ?? rootOtelContext))
68
+ const durationMs = performance.now() - startMs
69
+ setDebugInfo({ _tag: 'js', label: `${label}:queryString`, query: queryString, durationMs })
70
+ return queryString
71
+ } else {
72
+ return genQueryString
73
+ }
74
+ },
75
+ { label: `${label}:queryString`, meta: { liveStoreThunkType: 'sqlQueryString' } },
76
+ )
30
77
 
31
78
  this.queryString$ = queryString$
79
+
80
+ const queryLabel = `${label}:results`
81
+
82
+ const queriedTablesRef = { current: queriedTables }
83
+
84
+ const results$ = dbGraph.makeThunk<ReadonlyArray<Row>>(
85
+ (get, setDebugInfo, { store, otelTracer, rootOtelContext }, otelContext) =>
86
+ otelTracer.startActiveSpan(
87
+ 'sql:...', // NOTE span name will be overridden further down
88
+ {},
89
+ otelContext ?? rootOtelContext,
90
+ (span) => {
91
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
92
+
93
+ const sqlString = get(queryString$, otelContext)
94
+
95
+ if (queriedTablesRef.current === undefined) {
96
+ queriedTablesRef.current = store.inMemoryDB.getTablesUsed(sqlString)
97
+ }
98
+
99
+ // Establish a reactive dependency on the tables used in the query
100
+ for (const tableName of queriedTablesRef.current) {
101
+ const tableRef = store.tableRefs[tableName] ?? shouldNeverHappen(`No table ref found for ${tableName}`)
102
+ get(tableRef, otelContext)
103
+ }
104
+
105
+ span.setAttribute('sql.query', sqlString)
106
+ span.updateName(`sql:${sqlString.slice(0, 50)}`)
107
+
108
+ const results = store.inMemoryDB.select<Row>(sqlString, {
109
+ queriedTables,
110
+ bindValues: bindValues ? prepareBindValues(bindValues, sqlString) : undefined,
111
+ otelContext,
112
+ })
113
+
114
+ span.setAttribute('sql.rowsCount', results.length)
115
+
116
+ span.end()
117
+
118
+ const durationMs = getDurationMsFromSpan(span)
119
+
120
+ setDebugInfo({ _tag: 'sql', label, query: sqlString, durationMs })
121
+
122
+ return results
123
+ },
124
+ ),
125
+ { label: queryLabel },
126
+ )
127
+
32
128
  this.results$ = results$
33
129
  }
34
130
 
@@ -36,30 +132,34 @@ export class LiveStoreSQLQuery<Row> extends LiveStoreQueryBase<Row> {
36
132
  * Returns a new reactive query that contains the result of
37
133
  * running an arbitrary JS computation on the results of this SQL query.
38
134
  */
39
- pipe = <U>(fn: (result: ReadonlyArray<Row>, get: GetAtom) => U): LiveStoreJSQuery<U> =>
40
- this.store.queryJS(
41
- (get) => {
42
- const results = get(this.results$)
135
+ pipe = <U>(fn: (result: ReadonlyArray<Row>, get: GetAtomResult) => U): LiveStoreJSQuery<U> =>
136
+ new LiveStoreJSQuery({
137
+ fn: (get) => {
138
+ const results = get(this.results$!)
43
139
  return fn(results, get)
44
140
  },
45
- {
46
- componentKey: this.componentKey,
47
- label: `${this.label}:js`,
48
- otelContext: this.otelContext,
49
- },
50
- )
141
+ label: `${this.label}:js`,
142
+ onDestroy: () => this.destroy(),
143
+ })
51
144
 
52
145
  /** Returns a reactive query */
53
146
  getFirstRow = (args?: { defaultValue?: Row }) =>
54
- this.store.queryJS(
55
- (get) => {
56
- const results = get(this.results$)
147
+ new LiveStoreJSQuery({
148
+ fn: (get) => {
149
+ const results = get(this.results$!)
57
150
  if (results.length === 0 && args?.defaultValue === undefined) {
58
- const queryLabel = this._tag === 'sql' ? this.queryString$.result : this.label
151
+ // const queryLabel = this._tag === 'sql' ? this.queryString$!.computeResult(otelContext) : this.label
152
+ const queryLabel = this.label
59
153
  throw new Error(`Expected query ${queryLabel} to return at least one result`)
60
154
  }
61
- return (results[0] ?? args?.defaultValue) as Row
155
+ return results[0] ?? args!.defaultValue!
62
156
  },
63
- { componentKey: this.componentKey, label: `${this.label}:first`, otelContext: this.otelContext },
64
- )
157
+ label: `${this.label}:first`,
158
+ onDestroy: () => this.destroy(),
159
+ })
160
+
161
+ destroy = () => {
162
+ dbGraph.destroy(this.queryString$)
163
+ dbGraph.destroy(this.results$)
164
+ }
65
165
  }