@livestore/livestore 0.0.21 → 0.0.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 (78) hide show
  1. package/README.md +14 -4
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/react/fixture.d.ts.map +1 -1
  4. package/dist/__tests__/react/fixture.js +0 -2
  5. package/dist/__tests__/react/fixture.js.map +1 -1
  6. package/dist/__tests__/react/useQuery.test.js +1 -1
  7. package/dist/__tests__/react/useQuery.test.js.map +1 -1
  8. package/dist/__tests__/react/utils/stack-info.test.d.ts +2 -0
  9. package/dist/__tests__/react/utils/stack-info.test.d.ts.map +1 -0
  10. package/dist/__tests__/react/utils/stack-info.test.js +43 -0
  11. package/dist/__tests__/react/utils/stack-info.test.js.map +1 -0
  12. package/dist/__tests__/reactive.test.js +13 -1
  13. package/dist/__tests__/reactive.test.js.map +1 -1
  14. package/dist/__tests__/reactiveQueries/sql.test.js +3 -3
  15. package/dist/__tests__/reactiveQueries/sql.test.js.map +1 -1
  16. package/dist/inMemoryDatabase.d.ts +2 -1
  17. package/dist/inMemoryDatabase.d.ts.map +1 -1
  18. package/dist/inMemoryDatabase.js +3 -2
  19. package/dist/inMemoryDatabase.js.map +1 -1
  20. package/dist/react/index.d.ts +1 -0
  21. package/dist/react/index.d.ts.map +1 -1
  22. package/dist/react/index.js +1 -0
  23. package/dist/react/index.js.map +1 -1
  24. package/dist/react/useComponentState.d.ts.map +1 -1
  25. package/dist/react/useComponentState.js +19 -27
  26. package/dist/react/useComponentState.js.map +1 -1
  27. package/dist/react/useQuery.d.ts.map +1 -1
  28. package/dist/react/useQuery.js +46 -26
  29. package/dist/react/useQuery.js.map +1 -1
  30. package/dist/react/useTemporaryQuery.d.ts.map +1 -1
  31. package/dist/react/useTemporaryQuery.js +2 -0
  32. package/dist/react/useTemporaryQuery.js.map +1 -1
  33. package/dist/react/utils/stack-info.d.ts +11 -0
  34. package/dist/react/utils/stack-info.d.ts.map +1 -0
  35. package/dist/react/utils/stack-info.js +49 -0
  36. package/dist/react/utils/stack-info.js.map +1 -0
  37. package/dist/reactive.d.ts +33 -43
  38. package/dist/reactive.d.ts.map +1 -1
  39. package/dist/reactive.js +66 -255
  40. package/dist/reactive.js.map +1 -1
  41. package/dist/reactiveQueries/base-class.d.ts +15 -13
  42. package/dist/reactiveQueries/base-class.d.ts.map +1 -1
  43. package/dist/reactiveQueries/base-class.js +5 -8
  44. package/dist/reactiveQueries/base-class.js.map +1 -1
  45. package/dist/reactiveQueries/graphql.d.ts +4 -3
  46. package/dist/reactiveQueries/graphql.d.ts.map +1 -1
  47. package/dist/reactiveQueries/graphql.js +29 -34
  48. package/dist/reactiveQueries/graphql.js.map +1 -1
  49. package/dist/reactiveQueries/js.d.ts +2 -1
  50. package/dist/reactiveQueries/js.d.ts.map +1 -1
  51. package/dist/reactiveQueries/js.js +8 -9
  52. package/dist/reactiveQueries/js.js.map +1 -1
  53. package/dist/reactiveQueries/sql.d.ts +11 -5
  54. package/dist/reactiveQueries/sql.d.ts.map +1 -1
  55. package/dist/reactiveQueries/sql.js +31 -34
  56. package/dist/reactiveQueries/sql.js.map +1 -1
  57. package/dist/store.d.ts +26 -12
  58. package/dist/store.d.ts.map +1 -1
  59. package/dist/store.js +41 -255
  60. package/dist/store.js.map +1 -1
  61. package/package.json +3 -3
  62. package/src/__tests__/react/fixture.tsx +0 -3
  63. package/src/__tests__/react/useQuery.test.tsx +1 -1
  64. package/src/__tests__/react/utils/{extractStackInfoFromStackTrace.test.ts → stack-info.test.ts} +25 -20
  65. package/src/__tests__/reactive.test.ts +20 -1
  66. package/src/__tests__/reactiveQueries/sql.test.ts +3 -3
  67. package/src/inMemoryDatabase.ts +9 -6
  68. package/src/react/index.ts +1 -0
  69. package/src/react/useComponentState.ts +25 -30
  70. package/src/react/useQuery.ts +66 -34
  71. package/src/react/useTemporaryQuery.ts +2 -0
  72. package/src/react/utils/{extractStackInfoFromStackTrace.ts → stack-info.ts} +21 -5
  73. package/src/reactive.ts +148 -339
  74. package/src/reactiveQueries/base-class.ts +23 -22
  75. package/src/reactiveQueries/graphql.ts +34 -36
  76. package/src/reactiveQueries/js.ts +14 -10
  77. package/src/reactiveQueries/sql.ts +55 -48
  78. package/src/store.ts +70 -305
@@ -11,6 +11,7 @@ export { LiveStoreProvider } from './LiveStoreProvider.js'
11
11
  export { useComponentState } from './useComponentState.js'
12
12
  export { useQuery } from './useQuery.js'
13
13
  export { useTemporaryQuery } from './useTemporaryQuery.js'
14
+ export { useStackInfo } from './utils/stack-info.js'
14
15
 
15
16
  // Needed to make TS happy
16
17
  export type { TypedDocumentNode } from '@graphql-typed-document-node/core'
@@ -17,7 +17,7 @@ import { SCHEMA_META_TABLE } from '../schema.js'
17
17
  import type { BaseGraphQLContext, LiveStoreQuery, Store } from '../store.js'
18
18
  import { sql } from '../util.js'
19
19
  import { useStore } from './LiveStoreContext.js'
20
- import { extractStackInfoFromStackTrace, originalStackLimit } from './utils/extractStackInfoFromStackTrace.js'
20
+ import { extractStackInfoFromStackTrace, originalStackLimit } from './utils/stack-info.js'
21
21
  import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInput.js'
22
22
 
23
23
  export interface QueryDefinitions {
@@ -100,6 +100,14 @@ export const useComponentState = <TStateColumns extends ComponentColumns>({
100
100
 
101
101
  const componentKeyLabel = React.useMemo(() => labelForKey(componentKey), [componentKey])
102
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
+
103
111
  // The following `React.useMemo` and `React.useEffect` calls are used to start and end a span for the lifetime of this component.
104
112
  const { span, otelContext } = React.useMemo(() => {
105
113
  const existingSpan = spanAlreadyStartedCache.get(componentKeyLabel)
@@ -107,7 +115,7 @@ export const useComponentState = <TStateColumns extends ComponentColumns>({
107
115
 
108
116
  const span = store.otel.tracer.startSpan(
109
117
  `LiveStore:useComponentState:${componentKeyLabel}`,
110
- {},
118
+ { attributes: { stackInfo: JSON.stringify(stackInfo) } },
111
119
  store.otel.queriesSpanContext,
112
120
  )
113
121
 
@@ -116,7 +124,7 @@ export const useComponentState = <TStateColumns extends ComponentColumns>({
116
124
  spanAlreadyStartedCache.set(componentKeyLabel, { span, otelContext })
117
125
 
118
126
  return { span, otelContext }
119
- }, [componentKeyLabel, store.otel.queriesSpanContext, store.otel.tracer])
127
+ }, [componentKeyLabel, stackInfo, store.otel.queriesSpanContext, store.otel.tracer])
120
128
 
121
129
  React.useEffect(
122
130
  () => () => {
@@ -143,7 +151,6 @@ export const useComponentState = <TStateColumns extends ComponentColumns>({
143
151
  )
144
152
 
145
153
  const state$ = React.useMemo(() => {
146
- console.log('useComponentState make state$', labelForKey(componentKey))
147
154
  // create state query
148
155
  if (stateSchema === undefined) {
149
156
  // TODO don't set up a query if there's no state schema (keeps the graph more clean)
@@ -198,7 +205,10 @@ export const useComponentState = <TStateColumns extends ComponentColumns>({
198
205
 
199
206
  // Step 1:
200
207
  // Synchronously create state and queries for initial render pass.
201
- const initialComponentState = React.useMemo(() => state$.run(otelContext), [otelContext, state$])
208
+ const initialComponentState = React.useMemo(
209
+ () => state$.run(otelContext, { _tag: 'react', api: 'useComponentState', label: state$.label, stackInfo }),
210
+ [otelContext, stackInfo, state$],
211
+ )
202
212
 
203
213
  // Now that we've computed the initial state synchronously,
204
214
  // we can set up our useState calls w/ a default value populated...
@@ -240,14 +250,6 @@ export const useComponentState = <TStateColumns extends ComponentColumns>({
240
250
  return store.applyEvent('updateComponentState', { componentKey, columnNames, ...columnValues })
241
251
  }
242
252
 
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
-
251
253
  // OK, now all the synchronous work is done;
252
254
  // time to set up our long-running queries in an effect
253
255
  React.useEffect(() => {
@@ -258,11 +260,12 @@ export const useComponentState = <TStateColumns extends ComponentColumns>({
258
260
  (span) => {
259
261
  const unsubs: (() => void)[] = []
260
262
 
263
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
261
264
  if (stateSchema !== undefined) {
262
- insertRowForComponentInstance({ store, componentKey, stateSchema })
265
+ insertRowForComponentInstance({ store, componentKey, stateSchema, otelContext })
263
266
  }
264
267
 
265
- state$.activeSubscriptions.add(subscriptionInfo)
268
+ state$.activeSubscriptions.add(stackInfo)
266
269
 
267
270
  unsubs.push(
268
271
  store.subscribe(
@@ -273,9 +276,9 @@ export const useComponentState = <TStateColumns extends ComponentColumns>({
273
276
  }
274
277
  },
275
278
  undefined,
276
- { label: `useComponentState:localState:subscribe:${state$.label}` },
279
+ { label: `useComponentState:localState:subscribe:${state$.label}`, otelContext },
277
280
  ),
278
- () => state$.activeSubscriptions.delete(subscriptionInfo),
281
+ () => state$.activeSubscriptions.delete(stackInfo),
279
282
  )
280
283
 
281
284
  return () => {
@@ -287,13 +290,9 @@ export const useComponentState = <TStateColumns extends ComponentColumns>({
287
290
  }
288
291
  },
289
292
  )
290
- // NOTE excluding `setComponentState_` and `setQueryResults_` from the deps array as it seems to cause an infinite loop
291
- // This should probably be improved
292
- // TODO is this still true?
293
- // // eslint-disable-next-line react-hooks/exhaustive-deps
294
293
  }, [
295
294
  store,
296
- subscriptionInfo,
295
+ stackInfo,
297
296
  stateSchema,
298
297
  defaultComponentState,
299
298
  otelContext,
@@ -303,14 +302,7 @@ export const useComponentState = <TStateColumns extends ComponentColumns>({
303
302
  componentKey,
304
303
  ])
305
304
 
306
- // Very important: remove any queries / other resources associated w/ this component
307
- React.useEffect(
308
- () => () => {
309
- console.log('useComponentState destroy', labelForKey(componentKey))
310
- return state$.destroy()
311
- },
312
- [state$],
313
- )
305
+ React.useEffect(() => () => state$.destroy(), [state$])
314
306
 
315
307
  const state = componentStateRef.current
316
308
 
@@ -377,10 +369,12 @@ const insertRowForComponentInstance = ({
377
369
  store,
378
370
  componentKey,
379
371
  stateSchema,
372
+ otelContext,
380
373
  }: {
381
374
  store: Store<BaseGraphQLContext>
382
375
  componentKey: ComponentKey
383
376
  stateSchema: SqliteDsl.TableDefinition<string, SqliteDsl.Columns>
377
+ otelContext: otel.Context
384
378
  }) => {
385
379
  const columnNames = ['id', ...Object.keys(stateSchema.columns)]
386
380
  const columnValues = columnNames.map((name) => `$${name}`).join(', ')
@@ -397,6 +391,7 @@ const insertRowForComponentInstance = ({
397
391
  id: componentKey.id,
398
392
  },
399
393
  [tableName],
394
+ otelContext,
400
395
  )
401
396
  }
402
397
 
@@ -1,58 +1,90 @@
1
+ import * as otel from '@opentelemetry/api'
1
2
  import { isEqual } from 'lodash-es'
2
3
  import React from 'react'
3
4
 
4
5
  import type { ILiveStoreQuery } from '../reactiveQueries/base-class.js'
5
6
  import { useStore } from './LiveStoreContext.js'
6
- import { extractStackInfoFromStackTrace, originalStackLimit } from './utils/extractStackInfoFromStackTrace.js'
7
+ import { extractStackInfoFromStackTrace, originalStackLimit } from './utils/stack-info.js'
7
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 }>()
8
15
 
9
16
  export const useQuery = <TResult>(query: ILiveStoreQuery<TResult>): TResult => {
10
17
  const { store } = useStore()
11
18
 
12
- // TODO proper otel context
13
- const initialResult = React.useMemo(() => query.run(), [query])
14
-
15
- // We know the query has a result by the time we use it; so we can synchronously populate a default state
16
- const [valueRef, setValue] = useStateRefWithReactiveInput<TResult>(initialResult)
17
-
18
- const subscriptionInfo = React.useMemo(() => {
19
+ const stackInfo = React.useMemo(() => {
19
20
  Error.stackTraceLimit = 10
20
21
  // eslint-disable-next-line unicorn/error-message
21
22
  const stack = new Error().stack!
22
23
  Error.stackTraceLimit = originalStackLimit
23
- return { stack: extractStackInfoFromStackTrace(stack) }
24
+ return extractStackInfoFromStackTrace(stack)
24
25
  }, [])
25
26
 
26
- // Subscribe to future updates for this query
27
- React.useEffect(() => {
28
- return store.otel.tracer.startActiveSpan(
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(
29
33
  `LiveStore:useQuery:${query.label}`,
30
- // `LiveStore:useQuery:${labelForKey(query.componentKey)}:${query.label}`,
31
- { attributes: { label: query.label } },
34
+ { attributes: { label: query.label, stackInfo: JSON.stringify(stackInfo) } },
32
35
  store.otel.queriesSpanContext,
33
- (span) => {
34
- query.activeSubscriptions.add(subscriptionInfo)
35
- const unsub = store.subscribe(
36
- query,
37
- (newValue) => {
38
- // NOTE: we return a reference to the result object within LiveStore;
39
- // this implies that app code must not mutate the results, or else
40
- // there may be weird reactivity bugs.
41
- if (isEqual(newValue, valueRef.current) === false) {
42
- setValue(newValue)
43
- }
44
- },
45
- undefined,
46
- { label: query.label },
47
- )
48
- return () => {
49
- query.activeSubscriptions.delete(subscriptionInfo)
50
- unsub()
51
- span.end()
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
+ )
55
+
56
+ // We know the query has a result by the time we use it; so we can synchronously populate a default state
57
+ const [valueRef, setValue] = useStateRefWithReactiveInput<TResult>(initialResult)
58
+
59
+ React.useEffect(
60
+ () => () => {
61
+ spanAlreadyStartedCache.delete(query)
62
+ span.end()
63
+ },
64
+ [query, span],
65
+ )
66
+
67
+ // Subscribe to future updates for this query
68
+ React.useEffect(() => {
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)
52
78
  }
53
79
  },
80
+ undefined,
81
+ { label: query.label, otelContext },
54
82
  )
55
- }, [subscriptionInfo, query, setValue, store, valueRef])
83
+ return () => {
84
+ query.activeSubscriptions.delete(stackInfo)
85
+ unsub()
86
+ }
87
+ }, [stackInfo, query, setValue, store, valueRef, otelContext, span])
56
88
 
57
89
  return valueRef.current
58
90
  }
@@ -9,6 +9,8 @@ import { useQuery } from './useQuery.js'
9
9
  * Make sure `makeQuery` is a memoized function.
10
10
  */
11
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
12
14
  const query = React.useMemo(() => makeQuery(), [makeQuery])
13
15
 
14
16
  React.useEffect(() => {
@@ -1,6 +1,12 @@
1
+ import React from 'react'
2
+
1
3
  export const originalStackLimit = Error.stackTraceLimit
2
4
 
3
5
  export type StackInfo = {
6
+ frames: StackFrame[]
7
+ }
8
+
9
+ export type StackFrame = {
4
10
  name: string
5
11
  filePath: string
6
12
  }
@@ -24,10 +30,10 @@ Approach:
24
30
  - Start filtering at `at useQuery` (including)
25
31
  - Stop filtering at `at renderWithHooks` (excluding)
26
32
  */
27
- export const extractStackInfoFromStackTrace = (stackTrace: string): StackInfo[] => {
33
+ export const extractStackInfoFromStackTrace = (stackTrace: string): StackInfo => {
28
34
  const namePattern = /at (\S+) \((.+)\)/g
29
35
  let match: RegExpExecArray | null
30
- const stackInfoArr: StackInfo[] = []
36
+ const frames: StackFrame[] = []
31
37
  let hasReachedStart = false
32
38
 
33
39
  while ((match = namePattern.exec(stackTrace)) !== null) {
@@ -35,13 +41,23 @@ export const extractStackInfoFromStackTrace = (stackTrace: string): StackInfo[]
35
41
  if (name.startsWith('use')) {
36
42
  hasReachedStart = true
37
43
 
38
- stackInfoArr.unshift({ name, filePath })
44
+ frames.unshift({ name, filePath })
39
45
  } else if (hasReachedStart) {
40
46
  // We've reached the end of the `use*` functions, so we're adding the component name and stop
41
- stackInfoArr.unshift({ name, filePath })
47
+ frames.unshift({ name, filePath })
42
48
  break
43
49
  }
44
50
  }
45
51
 
46
- return stackInfoArr
52
+ return { frames }
47
53
  }
54
+
55
+ export const useStackInfo = (): StackInfo =>
56
+ React.useMemo(() => {
57
+ Error.stackTraceLimit = 10
58
+ // eslint-disable-next-line unicorn/error-message
59
+ const stack = new Error().stack!
60
+ console.log('stack', stack)
61
+ Error.stackTraceLimit = originalStackLimit
62
+ return extractStackInfoFromStackTrace(stack)
63
+ }, [])