@livestore/solid 0.4.0-dev.22 → 0.4.0-dev.23

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 (94) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/StoreRegistryContext.d.ts +56 -0
  3. package/dist/StoreRegistryContext.d.ts.map +1 -0
  4. package/dist/StoreRegistryContext.jsx +60 -0
  5. package/dist/StoreRegistryContext.jsx.map +1 -0
  6. package/dist/__tests__/fixture.d.ts +14 -0
  7. package/dist/__tests__/fixture.d.ts.map +1 -0
  8. package/dist/__tests__/fixture.jsx +13 -0
  9. package/dist/__tests__/fixture.jsx.map +1 -0
  10. package/dist/experimental/components/LiveList.d.ts +24 -0
  11. package/dist/experimental/components/LiveList.d.ts.map +1 -0
  12. package/dist/experimental/components/LiveList.jsx +24 -0
  13. package/dist/experimental/components/LiveList.jsx.map +1 -0
  14. package/dist/experimental/mod.d.ts +2 -0
  15. package/dist/experimental/mod.d.ts.map +1 -0
  16. package/dist/experimental/mod.js +2 -0
  17. package/dist/experimental/mod.js.map +1 -0
  18. package/dist/mod.d.ts +6 -2
  19. package/dist/mod.d.ts.map +1 -1
  20. package/dist/mod.js +4 -2
  21. package/dist/mod.js.map +1 -1
  22. package/dist/useClientDocument.client.test.d.ts +2 -0
  23. package/dist/useClientDocument.client.test.d.ts.map +1 -0
  24. package/dist/useClientDocument.client.test.jsx +177 -0
  25. package/dist/useClientDocument.client.test.jsx.map +1 -0
  26. package/dist/useClientDocument.d.ts +71 -0
  27. package/dist/useClientDocument.d.ts.map +1 -0
  28. package/dist/useClientDocument.js +74 -0
  29. package/dist/useClientDocument.js.map +1 -0
  30. package/dist/useClientDocument.server.test.d.ts +6 -0
  31. package/dist/useClientDocument.server.test.d.ts.map +1 -0
  32. package/dist/useClientDocument.server.test.jsx +76 -0
  33. package/dist/useClientDocument.server.test.jsx.map +1 -0
  34. package/dist/useQuery.client.test.d.ts +2 -0
  35. package/dist/useQuery.client.test.d.ts.map +1 -0
  36. package/dist/useQuery.client.test.jsx +165 -0
  37. package/dist/useQuery.client.test.jsx.map +1 -0
  38. package/dist/useQuery.d.ts +32 -0
  39. package/dist/useQuery.d.ts.map +1 -0
  40. package/dist/useQuery.js +64 -0
  41. package/dist/useQuery.js.map +1 -0
  42. package/dist/useQuery.server.test.d.ts +6 -0
  43. package/dist/useQuery.server.test.d.ts.map +1 -0
  44. package/dist/useQuery.server.test.jsx +88 -0
  45. package/dist/useQuery.server.test.jsx.map +1 -0
  46. package/dist/useStore.client.test.d.ts +2 -0
  47. package/dist/useStore.client.test.d.ts.map +1 -0
  48. package/dist/useStore.client.test.jsx +438 -0
  49. package/dist/useStore.client.test.jsx.map +1 -0
  50. package/dist/useStore.d.ts +91 -0
  51. package/dist/useStore.d.ts.map +1 -0
  52. package/dist/useStore.js +94 -0
  53. package/dist/useStore.js.map +1 -0
  54. package/dist/useStore.server.test.d.ts +6 -0
  55. package/dist/useStore.server.test.d.ts.map +1 -0
  56. package/dist/useStore.server.test.jsx +56 -0
  57. package/dist/useStore.server.test.jsx.map +1 -0
  58. package/dist/utils.d.ts +4 -0
  59. package/dist/utils.d.ts.map +1 -0
  60. package/dist/utils.js +7 -0
  61. package/dist/utils.js.map +1 -0
  62. package/dist/whenever.d.ts +32 -0
  63. package/dist/whenever.d.ts.map +1 -0
  64. package/dist/whenever.js +51 -0
  65. package/dist/whenever.js.map +1 -0
  66. package/package.json +64 -16
  67. package/src/StoreRegistryContext.tsx +70 -0
  68. package/src/__snapshots__/useClientDocument.client.test.tsx.snap +570 -0
  69. package/src/__snapshots__/useQuery.client.test.tsx.snap +1550 -0
  70. package/src/__tests__/fixture.tsx +42 -0
  71. package/src/experimental/components/LiveList.tsx +54 -0
  72. package/src/experimental/mod.ts +1 -0
  73. package/src/mod.ts +6 -2
  74. package/src/useClientDocument.client.test.tsx +299 -0
  75. package/src/useClientDocument.server.test.tsx +107 -0
  76. package/src/useClientDocument.ts +146 -0
  77. package/src/useQuery.client.test.tsx +293 -0
  78. package/src/useQuery.server.test.tsx +128 -0
  79. package/src/useQuery.ts +115 -0
  80. package/src/useStore.client.test.tsx +632 -0
  81. package/src/useStore.server.test.tsx +70 -0
  82. package/src/useStore.ts +179 -0
  83. package/src/utils.ts +10 -0
  84. package/src/whenever.ts +80 -0
  85. package/dist/query.d.ts +0 -4
  86. package/dist/query.d.ts.map +0 -1
  87. package/dist/query.js +0 -15
  88. package/dist/query.js.map +0 -1
  89. package/dist/store.d.ts +0 -6
  90. package/dist/store.d.ts.map +0 -1
  91. package/dist/store.js +0 -99
  92. package/dist/store.js.map +0 -1
  93. package/src/query.ts +0 -22
  94. package/src/store.ts +0 -196
@@ -0,0 +1,293 @@
1
+ /** biome-ignore-all lint/a11y: test */
2
+ import * as LiveStore from '@livestore/livestore'
3
+ import { queryDb, signal } from '@livestore/livestore'
4
+ import { RG } from '@livestore/livestore/internal/testing-utils'
5
+ import { Effect, Schema } from '@livestore/utils/effect'
6
+ import { Vitest } from '@livestore/utils-dev/node-vitest'
7
+ import * as SolidTesting from '@solidjs/testing-library'
8
+ import * as Solid from 'solid-js'
9
+ import { expect } from 'vitest'
10
+
11
+ import { events, makeTodoMvcSolid, StoreInternalsSymbol, tables } from './__tests__/fixture.tsx'
12
+
13
+ const makeVirtualContainerStyle = (containerHeight: number) => {
14
+ return { height: `${containerHeight}px`, overflow: 'auto' } as const
15
+ }
16
+
17
+ const makeVirtualContentStyle = (itemCount: number, itemHeight: number) => {
18
+ return { height: `${itemCount * itemHeight}px`, position: 'relative' } as const
19
+ }
20
+
21
+ const makeVirtualItemStyle = (index: number, itemHeight: number) => {
22
+ return {
23
+ position: 'absolute',
24
+ top: `${index * itemHeight}px`,
25
+ height: `${itemHeight}px`,
26
+ } as const
27
+ }
28
+
29
+ Vitest.describe('useQuery', () => {
30
+ Vitest.afterEach(() => {
31
+ RG.__resetIds()
32
+ })
33
+
34
+ Vitest.scopedLive('simple', () =>
35
+ Effect.gen(function* () {
36
+ const { wrapper, store } = yield* makeTodoMvcSolid({})
37
+
38
+ const allTodos$ = queryDb({ query: `select * from todos`, schema: Schema.Array(tables.todos.rowSchema) })
39
+
40
+ const { result } = SolidTesting.renderHook(
41
+ () => {
42
+ return store.useQuery(allTodos$)
43
+ },
44
+ { wrapper },
45
+ )
46
+
47
+ expect(result()?.length).toBe(0)
48
+ expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
49
+
50
+ store.commit(events.todoCreated({ id: 't1', text: 'buy milk', completed: false }))
51
+
52
+ expect(result()?.length).toBe(1)
53
+ expect(result()?.[0]?.text).toBe('buy milk')
54
+ expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
55
+ }),
56
+ )
57
+
58
+ Vitest.scopedLive('same `useQuery` hook invoked with different queries', () =>
59
+ Effect.gen(function* () {
60
+ const { wrapper, store } = yield* makeTodoMvcSolid({})
61
+
62
+ const todo1$ = queryDb(
63
+ { query: `select * from todos where id = 't1'`, schema: Schema.Array(tables.todos.rowSchema) },
64
+ { label: 'libraryTracksView1' },
65
+ )
66
+ const todo2$ = queryDb(
67
+ { query: `select * from todos where id = 't2'`, schema: Schema.Array(tables.todos.rowSchema) },
68
+ { label: 'libraryTracksView2' },
69
+ )
70
+
71
+ store.commit(
72
+ events.todoCreated({ id: 't1', text: 'buy milk', completed: false }),
73
+ events.todoCreated({ id: 't2', text: 'buy eggs', completed: false }),
74
+ )
75
+
76
+ const [todoId, setTodoId] = Solid.createSignal('t1')
77
+
78
+ const { result } = SolidTesting.renderHook(
79
+ () => {
80
+ const query = store.useQuery(() => (todoId() === 't1' ? todo1$ : todo2$))
81
+ return () => query()?.[0]?.text
82
+ },
83
+ { wrapper },
84
+ )
85
+
86
+ expect(result()).toBe('buy milk')
87
+ expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot(
88
+ '1: after first render',
89
+ )
90
+
91
+ store.commit(events.todoUpdated({ id: 't1', text: 'buy soy milk' }))
92
+
93
+ expect(result()).toBe('buy soy milk')
94
+ expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot(
95
+ '2: after first commit',
96
+ )
97
+
98
+ setTodoId('t2')
99
+
100
+ expect(result()).toBe('buy eggs')
101
+ expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot(
102
+ '3: after forced rerender',
103
+ )
104
+ }),
105
+ )
106
+
107
+ Vitest.scopedLive('filtered dependency query', () =>
108
+ Effect.gen(function* () {
109
+ const { wrapper, store } = yield* makeTodoMvcSolid({})
110
+
111
+ const filter$ = signal('t1', { label: 'id-filter' })
112
+
113
+ const todo$ = queryDb((get) => tables.todos.where('id', get(filter$)), { label: 'todo' })
114
+
115
+ store.commit(
116
+ events.todoCreated({ id: 't1', text: 'buy milk', completed: false }),
117
+ events.todoCreated({ id: 't2', text: 'buy eggs', completed: false }),
118
+ )
119
+
120
+ const { result } = SolidTesting.renderHook(
121
+ () => {
122
+ const query = store.useQuery(todo$)
123
+ return () => query()?.[0]?.text
124
+ },
125
+ { wrapper },
126
+ )
127
+
128
+ expect(result()).toBe('buy milk')
129
+
130
+ expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
131
+
132
+ store.commit(events.todoUpdated({ id: 't1', text: 'buy soy milk' }))
133
+
134
+ expect(result()).toBe('buy soy milk')
135
+ expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
136
+
137
+ store.setSignal(filter$, 't2')
138
+
139
+ expect(result()).toBe('buy eggs')
140
+ expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
141
+ }),
142
+ )
143
+
144
+ Vitest.scopedLive('should work for a dynamic list with query swapping', () =>
145
+ Effect.gen(function* () {
146
+ const { wrapper, store } = yield* makeTodoMvcSolid({})
147
+
148
+ const ListItem = (props: { id: number }) => {
149
+ const res = store.useQuery(() =>
150
+ LiveStore.computed(() => props.id, { label: `ListItem.${props.id}`, deps: props.id }),
151
+ )
152
+ return <div role="listitem">{String(res())}</div>
153
+ }
154
+
155
+ const [numItems, setNumItems] = Solid.createSignal(1)
156
+
157
+ const ListWrapper = () => {
158
+ return (
159
+ <div>
160
+ <Solid.For each={Array.from({ length: numItems() }, (_, i) => i).toReversed()}>
161
+ {(id) => <ListItem id={id} />}
162
+ </Solid.For>
163
+ </div>
164
+ )
165
+ }
166
+
167
+ const { container } = SolidTesting.render(() => <ListWrapper />, { wrapper })
168
+
169
+ expect(container.textContent).toBe('0')
170
+
171
+ setNumItems(3)
172
+
173
+ expect(container.textContent).toBe('210')
174
+ }),
175
+ )
176
+
177
+ // NOTE: This test covers special Solid lifecycle patterns similar to react-window
178
+ // It causes query swapping in reactive computations and cleanup calls
179
+ // This tests the `_tag: 'destroyed'` state in the `spanAlreadyStartedCache`
180
+ Vitest.scopedLive('should work for a virtualized list with @solid-primitives/virtual', () =>
181
+ Effect.gen(function* () {
182
+ const { wrapper, store } = yield* makeTodoMvcSolid({})
183
+
184
+ // Create a signal to control the number of items
185
+ const [numItems, setNumItems] = Solid.createSignal(1)
186
+
187
+ const VirtualizedList = () => {
188
+ const itemData = Solid.createMemo(() => Array.from({ length: numItems() }, (_, i) => i).reverse())
189
+
190
+ const containerHeight = 100
191
+ const itemHeight = 10
192
+ const visibleCount = Math.ceil(containerHeight / itemHeight)
193
+ const containerStyle = makeVirtualContainerStyle(containerHeight)
194
+ const contentStyle = Solid.createMemo(() => makeVirtualContentStyle(itemData().length, itemHeight))
195
+
196
+ return (
197
+ <div style={containerStyle}>
198
+ <div style={contentStyle()}>
199
+ <Solid.For each={itemData().slice(0, visibleCount + 1)}>
200
+ {(id, index) => <VirtualListItem id={id} index={index()} itemHeight={itemHeight} />}
201
+ </Solid.For>
202
+ </div>
203
+ </div>
204
+ )
205
+ }
206
+
207
+ const VirtualListItem = (props: { id: number; index: number; itemHeight: number }) => {
208
+ const res = store.useQuery(() =>
209
+ LiveStore.computed(() => props.id, { label: `VirtualListItem.${props.id}`, deps: props.id }),
210
+ )
211
+ const style = makeVirtualItemStyle(props.index, props.itemHeight)
212
+ return (
213
+ <div role="listitem" style={style}>
214
+ {res()}
215
+ </div>
216
+ )
217
+ }
218
+
219
+ const { container } = SolidTesting.render(() => <VirtualizedList />, { wrapper })
220
+
221
+ expect(container.textContent?.trim()).toBe('0')
222
+
223
+ // Test virtualized list update - this causes query swapping similar to react-window
224
+ setNumItems(3)
225
+
226
+ // In Solid, reactivity is synchronous, so we don't need to wait
227
+ expect(container.textContent?.replace(/\s+/g, '')).toBe('210')
228
+ }),
229
+ )
230
+
231
+ Vitest.scopedLive('should work with signal', () =>
232
+ Effect.gen(function* () {
233
+ const { wrapper, store } = yield* makeTodoMvcSolid({})
234
+ const num$ = signal(0)
235
+
236
+ const { result } = SolidTesting.renderHook(() => store.useQuery(num$), { wrapper })
237
+
238
+ expect(result()).toBe(0)
239
+
240
+ store.setSignal(num$, 1)
241
+
242
+ expect(result()).toBe(1)
243
+ }),
244
+ )
245
+
246
+ Vitest.scopedLive('supports query builders directly', () =>
247
+ Effect.gen(function* () {
248
+ const { wrapper, store } = yield* makeTodoMvcSolid({})
249
+
250
+ store.commit(
251
+ events.todoCreated({ id: 't1', text: 'buy milk', completed: false }),
252
+ events.todoCreated({ id: 't2', text: 'buy eggs', completed: true }),
253
+ )
254
+
255
+ const todosWhereIncomplete = tables.todos.where({ completed: false })
256
+
257
+ const { result } = SolidTesting.renderHook(
258
+ () => {
259
+ const todos = store.useQuery(todosWhereIncomplete)
260
+ return () => todos()?.map((todo) => todo.id)
261
+ },
262
+ { wrapper },
263
+ )
264
+
265
+ expect(result()).toEqual(['t1'])
266
+ }),
267
+ )
268
+
269
+ Vitest.scopedLive('union of different result types with useQuery', () =>
270
+ Effect.gen(function* () {
271
+ const { wrapper, store } = yield* makeTodoMvcSolid({})
272
+
273
+ const str$ = signal('hello', { label: 'str' })
274
+ const num$ = signal(123, { label: 'num' })
275
+
276
+ const [useNum, setUseNum] = Solid.createSignal(false)
277
+
278
+ const { result } = SolidTesting.renderHook(
279
+ () => {
280
+ const query$ = Solid.createMemo(() => (useNum() === true ? num$ : str$))
281
+ return store.useQuery(query$)
282
+ },
283
+ { wrapper },
284
+ )
285
+
286
+ expect(result()).toBe('hello')
287
+
288
+ setUseNum(true)
289
+
290
+ expect(result()).toBe(123)
291
+ }),
292
+ )
293
+ })
@@ -0,0 +1,128 @@
1
+ /**
2
+ * SSR tests for useQuery
3
+ * These tests run in node environment with SSR JSX transform using renderToString.
4
+ */
5
+
6
+ import { isServer, renderToString } from 'solid-js/web'
7
+ import { describe, expect, it } from 'vitest'
8
+
9
+ import { provideOtel } from '@livestore/common'
10
+ import { queryDb, signal } from '@livestore/livestore'
11
+ import { Effect, Schema } from '@livestore/utils/effect'
12
+
13
+ import { events, makeTodoMvcSolid, tables } from './__tests__/fixture.tsx'
14
+
15
+ describe('environment', () => {
16
+ it('runs on server', () => {
17
+ // Use 'window' in globalThis to avoid TypeScript error without DOM lib
18
+ expect('window' in globalThis).toBe(false)
19
+ expect(isServer).toBe(true)
20
+ })
21
+ })
22
+
23
+ describe('useQuery SSR', () => {
24
+ it('renders simple query result to string', async () => {
25
+ await Effect.gen(function* () {
26
+ const { store } = yield* makeTodoMvcSolid({})
27
+
28
+ store.commit(events.todoCreated({ id: 't1', text: 'SSR Todo', completed: false }))
29
+
30
+ const allTodos$ = queryDb({ query: `select * from todos`, schema: Schema.Array(tables.todos.rowSchema) })
31
+
32
+ const TodoList = () => {
33
+ const todos = store.useQuery(allTodos$)
34
+ return (
35
+ <ul>
36
+ {todos()?.map((todo) => (
37
+ <li>{todo.text}</li>
38
+ ))}
39
+ </ul>
40
+ )
41
+ }
42
+
43
+ const html = renderToString(() => <TodoList />)
44
+
45
+ expect(html).toContain('SSR Todo')
46
+ expect(html).toContain('<ul')
47
+ expect(html).toContain('<li')
48
+ }).pipe(provideOtel({}), Effect.scoped, Effect.runPromise)
49
+ })
50
+
51
+ it('renders filtered dependency query to string', async () => {
52
+ await Effect.gen(function* () {
53
+ const { store } = yield* makeTodoMvcSolid({})
54
+
55
+ const filter$ = signal('t1', { label: 'id-filter' })
56
+ const todo$ = queryDb((get) => tables.todos.where('id', get(filter$)), { label: 'todo' })
57
+
58
+ store.commit(
59
+ events.todoCreated({ id: 't1', text: 'buy milk', completed: false }),
60
+ events.todoCreated({ id: 't2', text: 'buy eggs', completed: false }),
61
+ )
62
+
63
+ const TodoItem = () => {
64
+ const query = store.useQuery(todo$)
65
+ return <div>{query()?.[0]?.text ?? 'No todo'}</div>
66
+ }
67
+
68
+ const html = renderToString(() => <TodoItem />)
69
+
70
+ expect(html).toContain('buy milk')
71
+ }).pipe(provideOtel({}), Effect.scoped, Effect.runPromise)
72
+ })
73
+
74
+ it('renders signal query to string', async () => {
75
+ await Effect.gen(function* () {
76
+ const { store } = yield* makeTodoMvcSolid({})
77
+ const num$ = signal(42)
78
+
79
+ const Counter = () => {
80
+ const count = store.useQuery(num$)
81
+ return <div>Count: {count()}</div>
82
+ }
83
+
84
+ const html = renderToString(() => <Counter />)
85
+
86
+ expect(html).toContain('Count:')
87
+ expect(html).toContain('42')
88
+ }).pipe(provideOtel({}), Effect.scoped, Effect.runPromise)
89
+ })
90
+
91
+ it('renders multiple todos to string', async () => {
92
+ await Effect.gen(function* () {
93
+ const { store } = yield* makeTodoMvcSolid({})
94
+
95
+ store.commit(
96
+ events.todoCreated({ id: 't1', text: 'First', completed: false }),
97
+ events.todoCreated({ id: 't2', text: 'Second', completed: true }),
98
+ events.todoCreated({ id: 't3', text: 'Third', completed: false }),
99
+ )
100
+
101
+ const allTodos$ = queryDb({ query: `select * from todos`, schema: Schema.Array(tables.todos.rowSchema) })
102
+
103
+ const App = () => {
104
+ const todos = store.useQuery(allTodos$)
105
+ return (
106
+ <div>
107
+ <h1>Todo App</h1>
108
+ <p>Count: {todos()?.length}</p>
109
+ <ul>
110
+ {todos()?.map((todo) => (
111
+ <li>{todo.text}</li>
112
+ ))}
113
+ </ul>
114
+ </div>
115
+ )
116
+ }
117
+
118
+ const html = renderToString(() => <App />)
119
+
120
+ expect(html).toContain('Todo App')
121
+ expect(html).toContain('Count:')
122
+ expect(html).toContain('3')
123
+ expect(html).toContain('First')
124
+ expect(html).toContain('Second')
125
+ expect(html).toContain('Third')
126
+ }).pipe(provideOtel({}), Effect.scoped, Effect.runPromise)
127
+ })
128
+ })
@@ -0,0 +1,115 @@
1
+ import type * as otel from '@opentelemetry/api'
2
+ import * as Solid from 'solid-js'
3
+
4
+ import {
5
+ captureStackInfo,
6
+ computeRcRefKey,
7
+ createQueryResource,
8
+ type NormalizedQueryable,
9
+ normalizeQueryable,
10
+ runInitialQuery,
11
+ } from '@livestore/framework-toolkit'
12
+ import type { LiveQuery, Queryable, Store } from '@livestore/livestore'
13
+ import type { LiveQueries } from '@livestore/livestore/internal'
14
+ import { deepEqual } from '@livestore/utils'
15
+
16
+ import { type AccessorMaybe, resolve } from './utils.ts'
17
+
18
+ /**
19
+ * Returns the result of a query and subscribes to future updates.
20
+ *
21
+ * Example:
22
+ * ```tsx
23
+ * const App = () => {
24
+ * const todos = useQuery(queryDb(tables.todos.query.where({ complete: true })))
25
+ * return <div>{todos.map((todo) => <div key={todo.id}>{todo.title}</div>)}</div>
26
+ * }
27
+ * ```
28
+ */
29
+ export const useQuery = <TQueryable extends Queryable<any>>(
30
+ queryDef: AccessorMaybe<TQueryable>,
31
+ options: { store: Store<any, any> },
32
+ ): Solid.Accessor<Queryable.Result<TQueryable>> => useQueryRef(queryDef, options).valueRef
33
+
34
+ /**
35
+ */
36
+ export const useQueryRef = <TQueryable extends Queryable<any>>(
37
+ queryable: AccessorMaybe<TQueryable>,
38
+ options: {
39
+ store: Store<any, any>
40
+ /** Parent otel context for the query */
41
+ otelContext?: otel.Context
42
+ /** The name of the span to use for the query */
43
+ otelSpanName?: string
44
+ },
45
+ ): {
46
+ valueRef: Solid.Accessor<Queryable.Result<TQueryable>>
47
+ queryRcRef: Solid.Accessor<LiveQueries.RcRef<LiveQuery<Queryable.Result<TQueryable>>>>
48
+ } => {
49
+ type TResult = Queryable.Result<TQueryable>
50
+
51
+ const normalized = Solid.createMemo<NormalizedQueryable<TResult>>(() =>
52
+ normalizeQueryable(resolve(queryable) as Queryable<TResult>),
53
+ )
54
+
55
+ const rcRefKey = Solid.createMemo(() => computeRcRefKey(options.store, normalized()))
56
+
57
+ const stackInfo = captureStackInfo()
58
+
59
+ const resource = Solid.createMemo(
60
+ Solid.on(rcRefKey, () => {
61
+ const _normalized = normalized()
62
+
63
+ const { queryRcRef, span, otelContext } = createQueryResource(options.store, _normalized, stackInfo, {
64
+ otelSpanName: options.otelSpanName,
65
+ otelContext: options.otelContext,
66
+ })
67
+
68
+ const [valueRef, setValueRef] = Solid.createSignal<Queryable.Result<TQueryable>>(
69
+ runInitialQuery(queryRcRef.value, otelContext, stackInfo, 'solid'),
70
+ )
71
+
72
+ queryRcRef.value.activeSubscriptions.add(stackInfo)
73
+
74
+ // Dynamic queries only set their actual label after they've been run the first time,
75
+ // so we're also updating the span name here.
76
+ span.updateName(options.otelSpanName ?? `LiveStore:useQuery:${queryRcRef.value.label}`)
77
+
78
+ const cleanup = options.store.subscribe(
79
+ queryRcRef.value,
80
+ (newValue) => {
81
+ // NOTE: we return a reference to the result object within LiveStore;
82
+ // this implies that app code must not mutate the results, or else
83
+ // there may be weird reactivity bugs.
84
+ if (deepEqual(newValue, valueRef()) === false) {
85
+ setValueRef(newValue)
86
+ }
87
+ },
88
+ {
89
+ onUnsubsubscribe: () => {
90
+ queryRcRef.value.activeSubscriptions.delete(stackInfo)
91
+ },
92
+ label: queryRcRef.value.label,
93
+ otelContext,
94
+ },
95
+ )
96
+
97
+ Solid.onCleanup(() => {
98
+ queryRcRef.deref()
99
+ span.end()
100
+ cleanup()
101
+ })
102
+
103
+ return { valueRef, queryRcRef }
104
+ }),
105
+ )
106
+
107
+ return {
108
+ valueRef() {
109
+ return resource().valueRef()
110
+ },
111
+ queryRcRef() {
112
+ return resource().queryRcRef
113
+ },
114
+ }
115
+ }