@livestore/livestore 0.0.0-snapshot-d9d66b354a9f4cfae987725d38971992ff14e4ad → 0.0.0-snapshot-1d99fea7d2ce2c7a5d9ed0a3752f8a7bda6bc3db

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