@livestore/livestore 0.0.19 → 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 +4 -3
  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
package/src/store.ts CHANGED
@@ -1,34 +1,28 @@
1
- import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'
2
1
  import { assertNever, makeNoopSpan, makeNoopTracer, shouldNeverHappen } from '@livestore/utils'
3
2
  import { identity } from '@livestore/utils/effect'
4
3
  import * as otel from '@opentelemetry/api'
5
4
  import type { GraphQLSchema } from 'graphql'
6
- import * as graphql from 'graphql'
7
- import { uniqueId } from 'lodash-es'
8
- import * as ReactDOM from 'react-dom'
9
5
  import type * as Sqlite from 'sqlite-esm'
10
6
  import { v4 as uuid } from 'uuid'
11
7
 
12
8
  import type { ComponentKey } from './componentKey.js'
13
9
  import { tableNameForComponentKey } from './componentKey.js'
14
- import type { QueryDefinition } from './effect/LiveStore.js'
15
10
  import type { LiveStoreEvent } from './events.js'
16
11
  import { InMemoryDatabase } from './inMemoryDatabase.js'
17
12
  import { migrateDb } from './migrations.js'
18
13
  import { getDurationMsFromSpan } from './otel.js'
19
- import type { Atom, Ref } from './reactive.js'
20
- import { ReactiveGraph } from './reactive.js'
21
- import { LiveStoreGraphQLQuery } from './reactiveQueries/graphql.js'
22
- import { LiveStoreJSQuery } from './reactiveQueries/js.js'
23
- import { LiveStoreSQLQuery } from './reactiveQueries/sql.js'
14
+ import type { ReactiveGraph, Ref } from './reactive.js'
15
+ import type { ILiveStoreQuery } from './reactiveQueries/base-class.js'
16
+ import { type DbContext, dbGraph } from './reactiveQueries/graph.js'
17
+ import type { LiveStoreGraphQLQuery } from './reactiveQueries/graphql.js'
18
+ import type { LiveStoreJSQuery } from './reactiveQueries/js.js'
19
+ import type { LiveStoreSQLQuery } from './reactiveQueries/sql.js'
24
20
  import type { ActionDefinition, GetActionArgs, Schema, SQLWriteStatement } from './schema.js'
25
21
  import { componentStateTables } from './schema.js'
26
22
  import type { Storage, StorageInit } from './storage/index.js'
27
- import type { Bindable, ParamsObject } from './util.js'
23
+ import type { ParamsObject } from './util.js'
28
24
  import { isPromise, prepareBindValues, sql } from './util.js'
29
25
 
30
- export type GetAtomResult = <T>(atom: Atom<T> | LiveStoreJSQuery<T>) => T
31
-
32
26
  export type LiveStoreQuery<TResult extends Record<string, any> = any> =
33
27
  | LiveStoreSQLQuery<TResult>
34
28
  | LiveStoreJSQuery<TResult>
@@ -48,7 +42,7 @@ export type QueryResult<TQuery> = TQuery extends LiveStoreSQLQuery<infer R>
48
42
  ? Readonly<Result>
49
43
  : never
50
44
 
51
- const globalComponentKey: ComponentKey = { _tag: 'singleton', componentName: '__global', id: 'singleton' }
45
+ export const globalComponentKey: ComponentKey = { _tag: 'singleton', componentName: '__global', id: 'singleton' }
52
46
 
53
47
  export type GraphQLOptions<TContext> = {
54
48
  schema: GraphQLSchema
@@ -104,7 +98,7 @@ export type StoreOtel = {
104
98
  }
105
99
 
106
100
  export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLContext> {
107
- graph: ReactiveGraph<RefreshReason, QueryDebugInfo>
101
+ graph: ReactiveGraph<RefreshReason, QueryDebugInfo, DbContext>
108
102
  inMemoryDB: InMemoryDatabase
109
103
  // TODO refactor
110
104
  _proxyDb: InMemoryDatabase
@@ -132,12 +126,12 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
132
126
  }: StoreOptions<TGraphQLContext>) {
133
127
  this.inMemoryDB = db
134
128
  this._proxyDb = dbProxy
135
- this.graph = new ReactiveGraph({
136
- // TODO move this into React module
137
- // Do all our updates inside a single React setState batch to avoid multiple UI re-renders
138
- effectsWrapper: (run) => ReactDOM.unstable_batchedUpdates(() => run()),
139
- otelTracer,
140
- })
129
+ // this.graph = new ReactiveGraph({
130
+ // // TODO move this into React module
131
+ // // Do all our updates inside a single React setState batch to avoid multiple UI re-renders
132
+ // effectsWrapper: (run) => ReactDOM.unstable_batchedUpdates(() => run()),
133
+ // otelTracer,
134
+ // })
141
135
  this.schema = schema
142
136
  // TODO generalize the `tableRefs` concept to allow finer-grained refs
143
137
  this.tableRefs = {}
@@ -150,6 +144,9 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
150
144
  const queriesSpan = otelTracer.startSpan('LiveStore:queries', {}, otelRootSpanContext)
151
145
  const otelQueriesSpanContext = otel.trace.setSpan(otel.context.active(), queriesSpan)
152
146
 
147
+ this.graph = dbGraph
148
+ this.graph.context = { store: this, otelTracer, rootOtelContext: otelQueriesSpanContext }
149
+
153
150
  this.otel = {
154
151
  tracer: otelTracer,
155
152
  applyEventsSpanContext: otelApplyEventsSpanContext,
@@ -194,398 +191,334 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
194
191
  *
195
192
  * NOTE The query is actually running (even if no one has subscribed to it yet) and will be kept up to date.
196
193
  */
197
- querySQL = <TResult>(
198
- genQueryString: string | ((get: GetAtomResult) => string),
199
- {
200
- queriedTables,
201
- bindValues,
202
- componentKey,
203
- label,
204
- otelContext = otel.context.active(),
205
- }: {
206
- /**
207
- * List of tables that are queried in this query;
208
- * used to determine reactive dependencies.
209
- *
210
- * NOTE In the future we want to auto-generate this via parsing the query
211
- */
212
- queriedTables: string[]
213
- bindValues?: Bindable | undefined
214
- componentKey?: ComponentKey | undefined
215
- label?: string | undefined
216
- otelContext?: otel.Context
217
- },
218
- ): LiveStoreSQLQuery<TResult> =>
219
- this.otel.tracer.startActiveSpan(
220
- 'querySQL', // NOTE span name will be overridden further down
221
- { attributes: { label } },
222
- otelContext,
223
- (span) => {
224
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
225
-
226
- const queryString$ = this.graph.makeThunk(
227
- (get, addDebugInfo) => {
228
- if (typeof genQueryString === 'function') {
229
- const getAtom: GetAtomResult = (atom) => {
230
- if (atom._tag === 'thunk' || atom._tag === 'ref') return get(atom)
231
- return get(atom.results$)
232
- }
233
- const queryString = genQueryString(getAtom)
234
- addDebugInfo({ _tag: 'js', label: `${label}:queryString`, query: queryString })
235
- return queryString
236
- } else {
237
- return genQueryString
238
- }
239
- },
240
- { label: `${label}:queryString`, meta: { liveStoreThunkType: 'sqlQueryString' } },
241
- otelContext,
242
- )
243
-
244
- label = label ?? queryString$.result
245
- span.updateName(`querySQL:${label}`)
246
-
247
- const queryLabel = `${label}:results` + (this.temporaryQueries ? ':temp' : '')
248
-
249
- const results$ = this.graph.makeThunk<ReadonlyArray<TResult>>(
250
- (get, addDebugInfo) =>
251
- this.otel.tracer.startActiveSpan(
252
- 'sql:', // NOTE span name will be overridden further down
253
- {},
254
- otelContext,
255
- (span) => {
256
- try {
257
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
258
-
259
- // Establish a reactive dependency on the tables used in the query
260
- for (const tableName of queriedTables) {
261
- const tableRef =
262
- this.tableRefs[tableName] ?? shouldNeverHappen(`No table ref found for ${tableName}`)
263
- get(tableRef)
264
- }
265
- const sqlString = get(queryString$)
266
-
267
- span.setAttribute('sql.query', sqlString)
268
- span.updateName(`sql:${sqlString.slice(0, 50)}`)
269
-
270
- const results = this.inMemoryDB.select<TResult>(sqlString, {
271
- queriedTables,
272
- bindValues: bindValues ? prepareBindValues(bindValues, sqlString) : undefined,
273
- otelContext,
274
- })
275
-
276
- span.setAttribute('sql.rowsCount', results.length)
277
- addDebugInfo({ _tag: 'sql', label: label ?? '', query: sqlString })
278
-
279
- return results
280
- } finally {
281
- span.end()
282
- }
283
- },
284
- ),
285
- { label: queryLabel },
286
- otelContext,
287
- )
288
-
289
- const query = new LiveStoreSQLQuery<TResult>({
290
- label,
291
- queryString$,
292
- results$,
293
- componentKey: componentKey ?? globalComponentKey,
294
- store: this,
295
- otelContext,
296
- })
297
-
298
- this.activeQueries.add(query)
299
-
300
- // TODO get rid of temporary query workaround
301
- if (this.temporaryQueries !== undefined) {
302
- this.temporaryQueries.add(query)
303
- }
304
-
305
- // NOTE we are not ending the span here but in the query `destroy` method
306
- return query
307
- },
308
- )
309
-
310
- queryJS = <TResult>(
311
- genResults: (get: GetAtomResult) => TResult,
312
- {
313
- componentKey = globalComponentKey,
314
- label = `js${uniqueId()}`,
315
- otelContext = otel.context.active(),
316
- }: { componentKey?: ComponentKey; label?: string; otelContext?: otel.Context },
317
- ): LiveStoreJSQuery<TResult> =>
318
- this.otel.tracer.startActiveSpan(`queryJS:${label}`, { attributes: { label } }, otelContext, (span) => {
319
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
320
- const queryLabel = `${label}:results` + (this.temporaryQueries ? ':temp' : '')
321
- const results$ = this.graph.makeThunk(
322
- (get, addDebugInfo) => {
323
- const getAtom: GetAtomResult = (atom) => {
324
- if (atom._tag === 'thunk' || atom._tag === 'ref') return get(atom)
325
- return get(atom.results$)
326
- }
327
- addDebugInfo({ _tag: 'js', label, query: genResults.toString() })
328
- return genResults(getAtom)
329
- },
330
- { label: queryLabel, meta: { liveStoreThunkType: 'jsResults' } },
331
- otelContext,
332
- )
333
-
334
- const query = new LiveStoreJSQuery<TResult>({
335
- label,
336
- results$,
337
- componentKey,
338
- store: this,
339
- otelContext,
340
- })
341
-
342
- this.activeQueries.add(query)
343
-
344
- // TODO get rid of temporary query workaround
345
- if (this.temporaryQueries !== undefined) {
346
- this.temporaryQueries.add(query)
347
- }
348
-
349
- // NOTE we are not ending the span here but in the query `destroy` method
350
- return query
351
- })
352
-
353
- queryGraphQL = <TResult extends Record<string, any>, TVariableValues extends Record<string, any>>(
354
- document: DocumentNode<TResult, TVariableValues>,
355
- genVariableValues: TVariableValues | ((get: GetAtomResult) => TVariableValues),
356
- {
357
- componentKey,
358
- label,
359
- otelContext = otel.context.active(),
360
- }: {
361
- componentKey: ComponentKey
362
- label?: string
363
- otelContext?: otel.Context
364
- },
365
- ): LiveStoreGraphQLQuery<TResult, TVariableValues, TGraphQLContext> =>
366
- this.otel.tracer.startActiveSpan(
367
- `queryGraphQL:`, // NOTE span name will be overridden further down
368
- {},
369
- otelContext,
370
- (span) => {
371
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
372
-
373
- if (this.graphQLContext === undefined) {
374
- return shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL context")
375
- }
376
-
377
- const labelWithDefault = label ?? graphql.getOperationAST(document)?.name?.value ?? 'graphql'
378
-
379
- span.updateName(`queryGraphQL:${labelWithDefault}`)
380
-
381
- const variableValues$ = this.graph.makeThunk(
382
- (get) => {
383
- if (typeof genVariableValues === 'function') {
384
- const getAtom: GetAtomResult = (atom) => {
385
- if (atom._tag === 'thunk' || atom._tag === 'ref') return get(atom)
386
- return get(atom.results$)
387
- }
388
- return genVariableValues(getAtom)
389
- } else {
390
- return genVariableValues
391
- }
392
- },
393
- { label: `${labelWithDefault}:variableValues`, meta: { liveStoreThunkType: 'graphqlVariableValues' } },
394
- otelContext,
395
- )
396
-
397
- const resultsLabel = `${labelWithDefault}:results` + (this.temporaryQueries ? ':temp' : '')
398
- const results$ = this.graph.makeThunk<TResult>(
399
- (get, addDebugInfo) => {
400
- const variableValues = get(variableValues$)
401
- const { result, queriedTables } = this.queryGraphQLOnce(document, variableValues, otelContext)
402
-
403
- // Add dependencies on any tables that were used
404
- for (const tableName of queriedTables) {
405
- const tableRef = this.tableRefs[tableName]
406
- assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
407
- get(tableRef!)
408
- }
409
-
410
- addDebugInfo({ _tag: 'graphql', label: resultsLabel, query: graphql.print(document) })
411
-
412
- return result
413
- },
414
- { label: resultsLabel, meta: { liveStoreThunkType: 'graphqlResults' } },
415
- otelContext,
416
- )
417
-
418
- const query = new LiveStoreGraphQLQuery({
419
- document,
420
- context: this.graphQLContext,
421
- results$,
422
- componentKey,
423
- label: labelWithDefault,
424
- store: this,
425
- otelContext,
426
- })
427
-
428
- this.activeQueries.add(query)
429
-
430
- // TODO get rid of temporary query workaround
431
- if (this.temporaryQueries !== undefined) {
432
- this.temporaryQueries.add(query)
433
- }
434
-
435
- // NOTE we are not ending the span here but in the query `destroy` method
436
- return query
437
- },
438
- )
439
-
440
- queryGraphQLOnce = <TResult extends Record<string, any>, TVariableValues extends Record<string, any>>(
441
- document: DocumentNode<TResult, TVariableValues>,
442
- variableValues: TVariableValues,
443
- otelContext: otel.Context = this.otel.queriesSpanContext,
444
- ): { result: TResult; queriedTables: string[] } => {
445
- const schema =
446
- this.graphQLSchema ?? shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL schema")
447
- const context =
448
- this.graphQLContext ?? shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL context")
449
- const tracer = this.otel.tracer
450
-
451
- const operationName = graphql.getOperationAST(document)?.name?.value
452
-
453
- return tracer.startActiveSpan(`executeGraphQLQuery: ${operationName}`, {}, otelContext, (span) => {
454
- try {
455
- span.setAttribute('graphql.variables', JSON.stringify(variableValues))
456
- span.setAttribute('graphql.query', graphql.print(document))
457
-
458
- context.queriedTables.clear()
459
-
460
- context.otelContext = otel.trace.setSpan(otel.context.active(), span)
461
-
462
- const res = graphql.executeSync({
463
- document,
464
- contextValue: context,
465
- schema: schema,
466
- variableValues,
467
- })
468
-
469
- // TODO track number of nested SQL queries via Otel + debug info
470
-
471
- if (res.errors) {
472
- span.setStatus({ code: otel.SpanStatusCode.ERROR, message: 'GraphQL error' })
473
- span.setAttribute('graphql.error', res.errors.join('\n'))
474
- span.setAttribute('graphql.error-detail', JSON.stringify(res.errors))
475
- console.error(`graphql error (${operationName})`, res.errors)
476
- }
477
-
478
- return { result: res.data as unknown as TResult, queriedTables: Array.from(context.queriedTables.values()) }
479
- } finally {
480
- span.end()
481
- }
482
- })
483
- }
194
+ // querySQL = <TResult>(
195
+ // genQueryString: string | ((get: GetAtomResult) => string),
196
+ // {
197
+ // queriedTables,
198
+ // bindValues,
199
+ // componentKey,
200
+ // label,
201
+ // otelContext = otel.context.active(),
202
+ // }: {
203
+ // /**
204
+ // * List of tables that are queried in this query;
205
+ // * used to determine reactive dependencies.
206
+ // *
207
+ // * NOTE In the future we want to auto-generate this via parsing the query
208
+ // */
209
+ // queriedTables: string[]
210
+ // bindValues?: Bindable | undefined
211
+ // componentKey?: ComponentKey | undefined
212
+ // label?: string | undefined
213
+ // otelContext?: otel.Context
214
+ // },
215
+ // ): LiveStoreSQLQuery<TResult> =>
216
+ // this.otel.tracer.startActiveSpan(
217
+ // 'querySQL', // NOTE span name will be overridden further down
218
+ // { attributes: { label } },
219
+ // otelContext,
220
+ // (span) => {
221
+ // const otelContext = otel.trace.setSpan(otel.context.active(), span)
222
+
223
+ // const queryString$ = this.graph.makeThunk(
224
+ // (get, addDebugInfo) => {
225
+ // if (typeof genQueryString === 'function') {
226
+ // const queryString = genQueryString(makeGetAtomResult(get))
227
+ // addDebugInfo({ _tag: 'js', label: `${label}:queryString`, query: queryString })
228
+ // return queryString
229
+ // } else {
230
+ // return genQueryString
231
+ // }
232
+ // },
233
+ // { label: `${label}:queryString`, meta: { liveStoreThunkType: 'sqlQueryString' } },
234
+ // otelContext,
235
+ // )
236
+
237
+ // label = label ?? queryString$.result
238
+ // span.updateName(`querySQL:${label}`)
239
+
240
+ // const queryLabel = `${label}:results` + (this.temporaryQueries ? ':temp' : '')
241
+
242
+ // const results$ = this.graph.makeThunk<ReadonlyArray<TResult>>(
243
+ // (get, addDebugInfo) =>
244
+ // this.otel.tracer.startActiveSpan(
245
+ // 'sql:', // NOTE span name will be overridden further down
246
+ // {},
247
+ // otelContext,
248
+ // (span) => {
249
+ // try {
250
+ // const otelContext = otel.trace.setSpan(otel.context.active(), span)
251
+
252
+ // // Establish a reactive dependency on the tables used in the query
253
+ // for (const tableName of queriedTables) {
254
+ // const tableRef =
255
+ // this.tableRefs[tableName] ?? shouldNeverHappen(`No table ref found for ${tableName}`)
256
+ // get(tableRef)
257
+ // }
258
+ // const sqlString = get(queryString$)
259
+
260
+ // span.setAttribute('sql.query', sqlString)
261
+ // span.updateName(`sql:${sqlString.slice(0, 50)}`)
262
+
263
+ // const results = this.inMemoryDB.select<TResult>(sqlString, {
264
+ // queriedTables,
265
+ // bindValues: bindValues ? prepareBindValues(bindValues, sqlString) : undefined,
266
+ // otelContext,
267
+ // })
268
+
269
+ // span.setAttribute('sql.rowsCount', results.length)
270
+ // addDebugInfo({ _tag: 'sql', label: label ?? '', query: sqlString })
271
+
272
+ // return results
273
+ // } finally {
274
+ // span.end()
275
+ // }
276
+ // },
277
+ // ),
278
+ // { label: queryLabel },
279
+ // otelContext,
280
+ // )
281
+
282
+ // const query = new LiveStoreSQLQuery<TResult>({
283
+ // label,
284
+ // queryString$,
285
+ // results$,
286
+ // componentKey: componentKey ?? globalComponentKey,
287
+ // store: this,
288
+ // otelContext,
289
+ // })
290
+
291
+ // this.activeQueries.add(query)
292
+
293
+ // // TODO get rid of temporary query workaround
294
+ // if (this.temporaryQueries !== undefined) {
295
+ // this.temporaryQueries.add(query)
296
+ // }
297
+
298
+ // // NOTE we are not ending the span here but in the query `destroy` method
299
+ // return query
300
+ // },
301
+ // )
302
+
303
+ // queryJS = <TResult>(
304
+ // genResults: (get: GetAtomResult) => TResult,
305
+ // {
306
+ // componentKey = globalComponentKey,
307
+ // label = `js${uniqueId()}`,
308
+ // otelContext = otel.context.active(),
309
+ // }: { componentKey?: ComponentKey; label?: string; otelContext?: otel.Context },
310
+ // ): LiveStoreJSQuery<TResult> =>
311
+ // this.otel.tracer.startActiveSpan(`queryJS:${label}`, { attributes: { label } }, otelContext, (span) => {
312
+ // const otelContext = otel.trace.setSpan(otel.context.active(), span)
313
+ // const queryLabel = `${label}:results` + (this.temporaryQueries ? ':temp' : '')
314
+ // const results$ = this.graph.makeThunk(
315
+ // (get, addDebugInfo) => {
316
+ // addDebugInfo({ _tag: 'js', label, query: genResults.toString() })
317
+ // return genResults(makeGetAtomResult(get))
318
+ // },
319
+ // { label: queryLabel, meta: { liveStoreThunkType: 'jsResults' } },
320
+ // otelContext,
321
+ // )
322
+
323
+ // // const query = new LiveStoreJSQuery<TResult>({
324
+ // // label,
325
+ // // results$,
326
+ // // componentKey,
327
+ // // store: this,
328
+ // // otelContext,
329
+ // // })
330
+
331
+ // this.activeQueries.add(query)
332
+
333
+ // // TODO get rid of temporary query workaround
334
+ // if (this.temporaryQueries !== undefined) {
335
+ // this.temporaryQueries.add(query)
336
+ // }
337
+
338
+ // // NOTE we are not ending the span here but in the query `destroy` method
339
+ // return query
340
+ // })
341
+
342
+ // queryGraphQL = <TResult extends Record<string, any>, TVariableValues extends Record<string, any>>(
343
+ // document: DocumentNode<TResult, TVariableValues>,
344
+ // genVariableValues: TVariableValues | ((get: GetAtomResult) => TVariableValues),
345
+ // {
346
+ // componentKey,
347
+ // label,
348
+ // otelContext = otel.context.active(),
349
+ // }: {
350
+ // componentKey: ComponentKey
351
+ // label?: string
352
+ // otelContext?: otel.Context
353
+ // },
354
+ // ): LiveStoreGraphQLQuery<TResult, TVariableValues, TGraphQLContext> =>
355
+ // this.otel.tracer.startActiveSpan(
356
+ // `queryGraphQL:`, // NOTE span name will be overridden further down
357
+ // {},
358
+ // otelContext,
359
+ // (span) => {
360
+ // const otelContext = otel.trace.setSpan(otel.context.active(), span)
361
+
362
+ // if (this.graphQLContext === undefined) {
363
+ // return shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL context")
364
+ // }
365
+
366
+ // const labelWithDefault = label ?? graphql.getOperationAST(document)?.name?.value ?? 'graphql'
367
+
368
+ // span.updateName(`queryGraphQL:${labelWithDefault}`)
369
+
370
+ // const variableValues$ = this.graph.makeThunk(
371
+ // (get) => {
372
+ // if (typeof genVariableValues === 'function') {
373
+ // return genVariableValues(makeGetAtomResult(get))
374
+ // } else {
375
+ // return genVariableValues
376
+ // }
377
+ // },
378
+ // { label: `${labelWithDefault}:variableValues`, meta: { liveStoreThunkType: 'graphqlVariableValues' } },
379
+ // // otelContext,
380
+ // )
381
+
382
+ // const resultsLabel = `${labelWithDefault}:results` + (this.temporaryQueries ? ':temp' : '')
383
+ // const results$ = this.graph.makeThunk<TResult>(
384
+ // (get, addDebugInfo) => {
385
+ // const variableValues = get(variableValues$)
386
+ // const { result, queriedTables } = this.queryGraphQLOnce(document, variableValues, otelContext)
387
+
388
+ // // Add dependencies on any tables that were used
389
+ // for (const tableName of queriedTables) {
390
+ // const tableRef = this.tableRefs[tableName]
391
+ // assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
392
+ // get(tableRef!)
393
+ // }
394
+
395
+ // addDebugInfo({ _tag: 'graphql', label: resultsLabel, query: graphql.print(document) })
396
+
397
+ // return result
398
+ // },
399
+ // { label: resultsLabel, meta: { liveStoreThunkType: 'graphqlResults' } },
400
+ // // otelContext,
401
+ // )
402
+
403
+ // const query = new LiveStoreGraphQLQuery({
404
+ // document,
405
+ // context: this.graphQLContext,
406
+ // results$,
407
+ // componentKey,
408
+ // label: labelWithDefault,
409
+ // store: this,
410
+ // otelContext,
411
+ // })
412
+
413
+ // this.activeQueries.add(query)
414
+
415
+ // // TODO get rid of temporary query workaround
416
+ // if (this.temporaryQueries !== undefined) {
417
+ // this.temporaryQueries.add(query)
418
+ // }
419
+
420
+ // // NOTE we are not ending the span here but in the query `destroy` method
421
+ // return query
422
+ // },
423
+ // )
424
+
425
+ // queryGraphQLOnce = <TResult extends Record<string, any>, TVariableValues extends Record<string, any>>(
426
+ // document: DocumentNode<TResult, TVariableValues>,
427
+ // variableValues: TVariableValues,
428
+ // otelContext: otel.Context = this.otel.queriesSpanContext,
429
+ // ): { result: TResult; queriedTables: string[] } => {
430
+ // const schema =
431
+ // this.graphQLSchema ?? shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL schema")
432
+ // const context =
433
+ // this.graphQLContext ?? shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL context")
434
+ // const tracer = this.otel.tracer
435
+
436
+ // const operationName = graphql.getOperationAST(document)?.name?.value
437
+
438
+ // return tracer.startActiveSpan(`executeGraphQLQuery: ${operationName}`, {}, otelContext, (span) => {
439
+ // try {
440
+ // span.setAttribute('graphql.variables', JSON.stringify(variableValues))
441
+ // span.setAttribute('graphql.query', graphql.print(document))
442
+
443
+ // context.queriedTables.clear()
444
+
445
+ // context.otelContext = otel.trace.setSpan(otel.context.active(), span)
446
+
447
+ // const res = graphql.executeSync({
448
+ // document,
449
+ // contextValue: context,
450
+ // schema: schema,
451
+ // variableValues,
452
+ // })
453
+
454
+ // // TODO track number of nested SQL queries via Otel + debug info
455
+
456
+ // if (res.errors) {
457
+ // span.setStatus({ code: otel.SpanStatusCode.ERROR, message: 'GraphQL error' })
458
+ // span.setAttribute('graphql.error', res.errors.join('\n'))
459
+ // span.setAttribute('graphql.error-detail', JSON.stringify(res.errors))
460
+ // console.error(`graphql error (${operationName})`, res.errors)
461
+ // }
462
+
463
+ // return { result: res.data as unknown as TResult, queriedTables: Array.from(context.queriedTables.values()) }
464
+ // } finally {
465
+ // span.end()
466
+ // }
467
+ // })
468
+ // }
484
469
 
485
470
  /**
486
471
  * Subscribe to the results of a query
487
472
  * Returns a function to cancel the subscription.
488
473
  */
489
- subscribe = <TQuery extends LiveStoreQuery>(
490
- query: TQuery,
491
- onNewValue: (value: QueryResult<TQuery>) => void,
474
+ subscribe = <TResult>(
475
+ query: ILiveStoreQuery<TResult>,
476
+ onNewValue: (value: TResult) => void,
492
477
  onSubsubscribe?: () => void,
493
- options?: { label?: string } | undefined,
478
+ options?: { label?: string; otelContext?: otel.Context } | undefined,
494
479
  ): (() => void) =>
495
480
  this.otel.tracer.startActiveSpan(
496
481
  `LiveStore.subscribe`,
497
482
  { attributes: { label: options?.label } },
498
- query.otelContext,
483
+ options?.otelContext ?? this.otel.queriesSpanContext,
499
484
  (span) => {
500
485
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
501
486
 
502
- const effect = this.graph.makeEffect(
503
- (get) => {
504
- const result = get(query.results$) as QueryResult<TQuery>
505
- onNewValue(result)
506
- },
507
- { label: `subscribe:${options?.label}` },
508
- otelContext,
509
- )
487
+ const label = `subscribe:${options?.label}`
488
+ const effect = this.graph.makeEffect((get) => onNewValue(get(query.results$)), { label })
510
489
 
511
- const subscriptionKey = uuid()
490
+ effect.doEffect(otelContext)
512
491
 
513
492
  const unsubscribe = () => {
514
493
  try {
515
494
  this.graph.destroy(effect)
516
- query.activeSubscriptions.delete(subscriptionKey)
495
+ this.activeQueries.delete(query as LiveStoreQuery)
517
496
  onSubsubscribe?.()
518
497
  } finally {
519
498
  span.end()
520
499
  }
521
500
  }
522
501
 
523
- query.activeSubscriptions.set(subscriptionKey, unsubscribe)
502
+ this.activeQueries.add(query as LiveStoreQuery)
524
503
 
525
504
  return unsubscribe
526
505
  },
527
506
  )
528
507
 
529
- /**
530
- * Any queries created in the callback will be destroyed when the callback is complete.
531
- * Useful for temporarily creating reactive queries, which is an idempotent operation
532
- * that can be safely called inside a React useMemo hook.
533
- */
534
- inTempQueryContext = <TResult>(callback: () => TResult): TResult => {
535
- this.temporaryQueries = new Set()
536
- // TODO: consider errors / try/finally here?
537
- const result = callback()
538
- for (const query of this.temporaryQueries) {
539
- this.destroyQuery(query)
540
- }
541
- this.temporaryQueries = undefined
542
- return result
543
- }
544
-
545
508
  /**
546
509
  * Destroys the entire store, including all queries and subscriptions.
547
510
  *
548
511
  * Currently only used when shutting down the app for debugging purposes (e.g. to close Otel spans).
549
512
  */
550
513
  destroy = () => {
551
- for (const query of this.activeQueries) {
552
- this.destroyQuery(query)
553
- }
554
-
555
514
  Object.values(this.tableRefs).forEach((tableRef) => this.graph.destroy(tableRef))
556
515
 
557
- const applyEventsSpan = otel.trace.getSpan(this.otel.applyEventsSpanContext)!
558
- applyEventsSpan.end()
559
-
560
- const queriesSpan = otel.trace.getSpan(this.otel.queriesSpanContext)!
561
- queriesSpan.end()
516
+ otel.trace.getSpan(this.otel.applyEventsSpanContext)!.end()
517
+ otel.trace.getSpan(this.otel.queriesSpanContext)!.end()
562
518
 
563
519
  // TODO destroy active subscriptions
564
520
  }
565
521
 
566
- private destroyQuery = (query: LiveStoreQuery) => {
567
- if (query._tag === 'sql') {
568
- // results are downstream of query string, so will automatically be destroyed together
569
- this.graph.destroy(query.queryString$)
570
- } else {
571
- this.graph.destroy(query.results$)
572
- }
573
- this.activeQueries.delete(query)
574
- query.destroy()
575
- }
576
-
577
- /**
578
- * Clean up queries and downstream subscriptions associated with a component.
579
- * This is critical to avoid memory leaks.
580
- */
581
- unmountComponent = (componentKey: ComponentKey) => {
582
- for (const query of this.activeQueries) {
583
- if (query.componentKey === componentKey) {
584
- this.destroyQuery(query)
585
- }
586
- }
587
- }
588
-
589
522
  /* Apply a single write event to the store, and refresh all queries in response */
590
523
  applyEvent = <TEventType extends string & keyof LiveStoreActionDefinitionsTypes>(
591
524
  eventType: TEventType,
@@ -614,20 +547,25 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
614
547
  tablesToUpdate.push([tableRef!, null])
615
548
  }
616
549
 
550
+ const debugRefreshReason = {
551
+ _tag: 'applyEvent' as const,
552
+ event: { type: eventType, args },
553
+ writeTables: [...writeTables],
554
+ }
555
+
617
556
  // Update all table refs together in a batch, to only trigger one reactive update
618
- this.graph.setRefs(
619
- tablesToUpdate,
620
- {
621
- otelHint: 'applyEvents',
622
- skipRefresh,
623
- debugRefreshReason: {
624
- _tag: 'applyEvent',
625
- event: { type: eventType, args },
626
- writeTables: [...writeTables],
627
- },
628
- },
629
- otelContext,
630
- )
557
+ this.graph.setRefs(tablesToUpdate, { debugRefreshReason, otelContext })
558
+
559
+ if (skipRefresh === false) {
560
+ // TODO update the graph
561
+ // this.graph.refresh(
562
+ // {
563
+ // otelHint: 'applyEvents',
564
+ // debugRefreshReason,
565
+ // },
566
+ // otelContext,
567
+ // )
568
+ }
631
569
  } catch (e: any) {
632
570
  span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() })
633
571
 
@@ -710,20 +648,18 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
710
648
  tablesToUpdate.push([tableRef!, null])
711
649
  }
712
650
 
651
+ const debugRefreshReason = {
652
+ _tag: 'applyEvents' as const,
653
+ events: [...events].map((e) => ({ type: e.eventType, args: e.args })),
654
+ writeTables: [...writeTables],
655
+ }
713
656
  // Update all table refs together in a batch, to only trigger one reactive update
714
- this.graph.setRefs(
715
- tablesToUpdate,
716
- {
717
- otelHint: 'applyEvents',
718
- skipRefresh,
719
- debugRefreshReason: {
720
- _tag: 'applyEvents',
721
- events: [...events].map((e) => ({ type: e.eventType, args: e.args })),
722
- writeTables: [...writeTables],
723
- },
724
- },
725
- otelContext,
726
- )
657
+ this.graph.setRefs(tablesToUpdate, { debugRefreshReason, otelContext })
658
+
659
+ if (skipRefresh === false) {
660
+ // TODO update the graph
661
+ // this.graph.refresh({ debugRefreshReason, otelHint: 'applyEvents' }, otelContext)
662
+ }
727
663
  } catch (e: any) {
728
664
  span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() })
729
665
  } finally {
@@ -746,20 +682,14 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
746
682
  { attributes: { 'livestore.manualRefreshLabel': label } },
747
683
  this.otel.applyEventsSpanContext,
748
684
  (span) => {
749
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
750
- this.graph.refresh({ otelHint: 'manualRefresh', debugRefreshReason: { _tag: 'manualRefresh' } }, otelContext)
685
+ // const otelContext = otel.trace.setSpan(otel.context.active(), span)
686
+ // TODO update the graph
687
+ // this.graph.refresh({ otelHint: 'manualRefresh', debugRefreshReason: { _tag: 'manualRefresh' } }, otelContext)
751
688
  span.end()
752
689
  },
753
690
  )
754
691
  }
755
692
 
756
- // TODO get rid of this as part of new query definition approach https://www.notion.so/schickling/New-query-definition-approach-1097a78ef0e9495bac25f90417374756?pvs=4
757
- runOnce = <TQueryDef extends QueryDefinition>(queryDef: TQueryDef): QueryResult<ReturnType<TQueryDef>> => {
758
- return this.inTempQueryContext(() => {
759
- return queryDef(this).results$.result
760
- })
761
- }
762
-
763
693
  /**
764
694
  * Apply an event to the store.
765
695
  * Returns the tables that were affected by the event.
@@ -853,7 +783,7 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
853
783
  * This should only be used for framework-internal purposes;
854
784
  * all app writes should go through applyEvent.
855
785
  */
856
- execute = async (query: string, params: ParamsObject = {}, writeTables?: string[]) => {
786
+ execute = (query: string, params: ParamsObject = {}, writeTables?: string[]) => {
857
787
  this.inMemoryDB.execute(query, prepareBindValues(params, query), writeTables)
858
788
 
859
789
  if (this.storage !== undefined) {