@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.
- package/README.md +29 -22
- package/dist/.tsbuildinfo +1 -1
- package/dist/QueryCache.d.ts +1 -1
- package/dist/QueryCache.d.ts.map +1 -1
- package/dist/QueryCache.js.map +1 -1
- package/dist/__tests__/react/fixture.d.ts +5 -4
- package/dist/__tests__/react/fixture.d.ts.map +1 -1
- package/dist/__tests__/react/fixture.js +3 -5
- package/dist/__tests__/react/fixture.js.map +1 -1
- package/dist/__tests__/react/useComponentState.test.d.ts +2 -0
- package/dist/__tests__/react/useComponentState.test.d.ts.map +1 -0
- package/dist/__tests__/react/useComponentState.test.js +68 -0
- package/dist/__tests__/react/useComponentState.test.js.map +1 -0
- package/dist/__tests__/react/useLQuery.test.d.ts +2 -0
- package/dist/__tests__/react/useLQuery.test.d.ts.map +1 -0
- package/dist/__tests__/react/useLQuery.test.js +38 -0
- package/dist/__tests__/react/useLQuery.test.js.map +1 -0
- package/dist/__tests__/react/useLiveStoreComponent.test.js +4 -9
- package/dist/__tests__/react/useLiveStoreComponent.test.js.map +1 -1
- package/dist/__tests__/react/useQuery.test.d.ts +2 -0
- package/dist/__tests__/react/useQuery.test.d.ts.map +1 -0
- package/dist/__tests__/react/useQuery.test.js +33 -0
- package/dist/__tests__/react/useQuery.test.js.map +1 -0
- package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.d.ts +2 -0
- package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.d.ts.map +1 -0
- package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.js +38 -0
- package/dist/__tests__/react/utils/extractStackInfoFromStackTrace.test.js.map +1 -0
- package/dist/__tests__/react/utils/stack-info.test.d.ts +2 -0
- package/dist/__tests__/react/utils/stack-info.test.d.ts.map +1 -0
- package/dist/__tests__/react/utils/stack-info.test.js +43 -0
- package/dist/__tests__/react/utils/stack-info.test.js.map +1 -0
- package/dist/__tests__/reactive.test.js +179 -93
- package/dist/__tests__/reactive.test.js.map +1 -1
- package/dist/__tests__/reactiveQueries/sql.test.d.ts +2 -0
- package/dist/__tests__/reactiveQueries/sql.test.d.ts.map +1 -0
- package/dist/__tests__/reactiveQueries/sql.test.js +337 -0
- package/dist/__tests__/reactiveQueries/sql.test.js.map +1 -0
- package/dist/inMemoryDatabase.d.ts +4 -3
- package/dist/inMemoryDatabase.d.ts.map +1 -1
- package/dist/inMemoryDatabase.js +3 -2
- package/dist/inMemoryDatabase.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +4 -3
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +3 -2
- package/dist/react/index.js.map +1 -1
- package/dist/react/useComponentState.d.ts +50 -0
- package/dist/react/useComponentState.d.ts.map +1 -0
- package/dist/react/useComponentState.js +240 -0
- package/dist/react/useComponentState.js.map +1 -0
- package/dist/react/useGlobalQuery.d.ts +3 -0
- package/dist/react/useGlobalQuery.d.ts.map +1 -0
- package/dist/react/useGlobalQuery.js +26 -0
- package/dist/react/useGlobalQuery.js.map +1 -0
- package/dist/react/useGraphQL.d.ts +3 -3
- package/dist/react/useGraphQL.d.ts.map +1 -1
- package/dist/react/useGraphQL.js +10 -8
- package/dist/react/useGraphQL.js.map +1 -1
- package/dist/react/useLiveStoreComponent.d.ts +6 -6
- package/dist/react/useLiveStoreComponent.d.ts.map +1 -1
- package/dist/react/useLiveStoreComponent.js +143 -99
- package/dist/react/useLiveStoreComponent.js.map +1 -1
- package/dist/react/useQuery.d.ts +2 -2
- package/dist/react/useQuery.d.ts.map +1 -1
- package/dist/react/useQuery.js +54 -30
- package/dist/react/useQuery.js.map +1 -1
- package/dist/react/useTemporaryQuery.d.ts +8 -0
- package/dist/react/useTemporaryQuery.d.ts.map +1 -0
- package/dist/react/useTemporaryQuery.js +19 -0
- package/dist/react/useTemporaryQuery.js.map +1 -0
- package/dist/react/utils/extractNamesFromStackTrace.d.ts +3 -0
- package/dist/react/utils/extractNamesFromStackTrace.d.ts.map +1 -0
- package/dist/react/utils/extractNamesFromStackTrace.js +40 -0
- package/dist/react/utils/extractNamesFromStackTrace.js.map +1 -0
- package/dist/react/utils/extractStackInfoFromStackTrace.d.ts +7 -0
- package/dist/react/utils/extractStackInfoFromStackTrace.d.ts.map +1 -0
- package/dist/react/utils/extractStackInfoFromStackTrace.js +40 -0
- package/dist/react/utils/extractStackInfoFromStackTrace.js.map +1 -0
- package/dist/react/utils/stack-info.d.ts +11 -0
- package/dist/react/utils/stack-info.d.ts.map +1 -0
- package/dist/react/utils/stack-info.js +49 -0
- package/dist/react/utils/stack-info.js.map +1 -0
- package/dist/reactive.d.ts +51 -67
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +138 -220
- package/dist/reactive.js.map +1 -1
- package/dist/reactiveQueries/base-class.d.ts +28 -21
- package/dist/reactiveQueries/base-class.d.ts.map +1 -1
- package/dist/reactiveQueries/base-class.js +22 -18
- package/dist/reactiveQueries/base-class.js.map +1 -1
- package/dist/reactiveQueries/graph.d.ts +10 -0
- package/dist/reactiveQueries/graph.d.ts.map +1 -0
- package/dist/reactiveQueries/graph.js +6 -0
- package/dist/reactiveQueries/graph.js.map +1 -0
- package/dist/reactiveQueries/graphql.d.ts +35 -17
- package/dist/reactiveQueries/graphql.d.ts.map +1 -1
- package/dist/reactiveQueries/graphql.js +86 -10
- package/dist/reactiveQueries/graphql.js.map +1 -1
- package/dist/reactiveQueries/js.d.ts +17 -12
- package/dist/reactiveQueries/js.d.ts.map +1 -1
- package/dist/reactiveQueries/js.js +30 -8
- package/dist/reactiveQueries/js.js.map +1 -1
- package/dist/reactiveQueries/sql.d.ts +28 -18
- package/dist/reactiveQueries/sql.d.ts.map +1 -1
- package/dist/reactiveQueries/sql.js +79 -16
- package/dist/reactiveQueries/sql.js.map +1 -1
- package/dist/store.d.ts +35 -61
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +77 -272
- package/dist/store.js.map +1 -1
- package/package.json +4 -3
- package/src/QueryCache.ts +1 -1
- package/src/__tests__/react/fixture.tsx +10 -8
- package/src/__tests__/react/{useLiveStoreComponent.test.tsx → useComponentState.test.tsx} +9 -20
- package/src/__tests__/react/useQuery.test.tsx +48 -0
- package/src/__tests__/react/utils/stack-info.test.ts +45 -0
- package/src/__tests__/reactive.test.ts +212 -140
- package/src/__tests__/reactiveQueries/sql.test.ts +372 -0
- package/src/inMemoryDatabase.ts +11 -8
- package/src/index.ts +7 -11
- package/src/react/index.ts +4 -7
- package/src/react/{useLiveStoreComponent.ts → useComponentState.ts} +90 -253
- package/src/react/useQuery.ts +74 -40
- package/src/react/useTemporaryQuery.ts +23 -0
- package/src/react/utils/stack-info.ts +63 -0
- package/src/reactive.ts +234 -308
- package/src/reactiveQueries/base-class.ts +59 -42
- package/src/reactiveQueries/graph.ts +15 -0
- package/src/reactiveQueries/graphql.ts +143 -29
- package/src/reactiveQueries/js.ts +57 -20
- package/src/reactiveQueries/sql.ts +136 -36
- package/src/store.ts +121 -426
- 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 {
|
|
20
|
-
import { ReactiveGraph } from './reactive.js'
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
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 {
|
|
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 = {
|
|
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
|
-
|
|
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
|
|
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 = <
|
|
490
|
-
query:
|
|
491
|
-
onNewValue: (value:
|
|
492
|
-
|
|
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
|
-
|
|
209
|
+
options?.otelContext ?? this.otel.queriesSpanContext,
|
|
499
210
|
(span) => {
|
|
500
211
|
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
501
212
|
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
|
|
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
|
-
|
|
517
|
-
|
|
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
|
-
|
|
558
|
-
|
|
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
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
+
}
|