@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
package/src/store.ts CHANGED
@@ -1,34 +1,29 @@
1
- import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'
2
1
  import { assertNever, makeNoopSpan, makeNoopTracer, shouldNeverHappen } from '@livestore/utils'
3
2
  import { identity } from '@livestore/utils/effect'
4
3
  import * as otel from '@opentelemetry/api'
5
4
  import type { GraphQLSchema } from 'graphql'
6
- import * as graphql from 'graphql'
7
- import { uniqueId } from 'lodash-es'
8
- import * as ReactDOM from 'react-dom'
9
5
  import type * as Sqlite from 'sqlite-esm'
10
6
  import { v4 as uuid } from 'uuid'
11
7
 
12
8
  import type { ComponentKey } from './componentKey.js'
13
9
  import { tableNameForComponentKey } from './componentKey.js'
14
- import type { QueryDefinition } from './effect/LiveStore.js'
15
10
  import type { LiveStoreEvent } from './events.js'
16
11
  import { InMemoryDatabase } from './inMemoryDatabase.js'
17
12
  import { migrateDb } from './migrations.js'
18
13
  import { getDurationMsFromSpan } from './otel.js'
19
- import type { Atom, Ref } from './reactive.js'
20
- import { ReactiveGraph } from './reactive.js'
21
- import { LiveStoreGraphQLQuery } from './reactiveQueries/graphql.js'
22
- import { LiveStoreJSQuery } from './reactiveQueries/js.js'
23
- import { LiveStoreSQLQuery } from './reactiveQueries/sql.js'
14
+ import type { StackInfo } from './react/utils/stack-info.js'
15
+ import type { ReactiveGraph, Ref } from './reactive.js'
16
+ import type { ILiveStoreQuery } from './reactiveQueries/base-class.js'
17
+ import { type DbContext, dbGraph } from './reactiveQueries/graph.js'
18
+ import type { LiveStoreGraphQLQuery } from './reactiveQueries/graphql.js'
19
+ import type { LiveStoreJSQuery } from './reactiveQueries/js.js'
20
+ import type { LiveStoreSQLQuery } from './reactiveQueries/sql.js'
24
21
  import type { ActionDefinition, GetActionArgs, Schema, SQLWriteStatement } from './schema.js'
25
22
  import { componentStateTables } from './schema.js'
26
23
  import type { Storage, StorageInit } from './storage/index.js'
27
- import type { Bindable, ParamsObject } from './util.js'
24
+ import type { ParamsObject } from './util.js'
28
25
  import { isPromise, prepareBindValues, sql } from './util.js'
29
26
 
30
- export type GetAtomResult = <T>(atom: Atom<T> | LiveStoreJSQuery<T>) => T
31
-
32
27
  export type LiveStoreQuery<TResult extends Record<string, any> = any> =
33
28
  | LiveStoreSQLQuery<TResult>
34
29
  | LiveStoreJSQuery<TResult>
@@ -48,8 +43,6 @@ export type QueryResult<TQuery> = TQuery extends LiveStoreSQLQuery<infer R>
48
43
  ? Readonly<Result>
49
44
  : never
50
45
 
51
- const globalComponentKey: ComponentKey = { _tag: 'singleton', componentName: '__global', id: 'singleton' }
52
-
53
46
  export type GraphQLOptions<TContext> = {
54
47
  schema: GraphQLSchema
55
48
  makeContext: (db: InMemoryDatabase, tracer: otel.Tracer) => TContext
@@ -93,9 +86,21 @@ export type RefreshReason =
93
86
  _tag: 'makeThunk'
94
87
  label?: string
95
88
  }
89
+ | {
90
+ _tag: 'react'
91
+ api: string
92
+ label?: string
93
+ stackInfo?: StackInfo
94
+ }
95
+ | { _tag: 'manual'; label?: string }
96
96
  | { _tag: 'unknown' }
97
97
 
98
- export type QueryDebugInfo = { _tag: 'graphql' | 'sql' | 'js' | 'unknown'; label: string; query: string }
98
+ export type QueryDebugInfo = {
99
+ _tag: 'graphql' | 'sql' | 'js' | 'unknown'
100
+ label: string
101
+ query: string
102
+ durationMs: number
103
+ }
99
104
 
100
105
  export type StoreOtel = {
101
106
  tracer: otel.Tracer
@@ -104,7 +109,7 @@ export type StoreOtel = {
104
109
  }
105
110
 
106
111
  export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLContext> {
107
- graph: ReactiveGraph<RefreshReason, QueryDebugInfo>
112
+ graph: ReactiveGraph<RefreshReason, QueryDebugInfo, DbContext>
108
113
  inMemoryDB: InMemoryDatabase
109
114
  // TODO refactor
110
115
  _proxyDb: InMemoryDatabase
@@ -116,10 +121,11 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
116
121
  * Note we're using `Ref<null>` here as we don't care about the value but only about *that* something has changed.
117
122
  * This only works in combination with `equal: () => false` which will always trigger a refresh.
118
123
  */
119
- tableRefs: { [key: string]: Ref<null> }
120
- activeQueries: Set<LiveStoreQuery>
124
+ tableRefs: { [key: string]: Ref<null, DbContext, RefreshReason> }
125
+
126
+ /** RC-based set to see which queries are currently subscribed to */
127
+ activeQueries: ReferenceCountedSet<LiveStoreQuery>
121
128
  storage?: Storage
122
- temporaryQueries: Set<LiveStoreQuery> | undefined
123
129
 
124
130
  private constructor({
125
131
  db,
@@ -132,16 +138,10 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
132
138
  }: StoreOptions<TGraphQLContext>) {
133
139
  this.inMemoryDB = db
134
140
  this._proxyDb = dbProxy
135
- this.graph = new ReactiveGraph({
136
- // TODO move this into React module
137
- // Do all our updates inside a single React setState batch to avoid multiple UI re-renders
138
- effectsWrapper: (run) => ReactDOM.unstable_batchedUpdates(() => run()),
139
- otelTracer,
140
- })
141
141
  this.schema = schema
142
142
  // TODO generalize the `tableRefs` concept to allow finer-grained refs
143
143
  this.tableRefs = {}
144
- this.activeQueries = new Set()
144
+ this.activeQueries = new ReferenceCountedSet()
145
145
  this.storage = storage
146
146
 
147
147
  const applyEventsSpan = otelTracer.startSpan('LiveStore:applyEvents', {}, otelRootSpanContext)
@@ -150,6 +150,10 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
150
150
  const queriesSpan = otelTracer.startSpan('LiveStore:queries', {}, otelRootSpanContext)
151
151
  const otelQueriesSpanContext = otel.trace.setSpan(otel.context.active(), queriesSpan)
152
152
 
153
+ // TODO allow passing in a custom graph
154
+ this.graph = dbGraph
155
+ this.graph.context = { store: this, otelTracer, rootOtelContext: otelQueriesSpanContext }
156
+
153
157
  this.otel = {
154
158
  tracer: otelTracer,
155
159
  applyEventsSpanContext: otelApplyEventsSpanContext,
@@ -189,403 +193,61 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
189
193
  })
190
194
  }
191
195
 
192
- /**
193
- * Creates a reactive LiveStore SQL query
194
- *
195
- * NOTE The query is actually running (even if no one has subscribed to it yet) and will be kept up to date.
196
- */
197
- querySQL = <TResult>(
198
- genQueryString: string | ((get: GetAtomResult) => string),
199
- {
200
- queriedTables,
201
- bindValues,
202
- componentKey,
203
- label,
204
- otelContext = otel.context.active(),
205
- }: {
206
- /**
207
- * List of tables that are queried in this query;
208
- * used to determine reactive dependencies.
209
- *
210
- * NOTE In the future we want to auto-generate this via parsing the query
211
- */
212
- queriedTables: string[]
213
- bindValues?: Bindable | undefined
214
- componentKey?: ComponentKey | undefined
215
- label?: string | undefined
216
- otelContext?: otel.Context
217
- },
218
- ): LiveStoreSQLQuery<TResult> =>
219
- this.otel.tracer.startActiveSpan(
220
- 'querySQL', // NOTE span name will be overridden further down
221
- { attributes: { label } },
222
- otelContext,
223
- (span) => {
224
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
225
-
226
- const queryString$ = this.graph.makeThunk(
227
- (get, addDebugInfo) => {
228
- if (typeof genQueryString === 'function') {
229
- const getAtom: GetAtomResult = (atom) => {
230
- if (atom._tag === 'thunk' || atom._tag === 'ref') return get(atom)
231
- return get(atom.results$)
232
- }
233
- const queryString = genQueryString(getAtom)
234
- addDebugInfo({ _tag: 'js', label: `${label}:queryString`, query: queryString })
235
- return queryString
236
- } else {
237
- return genQueryString
238
- }
239
- },
240
- { label: `${label}:queryString`, meta: { liveStoreThunkType: 'sqlQueryString' } },
241
- otelContext,
242
- )
243
-
244
- label = label ?? queryString$.result
245
- span.updateName(`querySQL:${label}`)
246
-
247
- const queryLabel = `${label}:results` + (this.temporaryQueries ? ':temp' : '')
248
-
249
- const results$ = this.graph.makeThunk<ReadonlyArray<TResult>>(
250
- (get, addDebugInfo) =>
251
- this.otel.tracer.startActiveSpan(
252
- 'sql:', // NOTE span name will be overridden further down
253
- {},
254
- otelContext,
255
- (span) => {
256
- try {
257
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
258
-
259
- // Establish a reactive dependency on the tables used in the query
260
- for (const tableName of queriedTables) {
261
- const tableRef =
262
- this.tableRefs[tableName] ?? shouldNeverHappen(`No table ref found for ${tableName}`)
263
- get(tableRef)
264
- }
265
- const sqlString = get(queryString$)
266
-
267
- span.setAttribute('sql.query', sqlString)
268
- span.updateName(`sql:${sqlString.slice(0, 50)}`)
269
-
270
- const results = this.inMemoryDB.select<TResult>(sqlString, {
271
- queriedTables,
272
- bindValues: bindValues ? prepareBindValues(bindValues, sqlString) : undefined,
273
- otelContext,
274
- })
275
-
276
- span.setAttribute('sql.rowsCount', results.length)
277
- addDebugInfo({ _tag: 'sql', label: label ?? '', query: sqlString })
278
-
279
- return results
280
- } finally {
281
- span.end()
282
- }
283
- },
284
- ),
285
- { label: queryLabel },
286
- otelContext,
287
- )
288
-
289
- const query = new LiveStoreSQLQuery<TResult>({
290
- label,
291
- queryString$,
292
- results$,
293
- componentKey: componentKey ?? globalComponentKey,
294
- store: this,
295
- otelContext,
296
- })
297
-
298
- this.activeQueries.add(query)
299
-
300
- // TODO get rid of temporary query workaround
301
- if (this.temporaryQueries !== undefined) {
302
- this.temporaryQueries.add(query)
303
- }
304
-
305
- // NOTE we are not ending the span here but in the query `destroy` method
306
- return query
307
- },
308
- )
309
-
310
- queryJS = <TResult>(
311
- genResults: (get: GetAtomResult) => TResult,
312
- {
313
- componentKey = globalComponentKey,
314
- label = `js${uniqueId()}`,
315
- otelContext = otel.context.active(),
316
- }: { componentKey?: ComponentKey; label?: string; otelContext?: otel.Context },
317
- ): LiveStoreJSQuery<TResult> =>
318
- this.otel.tracer.startActiveSpan(`queryJS:${label}`, { attributes: { label } }, otelContext, (span) => {
319
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
320
- const queryLabel = `${label}:results` + (this.temporaryQueries ? ':temp' : '')
321
- const results$ = this.graph.makeThunk(
322
- (get, addDebugInfo) => {
323
- const getAtom: GetAtomResult = (atom) => {
324
- if (atom._tag === 'thunk' || atom._tag === 'ref') return get(atom)
325
- return get(atom.results$)
326
- }
327
- addDebugInfo({ _tag: 'js', label, query: genResults.toString() })
328
- return genResults(getAtom)
329
- },
330
- { label: queryLabel, meta: { liveStoreThunkType: 'jsResults' } },
331
- otelContext,
332
- )
333
-
334
- const query = new LiveStoreJSQuery<TResult>({
335
- label,
336
- results$,
337
- componentKey,
338
- store: this,
339
- otelContext,
340
- })
341
-
342
- this.activeQueries.add(query)
343
-
344
- // TODO get rid of temporary query workaround
345
- if (this.temporaryQueries !== undefined) {
346
- this.temporaryQueries.add(query)
347
- }
348
-
349
- // NOTE we are not ending the span here but in the query `destroy` method
350
- return query
351
- })
352
-
353
- queryGraphQL = <TResult extends Record<string, any>, TVariableValues extends Record<string, any>>(
354
- document: DocumentNode<TResult, TVariableValues>,
355
- genVariableValues: TVariableValues | ((get: GetAtomResult) => TVariableValues),
356
- {
357
- componentKey,
358
- label,
359
- otelContext = otel.context.active(),
360
- }: {
361
- componentKey: ComponentKey
362
- label?: string
363
- otelContext?: otel.Context
364
- },
365
- ): LiveStoreGraphQLQuery<TResult, TVariableValues, TGraphQLContext> =>
366
- this.otel.tracer.startActiveSpan(
367
- `queryGraphQL:`, // NOTE span name will be overridden further down
368
- {},
369
- otelContext,
370
- (span) => {
371
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
372
-
373
- if (this.graphQLContext === undefined) {
374
- return shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL context")
375
- }
376
-
377
- const labelWithDefault = label ?? graphql.getOperationAST(document)?.name?.value ?? 'graphql'
378
-
379
- span.updateName(`queryGraphQL:${labelWithDefault}`)
380
-
381
- const variableValues$ = this.graph.makeThunk(
382
- (get) => {
383
- if (typeof genVariableValues === 'function') {
384
- const getAtom: GetAtomResult = (atom) => {
385
- if (atom._tag === 'thunk' || atom._tag === 'ref') return get(atom)
386
- return get(atom.results$)
387
- }
388
- return genVariableValues(getAtom)
389
- } else {
390
- return genVariableValues
391
- }
392
- },
393
- { label: `${labelWithDefault}:variableValues`, meta: { liveStoreThunkType: 'graphqlVariableValues' } },
394
- otelContext,
395
- )
396
-
397
- const resultsLabel = `${labelWithDefault}:results` + (this.temporaryQueries ? ':temp' : '')
398
- const results$ = this.graph.makeThunk<TResult>(
399
- (get, addDebugInfo) => {
400
- const variableValues = get(variableValues$)
401
- const { result, queriedTables } = this.queryGraphQLOnce(document, variableValues, otelContext)
402
-
403
- // Add dependencies on any tables that were used
404
- for (const tableName of queriedTables) {
405
- const tableRef = this.tableRefs[tableName]
406
- assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
407
- get(tableRef!)
408
- }
409
-
410
- addDebugInfo({ _tag: 'graphql', label: resultsLabel, query: graphql.print(document) })
411
-
412
- return result
413
- },
414
- { label: resultsLabel, meta: { liveStoreThunkType: 'graphqlResults' } },
415
- otelContext,
416
- )
417
-
418
- const query = new LiveStoreGraphQLQuery({
419
- document,
420
- context: this.graphQLContext,
421
- results$,
422
- componentKey,
423
- label: labelWithDefault,
424
- store: this,
425
- otelContext,
426
- })
427
-
428
- this.activeQueries.add(query)
429
-
430
- // TODO get rid of temporary query workaround
431
- if (this.temporaryQueries !== undefined) {
432
- this.temporaryQueries.add(query)
433
- }
434
-
435
- // NOTE we are not ending the span here but in the query `destroy` method
436
- return query
437
- },
438
- )
439
-
440
- queryGraphQLOnce = <TResult extends Record<string, any>, TVariableValues extends Record<string, any>>(
441
- document: DocumentNode<TResult, TVariableValues>,
442
- variableValues: TVariableValues,
443
- otelContext: otel.Context = this.otel.queriesSpanContext,
444
- ): { result: TResult; queriedTables: string[] } => {
445
- const schema =
446
- this.graphQLSchema ?? shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL schema")
447
- const context =
448
- this.graphQLContext ?? shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL context")
449
- const tracer = this.otel.tracer
450
-
451
- const operationName = graphql.getOperationAST(document)?.name?.value
452
-
453
- return tracer.startActiveSpan(`executeGraphQLQuery: ${operationName}`, {}, otelContext, (span) => {
454
- try {
455
- span.setAttribute('graphql.variables', JSON.stringify(variableValues))
456
- span.setAttribute('graphql.query', graphql.print(document))
457
-
458
- context.queriedTables.clear()
459
-
460
- context.otelContext = otel.trace.setSpan(otel.context.active(), span)
461
-
462
- const res = graphql.executeSync({
463
- document,
464
- contextValue: context,
465
- schema: schema,
466
- variableValues,
467
- })
468
-
469
- // TODO track number of nested SQL queries via Otel + debug info
470
-
471
- if (res.errors) {
472
- span.setStatus({ code: otel.SpanStatusCode.ERROR, message: 'GraphQL error' })
473
- span.setAttribute('graphql.error', res.errors.join('\n'))
474
- span.setAttribute('graphql.error-detail', JSON.stringify(res.errors))
475
- console.error(`graphql error (${operationName})`, res.errors)
476
- }
477
-
478
- return { result: res.data as unknown as TResult, queriedTables: Array.from(context.queriedTables.values()) }
479
- } finally {
480
- span.end()
481
- }
482
- })
483
- }
484
-
485
196
  /**
486
197
  * Subscribe to the results of a query
487
198
  * Returns a function to cancel the subscription.
488
199
  */
489
- subscribe = <TQuery extends LiveStoreQuery>(
490
- query: TQuery,
491
- onNewValue: (value: QueryResult<TQuery>) => void,
492
- onSubsubscribe?: () => void,
493
- options?: { label?: string } | undefined,
200
+ subscribe = <TResult>(
201
+ query: ILiveStoreQuery<TResult>,
202
+ onNewValue: (value: TResult) => void,
203
+ onUnsubsubscribe?: () => void,
204
+ options?: { label?: string; otelContext?: otel.Context; skipInitialRun?: boolean } | undefined,
494
205
  ): (() => void) =>
495
206
  this.otel.tracer.startActiveSpan(
496
207
  `LiveStore.subscribe`,
497
208
  { attributes: { label: options?.label } },
498
- query.otelContext,
209
+ options?.otelContext ?? this.otel.queriesSpanContext,
499
210
  (span) => {
500
211
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
501
212
 
502
- const effect = this.graph.makeEffect(
503
- (get) => {
504
- const result = get(query.results$) as QueryResult<TQuery>
505
- onNewValue(result)
506
- },
507
- { label: `subscribe:${options?.label}` },
508
- otelContext,
509
- )
213
+ const label = `subscribe:${options?.label}`
214
+ const effect = this.graph.makeEffect((get) => onNewValue(get(query.results$)), { label })
215
+
216
+ this.activeQueries.add(query as LiveStoreQuery)
510
217
 
511
- const subscriptionKey = uuid()
218
+ // Running effect right away to get initial value (unless `skipInitialRun` is set)
219
+ if (options?.skipInitialRun !== true) {
220
+ effect.doEffect(otelContext)
221
+ }
512
222
 
513
223
  const unsubscribe = () => {
514
224
  try {
515
225
  this.graph.destroy(effect)
516
- query.activeSubscriptions.delete(subscriptionKey)
517
- onSubsubscribe?.()
226
+ this.activeQueries.remove(query as LiveStoreQuery)
227
+ onUnsubsubscribe?.()
518
228
  } finally {
519
229
  span.end()
520
230
  }
521
231
  }
522
232
 
523
- query.activeSubscriptions.set(subscriptionKey, unsubscribe)
524
-
525
233
  return unsubscribe
526
234
  },
527
235
  )
528
236
 
529
- /**
530
- * Any queries created in the callback will be destroyed when the callback is complete.
531
- * Useful for temporarily creating reactive queries, which is an idempotent operation
532
- * that can be safely called inside a React useMemo hook.
533
- */
534
- inTempQueryContext = <TResult>(callback: () => TResult): TResult => {
535
- this.temporaryQueries = new Set()
536
- // TODO: consider errors / try/finally here?
537
- const result = callback()
538
- for (const query of this.temporaryQueries) {
539
- this.destroyQuery(query)
540
- }
541
- this.temporaryQueries = undefined
542
- return result
543
- }
544
-
545
237
  /**
546
238
  * Destroys the entire store, including all queries and subscriptions.
547
239
  *
548
240
  * Currently only used when shutting down the app for debugging purposes (e.g. to close Otel spans).
549
241
  */
550
242
  destroy = () => {
551
- for (const query of this.activeQueries) {
552
- this.destroyQuery(query)
553
- }
554
-
555
243
  Object.values(this.tableRefs).forEach((tableRef) => this.graph.destroy(tableRef))
556
244
 
557
- const applyEventsSpan = otel.trace.getSpan(this.otel.applyEventsSpanContext)!
558
- applyEventsSpan.end()
559
-
560
- const queriesSpan = otel.trace.getSpan(this.otel.queriesSpanContext)!
561
- queriesSpan.end()
245
+ otel.trace.getSpan(this.otel.applyEventsSpanContext)!.end()
246
+ otel.trace.getSpan(this.otel.queriesSpanContext)!.end()
562
247
 
563
248
  // TODO destroy active subscriptions
564
249
  }
565
250
 
566
- private destroyQuery = (query: LiveStoreQuery) => {
567
- if (query._tag === 'sql') {
568
- // results are downstream of query string, so will automatically be destroyed together
569
- this.graph.destroy(query.queryString$)
570
- } else {
571
- this.graph.destroy(query.results$)
572
- }
573
- this.activeQueries.delete(query)
574
- query.destroy()
575
- }
576
-
577
- /**
578
- * Clean up queries and downstream subscriptions associated with a component.
579
- * This is critical to avoid memory leaks.
580
- */
581
- unmountComponent = (componentKey: ComponentKey) => {
582
- for (const query of this.activeQueries) {
583
- if (query.componentKey === componentKey) {
584
- this.destroyQuery(query)
585
- }
586
- }
587
- }
588
-
589
251
  /* Apply a single write event to the store, and refresh all queries in response */
590
252
  applyEvent = <TEventType extends string & keyof LiveStoreActionDefinitionsTypes>(
591
253
  eventType: TEventType,
@@ -607,27 +269,32 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
607
269
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
608
270
  const writeTables = this.applyEventWithoutRefresh(eventType, args, otelContext).writeTables
609
271
 
610
- const tablesToUpdate = [] as [Ref<null>, null][]
272
+ const tablesToUpdate = [] as [Ref<null, DbContext, RefreshReason>, null][]
611
273
  for (const tableName of writeTables) {
612
274
  const tableRef = this.tableRefs[tableName]
613
275
  assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
614
276
  tablesToUpdate.push([tableRef!, null])
615
277
  }
616
278
 
279
+ const debugRefreshReason = {
280
+ _tag: 'applyEvent' as const,
281
+ event: { type: eventType, args },
282
+ writeTables: [...writeTables],
283
+ }
284
+
617
285
  // Update all table refs together in a batch, to only trigger one reactive update
618
- this.graph.setRefs(
619
- tablesToUpdate,
620
- {
621
- otelHint: 'applyEvents',
622
- skipRefresh,
623
- debugRefreshReason: {
624
- _tag: 'applyEvent',
625
- event: { type: eventType, args },
626
- writeTables: [...writeTables],
627
- },
628
- },
629
- otelContext,
630
- )
286
+ this.graph.setRefs(tablesToUpdate, { debugRefreshReason, otelContext })
287
+
288
+ if (skipRefresh === false) {
289
+ // TODO update the graph
290
+ // this.graph.refresh(
291
+ // {
292
+ // otelHint: 'applyEvents',
293
+ // debugRefreshReason,
294
+ // },
295
+ // otelContext,
296
+ // )
297
+ }
631
298
  } catch (e: any) {
632
299
  span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() })
633
300
 
@@ -703,27 +370,25 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
703
370
  },
704
371
  )
705
372
 
706
- const tablesToUpdate = [] as [Ref<null>, null][]
373
+ const tablesToUpdate = [] as [Ref<null, DbContext, RefreshReason>, null][]
707
374
  for (const tableName of writeTables) {
708
375
  const tableRef = this.tableRefs[tableName]
709
376
  assertNever(tableRef !== undefined, `No table ref found for ${tableName}`)
710
377
  tablesToUpdate.push([tableRef!, null])
711
378
  }
712
379
 
380
+ const debugRefreshReason = {
381
+ _tag: 'applyEvents' as const,
382
+ events: [...events].map((e) => ({ type: e.eventType, args: e.args })),
383
+ writeTables: [...writeTables],
384
+ }
713
385
  // Update all table refs together in a batch, to only trigger one reactive update
714
- this.graph.setRefs(
715
- tablesToUpdate,
716
- {
717
- otelHint: 'applyEvents',
718
- skipRefresh,
719
- debugRefreshReason: {
720
- _tag: 'applyEvents',
721
- events: [...events].map((e) => ({ type: e.eventType, args: e.args })),
722
- writeTables: [...writeTables],
723
- },
724
- },
725
- otelContext,
726
- )
386
+ this.graph.setRefs(tablesToUpdate, { debugRefreshReason, otelContext })
387
+
388
+ if (skipRefresh === false) {
389
+ // TODO update the graph
390
+ // this.graph.refresh({ debugRefreshReason, otelHint: 'applyEvents' }, otelContext)
391
+ }
727
392
  } catch (e: any) {
728
393
  span.setStatus({ code: otel.SpanStatusCode.ERROR, message: e.toString() })
729
394
  } finally {
@@ -746,20 +411,14 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
746
411
  { attributes: { 'livestore.manualRefreshLabel': label } },
747
412
  this.otel.applyEventsSpanContext,
748
413
  (span) => {
749
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
750
- this.graph.refresh({ otelHint: 'manualRefresh', debugRefreshReason: { _tag: 'manualRefresh' } }, otelContext)
414
+ // const otelContext = otel.trace.setSpan(otel.context.active(), span)
415
+ // TODO update the graph
416
+ // this.graph.refresh({ otelHint: 'manualRefresh', debugRefreshReason: { _tag: 'manualRefresh' } }, otelContext)
751
417
  span.end()
752
418
  },
753
419
  )
754
420
  }
755
421
 
756
- // TODO get rid of this as part of new query definition approach https://www.notion.so/schickling/New-query-definition-approach-1097a78ef0e9495bac25f90417374756?pvs=4
757
- runOnce = <TQueryDef extends QueryDefinition>(queryDef: TQueryDef): QueryResult<ReturnType<TQueryDef>> => {
758
- return this.inTempQueryContext(() => {
759
- return queryDef(this).results$.result
760
- })
761
- }
762
-
763
422
  /**
764
423
  * Apply an event to the store.
765
424
  * Returns the tables that were affected by the event.
@@ -853,8 +512,8 @@ export class Store<TGraphQLContext extends BaseGraphQLContext = BaseGraphQLConte
853
512
  * This should only be used for framework-internal purposes;
854
513
  * all app writes should go through applyEvent.
855
514
  */
856
- execute = async (query: string, params: ParamsObject = {}, writeTables?: string[]) => {
857
- this.inMemoryDB.execute(query, prepareBindValues(params, query), writeTables)
515
+ execute = (query: string, params: ParamsObject = {}, writeTables?: string[], otelContext?: otel.Context) => {
516
+ this.inMemoryDB.execute(query, prepareBindValues(params, query), writeTables, { otelContext })
858
517
 
859
518
  if (this.storage !== undefined) {
860
519
  const parentSpan = otel.trace.getSpan(otel.context.active())
@@ -977,3 +636,39 @@ const eventToSql = (
977
636
 
978
637
  return { statement, bindValues }
979
638
  }
639
+
640
+ class ReferenceCountedSet<T> {
641
+ private map: Map<T, number>
642
+
643
+ constructor() {
644
+ this.map = new Map<T, number>()
645
+ }
646
+
647
+ add = (key: T) => {
648
+ const count = this.map.get(key) ?? 0
649
+ this.map.set(key, count + 1)
650
+ }
651
+
652
+ remove = (key: T) => {
653
+ const count = this.map.get(key) ?? 0
654
+ if (count === 1) {
655
+ this.map.delete(key)
656
+ } else {
657
+ this.map.set(key, count - 1)
658
+ }
659
+ }
660
+
661
+ has = (key: T) => {
662
+ return this.map.has(key)
663
+ }
664
+
665
+ get size() {
666
+ return this.map.size
667
+ }
668
+
669
+ *[Symbol.iterator]() {
670
+ for (const key of this.map.keys()) {
671
+ yield key
672
+ }
673
+ }
674
+ }