@livestore/livestore 0.0.46-dev.4 → 0.0.47-dev.0
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 +10 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/react/fixture.d.ts +28 -12
- package/dist/__tests__/react/fixture.d.ts.map +1 -1
- package/dist/__tests__/react/fixture.js +27 -3
- package/dist/__tests__/react/fixture.js.map +1 -1
- package/dist/__tests__/react/utils/otel.d.ts +10 -0
- package/dist/__tests__/react/utils/otel.d.ts.map +1 -0
- package/dist/__tests__/react/utils/otel.js +42 -0
- package/dist/__tests__/react/utils/otel.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/react/LiveStoreProvider.js +39 -6
- package/dist/react/LiveStoreProvider.js.map +1 -1
- package/dist/react/LiveStoreProvider.test.d.ts +2 -0
- package/dist/react/LiveStoreProvider.test.d.ts.map +1 -0
- package/dist/react/LiveStoreProvider.test.js +40 -0
- package/dist/react/LiveStoreProvider.test.js.map +1 -0
- package/dist/react/components/LiveList.d.ts +21 -0
- package/dist/react/components/LiveList.d.ts.map +1 -0
- package/dist/react/components/LiveList.js +31 -0
- package/dist/react/components/LiveList.js.map +1 -0
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react/useAtom.d.ts +1 -1
- package/dist/react/useAtom.d.ts.map +1 -1
- package/dist/react/useAtom.js +6 -1
- package/dist/react/useAtom.js.map +1 -1
- package/dist/react/useQuery.d.ts +4 -1
- package/dist/react/useQuery.d.ts.map +1 -1
- package/dist/react/useQuery.js +24 -19
- package/dist/react/useQuery.js.map +1 -1
- package/dist/react/useQuery.test.js +11 -11
- package/dist/react/useQuery.test.js.map +1 -1
- package/dist/react/useRow.d.ts.map +1 -1
- package/dist/react/useRow.js +14 -69
- package/dist/react/useRow.js.map +1 -1
- package/dist/react/useRow.test.js +440 -28
- package/dist/react/useRow.test.js.map +1 -1
- package/dist/react/useTemporaryQuery.d.ts +15 -3
- package/dist/react/useTemporaryQuery.d.ts.map +1 -1
- package/dist/react/useTemporaryQuery.js +60 -27
- package/dist/react/useTemporaryQuery.js.map +1 -1
- package/dist/react/useTemporaryQuery.test.js +10 -9
- package/dist/react/useTemporaryQuery.test.js.map +1 -1
- package/dist/reactive.d.ts +23 -5
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +44 -11
- package/dist/reactive.js.map +1 -1
- package/dist/reactive.test.js +1 -1
- package/dist/reactive.test.js.map +1 -1
- package/dist/reactiveQueries/base-class.d.ts +1 -1
- package/dist/reactiveQueries/base-class.d.ts.map +1 -1
- package/dist/reactiveQueries/base-class.js.map +1 -1
- package/dist/reactiveQueries/graphql.d.ts +2 -2
- package/dist/reactiveQueries/graphql.d.ts.map +1 -1
- package/dist/reactiveQueries/graphql.js +21 -11
- package/dist/reactiveQueries/graphql.js.map +1 -1
- package/dist/reactiveQueries/sql.d.ts +1 -1
- package/dist/reactiveQueries/sql.d.ts.map +1 -1
- package/dist/reactiveQueries/sql.js +15 -11
- package/dist/reactiveQueries/sql.js.map +1 -1
- package/dist/reactiveQueries/sql.test.js +1 -40
- package/dist/reactiveQueries/sql.test.js.map +1 -1
- package/dist/row-query.d.ts.map +1 -1
- package/dist/row-query.js +3 -1
- package/dist/row-query.js.map +1 -1
- package/dist/store.d.ts +7 -5
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +50 -38
- package/dist/store.js.map +1 -1
- package/package.json +11 -13
- package/src/__tests__/react/fixture.tsx +35 -2
- package/src/__tests__/react/utils/otel.ts +61 -0
- package/src/index.ts +12 -1
- package/src/react/LiveStoreProvider.test.tsx +63 -0
- package/src/react/LiveStoreProvider.tsx +42 -7
- package/src/react/components/LiveList.tsx +84 -0
- package/src/react/index.ts +1 -1
- package/src/react/useAtom.ts +6 -2
- package/src/react/useQuery.test.tsx +11 -11
- package/src/react/useQuery.ts +29 -22
- package/src/react/useRow.test.tsx +502 -30
- package/src/react/useRow.ts +19 -107
- package/src/react/useTemporaryQuery.test.tsx +17 -16
- package/src/react/useTemporaryQuery.ts +96 -28
- package/src/reactive.test.ts +1 -1
- package/src/reactive.ts +76 -15
- package/src/reactiveQueries/base-class.ts +2 -1
- package/src/reactiveQueries/graphql.ts +26 -16
- package/src/reactiveQueries/sql.test.ts +1 -54
- package/src/reactiveQueries/sql.ts +20 -14
- package/src/row-query.ts +3 -1
- package/src/store.ts +71 -49
- package/tsconfig.json +0 -1
- package/dist/react/components/DiffableList.d.ts +0 -20
- package/dist/react/components/DiffableList.d.ts.map +0 -1
- package/dist/react/components/DiffableList.js +0 -113
- package/dist/react/components/DiffableList.js.map +0 -1
- package/dist/react/utils/useCleanup.d.ts +0 -7
- package/dist/react/utils/useCleanup.d.ts.map +0 -1
- package/dist/react/utils/useCleanup.js +0 -19
- package/dist/react/utils/useCleanup.js.map +0 -1
- package/src/react/components/DiffableList.tsx +0 -192
- package/src/react/utils/useCleanup.ts +0 -25
package/src/react/useRow.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { DbSchema } from '@livestore/common/schema'
|
|
2
|
-
import * as otel from '@opentelemetry/api'
|
|
3
2
|
import type { SqliteDsl } from 'effect-db-schema'
|
|
4
3
|
import { mapValues } from 'lodash-es'
|
|
5
4
|
import React from 'react'
|
|
@@ -11,7 +10,7 @@ import type { RowResult } from '../row-query.js'
|
|
|
11
10
|
import { rowQuery } from '../row-query.js'
|
|
12
11
|
import { useStore } from './LiveStoreContext.js'
|
|
13
12
|
import { useQueryRef } from './useQuery.js'
|
|
14
|
-
import {
|
|
13
|
+
import { useMakeTemporaryQuery } from './useTemporaryQuery.js'
|
|
15
14
|
|
|
16
15
|
export type UseRowResult<TTableDef extends DbSchema.TableDef> = [
|
|
17
16
|
row: RowResult<TTableDef>,
|
|
@@ -73,52 +72,24 @@ export const useRow: {
|
|
|
73
72
|
|
|
74
73
|
const { store } = useStore()
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const { query$, otelContext } =
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
97
|
-
|
|
98
|
-
const query$ = DbSchema.tableIsSingleton(table)
|
|
99
|
-
? (rowQuery(table, { otelContext, dbGraph }) as LiveQuery<RowResult<TTableDef>, QueryInfo>)
|
|
100
|
-
: (rowQuery(table as TTableDef & { options: { isSingleton: false } }, id!, {
|
|
101
|
-
otelContext,
|
|
102
|
-
defaultValues: defaultValues!,
|
|
103
|
-
dbGraph,
|
|
104
|
-
}) as any as LiveQuery<RowResult<TTableDef>, QueryInfo>)
|
|
105
|
-
|
|
106
|
-
rcCache.set(table, id ?? 'singleton', query$, reactId, otelContext, span)
|
|
107
|
-
|
|
108
|
-
return { query$, otelContext }
|
|
109
|
-
}, [table, id, reactId, store, defaultValues, dbGraph])
|
|
110
|
-
|
|
111
|
-
useCleanup(
|
|
112
|
-
React.useCallback(() => {
|
|
113
|
-
const cachedItem = rcCache.get(table, id ?? 'singleton')!
|
|
114
|
-
|
|
115
|
-
cachedItem.reactIds.delete(reactId)
|
|
116
|
-
if (cachedItem.reactIds.size === 0) {
|
|
117
|
-
rcCache.delete(cachedItem.query$)
|
|
118
|
-
cachedItem.query$.destroy()
|
|
119
|
-
cachedItem.span.end()
|
|
120
|
-
}
|
|
121
|
-
}, [table, id, reactId]),
|
|
75
|
+
// console.debug('useRow', table.sqliteDef.name, id)
|
|
76
|
+
|
|
77
|
+
const { query$, otelContext } = useMakeTemporaryQuery(
|
|
78
|
+
(otelContext) =>
|
|
79
|
+
DbSchema.tableIsSingleton(table)
|
|
80
|
+
? (rowQuery(table, { otelContext, dbGraph }) as LiveQuery<RowResult<TTableDef>, QueryInfo>)
|
|
81
|
+
: (rowQuery(table as TTableDef & { options: { isSingleton: false } }, id!, {
|
|
82
|
+
otelContext,
|
|
83
|
+
defaultValues: defaultValues!,
|
|
84
|
+
dbGraph,
|
|
85
|
+
}) as any as LiveQuery<RowResult<TTableDef>, QueryInfo>),
|
|
86
|
+
[id!, table.sqliteDef.name],
|
|
87
|
+
{
|
|
88
|
+
otel: {
|
|
89
|
+
spanName: `LiveStore:useRow:${table.sqliteDef.name}${id === undefined ? '' : `:${id}`}`,
|
|
90
|
+
attributes: { id },
|
|
91
|
+
},
|
|
92
|
+
},
|
|
122
93
|
)
|
|
123
94
|
|
|
124
95
|
const query$Ref = useQueryRef(query$, otelContext) as React.MutableRefObject<RowResult<TTableDef>>
|
|
@@ -179,62 +150,3 @@ export type StateSetters<TTableDef extends DbSchema.TableDef> = TTableDef['isSin
|
|
|
179
150
|
} & {
|
|
180
151
|
setMany: Dispatch<SetStateAction<Partial<RowResult<TTableDef>>>>
|
|
181
152
|
}
|
|
182
|
-
|
|
183
|
-
/** Reference counted cache for `query$` and otel context */
|
|
184
|
-
class RCCache {
|
|
185
|
-
private readonly cache = new Map<
|
|
186
|
-
DbSchema.TableDef,
|
|
187
|
-
Map<
|
|
188
|
-
string,
|
|
189
|
-
{
|
|
190
|
-
reactIds: Set<string>
|
|
191
|
-
span: otel.Span
|
|
192
|
-
otelContext: otel.Context
|
|
193
|
-
query$: LiveQuery<any, any>
|
|
194
|
-
}
|
|
195
|
-
>
|
|
196
|
-
>()
|
|
197
|
-
private reverseCache = new Map<LiveQuery<any, any>, [DbSchema.TableDef, string]>()
|
|
198
|
-
|
|
199
|
-
get = (table: DbSchema.TableDef, id: string) => {
|
|
200
|
-
const queries = this.cache.get(table)
|
|
201
|
-
if (queries === undefined) return undefined
|
|
202
|
-
return queries.get(id)
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
set = (
|
|
206
|
-
table: DbSchema.TableDef,
|
|
207
|
-
id: string,
|
|
208
|
-
query$: LiveQuery<any, any>,
|
|
209
|
-
reactId: string,
|
|
210
|
-
otelContext: otel.Context,
|
|
211
|
-
span: otel.Span,
|
|
212
|
-
) => {
|
|
213
|
-
let queries = this.cache.get(table)
|
|
214
|
-
if (queries === undefined) {
|
|
215
|
-
queries = new Map()
|
|
216
|
-
this.cache.set(table, queries)
|
|
217
|
-
}
|
|
218
|
-
queries.set(id, { query$, otelContext, span, reactIds: new Set([reactId]) })
|
|
219
|
-
this.reverseCache.set(query$, [table, id])
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
delete = (query$: LiveQuery<any, any>) => {
|
|
223
|
-
const item = this.reverseCache.get(query$)
|
|
224
|
-
if (item === undefined) return
|
|
225
|
-
|
|
226
|
-
const [table, id] = item
|
|
227
|
-
const queries = this.cache.get(table)
|
|
228
|
-
if (queries === undefined) return
|
|
229
|
-
|
|
230
|
-
queries.delete(id)
|
|
231
|
-
|
|
232
|
-
if (queries.size === 0) {
|
|
233
|
-
this.cache.delete(table)
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
this.reverseCache.delete(query$)
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const rcCache = new RCCache()
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { renderHook } from '@testing-library/react'
|
|
2
|
-
import React from 'react'
|
|
3
2
|
import { describe, expect, it } from 'vitest'
|
|
4
3
|
|
|
5
4
|
import { makeTodoMvc, parseTodos } from '../__tests__/react/fixture.js'
|
|
@@ -9,43 +8,45 @@ import * as LiveStoreReact from './index.js'
|
|
|
9
8
|
|
|
10
9
|
describe('useTemporaryQuery', () => {
|
|
11
10
|
it('simple', async () => {
|
|
12
|
-
|
|
11
|
+
const { wrapper, store, cud, makeRenderCount } = await makeTodoMvc()
|
|
13
12
|
|
|
14
|
-
const
|
|
13
|
+
const renderCount = makeRenderCount()
|
|
15
14
|
|
|
16
15
|
store.mutate(
|
|
17
16
|
cud.todos.insert({ id: 't1', text: 'buy milk', completed: false }),
|
|
18
17
|
cud.todos.insert({ id: 't2', text: 'buy bread', completed: false }),
|
|
19
18
|
)
|
|
20
19
|
|
|
21
|
-
const queryMap = new Map<string, LiveStore.LiveQuery<any
|
|
20
|
+
const queryMap = new Map<string, LiveStore.LiveQuery<any>>()
|
|
22
21
|
|
|
23
|
-
const { rerender, result } = renderHook(
|
|
22
|
+
const { rerender, result, unmount } = renderHook(
|
|
24
23
|
(id: string) => {
|
|
25
|
-
renderCount
|
|
26
|
-
|
|
27
|
-
return LiveStoreReact.useTemporaryQuery(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}, [id]),
|
|
33
|
-
)
|
|
24
|
+
renderCount.inc()
|
|
25
|
+
|
|
26
|
+
return LiveStoreReact.useTemporaryQuery(() => {
|
|
27
|
+
const query$ = querySQL(`select * from todos where id = '${id}'`, { map: parseTodos })
|
|
28
|
+
queryMap.set(id, query$)
|
|
29
|
+
return query$
|
|
30
|
+
}, id)
|
|
34
31
|
},
|
|
35
32
|
{ wrapper, initialProps: 't1' },
|
|
36
33
|
)
|
|
37
34
|
|
|
38
35
|
expect(result.current.length).toBe(1)
|
|
39
36
|
expect(result.current[0]!.text).toBe('buy milk')
|
|
40
|
-
expect(renderCount).toBe(1)
|
|
37
|
+
expect(renderCount.val).toBe(1)
|
|
41
38
|
expect(queryMap.get('t1')!.runs).toBe(1)
|
|
42
39
|
|
|
43
40
|
rerender('t2')
|
|
44
41
|
|
|
45
42
|
expect(result.current.length).toBe(1)
|
|
46
43
|
expect(result.current[0]!.text).toBe('buy bread')
|
|
47
|
-
expect(renderCount).toBe(2)
|
|
44
|
+
expect(renderCount.val).toBe(2)
|
|
48
45
|
expect(queryMap.get('t1')!.runs).toBe(1)
|
|
49
46
|
expect(queryMap.get('t2')!.runs).toBe(1)
|
|
47
|
+
|
|
48
|
+
unmount()
|
|
49
|
+
|
|
50
|
+
expect(queryMap.get('t2')!.runs).toBe(1)
|
|
50
51
|
})
|
|
51
52
|
})
|
|
@@ -1,53 +1,121 @@
|
|
|
1
|
+
import * as otel from '@opentelemetry/api'
|
|
1
2
|
import React from 'react'
|
|
2
3
|
|
|
4
|
+
import type { QueryInfo } from '../query-info.js'
|
|
3
5
|
import type { LiveQuery } from '../reactiveQueries/base-class.js'
|
|
6
|
+
import { useStore } from './LiveStoreContext.js'
|
|
4
7
|
import { useQueryRef } from './useQuery.js'
|
|
5
|
-
import { useCleanup } from './utils/useCleanup.js'
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
9
|
+
// NOTE Given `useMemo` will be called multiple times (e.g. when using React Strict mode or Fast Refresh),
|
|
10
|
+
// we are using this cache to avoid starting multiple queries/spans for the same component.
|
|
11
|
+
// This is somewhat against some recommended React best practices, but it should be fine in our case below.
|
|
12
|
+
// Please definitely open an issue if you see or run into any problems with this approach!
|
|
13
|
+
const cache = new Map<
|
|
14
|
+
string,
|
|
15
|
+
{
|
|
16
|
+
rc: number
|
|
17
|
+
query$: LiveQuery<any, any>
|
|
18
|
+
span: otel.Span
|
|
19
|
+
otelContext: otel.Context
|
|
20
|
+
}
|
|
21
|
+
>()
|
|
22
|
+
|
|
23
|
+
export type DepKey = string | number | ReadonlyArray<string | number>
|
|
12
24
|
|
|
13
25
|
/**
|
|
14
26
|
* Creates a query, subscribes and destroys it when the component unmounts.
|
|
15
27
|
*
|
|
16
|
-
*
|
|
28
|
+
* The `key` is used to determine whether the a new query should be created or if the existing one should be reused.
|
|
17
29
|
*/
|
|
18
|
-
export const useTemporaryQuery = <TResult>(makeQuery: () => LiveQuery<TResult
|
|
19
|
-
useTemporaryQueryRef(makeQuery).current
|
|
30
|
+
export const useTemporaryQuery = <TResult>(makeQuery: () => LiveQuery<TResult>, key: DepKey): TResult =>
|
|
31
|
+
useTemporaryQueryRef(makeQuery, key).current
|
|
32
|
+
|
|
33
|
+
export const useTemporaryQueryRef = <TResult>(
|
|
34
|
+
makeQuery: () => LiveQuery<TResult>,
|
|
35
|
+
key: DepKey,
|
|
36
|
+
): React.MutableRefObject<TResult> => {
|
|
37
|
+
const { query$ } = useMakeTemporaryQuery(makeQuery, key)
|
|
38
|
+
|
|
39
|
+
return useQueryRef(query$)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const useMakeTemporaryQuery = <TResult, TQueryInfo extends QueryInfo>(
|
|
43
|
+
makeQuery: (otelContext: otel.Context) => LiveQuery<TResult, TQueryInfo>,
|
|
44
|
+
key: DepKey,
|
|
45
|
+
options?: {
|
|
46
|
+
otel?: {
|
|
47
|
+
spanName?: string
|
|
48
|
+
attributes?: otel.Attributes
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
): { query$: LiveQuery<TResult, TQueryInfo>; otelContext: otel.Context } => {
|
|
52
|
+
const { store } = useStore()
|
|
53
|
+
const fullKey = React.useMemo(
|
|
54
|
+
// NOTE We're using the `makeQuery` function body string to make sure the key is unique across the app
|
|
55
|
+
// TODO we should figure out whether this could cause some problems and/or if there's a better way to do this
|
|
56
|
+
() => (Array.isArray(key) ? key.join('-') : key) + '-' + store.graph.id + '-' + makeQuery.toString(),
|
|
57
|
+
[key, makeQuery, store.graph.id],
|
|
58
|
+
)
|
|
59
|
+
const fullKeyRef = React.useRef<string>()
|
|
60
|
+
|
|
61
|
+
const { query$, otelContext } = React.useMemo(() => {
|
|
62
|
+
if (fullKeyRef.current !== undefined && fullKeyRef.current !== fullKey) {
|
|
63
|
+
// console.debug('fullKey changed, destroying previous', fullKeyRef.current.split('-')[0]!, fullKey.split('-')[0]!)
|
|
20
64
|
|
|
21
|
-
|
|
22
|
-
|
|
65
|
+
const cachedItem = cache.get(fullKeyRef.current)
|
|
66
|
+
if (cachedItem !== undefined) {
|
|
67
|
+
cachedItem.rc--
|
|
23
68
|
|
|
24
|
-
|
|
25
|
-
|
|
69
|
+
if (cachedItem.rc === 0) {
|
|
70
|
+
cachedItem.query$.destroy()
|
|
71
|
+
cachedItem.span.end()
|
|
72
|
+
cache.delete(fullKeyRef.current)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const cachedItem = cache.get(fullKey)
|
|
26
78
|
if (cachedItem !== undefined) {
|
|
27
|
-
cachedItem.
|
|
79
|
+
cachedItem.rc++
|
|
28
80
|
|
|
29
|
-
return cachedItem
|
|
81
|
+
return cachedItem
|
|
30
82
|
}
|
|
31
83
|
|
|
32
|
-
const
|
|
84
|
+
const spanName = options?.otel?.spanName ?? `LiveStore:useTemporaryQuery:${key}`
|
|
85
|
+
|
|
86
|
+
const span = store.otel.tracer.startSpan(
|
|
87
|
+
spanName,
|
|
88
|
+
{ attributes: options?.otel?.attributes },
|
|
89
|
+
store.otel.queriesSpanContext,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
33
93
|
|
|
34
|
-
|
|
94
|
+
const query$ = makeQuery(otelContext)
|
|
35
95
|
|
|
36
|
-
|
|
37
|
-
}, [reactId, makeQuery])
|
|
96
|
+
cache.set(fullKey, { rc: 1, query$, span, otelContext })
|
|
38
97
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
98
|
+
return { query$, otelContext }
|
|
99
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
100
|
+
}, [fullKey])
|
|
42
101
|
|
|
43
|
-
|
|
102
|
+
fullKeyRef.current = fullKey
|
|
44
103
|
|
|
45
|
-
|
|
104
|
+
React.useEffect(() => {
|
|
105
|
+
return () => {
|
|
106
|
+
const cachedItem = cache.get(fullKey)
|
|
107
|
+
// NOTE in case the fullKey changed then the query was already destroyed in the useMemo above
|
|
108
|
+
if (cachedItem === undefined) return
|
|
109
|
+
|
|
110
|
+
cachedItem.rc--
|
|
111
|
+
|
|
112
|
+
if (cachedItem.rc === 0) {
|
|
46
113
|
cachedItem.query$.destroy()
|
|
47
|
-
|
|
114
|
+
cachedItem.span.end()
|
|
115
|
+
cache.delete(fullKey)
|
|
48
116
|
}
|
|
49
|
-
}
|
|
50
|
-
)
|
|
117
|
+
}
|
|
118
|
+
}, [fullKey])
|
|
51
119
|
|
|
52
|
-
return
|
|
120
|
+
return { query$, otelContext }
|
|
53
121
|
}
|
package/src/reactive.test.ts
CHANGED
|
@@ -245,7 +245,7 @@ describe('a trivial graph', () => {
|
|
|
245
245
|
expect(e.isDirty).toBe(true)
|
|
246
246
|
|
|
247
247
|
expect(() => c.computeResult()).toThrowErrorMatchingInlineSnapshot(
|
|
248
|
-
`[Error: This should never happen LiveStore Error: Attempted to compute destroyed
|
|
248
|
+
`[Error: This should never happen LiveStore Error: Attempted to compute destroyed ref (node-58): b]`,
|
|
249
249
|
)
|
|
250
250
|
})
|
|
251
251
|
})
|
package/src/reactive.ts
CHANGED
|
@@ -88,6 +88,12 @@ export type Node<T, TContext, TDebugRefreshReason extends DebugRefreshReason> =
|
|
|
88
88
|
| Atom<T, TContext, TDebugRefreshReason>
|
|
89
89
|
| Effect
|
|
90
90
|
|
|
91
|
+
export const isThunk = <T, TContext, TDebugRefreshReason extends DebugRefreshReason>(
|
|
92
|
+
obj: unknown,
|
|
93
|
+
): obj is Thunk<T, TContext, TDebugRefreshReason> => {
|
|
94
|
+
return typeof obj === 'object' && obj !== null && '_tag' in obj && (obj as any)._tag === 'thunk'
|
|
95
|
+
}
|
|
96
|
+
|
|
91
97
|
export type DebugThunkInfo<T extends string = string> = {
|
|
92
98
|
_tag: T
|
|
93
99
|
durationMs: number
|
|
@@ -133,9 +139,31 @@ const unknownRefreshReason = () => {
|
|
|
133
139
|
return { _tag: 'unknown' as const }
|
|
134
140
|
}
|
|
135
141
|
|
|
136
|
-
export type
|
|
142
|
+
export type EncodedOption<A> = { _tag: 'Some'; value?: A } | { _tag: 'None' }
|
|
143
|
+
const encodedOptionSome = <A>(value: A): EncodedOption<A> => ({ _tag: 'Some', value })
|
|
144
|
+
const encodedOptionNone = <A>(): EncodedOption<A> => ({ _tag: 'None' })
|
|
145
|
+
|
|
146
|
+
export type SerializedAtom = SerializedRef | SerializedThunk
|
|
147
|
+
|
|
148
|
+
export type SerializedRef = Readonly<
|
|
149
|
+
PrettifyFlat<
|
|
150
|
+
Pick<Ref<unknown, unknown, any>, '_tag' | 'id' | 'label' | 'meta' | 'isDirty' | 'isDestroyed' | 'refreshes'> & {
|
|
151
|
+
/** Is `None` if `getSnapshot` was called with `includeResults: false` which is the default */
|
|
152
|
+
previousResult: EncodedOption<string>
|
|
153
|
+
sub: ReadonlyArray<string>
|
|
154
|
+
super: ReadonlyArray<string>
|
|
155
|
+
}
|
|
156
|
+
>
|
|
157
|
+
>
|
|
158
|
+
|
|
159
|
+
export type SerializedThunk = Readonly<
|
|
137
160
|
PrettifyFlat<
|
|
138
|
-
Pick<
|
|
161
|
+
Pick<
|
|
162
|
+
Thunk<unknown, unknown, any>,
|
|
163
|
+
'_tag' | 'id' | 'label' | 'meta' | 'isDirty' | 'isDestroyed' | 'recomputations'
|
|
164
|
+
> & {
|
|
165
|
+
/** Is `None` if `getSnapshot` was called with `includeResults: false` which is the default */
|
|
166
|
+
previousResult: EncodedOption<string>
|
|
139
167
|
sub: ReadonlyArray<string>
|
|
140
168
|
super: ReadonlyArray<string>
|
|
141
169
|
}
|
|
@@ -144,13 +172,13 @@ export type SerializedAtom = Readonly<
|
|
|
144
172
|
|
|
145
173
|
export type SerializedEffect = Readonly<
|
|
146
174
|
PrettifyFlat<
|
|
147
|
-
Pick<Effect, '_tag' | 'id' | 'label'> & {
|
|
175
|
+
Pick<Effect, '_tag' | 'id' | 'label' | 'invocations' | 'isDestroyed'> & {
|
|
148
176
|
sub: ReadonlyArray<string>
|
|
149
177
|
}
|
|
150
178
|
>
|
|
151
179
|
>
|
|
152
180
|
|
|
153
|
-
type ReactiveGraphSnapshot = {
|
|
181
|
+
export type ReactiveGraphSnapshot = {
|
|
154
182
|
readonly atoms: ReadonlyArray<SerializedAtom>
|
|
155
183
|
readonly effects: ReadonlyArray<SerializedEffect>
|
|
156
184
|
/** IDs of deferred effects */
|
|
@@ -267,7 +295,7 @@ export class ReactiveGraph<
|
|
|
267
295
|
const resultChanged = thunk.equal(thunk.previousResult as T, result) === false
|
|
268
296
|
|
|
269
297
|
const debugInfoForAtom = {
|
|
270
|
-
atom: serializeAtom(thunk),
|
|
298
|
+
atom: serializeAtom(thunk, false),
|
|
271
299
|
resultChanged,
|
|
272
300
|
debugInfo: debugInfo ?? (unknownRefreshReason() as TDebugThunkInfo),
|
|
273
301
|
} satisfies AtomDebugInfo<TDebugThunkInfo>
|
|
@@ -290,7 +318,7 @@ export class ReactiveGraph<
|
|
|
290
318
|
refreshedAtoms,
|
|
291
319
|
durationMs,
|
|
292
320
|
completedTimestamp: Date.now(),
|
|
293
|
-
graphSnapshot: this.getSnapshot(),
|
|
321
|
+
graphSnapshot: this.getSnapshot({ includeResults: false }),
|
|
294
322
|
})
|
|
295
323
|
}
|
|
296
324
|
|
|
@@ -455,13 +483,11 @@ export class ReactiveGraph<
|
|
|
455
483
|
refreshedAtoms,
|
|
456
484
|
durationMs,
|
|
457
485
|
completedTimestamp: Date.now(),
|
|
458
|
-
graphSnapshot: this.getSnapshot(),
|
|
486
|
+
graphSnapshot: this.getSnapshot({ includeResults: false }),
|
|
459
487
|
}
|
|
460
488
|
this.debugRefreshInfos.push(refreshDebugInfo)
|
|
461
489
|
|
|
462
|
-
|
|
463
|
-
cb()
|
|
464
|
-
}
|
|
490
|
+
this.runRefreshCallbacks()
|
|
465
491
|
})
|
|
466
492
|
}
|
|
467
493
|
|
|
@@ -481,12 +507,20 @@ export class ReactiveGraph<
|
|
|
481
507
|
}
|
|
482
508
|
}
|
|
483
509
|
|
|
510
|
+
runRefreshCallbacks = () => {
|
|
511
|
+
for (const cb of this.refreshCallbacks) {
|
|
512
|
+
cb()
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
484
516
|
addEdge(
|
|
485
517
|
superComp: Thunk<any, TContext, TDebugRefreshReason> | Effect,
|
|
486
518
|
subComp: Atom<any, TContext, TDebugRefreshReason>,
|
|
487
519
|
) {
|
|
488
520
|
superComp.sub.add(subComp)
|
|
489
521
|
subComp.super.add(superComp)
|
|
522
|
+
|
|
523
|
+
this.runRefreshCallbacks()
|
|
490
524
|
}
|
|
491
525
|
|
|
492
526
|
removeEdge(
|
|
@@ -502,13 +536,16 @@ export class ReactiveGraph<
|
|
|
502
536
|
}
|
|
503
537
|
|
|
504
538
|
subComp.super.delete(superComp)
|
|
539
|
+
|
|
540
|
+
this.runRefreshCallbacks()
|
|
505
541
|
}
|
|
506
542
|
|
|
507
543
|
// NOTE This function is performance-optimized (i.e. not using `Array.from`)
|
|
508
|
-
getSnapshot = (): ReactiveGraphSnapshot => {
|
|
544
|
+
getSnapshot = (opts?: { includeResults: boolean }): ReactiveGraphSnapshot => {
|
|
545
|
+
const { includeResults = false } = opts ?? {}
|
|
509
546
|
const atoms: SerializedAtom[] = []
|
|
510
547
|
for (const atom of this.atoms) {
|
|
511
|
-
atoms.push(serializeAtom(atom))
|
|
548
|
+
atoms.push(serializeAtom(atom, includeResults))
|
|
512
549
|
}
|
|
513
550
|
|
|
514
551
|
const effects: SerializedEffect[] = []
|
|
@@ -535,7 +572,7 @@ export class ReactiveGraph<
|
|
|
535
572
|
const compute = <T>(atom: Atom<T, unknown, any>, otelContext: otel.Context): T => {
|
|
536
573
|
// const __getResult = atom._tag === 'thunk' ? atom.__getResult.toString() : ''
|
|
537
574
|
if (atom.isDestroyed) {
|
|
538
|
-
shouldNeverHappen(`LiveStore Error: Attempted to compute destroyed atom`)
|
|
575
|
+
shouldNeverHappen(`LiveStore Error: Attempted to compute destroyed ${atom._tag} (${atom.id}): ${atom.label ?? ''}`)
|
|
539
576
|
}
|
|
540
577
|
|
|
541
578
|
if (atom.isDirty) {
|
|
@@ -566,7 +603,7 @@ export const throwContextNotSetError = (graph: ReactiveGraph<any, any, any>): ne
|
|
|
566
603
|
}
|
|
567
604
|
|
|
568
605
|
// NOTE This function is performance-optimized (i.e. not using `pick` and `Array.from`)
|
|
569
|
-
const serializeAtom = (atom: Atom<any, unknown, any
|
|
606
|
+
const serializeAtom = (atom: Atom<any, unknown, any>, includeResult: boolean): SerializedAtom => {
|
|
570
607
|
const sub: string[] = []
|
|
571
608
|
for (const a of atom.sub) {
|
|
572
609
|
sub.push(a.id)
|
|
@@ -577,14 +614,36 @@ const serializeAtom = (atom: Atom<any, unknown, any>): SerializedAtom => {
|
|
|
577
614
|
super_.push(a.id)
|
|
578
615
|
}
|
|
579
616
|
|
|
617
|
+
const previousResult: EncodedOption<string> = includeResult
|
|
618
|
+
? encodedOptionSome(JSON.stringify(atom.previousResult))
|
|
619
|
+
: encodedOptionNone()
|
|
620
|
+
|
|
621
|
+
if (atom._tag === 'ref') {
|
|
622
|
+
return {
|
|
623
|
+
_tag: atom._tag,
|
|
624
|
+
id: atom.id,
|
|
625
|
+
label: atom.label,
|
|
626
|
+
meta: atom.meta,
|
|
627
|
+
isDirty: atom.isDirty,
|
|
628
|
+
sub,
|
|
629
|
+
super: super_,
|
|
630
|
+
isDestroyed: atom.isDestroyed,
|
|
631
|
+
refreshes: atom.refreshes,
|
|
632
|
+
previousResult,
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
580
636
|
return {
|
|
581
|
-
_tag:
|
|
637
|
+
_tag: 'thunk',
|
|
582
638
|
id: atom.id,
|
|
583
639
|
label: atom.label,
|
|
584
640
|
meta: atom.meta,
|
|
585
641
|
isDirty: atom.isDirty,
|
|
586
642
|
sub,
|
|
587
643
|
super: super_,
|
|
644
|
+
isDestroyed: atom.isDestroyed,
|
|
645
|
+
recomputations: atom.recomputations,
|
|
646
|
+
previousResult,
|
|
588
647
|
}
|
|
589
648
|
}
|
|
590
649
|
|
|
@@ -600,5 +659,7 @@ const serializeEffect = (effect: Effect): SerializedEffect => {
|
|
|
600
659
|
id: effect.id,
|
|
601
660
|
label: effect.label,
|
|
602
661
|
sub,
|
|
662
|
+
invocations: effect.invocations,
|
|
663
|
+
isDestroyed: effect.isDestroyed,
|
|
603
664
|
}
|
|
604
665
|
}
|
|
@@ -22,7 +22,8 @@ export type DbContext = {
|
|
|
22
22
|
|
|
23
23
|
export type UnsubscribeQuery = () => void
|
|
24
24
|
|
|
25
|
-
export type GetResult<TQuery extends LiveQueryAny> =
|
|
25
|
+
export type GetResult<TQuery extends LiveQueryAny> =
|
|
26
|
+
TQuery extends LiveQuery<infer TResult, infer _1> ? TResult : unknown
|
|
26
27
|
|
|
27
28
|
let queryIdCounter = 0
|
|
28
29
|
|
|
@@ -6,7 +6,7 @@ import * as graphql from 'graphql'
|
|
|
6
6
|
|
|
7
7
|
import { globalDbGraph } from '../global-state.js'
|
|
8
8
|
import type { QueryInfoNone } from '../query-info.js'
|
|
9
|
-
import type
|
|
9
|
+
import { isThunk, type Thunk } from '../reactive.js'
|
|
10
10
|
import type { BaseGraphQLContext, RefreshReason, Store } from '../store.js'
|
|
11
11
|
import { getDurationMsFromSpan } from '../utils/otel.js'
|
|
12
12
|
import type { DbContext, DbGraph, GetAtomResult, LiveQuery } from './base-class.js'
|
|
@@ -39,7 +39,7 @@ export class LiveStoreGraphQLQuery<
|
|
|
39
39
|
/** A reactive thunk representing the query results */
|
|
40
40
|
results$: Thunk<TResultMapped, DbContext, RefreshReason>
|
|
41
41
|
|
|
42
|
-
variableValues$: Thunk<TVariableValues, DbContext, RefreshReason>
|
|
42
|
+
variableValues$: Thunk<TVariableValues, DbContext, RefreshReason> | undefined
|
|
43
43
|
|
|
44
44
|
label: string
|
|
45
45
|
|
|
@@ -89,23 +89,26 @@ export class LiveStoreGraphQLQuery<
|
|
|
89
89
|
: shouldNeverHappen(`Invalid map function ${map}`)
|
|
90
90
|
|
|
91
91
|
// TODO don't even create a thunk if variables are static
|
|
92
|
-
|
|
93
|
-
(get, _setDebugInfo, { rootOtelContext }, otelContext) => {
|
|
94
|
-
if (typeof genVariableValues === 'function') {
|
|
95
|
-
return genVariableValues(makeGetAtomResult(get, otelContext ?? rootOtelContext))
|
|
96
|
-
} else {
|
|
97
|
-
return genVariableValues
|
|
98
|
-
}
|
|
99
|
-
},
|
|
100
|
-
{ label: `${labelWithDefault}:variableValues`, meta: { liveStoreThunkType: 'graphqlVariableValues' } },
|
|
101
|
-
)
|
|
92
|
+
let variableValues$OrvariableValues
|
|
102
93
|
|
|
103
|
-
|
|
94
|
+
if (typeof genVariableValues === 'function') {
|
|
95
|
+
variableValues$OrvariableValues = this.dbGraph.makeThunk(
|
|
96
|
+
(get, _setDebugInfo, { rootOtelContext }, otelContext) => {
|
|
97
|
+
return genVariableValues(makeGetAtomResult(get, otelContext ?? rootOtelContext))
|
|
98
|
+
},
|
|
99
|
+
{ label: `${labelWithDefault}:variableValues`, meta: { liveStoreThunkType: 'graphqlVariableValues' } },
|
|
100
|
+
)
|
|
101
|
+
this.variableValues$ = variableValues$OrvariableValues
|
|
102
|
+
} else {
|
|
103
|
+
variableValues$OrvariableValues = genVariableValues
|
|
104
|
+
}
|
|
104
105
|
|
|
105
106
|
const resultsLabel = `${labelWithDefault}:results`
|
|
106
107
|
this.results$ = this.dbGraph.makeThunk<TResultMapped>(
|
|
107
108
|
(get, setDebugInfo, { store, otelTracer, rootOtelContext }, otelContext) => {
|
|
108
|
-
const variableValues =
|
|
109
|
+
const variableValues = isThunk(variableValues$OrvariableValues)
|
|
110
|
+
? (get(variableValues$OrvariableValues) as TVariableValues)
|
|
111
|
+
: (variableValues$OrvariableValues as TVariableValues)
|
|
109
112
|
const { result, queriedTables, durationMs } = this.queryOnce({
|
|
110
113
|
document,
|
|
111
114
|
variableValues,
|
|
@@ -188,7 +191,11 @@ export class LiveStoreGraphQLQuery<
|
|
|
188
191
|
span.setStatus({ code: otel.SpanStatusCode.ERROR, message: 'GraphQL error' })
|
|
189
192
|
span.setAttribute('graphql.error', res.errors.join('\n'))
|
|
190
193
|
span.setAttribute('graphql.error-detail', JSON.stringify(res.errors))
|
|
191
|
-
console.error(`graphql error (${operationName})
|
|
194
|
+
console.error(`graphql error (${operationName}) - ${res.errors.length} errors`)
|
|
195
|
+
for (const error of res.errors) {
|
|
196
|
+
console.error(error)
|
|
197
|
+
}
|
|
198
|
+
debugger
|
|
192
199
|
}
|
|
193
200
|
|
|
194
201
|
span.end()
|
|
@@ -208,7 +215,10 @@ export class LiveStoreGraphQLQuery<
|
|
|
208
215
|
}
|
|
209
216
|
|
|
210
217
|
destroy = () => {
|
|
211
|
-
|
|
218
|
+
if (this.variableValues$ !== undefined) {
|
|
219
|
+
this.dbGraph.destroyNode(this.variableValues$)
|
|
220
|
+
}
|
|
221
|
+
|
|
212
222
|
this.dbGraph.destroyNode(this.results$)
|
|
213
223
|
}
|
|
214
224
|
}
|