@livestore/livestore 0.0.0-snapshot-909cdd1ac2fd591945c2be2b0f53e14d87f3c9d4
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 +1 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/QueryCache.d.ts +20 -0
- package/dist/QueryCache.d.ts.map +1 -0
- package/dist/QueryCache.js +61 -0
- package/dist/QueryCache.js.map +1 -0
- package/dist/SynchronousDatabaseWrapper.d.ts +36 -0
- package/dist/SynchronousDatabaseWrapper.d.ts.map +1 -0
- package/dist/SynchronousDatabaseWrapper.js +176 -0
- package/dist/SynchronousDatabaseWrapper.js.map +1 -0
- package/dist/effect/LiveStore.d.ts +38 -0
- package/dist/effect/LiveStore.d.ts.map +1 -0
- package/dist/effect/LiveStore.js +38 -0
- package/dist/effect/LiveStore.js.map +1 -0
- package/dist/effect/index.d.ts +2 -0
- package/dist/effect/index.d.ts.map +1 -0
- package/dist/effect/index.js +2 -0
- package/dist/effect/index.js.map +1 -0
- package/dist/global-state.d.ts +14 -0
- package/dist/global-state.d.ts.map +1 -0
- package/dist/global-state.js +16 -0
- package/dist/global-state.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/reactive.d.ts +163 -0
- package/dist/reactive.d.ts.map +1 -0
- package/dist/reactive.js +382 -0
- package/dist/reactive.js.map +1 -0
- package/dist/reactive.test.d.ts +2 -0
- package/dist/reactive.test.d.ts.map +1 -0
- package/dist/reactive.test.js +345 -0
- package/dist/reactive.test.js.map +1 -0
- package/dist/reactiveQueries/base-class.d.ts +59 -0
- package/dist/reactiveQueries/base-class.d.ts.map +1 -0
- package/dist/reactiveQueries/base-class.js +29 -0
- package/dist/reactiveQueries/base-class.js.map +1 -0
- package/dist/reactiveQueries/graphql.d.ts +52 -0
- package/dist/reactiveQueries/graphql.d.ts.map +1 -0
- package/dist/reactiveQueries/graphql.js +136 -0
- package/dist/reactiveQueries/graphql.js.map +1 -0
- package/dist/reactiveQueries/js.d.ts +35 -0
- package/dist/reactiveQueries/js.d.ts.map +1 -0
- package/dist/reactiveQueries/js.js +57 -0
- package/dist/reactiveQueries/js.js.map +1 -0
- package/dist/reactiveQueries/sql.d.ts +49 -0
- package/dist/reactiveQueries/sql.d.ts.map +1 -0
- package/dist/reactiveQueries/sql.js +130 -0
- package/dist/reactiveQueries/sql.js.map +1 -0
- package/dist/reactiveQueries/sql.test.d.ts +2 -0
- package/dist/reactiveQueries/sql.test.d.ts.map +1 -0
- package/dist/reactiveQueries/sql.test.js +284 -0
- package/dist/reactiveQueries/sql.test.js.map +1 -0
- package/dist/row-query.d.ts +33 -0
- package/dist/row-query.d.ts.map +1 -0
- package/dist/row-query.js +84 -0
- package/dist/row-query.js.map +1 -0
- package/dist/store-context.d.ts +26 -0
- package/dist/store-context.d.ts.map +1 -0
- package/dist/store-context.js +6 -0
- package/dist/store-context.js.map +1 -0
- package/dist/store-devtools.d.ts +19 -0
- package/dist/store-devtools.d.ts.map +1 -0
- package/dist/store-devtools.js +141 -0
- package/dist/store-devtools.js.map +1 -0
- package/dist/store.d.ts +175 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +507 -0
- package/dist/store.js.map +1 -0
- package/dist/utils/data-structures.d.ts +10 -0
- package/dist/utils/data-structures.d.ts.map +1 -0
- package/dist/utils/data-structures.js +32 -0
- package/dist/utils/data-structures.js.map +1 -0
- package/dist/utils/dev.d.ts +3 -0
- package/dist/utils/dev.d.ts.map +1 -0
- package/dist/utils/dev.js +17 -0
- package/dist/utils/dev.js.map +1 -0
- package/dist/utils/otel.d.ts +4 -0
- package/dist/utils/otel.d.ts.map +1 -0
- package/dist/utils/otel.js +6 -0
- package/dist/utils/otel.js.map +1 -0
- package/dist/utils/stack-info.d.ts +10 -0
- package/dist/utils/stack-info.d.ts.map +1 -0
- package/dist/utils/stack-info.js +41 -0
- package/dist/utils/stack-info.js.map +1 -0
- package/dist/utils/stack-info.test.d.ts +2 -0
- package/dist/utils/stack-info.test.d.ts.map +1 -0
- package/dist/utils/stack-info.test.js +75 -0
- package/dist/utils/stack-info.test.js.map +1 -0
- package/dist/utils/tests/fixture.d.ts +259 -0
- package/dist/utils/tests/fixture.d.ts.map +1 -0
- package/dist/utils/tests/fixture.js +33 -0
- package/dist/utils/tests/fixture.js.map +1 -0
- package/dist/utils/tests/mod.d.ts +3 -0
- package/dist/utils/tests/mod.d.ts.map +1 -0
- package/dist/utils/tests/mod.js +3 -0
- package/dist/utils/tests/mod.js.map +1 -0
- package/dist/utils/tests/otel.d.ts +10 -0
- package/dist/utils/tests/otel.d.ts.map +1 -0
- package/dist/utils/tests/otel.js +42 -0
- package/dist/utils/tests/otel.js.map +1 -0
- package/package.json +60 -0
- package/src/QueryCache.ts +81 -0
- package/src/SynchronousDatabaseWrapper.ts +256 -0
- package/src/ambient.d.ts +10 -0
- package/src/effect/LiveStore.ts +112 -0
- package/src/effect/index.ts +8 -0
- package/src/global-state.ts +20 -0
- package/src/index.ts +64 -0
- package/src/reactive.test.ts +426 -0
- package/src/reactive.ts +661 -0
- package/src/reactiveQueries/base-class.ts +115 -0
- package/src/reactiveQueries/graphql.ts +233 -0
- package/src/reactiveQueries/js.ts +108 -0
- package/src/reactiveQueries/sql.test.ts +308 -0
- package/src/reactiveQueries/sql.ts +226 -0
- package/src/row-query.ts +200 -0
- package/src/store-context.ts +23 -0
- package/src/store-devtools.ts +217 -0
- package/src/store.ts +920 -0
- package/src/utils/data-structures.ts +36 -0
- package/src/utils/dev.ts +24 -0
- package/src/utils/otel.ts +9 -0
- package/src/utils/stack-info.test.ts +79 -0
- package/src/utils/stack-info.ts +54 -0
- package/src/utils/tests/fixture.ts +77 -0
- package/src/utils/tests/mod.ts +2 -0
- package/src/utils/tests/otel.ts +61 -0
- package/tsconfig.json +18 -0
- package/vitest.config.js +9 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { QueryInfo, QueryInfoNone } from '@livestore/common'
|
|
2
|
+
import type * as otel from '@opentelemetry/api'
|
|
3
|
+
|
|
4
|
+
import { type Atom, type GetAtom, ReactiveGraph, throwContextNotSetError, type Thunk } from '../reactive.js'
|
|
5
|
+
import type { QueryDebugInfo, RefreshReason, Store } from '../store.js'
|
|
6
|
+
import type { StackInfo } from '../utils/stack-info.js'
|
|
7
|
+
|
|
8
|
+
export type ReactivityGraph = ReactiveGraph<RefreshReason, QueryDebugInfo, QueryContext>
|
|
9
|
+
|
|
10
|
+
export const makeReactivityGraph = (): ReactivityGraph =>
|
|
11
|
+
new ReactiveGraph<RefreshReason, QueryDebugInfo, QueryContext>()
|
|
12
|
+
|
|
13
|
+
export type QueryContext = {
|
|
14
|
+
store: Store
|
|
15
|
+
otelTracer: otel.Tracer
|
|
16
|
+
rootOtelContext: otel.Context
|
|
17
|
+
effectsWrapper: (run: () => void) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type UnsubscribeQuery = () => void
|
|
21
|
+
|
|
22
|
+
export type GetResult<TQuery extends LiveQueryAny> =
|
|
23
|
+
TQuery extends LiveQuery<infer TResult, infer _1> ? TResult : unknown
|
|
24
|
+
|
|
25
|
+
let queryIdCounter = 0
|
|
26
|
+
|
|
27
|
+
export type LiveQueryAny = LiveQuery<any, QueryInfo>
|
|
28
|
+
|
|
29
|
+
export interface LiveQuery<TResult, TQueryInfo extends QueryInfo = QueryInfoNone> {
|
|
30
|
+
id: number
|
|
31
|
+
_tag: 'js' | 'sql' | 'graphql'
|
|
32
|
+
|
|
33
|
+
/** This should only be used on a type-level and doesn't hold any value during runtime */
|
|
34
|
+
'__result!': TResult
|
|
35
|
+
|
|
36
|
+
/** A reactive thunk representing the query results */
|
|
37
|
+
results$: Thunk<TResult, QueryContext, RefreshReason>
|
|
38
|
+
|
|
39
|
+
label: string
|
|
40
|
+
|
|
41
|
+
run: (otelContext?: otel.Context, debugRefreshReason?: RefreshReason) => TResult
|
|
42
|
+
|
|
43
|
+
runAndDestroy: (otelContext?: otel.Context, debugRefreshReason?: RefreshReason) => TResult
|
|
44
|
+
|
|
45
|
+
destroy(): void
|
|
46
|
+
|
|
47
|
+
subscribe(
|
|
48
|
+
onNewValue: (value: TResult) => void,
|
|
49
|
+
onUnsubsubscribe?: () => void,
|
|
50
|
+
options?: { label?: string; otelContext?: otel.Context },
|
|
51
|
+
): () => void
|
|
52
|
+
|
|
53
|
+
activeSubscriptions: Set<StackInfo>
|
|
54
|
+
|
|
55
|
+
queryInfo: TQueryInfo
|
|
56
|
+
|
|
57
|
+
runs: number
|
|
58
|
+
|
|
59
|
+
executionTimes: number[]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export abstract class LiveStoreQueryBase<TResult, TQueryInfo extends QueryInfo>
|
|
63
|
+
implements LiveQuery<TResult, TQueryInfo>
|
|
64
|
+
{
|
|
65
|
+
'__result!'!: TResult
|
|
66
|
+
id = queryIdCounter++
|
|
67
|
+
abstract _tag: 'js' | 'sql' | 'graphql'
|
|
68
|
+
|
|
69
|
+
/** Human-readable label for the query for debugging */
|
|
70
|
+
abstract label: string
|
|
71
|
+
|
|
72
|
+
abstract results$: Thunk<TResult, QueryContext, RefreshReason>
|
|
73
|
+
|
|
74
|
+
activeSubscriptions: Set<StackInfo> = new Set()
|
|
75
|
+
|
|
76
|
+
protected abstract reactivityGraph: ReactivityGraph
|
|
77
|
+
|
|
78
|
+
abstract queryInfo: TQueryInfo
|
|
79
|
+
|
|
80
|
+
get runs() {
|
|
81
|
+
return this.results$.recomputations
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
executionTimes: number[] = []
|
|
85
|
+
|
|
86
|
+
abstract destroy: () => void
|
|
87
|
+
|
|
88
|
+
run = (otelContext?: otel.Context, debugRefreshReason?: RefreshReason): TResult =>
|
|
89
|
+
this.results$.computeResult(otelContext, debugRefreshReason)
|
|
90
|
+
|
|
91
|
+
runAndDestroy = (otelContext?: otel.Context, debugRefreshReason?: RefreshReason): TResult => {
|
|
92
|
+
const result = this.run(otelContext, debugRefreshReason)
|
|
93
|
+
this.destroy()
|
|
94
|
+
return result
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
subscribe = (
|
|
98
|
+
onNewValue: (value: TResult) => void,
|
|
99
|
+
onUnsubsubscribe?: () => void,
|
|
100
|
+
options?: { label?: string; otelContext?: otel.Context } | undefined,
|
|
101
|
+
): (() => void) =>
|
|
102
|
+
this.reactivityGraph.context?.store.subscribe(this, onNewValue, onUnsubsubscribe, options) ??
|
|
103
|
+
throwContextNotSetError(this.reactivityGraph)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type GetAtomResult = <T>(atom: Atom<T, any, RefreshReason> | LiveQuery<T, any>) => T
|
|
107
|
+
|
|
108
|
+
export const makeGetAtomResult = (get: GetAtom, otelContext: otel.Context) => {
|
|
109
|
+
const getAtom: GetAtomResult = (atom) => {
|
|
110
|
+
if (atom._tag === 'thunk' || atom._tag === 'ref') return get(atom, otelContext)
|
|
111
|
+
return get(atom.results$, otelContext)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return getAtom
|
|
115
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'
|
|
2
|
+
import type { QueryInfoNone } from '@livestore/common'
|
|
3
|
+
import { shouldNeverHappen } from '@livestore/utils'
|
|
4
|
+
import { Schema, TreeFormatter } from '@livestore/utils/effect'
|
|
5
|
+
import * as otel from '@opentelemetry/api'
|
|
6
|
+
import * as graphql from 'graphql'
|
|
7
|
+
|
|
8
|
+
import { globalReactivityGraph } from '../global-state.js'
|
|
9
|
+
import { isThunk, type Thunk } from '../reactive.js'
|
|
10
|
+
import type { BaseGraphQLContext, RefreshReason, Store } from '../store.js'
|
|
11
|
+
import { getDurationMsFromSpan } from '../utils/otel.js'
|
|
12
|
+
import type { GetAtomResult, LiveQuery, QueryContext, ReactivityGraph } from './base-class.js'
|
|
13
|
+
import { LiveStoreQueryBase, makeGetAtomResult } from './base-class.js'
|
|
14
|
+
|
|
15
|
+
export type MapResult<To, From> = ((res: From, get: GetAtomResult) => To) | Schema.Schema<To, From>
|
|
16
|
+
|
|
17
|
+
export const queryGraphQL = <
|
|
18
|
+
TResult extends Record<string, any>,
|
|
19
|
+
TVariableValues extends Record<string, any>,
|
|
20
|
+
TResultMapped extends Record<string, any> = TResult,
|
|
21
|
+
>(
|
|
22
|
+
document: DocumentNode<TResult, TVariableValues>,
|
|
23
|
+
genVariableValues: TVariableValues | ((get: GetAtomResult) => TVariableValues),
|
|
24
|
+
{
|
|
25
|
+
label,
|
|
26
|
+
reactivityGraph,
|
|
27
|
+
map,
|
|
28
|
+
}: {
|
|
29
|
+
label?: string
|
|
30
|
+
reactivityGraph?: ReactivityGraph
|
|
31
|
+
map?: MapResult<TResultMapped, TResult>
|
|
32
|
+
} = {},
|
|
33
|
+
): LiveQuery<TResultMapped, QueryInfoNone> =>
|
|
34
|
+
new LiveStoreGraphQLQuery({ document, genVariableValues, label, reactivityGraph, map })
|
|
35
|
+
|
|
36
|
+
export class LiveStoreGraphQLQuery<
|
|
37
|
+
TResult extends Record<string, any>,
|
|
38
|
+
TVariableValues extends Record<string, any>,
|
|
39
|
+
TContext extends BaseGraphQLContext,
|
|
40
|
+
TResultMapped extends Record<string, any> = TResult,
|
|
41
|
+
> extends LiveStoreQueryBase<TResultMapped, QueryInfoNone> {
|
|
42
|
+
_tag: 'graphql' = 'graphql'
|
|
43
|
+
|
|
44
|
+
/** The abstract GraphQL query */
|
|
45
|
+
document: DocumentNode<TResult, TVariableValues>
|
|
46
|
+
|
|
47
|
+
/** A reactive thunk representing the query results */
|
|
48
|
+
results$: Thunk<TResultMapped, QueryContext, RefreshReason>
|
|
49
|
+
|
|
50
|
+
variableValues$: Thunk<TVariableValues, QueryContext, RefreshReason> | undefined
|
|
51
|
+
|
|
52
|
+
label: string
|
|
53
|
+
|
|
54
|
+
protected reactivityGraph: ReactivityGraph
|
|
55
|
+
|
|
56
|
+
queryInfo: QueryInfoNone = { _tag: 'None' }
|
|
57
|
+
|
|
58
|
+
private mapResult
|
|
59
|
+
|
|
60
|
+
constructor({
|
|
61
|
+
document,
|
|
62
|
+
label,
|
|
63
|
+
genVariableValues,
|
|
64
|
+
reactivityGraph,
|
|
65
|
+
map,
|
|
66
|
+
}: {
|
|
67
|
+
document: DocumentNode<TResult, TVariableValues>
|
|
68
|
+
genVariableValues: TVariableValues | ((get: GetAtomResult) => TVariableValues)
|
|
69
|
+
label?: string
|
|
70
|
+
reactivityGraph?: ReactivityGraph
|
|
71
|
+
map?: MapResult<TResultMapped, TResult>
|
|
72
|
+
}) {
|
|
73
|
+
super()
|
|
74
|
+
|
|
75
|
+
const labelWithDefault = label ?? graphql.getOperationAST(document)?.name?.value ?? 'graphql'
|
|
76
|
+
|
|
77
|
+
this.label = labelWithDefault
|
|
78
|
+
this.document = document
|
|
79
|
+
|
|
80
|
+
this.reactivityGraph = reactivityGraph ?? globalReactivityGraph
|
|
81
|
+
|
|
82
|
+
this.mapResult =
|
|
83
|
+
map === undefined
|
|
84
|
+
? (res: TResult) => res as any as TResultMapped
|
|
85
|
+
: Schema.isSchema(map)
|
|
86
|
+
? (res: TResult) => {
|
|
87
|
+
const parseResult = Schema.decodeEither(map as Schema.Schema<TResultMapped, TResult>)(res)
|
|
88
|
+
if (parseResult._tag === 'Left') {
|
|
89
|
+
console.error(`Error parsing GraphQL query result: ${TreeFormatter.formatErrorSync(parseResult.left)}`)
|
|
90
|
+
return shouldNeverHappen(`Error parsing SQL query result: ${parseResult.left}`)
|
|
91
|
+
} else {
|
|
92
|
+
return parseResult.right as TResultMapped
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
: typeof map === 'function'
|
|
96
|
+
? map
|
|
97
|
+
: shouldNeverHappen(`Invalid map function ${map}`)
|
|
98
|
+
|
|
99
|
+
// TODO don't even create a thunk if variables are static
|
|
100
|
+
let variableValues$OrvariableValues
|
|
101
|
+
|
|
102
|
+
if (typeof genVariableValues === 'function') {
|
|
103
|
+
variableValues$OrvariableValues = this.reactivityGraph.makeThunk(
|
|
104
|
+
(get, _setDebugInfo, { rootOtelContext }, otelContext) => {
|
|
105
|
+
return genVariableValues(makeGetAtomResult(get, otelContext ?? rootOtelContext))
|
|
106
|
+
},
|
|
107
|
+
{ label: `${labelWithDefault}:variableValues`, meta: { liveStoreThunkType: 'graphqlVariableValues' } },
|
|
108
|
+
)
|
|
109
|
+
this.variableValues$ = variableValues$OrvariableValues
|
|
110
|
+
} else {
|
|
111
|
+
variableValues$OrvariableValues = genVariableValues
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const resultsLabel = `${labelWithDefault}:results`
|
|
115
|
+
this.results$ = this.reactivityGraph.makeThunk<TResultMapped>(
|
|
116
|
+
(get, setDebugInfo, { store, otelTracer, rootOtelContext }, otelContext) => {
|
|
117
|
+
const variableValues = isThunk(variableValues$OrvariableValues)
|
|
118
|
+
? (get(variableValues$OrvariableValues) as TVariableValues)
|
|
119
|
+
: (variableValues$OrvariableValues as TVariableValues)
|
|
120
|
+
const { result, queriedTables, durationMs } = this.queryOnce({
|
|
121
|
+
document,
|
|
122
|
+
variableValues,
|
|
123
|
+
otelContext: otelContext ?? rootOtelContext,
|
|
124
|
+
otelTracer,
|
|
125
|
+
store: store as Store<TContext>,
|
|
126
|
+
get: makeGetAtomResult(get, otelContext ?? rootOtelContext),
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// Add dependencies on any tables that were used
|
|
130
|
+
for (const tableName of queriedTables) {
|
|
131
|
+
const tableRef = store.tableRefs[tableName] ?? shouldNeverHappen(`No table ref found for ${tableName}`)
|
|
132
|
+
get(tableRef)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
setDebugInfo({ _tag: 'graphql', label: resultsLabel, query: graphql.print(document), durationMs })
|
|
136
|
+
|
|
137
|
+
return result
|
|
138
|
+
},
|
|
139
|
+
{ label: resultsLabel, meta: { liveStoreThunkType: 'graphqlResults' } },
|
|
140
|
+
// otelContext,
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Returns a new reactive query that contains the result of
|
|
146
|
+
* running an arbitrary JS computation on the results of this SQL query.
|
|
147
|
+
*/
|
|
148
|
+
// pipe = <U>(fn: (result: TResult, get: GetAtomResult) => U): LiveStoreJSQuery<U> =>
|
|
149
|
+
// new LiveStoreJSQuery({
|
|
150
|
+
// fn: (get) => {
|
|
151
|
+
// const results = get(this.results$)
|
|
152
|
+
// return fn(results, get)
|
|
153
|
+
// },
|
|
154
|
+
// label: `${this.label}:js`,
|
|
155
|
+
// onDestroy: () => this.destroy(),
|
|
156
|
+
// reactivityGraph: this.reactivityGraph,
|
|
157
|
+
// })
|
|
158
|
+
|
|
159
|
+
queryOnce = ({
|
|
160
|
+
document,
|
|
161
|
+
otelContext,
|
|
162
|
+
otelTracer,
|
|
163
|
+
variableValues,
|
|
164
|
+
store,
|
|
165
|
+
get,
|
|
166
|
+
}: {
|
|
167
|
+
document: graphql.DocumentNode
|
|
168
|
+
otelContext: otel.Context
|
|
169
|
+
otelTracer: otel.Tracer
|
|
170
|
+
variableValues: TVariableValues
|
|
171
|
+
store: Store<TContext>
|
|
172
|
+
get: GetAtomResult
|
|
173
|
+
}) => {
|
|
174
|
+
const schema =
|
|
175
|
+
store.graphQLSchema ?? shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL schema")
|
|
176
|
+
const context =
|
|
177
|
+
store.graphQLContext ?? shouldNeverHappen("Can't run a GraphQL query on a store without GraphQL context")
|
|
178
|
+
|
|
179
|
+
const operationName = graphql.getOperationAST(document)?.name?.value
|
|
180
|
+
|
|
181
|
+
return otelTracer.startActiveSpan(`executeGraphQLQuery: ${operationName}`, {}, otelContext, (span) => {
|
|
182
|
+
span.setAttribute('graphql.variables', JSON.stringify(variableValues))
|
|
183
|
+
span.setAttribute('graphql.query', graphql.print(document))
|
|
184
|
+
|
|
185
|
+
context.queriedTables.clear()
|
|
186
|
+
|
|
187
|
+
context.otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
188
|
+
|
|
189
|
+
const res = graphql.executeSync({
|
|
190
|
+
document,
|
|
191
|
+
contextValue: context,
|
|
192
|
+
schema: schema,
|
|
193
|
+
variableValues,
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
// TODO track number of nested SQL queries via Otel + debug info
|
|
197
|
+
|
|
198
|
+
if (res.errors) {
|
|
199
|
+
span.setStatus({ code: otel.SpanStatusCode.ERROR, message: 'GraphQL error' })
|
|
200
|
+
span.setAttribute('graphql.error', res.errors.join('\n'))
|
|
201
|
+
span.setAttribute('graphql.error-detail', JSON.stringify(res.errors))
|
|
202
|
+
console.error(`graphql error (${operationName}) - ${res.errors.length} errors`)
|
|
203
|
+
for (const error of res.errors) {
|
|
204
|
+
console.error(error)
|
|
205
|
+
}
|
|
206
|
+
debugger
|
|
207
|
+
shouldNeverHappen(`GraphQL error: ${res.errors.join('\n')}`)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
span.end()
|
|
211
|
+
|
|
212
|
+
const result = this.mapResult(res.data as unknown as TResult, get)
|
|
213
|
+
|
|
214
|
+
const durationMs = getDurationMsFromSpan(span)
|
|
215
|
+
|
|
216
|
+
this.executionTimes.push(durationMs)
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
result,
|
|
220
|
+
queriedTables: Array.from(context.queriedTables.values()),
|
|
221
|
+
durationMs,
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
destroy = () => {
|
|
227
|
+
if (this.variableValues$ !== undefined) {
|
|
228
|
+
this.reactivityGraph.destroyNode(this.variableValues$)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this.reactivityGraph.destroyNode(this.results$)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { QueryInfo, QueryInfoNone } from '@livestore/common'
|
|
2
|
+
import * as otel from '@opentelemetry/api'
|
|
3
|
+
|
|
4
|
+
import { globalReactivityGraph } from '../global-state.js'
|
|
5
|
+
import type { Thunk } from '../reactive.js'
|
|
6
|
+
import type { RefreshReason } from '../store.js'
|
|
7
|
+
import { getDurationMsFromSpan } from '../utils/otel.js'
|
|
8
|
+
import type { GetAtomResult, LiveQuery, QueryContext, ReactivityGraph } from './base-class.js'
|
|
9
|
+
import { LiveStoreQueryBase, makeGetAtomResult } from './base-class.js'
|
|
10
|
+
|
|
11
|
+
export const computed = <TResult, TQueryInfo extends QueryInfo = QueryInfoNone>(
|
|
12
|
+
fn: (get: GetAtomResult) => TResult,
|
|
13
|
+
options?: {
|
|
14
|
+
label: string
|
|
15
|
+
reactivityGraph?: ReactivityGraph
|
|
16
|
+
queryInfo?: TQueryInfo
|
|
17
|
+
},
|
|
18
|
+
): LiveQuery<TResult, TQueryInfo> =>
|
|
19
|
+
new LiveStoreJSQuery<TResult, TQueryInfo>({
|
|
20
|
+
fn,
|
|
21
|
+
label: options?.label ?? fn.toString(),
|
|
22
|
+
reactivityGraph: options?.reactivityGraph,
|
|
23
|
+
queryInfo: options?.queryInfo,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export class LiveStoreJSQuery<TResult, TQueryInfo extends QueryInfo = QueryInfoNone> extends LiveStoreQueryBase<
|
|
27
|
+
TResult,
|
|
28
|
+
TQueryInfo
|
|
29
|
+
> {
|
|
30
|
+
_tag: 'js' = 'js'
|
|
31
|
+
|
|
32
|
+
/** A reactive thunk representing the query results */
|
|
33
|
+
results$: Thunk<TResult, QueryContext, RefreshReason>
|
|
34
|
+
|
|
35
|
+
label: string
|
|
36
|
+
|
|
37
|
+
protected reactivityGraph: ReactivityGraph
|
|
38
|
+
|
|
39
|
+
queryInfo: TQueryInfo
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Currently only used for "nested destruction" of piped queries
|
|
43
|
+
*
|
|
44
|
+
* i.e. when doing something like `const q = querySQL(...).pipe(...)`
|
|
45
|
+
* we need to also destory the SQL query when the JS query `q` is destroyed
|
|
46
|
+
*/
|
|
47
|
+
private onDestroy: (() => void) | undefined
|
|
48
|
+
|
|
49
|
+
constructor({
|
|
50
|
+
fn,
|
|
51
|
+
label,
|
|
52
|
+
onDestroy,
|
|
53
|
+
reactivityGraph,
|
|
54
|
+
queryInfo,
|
|
55
|
+
}: {
|
|
56
|
+
label: string
|
|
57
|
+
fn: (get: GetAtomResult) => TResult
|
|
58
|
+
/** Currently only used for "nested destruction" of piped queries */
|
|
59
|
+
onDestroy?: () => void
|
|
60
|
+
reactivityGraph?: ReactivityGraph
|
|
61
|
+
queryInfo?: TQueryInfo
|
|
62
|
+
}) {
|
|
63
|
+
super()
|
|
64
|
+
|
|
65
|
+
this.onDestroy = onDestroy
|
|
66
|
+
this.label = label
|
|
67
|
+
|
|
68
|
+
this.reactivityGraph = reactivityGraph ?? globalReactivityGraph
|
|
69
|
+
this.queryInfo = queryInfo ?? ({ _tag: 'None' } as TQueryInfo)
|
|
70
|
+
|
|
71
|
+
const queryLabel = `${label}:results`
|
|
72
|
+
|
|
73
|
+
this.results$ = this.reactivityGraph.makeThunk(
|
|
74
|
+
(get, setDebugInfo, { otelTracer, rootOtelContext }, otelContext) =>
|
|
75
|
+
otelTracer.startActiveSpan(`js:${label}`, {}, otelContext ?? rootOtelContext, (span) => {
|
|
76
|
+
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
77
|
+
const res = fn(makeGetAtomResult(get, otelContext))
|
|
78
|
+
|
|
79
|
+
span.end()
|
|
80
|
+
|
|
81
|
+
const durationMs = getDurationMsFromSpan(span)
|
|
82
|
+
|
|
83
|
+
this.executionTimes.push(durationMs)
|
|
84
|
+
|
|
85
|
+
setDebugInfo({ _tag: 'js', label, query: fn.toString(), durationMs })
|
|
86
|
+
|
|
87
|
+
return res
|
|
88
|
+
}),
|
|
89
|
+
{ label: queryLabel, meta: { liveStoreThunkType: 'jsResults' } },
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// pipe = <U>(fn: (result: TResult, get: GetAtomResult) => U): LiveStoreJSQuery<U> =>
|
|
94
|
+
// new LiveStoreJSQuery({
|
|
95
|
+
// fn: (get) => {
|
|
96
|
+
// const results = get(this.results$)
|
|
97
|
+
// return fn(results, get)
|
|
98
|
+
// },
|
|
99
|
+
// label: `${this.label}:js`,
|
|
100
|
+
// onDestroy: () => this.destroy(),
|
|
101
|
+
// reactivityGraph: this.reactivityGraph,
|
|
102
|
+
// })
|
|
103
|
+
|
|
104
|
+
destroy = () => {
|
|
105
|
+
this.reactivityGraph.destroyNode(this.results$)
|
|
106
|
+
this.onDestroy?.()
|
|
107
|
+
}
|
|
108
|
+
}
|