@livestore/react 0.4.0-dev.9 → 0.4.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 +1 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/StoreRegistryContext.d.ts +56 -0
- package/dist/StoreRegistryContext.d.ts.map +1 -0
- package/dist/StoreRegistryContext.js +61 -0
- package/dist/StoreRegistryContext.js.map +1 -0
- package/dist/__tests__/fixture.d.ts +12 -283
- package/dist/__tests__/fixture.d.ts.map +1 -1
- package/dist/__tests__/fixture.js +12 -86
- package/dist/__tests__/fixture.js.map +1 -1
- package/dist/experimental/components/LiveList.d.ts +4 -2
- package/dist/experimental/components/LiveList.d.ts.map +1 -1
- package/dist/experimental/components/LiveList.js +10 -8
- package/dist/experimental/components/LiveList.js.map +1 -1
- package/dist/mod.d.ts +8 -5
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +6 -4
- package/dist/mod.js.map +1 -1
- package/dist/useClientDocument.d.ts +12 -4
- package/dist/useClientDocument.d.ts.map +1 -1
- package/dist/useClientDocument.js +4 -18
- package/dist/useClientDocument.js.map +1 -1
- package/dist/useClientDocument.test.js +21 -9
- package/dist/useClientDocument.test.js.map +1 -1
- package/dist/useQuery.d.ts +29 -8
- package/dist/useQuery.d.ts.map +1 -1
- package/dist/useQuery.js +43 -70
- package/dist/useQuery.js.map +1 -1
- package/dist/useQuery.test.js +57 -13
- package/dist/useQuery.test.js.map +1 -1
- package/dist/useRcResource.d.ts +1 -1
- package/dist/useRcResource.d.ts.map +1 -1
- package/dist/useRcResource.js +41 -34
- package/dist/useRcResource.js.map +1 -1
- package/dist/useRcResource.test.js +72 -50
- package/dist/useRcResource.test.js.map +1 -1
- package/dist/useStore.d.ts +74 -7
- package/dist/useStore.d.ts.map +1 -1
- package/dist/useStore.js +82 -16
- package/dist/useStore.js.map +1 -1
- package/dist/useStore.test.d.ts +2 -0
- package/dist/useStore.test.d.ts.map +1 -0
- package/dist/useStore.test.js +241 -0
- package/dist/useStore.test.js.map +1 -0
- package/dist/useSyncStatus.d.ts +22 -0
- package/dist/useSyncStatus.d.ts.map +1 -0
- package/dist/useSyncStatus.js +28 -0
- package/dist/useSyncStatus.js.map +1 -0
- package/package.json +69 -26
- package/src/StoreRegistryContext.tsx +70 -0
- package/src/__snapshots__/useClientDocument.test.tsx.snap +112 -78
- package/src/__snapshots__/useQuery.test.tsx.snap +12 -12
- package/src/__tests__/fixture.tsx +29 -133
- package/src/experimental/components/LiveList.tsx +23 -10
- package/src/mod.ts +8 -12
- package/src/useClientDocument.test.tsx +90 -79
- package/src/useClientDocument.ts +19 -38
- package/src/useQuery.test.tsx +99 -13
- package/src/useQuery.ts +74 -89
- package/src/useRcResource.test.tsx +115 -59
- package/src/useRcResource.ts +51 -37
- package/src/useStore.test.tsx +347 -0
- package/src/useStore.ts +115 -22
- package/src/useSyncStatus.ts +34 -0
- package/dist/LiveStoreContext.d.ts +0 -13
- package/dist/LiveStoreContext.d.ts.map +0 -1
- package/dist/LiveStoreContext.js +0 -3
- package/dist/LiveStoreContext.js.map +0 -1
- package/dist/LiveStoreProvider.d.ts +0 -65
- package/dist/LiveStoreProvider.d.ts.map +0 -1
- package/dist/LiveStoreProvider.js +0 -221
- package/dist/LiveStoreProvider.js.map +0 -1
- package/dist/LiveStoreProvider.test.d.ts +0 -2
- package/dist/LiveStoreProvider.test.d.ts.map +0 -1
- package/dist/LiveStoreProvider.test.js +0 -117
- package/dist/LiveStoreProvider.test.js.map +0 -1
- package/dist/utils/stack-info.d.ts +0 -4
- package/dist/utils/stack-info.d.ts.map +0 -1
- package/dist/utils/stack-info.js +0 -10
- package/dist/utils/stack-info.js.map +0 -1
- package/src/LiveStoreContext.ts +0 -14
- package/src/LiveStoreProvider.test.tsx +0 -248
- package/src/LiveStoreProvider.tsx +0 -413
- package/src/ambient.d.ts +0 -1
- package/src/utils/stack-info.ts +0 -13
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
/** biome-ignore-all lint/a11y: test files need a11y disabled */
|
|
2
|
-
import { makeInMemoryAdapter } from '@livestore/adapter-web'
|
|
3
|
-
import { queryDb, type Store } from '@livestore/livestore'
|
|
4
|
-
import { Schema } from '@livestore/utils/effect'
|
|
5
|
-
import * as ReactTesting from '@testing-library/react'
|
|
6
|
-
import React from 'react'
|
|
7
|
-
import { unstable_batchedUpdates as batchUpdates } from 'react-dom'
|
|
8
|
-
import { describe, expect, it } from 'vitest'
|
|
9
|
-
|
|
10
|
-
import { events, schema, tables } from './__tests__/fixture.js'
|
|
11
|
-
import { LiveStoreProvider } from './LiveStoreProvider.js'
|
|
12
|
-
import * as LiveStoreReact from './mod.js'
|
|
13
|
-
|
|
14
|
-
describe.each([true, false])('LiveStoreProvider (strictMode: %s)', (strictMode) => {
|
|
15
|
-
const WithStrictMode = strictMode ? React.StrictMode : React.Fragment
|
|
16
|
-
|
|
17
|
-
it('simple', async () => {
|
|
18
|
-
let appRenderCount = 0
|
|
19
|
-
|
|
20
|
-
const allTodos$ = queryDb({ query: `select * from todos`, schema: Schema.Array(tables.todos.rowSchema) })
|
|
21
|
-
|
|
22
|
-
const App = () => {
|
|
23
|
-
appRenderCount++
|
|
24
|
-
const { store } = LiveStoreReact.useStore()
|
|
25
|
-
|
|
26
|
-
const todos = store.useQuery(allTodos$)
|
|
27
|
-
|
|
28
|
-
return <div>{JSON.stringify(todos)}</div>
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const abortController = new AbortController()
|
|
32
|
-
|
|
33
|
-
const Root = ({ forceUpdate }: { forceUpdate: number }) => {
|
|
34
|
-
const bootCb = React.useCallback(
|
|
35
|
-
(store: Store) => store.commit(events.todoCreated({ id: 't1', text: 'buy milk', completed: false })),
|
|
36
|
-
[],
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: forceUpdate is used to force a re-render
|
|
40
|
-
const adapterMemo = React.useMemo(() => makeInMemoryAdapter(), [forceUpdate])
|
|
41
|
-
return (
|
|
42
|
-
<WithStrictMode>
|
|
43
|
-
<LiveStoreProvider
|
|
44
|
-
schema={schema}
|
|
45
|
-
renderLoading={(status) => <div>Loading LiveStore: {status.stage}</div>}
|
|
46
|
-
adapter={adapterMemo}
|
|
47
|
-
boot={bootCb}
|
|
48
|
-
signal={abortController.signal}
|
|
49
|
-
batchUpdates={batchUpdates}
|
|
50
|
-
>
|
|
51
|
-
<App />
|
|
52
|
-
</LiveStoreProvider>
|
|
53
|
-
</WithStrictMode>
|
|
54
|
-
)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const { rerender } = ReactTesting.render(<Root forceUpdate={1} />)
|
|
58
|
-
|
|
59
|
-
expect(appRenderCount).toBe(0)
|
|
60
|
-
|
|
61
|
-
await ReactTesting.waitForElementToBeRemoved(() =>
|
|
62
|
-
ReactTesting.screen.getByText((_) => _.startsWith('Loading LiveStore')),
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
expect(appRenderCount).toBe(strictMode ? 2 : 1)
|
|
66
|
-
|
|
67
|
-
rerender(<Root forceUpdate={2} />)
|
|
68
|
-
|
|
69
|
-
await ReactTesting.waitFor(() => ReactTesting.screen.getByText('Loading LiveStore: loading'))
|
|
70
|
-
await ReactTesting.waitFor(() => ReactTesting.screen.getByText((_) => _.includes('buy milk')))
|
|
71
|
-
|
|
72
|
-
expect(appRenderCount).toBe(strictMode ? 4 : 2)
|
|
73
|
-
|
|
74
|
-
abortController.abort()
|
|
75
|
-
|
|
76
|
-
await ReactTesting.waitFor(() =>
|
|
77
|
-
ReactTesting.screen.getByText('LiveStore Shutdown due to interrupted', { exact: false }),
|
|
78
|
-
)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
// TODO test aborting during boot
|
|
82
|
-
|
|
83
|
-
it('error during boot', async () => {
|
|
84
|
-
let appRenderCount = 0
|
|
85
|
-
|
|
86
|
-
const App = () => {
|
|
87
|
-
appRenderCount++
|
|
88
|
-
|
|
89
|
-
return <div>hello world</div>
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const Root = ({ forceUpdate }: { forceUpdate: number }) => {
|
|
93
|
-
const bootCb = React.useCallback((_store: Store) => {
|
|
94
|
-
// This should trigger an error because we're trying to insert invalid data
|
|
95
|
-
throw new Error('Simulated boot error')
|
|
96
|
-
}, [])
|
|
97
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: forceUpdate is used to force a re-render
|
|
98
|
-
const adapterMemo = React.useMemo(() => makeInMemoryAdapter(), [forceUpdate])
|
|
99
|
-
return (
|
|
100
|
-
<WithStrictMode>
|
|
101
|
-
<LiveStoreProvider
|
|
102
|
-
schema={schema}
|
|
103
|
-
renderLoading={(status) => <div>Loading LiveStore: {status.stage}</div>}
|
|
104
|
-
adapter={adapterMemo}
|
|
105
|
-
boot={bootCb}
|
|
106
|
-
batchUpdates={batchUpdates}
|
|
107
|
-
>
|
|
108
|
-
<App />
|
|
109
|
-
</LiveStoreProvider>
|
|
110
|
-
</WithStrictMode>
|
|
111
|
-
)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
ReactTesting.render(<Root forceUpdate={1} />)
|
|
115
|
-
|
|
116
|
-
expect(appRenderCount).toBe(0)
|
|
117
|
-
|
|
118
|
-
await ReactTesting.waitFor(() => ReactTesting.screen.getByText((_) => _.startsWith('LiveStore.UnexpectedError')))
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
it('unmounts when store is shutdown', async () => {
|
|
122
|
-
let appRenderCount = 0
|
|
123
|
-
|
|
124
|
-
const allTodos$ = queryDb({ query: `select * from todos`, schema: Schema.Array(tables.todos.rowSchema) })
|
|
125
|
-
|
|
126
|
-
const shutdownDeferred = Promise.withResolvers<void>()
|
|
127
|
-
|
|
128
|
-
const App = () => {
|
|
129
|
-
appRenderCount++
|
|
130
|
-
const { store } = LiveStoreReact.useStore()
|
|
131
|
-
|
|
132
|
-
React.useEffect(() => {
|
|
133
|
-
shutdownDeferred.promise.then(() => {
|
|
134
|
-
console.log('shutdown')
|
|
135
|
-
return store.shutdown()
|
|
136
|
-
})
|
|
137
|
-
}, [store])
|
|
138
|
-
|
|
139
|
-
const todos = store.useQuery(allTodos$)
|
|
140
|
-
|
|
141
|
-
return <div>{JSON.stringify(todos)}</div>
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const adapter = makeInMemoryAdapter()
|
|
145
|
-
|
|
146
|
-
const Root = () => {
|
|
147
|
-
return (
|
|
148
|
-
<WithStrictMode>
|
|
149
|
-
<LiveStoreProvider
|
|
150
|
-
schema={schema}
|
|
151
|
-
renderLoading={(status) => <div>Loading LiveStore: {status.stage}</div>}
|
|
152
|
-
adapter={adapter}
|
|
153
|
-
batchUpdates={batchUpdates}
|
|
154
|
-
>
|
|
155
|
-
<App />
|
|
156
|
-
</LiveStoreProvider>
|
|
157
|
-
</WithStrictMode>
|
|
158
|
-
)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
ReactTesting.render(<Root />)
|
|
162
|
-
|
|
163
|
-
expect(appRenderCount).toBe(0)
|
|
164
|
-
|
|
165
|
-
await ReactTesting.waitFor(() => ReactTesting.screen.getByText('[]'))
|
|
166
|
-
|
|
167
|
-
React.act(() => shutdownDeferred.resolve())
|
|
168
|
-
|
|
169
|
-
expect(appRenderCount).toBe(strictMode ? 2 : 1)
|
|
170
|
-
|
|
171
|
-
await ReactTesting.waitFor(() =>
|
|
172
|
-
ReactTesting.screen.getByText('LiveStore Shutdown due to manual shutdown', { exact: false }),
|
|
173
|
-
)
|
|
174
|
-
})
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
it('should work two stores with the same storeId', async () => {
|
|
178
|
-
const allTodos$ = queryDb({ query: `select * from todos`, schema: Schema.Array(tables.todos.rowSchema) })
|
|
179
|
-
|
|
180
|
-
const appRenderCount = {
|
|
181
|
-
store1: 0,
|
|
182
|
-
store2: 0,
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const App = () => {
|
|
186
|
-
const { store } = LiveStoreReact.useStore()
|
|
187
|
-
const instanceId = store.clientSession.debugInstanceId as 'store1' | 'store2'
|
|
188
|
-
appRenderCount[instanceId]!++
|
|
189
|
-
|
|
190
|
-
const todos = store.useQuery(allTodos$)
|
|
191
|
-
|
|
192
|
-
return (
|
|
193
|
-
<div id={instanceId}>
|
|
194
|
-
<div role="heading">{instanceId}</div>
|
|
195
|
-
<div role="content">{JSON.stringify(todos)}</div>
|
|
196
|
-
<button onClick={() => store.commit(events.todoCreated({ id: 't1', text: 'buy milk', completed: false }))}>
|
|
197
|
-
create todo {instanceId}
|
|
198
|
-
</button>
|
|
199
|
-
</div>
|
|
200
|
-
)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const Root = () => {
|
|
204
|
-
const storeId = 'fixed-store-id'
|
|
205
|
-
return (
|
|
206
|
-
<div>
|
|
207
|
-
<LiveStoreProvider
|
|
208
|
-
storeId={storeId}
|
|
209
|
-
debug={{ instanceId: 'store1' }}
|
|
210
|
-
schema={schema}
|
|
211
|
-
adapter={makeInMemoryAdapter()}
|
|
212
|
-
batchUpdates={batchUpdates}
|
|
213
|
-
>
|
|
214
|
-
<App />
|
|
215
|
-
</LiveStoreProvider>
|
|
216
|
-
<LiveStoreProvider
|
|
217
|
-
storeId={storeId}
|
|
218
|
-
debug={{ instanceId: 'store2' }}
|
|
219
|
-
schema={schema}
|
|
220
|
-
adapter={makeInMemoryAdapter()}
|
|
221
|
-
batchUpdates={batchUpdates}
|
|
222
|
-
>
|
|
223
|
-
<App />
|
|
224
|
-
</LiveStoreProvider>
|
|
225
|
-
</div>
|
|
226
|
-
)
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const { container } = ReactTesting.render(<Root />)
|
|
230
|
-
|
|
231
|
-
await ReactTesting.waitFor(() => ReactTesting.screen.getByRole('heading', { name: 'store1' }))
|
|
232
|
-
await ReactTesting.waitFor(() => ReactTesting.screen.getByRole('heading', { name: 'store2' }))
|
|
233
|
-
|
|
234
|
-
expect(appRenderCount.store1).toBe(1)
|
|
235
|
-
expect(appRenderCount.store2).toBe(1)
|
|
236
|
-
|
|
237
|
-
ReactTesting.fireEvent.click(ReactTesting.screen.getByText('create todo store1'))
|
|
238
|
-
|
|
239
|
-
expect(appRenderCount.store1).toBe(2)
|
|
240
|
-
|
|
241
|
-
expect(container.querySelector('#store1 > div[role="content"]')?.textContent).toBe(
|
|
242
|
-
'[{"id":"t1","text":"buy milk","completed":false}]',
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
expect(container.querySelector('#store2 > div[role="content"]')?.textContent).toBe('[]')
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
// TODO test that checks that there are no two exact same instances (i.e. same storeId, clientId, sessionId)
|
|
@@ -1,413 +0,0 @@
|
|
|
1
|
-
import type { Adapter, BootStatus, IntentionalShutdownCause, MigrationsReport, SyncError } from '@livestore/common'
|
|
2
|
-
import { provideOtel, UnexpectedError } from '@livestore/common'
|
|
3
|
-
import type { LiveStoreSchema } from '@livestore/common/schema'
|
|
4
|
-
import type {
|
|
5
|
-
CreateStoreOptions,
|
|
6
|
-
OtelOptions,
|
|
7
|
-
ShutdownDeferred,
|
|
8
|
-
Store,
|
|
9
|
-
LiveStoreContext as StoreContext_,
|
|
10
|
-
} from '@livestore/livestore'
|
|
11
|
-
import { createStore, makeShutdownDeferred, StoreInterrupted } from '@livestore/livestore'
|
|
12
|
-
import { errorToString, IS_REACT_NATIVE, LS_DEV, omitUndefineds } from '@livestore/utils'
|
|
13
|
-
import type { OtelTracer } from '@livestore/utils/effect'
|
|
14
|
-
import {
|
|
15
|
-
Cause,
|
|
16
|
-
Deferred,
|
|
17
|
-
Effect,
|
|
18
|
-
Exit,
|
|
19
|
-
identity,
|
|
20
|
-
Logger,
|
|
21
|
-
LogLevel,
|
|
22
|
-
Schema,
|
|
23
|
-
Scope,
|
|
24
|
-
TaskTracing,
|
|
25
|
-
} from '@livestore/utils/effect'
|
|
26
|
-
import type * as otel from '@opentelemetry/api'
|
|
27
|
-
import React from 'react'
|
|
28
|
-
|
|
29
|
-
import { LiveStoreContext } from './LiveStoreContext.ts'
|
|
30
|
-
|
|
31
|
-
export interface LiveStoreProviderProps {
|
|
32
|
-
schema: LiveStoreSchema
|
|
33
|
-
/**
|
|
34
|
-
* The `storeId` can be used to isolate multiple stores from each other.
|
|
35
|
-
* So it can be useful for multi-tenancy scenarios.
|
|
36
|
-
*
|
|
37
|
-
* The `storeId` is also used for persistence.
|
|
38
|
-
*
|
|
39
|
-
* Make sure to also configure `storeId` in LiveStore Devtools (e.g. in Vite plugin).
|
|
40
|
-
*
|
|
41
|
-
* @default 'default'
|
|
42
|
-
*/
|
|
43
|
-
storeId?: string
|
|
44
|
-
boot?: (
|
|
45
|
-
store: Store<LiveStoreSchema>,
|
|
46
|
-
ctx: { migrationsReport: MigrationsReport; parentSpan: otel.Span },
|
|
47
|
-
) => void | Promise<void> | Effect.Effect<void, unknown, OtelTracer.OtelTracer>
|
|
48
|
-
otelOptions?: Partial<OtelOptions>
|
|
49
|
-
renderLoading?: (status: BootStatus) => React.ReactNode
|
|
50
|
-
renderError?: (error: UnexpectedError | unknown) => React.ReactNode
|
|
51
|
-
renderShutdown?: (cause: IntentionalShutdownCause | StoreInterrupted | SyncError) => React.ReactNode
|
|
52
|
-
adapter: Adapter
|
|
53
|
-
/**
|
|
54
|
-
* In order for LiveStore to apply multiple events in a single render,
|
|
55
|
-
* you need to pass the `batchUpdates` function from either `react-dom` or `react-native`.
|
|
56
|
-
*
|
|
57
|
-
* ```ts
|
|
58
|
-
* // With React DOM
|
|
59
|
-
* import { unstable_batchedUpdates as batchUpdates } from 'react-dom'
|
|
60
|
-
*
|
|
61
|
-
* // With React Native
|
|
62
|
-
* import { unstable_batchedUpdates as batchUpdates } from 'react-native'
|
|
63
|
-
* ```
|
|
64
|
-
*/
|
|
65
|
-
batchUpdates: (run: () => void) => void
|
|
66
|
-
disableDevtools?: boolean
|
|
67
|
-
signal?: AbortSignal
|
|
68
|
-
/**
|
|
69
|
-
* Currently only used in the web adapter:
|
|
70
|
-
* If true, registers a beforeunload event listener to confirm unsaved changes.
|
|
71
|
-
*
|
|
72
|
-
* @default true
|
|
73
|
-
*/
|
|
74
|
-
confirmUnsavedChanges?: boolean
|
|
75
|
-
/**
|
|
76
|
-
* Payload that will be passed to the sync backend when connecting
|
|
77
|
-
*
|
|
78
|
-
* @default undefined
|
|
79
|
-
*/
|
|
80
|
-
syncPayload?: Schema.JsonValue
|
|
81
|
-
debug?: {
|
|
82
|
-
instanceId?: string
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const defaultRenderError = (error: UnexpectedError | unknown) =>
|
|
87
|
-
IS_REACT_NATIVE ? null : Schema.is(UnexpectedError)(error) ? error.toString() : errorToString(error)
|
|
88
|
-
|
|
89
|
-
const defaultRenderShutdown = (cause: IntentionalShutdownCause | StoreInterrupted | SyncError) => {
|
|
90
|
-
const reason =
|
|
91
|
-
cause._tag === 'LiveStore.StoreInterrupted'
|
|
92
|
-
? `interrupted due to: ${cause.reason}`
|
|
93
|
-
: cause._tag === 'InvalidPushError' || cause._tag === 'InvalidPullError'
|
|
94
|
-
? `sync error: ${cause.cause}`
|
|
95
|
-
: cause.reason === 'devtools-import'
|
|
96
|
-
? 'devtools import'
|
|
97
|
-
: cause.reason === 'devtools-reset'
|
|
98
|
-
? 'devtools reset'
|
|
99
|
-
: cause.reason === 'adapter-reset'
|
|
100
|
-
? 'adapter reset'
|
|
101
|
-
: cause.reason === 'manual'
|
|
102
|
-
? 'manual shutdown'
|
|
103
|
-
: 'unknown reason'
|
|
104
|
-
|
|
105
|
-
return IS_REACT_NATIVE ? null : <>LiveStore Shutdown due to {reason}</>
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const defaultRenderLoading = (status: BootStatus) =>
|
|
109
|
-
IS_REACT_NATIVE ? null : <>LiveStore is loading ({status.stage})...</>
|
|
110
|
-
|
|
111
|
-
export const LiveStoreProvider = ({
|
|
112
|
-
renderLoading = defaultRenderLoading,
|
|
113
|
-
renderError = defaultRenderError,
|
|
114
|
-
renderShutdown = defaultRenderShutdown,
|
|
115
|
-
otelOptions,
|
|
116
|
-
children,
|
|
117
|
-
schema,
|
|
118
|
-
storeId = 'default',
|
|
119
|
-
boot,
|
|
120
|
-
adapter,
|
|
121
|
-
batchUpdates,
|
|
122
|
-
disableDevtools,
|
|
123
|
-
signal,
|
|
124
|
-
confirmUnsavedChanges = true,
|
|
125
|
-
syncPayload,
|
|
126
|
-
debug,
|
|
127
|
-
}: LiveStoreProviderProps & React.PropsWithChildren): React.ReactNode => {
|
|
128
|
-
const storeCtx = useCreateStore({
|
|
129
|
-
storeId,
|
|
130
|
-
schema,
|
|
131
|
-
adapter,
|
|
132
|
-
batchUpdates,
|
|
133
|
-
confirmUnsavedChanges,
|
|
134
|
-
...omitUndefineds({
|
|
135
|
-
otelOptions,
|
|
136
|
-
boot,
|
|
137
|
-
disableDevtools,
|
|
138
|
-
signal,
|
|
139
|
-
syncPayload,
|
|
140
|
-
debug,
|
|
141
|
-
}),
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
if (storeCtx.stage === 'error') {
|
|
145
|
-
return renderError(storeCtx.error)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (storeCtx.stage === 'shutdown') {
|
|
149
|
-
return renderShutdown(storeCtx.cause)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (storeCtx.stage !== 'running') {
|
|
153
|
-
return renderLoading(storeCtx)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
globalThis.__debugLiveStore ??= {}
|
|
157
|
-
if (Object.keys(globalThis.__debugLiveStore).length === 0) {
|
|
158
|
-
globalThis.__debugLiveStore._ = storeCtx.store
|
|
159
|
-
}
|
|
160
|
-
globalThis.__debugLiveStore[debug?.instanceId ?? storeId] = storeCtx.store
|
|
161
|
-
|
|
162
|
-
return <LiveStoreContext.Provider value={storeCtx as TODO}>{children}</LiveStoreContext.Provider>
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const useCreateStore = ({
|
|
166
|
-
schema,
|
|
167
|
-
storeId,
|
|
168
|
-
otelOptions,
|
|
169
|
-
boot,
|
|
170
|
-
adapter,
|
|
171
|
-
batchUpdates,
|
|
172
|
-
disableDevtools,
|
|
173
|
-
signal,
|
|
174
|
-
context,
|
|
175
|
-
params,
|
|
176
|
-
confirmUnsavedChanges,
|
|
177
|
-
syncPayload,
|
|
178
|
-
debug,
|
|
179
|
-
}: CreateStoreOptions<LiveStoreSchema> & {
|
|
180
|
-
signal?: AbortSignal
|
|
181
|
-
otelOptions?: Partial<OtelOptions>
|
|
182
|
-
}) => {
|
|
183
|
-
const [_, rerender] = React.useState(0)
|
|
184
|
-
const ctxValueRef = React.useRef<{
|
|
185
|
-
value: StoreContext_ | BootStatus
|
|
186
|
-
componentScope: Scope.CloseableScope | undefined
|
|
187
|
-
shutdownDeferred: ShutdownDeferred | undefined
|
|
188
|
-
/** Used to wait for the previous shutdown deferred to fully complete before creating a new one */
|
|
189
|
-
previousShutdownDeferred: ShutdownDeferred | undefined
|
|
190
|
-
counter: number
|
|
191
|
-
}>({
|
|
192
|
-
value: { stage: 'loading' },
|
|
193
|
-
componentScope: undefined,
|
|
194
|
-
shutdownDeferred: undefined,
|
|
195
|
-
previousShutdownDeferred: undefined,
|
|
196
|
-
counter: 0,
|
|
197
|
-
})
|
|
198
|
-
const debugInstanceId = debug?.instanceId
|
|
199
|
-
|
|
200
|
-
// console.debug(`useCreateStore (${ctxValueRef.current.counter})`, ctxValueRef.current.value.stage)
|
|
201
|
-
|
|
202
|
-
const inputPropsCacheRef = React.useRef({
|
|
203
|
-
schema,
|
|
204
|
-
otelOptions,
|
|
205
|
-
boot,
|
|
206
|
-
adapter,
|
|
207
|
-
batchUpdates,
|
|
208
|
-
disableDevtools,
|
|
209
|
-
signal,
|
|
210
|
-
context,
|
|
211
|
-
params,
|
|
212
|
-
confirmUnsavedChanges,
|
|
213
|
-
syncPayload,
|
|
214
|
-
debugInstanceId,
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
const interrupt = React.useCallback(
|
|
218
|
-
(componentScope: Scope.CloseableScope, shutdownDeferred: ShutdownDeferred, error: StoreInterrupted) =>
|
|
219
|
-
Effect.gen(function* () {
|
|
220
|
-
// console.log('[@livestore/livestore/react] interupting', error)
|
|
221
|
-
yield* Scope.close(componentScope, Exit.fail(error))
|
|
222
|
-
yield* Deferred.fail(shutdownDeferred, error)
|
|
223
|
-
}).pipe(
|
|
224
|
-
Effect.tapErrorCause((cause) => Effect.logDebug('[@livestore/livestore/react] interupting', cause)),
|
|
225
|
-
Effect.runFork,
|
|
226
|
-
),
|
|
227
|
-
[],
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
const inputPropChanges = {
|
|
231
|
-
schema: inputPropsCacheRef.current.schema !== schema,
|
|
232
|
-
otelOptions: inputPropsCacheRef.current.otelOptions !== otelOptions,
|
|
233
|
-
boot: inputPropsCacheRef.current.boot !== boot,
|
|
234
|
-
adapter: inputPropsCacheRef.current.adapter !== adapter,
|
|
235
|
-
batchUpdates: inputPropsCacheRef.current.batchUpdates !== batchUpdates,
|
|
236
|
-
disableDevtools: inputPropsCacheRef.current.disableDevtools !== disableDevtools,
|
|
237
|
-
signal: inputPropsCacheRef.current.signal !== signal,
|
|
238
|
-
context: inputPropsCacheRef.current.context !== context,
|
|
239
|
-
params: inputPropsCacheRef.current.params !== params,
|
|
240
|
-
confirmUnsavedChanges: inputPropsCacheRef.current.confirmUnsavedChanges !== confirmUnsavedChanges,
|
|
241
|
-
syncPayload: inputPropsCacheRef.current.syncPayload !== syncPayload,
|
|
242
|
-
debugInstanceId: inputPropsCacheRef.current.debugInstanceId !== debugInstanceId,
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (
|
|
246
|
-
inputPropChanges.schema ||
|
|
247
|
-
inputPropChanges.otelOptions ||
|
|
248
|
-
inputPropChanges.boot ||
|
|
249
|
-
inputPropChanges.adapter ||
|
|
250
|
-
inputPropChanges.batchUpdates ||
|
|
251
|
-
inputPropChanges.disableDevtools ||
|
|
252
|
-
inputPropChanges.signal ||
|
|
253
|
-
inputPropChanges.context ||
|
|
254
|
-
inputPropChanges.params ||
|
|
255
|
-
inputPropChanges.confirmUnsavedChanges ||
|
|
256
|
-
inputPropChanges.syncPayload
|
|
257
|
-
) {
|
|
258
|
-
inputPropsCacheRef.current = {
|
|
259
|
-
schema,
|
|
260
|
-
otelOptions,
|
|
261
|
-
boot,
|
|
262
|
-
adapter,
|
|
263
|
-
batchUpdates,
|
|
264
|
-
disableDevtools,
|
|
265
|
-
signal,
|
|
266
|
-
context,
|
|
267
|
-
params,
|
|
268
|
-
confirmUnsavedChanges,
|
|
269
|
-
syncPayload,
|
|
270
|
-
debugInstanceId,
|
|
271
|
-
}
|
|
272
|
-
if (ctxValueRef.current.componentScope !== undefined && ctxValueRef.current.shutdownDeferred !== undefined) {
|
|
273
|
-
const changedInputProps = Object.keys(inputPropChanges).filter(
|
|
274
|
-
(key) => inputPropChanges[key as keyof typeof inputPropChanges],
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
interrupt(
|
|
278
|
-
ctxValueRef.current.componentScope,
|
|
279
|
-
ctxValueRef.current.shutdownDeferred,
|
|
280
|
-
new StoreInterrupted({ reason: `re-rendering due to changed input props: ${changedInputProps.join(', ')}` }),
|
|
281
|
-
)
|
|
282
|
-
ctxValueRef.current.componentScope = undefined
|
|
283
|
-
ctxValueRef.current.shutdownDeferred = undefined
|
|
284
|
-
}
|
|
285
|
-
ctxValueRef.current = {
|
|
286
|
-
value: { stage: 'loading' },
|
|
287
|
-
componentScope: undefined,
|
|
288
|
-
shutdownDeferred: undefined,
|
|
289
|
-
previousShutdownDeferred: ctxValueRef.current.shutdownDeferred,
|
|
290
|
-
counter: ctxValueRef.current.counter + 1,
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
React.useEffect(() => {
|
|
295
|
-
const counter = ctxValueRef.current.counter
|
|
296
|
-
|
|
297
|
-
const setContextValue = (value: StoreContext_ | BootStatus) => {
|
|
298
|
-
if (ctxValueRef.current.counter !== counter) return
|
|
299
|
-
ctxValueRef.current.value = value
|
|
300
|
-
rerender((c) => c + 1)
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
signal?.addEventListener('abort', () => {
|
|
304
|
-
if (
|
|
305
|
-
ctxValueRef.current.componentScope !== undefined &&
|
|
306
|
-
ctxValueRef.current.shutdownDeferred !== undefined &&
|
|
307
|
-
ctxValueRef.current.counter === counter
|
|
308
|
-
) {
|
|
309
|
-
interrupt(
|
|
310
|
-
ctxValueRef.current.componentScope,
|
|
311
|
-
ctxValueRef.current.shutdownDeferred,
|
|
312
|
-
new StoreInterrupted({ reason: 'Aborted via provided AbortController' }),
|
|
313
|
-
)
|
|
314
|
-
ctxValueRef.current.componentScope = undefined
|
|
315
|
-
ctxValueRef.current.shutdownDeferred = undefined
|
|
316
|
-
}
|
|
317
|
-
})
|
|
318
|
-
|
|
319
|
-
const cancel = Effect.gen(function* () {
|
|
320
|
-
// Wait for the previous store to fully shutdown before creating a new one
|
|
321
|
-
if (ctxValueRef.current.previousShutdownDeferred) {
|
|
322
|
-
yield* Deferred.await(ctxValueRef.current.previousShutdownDeferred)
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const componentScope = yield* Scope.make().pipe(Effect.acquireRelease(Scope.close))
|
|
326
|
-
const shutdownDeferred = yield* makeShutdownDeferred
|
|
327
|
-
|
|
328
|
-
ctxValueRef.current.componentScope = componentScope
|
|
329
|
-
ctxValueRef.current.shutdownDeferred = shutdownDeferred
|
|
330
|
-
|
|
331
|
-
yield* Effect.gen(function* () {
|
|
332
|
-
const store = yield* createStore({
|
|
333
|
-
schema,
|
|
334
|
-
storeId,
|
|
335
|
-
adapter,
|
|
336
|
-
shutdownDeferred,
|
|
337
|
-
...omitUndefineds({
|
|
338
|
-
boot,
|
|
339
|
-
batchUpdates,
|
|
340
|
-
disableDevtools,
|
|
341
|
-
context,
|
|
342
|
-
params,
|
|
343
|
-
confirmUnsavedChanges,
|
|
344
|
-
syncPayload,
|
|
345
|
-
}),
|
|
346
|
-
onBootStatus: (status) => {
|
|
347
|
-
if (ctxValueRef.current.value.stage === 'running' || ctxValueRef.current.value.stage === 'error') return
|
|
348
|
-
// NOTE sometimes when status come in in rapid succession, only the last value will be rendered by React
|
|
349
|
-
setContextValue(status)
|
|
350
|
-
},
|
|
351
|
-
debug: { ...omitUndefineds({ instanceId: debugInstanceId }) },
|
|
352
|
-
}).pipe(Effect.tapErrorCause((cause) => Deferred.failCause(shutdownDeferred, cause)))
|
|
353
|
-
|
|
354
|
-
setContextValue({ stage: 'running', store })
|
|
355
|
-
}).pipe(Scope.extend(componentScope), Effect.forkIn(componentScope))
|
|
356
|
-
|
|
357
|
-
const shutdownContext = (cause: IntentionalShutdownCause | StoreInterrupted | SyncError) =>
|
|
358
|
-
Effect.sync(() => setContextValue({ stage: 'shutdown', cause }))
|
|
359
|
-
|
|
360
|
-
yield* Deferred.await(shutdownDeferred).pipe(
|
|
361
|
-
Effect.tapErrorCause((cause) => Effect.logDebug('[@livestore/livestore/react] shutdown', Cause.pretty(cause))),
|
|
362
|
-
Effect.tap((intentionalShutdown) => shutdownContext(intentionalShutdown)),
|
|
363
|
-
Effect.catchTag('InvalidPushError', (cause) => shutdownContext(cause)),
|
|
364
|
-
Effect.catchTag('InvalidPullError', (cause) => shutdownContext(cause)),
|
|
365
|
-
Effect.catchTag('LiveStore.StoreInterrupted', (cause) => shutdownContext(cause)),
|
|
366
|
-
Effect.tapError((error) => Effect.sync(() => setContextValue({ stage: 'error', error }))),
|
|
367
|
-
Effect.tapDefect((defect) => Effect.sync(() => setContextValue({ stage: 'error', error: defect }))),
|
|
368
|
-
Effect.exit,
|
|
369
|
-
)
|
|
370
|
-
}).pipe(
|
|
371
|
-
Effect.scoped,
|
|
372
|
-
Effect.withSpan('@livestore/react:useCreateStore'),
|
|
373
|
-
LS_DEV ? TaskTracing.withAsyncTaggingTracing((name: string) => (console as any).createTask(name)) : identity,
|
|
374
|
-
provideOtel(omitUndefineds({ parentSpanContext: otelOptions?.rootSpanContext, otelTracer: otelOptions?.tracer })),
|
|
375
|
-
Effect.tapCauseLogPretty,
|
|
376
|
-
Effect.annotateLogs({ thread: 'window' }),
|
|
377
|
-
Effect.provide(Logger.prettyWithThread('window')),
|
|
378
|
-
Logger.withMinimumLogLevel(LogLevel.Debug),
|
|
379
|
-
Effect.runCallback,
|
|
380
|
-
)
|
|
381
|
-
|
|
382
|
-
return () => {
|
|
383
|
-
cancel()
|
|
384
|
-
|
|
385
|
-
if (ctxValueRef.current.componentScope !== undefined && ctxValueRef.current.shutdownDeferred !== undefined) {
|
|
386
|
-
interrupt(
|
|
387
|
-
ctxValueRef.current.componentScope,
|
|
388
|
-
ctxValueRef.current.shutdownDeferred,
|
|
389
|
-
new StoreInterrupted({ reason: 'unmounting component' }),
|
|
390
|
-
)
|
|
391
|
-
ctxValueRef.current.componentScope = undefined
|
|
392
|
-
ctxValueRef.current.shutdownDeferred = undefined
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}, [
|
|
396
|
-
schema,
|
|
397
|
-
otelOptions,
|
|
398
|
-
boot,
|
|
399
|
-
adapter,
|
|
400
|
-
batchUpdates,
|
|
401
|
-
disableDevtools,
|
|
402
|
-
signal,
|
|
403
|
-
storeId,
|
|
404
|
-
context,
|
|
405
|
-
params,
|
|
406
|
-
confirmUnsavedChanges,
|
|
407
|
-
syncPayload,
|
|
408
|
-
debugInstanceId,
|
|
409
|
-
interrupt,
|
|
410
|
-
])
|
|
411
|
-
|
|
412
|
-
return ctxValueRef.current.value
|
|
413
|
-
}
|
package/src/ambient.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
var __debugLiveStore: any
|
package/src/utils/stack-info.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { extractStackInfoFromStackTrace, type StackInfo } from '@livestore/livestore'
|
|
2
|
-
import React from 'react'
|
|
3
|
-
|
|
4
|
-
export const originalStackLimit = Error.stackTraceLimit
|
|
5
|
-
|
|
6
|
-
export const useStackInfo = (): StackInfo =>
|
|
7
|
-
React.useMemo(() => {
|
|
8
|
-
Error.stackTraceLimit = 10
|
|
9
|
-
|
|
10
|
-
const stack = new Error().stack!
|
|
11
|
-
Error.stackTraceLimit = originalStackLimit
|
|
12
|
-
return extractStackInfoFromStackTrace(stack)
|
|
13
|
-
}, [])
|