@livestore/livestore 0.0.16 → 0.0.21
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 +18 -21
- 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 +5 -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__/reactive.test.js +167 -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 +2 -2
- package/dist/inMemoryDatabase.d.ts.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 +3 -3
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +2 -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 +248 -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 +26 -22
- 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 +17 -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/reactive.d.ts +42 -48
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +293 -186
- package/dist/reactive.js.map +1 -1
- package/dist/reactiveQueries/base-class.d.ts +28 -23
- package/dist/reactiveQueries/base-class.d.ts.map +1 -1
- package/dist/reactiveQueries/base-class.js +25 -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 +34 -17
- package/dist/reactiveQueries/graphql.d.ts.map +1 -1
- package/dist/reactiveQueries/graphql.js +91 -10
- package/dist/reactiveQueries/graphql.js.map +1 -1
- package/dist/reactiveQueries/js.d.ts +16 -12
- package/dist/reactiveQueries/js.d.ts.map +1 -1
- package/dist/reactiveQueries/js.js +31 -8
- package/dist/reactiveQueries/js.js.map +1 -1
- package/dist/reactiveQueries/sql.d.ts +22 -18
- package/dist/reactiveQueries/sql.d.ts.map +1 -1
- package/dist/reactiveQueries/sql.js +82 -16
- package/dist/reactiveQueries/sql.js.map +1 -1
- package/dist/store.d.ts +12 -52
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +283 -264
- package/dist/store.js.map +1 -1
- package/package.json +10 -9
- package/src/QueryCache.ts +1 -1
- package/src/__tests__/react/fixture.tsx +12 -7
- 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/extractStackInfoFromStackTrace.test.ts +40 -0
- package/src/__tests__/reactive.test.ts +193 -140
- package/src/__tests__/reactiveQueries/sql.test.ts +372 -0
- package/src/inMemoryDatabase.ts +2 -2
- package/src/index.ts +7 -11
- package/src/react/index.ts +3 -7
- package/src/react/{useLiveStoreComponent.ts → useComponentState.ts} +89 -247
- package/src/react/useQuery.ts +29 -27
- package/src/react/useTemporaryQuery.ts +21 -0
- package/src/react/utils/extractStackInfoFromStackTrace.ts +47 -0
- package/src/reactive.ts +385 -268
- package/src/reactiveQueries/base-class.ts +60 -44
- package/src/reactiveQueries/graph.ts +15 -0
- package/src/reactiveQueries/graphql.ts +145 -29
- package/src/reactiveQueries/js.ts +53 -20
- package/src/reactiveQueries/sql.ts +129 -36
- package/src/store.ts +338 -408
- 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/extractStackInfoFromStackTrace.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,7 +95,6 @@ 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
|
|
|
@@ -151,7 +106,7 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
|
|
|
151
106
|
if (existingSpan !== undefined) return existingSpan
|
|
152
107
|
|
|
153
108
|
const span = store.otel.tracer.startSpan(
|
|
154
|
-
`LiveStore:
|
|
109
|
+
`LiveStore:useComponentState:${componentKeyLabel}`,
|
|
155
110
|
{},
|
|
156
111
|
store.otel.queriesSpanContext,
|
|
157
112
|
)
|
|
@@ -171,44 +126,6 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
|
|
|
171
126
|
[componentKeyLabel, span],
|
|
172
127
|
)
|
|
173
128
|
|
|
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
129
|
const defaultComponentState = React.useMemo(() => {
|
|
213
130
|
const defaultState = (
|
|
214
131
|
stateSchema === undefined ? {} : mapValues(stateSchema.columns, (c) => c.default)
|
|
@@ -225,100 +142,68 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
|
|
|
225
142
|
[stateSchema],
|
|
226
143
|
)
|
|
227
144
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
}
|
|
145
|
+
const state$ = React.useMemo(() => {
|
|
146
|
+
console.log('useComponentState make state$', labelForKey(componentKey))
|
|
147
|
+
// create state query
|
|
148
|
+
if (stateSchema === undefined) {
|
|
149
|
+
// TODO don't set up a query if there's no state schema (keeps the graph more clean)
|
|
150
|
+
return new LiveStoreJSQuery({
|
|
151
|
+
fn: () => ({}) as TComponentState,
|
|
152
|
+
label: 'empty-component-state',
|
|
153
|
+
// otelContext,
|
|
154
|
+
// otelTracer: store.otel.tracer,
|
|
155
|
+
})
|
|
156
|
+
} else {
|
|
157
|
+
const componentTableName = tableNameForComponentKey(componentKey)
|
|
158
|
+
const whereClause = componentKey._tag === 'singleton' ? '' : `where id = '${componentKey.id}'`
|
|
159
|
+
|
|
160
|
+
// TODO find a better solution for this
|
|
161
|
+
if (store.tableRefs[componentTableName] === undefined) {
|
|
162
|
+
const schemaHash = SqliteAst.hash(stateSchema.ast)
|
|
163
|
+
const res = store.inMemoryDB.select<{ schemaHash: number }>(
|
|
164
|
+
sql`SELECT schemaHash FROM ${SCHEMA_META_TABLE} WHERE tableName = '${componentTableName}'`,
|
|
165
|
+
)
|
|
166
|
+
if (res.length === 0 || res[0]!.schemaHash !== schemaHash) {
|
|
167
|
+
migrateTable({ db: store._proxyDb, tableDef: stateSchema.ast, otelContext, schemaHash })
|
|
168
|
+
}
|
|
260
169
|
|
|
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
|
|
170
|
+
store.tableRefs[componentTableName] = store.graph.makeRef(null, {
|
|
171
|
+
equal: () => false,
|
|
172
|
+
label: componentTableName,
|
|
173
|
+
meta: { liveStoreRefType: 'table' },
|
|
174
|
+
})
|
|
175
|
+
}
|
|
283
176
|
|
|
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
|
-
})
|
|
177
|
+
return (
|
|
178
|
+
new LiveStoreSQLQuery({
|
|
179
|
+
label: `localState:query:${componentKeyLabel}`,
|
|
180
|
+
genQueryString: () => sql`select * from ${componentTableName} ${whereClause} limit 1`,
|
|
181
|
+
queriedTables: [componentTableName],
|
|
182
|
+
})
|
|
183
|
+
// TODO consider to instead of just returning the default value, to write the default component state to the DB
|
|
184
|
+
.pipe<TComponentState>((results) =>
|
|
185
|
+
results.length === 1 ? Schema.parseSync(componentStateEffectSchema)(results[0]!) : defaultComponentState,
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
}
|
|
305
189
|
}, [
|
|
306
|
-
store,
|
|
307
|
-
otelContext,
|
|
308
|
-
stateSchema,
|
|
309
|
-
generateQueries,
|
|
310
190
|
componentKey,
|
|
311
191
|
componentKeyLabel,
|
|
312
192
|
componentStateEffectSchema,
|
|
313
193
|
defaultComponentState,
|
|
194
|
+
otelContext,
|
|
195
|
+
stateSchema,
|
|
196
|
+
store,
|
|
314
197
|
])
|
|
315
198
|
|
|
199
|
+
// Step 1:
|
|
200
|
+
// Synchronously create state and queries for initial render pass.
|
|
201
|
+
const initialComponentState = React.useMemo(() => state$.run(otelContext), [otelContext, state$])
|
|
202
|
+
|
|
316
203
|
// Now that we've computed the initial state synchronously,
|
|
317
204
|
// we can set up our useState calls w/ a default value populated...
|
|
318
205
|
const [componentStateRef, setComponentState_] = useStateRefWithReactiveInput<TComponentState>(initialComponentState)
|
|
319
206
|
|
|
320
|
-
const [queryResultsRef, setQueryResults_] = useStateRefWithReactiveInput<QueryResults<TQueries>>(initialQueryResults)
|
|
321
|
-
|
|
322
207
|
const setState = (
|
|
323
208
|
stateSchema === undefined
|
|
324
209
|
? {}
|
|
@@ -355,44 +240,30 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
|
|
|
355
240
|
return store.applyEvent('updateComponentState', { componentKey, columnNames, ...columnValues })
|
|
356
241
|
}
|
|
357
242
|
|
|
243
|
+
const subscriptionInfo = React.useMemo(() => {
|
|
244
|
+
Error.stackTraceLimit = 10
|
|
245
|
+
// eslint-disable-next-line unicorn/error-message
|
|
246
|
+
const stack = new Error().stack!
|
|
247
|
+
Error.stackTraceLimit = originalStackLimit
|
|
248
|
+
return { stack: extractStackInfoFromStackTrace(stack) }
|
|
249
|
+
}, [])
|
|
250
|
+
|
|
358
251
|
// OK, now all the synchronous work is done;
|
|
359
252
|
// time to set up our long-running queries in an effect
|
|
360
253
|
React.useEffect(() => {
|
|
361
254
|
return store.otel.tracer.startActiveSpan(
|
|
362
|
-
'LiveStore:
|
|
255
|
+
'LiveStore:useComponentState:long-running',
|
|
363
256
|
{ attributes: {} },
|
|
364
257
|
otelContext,
|
|
365
258
|
(span) => {
|
|
366
|
-
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
367
259
|
const unsubs: (() => void)[] = []
|
|
368
260
|
|
|
369
|
-
|
|
370
|
-
let state$: LiveStoreJSQuery<TComponentState>
|
|
371
|
-
if (stateSchema === undefined) {
|
|
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)
|
|
261
|
+
if (stateSchema !== undefined) {
|
|
380
262
|
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
|
-
)
|
|
394
263
|
}
|
|
395
264
|
|
|
265
|
+
state$.activeSubscriptions.add(subscriptionInfo)
|
|
266
|
+
|
|
396
267
|
unsubs.push(
|
|
397
268
|
store.subscribe(
|
|
398
269
|
state$,
|
|
@@ -402,44 +273,11 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
|
|
|
402
273
|
}
|
|
403
274
|
},
|
|
404
275
|
undefined,
|
|
405
|
-
{ label: `
|
|
276
|
+
{ label: `useComponentState:localState:subscribe:${state$.label}` },
|
|
406
277
|
),
|
|
278
|
+
() => state$.activeSubscriptions.delete(subscriptionInfo),
|
|
407
279
|
)
|
|
408
280
|
|
|
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
281
|
return () => {
|
|
444
282
|
for (const unsub of unsubs) {
|
|
445
283
|
unsub()
|
|
@@ -451,24 +289,28 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
|
|
|
451
289
|
)
|
|
452
290
|
// NOTE excluding `setComponentState_` and `setQueryResults_` from the deps array as it seems to cause an infinite loop
|
|
453
291
|
// This should probably be improved
|
|
454
|
-
//
|
|
292
|
+
// TODO is this still true?
|
|
293
|
+
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
455
294
|
}, [
|
|
456
295
|
store,
|
|
457
|
-
|
|
296
|
+
subscriptionInfo,
|
|
458
297
|
stateSchema,
|
|
459
298
|
defaultComponentState,
|
|
460
|
-
generateQueries,
|
|
461
299
|
otelContext,
|
|
462
300
|
componentStateRef,
|
|
463
|
-
|
|
464
|
-
|
|
301
|
+
state$,
|
|
302
|
+
setComponentState_,
|
|
303
|
+
componentKey,
|
|
465
304
|
])
|
|
466
305
|
|
|
467
306
|
// Very important: remove any queries / other resources associated w/ this component
|
|
468
|
-
React.useEffect(
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
307
|
+
React.useEffect(
|
|
308
|
+
() => () => {
|
|
309
|
+
console.log('useComponentState destroy', labelForKey(componentKey))
|
|
310
|
+
return state$.destroy()
|
|
311
|
+
},
|
|
312
|
+
[state$],
|
|
313
|
+
)
|
|
472
314
|
|
|
473
315
|
const state = componentStateRef.current
|
|
474
316
|
|
|
@@ -498,7 +340,7 @@ export const useLiveStoreComponent = <TStateColumns extends ComponentColumns, TQ
|
|
|
498
340
|
}
|
|
499
341
|
|
|
500
342
|
return {
|
|
501
|
-
|
|
343
|
+
state$,
|
|
502
344
|
state,
|
|
503
345
|
setState,
|
|
504
346
|
useLiveStoreJsonState,
|
package/src/react/useQuery.ts
CHANGED
|
@@ -1,56 +1,58 @@
|
|
|
1
|
+
import { isEqual } from 'lodash-es'
|
|
1
2
|
import React from 'react'
|
|
2
3
|
|
|
3
|
-
import {
|
|
4
|
-
import type { QueryDefinition } from '../effect/LiveStore.js'
|
|
5
|
-
import type { LiveStoreQuery, QueryResult, Store } from '../store.js'
|
|
4
|
+
import type { ILiveStoreQuery } from '../reactiveQueries/base-class.js'
|
|
6
5
|
import { useStore } from './LiveStoreContext.js'
|
|
6
|
+
import { extractStackInfoFromStackTrace, originalStackLimit } from './utils/extractStackInfoFromStackTrace.js'
|
|
7
|
+
import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInput.js'
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
const queryCache = new Map<QueryDefinition, LiveStoreQuery>()
|
|
10
|
-
|
|
11
|
-
export const useQuery = <Q extends LiveStoreQuery>(queryDef: (store: Store) => Q): QueryResult<Q> => {
|
|
9
|
+
export const useQuery = <TResult>(query: ILiveStoreQuery<TResult>): TResult => {
|
|
12
10
|
const { store } = useStore()
|
|
13
|
-
const query = React.useMemo(() => {
|
|
14
|
-
if (queryCache.has(queryDef)) return queryCache.get(queryDef) as Q
|
|
15
11
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return query
|
|
19
|
-
}, [store, queryDef])
|
|
12
|
+
// TODO proper otel context
|
|
13
|
+
const initialResult = React.useMemo(() => query.run(), [query])
|
|
20
14
|
|
|
21
15
|
// We know the query has a result by the time we use it; so we can synchronously populate a default state
|
|
22
|
-
const [
|
|
16
|
+
const [valueRef, setValue] = useStateRefWithReactiveInput<TResult>(initialResult)
|
|
17
|
+
|
|
18
|
+
const subscriptionInfo = React.useMemo(() => {
|
|
19
|
+
Error.stackTraceLimit = 10
|
|
20
|
+
// eslint-disable-next-line unicorn/error-message
|
|
21
|
+
const stack = new Error().stack!
|
|
22
|
+
Error.stackTraceLimit = originalStackLimit
|
|
23
|
+
return { stack: extractStackInfoFromStackTrace(stack) }
|
|
24
|
+
}, [])
|
|
23
25
|
|
|
24
26
|
// Subscribe to future updates for this query
|
|
25
27
|
React.useEffect(() => {
|
|
26
28
|
return store.otel.tracer.startActiveSpan(
|
|
27
|
-
`LiveStore:useQuery:${
|
|
29
|
+
`LiveStore:useQuery:${query.label}`,
|
|
30
|
+
// `LiveStore:useQuery:${labelForKey(query.componentKey)}:${query.label}`,
|
|
28
31
|
{ attributes: { label: query.label } },
|
|
29
|
-
|
|
32
|
+
store.otel.queriesSpanContext,
|
|
30
33
|
(span) => {
|
|
31
|
-
|
|
34
|
+
query.activeSubscriptions.add(subscriptionInfo)
|
|
35
|
+
const unsub = store.subscribe(
|
|
32
36
|
query,
|
|
33
|
-
(
|
|
37
|
+
(newValue) => {
|
|
34
38
|
// NOTE: we return a reference to the result object within LiveStore;
|
|
35
39
|
// this implies that app code must not mutate the results, or else
|
|
36
40
|
// there may be weird reactivity bugs.
|
|
37
|
-
|
|
41
|
+
if (isEqual(newValue, valueRef.current) === false) {
|
|
42
|
+
setValue(newValue)
|
|
43
|
+
}
|
|
38
44
|
},
|
|
39
45
|
undefined,
|
|
40
46
|
{ label: query.label },
|
|
41
47
|
)
|
|
42
48
|
return () => {
|
|
43
|
-
|
|
44
|
-
|
|
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
|
+
query.activeSubscriptions.delete(subscriptionInfo)
|
|
50
|
+
unsub()
|
|
49
51
|
span.end()
|
|
50
52
|
}
|
|
51
53
|
},
|
|
52
54
|
)
|
|
53
|
-
}, [query, store])
|
|
55
|
+
}, [subscriptionInfo, query, setValue, store, valueRef])
|
|
54
56
|
|
|
55
|
-
return
|
|
57
|
+
return valueRef.current
|
|
56
58
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
const query = React.useMemo(() => makeQuery(), [makeQuery])
|
|
13
|
+
|
|
14
|
+
React.useEffect(() => {
|
|
15
|
+
return () => {
|
|
16
|
+
query.destroy()
|
|
17
|
+
}
|
|
18
|
+
}, [query])
|
|
19
|
+
|
|
20
|
+
return useQuery(query)
|
|
21
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export const originalStackLimit = Error.stackTraceLimit
|
|
2
|
+
|
|
3
|
+
export type StackInfo = {
|
|
4
|
+
name: string
|
|
5
|
+
filePath: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
Example stack trace:
|
|
10
|
+
|
|
11
|
+
Error
|
|
12
|
+
at https://localhost:8081/@fs/Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/livestore/dist/react/useQuery.js?t=1699550216884:18:23
|
|
13
|
+
at mountMemo (https://localhost:8081/node_modules/.vite-web/deps/chunk-M23HUTQV.js?v=3eb66ed6:12817:27)
|
|
14
|
+
at Object.useMemo (https://localhost:8081/node_modules/.vite-web/deps/chunk-M23HUTQV.js?v=3eb66ed6:13141:24)
|
|
15
|
+
at Object.useMemo (https://localhost:8081/node_modules/.vite-web/deps/chunk-4WADDZ2G.js?v=3eb66ed6:1094:29)
|
|
16
|
+
at useQuery (https://localhost:8081/@fs/Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/livestore/dist/react/useQuery.js?t=1699550216884:13:33)
|
|
17
|
+
at useAppState (https://localhost:8081/src/db/AppState.ts?t=1699550216884:17:34)
|
|
18
|
+
at useRoute (https://localhost:8081/src/db/AppState.ts?t=1699550216884:74:22)
|
|
19
|
+
at RouteLink (https://localhost:8081/src/components/Link.tsx?t=1699550216884:36:7)
|
|
20
|
+
at renderWithHooks (https://localhost:8081/node_modules/.vite-web/deps/chunk-M23HUTQV.js?v=3eb66ed6:12171:26)
|
|
21
|
+
at mountIndeterminateComponent (https://localhost:8081/node_modules/.vite-web/deps/chunk-M23HUTQV.js?v=3eb66ed6:14921:21)
|
|
22
|
+
|
|
23
|
+
Approach:
|
|
24
|
+
- Start filtering at `at useQuery` (including)
|
|
25
|
+
- Stop filtering at `at renderWithHooks` (excluding)
|
|
26
|
+
*/
|
|
27
|
+
export const extractStackInfoFromStackTrace = (stackTrace: string): StackInfo[] => {
|
|
28
|
+
const namePattern = /at (\S+) \((.+)\)/g
|
|
29
|
+
let match: RegExpExecArray | null
|
|
30
|
+
const stackInfoArr: StackInfo[] = []
|
|
31
|
+
let hasReachedStart = false
|
|
32
|
+
|
|
33
|
+
while ((match = namePattern.exec(stackTrace)) !== null) {
|
|
34
|
+
const [, name, filePath] = match as any as [string, string, string]
|
|
35
|
+
if (name.startsWith('use')) {
|
|
36
|
+
hasReachedStart = true
|
|
37
|
+
|
|
38
|
+
stackInfoArr.unshift({ name, filePath })
|
|
39
|
+
} else if (hasReachedStart) {
|
|
40
|
+
// We've reached the end of the `use*` functions, so we're adding the component name and stop
|
|
41
|
+
stackInfoArr.unshift({ name, filePath })
|
|
42
|
+
break
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return stackInfoArr
|
|
47
|
+
}
|