@livestore/livestore 0.0.19 → 0.0.21

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/README.md +18 -21
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/QueryCache.d.ts +1 -1
  4. package/dist/QueryCache.d.ts.map +1 -1
  5. package/dist/QueryCache.js.map +1 -1
  6. package/dist/__tests__/react/fixture.d.ts +5 -4
  7. package/dist/__tests__/react/fixture.d.ts.map +1 -1
  8. package/dist/__tests__/react/fixture.js +5 -5
  9. package/dist/__tests__/react/fixture.js.map +1 -1
  10. package/dist/__tests__/react/useComponentState.test.d.ts +2 -0
  11. package/dist/__tests__/react/useComponentState.test.d.ts.map +1 -0
  12. package/dist/__tests__/react/useComponentState.test.js +68 -0
  13. package/dist/__tests__/react/useComponentState.test.js.map +1 -0
  14. package/dist/__tests__/react/useLQuery.test.d.ts +2 -0
  15. package/dist/__tests__/react/useLQuery.test.d.ts.map +1 -0
  16. package/dist/__tests__/react/useLQuery.test.js +38 -0
  17. package/dist/__tests__/react/useLQuery.test.js.map +1 -0
  18. package/dist/__tests__/react/useLiveStoreComponent.test.js +4 -9
  19. package/dist/__tests__/react/useLiveStoreComponent.test.js.map +1 -1
  20. package/dist/__tests__/react/useQuery.test.d.ts +2 -0
  21. package/dist/__tests__/react/useQuery.test.d.ts.map +1 -0
  22. package/dist/__tests__/react/useQuery.test.js +33 -0
  23. package/dist/__tests__/react/useQuery.test.js.map +1 -0
  24. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.d.ts +2 -0
  25. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.d.ts.map +1 -0
  26. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.js +38 -0
  27. package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.js.map +1 -0
  28. package/dist/__tests__/reactive.test.js +167 -93
  29. package/dist/__tests__/reactive.test.js.map +1 -1
  30. package/dist/__tests__/reactiveQueries/sql.test.d.ts +2 -0
  31. package/dist/__tests__/reactiveQueries/sql.test.d.ts.map +1 -0
  32. package/dist/__tests__/reactiveQueries/sql.test.js +337 -0
  33. package/dist/__tests__/reactiveQueries/sql.test.js.map +1 -0
  34. package/dist/inMemoryDatabase.d.ts +2 -2
  35. package/dist/inMemoryDatabase.d.ts.map +1 -1
  36. package/dist/index.d.ts +7 -5
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +4 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/react/index.d.ts +3 -3
  41. package/dist/react/index.d.ts.map +1 -1
  42. package/dist/react/index.js +2 -2
  43. package/dist/react/index.js.map +1 -1
  44. package/dist/react/useComponentState.d.ts +50 -0
  45. package/dist/react/useComponentState.d.ts.map +1 -0
  46. package/dist/react/useComponentState.js +248 -0
  47. package/dist/react/useComponentState.js.map +1 -0
  48. package/dist/react/useGlobalQuery.d.ts +3 -0
  49. package/dist/react/useGlobalQuery.d.ts.map +1 -0
  50. package/dist/react/useGlobalQuery.js +26 -0
  51. package/dist/react/useGlobalQuery.js.map +1 -0
  52. package/dist/react/useGraphQL.d.ts +3 -3
  53. package/dist/react/useGraphQL.d.ts.map +1 -1
  54. package/dist/react/useGraphQL.js +10 -8
  55. package/dist/react/useGraphQL.js.map +1 -1
  56. package/dist/react/useLiveStoreComponent.d.ts +6 -6
  57. package/dist/react/useLiveStoreComponent.d.ts.map +1 -1
  58. package/dist/react/useLiveStoreComponent.js +143 -99
  59. package/dist/react/useLiveStoreComponent.js.map +1 -1
  60. package/dist/react/useQuery.d.ts +2 -2
  61. package/dist/react/useQuery.d.ts.map +1 -1
  62. package/dist/react/useQuery.js +26 -22
  63. package/dist/react/useQuery.js.map +1 -1
  64. package/dist/react/useTemporaryQuery.d.ts +8 -0
  65. package/dist/react/useTemporaryQuery.d.ts.map +1 -0
  66. package/dist/react/useTemporaryQuery.js +17 -0
  67. package/dist/react/useTemporaryQuery.js.map +1 -0
  68. package/dist/react/utils/extractNamesFromStackTrace.d.ts +3 -0
  69. package/dist/react/utils/extractNamesFromStackTrace.d.ts.map +1 -0
  70. package/dist/react/utils/extractNamesFromStackTrace.js +40 -0
  71. package/dist/react/utils/extractNamesFromStackTrace.js.map +1 -0
  72. package/dist/react/utils/extractStackInfoFromStackTrace.d.ts +7 -0
  73. package/dist/react/utils/extractStackInfoFromStackTrace.d.ts.map +1 -0
  74. package/dist/react/utils/extractStackInfoFromStackTrace.js +40 -0
  75. package/dist/react/utils/extractStackInfoFromStackTrace.js.map +1 -0
  76. package/dist/reactive.d.ts +42 -48
  77. package/dist/reactive.d.ts.map +1 -1
  78. package/dist/reactive.js +293 -186
  79. package/dist/reactive.js.map +1 -1
  80. package/dist/reactiveQueries/base-class.d.ts +28 -23
  81. package/dist/reactiveQueries/base-class.d.ts.map +1 -1
  82. package/dist/reactiveQueries/base-class.js +25 -18
  83. package/dist/reactiveQueries/base-class.js.map +1 -1
  84. package/dist/reactiveQueries/graph.d.ts +10 -0
  85. package/dist/reactiveQueries/graph.d.ts.map +1 -0
  86. package/dist/reactiveQueries/graph.js +6 -0
  87. package/dist/reactiveQueries/graph.js.map +1 -0
  88. package/dist/reactiveQueries/graphql.d.ts +34 -17
  89. package/dist/reactiveQueries/graphql.d.ts.map +1 -1
  90. package/dist/reactiveQueries/graphql.js +91 -10
  91. package/dist/reactiveQueries/graphql.js.map +1 -1
  92. package/dist/reactiveQueries/js.d.ts +16 -12
  93. package/dist/reactiveQueries/js.d.ts.map +1 -1
  94. package/dist/reactiveQueries/js.js +31 -8
  95. package/dist/reactiveQueries/js.js.map +1 -1
  96. package/dist/reactiveQueries/sql.d.ts +22 -18
  97. package/dist/reactiveQueries/sql.d.ts.map +1 -1
  98. package/dist/reactiveQueries/sql.js +82 -16
  99. package/dist/reactiveQueries/sql.js.map +1 -1
  100. package/dist/store.d.ts +12 -52
  101. package/dist/store.d.ts.map +1 -1
  102. package/dist/store.js +283 -264
  103. package/dist/store.js.map +1 -1
  104. package/package.json +4 -3
  105. package/src/QueryCache.ts +1 -1
  106. package/src/__tests__/react/fixture.tsx +12 -7
  107. package/src/__tests__/react/{useLiveStoreComponent.test.tsx → useComponentState.test.tsx} +9 -20
  108. package/src/__tests__/react/useQuery.test.tsx +48 -0
  109. package/src/__tests__/react/utils/extractStackInfoFromStackTrace.test.ts +40 -0
  110. package/src/__tests__/reactive.test.ts +193 -140
  111. package/src/__tests__/reactiveQueries/sql.test.ts +372 -0
  112. package/src/inMemoryDatabase.ts +2 -2
  113. package/src/index.ts +7 -11
  114. package/src/react/index.ts +3 -7
  115. package/src/react/{useLiveStoreComponent.ts → useComponentState.ts} +89 -247
  116. package/src/react/useQuery.ts +29 -27
  117. package/src/react/useTemporaryQuery.ts +21 -0
  118. package/src/react/utils/extractStackInfoFromStackTrace.ts +47 -0
  119. package/src/reactive.ts +385 -268
  120. package/src/reactiveQueries/base-class.ts +60 -44
  121. package/src/reactiveQueries/graph.ts +15 -0
  122. package/src/reactiveQueries/graphql.ts +145 -29
  123. package/src/reactiveQueries/js.ts +53 -20
  124. package/src/reactiveQueries/sql.ts +129 -36
  125. package/src/store.ts +338 -408
  126. package/src/react/useGraphQL.ts +0 -138
@@ -1,5 +1,4 @@
1
- import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'
2
- import type { LiteralUnion, PrettifyFlat } from '@livestore/utils'
1
+ import type { LiteralUnion } from '@livestore/utils'
3
2
  import { omit, shouldNeverHappen } from '@livestore/utils'
4
3
  import { Schema } from '@livestore/utils/effect'
5
4
  import * as otel from '@opentelemetry/api'
@@ -12,63 +11,21 @@ import { v4 as uuid } from 'uuid'
12
11
  import type { ComponentKey } from '../componentKey.js'
13
12
  import { labelForKey, tableNameForComponentKey } from '../componentKey.js'
14
13
  import { migrateTable } from '../migrations.js'
15
- import type { LiveStoreGraphQLQuery } from '../reactiveQueries/graphql.js'
16
- import type { LiveStoreJSQuery } from '../reactiveQueries/js.js'
17
- import type { LiveStoreSQLQuery } from '../reactiveQueries/sql.js'
14
+ import { LiveStoreJSQuery } from '../reactiveQueries/js.js'
15
+ import { LiveStoreSQLQuery } from '../reactiveQueries/sql.js'
18
16
  import { SCHEMA_META_TABLE } from '../schema.js'
19
- import type { BaseGraphQLContext, GetAtomResult, LiveStoreQuery, QueryResult, Store } from '../store.js'
20
- import type { Bindable } from '../util.js'
17
+ import type { BaseGraphQLContext, LiveStoreQuery, Store } from '../store.js'
21
18
  import { sql } from '../util.js'
22
19
  import { useStore } from './LiveStoreContext.js'
20
+ import { extractStackInfoFromStackTrace, originalStackLimit } from './utils/extractStackInfoFromStackTrace.js'
23
21
  import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInput.js'
24
22
 
25
23
  export interface QueryDefinitions {
26
24
  [queryName: string]: LiveStoreQuery
27
25
  }
28
26
 
29
- export type QueryResults<TQuery> = { [queryName in keyof TQuery]: PrettifyFlat<QueryResult<TQuery[queryName]>> }
30
-
31
- export type ReactiveSQL = <TResult>(
32
- query: string | ((get: GetAtomResult) => string),
33
- queriedTables: string[],
34
- bindValues?: Bindable | undefined,
35
- ) => LiveStoreSQLQuery<TResult>
36
-
37
- export type ReactiveJS = <TResult>(query: (get: GetAtomResult) => TResult) => LiveStoreJSQuery<TResult>
38
-
39
- export type ReactiveGraphQL = <
40
- TResult extends Record<string, any>,
41
- TVariables extends Record<string, any>,
42
- TContext extends BaseGraphQLContext,
43
- >(
44
- query: DocumentNode<TResult, TVariables>,
45
- variableValues: TVariables | ((get: GetAtomResult) => TVariables),
46
- label?: string,
47
- ) => LiveStoreGraphQLQuery<TResult, TVariables, TContext>
48
-
49
- type RegisterSubscription = <TQuery extends LiveStoreQuery>(
50
- query: TQuery,
51
- onNewValue: (value: QueryResult<TQuery>) => void,
52
- onUnsubscribe?: () => void,
53
- ) => void
54
-
55
- type GenQueries<TQueries, TStateResult> = (args: {
56
- rxSQL: ReactiveSQL
57
- rxGraphQL: ReactiveGraphQL
58
- rxJS: ReactiveJS
59
- state$: LiveStoreJSQuery<TStateResult>
60
- /**
61
- * Registers a subscription.
62
- *
63
- * Passed down for some manual subscribing. Use carefully.
64
- */
65
- subscribe: RegisterSubscription
66
- isTemporaryQuery: boolean
67
- }) => TQueries
68
-
69
- export type UseLiveStoreComponentProps<TQueries, TStateColumns extends ComponentColumns> = {
70
- stateSchema?: SqliteDsl.TableDefinition<string, TStateColumns>
71
- queries?: GenQueries<TQueries, SqliteDsl.FromColumns.RowDecoded<TStateColumns>>
27
+ export type UseComponentStateProps<TStateColumns extends ComponentColumns> = {
28
+ schema?: SqliteDsl.TableDefinition<string, TStateColumns>
72
29
  reactDeps?: React.DependencyList
73
30
  componentKey: ComponentKeyConfig
74
31
  }
@@ -119,13 +76,12 @@ export type GetStateTypeEncoded<TTableDef extends SqliteDsl.TableDefinition<any,
119
76
  * @param config.componentKey A function that returns a unique key for this component.
120
77
  * @param config.reactDeps A list of React-level dependencies that will refresh the queries.
121
78
  */
122
- export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQueries extends QueryDefinitions>({
123
- stateSchema: stateSchema_,
124
- queries = () => ({}) as TQueries,
79
+ export const useComponentState = <TStateColumns extends ComponentColumns>({
80
+ schema: stateSchema_,
125
81
  componentKey: componentKeyConfig,
126
82
  reactDeps = [],
127
- }: UseLiveStoreComponentProps<TQueries, TStateColumns>): {
128
- queryResults: QueryResults<TQueries>
83
+ }: UseComponentStateProps<TStateColumns>): {
84
+ state$: LiveStoreJSQuery<SqliteDsl.FromColumns.RowDecoded<TStateColumns>>
129
85
  state: SqliteDsl.FromColumns.RowDecoded<TStateColumns>
130
86
  setState: Setters<SqliteDsl.FromColumns.RowDecoded<TStateColumns>>
131
87
  useLiveStoreJsonState: UseLiveStoreJsonState<SqliteDsl.FromColumns.RowDecoded<TStateColumns>>
@@ -139,7 +95,6 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
139
95
  [stateSchema_],
140
96
  )
141
97
 
142
- // performance.mark('useLiveStoreComponent:start')
143
98
  const componentKey = useComponentKey(componentKeyConfig, reactDeps)
144
99
  const { store } = useStore()
145
100
 
@@ -151,7 +106,7 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
151
106
  if (existingSpan !== undefined) return existingSpan
152
107
 
153
108
  const span = store.otel.tracer.startSpan(
154
- `LiveStore:useLiveStoreComponent:${componentKeyLabel}`,
109
+ `LiveStore:useComponentState:${componentKeyLabel}`,
155
110
  {},
156
111
  store.otel.queriesSpanContext,
157
112
  )
@@ -171,44 +126,6 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
171
126
  [componentKeyLabel, span],
172
127
  )
173
128
 
174
- const generateQueries = React.useCallback(
175
- ({
176
- state$,
177
- otelContext,
178
- registerSubscription,
179
- isTemporaryQuery,
180
- }: {
181
- state$: LiveStoreJSQuery<TComponentState>
182
- otelContext: otel.Context
183
- registerSubscription: RegisterSubscription
184
- isTemporaryQuery: boolean
185
- }) =>
186
- queries({
187
- rxSQL: <T>(
188
- genQuery: string | ((get: GetAtomResult) => string),
189
- queriedTables: string[],
190
- bindValues?: Bindable,
191
- ) => store.querySQL<T>(genQuery, { queriedTables, bindValues, otelContext, componentKey }),
192
- rxGraphQL: <Result extends Record<string, any>, Variables extends Record<string, any>>(
193
- query: DocumentNode<Result, Variables>,
194
- genVariableValues: Variables | ((get: GetAtomResult) => Variables),
195
- label?: string,
196
- ) => store.queryGraphQL(query, genVariableValues, { componentKey, label, otelContext }),
197
- rxJS: <T>(genQuery: (get: GetAtomResult) => T) => store.queryJS(genQuery, { componentKey, otelContext }),
198
- state$,
199
- subscribe: registerSubscription,
200
- isTemporaryQuery,
201
- }),
202
-
203
- // NOTE: we don't include the queries function passed in by the user here;
204
- // the reason is that we don't want to force them to memoize that function.
205
- // Instead, we just assume that the function always has the same contents.
206
- // This makes sense for LiveStore because the component config should be static.
207
- // TODO: document this and consider whether it's the right API surface.
208
- // eslint-disable-next-line react-hooks/exhaustive-deps
209
- [store, componentKey],
210
- )
211
-
212
129
  const defaultComponentState = React.useMemo(() => {
213
130
  const defaultState = (
214
131
  stateSchema === undefined ? {} : mapValues(stateSchema.columns, (c) => c.default)
@@ -225,100 +142,68 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
225
142
  [stateSchema],
226
143
  )
227
144
 
228
- // Step 1:
229
- // Synchronously create state and queries for initial render pass.
230
- // We do this in a temporary query context which cleans up after itself, making it idempotent
231
- // TODO get rid of the temporary query workaround
232
- const { initialComponentState, initialQueryResults } = React.useMemo(() => {
233
- return store.otel.tracer.startActiveSpan('LiveStore:useLiveStoreComponent:initial', {}, otelContext, (span) => {
234
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
235
-
236
- // NOTE `inTempQueryContext` automatically destroys the queries once the callback is done
237
- return store.inTempQueryContext(() => {
238
- try {
239
- // create state query
240
- let state$: LiveStoreJSQuery<TComponentState>
241
- if (stateSchema === undefined) {
242
- // TODO don't set up a query if there's no state schema (keeps the graph more clean)
243
- state$ = store.queryJS(() => ({}), {
244
- componentKey,
245
- otelContext,
246
- }) as unknown as LiveStoreJSQuery<TComponentState>
247
- } else {
248
- const componentTableName = tableNameForComponentKey(componentKey)
249
- const whereClause = componentKey._tag === 'singleton' ? '' : `where id = '${componentKey.id}'`
250
-
251
- // TODO find a better solution for this
252
- if (store.tableRefs[componentTableName] === undefined) {
253
- const schemaHash = SqliteAst.hash(stateSchema.ast)
254
- const res = store.inMemoryDB.select<{ schemaHash: number }>(
255
- sql`SELECT schemaHash FROM ${SCHEMA_META_TABLE} WHERE tableName = '${componentTableName}'`,
256
- )
257
- if (res.length === 0 || res[0]!.schemaHash !== schemaHash) {
258
- migrateTable({ db: store._proxyDb, tableDef: stateSchema.ast, otelContext, schemaHash })
259
- }
145
+ const state$ = React.useMemo(() => {
146
+ console.log('useComponentState make state$', labelForKey(componentKey))
147
+ // create state query
148
+ if (stateSchema === undefined) {
149
+ // TODO don't set up a query if there's no state schema (keeps the graph more clean)
150
+ return new LiveStoreJSQuery({
151
+ fn: () => ({}) as TComponentState,
152
+ label: 'empty-component-state',
153
+ // otelContext,
154
+ // otelTracer: store.otel.tracer,
155
+ })
156
+ } else {
157
+ const componentTableName = tableNameForComponentKey(componentKey)
158
+ const whereClause = componentKey._tag === 'singleton' ? '' : `where id = '${componentKey.id}'`
159
+
160
+ // TODO find a better solution for this
161
+ if (store.tableRefs[componentTableName] === undefined) {
162
+ const schemaHash = SqliteAst.hash(stateSchema.ast)
163
+ const res = store.inMemoryDB.select<{ schemaHash: number }>(
164
+ sql`SELECT schemaHash FROM ${SCHEMA_META_TABLE} WHERE tableName = '${componentTableName}'`,
165
+ )
166
+ if (res.length === 0 || res[0]!.schemaHash !== schemaHash) {
167
+ migrateTable({ db: store._proxyDb, tableDef: stateSchema.ast, otelContext, schemaHash })
168
+ }
260
169
 
261
- store.tableRefs[componentTableName] = store.graph.makeRef(null, {
262
- equal: () => false,
263
- label: componentTableName,
264
- meta: { liveStoreRefType: 'table' },
265
- })
266
- }
267
-
268
- state$ = store
269
- .querySQL(() => sql`select * from ${componentTableName} ${whereClause} limit 1`, {
270
- queriedTables: [componentTableName],
271
- componentKey,
272
- label: `localState:query:${componentKeyLabel}`,
273
- otelContext,
274
- })
275
- // TODO consider to instead of just returning the default value, to write the default component state to the DB
276
- .pipe<TComponentState>((results) =>
277
- results.length === 1
278
- ? Schema.parseSync(componentStateEffectSchema)(results[0]!)
279
- : defaultComponentState,
280
- )
281
- }
282
- const initialComponentState = state$.results$.result
170
+ store.tableRefs[componentTableName] = store.graph.makeRef(null, {
171
+ equal: () => false,
172
+ label: componentTableName,
173
+ meta: { liveStoreRefType: 'table' },
174
+ })
175
+ }
283
176
 
284
- const queries = generateQueries({
285
- state$: state$,
286
- otelContext,
287
- registerSubscription: () => {},
288
- isTemporaryQuery: true,
289
- })
290
- for (const [name, query] of Object.entries(queries)) {
291
- query.label = name
292
- }
293
- const initialQueryResults = mapValues(
294
- queries,
295
- (query) => query.results$.result,
296
- // TODO improve typing
297
- ) as unknown as QueryResults<TQueries>
298
-
299
- return { initialComponentState, initialQueryResults }
300
- } finally {
301
- span.end()
302
- }
303
- })
304
- })
177
+ return (
178
+ new LiveStoreSQLQuery({
179
+ label: `localState:query:${componentKeyLabel}`,
180
+ genQueryString: () => sql`select * from ${componentTableName} ${whereClause} limit 1`,
181
+ queriedTables: [componentTableName],
182
+ })
183
+ // TODO consider to instead of just returning the default value, to write the default component state to the DB
184
+ .pipe<TComponentState>((results) =>
185
+ results.length === 1 ? Schema.parseSync(componentStateEffectSchema)(results[0]!) : defaultComponentState,
186
+ )
187
+ )
188
+ }
305
189
  }, [
306
- store,
307
- otelContext,
308
- stateSchema,
309
- generateQueries,
310
190
  componentKey,
311
191
  componentKeyLabel,
312
192
  componentStateEffectSchema,
313
193
  defaultComponentState,
194
+ otelContext,
195
+ stateSchema,
196
+ store,
314
197
  ])
315
198
 
199
+ // Step 1:
200
+ // Synchronously create state and queries for initial render pass.
201
+ const initialComponentState = React.useMemo(() => state$.run(otelContext), [otelContext, state$])
202
+
316
203
  // Now that we've computed the initial state synchronously,
317
204
  // we can set up our useState calls w/ a default value populated...
318
205
  const [componentStateRef, setComponentState_] = useStateRefWithReactiveInput<TComponentState>(initialComponentState)
319
206
 
320
- const [queryResultsRef, setQueryResults_] = useStateRefWithReactiveInput<QueryResults<TQueries>>(initialQueryResults)
321
-
322
207
  const setState = (
323
208
  stateSchema === undefined
324
209
  ? {}
@@ -355,44 +240,30 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
355
240
  return store.applyEvent('updateComponentState', { componentKey, columnNames, ...columnValues })
356
241
  }
357
242
 
243
+ const subscriptionInfo = React.useMemo(() => {
244
+ Error.stackTraceLimit = 10
245
+ // eslint-disable-next-line unicorn/error-message
246
+ const stack = new Error().stack!
247
+ Error.stackTraceLimit = originalStackLimit
248
+ return { stack: extractStackInfoFromStackTrace(stack) }
249
+ }, [])
250
+
358
251
  // OK, now all the synchronous work is done;
359
252
  // time to set up our long-running queries in an effect
360
253
  React.useEffect(() => {
361
254
  return store.otel.tracer.startActiveSpan(
362
- 'LiveStore:useLiveStoreComponent:long-running',
255
+ 'LiveStore:useComponentState:long-running',
363
256
  { attributes: {} },
364
257
  otelContext,
365
258
  (span) => {
366
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
367
259
  const unsubs: (() => void)[] = []
368
260
 
369
- // create state query
370
- let state$: LiveStoreJSQuery<TComponentState>
371
- if (stateSchema === undefined) {
372
- // TODO remove this query
373
- state$ = store.queryJS(() => ({}) as TComponentState, {
374
- componentKey,
375
- otelContext,
376
- label: 'empty-component-state',
377
- })
378
- } else {
379
- const componentTableName = tableNameForComponentKey(componentKey)
261
+ if (stateSchema !== undefined) {
380
262
  insertRowForComponentInstance({ store, componentKey, stateSchema })
381
-
382
- const whereClause = componentKey._tag === 'singleton' ? '' : `where id = '${componentKey.id}'`
383
- state$ = store
384
- .querySQL<TComponentState>(() => sql`select * from ${componentTableName} ${whereClause} limit 1`, {
385
- queriedTables: [componentTableName],
386
- componentKey,
387
- label: `localState:query:${componentKeyLabel}`,
388
- otelContext,
389
- })
390
- // TODO consider to instead of just returning the default value, to write the default component state to the DB
391
- .pipe<TComponentState>((results) =>
392
- results.length === 1 ? Schema.parseSync(componentStateEffectSchema)(results[0]!) : defaultComponentState,
393
- )
394
263
  }
395
264
 
265
+ state$.activeSubscriptions.add(subscriptionInfo)
266
+
396
267
  unsubs.push(
397
268
  store.subscribe(
398
269
  state$,
@@ -402,44 +273,11 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
402
273
  }
403
274
  },
404
275
  undefined,
405
- { label: `useLiveStoreComponent:localState:subscribe:${state$.label}` },
276
+ { label: `useComponentState:localState:subscribe:${state$.label}` },
406
277
  ),
278
+ () => state$.activeSubscriptions.delete(subscriptionInfo),
407
279
  )
408
280
 
409
- const registerSubscription: RegisterSubscription = (query$, callback, onUnsubscribe) => {
410
- unsubs.push(
411
- store.subscribe(
412
- query$,
413
- (results) => {
414
- callback(results)
415
- },
416
- onUnsubscribe,
417
- { label: `useLiveStoreComponent:query:manual-subscribe:${query$.label}` },
418
- ),
419
- )
420
- }
421
-
422
- const queries = generateQueries({ state$, otelContext, registerSubscription, isTemporaryQuery: false })
423
-
424
- for (const [key, query] of Object.entries(queries)) {
425
- // Use the field name given to this query in the useQueries hook as its label
426
- query.label = key
427
-
428
- unsubs.push(
429
- store.subscribe(
430
- query,
431
- (results) => {
432
- const newQueryResults = { ...queryResultsRef.current, [key]: results }
433
- if (isEqual(newQueryResults, queryResultsRef.current) === false) {
434
- setQueryResults_(newQueryResults)
435
- }
436
- },
437
- undefined,
438
- { label: `useLiveStoreComponent:query:subscribe:${query.label}` },
439
- ),
440
- )
441
- }
442
-
443
281
  return () => {
444
282
  for (const unsub of unsubs) {
445
283
  unsub()
@@ -451,24 +289,28 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
451
289
  )
452
290
  // NOTE excluding `setComponentState_` and `setQueryResults_` from the deps array as it seems to cause an infinite loop
453
291
  // This should probably be improved
454
- // eslint-disable-next-line react-hooks/exhaustive-deps
292
+ // TODO is this still true?
293
+ // // eslint-disable-next-line react-hooks/exhaustive-deps
455
294
  }, [
456
295
  store,
457
- componentKey,
296
+ subscriptionInfo,
458
297
  stateSchema,
459
298
  defaultComponentState,
460
- generateQueries,
461
299
  otelContext,
462
300
  componentStateRef,
463
- // setComponentState_,
464
- // setQueryResults_,
301
+ state$,
302
+ setComponentState_,
303
+ componentKey,
465
304
  ])
466
305
 
467
306
  // Very important: remove any queries / other resources associated w/ this component
468
- React.useEffect(() => () => store.unmountComponent(componentKey), [store, componentKey])
469
-
470
- // performance.mark('useLiveStoreComponent:end')
471
- // performance.measure(`useLiveStoreComponent:${componentKey.type}`, 'useLiveStoreComponent:start', 'useLiveStoreComponent:end')
307
+ React.useEffect(
308
+ () => () => {
309
+ console.log('useComponentState destroy', labelForKey(componentKey))
310
+ return state$.destroy()
311
+ },
312
+ [state$],
313
+ )
472
314
 
473
315
  const state = componentStateRef.current
474
316
 
@@ -498,7 +340,7 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
498
340
  }
499
341
 
500
342
  return {
501
- queryResults: queryResultsRef.current,
343
+ state$,
502
344
  state,
503
345
  setState,
504
346
  useLiveStoreJsonState,
@@ -1,56 +1,58 @@
1
+ import { isEqual } from 'lodash-es'
1
2
  import React from 'react'
2
3
 
3
- import { labelForKey } from '../componentKey.js'
4
- import type { QueryDefinition } from '../effect/LiveStore.js'
5
- import type { LiveStoreQuery, QueryResult, Store } from '../store.js'
4
+ import type { ILiveStoreQuery } from '../reactiveQueries/base-class.js'
6
5
  import { useStore } from './LiveStoreContext.js'
6
+ import { extractStackInfoFromStackTrace, originalStackLimit } from './utils/extractStackInfoFromStackTrace.js'
7
+ import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInput.js'
7
8
 
8
- // TODO get rid of the query cache in favour of the new side-effect-free query definition approach https://www.notion.so/schickling/New-query-definition-approach-1097a78ef0e9495bac25f90417374756?pvs=4
9
- const queryCache = new Map<QueryDefinition, LiveStoreQuery>()
10
-
11
- export const useQuery = <Q extends LiveStoreQuery>(queryDef: (store: Store) => Q): QueryResult<Q> => {
9
+ export const useQuery = <TResult>(query: ILiveStoreQuery<TResult>): TResult => {
12
10
  const { store } = useStore()
13
- const query = React.useMemo(() => {
14
- if (queryCache.has(queryDef)) return queryCache.get(queryDef) as Q
15
11
 
16
- const query = queryDef(store)
17
- queryCache.set(queryDef, query)
18
- return query
19
- }, [store, queryDef])
12
+ // TODO proper otel context
13
+ const initialResult = React.useMemo(() => query.run(), [query])
20
14
 
21
15
  // We know the query has a result by the time we use it; so we can synchronously populate a default state
22
- const [value, setValue] = React.useState<QueryResult<Q>>(query.results$.result)
16
+ const [valueRef, setValue] = useStateRefWithReactiveInput<TResult>(initialResult)
17
+
18
+ const subscriptionInfo = React.useMemo(() => {
19
+ Error.stackTraceLimit = 10
20
+ // eslint-disable-next-line unicorn/error-message
21
+ const stack = new Error().stack!
22
+ Error.stackTraceLimit = originalStackLimit
23
+ return { stack: extractStackInfoFromStackTrace(stack) }
24
+ }, [])
23
25
 
24
26
  // Subscribe to future updates for this query
25
27
  React.useEffect(() => {
26
28
  return store.otel.tracer.startActiveSpan(
27
- `LiveStore:useQuery:${labelForKey(query.componentKey)}:${query.label}`,
29
+ `LiveStore:useQuery:${query.label}`,
30
+ // `LiveStore:useQuery:${labelForKey(query.componentKey)}:${query.label}`,
28
31
  { attributes: { label: query.label } },
29
- query.otelContext,
32
+ store.otel.queriesSpanContext,
30
33
  (span) => {
31
- const cancel = store.subscribe(
34
+ query.activeSubscriptions.add(subscriptionInfo)
35
+ const unsub = store.subscribe(
32
36
  query,
33
- (v) => {
37
+ (newValue) => {
34
38
  // NOTE: we return a reference to the result object within LiveStore;
35
39
  // this implies that app code must not mutate the results, or else
36
40
  // there may be weird reactivity bugs.
37
- return setValue(v)
41
+ if (isEqual(newValue, valueRef.current) === false) {
42
+ setValue(newValue)
43
+ }
38
44
  },
39
45
  undefined,
40
46
  { label: query.label },
41
47
  )
42
48
  return () => {
43
- // // NOTE destroying the whole query will also unsubscribe it
44
- // query.destroy()
45
-
46
- // TODO for now we'll still `cancel` manually, but we should remove this once we have some kind of
47
- // ARC-based system
48
- cancel()
49
+ query.activeSubscriptions.delete(subscriptionInfo)
50
+ unsub()
49
51
  span.end()
50
52
  }
51
53
  },
52
54
  )
53
- }, [query, store])
55
+ }, [subscriptionInfo, query, setValue, store, valueRef])
54
56
 
55
- return value
57
+ return valueRef.current
56
58
  }
@@ -0,0 +1,21 @@
1
+ import React from 'react'
2
+
3
+ import type { ILiveStoreQuery } from '../reactiveQueries/base-class.js'
4
+ import { useQuery } from './useQuery.js'
5
+
6
+ /**
7
+ * Creates a query, subscribes and destroys it when the component unmounts.
8
+ *
9
+ * Make sure `makeQuery` is a memoized function.
10
+ */
11
+ export const useTemporaryQuery = <TResult>(makeQuery: () => ILiveStoreQuery<TResult>): TResult => {
12
+ const query = React.useMemo(() => makeQuery(), [makeQuery])
13
+
14
+ React.useEffect(() => {
15
+ return () => {
16
+ query.destroy()
17
+ }
18
+ }, [query])
19
+
20
+ return useQuery(query)
21
+ }
@@ -0,0 +1,47 @@
1
+ export const originalStackLimit = Error.stackTraceLimit
2
+
3
+ export type StackInfo = {
4
+ name: string
5
+ filePath: string
6
+ }
7
+
8
+ /*
9
+ Example stack trace:
10
+
11
+ Error
12
+ at https://localhost:8081/@fs/Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/livestore/dist/react/useQuery.js?t=1699550216884:18:23
13
+ at mountMemo (https://localhost:8081/node_modules/.vite-web/deps/chunk-M23HUTQV.js?v=3eb66ed6:12817:27)
14
+ at Object.useMemo (https://localhost:8081/node_modules/.vite-web/deps/chunk-M23HUTQV.js?v=3eb66ed6:13141:24)
15
+ at Object.useMemo (https://localhost:8081/node_modules/.vite-web/deps/chunk-4WADDZ2G.js?v=3eb66ed6:1094:29)
16
+ at useQuery (https://localhost:8081/@fs/Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/livestore/dist/react/useQuery.js?t=1699550216884:13:33)
17
+ at useAppState (https://localhost:8081/src/db/AppState.ts?t=1699550216884:17:34)
18
+ at useRoute (https://localhost:8081/src/db/AppState.ts?t=1699550216884:74:22)
19
+ at RouteLink (https://localhost:8081/src/components/Link.tsx?t=1699550216884:36:7)
20
+ at renderWithHooks (https://localhost:8081/node_modules/.vite-web/deps/chunk-M23HUTQV.js?v=3eb66ed6:12171:26)
21
+ at mountIndeterminateComponent (https://localhost:8081/node_modules/.vite-web/deps/chunk-M23HUTQV.js?v=3eb66ed6:14921:21)
22
+
23
+ Approach:
24
+ - Start filtering at `at useQuery` (including)
25
+ - Stop filtering at `at renderWithHooks` (excluding)
26
+ */
27
+ export const extractStackInfoFromStackTrace = (stackTrace: string): StackInfo[] => {
28
+ const namePattern = /at (\S+) \((.+)\)/g
29
+ let match: RegExpExecArray | null
30
+ const stackInfoArr: StackInfo[] = []
31
+ let hasReachedStart = false
32
+
33
+ while ((match = namePattern.exec(stackTrace)) !== null) {
34
+ const [, name, filePath] = match as any as [string, string, string]
35
+ if (name.startsWith('use')) {
36
+ hasReachedStart = true
37
+
38
+ stackInfoArr.unshift({ name, filePath })
39
+ } else if (hasReachedStart) {
40
+ // We've reached the end of the `use*` functions, so we're adding the component name and stop
41
+ stackInfoArr.unshift({ name, filePath })
42
+ break
43
+ }
44
+ }
45
+
46
+ return stackInfoArr
47
+ }