@livestore/livestore 0.0.19 → 0.0.22

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 (136) hide show
  1. package/README.md +29 -22
  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 +3 -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__/react/utils/stack-info.test.d.ts +2 -0
  29. package/dist/__tests__/react/utils/stack-info.test.d.ts.map +1 -0
  30. package/dist/__tests__/react/utils/stack-info.test.js +43 -0
  31. package/dist/__tests__/react/utils/stack-info.test.js.map +1 -0
  32. package/dist/__tests__/reactive.test.js +179 -93
  33. package/dist/__tests__/reactive.test.js.map +1 -1
  34. package/dist/__tests__/reactiveQueries/sql.test.d.ts +2 -0
  35. package/dist/__tests__/reactiveQueries/sql.test.d.ts.map +1 -0
  36. package/dist/__tests__/reactiveQueries/sql.test.js +337 -0
  37. package/dist/__tests__/reactiveQueries/sql.test.js.map +1 -0
  38. package/dist/inMemoryDatabase.d.ts +4 -3
  39. package/dist/inMemoryDatabase.d.ts.map +1 -1
  40. package/dist/inMemoryDatabase.js +3 -2
  41. package/dist/inMemoryDatabase.js.map +1 -1
  42. package/dist/index.d.ts +7 -5
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +4 -0
  45. package/dist/index.js.map +1 -1
  46. package/dist/react/index.d.ts +4 -3
  47. package/dist/react/index.d.ts.map +1 -1
  48. package/dist/react/index.js +3 -2
  49. package/dist/react/index.js.map +1 -1
  50. package/dist/react/useComponentState.d.ts +50 -0
  51. package/dist/react/useComponentState.d.ts.map +1 -0
  52. package/dist/react/useComponentState.js +240 -0
  53. package/dist/react/useComponentState.js.map +1 -0
  54. package/dist/react/useGlobalQuery.d.ts +3 -0
  55. package/dist/react/useGlobalQuery.d.ts.map +1 -0
  56. package/dist/react/useGlobalQuery.js +26 -0
  57. package/dist/react/useGlobalQuery.js.map +1 -0
  58. package/dist/react/useGraphQL.d.ts +3 -3
  59. package/dist/react/useGraphQL.d.ts.map +1 -1
  60. package/dist/react/useGraphQL.js +10 -8
  61. package/dist/react/useGraphQL.js.map +1 -1
  62. package/dist/react/useLiveStoreComponent.d.ts +6 -6
  63. package/dist/react/useLiveStoreComponent.d.ts.map +1 -1
  64. package/dist/react/useLiveStoreComponent.js +143 -99
  65. package/dist/react/useLiveStoreComponent.js.map +1 -1
  66. package/dist/react/useQuery.d.ts +2 -2
  67. package/dist/react/useQuery.d.ts.map +1 -1
  68. package/dist/react/useQuery.js +54 -30
  69. package/dist/react/useQuery.js.map +1 -1
  70. package/dist/react/useTemporaryQuery.d.ts +8 -0
  71. package/dist/react/useTemporaryQuery.d.ts.map +1 -0
  72. package/dist/react/useTemporaryQuery.js +19 -0
  73. package/dist/react/useTemporaryQuery.js.map +1 -0
  74. package/dist/react/utils/extractNamesFromStackTrace.d.ts +3 -0
  75. package/dist/react/utils/extractNamesFromStackTrace.d.ts.map +1 -0
  76. package/dist/react/utils/extractNamesFromStackTrace.js +40 -0
  77. package/dist/react/utils/extractNamesFromStackTrace.js.map +1 -0
  78. package/dist/react/utils/extractStackInfoFromStackTrace.d.ts +7 -0
  79. package/dist/react/utils/extractStackInfoFromStackTrace.d.ts.map +1 -0
  80. package/dist/react/utils/extractStackInfoFromStackTrace.js +40 -0
  81. package/dist/react/utils/extractStackInfoFromStackTrace.js.map +1 -0
  82. package/dist/react/utils/stack-info.d.ts +11 -0
  83. package/dist/react/utils/stack-info.d.ts.map +1 -0
  84. package/dist/react/utils/stack-info.js +49 -0
  85. package/dist/react/utils/stack-info.js.map +1 -0
  86. package/dist/reactive.d.ts +51 -67
  87. package/dist/reactive.d.ts.map +1 -1
  88. package/dist/reactive.js +138 -220
  89. package/dist/reactive.js.map +1 -1
  90. package/dist/reactiveQueries/base-class.d.ts +28 -21
  91. package/dist/reactiveQueries/base-class.d.ts.map +1 -1
  92. package/dist/reactiveQueries/base-class.js +22 -18
  93. package/dist/reactiveQueries/base-class.js.map +1 -1
  94. package/dist/reactiveQueries/graph.d.ts +10 -0
  95. package/dist/reactiveQueries/graph.d.ts.map +1 -0
  96. package/dist/reactiveQueries/graph.js +6 -0
  97. package/dist/reactiveQueries/graph.js.map +1 -0
  98. package/dist/reactiveQueries/graphql.d.ts +35 -17
  99. package/dist/reactiveQueries/graphql.d.ts.map +1 -1
  100. package/dist/reactiveQueries/graphql.js +86 -10
  101. package/dist/reactiveQueries/graphql.js.map +1 -1
  102. package/dist/reactiveQueries/js.d.ts +17 -12
  103. package/dist/reactiveQueries/js.d.ts.map +1 -1
  104. package/dist/reactiveQueries/js.js +30 -8
  105. package/dist/reactiveQueries/js.js.map +1 -1
  106. package/dist/reactiveQueries/sql.d.ts +28 -18
  107. package/dist/reactiveQueries/sql.d.ts.map +1 -1
  108. package/dist/reactiveQueries/sql.js +79 -16
  109. package/dist/reactiveQueries/sql.js.map +1 -1
  110. package/dist/store.d.ts +35 -61
  111. package/dist/store.d.ts.map +1 -1
  112. package/dist/store.js +77 -272
  113. package/dist/store.js.map +1 -1
  114. package/package.json +4 -3
  115. package/src/QueryCache.ts +1 -1
  116. package/src/__tests__/react/fixture.tsx +10 -8
  117. package/src/__tests__/react/{useLiveStoreComponent.test.tsx → useComponentState.test.tsx} +9 -20
  118. package/src/__tests__/react/useQuery.test.tsx +48 -0
  119. package/src/__tests__/react/utils/stack-info.test.ts +45 -0
  120. package/src/__tests__/reactive.test.ts +212 -140
  121. package/src/__tests__/reactiveQueries/sql.test.ts +372 -0
  122. package/src/inMemoryDatabase.ts +11 -8
  123. package/src/index.ts +7 -11
  124. package/src/react/index.ts +4 -7
  125. package/src/react/{useLiveStoreComponent.ts → useComponentState.ts} +90 -253
  126. package/src/react/useQuery.ts +74 -40
  127. package/src/react/useTemporaryQuery.ts +23 -0
  128. package/src/react/utils/stack-info.ts +63 -0
  129. package/src/reactive.ts +234 -308
  130. package/src/reactiveQueries/base-class.ts +59 -42
  131. package/src/reactiveQueries/graph.ts +15 -0
  132. package/src/reactiveQueries/graphql.ts +143 -29
  133. package/src/reactiveQueries/js.ts +57 -20
  134. package/src/reactiveQueries/sql.ts +136 -36
  135. package/src/store.ts +121 -426
  136. 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/stack-info.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,20 +95,27 @@ 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
 
146
101
  const componentKeyLabel = React.useMemo(() => labelForKey(componentKey), [componentKey])
147
102
 
103
+ const stackInfo = React.useMemo(() => {
104
+ Error.stackTraceLimit = 10
105
+ // eslint-disable-next-line unicorn/error-message
106
+ const stack = new Error().stack!
107
+ Error.stackTraceLimit = originalStackLimit
108
+ return extractStackInfoFromStackTrace(stack)
109
+ }, [])
110
+
148
111
  // The following `React.useMemo` and `React.useEffect` calls are used to start and end a span for the lifetime of this component.
149
112
  const { span, otelContext } = React.useMemo(() => {
150
113
  const existingSpan = spanAlreadyStartedCache.get(componentKeyLabel)
151
114
  if (existingSpan !== undefined) return existingSpan
152
115
 
153
116
  const span = store.otel.tracer.startSpan(
154
- `LiveStore:useLiveStoreComponent:${componentKeyLabel}`,
155
- {},
117
+ `LiveStore:useComponentState:${componentKeyLabel}`,
118
+ { attributes: { stackInfo: JSON.stringify(stackInfo) } },
156
119
  store.otel.queriesSpanContext,
157
120
  )
158
121
 
@@ -161,7 +124,7 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
161
124
  spanAlreadyStartedCache.set(componentKeyLabel, { span, otelContext })
162
125
 
163
126
  return { span, otelContext }
164
- }, [componentKeyLabel, store.otel.queriesSpanContext, store.otel.tracer])
127
+ }, [componentKeyLabel, stackInfo, store.otel.queriesSpanContext, store.otel.tracer])
165
128
 
166
129
  React.useEffect(
167
130
  () => () => {
@@ -171,44 +134,6 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
171
134
  [componentKeyLabel, span],
172
135
  )
173
136
 
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
137
  const defaultComponentState = React.useMemo(() => {
213
138
  const defaultState = (
214
139
  stateSchema === undefined ? {} : mapValues(stateSchema.columns, (c) => c.default)
@@ -225,100 +150,70 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
225
150
  [stateSchema],
226
151
  )
227
152
 
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
- }
153
+ const state$ = React.useMemo(() => {
154
+ // create state query
155
+ if (stateSchema === undefined) {
156
+ // TODO don't set up a query if there's no state schema (keeps the graph more clean)
157
+ return new LiveStoreJSQuery({
158
+ fn: () => ({}) as TComponentState,
159
+ label: 'empty-component-state',
160
+ // otelContext,
161
+ // otelTracer: store.otel.tracer,
162
+ })
163
+ } else {
164
+ const componentTableName = tableNameForComponentKey(componentKey)
165
+ const whereClause = componentKey._tag === 'singleton' ? '' : `where id = '${componentKey.id}'`
166
+
167
+ // TODO find a better solution for this
168
+ if (store.tableRefs[componentTableName] === undefined) {
169
+ const schemaHash = SqliteAst.hash(stateSchema.ast)
170
+ const res = store.inMemoryDB.select<{ schemaHash: number }>(
171
+ sql`SELECT schemaHash FROM ${SCHEMA_META_TABLE} WHERE tableName = '${componentTableName}'`,
172
+ )
173
+ if (res.length === 0 || res[0]!.schemaHash !== schemaHash) {
174
+ migrateTable({ db: store._proxyDb, tableDef: stateSchema.ast, otelContext, schemaHash })
175
+ }
260
176
 
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
177
+ store.tableRefs[componentTableName] = store.graph.makeRef(null, {
178
+ equal: () => false,
179
+ label: componentTableName,
180
+ meta: { liveStoreRefType: 'table' },
181
+ })
182
+ }
283
183
 
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
- })
184
+ return (
185
+ new LiveStoreSQLQuery({
186
+ label: `localState:query:${componentKeyLabel}`,
187
+ genQueryString: () => sql`select * from ${componentTableName} ${whereClause} limit 1`,
188
+ queriedTables: [componentTableName],
189
+ })
190
+ // TODO consider to instead of just returning the default value, to write the default component state to the DB
191
+ .pipe<TComponentState>((results) =>
192
+ results.length === 1 ? Schema.parseSync(componentStateEffectSchema)(results[0]!) : defaultComponentState,
193
+ )
194
+ )
195
+ }
305
196
  }, [
306
- store,
307
- otelContext,
308
- stateSchema,
309
- generateQueries,
310
197
  componentKey,
311
198
  componentKeyLabel,
312
199
  componentStateEffectSchema,
313
200
  defaultComponentState,
201
+ otelContext,
202
+ stateSchema,
203
+ store,
314
204
  ])
315
205
 
206
+ // Step 1:
207
+ // Synchronously create state and queries for initial render pass.
208
+ const initialComponentState = React.useMemo(
209
+ () => state$.run(otelContext, { _tag: 'react', api: 'useComponentState', label: state$.label, stackInfo }),
210
+ [otelContext, stackInfo, state$],
211
+ )
212
+
316
213
  // Now that we've computed the initial state synchronously,
317
214
  // we can set up our useState calls w/ a default value populated...
318
215
  const [componentStateRef, setComponentState_] = useStateRefWithReactiveInput<TComponentState>(initialComponentState)
319
216
 
320
- const [queryResultsRef, setQueryResults_] = useStateRefWithReactiveInput<QueryResults<TQueries>>(initialQueryResults)
321
-
322
217
  const setState = (
323
218
  stateSchema === undefined
324
219
  ? {}
@@ -359,40 +254,19 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
359
254
  // time to set up our long-running queries in an effect
360
255
  React.useEffect(() => {
361
256
  return store.otel.tracer.startActiveSpan(
362
- 'LiveStore:useLiveStoreComponent:long-running',
257
+ 'LiveStore:useComponentState:long-running',
363
258
  { attributes: {} },
364
259
  otelContext,
365
260
  (span) => {
366
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
367
261
  const unsubs: (() => void)[] = []
368
262
 
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)
380
- 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
- )
263
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
264
+ if (stateSchema !== undefined) {
265
+ insertRowForComponentInstance({ store, componentKey, stateSchema, otelContext })
394
266
  }
395
267
 
268
+ state$.activeSubscriptions.add(stackInfo)
269
+
396
270
  unsubs.push(
397
271
  store.subscribe(
398
272
  state$,
@@ -402,44 +276,11 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
402
276
  }
403
277
  },
404
278
  undefined,
405
- { label: `useLiveStoreComponent:localState:subscribe:${state$.label}` },
279
+ { label: `useComponentState:localState:subscribe:${state$.label}`, otelContext },
406
280
  ),
281
+ () => state$.activeSubscriptions.delete(stackInfo),
407
282
  )
408
283
 
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
284
  return () => {
444
285
  for (const unsub of unsubs) {
445
286
  unsub()
@@ -449,26 +290,19 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
449
290
  }
450
291
  },
451
292
  )
452
- // NOTE excluding `setComponentState_` and `setQueryResults_` from the deps array as it seems to cause an infinite loop
453
- // This should probably be improved
454
- // eslint-disable-next-line react-hooks/exhaustive-deps
455
293
  }, [
456
294
  store,
457
- componentKey,
295
+ stackInfo,
458
296
  stateSchema,
459
297
  defaultComponentState,
460
- generateQueries,
461
298
  otelContext,
462
299
  componentStateRef,
463
- // setComponentState_,
464
- // setQueryResults_,
300
+ state$,
301
+ setComponentState_,
302
+ componentKey,
465
303
  ])
466
304
 
467
- // 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')
305
+ React.useEffect(() => () => state$.destroy(), [state$])
472
306
 
473
307
  const state = componentStateRef.current
474
308
 
@@ -498,7 +332,7 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
498
332
  }
499
333
 
500
334
  return {
501
- queryResults: queryResultsRef.current,
335
+ state$,
502
336
  state,
503
337
  setState,
504
338
  useLiveStoreJsonState,
@@ -535,10 +369,12 @@ const insertRowForComponentInstance = ({
535
369
  store,
536
370
  componentKey,
537
371
  stateSchema,
372
+ otelContext,
538
373
  }: {
539
374
  store: Store<BaseGraphQLContext>
540
375
  componentKey: ComponentKey
541
376
  stateSchema: SqliteDsl.TableDefinition<string, SqliteDsl.Columns>
377
+ otelContext: otel.Context
542
378
  }) => {
543
379
  const columnNames = ['id', ...Object.keys(stateSchema.columns)]
544
380
  const columnValues = columnNames.map((name) => `$${name}`).join(', ')
@@ -555,6 +391,7 @@ const insertRowForComponentInstance = ({
555
391
  id: componentKey.id,
556
392
  },
557
393
  [tableName],
394
+ otelContext,
558
395
  )
559
396
  }
560
397
 
@@ -1,56 +1,90 @@
1
+ import * as otel from '@opentelemetry/api'
2
+ import { isEqual } from 'lodash-es'
1
3
  import React from 'react'
2
4
 
3
- import { labelForKey } from '../componentKey.js'
4
- import type { QueryDefinition } from '../effect/LiveStore.js'
5
- import type { LiveStoreQuery, QueryResult, Store } from '../store.js'
5
+ import type { ILiveStoreQuery } from '../reactiveQueries/base-class.js'
6
6
  import { useStore } from './LiveStoreContext.js'
7
+ import { extractStackInfoFromStackTrace, originalStackLimit } from './utils/stack-info.js'
8
+ import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInput.js'
9
+ /**
10
+ * This is needed because the `React.useMemo` call below, can sometimes be called multiple times 🤷,
11
+ * so we need to "cache" the fact that we've already started a span for this component.
12
+ * The map entry is being removed again in the `React.useEffect` call below.
13
+ */
14
+ const spanAlreadyStartedCache = new Map<ILiveStoreQuery<any>, { span: otel.Span; otelContext: otel.Context }>()
7
15
 
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> => {
16
+ export const useQuery = <TResult>(query: ILiveStoreQuery<TResult>): TResult => {
12
17
  const { store } = useStore()
13
- const query = React.useMemo(() => {
14
- if (queryCache.has(queryDef)) return queryCache.get(queryDef) as Q
15
18
 
16
- const query = queryDef(store)
17
- queryCache.set(queryDef, query)
18
- return query
19
- }, [store, queryDef])
19
+ const stackInfo = React.useMemo(() => {
20
+ Error.stackTraceLimit = 10
21
+ // eslint-disable-next-line unicorn/error-message
22
+ const stack = new Error().stack!
23
+ Error.stackTraceLimit = originalStackLimit
24
+ return extractStackInfoFromStackTrace(stack)
25
+ }, [])
26
+
27
+ // The following `React.useMemo` and `React.useEffect` calls are used to start and end a span for the lifetime of this component.
28
+ const { span, otelContext } = React.useMemo(() => {
29
+ const existingSpan = spanAlreadyStartedCache.get(query)
30
+ if (existingSpan !== undefined) return existingSpan
31
+
32
+ const span = store.otel.tracer.startSpan(
33
+ `LiveStore:useQuery:${query.label}`,
34
+ { attributes: { label: query.label, stackInfo: JSON.stringify(stackInfo) } },
35
+ store.otel.queriesSpanContext,
36
+ )
37
+
38
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
39
+
40
+ spanAlreadyStartedCache.set(query, { span, otelContext })
41
+
42
+ return { span, otelContext }
43
+ }, [query, stackInfo, store.otel.queriesSpanContext, store.otel.tracer])
44
+
45
+ const initialResult = React.useMemo(
46
+ () =>
47
+ query.run(otelContext, {
48
+ _tag: 'react',
49
+ api: 'useQuery',
50
+ label: query.label,
51
+ stackInfo,
52
+ }),
53
+ [otelContext, query, stackInfo],
54
+ )
20
55
 
21
56
  // 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)
57
+ const [valueRef, setValue] = useStateRefWithReactiveInput<TResult>(initialResult)
58
+
59
+ React.useEffect(
60
+ () => () => {
61
+ spanAlreadyStartedCache.delete(query)
62
+ span.end()
63
+ },
64
+ [query, span],
65
+ )
23
66
 
24
67
  // Subscribe to future updates for this query
25
68
  React.useEffect(() => {
26
- return store.otel.tracer.startActiveSpan(
27
- `LiveStore:useQuery:${labelForKey(query.componentKey)}:${query.label}`,
28
- { attributes: { label: query.label } },
29
- query.otelContext,
30
- (span) => {
31
- const cancel = store.subscribe(
32
- query,
33
- (v) => {
34
- // NOTE: we return a reference to the result object within LiveStore;
35
- // this implies that app code must not mutate the results, or else
36
- // there may be weird reactivity bugs.
37
- return setValue(v)
38
- },
39
- undefined,
40
- { label: query.label },
41
- )
42
- 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
- span.end()
69
+ query.activeSubscriptions.add(stackInfo)
70
+ const unsub = store.subscribe(
71
+ query,
72
+ (newValue) => {
73
+ // NOTE: we return a reference to the result object within LiveStore;
74
+ // this implies that app code must not mutate the results, or else
75
+ // there may be weird reactivity bugs.
76
+ if (isEqual(newValue, valueRef.current) === false) {
77
+ setValue(newValue)
50
78
  }
51
79
  },
80
+ undefined,
81
+ { label: query.label, otelContext },
52
82
  )
53
- }, [query, store])
83
+ return () => {
84
+ query.activeSubscriptions.delete(stackInfo)
85
+ unsub()
86
+ }
87
+ }, [stackInfo, query, setValue, store, valueRef, otelContext, span])
54
88
 
55
- return value
89
+ return valueRef.current
56
90
  }
@@ -0,0 +1,23 @@
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
+ // TODO cache the query outside of the `useMemo` since `useMemo` might be called multiple times
13
+ // also need to update the `useEffect` below https://stackoverflow.com/questions/66446642/react-usememo-memory-clean/77457605#77457605
14
+ const query = React.useMemo(() => makeQuery(), [makeQuery])
15
+
16
+ React.useEffect(() => {
17
+ return () => {
18
+ query.destroy()
19
+ }
20
+ }, [query])
21
+
22
+ return useQuery(query)
23
+ }