@livestore/livestore 0.3.0-dev.10 → 0.3.0-dev.11

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 (114) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/SqliteDbWrapper.d.ts +54 -0
  3. package/dist/SqliteDbWrapper.d.ts.map +1 -0
  4. package/dist/SqliteDbWrapper.js +212 -0
  5. package/dist/SqliteDbWrapper.js.map +1 -0
  6. package/dist/SynchronousDatabaseWrapper.d.ts +14 -5
  7. package/dist/SynchronousDatabaseWrapper.d.ts.map +1 -1
  8. package/dist/SynchronousDatabaseWrapper.js +24 -4
  9. package/dist/SynchronousDatabaseWrapper.js.map +1 -1
  10. package/dist/effect/LiveStore.d.ts +12 -8
  11. package/dist/effect/LiveStore.d.ts.map +1 -1
  12. package/dist/effect/LiveStore.js +9 -2
  13. package/dist/effect/LiveStore.js.map +1 -1
  14. package/dist/index.d.ts +6 -7
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +4 -4
  17. package/dist/index.js.map +1 -1
  18. package/dist/live-queries/base-class.d.ts +64 -21
  19. package/dist/live-queries/base-class.d.ts.map +1 -1
  20. package/dist/live-queries/base-class.js +56 -13
  21. package/dist/live-queries/base-class.js.map +1 -1
  22. package/dist/live-queries/computed.d.ts +7 -7
  23. package/dist/live-queries/computed.d.ts.map +1 -1
  24. package/dist/live-queries/computed.js +35 -11
  25. package/dist/live-queries/computed.js.map +1 -1
  26. package/dist/live-queries/db-query.d.ts +67 -0
  27. package/dist/live-queries/db-query.d.ts.map +1 -0
  28. package/dist/live-queries/db-query.js +244 -0
  29. package/dist/live-queries/db-query.js.map +1 -0
  30. package/dist/live-queries/db-query.test.d.ts +2 -0
  31. package/dist/live-queries/db-query.test.d.ts.map +1 -0
  32. package/dist/live-queries/db-query.test.js +123 -0
  33. package/dist/live-queries/db-query.test.js.map +1 -0
  34. package/dist/live-queries/db.d.ts +12 -15
  35. package/dist/live-queries/db.d.ts.map +1 -1
  36. package/dist/live-queries/db.js +44 -25
  37. package/dist/live-queries/db.js.map +1 -1
  38. package/dist/live-queries/db.test.js +16 -14
  39. package/dist/live-queries/db.test.js.map +1 -1
  40. package/dist/live-queries/graphql.d.ts +8 -8
  41. package/dist/live-queries/graphql.d.ts.map +1 -1
  42. package/dist/live-queries/graphql.js +35 -9
  43. package/dist/live-queries/graphql.js.map +1 -1
  44. package/dist/live-queries/make-ref.d.ts +20 -0
  45. package/dist/live-queries/make-ref.d.ts.map +1 -0
  46. package/dist/live-queries/make-ref.js +33 -0
  47. package/dist/live-queries/make-ref.js.map +1 -0
  48. package/dist/reactive.d.ts +15 -13
  49. package/dist/reactive.d.ts.map +1 -1
  50. package/dist/reactive.js +15 -9
  51. package/dist/reactive.js.map +1 -1
  52. package/dist/row-query-utils.d.ts +4 -4
  53. package/dist/row-query-utils.d.ts.map +1 -1
  54. package/dist/row-query-utils.js +14 -10
  55. package/dist/row-query-utils.js.map +1 -1
  56. package/dist/store/create-store.d.ts +3 -4
  57. package/dist/store/create-store.d.ts.map +1 -1
  58. package/dist/store/create-store.js +7 -7
  59. package/dist/store/create-store.js.map +1 -1
  60. package/dist/store/devtools.d.ts +2 -2
  61. package/dist/store/devtools.d.ts.map +1 -1
  62. package/dist/store/devtools.js +15 -15
  63. package/dist/store/devtools.js.map +1 -1
  64. package/dist/store/store-types.d.ts +9 -4
  65. package/dist/store/store-types.d.ts.map +1 -1
  66. package/dist/store/store-types.js.map +1 -1
  67. package/dist/store/store.d.ts +34 -16
  68. package/dist/store/store.d.ts.map +1 -1
  69. package/dist/store/store.js +125 -75
  70. package/dist/store/store.js.map +1 -1
  71. package/dist/utils/expo.d.ts +2 -0
  72. package/dist/utils/expo.d.ts.map +1 -0
  73. package/dist/utils/expo.js +8 -0
  74. package/dist/utils/expo.js.map +1 -0
  75. package/dist/utils/function-string.d.ts +7 -0
  76. package/dist/utils/function-string.d.ts.map +1 -0
  77. package/dist/utils/function-string.js +9 -0
  78. package/dist/utils/function-string.js.map +1 -0
  79. package/dist/utils/stack-info.d.ts.map +1 -1
  80. package/dist/utils/stack-info.js +6 -1
  81. package/dist/utils/stack-info.js.map +1 -1
  82. package/dist/utils/stack-info.test.js +54 -1
  83. package/dist/utils/stack-info.test.js.map +1 -1
  84. package/dist/utils/tests/fixture.d.ts +2 -6
  85. package/dist/utils/tests/fixture.d.ts.map +1 -1
  86. package/dist/utils/tests/fixture.js +3 -5
  87. package/dist/utils/tests/fixture.js.map +1 -1
  88. package/dist/utils/tests/mod.d.ts +1 -0
  89. package/dist/utils/tests/mod.d.ts.map +1 -1
  90. package/dist/utils/tests/mod.js +1 -0
  91. package/dist/utils/tests/mod.js.map +1 -1
  92. package/package.json +5 -5
  93. package/src/{SynchronousDatabaseWrapper.ts → SqliteDbWrapper.ts} +41 -11
  94. package/src/effect/LiveStore.ts +22 -14
  95. package/src/index.ts +14 -7
  96. package/src/live-queries/__snapshots__/{db.test.ts.snap → db-query.test.ts.snap} +196 -42
  97. package/src/live-queries/base-class.ts +160 -40
  98. package/src/live-queries/computed.ts +45 -19
  99. package/src/live-queries/{db.test.ts → db-query.test.ts} +21 -11
  100. package/src/live-queries/{db.ts → db-query.ts} +97 -39
  101. package/src/live-queries/graphql.ts +47 -21
  102. package/src/live-queries/make-ref.ts +47 -0
  103. package/src/reactive.ts +52 -27
  104. package/src/row-query-utils.ts +29 -18
  105. package/src/store/create-store.ts +20 -23
  106. package/src/store/devtools.ts +17 -17
  107. package/src/store/store-types.ts +6 -4
  108. package/src/store/store.ts +227 -120
  109. package/src/utils/function-string.ts +12 -0
  110. package/src/utils/stack-info.test.ts +58 -1
  111. package/src/utils/stack-info.ts +6 -1
  112. package/src/utils/tests/fixture.ts +2 -7
  113. package/src/utils/tests/mod.ts +1 -0
  114. package/src/global-state.ts +0 -20
@@ -1,9 +1,10 @@
1
1
  import { Effect, Schema } from '@livestore/utils/effect'
2
2
  import * as otel from '@opentelemetry/api'
3
3
  import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
4
- import { describe, expect, it } from 'vitest'
4
+ import { beforeEach, describe, expect, it } from 'vitest'
5
5
 
6
6
  import { computed, queryDb, rawSqlMutation, sql } from '../index.js'
7
+ import * as RG from '../reactive.js'
7
8
  import { makeTodoMvc, tables } from '../utils/tests/fixture.js'
8
9
  import { getSimplifiedRootSpan } from '../utils/tests/otel.js'
9
10
 
@@ -17,6 +18,10 @@ TODO write tests for:
17
18
  describe('otel', () => {
18
19
  let cachedProvider: BasicTracerProvider | undefined
19
20
 
21
+ beforeEach(() => {
22
+ RG.__resetIds()
23
+ })
24
+
20
25
  const makeQuery = Effect.gen(function* () {
21
26
  const exporter = new InMemorySpanExporter()
22
27
 
@@ -31,7 +36,7 @@ describe('otel', () => {
31
36
  const span = otelTracer.startSpan('test-root')
32
37
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
33
38
 
34
- const { store } = yield* makeTodoMvc({ otelTracer, otelContext })
39
+ const store = yield* makeTodoMvc({ otelTracer, otelContext })
35
40
 
36
41
  return {
37
42
  store,
@@ -51,11 +56,11 @@ describe('otel', () => {
51
56
  schema: Schema.Array(tables.todos.schema),
52
57
  queriedTables: new Set(['todos']),
53
58
  })
54
- expect(query$.run()).toMatchInlineSnapshot('[]')
59
+ expect(store.query(query$)).toMatchInlineSnapshot('[]')
55
60
 
56
61
  store.mutate(rawSqlMutation({ sql: sql`INSERT INTO todos (id, text, completed) VALUES ('t1', 'buy milk', 0)` }))
57
62
 
58
- expect(query$.run()).toMatchInlineSnapshot(`
63
+ expect(store.query(query$)).toMatchInlineSnapshot(`
59
64
  [
60
65
  {
61
66
  "completed": false,
@@ -65,7 +70,6 @@ describe('otel', () => {
65
70
  ]
66
71
  `)
67
72
 
68
- query$.destroy()
69
73
  span.end()
70
74
 
71
75
  return { exporter }
@@ -89,7 +93,9 @@ describe('otel', () => {
89
93
  { label: 'all todos' },
90
94
  )
91
95
 
92
- expect(query$.run()).toMatchInlineSnapshot(`
96
+ expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
97
+
98
+ expect(store.query(query$)).toMatchInlineSnapshot(`
93
99
  {
94
100
  "completed": false,
95
101
  "id": "",
@@ -97,9 +103,13 @@ describe('otel', () => {
97
103
  }
98
104
  `)
99
105
 
106
+ expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
107
+
100
108
  store.mutate(rawSqlMutation({ sql: sql`INSERT INTO todos (id, text, completed) VALUES ('t1', 'buy milk', 0)` }))
101
109
 
102
- expect(query$.run()).toMatchInlineSnapshot(`
110
+ expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
111
+
112
+ expect(store.query(query$)).toMatchInlineSnapshot(`
103
113
  {
104
114
  "completed": false,
105
115
  "id": "t1",
@@ -107,7 +117,8 @@ describe('otel', () => {
107
117
  }
108
118
  `)
109
119
 
110
- query$.destroy()
120
+ expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
121
+
111
122
  span.end()
112
123
 
113
124
  return { exporter }
@@ -125,7 +136,7 @@ describe('otel', () => {
125
136
  const filter = computed(() => ({ completed: false }))
126
137
  const query$ = queryDb((get) => tables.todos.query.where(get(filter)).first({ fallback: () => defaultTodo }))
127
138
 
128
- expect(query$.run()).toMatchInlineSnapshot(`
139
+ expect(store.query(query$)).toMatchInlineSnapshot(`
129
140
  {
130
141
  "completed": false,
131
142
  "id": "",
@@ -135,7 +146,7 @@ describe('otel', () => {
135
146
 
136
147
  store.mutate(rawSqlMutation({ sql: sql`INSERT INTO todos (id, text, completed) VALUES ('t1', 'buy milk', 0)` }))
137
148
 
138
- expect(query$.run()).toMatchInlineSnapshot(`
149
+ expect(store.query(query$)).toMatchInlineSnapshot(`
139
150
  {
140
151
  "completed": false,
141
152
  "id": "t1",
@@ -143,7 +154,6 @@ describe('otel', () => {
143
154
  }
144
155
  `)
145
156
 
146
- query$.destroy()
147
157
  span.end()
148
158
 
149
159
  return { exporter }
@@ -11,14 +11,14 @@ import { deepEqual, shouldNeverHappen } from '@livestore/utils'
11
11
  import { Predicate, Schema, TreeFormatter } from '@livestore/utils/effect'
12
12
  import * as otel from '@opentelemetry/api'
13
13
 
14
- import { globalReactivityGraph } from '../global-state.js'
15
14
  import type { Thunk } from '../reactive.js'
16
15
  import { isThunk, NOT_REFRESHED_YET } from '../reactive.js'
17
16
  import { makeExecBeforeFirstRun, rowQueryLabel } from '../row-query-utils.js'
18
17
  import type { RefreshReason } from '../store/store-types.js'
18
+ import { isValidFunctionString } from '../utils/function-string.js'
19
19
  import { getDurationMsFromSpan } from '../utils/otel.js'
20
- import type { GetAtomResult, LiveQuery, QueryContext, ReactivityGraph } from './base-class.js'
21
- import { LiveStoreQueryBase, makeGetAtomResult } from './base-class.js'
20
+ import type { DepKey, GetAtomResult, LiveQueryDef, ReactivityGraph, ReactivityGraphContext } from './base-class.js'
21
+ import { defCounterRef, depsToString, LiveStoreQueryBase, makeGetAtomResult, withRCMap } from './base-class.js'
22
22
 
23
23
  export type QueryInputRaw<TDecoded, TEncoded, TQueryInfo extends QueryInfo> = {
24
24
  query: string
@@ -31,9 +31,12 @@ export type QueryInputRaw<TDecoded, TEncoded, TQueryInfo extends QueryInfo> = {
31
31
  */
32
32
  queriedTables?: Set<string>
33
33
  queryInfo?: TQueryInfo
34
- execBeforeFirstRun?: (ctx: QueryContext) => void
34
+ execBeforeFirstRun?: (ctx: ReactivityGraphContext) => void
35
35
  }
36
36
 
37
+ export const isQueryInputRaw = (value: unknown): value is QueryInputRaw<any, any, any> =>
38
+ Predicate.hasProperty(value, 'query') && Predicate.hasProperty(value, 'schema')
39
+
37
40
  export type QueryInput<TDecoded, TEncoded, TQueryInfo extends QueryInfo> =
38
41
  | QueryInputRaw<TDecoded, TEncoded, TQueryInfo>
39
42
  | QueryBuilder<TDecoded, any, any, TQueryInfo>
@@ -52,10 +55,10 @@ export const queryDb: {
52
55
  * Used for debugging / devtools
53
56
  */
54
57
  label?: string
55
- reactivityGraph?: ReactivityGraph
56
- otelContext?: otel.Context
58
+ deps?: DepKey
59
+ queryInfo?: TQueryInfo
57
60
  },
58
- ): LiveQuery<TResult, TQueryInfo>
61
+ ): LiveQueryDef<TResult, TQueryInfo>
59
62
  // NOTE in this "thunk case", we can't directly derive label/queryInfo from the queryInput,
60
63
  // so the caller needs to provide them explicitly otherwise queryInfo will be set to `None`,
61
64
  // and label will be set during the query execution
@@ -69,20 +72,47 @@ export const queryDb: {
69
72
  * Used for debugging / devtools
70
73
  */
71
74
  label?: string
72
- reactivityGraph?: ReactivityGraph
75
+ deps?: DepKey
73
76
  queryInfo?: TQueryInfo
74
- otelContext?: otel.Context
75
77
  },
76
- ): LiveQuery<TResult, TQueryInfo>
77
- } = (queryInput, options) =>
78
- new LiveStoreDbQuery({
79
- queryInput,
80
- label: options?.label,
81
- reactivityGraph: options?.reactivityGraph,
82
- map: options?.map,
83
- queryInfo: Predicate.hasProperty(options, 'queryInfo') ? (options.queryInfo as QueryInfo) : undefined,
84
- otelContext: options?.otelContext,
85
- })
78
+ ): LiveQueryDef<TResult, TQueryInfo>
79
+ } = (queryInput, options) => {
80
+ const queryString = isQueryBuilder(queryInput)
81
+ ? queryInput.toString()
82
+ : isQueryInputRaw(queryInput)
83
+ ? queryInput.query
84
+ : typeof queryInput === 'function'
85
+ ? queryInput.toString()
86
+ : shouldNeverHappen(`Invalid query input: ${queryInput}`)
87
+
88
+ const hash = options?.deps ? queryString + '-' + depsToString(options.deps) : queryString
89
+ if (isValidFunctionString(hash)._tag === 'invalid') {
90
+ throw new Error(`On Expo/React Native, db queries must provide a \`deps\` option`)
91
+ }
92
+
93
+ const label = options?.label ?? queryString
94
+
95
+ return {
96
+ _tag: 'def',
97
+ id: ++defCounterRef.current,
98
+ make: withRCMap(hash, (ctx, otelContext) => {
99
+ // TODO onDestroy
100
+ return new LiveStoreDbQuery({
101
+ reactivityGraph: ctx.reactivityGraph.deref()!,
102
+ queryInput,
103
+ label,
104
+ map: options?.map,
105
+ // We're not falling back to `None` here as the queryInfo will be set dynamically
106
+ queryInfo: options?.queryInfo,
107
+ otelContext,
108
+ })
109
+ }),
110
+ label,
111
+ hash,
112
+ queryInfo:
113
+ options?.queryInfo ?? (isQueryBuilder(queryInput) ? queryInfoFromQueryBuilder(queryInput) : { _tag: 'None' }),
114
+ }
115
+ }
86
116
 
87
117
  /* An object encapsulating a reactive SQL query */
88
118
  export class LiveStoreDbQuery<
@@ -93,16 +123,16 @@ export class LiveStoreDbQuery<
93
123
  _tag: 'db' = 'db'
94
124
 
95
125
  /** A reactive thunk representing the query text */
96
- queryInput$: Thunk<QueryInput<TResultSchema, ReadonlyArray<any>, TQueryInfo>, QueryContext, RefreshReason> | undefined
126
+ queryInput$: Thunk<QueryInputRaw<any, any, QueryInfo>, ReactivityGraphContext, RefreshReason> | undefined
97
127
 
98
128
  /** A reactive thunk representing the query results */
99
- results$: Thunk<TResult, QueryContext, RefreshReason>
129
+ results$: Thunk<TResult, ReactivityGraphContext, RefreshReason>
100
130
 
101
131
  label: string
102
132
 
103
133
  queryInfo: TQueryInfo
104
134
 
105
- protected reactivityGraph
135
+ readonly reactivityGraph
106
136
 
107
137
  private mapResult: (rows: TResultSchema) => TResult
108
138
 
@@ -117,17 +147,18 @@ export class LiveStoreDbQuery<
117
147
  label?: string
118
148
  queryInput:
119
149
  | QueryInput<TResultSchema, ReadonlyArray<any>, TQueryInfo>
120
- | ((get: GetAtomResult, ctx: QueryContext) => QueryInput<TResultSchema, ReadonlyArray<any>, TQueryInfo>)
121
- reactivityGraph?: ReactivityGraph
150
+ | ((get: GetAtomResult, ctx: ReactivityGraphContext) => QueryInput<TResultSchema, ReadonlyArray<any>, TQueryInfo>)
151
+ reactivityGraph: ReactivityGraph
122
152
  map?: (rows: TResultSchema) => TResult
123
153
  queryInfo?: TQueryInfo
154
+ /** Only used for the initial query execution */
124
155
  otelContext?: otel.Context
125
156
  }) {
126
157
  super()
127
158
 
128
159
  let label = inputLabel ?? 'db(unknown)'
129
160
  let queryInfo = inputQueryInfo ?? ({ _tag: 'None' } as TQueryInfo)
130
- this.reactivityGraph = reactivityGraph ?? globalReactivityGraph
161
+ this.reactivityGraph = reactivityGraph
131
162
 
132
163
  this.mapResult = map === undefined ? (rows: any) => rows as TResult : map
133
164
 
@@ -136,13 +167,15 @@ export class LiveStoreDbQuery<
136
167
  typeof queryInput === 'function' ? undefined : isQueryBuilder(queryInput) ? undefined : queryInput.schema,
137
168
  }
138
169
 
139
- const execBeforeFirstRunRef: { current: ((ctx: QueryContext, otelContext: otel.Context) => void) | undefined } = {
170
+ const execBeforeFirstRunRef: {
171
+ current: ((ctx: ReactivityGraphContext, otelContext: otel.Context) => void) | undefined
172
+ } = {
140
173
  current: undefined,
141
174
  }
142
175
 
143
176
  type TQueryInputRaw = QueryInputRaw<any, any, QueryInfo>
144
177
 
145
- let queryInputRaw$OrQueryInputRaw: TQueryInputRaw | Thunk<TQueryInputRaw, QueryContext, RefreshReason>
178
+ let queryInputRaw$OrQueryInputRaw: TQueryInputRaw | Thunk<TQueryInputRaw, ReactivityGraphContext, RefreshReason>
146
179
 
147
180
  const fromQueryBuilder = (qb: QueryBuilder.Any, otelContext: otel.Context | undefined) => {
148
181
  try {
@@ -156,7 +189,7 @@ export class LiveStoreDbQuery<
156
189
  schema,
157
190
  bindValues: qbRes.bindValues,
158
191
  queriedTables: new Set([ast.tableDef.sqliteDef.name]),
159
- queryInfo: ast._tag === 'RowQuery' ? { _tag: 'Row', table: ast.tableDef, id: ast.id } : { _tag: 'None' },
192
+ queryInfo: queryInfoFromQueryBuilder(qb),
160
193
  } satisfies TQueryInputRaw,
161
194
  label: ast._tag === 'RowQuery' ? rowQueryLabel(ast.tableDef, ast.id) : qb.toString(),
162
195
  execBeforeFirstRun:
@@ -178,7 +211,10 @@ export class LiveStoreDbQuery<
178
211
  queryInputRaw$OrQueryInputRaw = this.reactivityGraph.makeThunk(
179
212
  (get, setDebugInfo, ctx, otelContext) => {
180
213
  const startMs = performance.now()
181
- const queryInputResult = queryInput(makeGetAtomResult(get, otelContext ?? ctx.rootOtelContext), ctx)
214
+ const queryInputResult = queryInput(
215
+ makeGetAtomResult(get, ctx, otelContext ?? ctx.rootOtelContext, this.dependencyQueriesRef),
216
+ ctx,
217
+ )
182
218
  const durationMs = performance.now() - startMs
183
219
 
184
220
  let queryInputRaw: TQueryInputRaw
@@ -210,6 +246,8 @@ export class LiveStoreDbQuery<
210
246
  equal: (a, b) => a.query === b.query && deepEqual(a.bindValues, b.bindValues),
211
247
  },
212
248
  )
249
+
250
+ this.queryInput$ = queryInputRaw$OrQueryInputRaw
213
251
  } else {
214
252
  let queryInputRaw: TQueryInputRaw
215
253
  if (isQueryBuilder(queryInput)) {
@@ -253,10 +291,16 @@ export class LiveStoreDbQuery<
253
291
  : undefined
254
292
 
255
293
  const results$ = this.reactivityGraph.makeThunk<TResult>(
256
- (get, setDebugInfo, queryContext, otelContext) =>
294
+ (get, setDebugInfo, queryContext, otelContext, debugRefreshReason) =>
257
295
  queryContext.otelTracer.startActiveSpan(
258
296
  'db:...', // NOTE span name will be overridden further down
259
- {},
297
+ {
298
+ attributes: {
299
+ 'livestore.debugRefreshReason': Predicate.hasProperty(debugRefreshReason, 'label')
300
+ ? (debugRefreshReason.label as string)
301
+ : debugRefreshReason?._tag,
302
+ },
303
+ },
260
304
  otelContext ?? queryContext.rootOtelContext,
261
305
  (span) => {
262
306
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
@@ -268,14 +312,14 @@ export class LiveStoreDbQuery<
268
312
  }
269
313
 
270
314
  const queryInputResult = isThunk(queryInputRaw$OrQueryInputRaw)
271
- ? (get(queryInputRaw$OrQueryInputRaw, otelContext) as TQueryInputRaw)
315
+ ? (get(queryInputRaw$OrQueryInputRaw, otelContext, debugRefreshReason) as TQueryInputRaw)
272
316
  : (queryInputRaw$OrQueryInputRaw as TQueryInputRaw)
273
317
 
274
318
  const sqlString = queryInputResult.query
275
319
  const bindValues = queryInputResult.bindValues
276
320
 
277
321
  if (queriedTablesRef.current === undefined) {
278
- queriedTablesRef.current = store.syncDbWrapper.getTablesUsed(sqlString)
322
+ queriedTablesRef.current = store.sqliteDbWrapper.getTablesUsed(sqlString)
279
323
  }
280
324
 
281
325
  if (bindValues !== undefined) {
@@ -285,17 +329,20 @@ export class LiveStoreDbQuery<
285
329
  // Establish a reactive dependency on the tables used in the query
286
330
  for (const tableName of queriedTablesRef.current) {
287
331
  const tableRef = store.tableRefs[tableName] ?? shouldNeverHappen(`No table ref found for ${tableName}`)
288
- get(tableRef, otelContext)
332
+ get(tableRef, otelContext, debugRefreshReason)
289
333
  }
290
334
 
291
335
  span.setAttribute('sql.query', sqlString)
292
336
  span.updateName(`db:${sqlString.slice(0, 50)}`)
293
337
 
294
- const rawDbResults = store.syncDbWrapper.select<any>(sqlString, {
295
- queriedTables: queriedTablesRef.current,
296
- bindValues: bindValues ? prepareBindValues(bindValues, sqlString) : undefined,
297
- otelContext,
298
- })
338
+ const rawDbResults = store.sqliteDbWrapper.select<any>(
339
+ sqlString,
340
+ bindValues ? prepareBindValues(bindValues, sqlString) : undefined,
341
+ {
342
+ queriedTables: queriedTablesRef.current,
343
+ otelContext,
344
+ },
345
+ )
299
346
 
300
347
  span.setAttribute('sql.rowsCount', rawDbResults.length)
301
348
 
@@ -346,10 +393,21 @@ Result:`,
346
393
  }
347
394
 
348
395
  destroy = () => {
396
+ this.isDestroyed = true
397
+
349
398
  if (this.queryInput$ !== undefined) {
350
399
  this.reactivityGraph.destroyNode(this.queryInput$)
351
400
  }
352
401
 
353
402
  this.reactivityGraph.destroyNode(this.results$)
403
+
404
+ for (const query of this.dependencyQueriesRef) {
405
+ query.deref()
406
+ }
354
407
  }
355
408
  }
409
+
410
+ const queryInfoFromQueryBuilder = (qb: QueryBuilder.Any): QueryInfo.Row | QueryInfo.None => {
411
+ const ast = qb[QueryBuilderAstSymbol]
412
+ return ast._tag === 'RowQuery' ? { _tag: 'Row', table: ast.tableDef, id: ast.id } : { _tag: 'None' }
413
+ }
@@ -5,13 +5,12 @@ import { Schema, TreeFormatter } from '@livestore/utils/effect'
5
5
  import * as otel from '@opentelemetry/api'
6
6
  import * as graphql from 'graphql'
7
7
 
8
- import { globalReactivityGraph } from '../global-state.js'
9
8
  import { isThunk, type Thunk } from '../reactive.js'
10
9
  import type { Store } from '../store/store.js'
11
10
  import type { BaseGraphQLContext, RefreshReason } from '../store/store-types.js'
12
11
  import { getDurationMsFromSpan } from '../utils/otel.js'
13
- import type { GetAtomResult, LiveQuery, QueryContext, ReactivityGraph } from './base-class.js'
14
- import { LiveStoreQueryBase, makeGetAtomResult } from './base-class.js'
12
+ import type { DepKey, GetAtomResult, LiveQueryDef, ReactivityGraph, ReactivityGraphContext } from './base-class.js'
13
+ import { defCounterRef, depsToString, LiveStoreQueryBase, makeGetAtomResult, withRCMap } from './base-class.js'
15
14
 
16
15
  export type MapResult<To, From> = ((res: From, get: GetAtomResult) => To) | Schema.Schema<To, From>
17
16
 
@@ -22,17 +21,37 @@ export const queryGraphQL = <
22
21
  >(
23
22
  document: DocumentNode<TResult, TVariableValues>,
24
23
  genVariableValues: TVariableValues | ((get: GetAtomResult) => TVariableValues),
25
- {
26
- label,
27
- reactivityGraph,
28
- map,
29
- }: {
24
+ options: {
30
25
  label?: string
31
- reactivityGraph?: ReactivityGraph
26
+ // reactivityGraph?: ReactivityGraph
32
27
  map?: MapResult<TResultMapped, TResult>
28
+ deps?: DepKey
33
29
  } = {},
34
- ): LiveQuery<TResultMapped, QueryInfo.None> =>
35
- new LiveStoreGraphQLQuery({ document, genVariableValues, label, reactivityGraph, map })
30
+ ): LiveQueryDef<TResultMapped, QueryInfo.None> => {
31
+ const documentName = graphql.getOperationAST(document)?.name?.value
32
+ const hash = options.deps
33
+ ? depsToString(options.deps)
34
+ : (documentName ?? shouldNeverHappen('No document name found and no deps provided'))
35
+ const label = options.label ?? documentName ?? 'graphql'
36
+ const map = options.map
37
+
38
+ return {
39
+ _tag: 'def',
40
+ id: ++defCounterRef.current,
41
+ make: withRCMap(hash, (ctx, _otelContext) => {
42
+ return new LiveStoreGraphQLQuery({
43
+ document,
44
+ genVariableValues,
45
+ label,
46
+ map,
47
+ reactivityGraph: ctx.reactivityGraph.deref()!,
48
+ })
49
+ }),
50
+ label,
51
+ hash,
52
+ queryInfo: { _tag: 'None' },
53
+ }
54
+ }
36
55
 
37
56
  export class LiveStoreGraphQLQuery<
38
57
  TResult extends Record<string, any>,
@@ -46,13 +65,13 @@ export class LiveStoreGraphQLQuery<
46
65
  document: DocumentNode<TResult, TVariableValues>
47
66
 
48
67
  /** A reactive thunk representing the query results */
49
- results$: Thunk<TResultMapped, QueryContext, RefreshReason>
68
+ results$: Thunk<TResultMapped, ReactivityGraphContext, RefreshReason>
50
69
 
51
- variableValues$: Thunk<TVariableValues, QueryContext, RefreshReason> | undefined
70
+ variableValues$: Thunk<TVariableValues, ReactivityGraphContext, RefreshReason> | undefined
52
71
 
53
72
  label: string
54
73
 
55
- protected reactivityGraph: ReactivityGraph
74
+ reactivityGraph: ReactivityGraph
56
75
 
57
76
  queryInfo: QueryInfo.None = { _tag: 'None' }
58
77
 
@@ -68,7 +87,7 @@ export class LiveStoreGraphQLQuery<
68
87
  document: DocumentNode<TResult, TVariableValues>
69
88
  genVariableValues: TVariableValues | ((get: GetAtomResult) => TVariableValues)
70
89
  label?: string
71
- reactivityGraph?: ReactivityGraph
90
+ reactivityGraph: ReactivityGraph
72
91
  map?: MapResult<TResultMapped, TResult>
73
92
  }) {
74
93
  super()
@@ -78,7 +97,7 @@ export class LiveStoreGraphQLQuery<
78
97
  this.label = labelWithDefault
79
98
  this.document = document
80
99
 
81
- this.reactivityGraph = reactivityGraph ?? globalReactivityGraph
100
+ this.reactivityGraph = reactivityGraph
82
101
 
83
102
  this.mapResult =
84
103
  map === undefined
@@ -102,8 +121,10 @@ export class LiveStoreGraphQLQuery<
102
121
 
103
122
  if (typeof genVariableValues === 'function') {
104
123
  variableValues$OrvariableValues = this.reactivityGraph.makeThunk(
105
- (get, _setDebugInfo, { rootOtelContext }, otelContext) => {
106
- return genVariableValues(makeGetAtomResult(get, otelContext ?? rootOtelContext))
124
+ (get, _setDebugInfo, ctx, otelContext) => {
125
+ return genVariableValues(
126
+ makeGetAtomResult(get, ctx, otelContext ?? ctx.rootOtelContext, this.dependencyQueriesRef),
127
+ )
107
128
  },
108
129
  { label: `${labelWithDefault}:variableValues`, meta: { liveStoreThunkType: 'graphql.variables' } },
109
130
  )
@@ -114,9 +135,10 @@ export class LiveStoreGraphQLQuery<
114
135
 
115
136
  const resultsLabel = `${labelWithDefault}:results`
116
137
  this.results$ = this.reactivityGraph.makeThunk<TResultMapped>(
117
- (get, setDebugInfo, { store, otelTracer, rootOtelContext }, otelContext) => {
138
+ (get, setDebugInfo, ctx, otelContext, debugRefreshReason) => {
139
+ const { store, otelTracer, rootOtelContext } = ctx
118
140
  const variableValues = isThunk(variableValues$OrvariableValues)
119
- ? (get(variableValues$OrvariableValues) as TVariableValues)
141
+ ? (get(variableValues$OrvariableValues, otelContext, debugRefreshReason) as TVariableValues)
120
142
  : (variableValues$OrvariableValues as TVariableValues)
121
143
  const { result, queriedTables, durationMs } = this.queryOnce({
122
144
  document,
@@ -124,7 +146,7 @@ export class LiveStoreGraphQLQuery<
124
146
  otelContext: otelContext ?? rootOtelContext,
125
147
  otelTracer,
126
148
  store: store as Store<TContext>,
127
- get: makeGetAtomResult(get, otelContext ?? rootOtelContext),
149
+ get: makeGetAtomResult(get, ctx, otelContext ?? rootOtelContext, this.dependencyQueriesRef),
128
150
  })
129
151
 
130
152
  // Add dependencies on any tables that were used
@@ -215,5 +237,9 @@ export class LiveStoreGraphQLQuery<
215
237
  }
216
238
 
217
239
  this.reactivityGraph.destroyNode(this.results$)
240
+
241
+ for (const query of this.dependencyQueriesRef) {
242
+ query.deref()
243
+ }
218
244
  }
219
245
  }
@@ -0,0 +1,47 @@
1
+ import { nanoid } from '@livestore/utils/nanoid'
2
+
3
+ import type * as RG from '../reactive.js'
4
+ import type { RefreshReason } from '../store/store-types.js'
5
+ import type { ILiveQueryRef, ILiveQueryRefDef, ReactivityGraph, ReactivityGraphContext } from './base-class.js'
6
+ import { withRCMap } from './base-class.js'
7
+
8
+ export const makeRef = <T>(
9
+ defaultValue: T,
10
+ options?: {
11
+ label?: string
12
+ },
13
+ ): ILiveQueryRefDef<T> => {
14
+ const id = nanoid()
15
+ return {
16
+ _tag: 'live-ref-def',
17
+ defaultValue,
18
+ make: withRCMap(id, (ctx) => new LiveQueryRef(defaultValue, ctx.reactivityGraph.deref()!, options)),
19
+ }
20
+ }
21
+
22
+ export class LiveQueryRef<T> implements ILiveQueryRef<T> {
23
+ _tag = 'live-ref' as const
24
+ readonly ref: RG.Ref<T, ReactivityGraphContext, RefreshReason>
25
+
26
+ constructor(
27
+ private defaultValue: T,
28
+ readonly reactivityGraph: ReactivityGraph,
29
+ private options?: {
30
+ label?: string
31
+ },
32
+ ) {
33
+ this.ref = reactivityGraph.makeRef(defaultValue, { label: options?.label })
34
+ }
35
+
36
+ set = (value: T) => {
37
+ this.reactivityGraph.setRef(this.ref, value)
38
+ }
39
+
40
+ get = () => {
41
+ return this.ref.computeResult()
42
+ }
43
+
44
+ destroy = () => {
45
+ this.reactivityGraph.destroyNode(this.ref)
46
+ }
47
+ }