@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
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { LiteralUnion, PrettifyFlat } from '@livestore/utils'
|
|
1
|
+
import type { LiteralUnion } from '@livestore/utils'
|
|
3
2
|
import { omit, shouldNeverHappen } from '@livestore/utils'
|
|
4
3
|
import { Schema } from '@livestore/utils/effect'
|
|
5
4
|
import * as otel from '@opentelemetry/api'
|
|
@@ -12,63 +11,21 @@ import { v4 as uuid } from 'uuid'
|
|
|
12
11
|
import type { ComponentKey } from '../componentKey.js'
|
|
13
12
|
import { labelForKey, tableNameForComponentKey } from '../componentKey.js'
|
|
14
13
|
import { migrateTable } from '../migrations.js'
|
|
15
|
-
import
|
|
16
|
-
import
|
|
17
|
-
import type { LiveStoreSQLQuery } from '../reactiveQueries/sql.js'
|
|
14
|
+
import { LiveStoreJSQuery } from '../reactiveQueries/js.js'
|
|
15
|
+
import { LiveStoreSQLQuery } from '../reactiveQueries/sql.js'
|
|
18
16
|
import { SCHEMA_META_TABLE } from '../schema.js'
|
|
19
|
-
import type { BaseGraphQLContext,
|
|
20
|
-
import type { Bindable } from '../util.js'
|
|
17
|
+
import type { BaseGraphQLContext, LiveStoreQuery, Store } from '../store.js'
|
|
21
18
|
import { sql } from '../util.js'
|
|
22
19
|
import { useStore } from './LiveStoreContext.js'
|
|
20
|
+
import { extractStackInfoFromStackTrace, originalStackLimit } from './utils/stack-info.js'
|
|
23
21
|
import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInput.js'
|
|
24
22
|
|
|
25
23
|
export interface QueryDefinitions {
|
|
26
24
|
[queryName: string]: LiveStoreQuery
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
export type
|
|
30
|
-
|
|
31
|
-
export type ReactiveSQL = <TResult>(
|
|
32
|
-
query: string | ((get: GetAtomResult) => string),
|
|
33
|
-
queriedTables: string[],
|
|
34
|
-
bindValues?: Bindable | undefined,
|
|
35
|
-
) => LiveStoreSQLQuery<TResult>
|
|
36
|
-
|
|
37
|
-
export type ReactiveJS = <TResult>(query: (get: GetAtomResult) => TResult) => LiveStoreJSQuery<TResult>
|
|
38
|
-
|
|
39
|
-
export type ReactiveGraphQL = <
|
|
40
|
-
TResult extends Record<string, any>,
|
|
41
|
-
TVariables extends Record<string, any>,
|
|
42
|
-
TContext extends BaseGraphQLContext,
|
|
43
|
-
>(
|
|
44
|
-
query: DocumentNode<TResult, TVariables>,
|
|
45
|
-
variableValues: TVariables | ((get: GetAtomResult) => TVariables),
|
|
46
|
-
label?: string,
|
|
47
|
-
) => LiveStoreGraphQLQuery<TResult, TVariables, TContext>
|
|
48
|
-
|
|
49
|
-
type RegisterSubscription = <TQuery extends LiveStoreQuery>(
|
|
50
|
-
query: TQuery,
|
|
51
|
-
onNewValue: (value: QueryResult<TQuery>) => void,
|
|
52
|
-
onUnsubscribe?: () => void,
|
|
53
|
-
) => void
|
|
54
|
-
|
|
55
|
-
type GenQueries<TQueries, TStateResult> = (args: {
|
|
56
|
-
rxSQL: ReactiveSQL
|
|
57
|
-
rxGraphQL: ReactiveGraphQL
|
|
58
|
-
rxJS: ReactiveJS
|
|
59
|
-
state$: LiveStoreJSQuery<TStateResult>
|
|
60
|
-
/**
|
|
61
|
-
* Registers a subscription.
|
|
62
|
-
*
|
|
63
|
-
* Passed down for some manual subscribing. Use carefully.
|
|
64
|
-
*/
|
|
65
|
-
subscribe: RegisterSubscription
|
|
66
|
-
isTemporaryQuery: boolean
|
|
67
|
-
}) => TQueries
|
|
68
|
-
|
|
69
|
-
export type UseLiveStoreComponentProps<TQueries, TStateColumns extends ComponentColumns> = {
|
|
70
|
-
stateSchema?: SqliteDsl.TableDefinition<string, TStateColumns>
|
|
71
|
-
queries?: GenQueries<TQueries, SqliteDsl.FromColumns.RowDecoded<TStateColumns>>
|
|
27
|
+
export type UseComponentStateProps<TStateColumns extends ComponentColumns> = {
|
|
28
|
+
schema?: SqliteDsl.TableDefinition<string, TStateColumns>
|
|
72
29
|
reactDeps?: React.DependencyList
|
|
73
30
|
componentKey: ComponentKeyConfig
|
|
74
31
|
}
|
|
@@ -119,13 +76,12 @@ export type GetStateTypeEncoded<TTableDef extends SqliteDsl.TableDefinition<any,
|
|
|
119
76
|
* @param config.componentKey A function that returns a unique key for this component.
|
|
120
77
|
* @param config.reactDeps A list of React-level dependencies that will refresh the queries.
|
|
121
78
|
*/
|
|
122
|
-
export const
|
|
123
|
-
|
|
124
|
-
queries = () => ({}) as TQueries,
|
|
79
|
+
export const useComponentState = <TStateColumns extends ComponentColumns>({
|
|
80
|
+
schema: stateSchema_,
|
|
125
81
|
componentKey: componentKeyConfig,
|
|
126
82
|
reactDeps = [],
|
|
127
|
-
}:
|
|
128
|
-
|
|
83
|
+
}: UseComponentStateProps<TStateColumns>): {
|
|
84
|
+
state$: LiveStoreJSQuery<SqliteDsl.FromColumns.RowDecoded<TStateColumns>>
|
|
129
85
|
state: SqliteDsl.FromColumns.RowDecoded<TStateColumns>
|
|
130
86
|
setState: Setters<SqliteDsl.FromColumns.RowDecoded<TStateColumns>>
|
|
131
87
|
useLiveStoreJsonState: UseLiveStoreJsonState<SqliteDsl.FromColumns.RowDecoded<TStateColumns>>
|
|
@@ -139,20 +95,27 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
|
|
|
139
95
|
[stateSchema_],
|
|
140
96
|
)
|
|
141
97
|
|
|
142
|
-
// performance.mark('useLiveStoreComponent:start')
|
|
143
98
|
const componentKey = useComponentKey(componentKeyConfig, reactDeps)
|
|
144
99
|
const { store } = useStore()
|
|
145
100
|
|
|
146
101
|
const componentKeyLabel = React.useMemo(() => labelForKey(componentKey), [componentKey])
|
|
147
102
|
|
|
103
|
+
const stackInfo = React.useMemo(() => {
|
|
104
|
+
Error.stackTraceLimit = 10
|
|
105
|
+
// eslint-disable-next-line unicorn/error-message
|
|
106
|
+
const stack = new Error().stack!
|
|
107
|
+
Error.stackTraceLimit = originalStackLimit
|
|
108
|
+
return extractStackInfoFromStackTrace(stack)
|
|
109
|
+
}, [])
|
|
110
|
+
|
|
148
111
|
// The following `React.useMemo` and `React.useEffect` calls are used to start and end a span for the lifetime of this component.
|
|
149
112
|
const { span, otelContext } = React.useMemo(() => {
|
|
150
113
|
const existingSpan = spanAlreadyStartedCache.get(componentKeyLabel)
|
|
151
114
|
if (existingSpan !== undefined) return existingSpan
|
|
152
115
|
|
|
153
116
|
const span = store.otel.tracer.startSpan(
|
|
154
|
-
`LiveStore:
|
|
155
|
-
{},
|
|
117
|
+
`LiveStore:useComponentState:${componentKeyLabel}`,
|
|
118
|
+
{ attributes: { stackInfo: JSON.stringify(stackInfo) } },
|
|
156
119
|
store.otel.queriesSpanContext,
|
|
157
120
|
)
|
|
158
121
|
|
|
@@ -161,7 +124,7 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
|
|
|
161
124
|
spanAlreadyStartedCache.set(componentKeyLabel, { span, otelContext })
|
|
162
125
|
|
|
163
126
|
return { span, otelContext }
|
|
164
|
-
}, [componentKeyLabel, store.otel.queriesSpanContext, store.otel.tracer])
|
|
127
|
+
}, [componentKeyLabel, stackInfo, store.otel.queriesSpanContext, store.otel.tracer])
|
|
165
128
|
|
|
166
129
|
React.useEffect(
|
|
167
130
|
() => () => {
|
|
@@ -171,44 +134,6 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
|
|
|
171
134
|
[componentKeyLabel, span],
|
|
172
135
|
)
|
|
173
136
|
|
|
174
|
-
const generateQueries = React.useCallback(
|
|
175
|
-
({
|
|
176
|
-
state$,
|
|
177
|
-
otelContext,
|
|
178
|
-
registerSubscription,
|
|
179
|
-
isTemporaryQuery,
|
|
180
|
-
}: {
|
|
181
|
-
state$: LiveStoreJSQuery<TComponentState>
|
|
182
|
-
otelContext: otel.Context
|
|
183
|
-
registerSubscription: RegisterSubscription
|
|
184
|
-
isTemporaryQuery: boolean
|
|
185
|
-
}) =>
|
|
186
|
-
queries({
|
|
187
|
-
rxSQL: <T>(
|
|
188
|
-
genQuery: string | ((get: GetAtomResult) => string),
|
|
189
|
-
queriedTables: string[],
|
|
190
|
-
bindValues?: Bindable,
|
|
191
|
-
) => store.querySQL<T>(genQuery, { queriedTables, bindValues, otelContext, componentKey }),
|
|
192
|
-
rxGraphQL: <Result extends Record<string, any>, Variables extends Record<string, any>>(
|
|
193
|
-
query: DocumentNode<Result, Variables>,
|
|
194
|
-
genVariableValues: Variables | ((get: GetAtomResult) => Variables),
|
|
195
|
-
label?: string,
|
|
196
|
-
) => store.queryGraphQL(query, genVariableValues, { componentKey, label, otelContext }),
|
|
197
|
-
rxJS: <T>(genQuery: (get: GetAtomResult) => T) => store.queryJS(genQuery, { componentKey, otelContext }),
|
|
198
|
-
state$,
|
|
199
|
-
subscribe: registerSubscription,
|
|
200
|
-
isTemporaryQuery,
|
|
201
|
-
}),
|
|
202
|
-
|
|
203
|
-
// NOTE: we don't include the queries function passed in by the user here;
|
|
204
|
-
// the reason is that we don't want to force them to memoize that function.
|
|
205
|
-
// Instead, we just assume that the function always has the same contents.
|
|
206
|
-
// This makes sense for LiveStore because the component config should be static.
|
|
207
|
-
// TODO: document this and consider whether it's the right API surface.
|
|
208
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
209
|
-
[store, componentKey],
|
|
210
|
-
)
|
|
211
|
-
|
|
212
137
|
const defaultComponentState = React.useMemo(() => {
|
|
213
138
|
const defaultState = (
|
|
214
139
|
stateSchema === undefined ? {} : mapValues(stateSchema.columns, (c) => c.default)
|
|
@@ -225,100 +150,70 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
|
|
|
225
150
|
[stateSchema],
|
|
226
151
|
)
|
|
227
152
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
// TODO find a better solution for this
|
|
252
|
-
if (store.tableRefs[componentTableName] === undefined) {
|
|
253
|
-
const schemaHash = SqliteAst.hash(stateSchema.ast)
|
|
254
|
-
const res = store.inMemoryDB.select<{ schemaHash: number }>(
|
|
255
|
-
sql`SELECT schemaHash FROM ${SCHEMA_META_TABLE} WHERE tableName = '${componentTableName}'`,
|
|
256
|
-
)
|
|
257
|
-
if (res.length === 0 || res[0]!.schemaHash !== schemaHash) {
|
|
258
|
-
migrateTable({ db: store._proxyDb, tableDef: stateSchema.ast, otelContext, schemaHash })
|
|
259
|
-
}
|
|
153
|
+
const state$ = React.useMemo(() => {
|
|
154
|
+
// create state query
|
|
155
|
+
if (stateSchema === undefined) {
|
|
156
|
+
// TODO don't set up a query if there's no state schema (keeps the graph more clean)
|
|
157
|
+
return new LiveStoreJSQuery({
|
|
158
|
+
fn: () => ({}) as TComponentState,
|
|
159
|
+
label: 'empty-component-state',
|
|
160
|
+
// otelContext,
|
|
161
|
+
// otelTracer: store.otel.tracer,
|
|
162
|
+
})
|
|
163
|
+
} else {
|
|
164
|
+
const componentTableName = tableNameForComponentKey(componentKey)
|
|
165
|
+
const whereClause = componentKey._tag === 'singleton' ? '' : `where id = '${componentKey.id}'`
|
|
166
|
+
|
|
167
|
+
// TODO find a better solution for this
|
|
168
|
+
if (store.tableRefs[componentTableName] === undefined) {
|
|
169
|
+
const schemaHash = SqliteAst.hash(stateSchema.ast)
|
|
170
|
+
const res = store.inMemoryDB.select<{ schemaHash: number }>(
|
|
171
|
+
sql`SELECT schemaHash FROM ${SCHEMA_META_TABLE} WHERE tableName = '${componentTableName}'`,
|
|
172
|
+
)
|
|
173
|
+
if (res.length === 0 || res[0]!.schemaHash !== schemaHash) {
|
|
174
|
+
migrateTable({ db: store._proxyDb, tableDef: stateSchema.ast, otelContext, schemaHash })
|
|
175
|
+
}
|
|
260
176
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
state$ = store
|
|
269
|
-
.querySQL(() => sql`select * from ${componentTableName} ${whereClause} limit 1`, {
|
|
270
|
-
queriedTables: [componentTableName],
|
|
271
|
-
componentKey,
|
|
272
|
-
label: `localState:query:${componentKeyLabel}`,
|
|
273
|
-
otelContext,
|
|
274
|
-
})
|
|
275
|
-
// TODO consider to instead of just returning the default value, to write the default component state to the DB
|
|
276
|
-
.pipe<TComponentState>((results) =>
|
|
277
|
-
results.length === 1
|
|
278
|
-
? Schema.parseSync(componentStateEffectSchema)(results[0]!)
|
|
279
|
-
: defaultComponentState,
|
|
280
|
-
)
|
|
281
|
-
}
|
|
282
|
-
const initialComponentState = state$.results$.result
|
|
177
|
+
store.tableRefs[componentTableName] = store.graph.makeRef(null, {
|
|
178
|
+
equal: () => false,
|
|
179
|
+
label: componentTableName,
|
|
180
|
+
meta: { liveStoreRefType: 'table' },
|
|
181
|
+
})
|
|
182
|
+
}
|
|
283
183
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
// TODO improve typing
|
|
297
|
-
) as unknown as QueryResults<TQueries>
|
|
298
|
-
|
|
299
|
-
return { initialComponentState, initialQueryResults }
|
|
300
|
-
} finally {
|
|
301
|
-
span.end()
|
|
302
|
-
}
|
|
303
|
-
})
|
|
304
|
-
})
|
|
184
|
+
return (
|
|
185
|
+
new LiveStoreSQLQuery({
|
|
186
|
+
label: `localState:query:${componentKeyLabel}`,
|
|
187
|
+
genQueryString: () => sql`select * from ${componentTableName} ${whereClause} limit 1`,
|
|
188
|
+
queriedTables: [componentTableName],
|
|
189
|
+
})
|
|
190
|
+
// TODO consider to instead of just returning the default value, to write the default component state to the DB
|
|
191
|
+
.pipe<TComponentState>((results) =>
|
|
192
|
+
results.length === 1 ? Schema.parseSync(componentStateEffectSchema)(results[0]!) : defaultComponentState,
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
}
|
|
305
196
|
}, [
|
|
306
|
-
store,
|
|
307
|
-
otelContext,
|
|
308
|
-
stateSchema,
|
|
309
|
-
generateQueries,
|
|
310
197
|
componentKey,
|
|
311
198
|
componentKeyLabel,
|
|
312
199
|
componentStateEffectSchema,
|
|
313
200
|
defaultComponentState,
|
|
201
|
+
otelContext,
|
|
202
|
+
stateSchema,
|
|
203
|
+
store,
|
|
314
204
|
])
|
|
315
205
|
|
|
206
|
+
// Step 1:
|
|
207
|
+
// Synchronously create state and queries for initial render pass.
|
|
208
|
+
const initialComponentState = React.useMemo(
|
|
209
|
+
() => state$.run(otelContext, { _tag: 'react', api: 'useComponentState', label: state$.label, stackInfo }),
|
|
210
|
+
[otelContext, stackInfo, state$],
|
|
211
|
+
)
|
|
212
|
+
|
|
316
213
|
// Now that we've computed the initial state synchronously,
|
|
317
214
|
// we can set up our useState calls w/ a default value populated...
|
|
318
215
|
const [componentStateRef, setComponentState_] = useStateRefWithReactiveInput<TComponentState>(initialComponentState)
|
|
319
216
|
|
|
320
|
-
const [queryResultsRef, setQueryResults_] = useStateRefWithReactiveInput<QueryResults<TQueries>>(initialQueryResults)
|
|
321
|
-
|
|
322
217
|
const setState = (
|
|
323
218
|
stateSchema === undefined
|
|
324
219
|
? {}
|
|
@@ -359,40 +254,19 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
|
|
|
359
254
|
// time to set up our long-running queries in an effect
|
|
360
255
|
React.useEffect(() => {
|
|
361
256
|
return store.otel.tracer.startActiveSpan(
|
|
362
|
-
'LiveStore:
|
|
257
|
+
'LiveStore:useComponentState:long-running',
|
|
363
258
|
{ attributes: {} },
|
|
364
259
|
otelContext,
|
|
365
260
|
(span) => {
|
|
366
|
-
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
367
261
|
const unsubs: (() => void)[] = []
|
|
368
262
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
// TODO remove this query
|
|
373
|
-
state$ = store.queryJS(() => ({}) as TComponentState, {
|
|
374
|
-
componentKey,
|
|
375
|
-
otelContext,
|
|
376
|
-
label: 'empty-component-state',
|
|
377
|
-
})
|
|
378
|
-
} else {
|
|
379
|
-
const componentTableName = tableNameForComponentKey(componentKey)
|
|
380
|
-
insertRowForComponentInstance({ store, componentKey, stateSchema })
|
|
381
|
-
|
|
382
|
-
const whereClause = componentKey._tag === 'singleton' ? '' : `where id = '${componentKey.id}'`
|
|
383
|
-
state$ = store
|
|
384
|
-
.querySQL<TComponentState>(() => sql`select * from ${componentTableName} ${whereClause} limit 1`, {
|
|
385
|
-
queriedTables: [componentTableName],
|
|
386
|
-
componentKey,
|
|
387
|
-
label: `localState:query:${componentKeyLabel}`,
|
|
388
|
-
otelContext,
|
|
389
|
-
})
|
|
390
|
-
// TODO consider to instead of just returning the default value, to write the default component state to the DB
|
|
391
|
-
.pipe<TComponentState>((results) =>
|
|
392
|
-
results.length === 1 ? Schema.parseSync(componentStateEffectSchema)(results[0]!) : defaultComponentState,
|
|
393
|
-
)
|
|
263
|
+
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
264
|
+
if (stateSchema !== undefined) {
|
|
265
|
+
insertRowForComponentInstance({ store, componentKey, stateSchema, otelContext })
|
|
394
266
|
}
|
|
395
267
|
|
|
268
|
+
state$.activeSubscriptions.add(stackInfo)
|
|
269
|
+
|
|
396
270
|
unsubs.push(
|
|
397
271
|
store.subscribe(
|
|
398
272
|
state$,
|
|
@@ -402,44 +276,11 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
|
|
|
402
276
|
}
|
|
403
277
|
},
|
|
404
278
|
undefined,
|
|
405
|
-
{ label: `
|
|
279
|
+
{ label: `useComponentState:localState:subscribe:${state$.label}`, otelContext },
|
|
406
280
|
),
|
|
281
|
+
() => state$.activeSubscriptions.delete(stackInfo),
|
|
407
282
|
)
|
|
408
283
|
|
|
409
|
-
const registerSubscription: RegisterSubscription = (query$, callback, onUnsubscribe) => {
|
|
410
|
-
unsubs.push(
|
|
411
|
-
store.subscribe(
|
|
412
|
-
query$,
|
|
413
|
-
(results) => {
|
|
414
|
-
callback(results)
|
|
415
|
-
},
|
|
416
|
-
onUnsubscribe,
|
|
417
|
-
{ label: `useLiveStoreComponent:query:manual-subscribe:${query$.label}` },
|
|
418
|
-
),
|
|
419
|
-
)
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const queries = generateQueries({ state$, otelContext, registerSubscription, isTemporaryQuery: false })
|
|
423
|
-
|
|
424
|
-
for (const [key, query] of Object.entries(queries)) {
|
|
425
|
-
// Use the field name given to this query in the useQueries hook as its label
|
|
426
|
-
query.label = key
|
|
427
|
-
|
|
428
|
-
unsubs.push(
|
|
429
|
-
store.subscribe(
|
|
430
|
-
query,
|
|
431
|
-
(results) => {
|
|
432
|
-
const newQueryResults = { ...queryResultsRef.current, [key]: results }
|
|
433
|
-
if (isEqual(newQueryResults, queryResultsRef.current) === false) {
|
|
434
|
-
setQueryResults_(newQueryResults)
|
|
435
|
-
}
|
|
436
|
-
},
|
|
437
|
-
undefined,
|
|
438
|
-
{ label: `useLiveStoreComponent:query:subscribe:${query.label}` },
|
|
439
|
-
),
|
|
440
|
-
)
|
|
441
|
-
}
|
|
442
|
-
|
|
443
284
|
return () => {
|
|
444
285
|
for (const unsub of unsubs) {
|
|
445
286
|
unsub()
|
|
@@ -449,26 +290,19 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
|
|
|
449
290
|
}
|
|
450
291
|
},
|
|
451
292
|
)
|
|
452
|
-
// NOTE excluding `setComponentState_` and `setQueryResults_` from the deps array as it seems to cause an infinite loop
|
|
453
|
-
// This should probably be improved
|
|
454
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
455
293
|
}, [
|
|
456
294
|
store,
|
|
457
|
-
|
|
295
|
+
stackInfo,
|
|
458
296
|
stateSchema,
|
|
459
297
|
defaultComponentState,
|
|
460
|
-
generateQueries,
|
|
461
298
|
otelContext,
|
|
462
299
|
componentStateRef,
|
|
463
|
-
|
|
464
|
-
|
|
300
|
+
state$,
|
|
301
|
+
setComponentState_,
|
|
302
|
+
componentKey,
|
|
465
303
|
])
|
|
466
304
|
|
|
467
|
-
|
|
468
|
-
React.useEffect(() => () => store.unmountComponent(componentKey), [store, componentKey])
|
|
469
|
-
|
|
470
|
-
// performance.mark('useLiveStoreComponent:end')
|
|
471
|
-
// performance.measure(`useLiveStoreComponent:${componentKey.type}`, 'useLiveStoreComponent:start', 'useLiveStoreComponent:end')
|
|
305
|
+
React.useEffect(() => () => state$.destroy(), [state$])
|
|
472
306
|
|
|
473
307
|
const state = componentStateRef.current
|
|
474
308
|
|
|
@@ -498,7 +332,7 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
|
|
|
498
332
|
}
|
|
499
333
|
|
|
500
334
|
return {
|
|
501
|
-
|
|
335
|
+
state$,
|
|
502
336
|
state,
|
|
503
337
|
setState,
|
|
504
338
|
useLiveStoreJsonState,
|
|
@@ -535,10 +369,12 @@ const insertRowForComponentInstance = ({
|
|
|
535
369
|
store,
|
|
536
370
|
componentKey,
|
|
537
371
|
stateSchema,
|
|
372
|
+
otelContext,
|
|
538
373
|
}: {
|
|
539
374
|
store: Store<BaseGraphQLContext>
|
|
540
375
|
componentKey: ComponentKey
|
|
541
376
|
stateSchema: SqliteDsl.TableDefinition<string, SqliteDsl.Columns>
|
|
377
|
+
otelContext: otel.Context
|
|
542
378
|
}) => {
|
|
543
379
|
const columnNames = ['id', ...Object.keys(stateSchema.columns)]
|
|
544
380
|
const columnValues = columnNames.map((name) => `$${name}`).join(', ')
|
|
@@ -555,6 +391,7 @@ const insertRowForComponentInstance = ({
|
|
|
555
391
|
id: componentKey.id,
|
|
556
392
|
},
|
|
557
393
|
[tableName],
|
|
394
|
+
otelContext,
|
|
558
395
|
)
|
|
559
396
|
}
|
|
560
397
|
|
package/src/react/useQuery.ts
CHANGED
|
@@ -1,56 +1,90 @@
|
|
|
1
|
+
import * as otel from '@opentelemetry/api'
|
|
2
|
+
import { isEqual } from 'lodash-es'
|
|
1
3
|
import React from 'react'
|
|
2
4
|
|
|
3
|
-
import {
|
|
4
|
-
import type { QueryDefinition } from '../effect/LiveStore.js'
|
|
5
|
-
import type { LiveStoreQuery, QueryResult, Store } from '../store.js'
|
|
5
|
+
import type { ILiveStoreQuery } from '../reactiveQueries/base-class.js'
|
|
6
6
|
import { useStore } from './LiveStoreContext.js'
|
|
7
|
+
import { extractStackInfoFromStackTrace, originalStackLimit } from './utils/stack-info.js'
|
|
8
|
+
import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInput.js'
|
|
9
|
+
/**
|
|
10
|
+
* This is needed because the `React.useMemo` call below, can sometimes be called multiple times 🤷,
|
|
11
|
+
* so we need to "cache" the fact that we've already started a span for this component.
|
|
12
|
+
* The map entry is being removed again in the `React.useEffect` call below.
|
|
13
|
+
*/
|
|
14
|
+
const spanAlreadyStartedCache = new Map<ILiveStoreQuery<any>, { span: otel.Span; otelContext: otel.Context }>()
|
|
7
15
|
|
|
8
|
-
|
|
9
|
-
const queryCache = new Map<QueryDefinition, LiveStoreQuery>()
|
|
10
|
-
|
|
11
|
-
export const useQuery = <Q extends LiveStoreQuery>(queryDef: (store: Store) => Q): QueryResult<Q> => {
|
|
16
|
+
export const useQuery = <TResult>(query: ILiveStoreQuery<TResult>): TResult => {
|
|
12
17
|
const { store } = useStore()
|
|
13
|
-
const query = React.useMemo(() => {
|
|
14
|
-
if (queryCache.has(queryDef)) return queryCache.get(queryDef) as Q
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
const stackInfo = React.useMemo(() => {
|
|
20
|
+
Error.stackTraceLimit = 10
|
|
21
|
+
// eslint-disable-next-line unicorn/error-message
|
|
22
|
+
const stack = new Error().stack!
|
|
23
|
+
Error.stackTraceLimit = originalStackLimit
|
|
24
|
+
return extractStackInfoFromStackTrace(stack)
|
|
25
|
+
}, [])
|
|
26
|
+
|
|
27
|
+
// The following `React.useMemo` and `React.useEffect` calls are used to start and end a span for the lifetime of this component.
|
|
28
|
+
const { span, otelContext } = React.useMemo(() => {
|
|
29
|
+
const existingSpan = spanAlreadyStartedCache.get(query)
|
|
30
|
+
if (existingSpan !== undefined) return existingSpan
|
|
31
|
+
|
|
32
|
+
const span = store.otel.tracer.startSpan(
|
|
33
|
+
`LiveStore:useQuery:${query.label}`,
|
|
34
|
+
{ attributes: { label: query.label, stackInfo: JSON.stringify(stackInfo) } },
|
|
35
|
+
store.otel.queriesSpanContext,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
39
|
+
|
|
40
|
+
spanAlreadyStartedCache.set(query, { span, otelContext })
|
|
41
|
+
|
|
42
|
+
return { span, otelContext }
|
|
43
|
+
}, [query, stackInfo, store.otel.queriesSpanContext, store.otel.tracer])
|
|
44
|
+
|
|
45
|
+
const initialResult = React.useMemo(
|
|
46
|
+
() =>
|
|
47
|
+
query.run(otelContext, {
|
|
48
|
+
_tag: 'react',
|
|
49
|
+
api: 'useQuery',
|
|
50
|
+
label: query.label,
|
|
51
|
+
stackInfo,
|
|
52
|
+
}),
|
|
53
|
+
[otelContext, query, stackInfo],
|
|
54
|
+
)
|
|
20
55
|
|
|
21
56
|
// We know the query has a result by the time we use it; so we can synchronously populate a default state
|
|
22
|
-
const [
|
|
57
|
+
const [valueRef, setValue] = useStateRefWithReactiveInput<TResult>(initialResult)
|
|
58
|
+
|
|
59
|
+
React.useEffect(
|
|
60
|
+
() => () => {
|
|
61
|
+
spanAlreadyStartedCache.delete(query)
|
|
62
|
+
span.end()
|
|
63
|
+
},
|
|
64
|
+
[query, span],
|
|
65
|
+
)
|
|
23
66
|
|
|
24
67
|
// Subscribe to future updates for this query
|
|
25
68
|
React.useEffect(() => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// this implies that app code must not mutate the results, or else
|
|
36
|
-
// there may be weird reactivity bugs.
|
|
37
|
-
return setValue(v)
|
|
38
|
-
},
|
|
39
|
-
undefined,
|
|
40
|
-
{ label: query.label },
|
|
41
|
-
)
|
|
42
|
-
return () => {
|
|
43
|
-
// // NOTE destroying the whole query will also unsubscribe it
|
|
44
|
-
// query.destroy()
|
|
45
|
-
|
|
46
|
-
// TODO for now we'll still `cancel` manually, but we should remove this once we have some kind of
|
|
47
|
-
// ARC-based system
|
|
48
|
-
cancel()
|
|
49
|
-
span.end()
|
|
69
|
+
query.activeSubscriptions.add(stackInfo)
|
|
70
|
+
const unsub = store.subscribe(
|
|
71
|
+
query,
|
|
72
|
+
(newValue) => {
|
|
73
|
+
// NOTE: we return a reference to the result object within LiveStore;
|
|
74
|
+
// this implies that app code must not mutate the results, or else
|
|
75
|
+
// there may be weird reactivity bugs.
|
|
76
|
+
if (isEqual(newValue, valueRef.current) === false) {
|
|
77
|
+
setValue(newValue)
|
|
50
78
|
}
|
|
51
79
|
},
|
|
80
|
+
undefined,
|
|
81
|
+
{ label: query.label, otelContext },
|
|
52
82
|
)
|
|
53
|
-
|
|
83
|
+
return () => {
|
|
84
|
+
query.activeSubscriptions.delete(stackInfo)
|
|
85
|
+
unsub()
|
|
86
|
+
}
|
|
87
|
+
}, [stackInfo, query, setValue, store, valueRef, otelContext, span])
|
|
54
88
|
|
|
55
|
-
return
|
|
89
|
+
return valueRef.current
|
|
56
90
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import type { ILiveStoreQuery } from '../reactiveQueries/base-class.js'
|
|
4
|
+
import { useQuery } from './useQuery.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates a query, subscribes and destroys it when the component unmounts.
|
|
8
|
+
*
|
|
9
|
+
* Make sure `makeQuery` is a memoized function.
|
|
10
|
+
*/
|
|
11
|
+
export const useTemporaryQuery = <TResult>(makeQuery: () => ILiveStoreQuery<TResult>): TResult => {
|
|
12
|
+
// TODO cache the query outside of the `useMemo` since `useMemo` might be called multiple times
|
|
13
|
+
// also need to update the `useEffect` below https://stackoverflow.com/questions/66446642/react-usememo-memory-clean/77457605#77457605
|
|
14
|
+
const query = React.useMemo(() => makeQuery(), [makeQuery])
|
|
15
|
+
|
|
16
|
+
React.useEffect(() => {
|
|
17
|
+
return () => {
|
|
18
|
+
query.destroy()
|
|
19
|
+
}
|
|
20
|
+
}, [query])
|
|
21
|
+
|
|
22
|
+
return useQuery(query)
|
|
23
|
+
}
|