@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.
Files changed (85) hide show
  1. package/README.md +1 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/StoreRegistryContext.d.ts +56 -0
  4. package/dist/StoreRegistryContext.d.ts.map +1 -0
  5. package/dist/StoreRegistryContext.js +61 -0
  6. package/dist/StoreRegistryContext.js.map +1 -0
  7. package/dist/__tests__/fixture.d.ts +12 -283
  8. package/dist/__tests__/fixture.d.ts.map +1 -1
  9. package/dist/__tests__/fixture.js +12 -86
  10. package/dist/__tests__/fixture.js.map +1 -1
  11. package/dist/experimental/components/LiveList.d.ts +4 -2
  12. package/dist/experimental/components/LiveList.d.ts.map +1 -1
  13. package/dist/experimental/components/LiveList.js +10 -8
  14. package/dist/experimental/components/LiveList.js.map +1 -1
  15. package/dist/mod.d.ts +8 -5
  16. package/dist/mod.d.ts.map +1 -1
  17. package/dist/mod.js +6 -4
  18. package/dist/mod.js.map +1 -1
  19. package/dist/useClientDocument.d.ts +12 -4
  20. package/dist/useClientDocument.d.ts.map +1 -1
  21. package/dist/useClientDocument.js +4 -18
  22. package/dist/useClientDocument.js.map +1 -1
  23. package/dist/useClientDocument.test.js +21 -9
  24. package/dist/useClientDocument.test.js.map +1 -1
  25. package/dist/useQuery.d.ts +29 -8
  26. package/dist/useQuery.d.ts.map +1 -1
  27. package/dist/useQuery.js +43 -70
  28. package/dist/useQuery.js.map +1 -1
  29. package/dist/useQuery.test.js +57 -13
  30. package/dist/useQuery.test.js.map +1 -1
  31. package/dist/useRcResource.d.ts +1 -1
  32. package/dist/useRcResource.d.ts.map +1 -1
  33. package/dist/useRcResource.js +41 -34
  34. package/dist/useRcResource.js.map +1 -1
  35. package/dist/useRcResource.test.js +72 -50
  36. package/dist/useRcResource.test.js.map +1 -1
  37. package/dist/useStore.d.ts +74 -7
  38. package/dist/useStore.d.ts.map +1 -1
  39. package/dist/useStore.js +82 -16
  40. package/dist/useStore.js.map +1 -1
  41. package/dist/useStore.test.d.ts +2 -0
  42. package/dist/useStore.test.d.ts.map +1 -0
  43. package/dist/useStore.test.js +241 -0
  44. package/dist/useStore.test.js.map +1 -0
  45. package/dist/useSyncStatus.d.ts +22 -0
  46. package/dist/useSyncStatus.d.ts.map +1 -0
  47. package/dist/useSyncStatus.js +28 -0
  48. package/dist/useSyncStatus.js.map +1 -0
  49. package/package.json +69 -26
  50. package/src/StoreRegistryContext.tsx +70 -0
  51. package/src/__snapshots__/useClientDocument.test.tsx.snap +112 -78
  52. package/src/__snapshots__/useQuery.test.tsx.snap +12 -12
  53. package/src/__tests__/fixture.tsx +29 -133
  54. package/src/experimental/components/LiveList.tsx +23 -10
  55. package/src/mod.ts +8 -12
  56. package/src/useClientDocument.test.tsx +90 -79
  57. package/src/useClientDocument.ts +19 -38
  58. package/src/useQuery.test.tsx +99 -13
  59. package/src/useQuery.ts +74 -89
  60. package/src/useRcResource.test.tsx +115 -59
  61. package/src/useRcResource.ts +51 -37
  62. package/src/useStore.test.tsx +347 -0
  63. package/src/useStore.ts +115 -22
  64. package/src/useSyncStatus.ts +34 -0
  65. package/dist/LiveStoreContext.d.ts +0 -13
  66. package/dist/LiveStoreContext.d.ts.map +0 -1
  67. package/dist/LiveStoreContext.js +0 -3
  68. package/dist/LiveStoreContext.js.map +0 -1
  69. package/dist/LiveStoreProvider.d.ts +0 -65
  70. package/dist/LiveStoreProvider.d.ts.map +0 -1
  71. package/dist/LiveStoreProvider.js +0 -221
  72. package/dist/LiveStoreProvider.js.map +0 -1
  73. package/dist/LiveStoreProvider.test.d.ts +0 -2
  74. package/dist/LiveStoreProvider.test.d.ts.map +0 -1
  75. package/dist/LiveStoreProvider.test.js +0 -117
  76. package/dist/LiveStoreProvider.test.js.map +0 -1
  77. package/dist/utils/stack-info.d.ts +0 -4
  78. package/dist/utils/stack-info.d.ts.map +0 -1
  79. package/dist/utils/stack-info.js +0 -10
  80. package/dist/utils/stack-info.js.map +0 -1
  81. package/src/LiveStoreContext.ts +0 -14
  82. package/src/LiveStoreProvider.test.tsx +0 -248
  83. package/src/LiveStoreProvider.tsx +0 -413
  84. package/src/ambient.d.ts +0 -1
  85. 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
@@ -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
- }, [])