@livestore/livestore 0.0.54-dev.5 → 0.0.55-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/dist/.tsbuildinfo +1 -1
- package/dist/MainDatabaseWrapper.d.ts +6 -5
- package/dist/MainDatabaseWrapper.d.ts.map +1 -1
- package/dist/MainDatabaseWrapper.js +3 -3
- package/dist/MainDatabaseWrapper.js.map +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 +9 -27
- package/dist/__tests__/react/fixture.d.ts.map +1 -1
- package/dist/__tests__/react/fixture.js +12 -10
- package/dist/__tests__/react/fixture.js.map +1 -1
- package/dist/effect/LiveStore.d.ts +20 -12
- package/dist/effect/LiveStore.d.ts.map +1 -1
- package/dist/effect/LiveStore.js +23 -22
- package/dist/effect/LiveStore.js.map +1 -1
- package/dist/effect/index.d.ts +1 -1
- package/dist/effect/index.d.ts.map +1 -1
- package/dist/effect/index.js +1 -1
- package/dist/effect/index.js.map +1 -1
- package/dist/global-state.d.ts +1 -3
- package/dist/global-state.d.ts.map +1 -1
- package/dist/global-state.js +2 -3
- package/dist/global-state.js.map +1 -1
- package/dist/index.d.ts +6 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -6
- package/dist/index.js.map +1 -1
- package/dist/react/LiveStoreContext.d.ts +5 -2
- package/dist/react/LiveStoreContext.d.ts.map +1 -1
- package/dist/react/LiveStoreContext.js +3 -0
- package/dist/react/LiveStoreContext.js.map +1 -1
- package/dist/react/LiveStoreProvider.d.ts +8 -7
- package/dist/react/LiveStoreProvider.d.ts.map +1 -1
- package/dist/react/LiveStoreProvider.js +70 -43
- package/dist/react/LiveStoreProvider.js.map +1 -1
- package/dist/react/LiveStoreProvider.test.js +33 -12
- package/dist/react/LiveStoreProvider.test.js.map +1 -1
- package/dist/react/components/LiveList.d.ts.map +1 -1
- package/dist/react/useAtom.d.ts +1 -1
- package/dist/react/useAtom.d.ts.map +1 -1
- package/dist/react/useLocalId.d.ts.map +1 -1
- package/dist/react/useQuery.d.ts.map +1 -1
- package/dist/react/useQuery.js +2 -2
- package/dist/react/useQuery.js.map +1 -1
- package/dist/react/useRow.d.ts +2 -2
- package/dist/react/useRow.d.ts.map +1 -1
- package/dist/react/useRow.js +5 -5
- package/dist/react/useRow.js.map +1 -1
- package/dist/react/useRow.test.js +22 -22
- package/dist/react/useRow.test.js.map +1 -1
- package/dist/react/useTemporaryQuery.d.ts.map +1 -1
- package/dist/react/useTemporaryQuery.js +1 -1
- package/dist/react/useTemporaryQuery.js.map +1 -1
- package/dist/react/utils/useStateRefWithReactiveInput.d.ts.map +1 -1
- package/dist/reactive.d.ts +1 -1
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +4 -5
- package/dist/reactive.js.map +1 -1
- package/dist/reactiveQueries/base-class.d.ts +6 -6
- package/dist/reactiveQueries/base-class.d.ts.map +1 -1
- package/dist/reactiveQueries/base-class.js +3 -3
- package/dist/reactiveQueries/base-class.js.map +1 -1
- package/dist/reactiveQueries/graphql.d.ts +8 -8
- package/dist/reactiveQueries/graphql.d.ts.map +1 -1
- package/dist/reactiveQueries/graphql.js +10 -10
- package/dist/reactiveQueries/graphql.js.map +1 -1
- package/dist/reactiveQueries/js.d.ts +6 -6
- package/dist/reactiveQueries/js.d.ts.map +1 -1
- package/dist/reactiveQueries/js.js +8 -8
- package/dist/reactiveQueries/js.js.map +1 -1
- package/dist/reactiveQueries/sql.d.ts +9 -10
- package/dist/reactiveQueries/sql.d.ts.map +1 -1
- package/dist/reactiveQueries/sql.js +12 -12
- package/dist/reactiveQueries/sql.js.map +1 -1
- package/dist/reactiveQueries/sql.test.js +6 -6
- package/dist/reactiveQueries/sql.test.js.map +1 -1
- package/dist/row-query.d.ts +2 -2
- package/dist/row-query.d.ts.map +1 -1
- package/dist/row-query.js +4 -38
- package/dist/row-query.js.map +1 -1
- package/dist/store.d.ts +41 -24
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +336 -223
- package/dist/store.js.map +1 -1
- package/dist/utils/otel.d.ts.map +1 -1
- package/package.json +10 -19
- package/src/MainDatabaseWrapper.ts +14 -8
- package/src/QueryCache.ts +1 -2
- package/src/__tests__/react/fixture.tsx +13 -11
- package/src/effect/LiveStore.ts +65 -54
- package/src/effect/index.ts +2 -1
- package/src/global-state.ts +2 -6
- package/src/index.ts +25 -7
- package/src/react/LiveStoreContext.ts +7 -2
- package/src/react/LiveStoreProvider.test.tsx +56 -14
- package/src/react/LiveStoreProvider.tsx +105 -46
- package/src/react/useQuery.ts +2 -2
- package/src/react/useRow.test.tsx +22 -22
- package/src/react/useRow.ts +7 -10
- package/src/react/useTemporaryQuery.ts +2 -2
- package/src/reactive.ts +6 -5
- package/src/reactiveQueries/base-class.ts +9 -9
- package/src/reactiveQueries/graphql.ts +19 -15
- package/src/reactiveQueries/js.ts +12 -12
- package/src/reactiveQueries/sql.test.ts +6 -6
- package/src/reactiveQueries/sql.ts +19 -21
- package/src/row-query.ts +8 -54
- package/src/store.ts +533 -284
- package/dist/utils/bounded-collections.d.ts +0 -34
- package/dist/utils/bounded-collections.d.ts.map +0 -1
- package/dist/utils/bounded-collections.js +0 -91
- package/dist/utils/bounded-collections.js.map +0 -1
- package/dist/utils/util.d.ts +0 -14
- package/dist/utils/util.d.ts.map +0 -1
- package/dist/utils/util.js +0 -19
- package/dist/utils/util.js.map +0 -1
- package/src/utils/util.ts +0 -31
|
@@ -7,27 +7,25 @@ import { describe, expect, it } from 'vitest'
|
|
|
7
7
|
|
|
8
8
|
import { parseTodos, schema } from '../__tests__/react/fixture.js'
|
|
9
9
|
import { querySQL } from '../reactiveQueries/sql.js'
|
|
10
|
-
import type { Store } from '../store.js'
|
|
11
10
|
import * as LiveStoreReact from './index.js'
|
|
12
11
|
import { LiveStoreProvider } from './LiveStoreProvider.js'
|
|
13
12
|
|
|
14
13
|
describe('LiveStoreProvider', () => {
|
|
15
14
|
it('simple', async () => {
|
|
16
|
-
let
|
|
15
|
+
let appRenderCount = 0
|
|
17
16
|
|
|
18
17
|
const allTodos$ = querySQL(`select * from todos`, { map: parseTodos })
|
|
19
|
-
let latestStoreCtx: { store: Store } | undefined = undefined
|
|
20
18
|
|
|
21
19
|
const App = () => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
latestStoreCtx = LiveStoreReact.useStore()
|
|
20
|
+
appRenderCount++
|
|
25
21
|
|
|
26
22
|
const todos = LiveStoreReact.useQuery(allTodos$)
|
|
27
23
|
|
|
28
24
|
return <div>{JSON.stringify(todos)}</div>
|
|
29
25
|
}
|
|
30
26
|
|
|
27
|
+
const abortController = new AbortController()
|
|
28
|
+
|
|
31
29
|
const Root = ({ forceUpdate }: { forceUpdate: number }) => {
|
|
32
30
|
const bootCb = React.useCallback(
|
|
33
31
|
(db: BootDb) =>
|
|
@@ -37,7 +35,13 @@ describe('LiveStoreProvider', () => {
|
|
|
37
35
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
38
36
|
const adapterMemo = React.useMemo(() => makeInMemoryAdapter(), [forceUpdate])
|
|
39
37
|
return (
|
|
40
|
-
<LiveStoreProvider
|
|
38
|
+
<LiveStoreProvider
|
|
39
|
+
schema={schema}
|
|
40
|
+
renderLoading={(status) => <div>Loading LiveStore: {status.stage}</div>}
|
|
41
|
+
adapter={adapterMemo}
|
|
42
|
+
boot={bootCb}
|
|
43
|
+
signal={abortController.signal}
|
|
44
|
+
>
|
|
41
45
|
<App />
|
|
42
46
|
</LiveStoreProvider>
|
|
43
47
|
)
|
|
@@ -45,19 +49,57 @@ describe('LiveStoreProvider', () => {
|
|
|
45
49
|
|
|
46
50
|
const { rerender } = render(<Root forceUpdate={1} />)
|
|
47
51
|
|
|
48
|
-
expect(
|
|
52
|
+
expect(appRenderCount).toBe(0)
|
|
49
53
|
|
|
50
|
-
await waitForElementToBeRemoved(() => screen.getByText('Loading LiveStore'))
|
|
54
|
+
await waitForElementToBeRemoved(() => screen.getByText((_) => _.startsWith('Loading LiveStore')))
|
|
51
55
|
|
|
52
|
-
expect(
|
|
56
|
+
expect(appRenderCount).toBe(1)
|
|
53
57
|
|
|
54
58
|
rerender(<Root forceUpdate={2} />)
|
|
55
59
|
|
|
56
|
-
await waitFor(() => screen.getByText('Loading LiveStore'))
|
|
57
|
-
await
|
|
60
|
+
await waitFor(() => screen.getByText('Loading LiveStore: loading'))
|
|
61
|
+
await waitFor(() => screen.getByText((_) => _.includes('buy milk')))
|
|
62
|
+
|
|
63
|
+
expect(appRenderCount).toBe(2)
|
|
64
|
+
|
|
65
|
+
abortController.abort()
|
|
66
|
+
|
|
67
|
+
await waitFor(() => screen.getByText('LiveStore Shutdown'))
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('error during boot', async () => {
|
|
71
|
+
let appRenderCount = 0
|
|
72
|
+
|
|
73
|
+
const App = () => {
|
|
74
|
+
appRenderCount++
|
|
75
|
+
|
|
76
|
+
return <div>hello world</div>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const Root = ({ forceUpdate }: { forceUpdate: number }) => {
|
|
80
|
+
const bootCb = React.useCallback(
|
|
81
|
+
(db: BootDb) =>
|
|
82
|
+
db.execute(sql`INSERT INTO todos_mising_table (id, text, completed) VALUES ('t1', 'buy milk', 0);`),
|
|
83
|
+
[],
|
|
84
|
+
)
|
|
85
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
86
|
+
const adapterMemo = React.useMemo(() => makeInMemoryAdapter(), [forceUpdate])
|
|
87
|
+
return (
|
|
88
|
+
<LiveStoreProvider
|
|
89
|
+
schema={schema}
|
|
90
|
+
renderLoading={(status) => <div>Loading LiveStore: {status.stage}</div>}
|
|
91
|
+
adapter={adapterMemo}
|
|
92
|
+
boot={bootCb}
|
|
93
|
+
>
|
|
94
|
+
<App />
|
|
95
|
+
</LiveStoreProvider>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
render(<Root forceUpdate={1} />)
|
|
58
100
|
|
|
59
|
-
expect(
|
|
101
|
+
expect(appRenderCount).toBe(0)
|
|
60
102
|
|
|
61
|
-
await
|
|
103
|
+
await waitFor(() => screen.getByText((_) => _.startsWith('LiveStore.UnexpectedError')))
|
|
62
104
|
})
|
|
63
105
|
})
|
|
@@ -1,53 +1,66 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type BootDb, type BootStatus, type StoreAdapterFactory, UnexpectedError } from '@livestore/common'
|
|
2
2
|
import type { LiveStoreSchema } from '@livestore/common/schema'
|
|
3
|
-
import {
|
|
3
|
+
import { errorToString } from '@livestore/utils'
|
|
4
|
+
import { Effect, Exit, FiberSet, Logger, LogLevel, Schema, Scope } from '@livestore/utils/effect'
|
|
4
5
|
import type * as otel from '@opentelemetry/api'
|
|
5
6
|
import type { ReactElement, ReactNode } from 'react'
|
|
6
7
|
import React from 'react'
|
|
7
8
|
|
|
8
9
|
// TODO refactor so the `react` module doesn't depend on `effect` module
|
|
9
10
|
import type { LiveStoreContext as StoreContext_, LiveStoreCreateStoreOptions } from '../effect/LiveStore.js'
|
|
10
|
-
import type { BaseGraphQLContext, GraphQLOptions,
|
|
11
|
+
import type { BaseGraphQLContext, GraphQLOptions, OtelOptions } from '../store.js'
|
|
11
12
|
import { createStore } from '../store.js'
|
|
12
13
|
import { LiveStoreContext } from './LiveStoreContext.js'
|
|
13
14
|
|
|
14
15
|
interface LiveStoreProviderProps<GraphQLContext> {
|
|
15
16
|
schema: LiveStoreSchema
|
|
16
|
-
boot?: (db: BootDb, parentSpan: otel.Span) =>
|
|
17
|
+
boot?: (db: BootDb, parentSpan: otel.Span) => void | Promise<void> | Effect.Effect<void, unknown, otel.Tracer>
|
|
17
18
|
graphQLOptions?: GraphQLOptions<GraphQLContext>
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
fallback: ReactElement
|
|
19
|
+
otelOptions?: OtelOptions
|
|
20
|
+
renderLoading: (status: BootStatus) => ReactElement
|
|
21
21
|
adapter: StoreAdapterFactory
|
|
22
22
|
batchUpdates?: (run: () => void) => void
|
|
23
23
|
disableDevtools?: boolean
|
|
24
|
+
signal?: AbortSignal
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export const LiveStoreProvider = <GraphQLContext extends BaseGraphQLContext>({
|
|
27
|
-
|
|
28
|
+
renderLoading,
|
|
28
29
|
graphQLOptions,
|
|
29
|
-
|
|
30
|
-
otelRootSpanContext,
|
|
30
|
+
otelOptions,
|
|
31
31
|
children,
|
|
32
32
|
schema,
|
|
33
33
|
boot,
|
|
34
34
|
adapter,
|
|
35
35
|
batchUpdates,
|
|
36
36
|
disableDevtools,
|
|
37
|
+
signal,
|
|
37
38
|
}: LiveStoreProviderProps<GraphQLContext> & { children?: ReactNode }): JSX.Element => {
|
|
38
39
|
const storeCtx = useCreateStore({
|
|
39
40
|
schema,
|
|
40
41
|
graphQLOptions,
|
|
41
|
-
|
|
42
|
-
otelRootSpanContext,
|
|
42
|
+
otelOptions,
|
|
43
43
|
boot,
|
|
44
44
|
adapter,
|
|
45
45
|
batchUpdates,
|
|
46
46
|
disableDevtools,
|
|
47
|
+
signal,
|
|
47
48
|
})
|
|
48
49
|
|
|
49
|
-
if (storeCtx ===
|
|
50
|
-
return
|
|
50
|
+
if (storeCtx.stage === 'error') {
|
|
51
|
+
return (
|
|
52
|
+
<div>
|
|
53
|
+
{Schema.is(UnexpectedError)(storeCtx.error) ? storeCtx.error.toString() : errorToString(storeCtx.error)}
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (storeCtx.stage === 'shutdown') {
|
|
59
|
+
return <div>LiveStore Shutdown</div>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (storeCtx.stage !== 'running') {
|
|
63
|
+
return <div>{renderLoading(storeCtx)}</div>
|
|
51
64
|
}
|
|
52
65
|
|
|
53
66
|
window.__debugLiveStore = storeCtx.store
|
|
@@ -58,78 +71,124 @@ export const LiveStoreProvider = <GraphQLContext extends BaseGraphQLContext>({
|
|
|
58
71
|
const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
|
|
59
72
|
schema,
|
|
60
73
|
graphQLOptions,
|
|
61
|
-
|
|
62
|
-
otelRootSpanContext,
|
|
74
|
+
otelOptions,
|
|
63
75
|
boot,
|
|
64
76
|
adapter,
|
|
65
77
|
batchUpdates,
|
|
66
78
|
disableDevtools,
|
|
79
|
+
signal,
|
|
67
80
|
}: LiveStoreCreateStoreOptions<GraphQLContext>) => {
|
|
68
81
|
const [_, rerender] = React.useState(0)
|
|
69
|
-
const ctxValueRef = React.useRef<
|
|
82
|
+
const ctxValueRef = React.useRef<{
|
|
83
|
+
value: StoreContext_ | BootStatus
|
|
84
|
+
scope: Scope.CloseableScope | undefined
|
|
85
|
+
counter: number
|
|
86
|
+
}>({
|
|
87
|
+
value: { stage: 'loading' },
|
|
88
|
+
scope: undefined,
|
|
89
|
+
counter: 0,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// console.debug(`useCreateStore (${ctxValueRef.current.counter})`, ctxValueRef.current.value.stage)
|
|
93
|
+
|
|
70
94
|
const inputPropsCacheRef = React.useRef({
|
|
71
95
|
schema,
|
|
72
96
|
graphQLOptions,
|
|
73
|
-
|
|
74
|
-
otelRootSpanContext,
|
|
97
|
+
otelOptions,
|
|
75
98
|
boot,
|
|
76
99
|
adapter,
|
|
77
100
|
batchUpdates,
|
|
101
|
+
disableDevtools,
|
|
102
|
+
signal,
|
|
78
103
|
})
|
|
79
|
-
const oldStoreAlreadyDestroyedRef = React.useRef(false)
|
|
80
104
|
|
|
81
105
|
if (
|
|
82
106
|
inputPropsCacheRef.current.schema !== schema ||
|
|
83
107
|
inputPropsCacheRef.current.graphQLOptions !== graphQLOptions ||
|
|
84
|
-
inputPropsCacheRef.current.
|
|
85
|
-
inputPropsCacheRef.current.otelRootSpanContext !== otelRootSpanContext ||
|
|
108
|
+
inputPropsCacheRef.current.otelOptions !== otelOptions ||
|
|
86
109
|
inputPropsCacheRef.current.boot !== boot ||
|
|
87
110
|
inputPropsCacheRef.current.adapter !== adapter ||
|
|
88
|
-
inputPropsCacheRef.current.batchUpdates !== batchUpdates
|
|
111
|
+
inputPropsCacheRef.current.batchUpdates !== batchUpdates ||
|
|
112
|
+
inputPropsCacheRef.current.disableDevtools !== disableDevtools ||
|
|
113
|
+
inputPropsCacheRef.current.signal !== signal
|
|
89
114
|
) {
|
|
90
115
|
inputPropsCacheRef.current = {
|
|
91
116
|
schema,
|
|
92
117
|
graphQLOptions,
|
|
93
|
-
|
|
94
|
-
otelRootSpanContext,
|
|
118
|
+
otelOptions,
|
|
95
119
|
boot,
|
|
96
120
|
adapter,
|
|
97
121
|
batchUpdates,
|
|
122
|
+
disableDevtools,
|
|
123
|
+
signal,
|
|
124
|
+
}
|
|
125
|
+
if (ctxValueRef.current.scope !== undefined) {
|
|
126
|
+
Scope.close(ctxValueRef.current.scope, Exit.void).pipe(Effect.tapCauseLogPretty, Effect.runFork)
|
|
98
127
|
}
|
|
99
|
-
ctxValueRef.current
|
|
100
|
-
oldStoreAlreadyDestroyedRef.current = true
|
|
101
|
-
ctxValueRef.current = undefined
|
|
128
|
+
ctxValueRef.current = { value: { stage: 'loading' }, scope: undefined, counter: ctxValueRef.current.counter + 1 }
|
|
102
129
|
}
|
|
103
130
|
|
|
104
131
|
React.useEffect(() => {
|
|
105
|
-
|
|
132
|
+
const storeScope = Scope.make().pipe(Effect.runSync)
|
|
106
133
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
134
|
+
const counter = ctxValueRef.current.counter
|
|
135
|
+
|
|
136
|
+
const setContextValue = (value: StoreContext_ | BootStatus) => {
|
|
137
|
+
if (ctxValueRef.current.counter !== counter) return
|
|
138
|
+
ctxValueRef.current.value = value
|
|
139
|
+
rerender((c) => c + 1)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
Scope.addFinalizer(
|
|
143
|
+
storeScope,
|
|
144
|
+
Effect.sync(() => setContextValue({ stage: 'shutdown' })),
|
|
145
|
+
).pipe(Effect.runSync)
|
|
146
|
+
|
|
147
|
+
ctxValueRef.current.scope = storeScope
|
|
148
|
+
|
|
149
|
+
signal?.addEventListener('abort', () => {
|
|
150
|
+
if (ctxValueRef.current.scope !== undefined && ctxValueRef.current.counter === counter) {
|
|
151
|
+
Scope.close(ctxValueRef.current.scope, Exit.void).pipe(Effect.tapCauseLogPretty, Effect.runFork)
|
|
152
|
+
ctxValueRef.current.scope = undefined
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
FiberSet.make().pipe(
|
|
157
|
+
Effect.andThen((fiberSet) =>
|
|
158
|
+
createStore({
|
|
159
|
+
fiberSet,
|
|
110
160
|
schema,
|
|
111
161
|
graphQLOptions,
|
|
112
|
-
|
|
113
|
-
otelRootSpanContext,
|
|
162
|
+
otelOptions,
|
|
114
163
|
boot,
|
|
115
164
|
adapter,
|
|
116
165
|
batchUpdates,
|
|
117
166
|
disableDevtools,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
167
|
+
onBootStatus: (status) => {
|
|
168
|
+
if (ctxValueRef.current.value.stage === 'running' || ctxValueRef.current.value.stage === 'error') return
|
|
169
|
+
setContextValue(status)
|
|
170
|
+
},
|
|
171
|
+
}),
|
|
172
|
+
),
|
|
173
|
+
Effect.tapSync((store) => setContextValue({ stage: 'running', store })),
|
|
174
|
+
Effect.tapError((error) => Effect.sync(() => setContextValue({ stage: 'error', error }))),
|
|
175
|
+
Effect.tapDefect((defect) => Effect.sync(() => setContextValue({ stage: 'error', error: defect }))),
|
|
176
|
+
Scope.extend(storeScope),
|
|
177
|
+
Effect.forkIn(storeScope),
|
|
178
|
+
Effect.tapCauseLogPretty,
|
|
179
|
+
Effect.annotateLogs({ thread: 'window' }),
|
|
180
|
+
Effect.provide(Logger.pretty),
|
|
181
|
+
Logger.withMinimumLogLevel(LogLevel.Debug),
|
|
182
|
+
Effect.runFork,
|
|
183
|
+
)
|
|
126
184
|
|
|
127
185
|
return () => {
|
|
128
|
-
if (
|
|
129
|
-
|
|
186
|
+
if (ctxValueRef.current.scope !== undefined) {
|
|
187
|
+
Scope.close(ctxValueRef.current.scope, Exit.void).pipe(Effect.tapCauseLogPretty, Effect.runFork)
|
|
188
|
+
ctxValueRef.current.scope = undefined
|
|
130
189
|
}
|
|
131
190
|
}
|
|
132
|
-
}, [schema, graphQLOptions,
|
|
191
|
+
}, [schema, graphQLOptions, otelOptions, boot, adapter, batchUpdates, disableDevtools, signal])
|
|
133
192
|
|
|
134
|
-
return ctxValueRef.current
|
|
193
|
+
return ctxValueRef.current.value
|
|
135
194
|
}
|
package/src/react/useQuery.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { deepEqual } from '@livestore/utils'
|
|
1
2
|
import * as otel from '@opentelemetry/api'
|
|
2
|
-
import { isEqual } from 'lodash-es'
|
|
3
3
|
import React from 'react'
|
|
4
4
|
|
|
5
5
|
import type { GetResult, LiveQueryAny } from '../reactiveQueries/base-class.js'
|
|
@@ -90,7 +90,7 @@ export const useQueryRef = <TQuery extends LiveQueryAny>(
|
|
|
90
90
|
// NOTE: we return a reference to the result object within LiveStore;
|
|
91
91
|
// this implies that app code must not mutate the results, or else
|
|
92
92
|
// there may be weird reactivity bugs.
|
|
93
|
-
if (
|
|
93
|
+
if (deepEqual(newValue, valueRef.current) === false) {
|
|
94
94
|
setValue(newValue)
|
|
95
95
|
}
|
|
96
96
|
},
|
|
@@ -15,9 +15,9 @@ import type { StackInfo } from './utils/stack-info.js'
|
|
|
15
15
|
// NOTE running tests concurrently doesn't work with the default global db graph
|
|
16
16
|
describe.concurrent('useRow', () => {
|
|
17
17
|
it('should update the data based on component key', async () => {
|
|
18
|
-
using inputs = await makeTodoMvc({
|
|
18
|
+
using inputs = await makeTodoMvc({ useGlobalReactivityGraph: false })
|
|
19
19
|
|
|
20
|
-
const { wrapper, AppComponentSchema, store,
|
|
20
|
+
const { wrapper, AppComponentSchema, store, reactivityGraph, makeRenderCount } = inputs
|
|
21
21
|
|
|
22
22
|
const renderCount = makeRenderCount()
|
|
23
23
|
|
|
@@ -25,7 +25,7 @@ describe.concurrent('useRow', () => {
|
|
|
25
25
|
(userId: string) => {
|
|
26
26
|
renderCount.inc()
|
|
27
27
|
|
|
28
|
-
const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, {
|
|
28
|
+
const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
|
|
29
29
|
return { state, setState }
|
|
30
30
|
},
|
|
31
31
|
{ wrapper, initialProps: 'u1' },
|
|
@@ -53,9 +53,9 @@ describe.concurrent('useRow', () => {
|
|
|
53
53
|
// TODO add a test that makes sure React doesn't re-render when a setter is used to set the same value
|
|
54
54
|
|
|
55
55
|
it('should update the data reactively - via setState', async () => {
|
|
56
|
-
using inputs = await makeTodoMvc({
|
|
56
|
+
using inputs = await makeTodoMvc({ useGlobalReactivityGraph: false })
|
|
57
57
|
|
|
58
|
-
const { wrapper, AppComponentSchema,
|
|
58
|
+
const { wrapper, AppComponentSchema, reactivityGraph, makeRenderCount } = inputs
|
|
59
59
|
|
|
60
60
|
const renderCount = makeRenderCount()
|
|
61
61
|
|
|
@@ -63,7 +63,7 @@ describe.concurrent('useRow', () => {
|
|
|
63
63
|
(userId: string) => {
|
|
64
64
|
renderCount.inc()
|
|
65
65
|
|
|
66
|
-
const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, {
|
|
66
|
+
const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
|
|
67
67
|
return { state, setState }
|
|
68
68
|
},
|
|
69
69
|
{ wrapper, initialProps: 'u1' },
|
|
@@ -81,9 +81,9 @@ describe.concurrent('useRow', () => {
|
|
|
81
81
|
})
|
|
82
82
|
|
|
83
83
|
it('should update the data reactively - via raw store mutation', async () => {
|
|
84
|
-
using inputs = await makeTodoMvc({
|
|
84
|
+
using inputs = await makeTodoMvc({ useGlobalReactivityGraph: false })
|
|
85
85
|
|
|
86
|
-
const { wrapper, AppComponentSchema, store,
|
|
86
|
+
const { wrapper, AppComponentSchema, store, reactivityGraph, makeRenderCount } = inputs
|
|
87
87
|
|
|
88
88
|
const renderCount = makeRenderCount()
|
|
89
89
|
|
|
@@ -91,7 +91,7 @@ describe.concurrent('useRow', () => {
|
|
|
91
91
|
(userId: string) => {
|
|
92
92
|
renderCount.inc()
|
|
93
93
|
|
|
94
|
-
const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, {
|
|
94
|
+
const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
|
|
95
95
|
return { state, setState }
|
|
96
96
|
},
|
|
97
97
|
{ wrapper, initialProps: 'u1' },
|
|
@@ -115,17 +115,17 @@ describe.concurrent('useRow', () => {
|
|
|
115
115
|
})
|
|
116
116
|
|
|
117
117
|
it('should work for a larger app', async () => {
|
|
118
|
-
using inputs = await makeTodoMvc({
|
|
119
|
-
const { wrapper, store,
|
|
118
|
+
using inputs = await makeTodoMvc({ useGlobalReactivityGraph: false })
|
|
119
|
+
const { wrapper, store, reactivityGraph, makeRenderCount, AppRouterSchema } = inputs
|
|
120
120
|
|
|
121
|
-
const allTodos$ = LiveStore.querySQL<Todo[]>(`select * from todos`, { label: 'allTodos',
|
|
121
|
+
const allTodos$ = LiveStore.querySQL<Todo[]>(`select * from todos`, { label: 'allTodos', reactivityGraph })
|
|
122
122
|
|
|
123
123
|
const appRouterRenderCount = makeRenderCount()
|
|
124
124
|
let globalSetState: LiveStoreReact.StateSetters<typeof AppRouterSchema> | undefined
|
|
125
125
|
const AppRouter: React.FC = () => {
|
|
126
126
|
appRouterRenderCount.inc()
|
|
127
127
|
|
|
128
|
-
const [state, setState] = LiveStoreReact.useRow(AppRouterSchema, {
|
|
128
|
+
const [state, setState] = LiveStoreReact.useRow(AppRouterSchema, { reactivityGraph })
|
|
129
129
|
|
|
130
130
|
globalSetState = setState
|
|
131
131
|
|
|
@@ -153,7 +153,7 @@ describe.concurrent('useRow', () => {
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
const TaskDetails: React.FC<{ id: string }> = ({ id }) => {
|
|
156
|
-
const [todo] = LiveStoreReact.useRow(todos, id, {
|
|
156
|
+
const [todo] = LiveStoreReact.useRow(todos, id, { reactivityGraph })
|
|
157
157
|
return <div role="content">{JSON.stringify(todo)}</div>
|
|
158
158
|
}
|
|
159
159
|
|
|
@@ -198,8 +198,8 @@ describe.concurrent('useRow', () => {
|
|
|
198
198
|
})
|
|
199
199
|
|
|
200
200
|
it('should work for a useRow query chained with a useTemporary query', async () => {
|
|
201
|
-
using inputs = await makeTodoMvc({
|
|
202
|
-
const { store, wrapper, AppComponentSchema,
|
|
201
|
+
using inputs = await makeTodoMvc({ useGlobalReactivityGraph: false })
|
|
202
|
+
const { store, wrapper, AppComponentSchema, reactivityGraph, makeRenderCount } = inputs
|
|
203
203
|
const renderCount = makeRenderCount()
|
|
204
204
|
|
|
205
205
|
store.mutate(
|
|
@@ -211,12 +211,12 @@ describe.concurrent('useRow', () => {
|
|
|
211
211
|
(userId: string) => {
|
|
212
212
|
renderCount.inc()
|
|
213
213
|
|
|
214
|
-
const [_row, _setRow, rowState$] = LiveStoreReact.useRow(AppComponentSchema, userId, {
|
|
214
|
+
const [_row, _setRow, rowState$] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
|
|
215
215
|
const todos = LiveStoreReact.useTemporaryQuery(
|
|
216
216
|
() =>
|
|
217
217
|
LiveStore.querySQL<any[]>(
|
|
218
218
|
(get) => LiveStore.sql`select * from todos where text like '%${get(rowState$).text}%'`,
|
|
219
|
-
{
|
|
219
|
+
{ reactivityGraph, label: 'todosFiltered' },
|
|
220
220
|
),
|
|
221
221
|
userId,
|
|
222
222
|
)
|
|
@@ -262,9 +262,9 @@ describe.concurrent('useRow', () => {
|
|
|
262
262
|
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
263
263
|
|
|
264
264
|
it('should update the data based on component key', async () => {
|
|
265
|
-
using inputs = await makeTodoMvc({
|
|
265
|
+
using inputs = await makeTodoMvc({ useGlobalReactivityGraph: false, otelContext, otelTracer })
|
|
266
266
|
|
|
267
|
-
const { wrapper, AppComponentSchema, store,
|
|
267
|
+
const { wrapper, AppComponentSchema, store, reactivityGraph, makeRenderCount, strictMode } = inputs
|
|
268
268
|
|
|
269
269
|
const renderCount = makeRenderCount()
|
|
270
270
|
|
|
@@ -272,7 +272,7 @@ describe.concurrent('useRow', () => {
|
|
|
272
272
|
(userId: string) => {
|
|
273
273
|
renderCount.inc()
|
|
274
274
|
|
|
275
|
-
const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, {
|
|
275
|
+
const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
|
|
276
276
|
return { state, setState }
|
|
277
277
|
},
|
|
278
278
|
{ wrapper, initialProps: 'u1' },
|
|
@@ -297,7 +297,7 @@ describe.concurrent('useRow', () => {
|
|
|
297
297
|
expect(renderCount.val).toBe(2)
|
|
298
298
|
|
|
299
299
|
unmount()
|
|
300
|
-
store.destroy()
|
|
300
|
+
await store.destroy()
|
|
301
301
|
span.end()
|
|
302
302
|
|
|
303
303
|
const mapAttributes = (attributes: otel.Attributes) => {
|
package/src/react/useRow.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import type { QueryInfo } from '@livestore/common'
|
|
2
|
-
import {} from // deriveCreateMutationDef as deriveCreateMutationDef_,
|
|
3
|
-
// updateMutationForQueryInfo as updateMutationForQueryInfo_,
|
|
4
|
-
'@livestore/common'
|
|
5
2
|
import { DbSchema } from '@livestore/common/schema'
|
|
6
3
|
import { shouldNeverHappen } from '@livestore/utils'
|
|
4
|
+
import { ReadonlyRecord } from '@livestore/utils/effect'
|
|
7
5
|
import type { SqliteDsl } from 'effect-db-schema'
|
|
8
|
-
import { mapValues } from 'lodash-es'
|
|
9
6
|
import React from 'react'
|
|
10
7
|
|
|
11
|
-
import type {
|
|
8
|
+
import type { LiveQuery, ReactivityGraph } from '../index.js'
|
|
12
9
|
import type { RowResult } from '../row-query.js'
|
|
13
10
|
import { rowQuery } from '../row-query.js'
|
|
14
11
|
import { useStore } from './LiveStoreContext.js'
|
|
@@ -26,7 +23,7 @@ export type UseRowOptionsDefaulValues<TTableDef extends DbSchema.TableDef> = {
|
|
|
26
23
|
}
|
|
27
24
|
|
|
28
25
|
export type UseRowOptionsBase = {
|
|
29
|
-
|
|
26
|
+
reactivityGraph?: ReactivityGraph
|
|
30
27
|
}
|
|
31
28
|
|
|
32
29
|
/**
|
|
@@ -76,7 +73,7 @@ export const useRow: {
|
|
|
76
73
|
const id = typeof idOrOptions === 'string' ? idOrOptions : undefined
|
|
77
74
|
const options: (UseRowOptionsBase & UseRowOptionsDefaulValues<TTableDef>) | undefined =
|
|
78
75
|
typeof idOrOptions === 'string' ? options_ : idOrOptions
|
|
79
|
-
const { defaultValues,
|
|
76
|
+
const { defaultValues, reactivityGraph } = options ?? {}
|
|
80
77
|
|
|
81
78
|
type TComponentState = SqliteDsl.FromColumns.RowDecoded<TTableDef['sqliteDef']['columns']>
|
|
82
79
|
|
|
@@ -100,11 +97,11 @@ export const useRow: {
|
|
|
100
97
|
const { query$, otelContext } = useMakeTemporaryQuery(
|
|
101
98
|
(otelContext) =>
|
|
102
99
|
DbSchema.tableIsSingleton(table)
|
|
103
|
-
? (rowQuery(table, { otelContext,
|
|
100
|
+
? (rowQuery(table, { otelContext, reactivityGraph }) as LiveQuery<RowResult<TTableDef>, QueryInfo>)
|
|
104
101
|
: (rowQuery(table as TTableDef & { options: { isSingleton: false } }, id!, {
|
|
105
102
|
otelContext,
|
|
106
103
|
defaultValues: defaultValues!,
|
|
107
|
-
|
|
104
|
+
reactivityGraph,
|
|
108
105
|
}) as any as LiveQuery<RowResult<TTableDef>, QueryInfo>),
|
|
109
106
|
[id!, tableName],
|
|
110
107
|
{
|
|
@@ -133,7 +130,7 @@ export const useRow: {
|
|
|
133
130
|
}
|
|
134
131
|
} else {
|
|
135
132
|
const setState = // TODO: do we have a better type for the values that can go in SQLite?
|
|
136
|
-
|
|
133
|
+
ReadonlyRecord.map(sqliteTableDef.columns, (column, columnName) => (newValueOrFn: any) => {
|
|
137
134
|
const newValue =
|
|
138
135
|
// @ts-expect-error TODO fix typing
|
|
139
136
|
typeof newValueOrFn === 'function' ? newValueOrFn(query$Ref.current[columnName]) : newValueOrFn
|
|
@@ -53,8 +53,8 @@ export const useMakeTemporaryQuery = <TResult, TQueryInfo extends QueryInfo>(
|
|
|
53
53
|
const fullKey = React.useMemo(
|
|
54
54
|
// NOTE We're using the `makeQuery` function body string to make sure the key is unique across the app
|
|
55
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.
|
|
57
|
-
[key, makeQuery, store.
|
|
56
|
+
() => (Array.isArray(key) ? key.join('-') : key) + '-' + store.reactivityGraph.id + '-' + makeQuery.toString(),
|
|
57
|
+
[key, makeQuery, store.reactivityGraph.id],
|
|
58
58
|
)
|
|
59
59
|
const fullKeyRef = React.useRef<string>()
|
|
60
60
|
|
package/src/reactive.ts
CHANGED
|
@@ -25,9 +25,8 @@
|
|
|
25
25
|
|
|
26
26
|
import { BoundArray } from '@livestore/common'
|
|
27
27
|
import type { PrettifyFlat } from '@livestore/utils'
|
|
28
|
-
import { shouldNeverHappen } from '@livestore/utils'
|
|
28
|
+
import { deepEqual, shouldNeverHappen } from '@livestore/utils'
|
|
29
29
|
import type * as otel from '@opentelemetry/api'
|
|
30
|
-
import { isEqual } from 'lodash-es'
|
|
31
30
|
// import { getDurationMsFromSpan } from './otel.js'
|
|
32
31
|
|
|
33
32
|
export const NOT_REFRESHED_YET = Symbol.for('NOT_REFRESHED_YET')
|
|
@@ -234,7 +233,7 @@ export class ReactiveGraph<
|
|
|
234
233
|
super: new Set(),
|
|
235
234
|
label: options?.label,
|
|
236
235
|
meta: options?.meta,
|
|
237
|
-
equal: options?.equal ??
|
|
236
|
+
equal: options?.equal ?? deepEqual,
|
|
238
237
|
refreshes: 0,
|
|
239
238
|
}
|
|
240
239
|
|
|
@@ -331,7 +330,7 @@ export class ReactiveGraph<
|
|
|
331
330
|
recomputations: 0,
|
|
332
331
|
label: options?.label,
|
|
333
332
|
meta: options?.meta,
|
|
334
|
-
equal: options?.equal ??
|
|
333
|
+
equal: options?.equal ?? deepEqual,
|
|
335
334
|
__getResult: getResult,
|
|
336
335
|
}
|
|
337
336
|
|
|
@@ -618,7 +617,9 @@ const serializeAtom = (atom: Atom<any, unknown, any>, includeResult: boolean): S
|
|
|
618
617
|
}
|
|
619
618
|
|
|
620
619
|
const previousResult: EncodedOption<string> = includeResult
|
|
621
|
-
? encodedOptionSome(
|
|
620
|
+
? encodedOptionSome(
|
|
621
|
+
atom.previousResult === NOT_REFRESHED_YET ? '"SYMBOL_NOT_REFRESHED_YET"' : JSON.stringify(atom.previousResult),
|
|
622
|
+
)
|
|
622
623
|
: encodedOptionNone()
|
|
623
624
|
|
|
624
625
|
if (atom._tag === 'ref') {
|
|
@@ -6,15 +6,15 @@ import type { StackInfo } from '../react/utils/stack-info.js'
|
|
|
6
6
|
import { type Atom, type GetAtom, ReactiveGraph, throwContextNotSetError, type Thunk } from '../reactive.js'
|
|
7
7
|
import type { QueryDebugInfo, RefreshReason, Store } from '../store.js'
|
|
8
8
|
|
|
9
|
-
export type
|
|
9
|
+
export type ReactivityGraph = ReactiveGraph<RefreshReason, QueryDebugInfo, QueryContext>
|
|
10
10
|
|
|
11
|
-
export const
|
|
12
|
-
new ReactiveGraph<RefreshReason, QueryDebugInfo,
|
|
11
|
+
export const makeReactivityGraph = (): ReactivityGraph =>
|
|
12
|
+
new ReactiveGraph<RefreshReason, QueryDebugInfo, QueryContext>({
|
|
13
13
|
// TODO also find a better way to only use this effects wrapper when used in a React app
|
|
14
14
|
effectsWrapper: (run) => ReactDOM.unstable_batchedUpdates(() => run()),
|
|
15
15
|
})
|
|
16
16
|
|
|
17
|
-
export type
|
|
17
|
+
export type QueryContext = {
|
|
18
18
|
store: Store
|
|
19
19
|
otelTracer: otel.Tracer
|
|
20
20
|
rootOtelContext: otel.Context
|
|
@@ -37,7 +37,7 @@ export interface LiveQuery<TResult, TQueryInfo extends QueryInfo = QueryInfoNone
|
|
|
37
37
|
'__result!': TResult
|
|
38
38
|
|
|
39
39
|
/** A reactive thunk representing the query results */
|
|
40
|
-
results$: Thunk<TResult,
|
|
40
|
+
results$: Thunk<TResult, QueryContext, RefreshReason>
|
|
41
41
|
|
|
42
42
|
label: string
|
|
43
43
|
|
|
@@ -72,11 +72,11 @@ export abstract class LiveStoreQueryBase<TResult, TQueryInfo extends QueryInfo>
|
|
|
72
72
|
/** Human-readable label for the query for debugging */
|
|
73
73
|
abstract label: string
|
|
74
74
|
|
|
75
|
-
abstract results$: Thunk<TResult,
|
|
75
|
+
abstract results$: Thunk<TResult, QueryContext, RefreshReason>
|
|
76
76
|
|
|
77
77
|
activeSubscriptions: Set<StackInfo> = new Set()
|
|
78
78
|
|
|
79
|
-
protected abstract
|
|
79
|
+
protected abstract reactivityGraph: ReactivityGraph
|
|
80
80
|
|
|
81
81
|
abstract queryInfo: TQueryInfo
|
|
82
82
|
|
|
@@ -102,8 +102,8 @@ export abstract class LiveStoreQueryBase<TResult, TQueryInfo extends QueryInfo>
|
|
|
102
102
|
onUnsubsubscribe?: () => void,
|
|
103
103
|
options?: { label?: string; otelContext?: otel.Context } | undefined,
|
|
104
104
|
): (() => void) =>
|
|
105
|
-
this.
|
|
106
|
-
throwContextNotSetError(this.
|
|
105
|
+
this.reactivityGraph.context?.store.subscribe(this, onNewValue, onUnsubsubscribe, options) ??
|
|
106
|
+
throwContextNotSetError(this.reactivityGraph)
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
export type GetAtomResult = <T>(atom: Atom<T, any, RefreshReason> | LiveQuery<T, any>) => T
|