@livestore/livestore 0.1.0-dev.9 → 0.2.0-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +3 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/global-state.d.ts +1 -1
  4. package/dist/global-state.d.ts.map +1 -1
  5. package/dist/global-state.js +1 -1
  6. package/dist/global-state.js.map +1 -1
  7. package/dist/index.d.ts +6 -6
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +5 -5
  10. package/dist/index.js.map +1 -1
  11. package/dist/live-queries/base-class.d.ts +64 -0
  12. package/dist/live-queries/base-class.d.ts.map +1 -0
  13. package/dist/live-queries/base-class.js +31 -0
  14. package/dist/live-queries/base-class.js.map +1 -0
  15. package/dist/live-queries/computed.d.ts +26 -0
  16. package/dist/live-queries/computed.d.ts.map +1 -0
  17. package/dist/{reactiveQueries/js.js → live-queries/computed.js} +7 -26
  18. package/dist/live-queries/computed.js.map +1 -0
  19. package/dist/live-queries/db.d.ts +66 -0
  20. package/dist/live-queries/db.d.ts.map +1 -0
  21. package/dist/live-queries/db.js +199 -0
  22. package/dist/live-queries/db.js.map +1 -0
  23. package/dist/live-queries/db.test.d.ts +2 -0
  24. package/dist/live-queries/db.test.d.ts.map +1 -0
  25. package/dist/live-queries/db.test.js +117 -0
  26. package/dist/live-queries/db.test.js.map +1 -0
  27. package/dist/live-queries/graphql.d.ts +49 -0
  28. package/dist/live-queries/graphql.d.ts.map +1 -0
  29. package/dist/live-queries/graphql.js +122 -0
  30. package/dist/live-queries/graphql.js.map +1 -0
  31. package/dist/live-queries/sql.d.ts +62 -0
  32. package/dist/live-queries/sql.d.ts.map +1 -0
  33. package/dist/live-queries/sql.js +175 -0
  34. package/dist/live-queries/sql.js.map +1 -0
  35. package/dist/live-queries/sql.test.d.ts +2 -0
  36. package/dist/live-queries/sql.test.d.ts.map +1 -0
  37. package/{src/reactiveQueries/sql.test.ts → dist/live-queries/sql.test.js} +68 -91
  38. package/dist/live-queries/sql.test.js.map +1 -0
  39. package/dist/reactiveQueries/base-class.d.ts +6 -2
  40. package/dist/reactiveQueries/base-class.d.ts.map +1 -1
  41. package/dist/reactiveQueries/base-class.js +2 -0
  42. package/dist/reactiveQueries/base-class.js.map +1 -1
  43. package/dist/reactiveQueries/computed.d.ts +4 -13
  44. package/dist/reactiveQueries/computed.d.ts.map +1 -1
  45. package/dist/reactiveQueries/computed.js +2 -21
  46. package/dist/reactiveQueries/computed.js.map +1 -1
  47. package/dist/reactiveQueries/graphql.d.ts +4 -8
  48. package/dist/reactiveQueries/graphql.d.ts.map +1 -1
  49. package/dist/reactiveQueries/graphql.js +1 -15
  50. package/dist/reactiveQueries/graphql.js.map +1 -1
  51. package/dist/reactiveQueries/sql.d.ts +36 -23
  52. package/dist/reactiveQueries/sql.d.ts.map +1 -1
  53. package/dist/reactiveQueries/sql.js +100 -55
  54. package/dist/reactiveQueries/sql.js.map +1 -1
  55. package/dist/reactiveQueries/sql.test.js +12 -11
  56. package/dist/reactiveQueries/sql.test.js.map +1 -1
  57. package/dist/row-query-utils.d.ts +17 -0
  58. package/dist/row-query-utils.d.ts.map +1 -0
  59. package/dist/row-query-utils.js +30 -0
  60. package/dist/row-query-utils.js.map +1 -0
  61. package/dist/row-query.d.ts +10 -27
  62. package/dist/row-query.d.ts.map +1 -1
  63. package/dist/row-query.js +16 -66
  64. package/dist/row-query.js.map +1 -1
  65. package/dist/store/create-store.d.ts +1 -1
  66. package/dist/store/create-store.d.ts.map +1 -1
  67. package/dist/store/devtools.d.ts +1 -1
  68. package/dist/store/devtools.d.ts.map +1 -1
  69. package/dist/store/devtools.js.map +1 -1
  70. package/dist/store/store-types.d.ts +2 -2
  71. package/dist/store/store-types.d.ts.map +1 -1
  72. package/dist/store/store.d.ts +8 -3
  73. package/dist/store/store.d.ts.map +1 -1
  74. package/dist/store/store.js +32 -4
  75. package/dist/store/store.js.map +1 -1
  76. package/dist/utils/tests/fixture.d.ts +168 -132
  77. package/dist/utils/tests/fixture.d.ts.map +1 -1
  78. package/package.json +5 -5
  79. package/src/global-state.ts +1 -1
  80. package/src/index.ts +8 -5
  81. package/src/live-queries/__snapshots__/db.test.ts.snap +301 -0
  82. package/src/{reactiveQueries → live-queries}/base-class.ts +10 -5
  83. package/src/{reactiveQueries → live-queries}/computed.ts +5 -29
  84. package/src/live-queries/db.test.ts +153 -0
  85. package/src/live-queries/db.ts +350 -0
  86. package/src/{reactiveQueries → live-queries}/graphql.ts +6 -21
  87. package/src/row-query-utils.ts +65 -0
  88. package/src/store/create-store.ts +1 -1
  89. package/src/store/devtools.ts +1 -1
  90. package/src/store/store-types.ts +2 -2
  91. package/src/store/store.ts +44 -7
  92. package/dist/reactiveQueries/js.d.ts +0 -35
  93. package/dist/reactiveQueries/js.d.ts.map +0 -1
  94. package/dist/reactiveQueries/js.js.map +0 -1
  95. package/dist/store/store-context.d.ts +0 -26
  96. package/dist/store/store-context.d.ts.map +0 -1
  97. package/dist/store/store-context.js +0 -6
  98. package/dist/store/store-context.js.map +0 -1
  99. package/dist/store-context.d.ts +0 -26
  100. package/dist/store-context.d.ts.map +0 -1
  101. package/dist/store-context.js +0 -6
  102. package/dist/store-context.js.map +0 -1
  103. package/dist/store-devtools.d.ts +0 -19
  104. package/dist/store-devtools.d.ts.map +0 -1
  105. package/dist/store-devtools.js +0 -141
  106. package/dist/store-devtools.js.map +0 -1
  107. package/dist/store.d.ts +0 -175
  108. package/dist/store.d.ts.map +0 -1
  109. package/dist/store.js +0 -509
  110. package/dist/store.js.map +0 -1
  111. package/src/reactiveQueries/sql.ts +0 -226
  112. package/src/row-query.ts +0 -196
@@ -0,0 +1,350 @@
1
+ import type { Bindable, QueryBuilder, QueryInfo } from '@livestore/common'
2
+ import {
3
+ getResultSchema,
4
+ isQueryBuilder,
5
+ prepareBindValues,
6
+ QueryBuilderAstSymbol,
7
+ replaceSessionIdSymbol,
8
+ } from '@livestore/common'
9
+ import { deepEqual, shouldNeverHappen } from '@livestore/utils'
10
+ import { Predicate, Schema, TreeFormatter } from '@livestore/utils/effect'
11
+ import * as otel from '@opentelemetry/api'
12
+
13
+ import { globalReactivityGraph } from '../global-state.js'
14
+ import type { Thunk } from '../reactive.js'
15
+ import { isThunk, NOT_REFRESHED_YET } from '../reactive.js'
16
+ import { makeExecBeforeFirstRun, rowQueryLabel } from '../row-query-utils.js'
17
+ import type { RefreshReason } from '../store/store-types.js'
18
+ import { getDurationMsFromSpan } from '../utils/otel.js'
19
+ import type { GetAtomResult, LiveQuery, QueryContext, ReactivityGraph } from './base-class.js'
20
+ import { LiveStoreQueryBase, makeGetAtomResult } from './base-class.js'
21
+
22
+ export type QueryInputRaw<TDecoded, TEncoded, TQueryInfo extends QueryInfo> = {
23
+ query: string
24
+ schema: Schema.Schema<TDecoded, TEncoded>
25
+ bindValues?: Bindable
26
+ /**
27
+ * Can be provided explicitly to slightly speed up initial query performance
28
+ *
29
+ * NOTE In the future we want to do this automatically at build time
30
+ */
31
+ queriedTables?: Set<string>
32
+ queryInfo?: TQueryInfo
33
+ execBeforeFirstRun?: (ctx: QueryContext) => void
34
+ }
35
+
36
+ export type QueryInput<TDecoded, TEncoded, TQueryInfo extends QueryInfo> =
37
+ | QueryInputRaw<TDecoded, TEncoded, TQueryInfo>
38
+ | QueryBuilder<TDecoded, any, any, TQueryInfo>
39
+
40
+ /**
41
+ * NOTE `query` is only supposed to read data. Don't use it to insert/update/delete data but use mutations instead.
42
+ */
43
+ export const queryDb: {
44
+ <TResultSchema, TResult = TResultSchema, TQueryInfo extends QueryInfo = QueryInfo.None>(
45
+ queryInput:
46
+ | QueryInputRaw<TResultSchema, ReadonlyArray<any>, TQueryInfo>
47
+ | QueryBuilder<TResultSchema, any, any, TQueryInfo>,
48
+ options?: {
49
+ map?: (rows: TResultSchema) => TResult
50
+ /**
51
+ * Used for debugging / devtools
52
+ */
53
+ label?: string
54
+ reactivityGraph?: ReactivityGraph
55
+ otelContext?: otel.Context
56
+ },
57
+ ): LiveQuery<TResult, TQueryInfo>
58
+ // NOTE in this "thunk case", we can't directly derive label/queryInfo from the queryInput,
59
+ // so the caller needs to provide them explicitly otherwise queryInfo will be set to `None`,
60
+ // and label will be set during the query execution
61
+ <TResultSchema, TResult = TResultSchema, TQueryInfo extends QueryInfo = QueryInfo.None>(
62
+ queryInput:
63
+ | ((get: GetAtomResult) => QueryInputRaw<TResultSchema, ReadonlyArray<any>, TQueryInfo>)
64
+ | ((get: GetAtomResult) => QueryBuilder<TResultSchema, any, any, TQueryInfo>),
65
+ options?: {
66
+ map?: (rows: TResultSchema) => TResult
67
+ /**
68
+ * Used for debugging / devtools
69
+ */
70
+ label?: string
71
+ reactivityGraph?: ReactivityGraph
72
+ queryInfo?: TQueryInfo
73
+ otelContext?: otel.Context
74
+ },
75
+ ): LiveQuery<TResult, TQueryInfo>
76
+ } = (queryInput, options) =>
77
+ new LiveStoreDbQuery({
78
+ queryInput,
79
+ label: options?.label,
80
+ reactivityGraph: options?.reactivityGraph,
81
+ map: options?.map,
82
+ queryInfo: Predicate.hasProperty(options, 'queryInfo') ? (options.queryInfo as QueryInfo) : undefined,
83
+ otelContext: options?.otelContext,
84
+ })
85
+
86
+ /* An object encapsulating a reactive SQL query */
87
+ export class LiveStoreDbQuery<
88
+ TResultSchema,
89
+ TResult = TResultSchema,
90
+ TQueryInfo extends QueryInfo = QueryInfo.None,
91
+ > extends LiveStoreQueryBase<TResult, TQueryInfo> {
92
+ _tag: 'db' = 'db'
93
+
94
+ /** A reactive thunk representing the query text */
95
+ queryInput$: Thunk<QueryInput<TResultSchema, ReadonlyArray<any>, TQueryInfo>, QueryContext, RefreshReason> | undefined
96
+
97
+ /** A reactive thunk representing the query results */
98
+ results$: Thunk<TResult, QueryContext, RefreshReason>
99
+
100
+ label: string
101
+
102
+ queryInfo: TQueryInfo
103
+
104
+ protected reactivityGraph
105
+
106
+ private mapResult: (rows: TResultSchema) => TResult
107
+
108
+ constructor({
109
+ queryInput,
110
+ label: inputLabel,
111
+ reactivityGraph,
112
+ map,
113
+ queryInfo: inputQueryInfo,
114
+ otelContext,
115
+ }: {
116
+ label?: string
117
+ queryInput:
118
+ | QueryInput<TResultSchema, ReadonlyArray<any>, TQueryInfo>
119
+ | ((get: GetAtomResult, ctx: QueryContext) => QueryInput<TResultSchema, ReadonlyArray<any>, TQueryInfo>)
120
+ reactivityGraph?: ReactivityGraph
121
+ map?: (rows: TResultSchema) => TResult
122
+ queryInfo?: TQueryInfo
123
+ otelContext?: otel.Context
124
+ }) {
125
+ super()
126
+
127
+ let label = inputLabel ?? 'db(unknown)'
128
+ let queryInfo = inputQueryInfo ?? ({ _tag: 'None' } as TQueryInfo)
129
+ this.reactivityGraph = reactivityGraph ?? globalReactivityGraph
130
+
131
+ this.mapResult = map === undefined ? (rows: any) => rows as TResult : map
132
+
133
+ const schemaRef: { current: Schema.Schema<any, any> | undefined } = {
134
+ current:
135
+ typeof queryInput === 'function' ? undefined : isQueryBuilder(queryInput) ? undefined : queryInput.schema,
136
+ }
137
+
138
+ const execBeforeFirstRunRef: { current: ((ctx: QueryContext, otelContext: otel.Context) => void) | undefined } = {
139
+ current: undefined,
140
+ }
141
+
142
+ type TQueryInputRaw = QueryInputRaw<any, any, QueryInfo>
143
+
144
+ let queryInputRaw$OrQueryInputRaw: TQueryInputRaw | Thunk<TQueryInputRaw, QueryContext, RefreshReason>
145
+
146
+ const fromQueryBuilder = (qb: QueryBuilder.Any, otelContext: otel.Context | undefined) => {
147
+ const qbRes = qb.asSql()
148
+ const schema = getResultSchema(qb) as Schema.Schema<TResultSchema, ReadonlyArray<any>>
149
+ const ast = qb[QueryBuilderAstSymbol]
150
+
151
+ return {
152
+ queryInputRaw: {
153
+ query: qbRes.query,
154
+ schema,
155
+ bindValues: qbRes.bindValues,
156
+ queriedTables: new Set([ast.tableDef.sqliteDef.name]),
157
+ queryInfo: ast._tag === 'RowQuery' ? { _tag: 'Row', table: ast.tableDef, id: ast.id } : { _tag: 'None' },
158
+ } satisfies TQueryInputRaw,
159
+ label: ast._tag === 'RowQuery' ? rowQueryLabel(ast.tableDef, ast.id) : qb.toString(),
160
+ execBeforeFirstRun:
161
+ ast._tag === 'RowQuery'
162
+ ? makeExecBeforeFirstRun({
163
+ table: ast.tableDef,
164
+ insertValues: ast.insertValues,
165
+ id: ast.id,
166
+ otelContext,
167
+ })
168
+ : undefined,
169
+ }
170
+ }
171
+
172
+ if (typeof queryInput === 'function') {
173
+ queryInputRaw$OrQueryInputRaw = this.reactivityGraph.makeThunk(
174
+ (get, setDebugInfo, ctx, otelContext) => {
175
+ const startMs = performance.now()
176
+ const queryInputResult = queryInput(makeGetAtomResult(get, otelContext ?? ctx.rootOtelContext), ctx)
177
+ const durationMs = performance.now() - startMs
178
+
179
+ let queryInputRaw: TQueryInputRaw
180
+
181
+ if (isQueryBuilder(queryInputResult)) {
182
+ const res = fromQueryBuilder(queryInputResult, otelContext)
183
+ queryInputRaw = res.queryInputRaw
184
+ // setting label dynamically here
185
+ this.label = res.label
186
+ execBeforeFirstRunRef.current = res.execBeforeFirstRun
187
+ } else {
188
+ queryInputRaw = queryInputResult
189
+ }
190
+
191
+ setDebugInfo({ _tag: 'computed', label: `${this.label}:queryInput`, query: queryInputRaw.query, durationMs })
192
+
193
+ schemaRef.current = queryInputRaw.schema
194
+
195
+ if (inputQueryInfo === undefined && queryInputRaw.queryInfo !== undefined) {
196
+ queryInfo = queryInputRaw.queryInfo as TQueryInfo
197
+ }
198
+
199
+ return queryInputRaw
200
+ },
201
+ {
202
+ label: `${label}:query`,
203
+ meta: { liveStoreThunkType: 'db.query' },
204
+ // NOTE we're not checking the schema here as we assume the query string to always change when the schema might change
205
+ equal: (a, b) => a.query === b.query && deepEqual(a.bindValues, b.bindValues),
206
+ },
207
+ )
208
+ } else {
209
+ let queryInputRaw: TQueryInputRaw
210
+ if (isQueryBuilder(queryInput)) {
211
+ const res = fromQueryBuilder(queryInput, otelContext)
212
+ queryInputRaw = res.queryInputRaw
213
+ label = res.label
214
+ execBeforeFirstRunRef.current = res.execBeforeFirstRun
215
+ } else {
216
+ queryInputRaw = queryInput
217
+ }
218
+
219
+ schemaRef.current = queryInputRaw.schema
220
+ queryInputRaw$OrQueryInputRaw = queryInputRaw
221
+
222
+ // this.label = inputLabel ? this.label : `db(${})`
223
+ if (inputLabel === undefined && isQueryBuilder(queryInput)) {
224
+ const ast = queryInput[QueryBuilderAstSymbol]
225
+ if (ast._tag === 'RowQuery') {
226
+ label = `db(${rowQueryLabel(ast.tableDef, ast.id)})`
227
+ }
228
+ }
229
+
230
+ if (inputQueryInfo === undefined && queryInputRaw.queryInfo !== undefined) {
231
+ queryInfo = queryInputRaw.queryInfo as TQueryInfo
232
+ }
233
+ }
234
+
235
+ const queriedTablesRef: { current: Set<string> | undefined } = { current: undefined }
236
+
237
+ const makeResultsEqual = (resultSchema: Schema.Schema<any, any>) => (a: TResult, b: TResult) =>
238
+ a === NOT_REFRESHED_YET || b === NOT_REFRESHED_YET ? false : Schema.equivalence(resultSchema)(a, b)
239
+
240
+ // NOTE we try to create the equality function eagerly as it might be expensive
241
+ // TODO also support derived equality for `map` (probably will depend on having an easy way to transform a schema without an `encode` step)
242
+ // This would mean dropping the `map` option
243
+ const resultsEqual =
244
+ map === undefined
245
+ ? schemaRef.current === undefined
246
+ ? (a: TResult, b: TResult) => makeResultsEqual(schemaRef.current!)(a, b)
247
+ : makeResultsEqual(schemaRef.current)
248
+ : undefined
249
+
250
+ const results$ = this.reactivityGraph.makeThunk<TResult>(
251
+ (get, setDebugInfo, queryContext, otelContext) =>
252
+ queryContext.otelTracer.startActiveSpan(
253
+ 'db:...', // NOTE span name will be overridden further down
254
+ {},
255
+ otelContext ?? queryContext.rootOtelContext,
256
+ (span) => {
257
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
258
+ const { store } = queryContext
259
+
260
+ if (execBeforeFirstRunRef.current !== undefined) {
261
+ execBeforeFirstRunRef.current(queryContext, otelContext)
262
+ execBeforeFirstRunRef.current = undefined
263
+ }
264
+
265
+ const queryInputResult = isThunk(queryInputRaw$OrQueryInputRaw)
266
+ ? (get(queryInputRaw$OrQueryInputRaw, otelContext) as TQueryInputRaw)
267
+ : (queryInputRaw$OrQueryInputRaw as TQueryInputRaw)
268
+
269
+ const sqlString = queryInputResult.query
270
+ const bindValues = queryInputResult.bindValues
271
+
272
+ if (queriedTablesRef.current === undefined) {
273
+ queriedTablesRef.current = store.syncDbWrapper.getTablesUsed(sqlString)
274
+ }
275
+
276
+ if (bindValues !== undefined) {
277
+ replaceSessionIdSymbol(bindValues, store.clientSession.coordinator.sessionId)
278
+ }
279
+
280
+ // Establish a reactive dependency on the tables used in the query
281
+ for (const tableName of queriedTablesRef.current) {
282
+ const tableRef = store.tableRefs[tableName] ?? shouldNeverHappen(`No table ref found for ${tableName}`)
283
+ get(tableRef, otelContext)
284
+ }
285
+
286
+ span.setAttribute('sql.query', sqlString)
287
+ span.updateName(`db:${sqlString.slice(0, 50)}`)
288
+
289
+ const rawDbResults = store.syncDbWrapper.select<any>(sqlString, {
290
+ queriedTables: queriedTablesRef.current,
291
+ bindValues: bindValues ? prepareBindValues(bindValues, sqlString) : undefined,
292
+ otelContext,
293
+ })
294
+
295
+ span.setAttribute('sql.rowsCount', rawDbResults.length)
296
+
297
+ const parsedResult = Schema.decodeEither(schemaRef.current!)(rawDbResults)
298
+
299
+ if (parsedResult._tag === 'Left') {
300
+ const parseErrorStr = TreeFormatter.formatErrorSync(parsedResult.left)
301
+ const expectedSchemaStr = String(schemaRef.current!.ast)
302
+ const bindValuesStr = bindValues === undefined ? '' : `\nBind values: ${JSON.stringify(bindValues)}`
303
+
304
+ console.error(
305
+ `\
306
+ Error parsing SQL query result.
307
+
308
+ Query: ${sqlString}\
309
+ ${bindValuesStr}
310
+
311
+ Expected schema: ${expectedSchemaStr}
312
+
313
+ Error: ${parseErrorStr}
314
+
315
+ Result:`,
316
+ rawDbResults,
317
+ )
318
+ return shouldNeverHappen(`Error parsing SQL query result: ${parsedResult.left}`)
319
+ }
320
+
321
+ const result = this.mapResult(parsedResult.right)
322
+
323
+ span.end()
324
+
325
+ const durationMs = getDurationMsFromSpan(span)
326
+
327
+ this.executionTimes.push(durationMs)
328
+
329
+ setDebugInfo({ _tag: 'db', label: `${label}:results`, query: sqlString, durationMs })
330
+
331
+ return result
332
+ },
333
+ ),
334
+ { label: `${label}:results`, meta: { liveStoreThunkType: 'db.results' }, equal: resultsEqual },
335
+ )
336
+
337
+ this.results$ = results$
338
+
339
+ this.label = label
340
+ this.queryInfo = queryInfo
341
+ }
342
+
343
+ destroy = () => {
344
+ if (this.queryInput$ !== undefined) {
345
+ this.reactivityGraph.destroyNode(this.queryInput$)
346
+ }
347
+
348
+ this.reactivityGraph.destroyNode(this.results$)
349
+ }
350
+ }
@@ -1,5 +1,5 @@
1
1
  import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'
2
- import type { QueryInfoNone } from '@livestore/common'
2
+ import type { QueryInfo } from '@livestore/common'
3
3
  import { shouldNeverHappen } from '@livestore/utils'
4
4
  import { Schema, TreeFormatter } from '@livestore/utils/effect'
5
5
  import * as otel from '@opentelemetry/api'
@@ -31,7 +31,7 @@ export const queryGraphQL = <
31
31
  reactivityGraph?: ReactivityGraph
32
32
  map?: MapResult<TResultMapped, TResult>
33
33
  } = {},
34
- ): LiveQuery<TResultMapped, QueryInfoNone> =>
34
+ ): LiveQuery<TResultMapped, QueryInfo.None> =>
35
35
  new LiveStoreGraphQLQuery({ document, genVariableValues, label, reactivityGraph, map })
36
36
 
37
37
  export class LiveStoreGraphQLQuery<
@@ -39,7 +39,7 @@ export class LiveStoreGraphQLQuery<
39
39
  TVariableValues extends Record<string, any>,
40
40
  TContext extends BaseGraphQLContext,
41
41
  TResultMapped extends Record<string, any> = TResult,
42
- > extends LiveStoreQueryBase<TResultMapped, QueryInfoNone> {
42
+ > extends LiveStoreQueryBase<TResultMapped, QueryInfo.None> {
43
43
  _tag: 'graphql' = 'graphql'
44
44
 
45
45
  /** The abstract GraphQL query */
@@ -54,7 +54,7 @@ export class LiveStoreGraphQLQuery<
54
54
 
55
55
  protected reactivityGraph: ReactivityGraph
56
56
 
57
- queryInfo: QueryInfoNone = { _tag: 'None' }
57
+ queryInfo: QueryInfo.None = { _tag: 'None' }
58
58
 
59
59
  private mapResult
60
60
 
@@ -105,7 +105,7 @@ export class LiveStoreGraphQLQuery<
105
105
  (get, _setDebugInfo, { rootOtelContext }, otelContext) => {
106
106
  return genVariableValues(makeGetAtomResult(get, otelContext ?? rootOtelContext))
107
107
  },
108
- { label: `${labelWithDefault}:variableValues`, meta: { liveStoreThunkType: 'graphqlVariableValues' } },
108
+ { label: `${labelWithDefault}:variableValues`, meta: { liveStoreThunkType: 'graphql.variables' } },
109
109
  )
110
110
  this.variableValues$ = variableValues$OrvariableValues
111
111
  } else {
@@ -137,26 +137,11 @@ export class LiveStoreGraphQLQuery<
137
137
 
138
138
  return result
139
139
  },
140
- { label: resultsLabel, meta: { liveStoreThunkType: 'graphqlResults' } },
140
+ { label: resultsLabel, meta: { liveStoreThunkType: 'graphql.results' } },
141
141
  // otelContext,
142
142
  )
143
143
  }
144
144
 
145
- /**
146
- * Returns a new reactive query that contains the result of
147
- * running an arbitrary JS computation on the results of this SQL query.
148
- */
149
- // pipe = <U>(fn: (result: TResult, get: GetAtomResult) => U): LiveStoreJSQuery<U> =>
150
- // new LiveStoreJSQuery({
151
- // fn: (get) => {
152
- // const results = get(this.results$)
153
- // return fn(results, get)
154
- // },
155
- // label: `${this.label}:js`,
156
- // onDestroy: () => this.destroy(),
157
- // reactivityGraph: this.reactivityGraph,
158
- // })
159
-
160
145
  queryOnce = ({
161
146
  document,
162
147
  otelContext,
@@ -0,0 +1,65 @@
1
+ import type { PreparedBindValues, QueryInfo } from '@livestore/common'
2
+ import { SessionIdSymbol } from '@livestore/common'
3
+ import { DbSchema } from '@livestore/common/schema'
4
+ import { shouldNeverHappen } from '@livestore/utils'
5
+ import type * as otel from '@opentelemetry/api'
6
+
7
+ import type { LiveQuery, LiveQueryAny, QueryContext } from './live-queries/base-class.js'
8
+ import { computed } from './live-queries/computed.js'
9
+
10
+ export const rowQueryLabel = (table: DbSchema.TableDefBase, id: string | SessionIdSymbol | undefined) =>
11
+ `row:${table.sqliteDef.name}${id === undefined ? '' : id === SessionIdSymbol ? `:sessionId` : `:${id}`}`
12
+
13
+ export const deriveColQuery: {
14
+ <TQuery extends LiveQuery<any, QueryInfo.None>, TCol extends keyof TQuery['__result!'] & string>(
15
+ query$: TQuery,
16
+ colName: TCol,
17
+ ): LiveQuery<TQuery['__result!'][TCol], QueryInfo.None>
18
+ <TQuery extends LiveQuery<any, QueryInfo.Row>, TCol extends keyof TQuery['__result!'] & string>(
19
+ query$: TQuery,
20
+ colName: TCol,
21
+ ): LiveQuery<TQuery['__result!'][TCol], QueryInfo.Col>
22
+ } = (query$: LiveQueryAny, colName: string) => {
23
+ return computed((get) => get(query$)[colName], {
24
+ label: `deriveColQuery:${query$.label}:${colName}`,
25
+ queryInfo:
26
+ query$.queryInfo._tag === 'Row'
27
+ ? { _tag: 'Col', table: query$.queryInfo.table, column: colName, id: query$.queryInfo.id }
28
+ : undefined,
29
+ }) as any
30
+ }
31
+
32
+ export const makeExecBeforeFirstRun =
33
+ ({
34
+ id,
35
+ insertValues,
36
+ table,
37
+ otelContext: otelContext_,
38
+ }: {
39
+ id?: string | SessionIdSymbol
40
+ insertValues?: any
41
+ table: DbSchema.TableDefBase
42
+ otelContext: otel.Context | undefined
43
+ }) =>
44
+ ({ store }: QueryContext) => {
45
+ const otelContext = otelContext_ ?? store.otel.queriesSpanContext
46
+
47
+ if (table.options.isSingleton === false) {
48
+ const idStr = id === SessionIdSymbol ? store.sessionId : id!
49
+ const rowExists =
50
+ store.syncDbWrapper.select(`SELECT 1 FROM '${table.sqliteDef.name}' WHERE id = ?`, {
51
+ bindValues: [idStr] as any as PreparedBindValues,
52
+ }).length === 1
53
+
54
+ if (rowExists) return
55
+
56
+ if (DbSchema.tableHasDerivedMutations(table) === false) {
57
+ return shouldNeverHappen(
58
+ `Cannot insert row for table "${table.sqliteDef.name}" which does not have 'deriveMutations: true' set`,
59
+ )
60
+ }
61
+
62
+ // NOTE It's important that we only mutate and don't refresh here, as this function is called during a render
63
+ store.mutateWithoutRefresh(table.insert({ id, ...insertValues }), { otelContext, coordinatorMode: 'default' })
64
+ }
65
+ }
@@ -28,7 +28,7 @@ import {
28
28
  import * as otel from '@opentelemetry/api'
29
29
 
30
30
  import { globalReactivityGraph } from '../global-state.js'
31
- import type { ReactivityGraph } from '../reactiveQueries/base-class.js'
31
+ import type { ReactivityGraph } from '../live-queries/base-class.js'
32
32
  import { connectDevtoolsToStore } from './devtools.js'
33
33
  import { Store } from './store.js'
34
34
  import type { BaseGraphQLContext, GraphQLOptions, OtelOptions } from './store-types.js'
@@ -4,8 +4,8 @@ import { throttle } from '@livestore/utils'
4
4
  import type { WebChannel } from '@livestore/utils/effect'
5
5
  import { Effect, Stream } from '@livestore/utils/effect'
6
6
 
7
+ import type { LiveQuery, ReactivityGraph } from '../live-queries/base-class.js'
7
8
  import { NOT_REFRESHED_YET } from '../reactive.js'
8
- import type { LiveQuery, ReactivityGraph } from '../reactiveQueries/base-class.js'
9
9
  import type { SynchronousDatabaseWrapper } from '../SynchronousDatabaseWrapper.js'
10
10
  import { emptyDebugInfo as makeEmptyDebugInfo } from '../SynchronousDatabaseWrapper.js'
11
11
  import type { ReferenceCountedSet } from '../utils/data-structures.js'
@@ -5,8 +5,8 @@ import { Schema } from '@livestore/utils/effect'
5
5
  import type * as otel from '@opentelemetry/api'
6
6
  import type { GraphQLSchema } from 'graphql'
7
7
 
8
+ import type { ReactivityGraph } from '../live-queries/base-class.js'
8
9
  import type { DebugRefreshReasonBase } from '../reactive.js'
9
- import type { ReactivityGraph } from '../reactiveQueries/base-class.js'
10
10
  import type { SynchronousDatabaseWrapper } from '../SynchronousDatabaseWrapper.js'
11
11
  import type { StackInfo } from '../utils/stack-info.js'
12
12
  import type { Store } from './store.js'
@@ -84,7 +84,7 @@ export type RefreshReason =
84
84
  | { _tag: 'manual'; label?: string }
85
85
 
86
86
  export type QueryDebugInfo = {
87
- _tag: 'graphql' | 'sql' | 'computed' | 'unknown'
87
+ _tag: 'graphql' | 'db' | 'computed' | 'unknown'
88
88
  label: string
89
89
  query: string
90
90
  durationMs: number
@@ -1,5 +1,12 @@
1
- import type { ClientSession, ParamsObject } from '@livestore/common'
2
- import { getExecArgsFromMutation, prepareBindValues, replaceSessionIdSymbol } from '@livestore/common'
1
+ import type { ClientSession, ParamsObject, PreparedBindValues, QueryBuilder } from '@livestore/common'
2
+ import {
3
+ getExecArgsFromMutation,
4
+ getResultSchema,
5
+ isQueryBuilder,
6
+ prepareBindValues,
7
+ QueryBuilderAstSymbol,
8
+ replaceSessionIdSymbol,
9
+ } from '@livestore/common'
3
10
  import type { LiveStoreSchema, MutationEvent } from '@livestore/common/schema'
4
11
  import {
5
12
  isPartialMutationEvent,
@@ -14,8 +21,9 @@ import { Data, Effect, FiberSet, Inspectable, MutableHashMap, Runtime, Schema, S
14
21
  import * as otel from '@opentelemetry/api'
15
22
  import type { GraphQLSchema } from 'graphql'
16
23
 
24
+ import type { LiveQuery, QueryContext, ReactivityGraph } from '../live-queries/base-class.js'
17
25
  import type { Ref } from '../reactive.js'
18
- import type { LiveQuery, QueryContext, ReactivityGraph } from '../reactiveQueries/base-class.js'
26
+ import { makeExecBeforeFirstRun } from '../row-query-utils.js'
19
27
  import { SynchronousDatabaseWrapper } from '../SynchronousDatabaseWrapper.js'
20
28
  import { ReferenceCountedSet } from '../utils/data-structures.js'
21
29
  import { downloadBlob, exposeDebugUtils } from '../utils/dev.js'
@@ -234,6 +242,39 @@ export class Store<
234
242
  },
235
243
  )
236
244
 
245
+ query = <TResult>(
246
+ query: QueryBuilder<TResult, any, any> | LiveQuery<TResult, any> | { query: string; bindValues: ParamsObject },
247
+ options?: { otelContext?: otel.Context },
248
+ ): TResult => {
249
+ if (typeof query === 'object' && 'query' in query && 'bindValues' in query) {
250
+ return this.syncDbWrapper.select(query.query, {
251
+ bindValues: prepareBindValues(query.bindValues, query.query),
252
+ otelContext: options?.otelContext,
253
+ }) as any
254
+ } else if (isQueryBuilder(query)) {
255
+ const ast = query[QueryBuilderAstSymbol]
256
+ if (ast._tag === 'RowQuery') {
257
+ makeExecBeforeFirstRun({
258
+ table: ast.tableDef,
259
+ id: ast.id,
260
+ insertValues: ast.insertValues,
261
+ otelContext: options?.otelContext,
262
+ })(this.reactivityGraph.context!)
263
+ }
264
+
265
+ const sqlRes = query.asSql()
266
+ const schema = getResultSchema(query)
267
+ const rawRes = this.syncDbWrapper.select(sqlRes.query, {
268
+ bindValues: sqlRes.bindValues as any as PreparedBindValues,
269
+ otelContext: options?.otelContext,
270
+ queriedTables: new Set([query[QueryBuilderAstSymbol].tableDef.sqliteDef.name]),
271
+ })
272
+ return Schema.decodeSync(schema)(rawRes)
273
+ } else {
274
+ return query.run(options?.otelContext)
275
+ }
276
+ }
277
+
237
278
  // #region mutate
238
279
  mutate: {
239
280
  <const TMutationArg extends ReadonlyArray<MutationEvent.PartialForSchema<TSchema>>>(...list: TMutationArg): void
@@ -518,10 +559,6 @@ export class Store<
518
559
  this.clientSession.coordinator.execute(query, prepareBindValues(params, query)).pipe(this.runEffectFork)
519
560
  }
520
561
 
521
- __select = (query: string, params: ParamsObject = {}) => {
522
- return this.syncDbWrapper.select(query, { bindValues: prepareBindValues(params, query) })
523
- }
524
-
525
562
  private makeTableRef = (tableName: string) =>
526
563
  this.reactivityGraph.makeRef(null, {
527
564
  equal: () => false,
@@ -1,35 +0,0 @@
1
- import type { QueryInfo, QueryInfoNone } from '@livestore/common';
2
- import type { Thunk } from '../reactive.js';
3
- import type { RefreshReason } from '../store.js';
4
- import type { GetAtomResult, LiveQuery, QueryContext, ReactivityGraph } from './base-class.js';
5
- import { LiveStoreQueryBase } from './base-class.js';
6
- export declare const computed: <TResult, TQueryInfo extends QueryInfo = QueryInfoNone>(fn: (get: GetAtomResult) => TResult, options?: {
7
- label: string;
8
- reactivityGraph?: ReactivityGraph;
9
- queryInfo?: TQueryInfo;
10
- }) => LiveQuery<TResult, TQueryInfo>;
11
- export declare class LiveStoreJSQuery<TResult, TQueryInfo extends QueryInfo = QueryInfoNone> extends LiveStoreQueryBase<TResult, TQueryInfo> {
12
- _tag: 'js';
13
- /** A reactive thunk representing the query results */
14
- results$: Thunk<TResult, QueryContext, RefreshReason>;
15
- label: string;
16
- protected reactivityGraph: ReactivityGraph;
17
- queryInfo: TQueryInfo;
18
- /**
19
- * Currently only used for "nested destruction" of piped queries
20
- *
21
- * i.e. when doing something like `const q = querySQL(...).pipe(...)`
22
- * we need to also destory the SQL query when the JS query `q` is destroyed
23
- */
24
- private onDestroy;
25
- constructor({ fn, label, onDestroy, reactivityGraph, queryInfo, }: {
26
- label: string;
27
- fn: (get: GetAtomResult) => TResult;
28
- /** Currently only used for "nested destruction" of piped queries */
29
- onDestroy?: () => void;
30
- reactivityGraph?: ReactivityGraph;
31
- queryInfo?: TQueryInfo;
32
- });
33
- destroy: () => void;
34
- }
35
- //# sourceMappingURL=js.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"js.d.ts","sourceRoot":"","sources":["../../src/reactiveQueries/js.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAIjE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAC3C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAEhD,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAC9F,OAAO,EAAE,kBAAkB,EAAqB,MAAM,iBAAiB,CAAA;AAEvE,eAAO,MAAM,QAAQ,GAAI,OAAO,EAAE,UAAU,SAAS,SAAS,sBACxD,CAAC,GAAG,EAAE,aAAa,KAAK,OAAO,YACzB;IACR,KAAK,EAAE,MAAM,CAAA;IACb,eAAe,CAAC,EAAE,eAAe,CAAA;IACjC,SAAS,CAAC,EAAE,UAAU,CAAA;CACvB,KACA,SAAS,CAAC,OAAO,EAAE,UAAU,CAM5B,CAAA;AAEJ,qBAAa,gBAAgB,CAAC,OAAO,EAAE,UAAU,SAAS,SAAS,GAAG,aAAa,CAAE,SAAQ,kBAAkB,CAC7G,OAAO,EACP,UAAU,CACX;IACC,IAAI,EAAE,IAAI,CAAO;IAEjB,sDAAsD;IACtD,QAAQ,EAAE,KAAK,CAAC,OAAO,EAAE,YAAY,EAAE,aAAa,CAAC,CAAA;IAErD,KAAK,EAAE,MAAM,CAAA;IAEb,SAAS,CAAC,eAAe,EAAE,eAAe,CAAA;IAE1C,SAAS,EAAE,UAAU,CAAA;IAErB;;;;;OAKG;IACH,OAAO,CAAC,SAAS,CAA0B;gBAE/B,EACV,EAAE,EACF,KAAK,EACL,SAAS,EACT,eAAe,EACf,SAAS,GACV,EAAE;QACD,KAAK,EAAE,MAAM,CAAA;QACb,EAAE,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,OAAO,CAAA;QACnC,oEAAoE;QACpE,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;QACtB,eAAe,CAAC,EAAE,eAAe,CAAA;QACjC,SAAS,CAAC,EAAE,UAAU,CAAA;KACvB;IA0CD,OAAO,aAGN;CACF"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"js.js","sourceRoot":"","sources":["../../src/reactiveQueries/js.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,IAAI,MAAM,oBAAoB,CAAA;AAE1C,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAG1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AAExD,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAEvE,MAAM,CAAC,MAAM,QAAQ,GAAG,CACtB,EAAmC,EACnC,OAIC,EAC+B,EAAE,CAClC,IAAI,gBAAgB,CAAsB;IACxC,EAAE;IACF,KAAK,EAAE,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC,QAAQ,EAAE;IACtC,eAAe,EAAE,OAAO,EAAE,eAAe;IACzC,SAAS,EAAE,OAAO,EAAE,SAAS;CAC9B,CAAC,CAAA;AAEJ,MAAM,OAAO,gBAAwE,SAAQ,kBAG5F;IACC,IAAI,GAAS,IAAI,CAAA;IAEjB,sDAAsD;IACtD,QAAQ,CAA6C;IAErD,KAAK,CAAQ;IAEH,eAAe,CAAiB;IAE1C,SAAS,CAAY;IAErB;;;;;OAKG;IACK,SAAS,CAA0B;IAE3C,YAAY,EACV,EAAE,EACF,KAAK,EACL,SAAS,EACT,eAAe,EACf,SAAS,GAQV;QACC,KAAK,EAAE,CAAA;QAEP,IAAI,CAAC,SAAS,GAAG,SAAS,CAAA;QAC1B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;QAElB,IAAI,CAAC,eAAe,GAAG,eAAe,IAAI,qBAAqB,CAAA;QAC/D,IAAI,CAAC,SAAS,GAAG,SAAS,IAAK,EAAE,IAAI,EAAE,MAAM,EAAiB,CAAA;QAE9D,MAAM,UAAU,GAAG,GAAG,KAAK,UAAU,CAAA;QAErC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,SAAS,CAC5C,CAAC,GAAG,EAAE,YAAY,EAAE,EAAE,UAAU,EAAE,eAAe,EAAE,EAAE,WAAW,EAAE,EAAE,CAClE,UAAU,CAAC,eAAe,CAAC,MAAM,KAAK,EAAE,EAAE,EAAE,EAAE,WAAW,IAAI,eAAe,EAAE,CAAC,IAAI,EAAE,EAAE;YACrF,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,CAAA;YACnE,MAAM,GAAG,GAAG,EAAE,CAAC,iBAAiB,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC,CAAA;YAEnD,IAAI,CAAC,GAAG,EAAE,CAAA;YAEV,MAAM,UAAU,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAA;YAE9C,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;YAEpC,YAAY,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,QAAQ,EAAE,EAAE,UAAU,EAAE,CAAC,CAAA;YAErE,OAAO,GAAG,CAAA;QACZ,CAAC,CAAC,EACJ,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,kBAAkB,EAAE,WAAW,EAAE,EAAE,CACjE,CAAA;IACH,CAAC;IAED,qFAAqF;IACrF,2BAA2B;IAC3B,qBAAqB;IACrB,2CAA2C;IAC3C,gCAAgC;IAChC,SAAS;IACT,iCAAiC;IACjC,uCAAuC;IACvC,6CAA6C;IAC7C,OAAO;IAEP,OAAO,GAAG,GAAG,EAAE;QACb,IAAI,CAAC,eAAe,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QAC/C,IAAI,CAAC,SAAS,EAAE,EAAE,CAAA;IACpB,CAAC,CAAA;CACF"}
@@ -1,26 +0,0 @@
1
- import type { IntentionalShutdownCause, UnexpectedError } from '@livestore/common';
2
- import { Schema } from '@livestore/utils/effect';
3
- import type { Store } from './store.js';
4
- export type LiveStoreContext = LiveStoreContextRunning | {
5
- stage: 'error';
6
- error: UnexpectedError | unknown;
7
- } | {
8
- stage: 'shutdown';
9
- cause: IntentionalShutdownCause | StoreAbort;
10
- };
11
- declare const StoreAbort_base: Schema.TaggedErrorClass<StoreAbort, "LiveStore.StoreAbort", {
12
- readonly _tag: Schema.tag<"LiveStore.StoreAbort">;
13
- }>;
14
- export declare class StoreAbort extends StoreAbort_base {
15
- }
16
- declare const StoreInterrupted_base: Schema.TaggedErrorClass<StoreInterrupted, "LiveStore.StoreInterrupted", {
17
- readonly _tag: Schema.tag<"LiveStore.StoreInterrupted">;
18
- }>;
19
- export declare class StoreInterrupted extends StoreInterrupted_base {
20
- }
21
- export type LiveStoreContextRunning = {
22
- stage: 'running';
23
- store: Store;
24
- };
25
- export {};
26
- //# sourceMappingURL=store-context.d.ts.map