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

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 (117) 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 +211 -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 +57 -21
  19. package/dist/live-queries/base-class.d.ts.map +1 -1
  20. package/dist/live-queries/base-class.js +54 -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 +34 -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 +243 -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 +113 -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 +34 -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 +19 -13
  49. package/dist/reactive.d.ts.map +1 -1
  50. package/dist/reactive.js +22 -18
  51. package/dist/reactive.js.map +1 -1
  52. package/dist/reactive.test.js +1 -1
  53. package/dist/reactive.test.js.map +1 -1
  54. package/dist/row-query-utils.d.ts +6 -6
  55. package/dist/row-query-utils.d.ts.map +1 -1
  56. package/dist/row-query-utils.js +15 -11
  57. package/dist/row-query-utils.js.map +1 -1
  58. package/dist/store/create-store.d.ts +7 -5
  59. package/dist/store/create-store.d.ts.map +1 -1
  60. package/dist/store/create-store.js +21 -7
  61. package/dist/store/create-store.js.map +1 -1
  62. package/dist/store/devtools.d.ts +5 -4
  63. package/dist/store/devtools.d.ts.map +1 -1
  64. package/dist/store/devtools.js +45 -23
  65. package/dist/store/devtools.js.map +1 -1
  66. package/dist/store/store-types.d.ts +9 -4
  67. package/dist/store/store-types.d.ts.map +1 -1
  68. package/dist/store/store-types.js.map +1 -1
  69. package/dist/store/store.d.ts +36 -18
  70. package/dist/store/store.d.ts.map +1 -1
  71. package/dist/store/store.js +127 -75
  72. package/dist/store/store.js.map +1 -1
  73. package/dist/utils/expo.d.ts +2 -0
  74. package/dist/utils/expo.d.ts.map +1 -0
  75. package/dist/utils/expo.js +8 -0
  76. package/dist/utils/expo.js.map +1 -0
  77. package/dist/utils/function-string.d.ts +7 -0
  78. package/dist/utils/function-string.d.ts.map +1 -0
  79. package/dist/utils/function-string.js +9 -0
  80. package/dist/utils/function-string.js.map +1 -0
  81. package/dist/utils/stack-info.d.ts.map +1 -1
  82. package/dist/utils/stack-info.js +6 -1
  83. package/dist/utils/stack-info.js.map +1 -1
  84. package/dist/utils/stack-info.test.js +54 -1
  85. package/dist/utils/stack-info.test.js.map +1 -1
  86. package/dist/utils/tests/fixture.d.ts +2 -6
  87. package/dist/utils/tests/fixture.d.ts.map +1 -1
  88. package/dist/utils/tests/fixture.js +3 -5
  89. package/dist/utils/tests/fixture.js.map +1 -1
  90. package/dist/utils/tests/mod.d.ts +1 -0
  91. package/dist/utils/tests/mod.d.ts.map +1 -1
  92. package/dist/utils/tests/mod.js +1 -0
  93. package/dist/utils/tests/mod.js.map +1 -1
  94. package/package.json +5 -5
  95. package/src/{SynchronousDatabaseWrapper.ts → SqliteDbWrapper.ts} +41 -12
  96. package/src/effect/LiveStore.ts +22 -14
  97. package/src/index.ts +14 -7
  98. package/src/live-queries/__snapshots__/{db.test.ts.snap → db-query.test.ts.snap} +196 -45
  99. package/src/live-queries/base-class.ts +151 -40
  100. package/src/live-queries/computed.ts +44 -19
  101. package/src/live-queries/{db.test.ts → db-query.test.ts} +44 -32
  102. package/src/live-queries/{db.ts → db-query.ts} +96 -39
  103. package/src/live-queries/graphql.ts +46 -21
  104. package/src/live-queries/make-ref.ts +47 -0
  105. package/src/reactive.test.ts +1 -1
  106. package/src/reactive.ts +60 -37
  107. package/src/row-query-utils.ts +32 -21
  108. package/src/store/create-store.ts +55 -27
  109. package/src/store/devtools.ts +74 -29
  110. package/src/store/store-types.ts +6 -4
  111. package/src/store/store.ts +231 -121
  112. package/src/utils/function-string.ts +12 -0
  113. package/src/utils/stack-info.test.ts +58 -1
  114. package/src/utils/stack-info.ts +6 -1
  115. package/src/utils/tests/fixture.ts +2 -7
  116. package/src/utils/tests/mod.ts +1 -0
  117. package/src/global-state.ts +0 -20
@@ -1,27 +1,47 @@
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 { 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
+ make: withRCMap(hash, (ctx, _otelContext) => {
29
+ // TODO onDestroy
30
+ return new LiveStoreComputedQuery<TResult, TQueryInfo>({
31
+ fn,
32
+ label: options?.label ?? fn.toString(),
33
+ queryInfo: options?.queryInfo,
34
+ reactivityGraph: ctx.reactivityGraph.deref()!,
35
+ })
36
+ }),
21
37
  label: options?.label ?? fn.toString(),
22
- reactivityGraph: options?.reactivityGraph,
23
- queryInfo: options?.queryInfo,
24
- })
38
+ // NOTE We're using the `makeQuery` function body string to make sure the key is unique across the app
39
+ // TODO we should figure out whether this could cause some problems and/or if there's a better way to do this
40
+ // NOTE `fn.toString()` doesn't work in Expo as it always produces `[native code]`
41
+ hash,
42
+ queryInfo,
43
+ }
44
+ }
25
45
 
26
46
  export class LiveStoreComputedQuery<TResult, TQueryInfo extends QueryInfo = QueryInfo.None> extends LiveStoreQueryBase<
27
47
  TResult,
@@ -30,11 +50,11 @@ export class LiveStoreComputedQuery<TResult, TQueryInfo extends QueryInfo = Quer
30
50
  _tag: 'computed' = 'computed'
31
51
 
32
52
  /** A reactive thunk representing the query results */
33
- results$: Thunk<TResult, QueryContext, RefreshReason>
53
+ results$: Thunk<TResult, ReactivityGraphContext, RefreshReason>
34
54
 
35
55
  label: string
36
56
 
37
- protected reactivityGraph: ReactivityGraph
57
+ reactivityGraph: ReactivityGraph
38
58
 
39
59
  queryInfo: TQueryInfo
40
60
 
@@ -46,23 +66,22 @@ export class LiveStoreComputedQuery<TResult, TQueryInfo extends QueryInfo = Quer
46
66
  }: {
47
67
  label: string
48
68
  fn: (get: GetAtomResult) => TResult
49
- reactivityGraph?: ReactivityGraph
69
+ reactivityGraph: ReactivityGraph
50
70
  queryInfo?: TQueryInfo
51
71
  }) {
52
72
  super()
53
73
 
54
74
  this.label = label
55
-
56
- this.reactivityGraph = reactivityGraph ?? globalReactivityGraph
75
+ this.reactivityGraph = reactivityGraph
57
76
  this.queryInfo = queryInfo ?? ({ _tag: 'None' } as TQueryInfo)
58
77
 
59
78
  const queryLabel = `${label}:results`
60
79
 
61
80
  this.results$ = this.reactivityGraph.makeThunk(
62
- (get, setDebugInfo, { otelTracer, rootOtelContext }, otelContext) =>
63
- otelTracer.startActiveSpan(`js:${label}`, {}, otelContext ?? rootOtelContext, (span) => {
81
+ (get, setDebugInfo, ctx, otelContext) =>
82
+ ctx.otelTracer.startActiveSpan(`js:${label}`, {}, otelContext ?? ctx.rootOtelContext, (span) => {
64
83
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
65
- const res = fn(makeGetAtomResult(get, otelContext))
84
+ const res = fn(makeGetAtomResult(get, ctx, otelContext, this.dependencyQueriesRef))
66
85
 
67
86
  span.end()
68
87
 
@@ -79,6 +98,12 @@ export class LiveStoreComputedQuery<TResult, TQueryInfo extends QueryInfo = Quer
79
98
  }
80
99
 
81
100
  destroy = () => {
101
+ this.isDestroyed = true
102
+
82
103
  this.reactivityGraph.destroyNode(this.results$)
104
+
105
+ for (const query of this.dependencyQueriesRef) {
106
+ query.deref()
107
+ }
83
108
  }
84
109
  }
@@ -1,9 +1,11 @@
1
1
  import { Effect, Schema } from '@livestore/utils/effect'
2
+ import { Vitest } from '@livestore/utils/node-vitest'
2
3
  import * as otel from '@opentelemetry/api'
3
4
  import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
4
- import { describe, expect, it } from 'vitest'
5
+ import { expect } from 'vitest'
5
6
 
6
7
  import { computed, queryDb, rawSqlMutation, sql } from '../index.js'
8
+ import * as RG from '../reactive.js'
7
9
  import { makeTodoMvc, tables } from '../utils/tests/fixture.js'
8
10
  import { getSimplifiedRootSpan } from '../utils/tests/otel.js'
9
11
 
@@ -14,12 +16,14 @@ TODO write tests for:
14
16
  - optional and explicit `queriedTables` argument
15
17
  */
16
18
 
17
- describe('otel', () => {
19
+ Vitest.describe('otel', () => {
18
20
  let cachedProvider: BasicTracerProvider | undefined
19
21
 
20
22
  const makeQuery = Effect.gen(function* () {
21
23
  const exporter = new InMemorySpanExporter()
22
24
 
25
+ RG.__resetIds()
26
+
23
27
  // const provider = cachedProvider ?? new BasicTracerProvider({ spanProcessors: [new SimpleSpanProcessor(exporter)] })
24
28
  const provider = cachedProvider ?? new BasicTracerProvider()
25
29
  cachedProvider = provider
@@ -31,7 +35,7 @@ describe('otel', () => {
31
35
  const span = otelTracer.startSpan('test-root')
32
36
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
33
37
 
34
- const { store } = yield* makeTodoMvc({ otelTracer, otelContext })
38
+ const store = yield* makeTodoMvc({ otelTracer, otelContext })
35
39
 
36
40
  return {
37
41
  store,
@@ -42,8 +46,8 @@ describe('otel', () => {
42
46
  }
43
47
  })
44
48
 
45
- it('otel', async () => {
46
- const { exporter } = await Effect.gen(function* () {
49
+ Vitest.scopedLive('otel', () =>
50
+ Effect.gen(function* () {
47
51
  const { store, exporter, span } = yield* makeQuery
48
52
 
49
53
  const query$ = queryDb({
@@ -51,11 +55,11 @@ describe('otel', () => {
51
55
  schema: Schema.Array(tables.todos.schema),
52
56
  queriedTables: new Set(['todos']),
53
57
  })
54
- expect(query$.run()).toMatchInlineSnapshot('[]')
58
+ expect(store.query(query$)).toMatchInlineSnapshot('[]')
55
59
 
56
60
  store.mutate(rawSqlMutation({ sql: sql`INSERT INTO todos (id, text, completed) VALUES ('t1', 'buy milk', 0)` }))
57
61
 
58
- expect(query$.run()).toMatchInlineSnapshot(`
62
+ expect(store.query(query$)).toMatchInlineSnapshot(`
59
63
  [
60
64
  {
61
65
  "completed": false,
@@ -65,17 +69,17 @@ describe('otel', () => {
65
69
  ]
66
70
  `)
67
71
 
68
- query$.destroy()
69
72
  span.end()
70
73
 
71
74
  return { exporter }
72
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise)
73
-
74
- expect(getSimplifiedRootSpan(exporter)).toMatchSnapshot()
75
- })
76
-
77
- it('with thunks', async () => {
78
- const { exporter } = await Effect.gen(function* () {
75
+ }).pipe(
76
+ Effect.scoped,
77
+ Effect.tap(({ exporter }) => expect(getSimplifiedRootSpan(exporter)).toMatchSnapshot()),
78
+ ),
79
+ )
80
+
81
+ Vitest.scopedLive('with thunks', () =>
82
+ Effect.gen(function* () {
79
83
  const { store, exporter, span } = yield* makeQuery
80
84
 
81
85
  const defaultTodo = { id: '', text: '', completed: false }
@@ -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,17 +117,19 @@ 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 }
114
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise)
115
-
116
- expect(getSimplifiedRootSpan(exporter)).toMatchSnapshot()
117
- })
118
-
119
- it('with thunks with query builder and without labels', async () => {
120
- const { exporter } = await Effect.gen(function* () {
125
+ }).pipe(
126
+ Effect.scoped,
127
+ Effect.tap(({ exporter }) => expect(getSimplifiedRootSpan(exporter)).toMatchSnapshot()),
128
+ ),
129
+ )
130
+
131
+ Vitest.scopedLive('with thunks with query builder and without labels', () =>
132
+ Effect.gen(function* () {
121
133
  const { store, exporter, span } = yield* makeQuery
122
134
 
123
135
  const defaultTodo = { id: '', text: '', completed: false }
@@ -125,7 +137,7 @@ describe('otel', () => {
125
137
  const filter = computed(() => ({ completed: false }))
126
138
  const query$ = queryDb((get) => tables.todos.query.where(get(filter)).first({ fallback: () => defaultTodo }))
127
139
 
128
- expect(query$.run()).toMatchInlineSnapshot(`
140
+ expect(store.query(query$)).toMatchInlineSnapshot(`
129
141
  {
130
142
  "completed": false,
131
143
  "id": "",
@@ -135,7 +147,7 @@ describe('otel', () => {
135
147
 
136
148
  store.mutate(rawSqlMutation({ sql: sql`INSERT INTO todos (id, text, completed) VALUES ('t1', 'buy milk', 0)` }))
137
149
 
138
- expect(query$.run()).toMatchInlineSnapshot(`
150
+ expect(store.query(query$)).toMatchInlineSnapshot(`
139
151
  {
140
152
  "completed": false,
141
153
  "id": "t1",
@@ -143,12 +155,12 @@ describe('otel', () => {
143
155
  }
144
156
  `)
145
157
 
146
- query$.destroy()
147
158
  span.end()
148
159
 
149
160
  return { exporter }
150
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise)
151
-
152
- expect(getSimplifiedRootSpan(exporter)).toMatchSnapshot()
153
- })
161
+ }).pipe(
162
+ Effect.scoped,
163
+ Effect.tap(({ exporter }) => expect(getSimplifiedRootSpan(exporter)).toMatchSnapshot()),
164
+ ),
165
+ )
154
166
  })
@@ -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 { 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,46 @@ 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
+ make: withRCMap(hash, (ctx, otelContext) => {
98
+ // TODO onDestroy
99
+ return new LiveStoreDbQuery({
100
+ reactivityGraph: ctx.reactivityGraph.deref()!,
101
+ queryInput,
102
+ label,
103
+ map: options?.map,
104
+ // We're not falling back to `None` here as the queryInfo will be set dynamically
105
+ queryInfo: options?.queryInfo,
106
+ otelContext,
107
+ })
108
+ }),
109
+ label,
110
+ hash,
111
+ queryInfo:
112
+ options?.queryInfo ?? (isQueryBuilder(queryInput) ? queryInfoFromQueryBuilder(queryInput) : { _tag: 'None' }),
113
+ }
114
+ }
86
115
 
87
116
  /* An object encapsulating a reactive SQL query */
88
117
  export class LiveStoreDbQuery<
@@ -93,16 +122,16 @@ export class LiveStoreDbQuery<
93
122
  _tag: 'db' = 'db'
94
123
 
95
124
  /** A reactive thunk representing the query text */
96
- queryInput$: Thunk<QueryInput<TResultSchema, ReadonlyArray<any>, TQueryInfo>, QueryContext, RefreshReason> | undefined
125
+ queryInput$: Thunk<QueryInputRaw<any, any, QueryInfo>, ReactivityGraphContext, RefreshReason> | undefined
97
126
 
98
127
  /** A reactive thunk representing the query results */
99
- results$: Thunk<TResult, QueryContext, RefreshReason>
128
+ results$: Thunk<TResult, ReactivityGraphContext, RefreshReason>
100
129
 
101
130
  label: string
102
131
 
103
132
  queryInfo: TQueryInfo
104
133
 
105
- protected reactivityGraph
134
+ readonly reactivityGraph
106
135
 
107
136
  private mapResult: (rows: TResultSchema) => TResult
108
137
 
@@ -117,17 +146,18 @@ export class LiveStoreDbQuery<
117
146
  label?: string
118
147
  queryInput:
119
148
  | QueryInput<TResultSchema, ReadonlyArray<any>, TQueryInfo>
120
- | ((get: GetAtomResult, ctx: QueryContext) => QueryInput<TResultSchema, ReadonlyArray<any>, TQueryInfo>)
121
- reactivityGraph?: ReactivityGraph
149
+ | ((get: GetAtomResult, ctx: ReactivityGraphContext) => QueryInput<TResultSchema, ReadonlyArray<any>, TQueryInfo>)
150
+ reactivityGraph: ReactivityGraph
122
151
  map?: (rows: TResultSchema) => TResult
123
152
  queryInfo?: TQueryInfo
153
+ /** Only used for the initial query execution */
124
154
  otelContext?: otel.Context
125
155
  }) {
126
156
  super()
127
157
 
128
158
  let label = inputLabel ?? 'db(unknown)'
129
159
  let queryInfo = inputQueryInfo ?? ({ _tag: 'None' } as TQueryInfo)
130
- this.reactivityGraph = reactivityGraph ?? globalReactivityGraph
160
+ this.reactivityGraph = reactivityGraph
131
161
 
132
162
  this.mapResult = map === undefined ? (rows: any) => rows as TResult : map
133
163
 
@@ -136,13 +166,15 @@ export class LiveStoreDbQuery<
136
166
  typeof queryInput === 'function' ? undefined : isQueryBuilder(queryInput) ? undefined : queryInput.schema,
137
167
  }
138
168
 
139
- const execBeforeFirstRunRef: { current: ((ctx: QueryContext, otelContext: otel.Context) => void) | undefined } = {
169
+ const execBeforeFirstRunRef: {
170
+ current: ((ctx: ReactivityGraphContext, otelContext: otel.Context) => void) | undefined
171
+ } = {
140
172
  current: undefined,
141
173
  }
142
174
 
143
175
  type TQueryInputRaw = QueryInputRaw<any, any, QueryInfo>
144
176
 
145
- let queryInputRaw$OrQueryInputRaw: TQueryInputRaw | Thunk<TQueryInputRaw, QueryContext, RefreshReason>
177
+ let queryInputRaw$OrQueryInputRaw: TQueryInputRaw | Thunk<TQueryInputRaw, ReactivityGraphContext, RefreshReason>
146
178
 
147
179
  const fromQueryBuilder = (qb: QueryBuilder.Any, otelContext: otel.Context | undefined) => {
148
180
  try {
@@ -156,7 +188,7 @@ export class LiveStoreDbQuery<
156
188
  schema,
157
189
  bindValues: qbRes.bindValues,
158
190
  queriedTables: new Set([ast.tableDef.sqliteDef.name]),
159
- queryInfo: ast._tag === 'RowQuery' ? { _tag: 'Row', table: ast.tableDef, id: ast.id } : { _tag: 'None' },
191
+ queryInfo: queryInfoFromQueryBuilder(qb),
160
192
  } satisfies TQueryInputRaw,
161
193
  label: ast._tag === 'RowQuery' ? rowQueryLabel(ast.tableDef, ast.id) : qb.toString(),
162
194
  execBeforeFirstRun:
@@ -178,7 +210,10 @@ export class LiveStoreDbQuery<
178
210
  queryInputRaw$OrQueryInputRaw = this.reactivityGraph.makeThunk(
179
211
  (get, setDebugInfo, ctx, otelContext) => {
180
212
  const startMs = performance.now()
181
- const queryInputResult = queryInput(makeGetAtomResult(get, otelContext ?? ctx.rootOtelContext), ctx)
213
+ const queryInputResult = queryInput(
214
+ makeGetAtomResult(get, ctx, otelContext ?? ctx.rootOtelContext, this.dependencyQueriesRef),
215
+ ctx,
216
+ )
182
217
  const durationMs = performance.now() - startMs
183
218
 
184
219
  let queryInputRaw: TQueryInputRaw
@@ -210,6 +245,8 @@ export class LiveStoreDbQuery<
210
245
  equal: (a, b) => a.query === b.query && deepEqual(a.bindValues, b.bindValues),
211
246
  },
212
247
  )
248
+
249
+ this.queryInput$ = queryInputRaw$OrQueryInputRaw
213
250
  } else {
214
251
  let queryInputRaw: TQueryInputRaw
215
252
  if (isQueryBuilder(queryInput)) {
@@ -253,10 +290,16 @@ export class LiveStoreDbQuery<
253
290
  : undefined
254
291
 
255
292
  const results$ = this.reactivityGraph.makeThunk<TResult>(
256
- (get, setDebugInfo, queryContext, otelContext) =>
293
+ (get, setDebugInfo, queryContext, otelContext, debugRefreshReason) =>
257
294
  queryContext.otelTracer.startActiveSpan(
258
295
  'db:...', // NOTE span name will be overridden further down
259
- {},
296
+ {
297
+ attributes: {
298
+ 'livestore.debugRefreshReason': Predicate.hasProperty(debugRefreshReason, 'label')
299
+ ? (debugRefreshReason.label as string)
300
+ : debugRefreshReason?._tag,
301
+ },
302
+ },
260
303
  otelContext ?? queryContext.rootOtelContext,
261
304
  (span) => {
262
305
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
@@ -268,14 +311,14 @@ export class LiveStoreDbQuery<
268
311
  }
269
312
 
270
313
  const queryInputResult = isThunk(queryInputRaw$OrQueryInputRaw)
271
- ? (get(queryInputRaw$OrQueryInputRaw, otelContext) as TQueryInputRaw)
314
+ ? (get(queryInputRaw$OrQueryInputRaw, otelContext, debugRefreshReason) as TQueryInputRaw)
272
315
  : (queryInputRaw$OrQueryInputRaw as TQueryInputRaw)
273
316
 
274
317
  const sqlString = queryInputResult.query
275
318
  const bindValues = queryInputResult.bindValues
276
319
 
277
320
  if (queriedTablesRef.current === undefined) {
278
- queriedTablesRef.current = store.syncDbWrapper.getTablesUsed(sqlString)
321
+ queriedTablesRef.current = store.sqliteDbWrapper.getTablesUsed(sqlString)
279
322
  }
280
323
 
281
324
  if (bindValues !== undefined) {
@@ -285,17 +328,20 @@ export class LiveStoreDbQuery<
285
328
  // Establish a reactive dependency on the tables used in the query
286
329
  for (const tableName of queriedTablesRef.current) {
287
330
  const tableRef = store.tableRefs[tableName] ?? shouldNeverHappen(`No table ref found for ${tableName}`)
288
- get(tableRef, otelContext)
331
+ get(tableRef, otelContext, debugRefreshReason)
289
332
  }
290
333
 
291
334
  span.setAttribute('sql.query', sqlString)
292
335
  span.updateName(`db:${sqlString.slice(0, 50)}`)
293
336
 
294
- const rawDbResults = store.syncDbWrapper.select<any>(sqlString, {
295
- queriedTables: queriedTablesRef.current,
296
- bindValues: bindValues ? prepareBindValues(bindValues, sqlString) : undefined,
297
- otelContext,
298
- })
337
+ const rawDbResults = store.sqliteDbWrapper.select<any>(
338
+ sqlString,
339
+ bindValues ? prepareBindValues(bindValues, sqlString) : undefined,
340
+ {
341
+ queriedTables: queriedTablesRef.current,
342
+ otelContext,
343
+ },
344
+ )
299
345
 
300
346
  span.setAttribute('sql.rowsCount', rawDbResults.length)
301
347
 
@@ -346,10 +392,21 @@ Result:`,
346
392
  }
347
393
 
348
394
  destroy = () => {
395
+ this.isDestroyed = true
396
+
349
397
  if (this.queryInput$ !== undefined) {
350
398
  this.reactivityGraph.destroyNode(this.queryInput$)
351
399
  }
352
400
 
353
401
  this.reactivityGraph.destroyNode(this.results$)
402
+
403
+ for (const query of this.dependencyQueriesRef) {
404
+ query.deref()
405
+ }
354
406
  }
355
407
  }
408
+
409
+ const queryInfoFromQueryBuilder = (qb: QueryBuilder.Any): QueryInfo.Row | QueryInfo.None => {
410
+ const ast = qb[QueryBuilderAstSymbol]
411
+ return ast._tag === 'RowQuery' ? { _tag: 'Row', table: ast.tableDef, id: ast.id } : { _tag: 'None' }
412
+ }