@livestore/livestore 0.2.0 → 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 (163) 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 +20 -6
  7. package/dist/SynchronousDatabaseWrapper.d.ts.map +1 -1
  8. package/dist/SynchronousDatabaseWrapper.js +38 -6
  9. package/dist/SynchronousDatabaseWrapper.js.map +1 -1
  10. package/dist/__tests__/fixture.d.ts +252 -0
  11. package/dist/__tests__/fixture.d.ts.map +1 -0
  12. package/dist/__tests__/fixture.js +18 -0
  13. package/dist/__tests__/fixture.js.map +1 -0
  14. package/dist/effect/LiveStore.d.ts +16 -12
  15. package/dist/effect/LiveStore.d.ts.map +1 -1
  16. package/dist/effect/LiveStore.js +14 -14
  17. package/dist/effect/LiveStore.js.map +1 -1
  18. package/dist/index.d.ts +6 -7
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +4 -4
  21. package/dist/index.js.map +1 -1
  22. package/dist/live-queries/base-class.d.ts +64 -21
  23. package/dist/live-queries/base-class.d.ts.map +1 -1
  24. package/dist/live-queries/base-class.js +56 -13
  25. package/dist/live-queries/base-class.js.map +1 -1
  26. package/dist/live-queries/computed.d.ts +7 -7
  27. package/dist/live-queries/computed.d.ts.map +1 -1
  28. package/dist/live-queries/computed.js +35 -11
  29. package/dist/live-queries/computed.js.map +1 -1
  30. package/dist/live-queries/{sql.d.ts → db-query.d.ts} +19 -14
  31. package/dist/live-queries/db-query.d.ts.map +1 -0
  32. package/dist/live-queries/db-query.js +244 -0
  33. package/dist/live-queries/db-query.js.map +1 -0
  34. package/dist/live-queries/db-query.test.d.ts +2 -0
  35. package/dist/live-queries/db-query.test.d.ts.map +1 -0
  36. package/dist/live-queries/db-query.test.js +123 -0
  37. package/dist/live-queries/db-query.test.js.map +1 -0
  38. package/dist/live-queries/db.d.ts +12 -15
  39. package/dist/live-queries/db.d.ts.map +1 -1
  40. package/dist/live-queries/db.js +72 -48
  41. package/dist/live-queries/db.js.map +1 -1
  42. package/dist/live-queries/db.test.js +18 -15
  43. package/dist/live-queries/db.test.js.map +1 -1
  44. package/dist/live-queries/graphql.d.ts +8 -8
  45. package/dist/live-queries/graphql.d.ts.map +1 -1
  46. package/dist/live-queries/graphql.js +35 -9
  47. package/dist/live-queries/graphql.js.map +1 -1
  48. package/dist/live-queries/make-ref.d.ts +20 -0
  49. package/dist/live-queries/make-ref.d.ts.map +1 -0
  50. package/dist/live-queries/make-ref.js +33 -0
  51. package/dist/live-queries/make-ref.js.map +1 -0
  52. package/dist/reactive.d.ts +15 -13
  53. package/dist/reactive.d.ts.map +1 -1
  54. package/dist/reactive.js +15 -9
  55. package/dist/reactive.js.map +1 -1
  56. package/dist/row-query-utils.d.ts +4 -4
  57. package/dist/row-query-utils.d.ts.map +1 -1
  58. package/dist/row-query-utils.js +14 -10
  59. package/dist/row-query-utils.js.map +1 -1
  60. package/dist/store/create-store.d.ts +13 -12
  61. package/dist/store/create-store.d.ts.map +1 -1
  62. package/dist/store/create-store.js +27 -33
  63. package/dist/store/create-store.js.map +1 -1
  64. package/dist/store/devtools.d.ts +3 -3
  65. package/dist/store/devtools.d.ts.map +1 -1
  66. package/dist/store/devtools.js +56 -34
  67. package/dist/store/devtools.js.map +1 -1
  68. package/dist/store/store-types.d.ts +18 -18
  69. package/dist/store/store-types.d.ts.map +1 -1
  70. package/dist/store/store-types.js.map +1 -1
  71. package/dist/store/store.d.ts +57 -38
  72. package/dist/store/store.d.ts.map +1 -1
  73. package/dist/store/store.js +225 -188
  74. package/dist/store/store.js.map +1 -1
  75. package/dist/store/store.test.d.ts +2 -0
  76. package/dist/store/store.test.d.ts.map +1 -0
  77. package/dist/store/store.test.js +27 -0
  78. package/dist/store/store.test.js.map +1 -0
  79. package/dist/utils/dev.d.ts.map +1 -1
  80. package/dist/utils/dev.js +3 -2
  81. package/dist/utils/dev.js.map +1 -1
  82. package/dist/utils/expo.d.ts +2 -0
  83. package/dist/utils/expo.d.ts.map +1 -0
  84. package/dist/utils/expo.js +8 -0
  85. package/dist/utils/expo.js.map +1 -0
  86. package/dist/utils/function-string.d.ts +7 -0
  87. package/dist/utils/function-string.d.ts.map +1 -0
  88. package/dist/utils/function-string.js +9 -0
  89. package/dist/utils/function-string.js.map +1 -0
  90. package/dist/utils/stack-info.d.ts.map +1 -1
  91. package/dist/utils/stack-info.js +6 -1
  92. package/dist/utils/stack-info.js.map +1 -1
  93. package/dist/utils/stack-info.test.js +54 -1
  94. package/dist/utils/stack-info.test.js.map +1 -1
  95. package/dist/utils/tests/fixture.d.ts +2 -6
  96. package/dist/utils/tests/fixture.d.ts.map +1 -1
  97. package/dist/utils/tests/fixture.js +7 -13
  98. package/dist/utils/tests/fixture.js.map +1 -1
  99. package/dist/utils/tests/mod.d.ts +1 -0
  100. package/dist/utils/tests/mod.d.ts.map +1 -1
  101. package/dist/utils/tests/mod.js +1 -0
  102. package/dist/utils/tests/mod.js.map +1 -1
  103. package/dist/utils/tests/otel.d.ts +60 -1
  104. package/dist/utils/tests/otel.d.ts.map +1 -1
  105. package/dist/utils/tests/otel.js +65 -4
  106. package/dist/utils/tests/otel.js.map +1 -1
  107. package/package.json +12 -12
  108. package/src/{SynchronousDatabaseWrapper.ts → SqliteDbWrapper.ts} +59 -13
  109. package/src/ambient.d.ts +1 -1
  110. package/src/effect/LiveStore.ts +32 -33
  111. package/src/index.ts +14 -7
  112. package/src/live-queries/__snapshots__/{db.test.ts.snap → db-query.test.ts.snap} +220 -69
  113. package/src/live-queries/base-class.ts +160 -40
  114. package/src/live-queries/computed.ts +45 -19
  115. package/src/live-queries/{db.test.ts → db-query.test.ts} +23 -12
  116. package/src/live-queries/{db.ts → db-query.ts} +124 -61
  117. package/src/live-queries/graphql.ts +47 -21
  118. package/src/live-queries/make-ref.ts +47 -0
  119. package/src/reactive.ts +52 -27
  120. package/src/row-query-utils.ts +29 -18
  121. package/src/store/create-store.ts +106 -113
  122. package/src/store/devtools.ts +65 -39
  123. package/src/store/store-types.ts +20 -18
  124. package/src/store/store.ts +361 -290
  125. package/src/utils/dev.ts +4 -2
  126. package/src/utils/function-string.ts +12 -0
  127. package/src/utils/stack-info.test.ts +58 -1
  128. package/src/utils/stack-info.ts +6 -1
  129. package/src/utils/tests/fixture.ts +6 -16
  130. package/src/utils/tests/mod.ts +1 -0
  131. package/src/utils/tests/otel.ts +71 -5
  132. package/dist/live-queries/sql.d.ts.map +0 -1
  133. package/dist/live-queries/sql.js +0 -175
  134. package/dist/live-queries/sql.js.map +0 -1
  135. package/dist/live-queries/sql.test.d.ts +0 -2
  136. package/dist/live-queries/sql.test.d.ts.map +0 -1
  137. package/dist/live-queries/sql.test.js +0 -285
  138. package/dist/live-queries/sql.test.js.map +0 -1
  139. package/dist/reactiveQueries/base-class.d.ts +0 -64
  140. package/dist/reactiveQueries/base-class.d.ts.map +0 -1
  141. package/dist/reactiveQueries/base-class.js +0 -31
  142. package/dist/reactiveQueries/base-class.js.map +0 -1
  143. package/dist/reactiveQueries/computed.d.ts +0 -26
  144. package/dist/reactiveQueries/computed.d.ts.map +0 -1
  145. package/dist/reactiveQueries/computed.js +0 -38
  146. package/dist/reactiveQueries/computed.js.map +0 -1
  147. package/dist/reactiveQueries/graphql.d.ts +0 -49
  148. package/dist/reactiveQueries/graphql.d.ts.map +0 -1
  149. package/dist/reactiveQueries/graphql.js +0 -122
  150. package/dist/reactiveQueries/graphql.js.map +0 -1
  151. package/dist/reactiveQueries/sql.d.ts +0 -62
  152. package/dist/reactiveQueries/sql.d.ts.map +0 -1
  153. package/dist/reactiveQueries/sql.js +0 -175
  154. package/dist/reactiveQueries/sql.js.map +0 -1
  155. package/dist/reactiveQueries/sql.test.d.ts +0 -2
  156. package/dist/reactiveQueries/sql.test.d.ts.map +0 -1
  157. package/dist/reactiveQueries/sql.test.js +0 -285
  158. package/dist/reactiveQueries/sql.test.js.map +0 -1
  159. package/dist/row-query.d.ts +0 -16
  160. package/dist/row-query.d.ts.map +0 -1
  161. package/dist/row-query.js +0 -30
  162. package/dist/row-query.js.map +0 -1
  163. package/src/global-state.ts +0 -20
@@ -1,27 +1,48 @@
1
1
  import type { QueryInfo } from '@livestore/common'
2
2
  import * as otel from '@opentelemetry/api'
3
3
 
4
- import { globalReactivityGraph } from '../global-state.js'
5
4
  import type { Thunk } from '../reactive.js'
6
5
  import type { RefreshReason } from '../store/store-types.js'
6
+ import { isValidFunctionString } from '../utils/function-string.js'
7
7
  import { getDurationMsFromSpan } from '../utils/otel.js'
8
- import type { GetAtomResult, LiveQuery, QueryContext, ReactivityGraph } from './base-class.js'
9
- import { LiveStoreQueryBase, makeGetAtomResult } from './base-class.js'
8
+ import type { DepKey, GetAtomResult, LiveQueryDef, ReactivityGraph, ReactivityGraphContext } from './base-class.js'
9
+ import { defCounterRef, depsToString, LiveStoreQueryBase, makeGetAtomResult, withRCMap } from './base-class.js'
10
10
 
11
11
  export const computed = <TResult, TQueryInfo extends QueryInfo = QueryInfo.None>(
12
12
  fn: (get: GetAtomResult) => TResult,
13
13
  options?: {
14
- label: string
15
- reactivityGraph?: ReactivityGraph
14
+ label?: string
16
15
  queryInfo?: TQueryInfo
16
+ deps?: DepKey
17
17
  },
18
- ): LiveQuery<TResult, TQueryInfo> =>
19
- new LiveStoreComputedQuery<TResult, TQueryInfo>({
20
- fn,
18
+ ): LiveQueryDef<TResult, TQueryInfo> => {
19
+ const hash = options?.deps ? depsToString(options.deps) : fn.toString()
20
+ if (isValidFunctionString(hash)._tag === 'invalid') {
21
+ throw new Error(`On Expo/React Native, computed queries must provide a \`deps\` option`)
22
+ }
23
+
24
+ const queryInfo = options?.queryInfo ?? ({ _tag: 'None' } as TQueryInfo)
25
+
26
+ return {
27
+ _tag: 'def',
28
+ id: ++defCounterRef.current,
29
+ make: withRCMap(hash, (ctx, _otelContext) => {
30
+ // TODO onDestroy
31
+ return new LiveStoreComputedQuery<TResult, TQueryInfo>({
32
+ fn,
33
+ label: options?.label ?? fn.toString(),
34
+ queryInfo: options?.queryInfo,
35
+ reactivityGraph: ctx.reactivityGraph.deref()!,
36
+ })
37
+ }),
21
38
  label: options?.label ?? fn.toString(),
22
- reactivityGraph: options?.reactivityGraph,
23
- queryInfo: options?.queryInfo,
24
- })
39
+ // NOTE We're using the `makeQuery` function body string to make sure the key is unique across the app
40
+ // TODO we should figure out whether this could cause some problems and/or if there's a better way to do this
41
+ // NOTE `fn.toString()` doesn't work in Expo as it always produces `[native code]`
42
+ hash,
43
+ queryInfo,
44
+ }
45
+ }
25
46
 
26
47
  export class LiveStoreComputedQuery<TResult, TQueryInfo extends QueryInfo = QueryInfo.None> extends LiveStoreQueryBase<
27
48
  TResult,
@@ -30,11 +51,11 @@ export class LiveStoreComputedQuery<TResult, TQueryInfo extends QueryInfo = Quer
30
51
  _tag: 'computed' = 'computed'
31
52
 
32
53
  /** A reactive thunk representing the query results */
33
- results$: Thunk<TResult, QueryContext, RefreshReason>
54
+ results$: Thunk<TResult, ReactivityGraphContext, RefreshReason>
34
55
 
35
56
  label: string
36
57
 
37
- protected reactivityGraph: ReactivityGraph
58
+ reactivityGraph: ReactivityGraph
38
59
 
39
60
  queryInfo: TQueryInfo
40
61
 
@@ -46,23 +67,22 @@ export class LiveStoreComputedQuery<TResult, TQueryInfo extends QueryInfo = Quer
46
67
  }: {
47
68
  label: string
48
69
  fn: (get: GetAtomResult) => TResult
49
- reactivityGraph?: ReactivityGraph
70
+ reactivityGraph: ReactivityGraph
50
71
  queryInfo?: TQueryInfo
51
72
  }) {
52
73
  super()
53
74
 
54
75
  this.label = label
55
-
56
- this.reactivityGraph = reactivityGraph ?? globalReactivityGraph
76
+ this.reactivityGraph = reactivityGraph
57
77
  this.queryInfo = queryInfo ?? ({ _tag: 'None' } as TQueryInfo)
58
78
 
59
79
  const queryLabel = `${label}:results`
60
80
 
61
81
  this.results$ = this.reactivityGraph.makeThunk(
62
- (get, setDebugInfo, { otelTracer, rootOtelContext }, otelContext) =>
63
- otelTracer.startActiveSpan(`js:${label}`, {}, otelContext ?? rootOtelContext, (span) => {
82
+ (get, setDebugInfo, ctx, otelContext) =>
83
+ ctx.otelTracer.startActiveSpan(`js:${label}`, {}, otelContext ?? ctx.rootOtelContext, (span) => {
64
84
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
65
- const res = fn(makeGetAtomResult(get, otelContext))
85
+ const res = fn(makeGetAtomResult(get, ctx, otelContext, this.dependencyQueriesRef))
66
86
 
67
87
  span.end()
68
88
 
@@ -79,6 +99,12 @@ export class LiveStoreComputedQuery<TResult, TQueryInfo extends QueryInfo = Quer
79
99
  }
80
100
 
81
101
  destroy = () => {
102
+ this.isDestroyed = true
103
+
82
104
  this.reactivityGraph.destroyNode(this.results$)
105
+
106
+ for (const query of this.dependencyQueriesRef) {
107
+ query.deref()
108
+ }
83
109
  }
84
110
  }
@@ -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,9 +18,14 @@ 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
 
28
+ // const provider = cachedProvider ?? new BasicTracerProvider({ spanProcessors: [new SimpleSpanProcessor(exporter)] })
23
29
  const provider = cachedProvider ?? new BasicTracerProvider()
24
30
  cachedProvider = provider
25
31
  provider.addSpanProcessor(new SimpleSpanProcessor(exporter))
@@ -27,10 +33,10 @@ describe('otel', () => {
27
33
 
28
34
  const otelTracer = otel.trace.getTracer('test')
29
35
 
30
- const span = otelTracer.startSpan('test')
36
+ const span = otelTracer.startSpan('test-root')
31
37
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
32
38
 
33
- const { store } = yield* makeTodoMvc({ otelTracer, otelContext })
39
+ const store = yield* makeTodoMvc({ otelTracer, otelContext })
34
40
 
35
41
  return {
36
42
  store,
@@ -50,11 +56,11 @@ describe('otel', () => {
50
56
  schema: Schema.Array(tables.todos.schema),
51
57
  queriedTables: new Set(['todos']),
52
58
  })
53
- expect(query$.run()).toMatchInlineSnapshot('[]')
59
+ expect(store.query(query$)).toMatchInlineSnapshot('[]')
54
60
 
55
61
  store.mutate(rawSqlMutation({ sql: sql`INSERT INTO todos (id, text, completed) VALUES ('t1', 'buy milk', 0)` }))
56
62
 
57
- expect(query$.run()).toMatchInlineSnapshot(`
63
+ expect(store.query(query$)).toMatchInlineSnapshot(`
58
64
  [
59
65
  {
60
66
  "completed": false,
@@ -64,7 +70,6 @@ describe('otel', () => {
64
70
  ]
65
71
  `)
66
72
 
67
- query$.destroy()
68
73
  span.end()
69
74
 
70
75
  return { exporter }
@@ -88,7 +93,9 @@ describe('otel', () => {
88
93
  { label: 'all todos' },
89
94
  )
90
95
 
91
- expect(query$.run()).toMatchInlineSnapshot(`
96
+ expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
97
+
98
+ expect(store.query(query$)).toMatchInlineSnapshot(`
92
99
  {
93
100
  "completed": false,
94
101
  "id": "",
@@ -96,9 +103,13 @@ describe('otel', () => {
96
103
  }
97
104
  `)
98
105
 
106
+ expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
107
+
99
108
  store.mutate(rawSqlMutation({ sql: sql`INSERT INTO todos (id, text, completed) VALUES ('t1', 'buy milk', 0)` }))
100
109
 
101
- expect(query$.run()).toMatchInlineSnapshot(`
110
+ expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
111
+
112
+ expect(store.query(query$)).toMatchInlineSnapshot(`
102
113
  {
103
114
  "completed": false,
104
115
  "id": "t1",
@@ -106,7 +117,8 @@ describe('otel', () => {
106
117
  }
107
118
  `)
108
119
 
109
- query$.destroy()
120
+ expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
121
+
110
122
  span.end()
111
123
 
112
124
  return { exporter }
@@ -124,7 +136,7 @@ describe('otel', () => {
124
136
  const filter = computed(() => ({ completed: false }))
125
137
  const query$ = queryDb((get) => tables.todos.query.where(get(filter)).first({ fallback: () => defaultTodo }))
126
138
 
127
- expect(query$.run()).toMatchInlineSnapshot(`
139
+ expect(store.query(query$)).toMatchInlineSnapshot(`
128
140
  {
129
141
  "completed": false,
130
142
  "id": "",
@@ -134,7 +146,7 @@ describe('otel', () => {
134
146
 
135
147
  store.mutate(rawSqlMutation({ sql: sql`INSERT INTO todos (id, text, completed) VALUES ('t1', 'buy milk', 0)` }))
136
148
 
137
- expect(query$.run()).toMatchInlineSnapshot(`
149
+ expect(store.query(query$)).toMatchInlineSnapshot(`
138
150
  {
139
151
  "completed": false,
140
152
  "id": "t1",
@@ -142,7 +154,6 @@ describe('otel', () => {
142
154
  }
143
155
  `)
144
156
 
145
- query$.destroy()
146
157
  span.end()
147
158
 
148
159
  return { exporter }
@@ -5,19 +5,20 @@ import {
5
5
  prepareBindValues,
6
6
  QueryBuilderAstSymbol,
7
7
  replaceSessionIdSymbol,
8
+ UnexpectedError,
8
9
  } from '@livestore/common'
9
10
  import { deepEqual, shouldNeverHappen } from '@livestore/utils'
10
11
  import { Predicate, Schema, TreeFormatter } from '@livestore/utils/effect'
11
12
  import * as otel from '@opentelemetry/api'
12
13
 
13
- import { globalReactivityGraph } from '../global-state.js'
14
14
  import type { Thunk } from '../reactive.js'
15
15
  import { isThunk, NOT_REFRESHED_YET } from '../reactive.js'
16
16
  import { makeExecBeforeFirstRun, rowQueryLabel } from '../row-query-utils.js'
17
17
  import type { RefreshReason } from '../store/store-types.js'
18
+ import { isValidFunctionString } from '../utils/function-string.js'
18
19
  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'
20
+ import type { DepKey, GetAtomResult, LiveQueryDef, ReactivityGraph, ReactivityGraphContext } from './base-class.js'
21
+ import { defCounterRef, depsToString, LiveStoreQueryBase, makeGetAtomResult, withRCMap } from './base-class.js'
21
22
 
22
23
  export type QueryInputRaw<TDecoded, TEncoded, TQueryInfo extends QueryInfo> = {
23
24
  query: string
@@ -30,9 +31,12 @@ export type QueryInputRaw<TDecoded, TEncoded, TQueryInfo extends QueryInfo> = {
30
31
  */
31
32
  queriedTables?: Set<string>
32
33
  queryInfo?: TQueryInfo
33
- execBeforeFirstRun?: (ctx: QueryContext) => void
34
+ execBeforeFirstRun?: (ctx: ReactivityGraphContext) => void
34
35
  }
35
36
 
37
+ export const isQueryInputRaw = (value: unknown): value is QueryInputRaw<any, any, any> =>
38
+ Predicate.hasProperty(value, 'query') && Predicate.hasProperty(value, 'schema')
39
+
36
40
  export type QueryInput<TDecoded, TEncoded, TQueryInfo extends QueryInfo> =
37
41
  | QueryInputRaw<TDecoded, TEncoded, TQueryInfo>
38
42
  | QueryBuilder<TDecoded, any, any, TQueryInfo>
@@ -51,10 +55,10 @@ export const queryDb: {
51
55
  * Used for debugging / devtools
52
56
  */
53
57
  label?: string
54
- reactivityGraph?: ReactivityGraph
55
- otelContext?: otel.Context
58
+ deps?: DepKey
59
+ queryInfo?: TQueryInfo
56
60
  },
57
- ): LiveQuery<TResult, TQueryInfo>
61
+ ): LiveQueryDef<TResult, TQueryInfo>
58
62
  // NOTE in this "thunk case", we can't directly derive label/queryInfo from the queryInput,
59
63
  // so the caller needs to provide them explicitly otherwise queryInfo will be set to `None`,
60
64
  // and label will be set during the query execution
@@ -68,20 +72,47 @@ export const queryDb: {
68
72
  * Used for debugging / devtools
69
73
  */
70
74
  label?: string
71
- reactivityGraph?: ReactivityGraph
75
+ deps?: DepKey
72
76
  queryInfo?: TQueryInfo
73
- otelContext?: otel.Context
74
77
  },
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
- })
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
+ }
85
116
 
86
117
  /* An object encapsulating a reactive SQL query */
87
118
  export class LiveStoreDbQuery<
@@ -92,16 +123,16 @@ export class LiveStoreDbQuery<
92
123
  _tag: 'db' = 'db'
93
124
 
94
125
  /** A reactive thunk representing the query text */
95
- queryInput$: Thunk<QueryInput<TResultSchema, ReadonlyArray<any>, TQueryInfo>, QueryContext, RefreshReason> | undefined
126
+ queryInput$: Thunk<QueryInputRaw<any, any, QueryInfo>, ReactivityGraphContext, RefreshReason> | undefined
96
127
 
97
128
  /** A reactive thunk representing the query results */
98
- results$: Thunk<TResult, QueryContext, RefreshReason>
129
+ results$: Thunk<TResult, ReactivityGraphContext, RefreshReason>
99
130
 
100
131
  label: string
101
132
 
102
133
  queryInfo: TQueryInfo
103
134
 
104
- protected reactivityGraph
135
+ readonly reactivityGraph
105
136
 
106
137
  private mapResult: (rows: TResultSchema) => TResult
107
138
 
@@ -116,17 +147,18 @@ export class LiveStoreDbQuery<
116
147
  label?: string
117
148
  queryInput:
118
149
  | QueryInput<TResultSchema, ReadonlyArray<any>, TQueryInfo>
119
- | ((get: GetAtomResult, ctx: QueryContext) => QueryInput<TResultSchema, ReadonlyArray<any>, TQueryInfo>)
120
- reactivityGraph?: ReactivityGraph
150
+ | ((get: GetAtomResult, ctx: ReactivityGraphContext) => QueryInput<TResultSchema, ReadonlyArray<any>, TQueryInfo>)
151
+ reactivityGraph: ReactivityGraph
121
152
  map?: (rows: TResultSchema) => TResult
122
153
  queryInfo?: TQueryInfo
154
+ /** Only used for the initial query execution */
123
155
  otelContext?: otel.Context
124
156
  }) {
125
157
  super()
126
158
 
127
159
  let label = inputLabel ?? 'db(unknown)'
128
160
  let queryInfo = inputQueryInfo ?? ({ _tag: 'None' } as TQueryInfo)
129
- this.reactivityGraph = reactivityGraph ?? globalReactivityGraph
161
+ this.reactivityGraph = reactivityGraph
130
162
 
131
163
  this.mapResult = map === undefined ? (rows: any) => rows as TResult : map
132
164
 
@@ -135,37 +167,43 @@ export class LiveStoreDbQuery<
135
167
  typeof queryInput === 'function' ? undefined : isQueryBuilder(queryInput) ? undefined : queryInput.schema,
136
168
  }
137
169
 
138
- const execBeforeFirstRunRef: { current: ((ctx: QueryContext, otelContext: otel.Context) => void) | undefined } = {
170
+ const execBeforeFirstRunRef: {
171
+ current: ((ctx: ReactivityGraphContext, otelContext: otel.Context) => void) | undefined
172
+ } = {
139
173
  current: undefined,
140
174
  }
141
175
 
142
176
  type TQueryInputRaw = QueryInputRaw<any, any, QueryInfo>
143
177
 
144
- let queryInputRaw$OrQueryInputRaw: TQueryInputRaw | Thunk<TQueryInputRaw, QueryContext, RefreshReason>
178
+ let queryInputRaw$OrQueryInputRaw: TQueryInputRaw | Thunk<TQueryInputRaw, ReactivityGraphContext, RefreshReason>
145
179
 
146
180
  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,
181
+ try {
182
+ const qbRes = qb.asSql()
183
+ const schema = getResultSchema(qb) as Schema.Schema<TResultSchema, ReadonlyArray<any>>
184
+ const ast = qb[QueryBuilderAstSymbol]
185
+
186
+ return {
187
+ queryInputRaw: {
188
+ query: qbRes.query,
189
+ schema,
190
+ bindValues: qbRes.bindValues,
191
+ queriedTables: new Set([ast.tableDef.sqliteDef.name]),
192
+ queryInfo: queryInfoFromQueryBuilder(qb),
193
+ } satisfies TQueryInputRaw,
194
+ label: ast._tag === 'RowQuery' ? rowQueryLabel(ast.tableDef, ast.id) : qb.toString(),
195
+ execBeforeFirstRun:
196
+ ast._tag === 'RowQuery'
197
+ ? makeExecBeforeFirstRun({
198
+ table: ast.tableDef,
199
+ insertValues: ast.insertValues,
200
+ id: ast.id,
201
+ otelContext,
202
+ })
203
+ : undefined,
204
+ }
205
+ } catch (cause) {
206
+ throw new UnexpectedError({ cause, note: `Error building query for ${qb.toString()}`, payload: { qb } })
169
207
  }
170
208
  }
171
209
 
@@ -173,7 +211,10 @@ export class LiveStoreDbQuery<
173
211
  queryInputRaw$OrQueryInputRaw = this.reactivityGraph.makeThunk(
174
212
  (get, setDebugInfo, ctx, otelContext) => {
175
213
  const startMs = performance.now()
176
- 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
+ )
177
218
  const durationMs = performance.now() - startMs
178
219
 
179
220
  let queryInputRaw: TQueryInputRaw
@@ -205,6 +246,8 @@ export class LiveStoreDbQuery<
205
246
  equal: (a, b) => a.query === b.query && deepEqual(a.bindValues, b.bindValues),
206
247
  },
207
248
  )
249
+
250
+ this.queryInput$ = queryInputRaw$OrQueryInputRaw
208
251
  } else {
209
252
  let queryInputRaw: TQueryInputRaw
210
253
  if (isQueryBuilder(queryInput)) {
@@ -248,10 +291,16 @@ export class LiveStoreDbQuery<
248
291
  : undefined
249
292
 
250
293
  const results$ = this.reactivityGraph.makeThunk<TResult>(
251
- (get, setDebugInfo, queryContext, otelContext) =>
294
+ (get, setDebugInfo, queryContext, otelContext, debugRefreshReason) =>
252
295
  queryContext.otelTracer.startActiveSpan(
253
296
  'db:...', // NOTE span name will be overridden further down
254
- {},
297
+ {
298
+ attributes: {
299
+ 'livestore.debugRefreshReason': Predicate.hasProperty(debugRefreshReason, 'label')
300
+ ? (debugRefreshReason.label as string)
301
+ : debugRefreshReason?._tag,
302
+ },
303
+ },
255
304
  otelContext ?? queryContext.rootOtelContext,
256
305
  (span) => {
257
306
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
@@ -263,34 +312,37 @@ export class LiveStoreDbQuery<
263
312
  }
264
313
 
265
314
  const queryInputResult = isThunk(queryInputRaw$OrQueryInputRaw)
266
- ? (get(queryInputRaw$OrQueryInputRaw, otelContext) as TQueryInputRaw)
315
+ ? (get(queryInputRaw$OrQueryInputRaw, otelContext, debugRefreshReason) as TQueryInputRaw)
267
316
  : (queryInputRaw$OrQueryInputRaw as TQueryInputRaw)
268
317
 
269
318
  const sqlString = queryInputResult.query
270
319
  const bindValues = queryInputResult.bindValues
271
320
 
272
321
  if (queriedTablesRef.current === undefined) {
273
- queriedTablesRef.current = store.syncDbWrapper.getTablesUsed(sqlString)
322
+ queriedTablesRef.current = store.sqliteDbWrapper.getTablesUsed(sqlString)
274
323
  }
275
324
 
276
325
  if (bindValues !== undefined) {
277
- replaceSessionIdSymbol(bindValues, store.clientSession.coordinator.sessionId)
326
+ replaceSessionIdSymbol(bindValues, store.clientSession.sessionId)
278
327
  }
279
328
 
280
329
  // Establish a reactive dependency on the tables used in the query
281
330
  for (const tableName of queriedTablesRef.current) {
282
331
  const tableRef = store.tableRefs[tableName] ?? shouldNeverHappen(`No table ref found for ${tableName}`)
283
- get(tableRef, otelContext)
332
+ get(tableRef, otelContext, debugRefreshReason)
284
333
  }
285
334
 
286
335
  span.setAttribute('sql.query', sqlString)
287
336
  span.updateName(`db:${sqlString.slice(0, 50)}`)
288
337
 
289
- const rawDbResults = store.syncDbWrapper.select<any>(sqlString, {
290
- queriedTables: queriedTablesRef.current,
291
- bindValues: bindValues ? prepareBindValues(bindValues, sqlString) : undefined,
292
- otelContext,
293
- })
338
+ const rawDbResults = store.sqliteDbWrapper.select<any>(
339
+ sqlString,
340
+ bindValues ? prepareBindValues(bindValues, sqlString) : undefined,
341
+ {
342
+ queriedTables: queriedTablesRef.current,
343
+ otelContext,
344
+ },
345
+ )
294
346
 
295
347
  span.setAttribute('sql.rowsCount', rawDbResults.length)
296
348
 
@@ -341,10 +393,21 @@ Result:`,
341
393
  }
342
394
 
343
395
  destroy = () => {
396
+ this.isDestroyed = true
397
+
344
398
  if (this.queryInput$ !== undefined) {
345
399
  this.reactivityGraph.destroyNode(this.queryInput$)
346
400
  }
347
401
 
348
402
  this.reactivityGraph.destroyNode(this.results$)
403
+
404
+ for (const query of this.dependencyQueriesRef) {
405
+ query.deref()
406
+ }
349
407
  }
350
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
+ }