@livestore/livestore 0.0.58-dev.1 → 0.0.58-dev.11
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 -117
- package/dist/.tsbuildinfo +1 -1
- package/dist/effect/LiveStore.d.ts +3 -3
- package/dist/effect/LiveStore.d.ts.map +1 -1
- package/dist/effect/LiveStore.js +1 -1
- package/dist/effect/LiveStore.js.map +1 -1
- package/dist/global-state.d.ts.map +1 -1
- package/dist/global-state.js +2 -1
- package/dist/global-state.js.map +1 -1
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/reactiveQueries/base-class.d.ts +1 -1
- package/dist/reactiveQueries/base-class.d.ts.map +1 -1
- package/dist/reactiveQueries/base-class.js.map +1 -1
- package/dist/reactiveQueries/sql.d.ts +1 -1
- package/dist/reactiveQueries/sql.d.ts.map +1 -1
- package/dist/reactiveQueries/sql.js +4 -4
- package/dist/reactiveQueries/sql.js.map +1 -1
- package/dist/reactiveQueries/sql.test.js +2 -2
- package/dist/reactiveQueries/sql.test.js.map +1 -1
- package/dist/row-query.d.ts +3 -2
- package/dist/row-query.d.ts.map +1 -1
- package/dist/row-query.js +18 -10
- package/dist/row-query.js.map +1 -1
- package/dist/store-devtools.d.ts +2 -2
- package/dist/store-devtools.d.ts.map +1 -1
- package/dist/store-devtools.js +3 -3
- package/dist/store-devtools.js.map +1 -1
- package/dist/store.d.ts +23 -19
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +90 -59
- package/dist/store.js.map +1 -1
- package/dist/utils/dev.d.ts.map +1 -1
- package/dist/utils/dev.js +1 -0
- package/dist/utils/dev.js.map +1 -1
- package/dist/{react/utils → utils}/stack-info.d.ts +1 -2
- package/dist/utils/stack-info.d.ts.map +1 -0
- package/dist/{react/utils → utils}/stack-info.js +1 -9
- package/dist/utils/stack-info.js.map +1 -0
- package/dist/utils/stack-info.test.d.ts.map +1 -0
- package/dist/{__tests__/react/utils → utils}/stack-info.test.js +1 -1
- package/dist/utils/stack-info.test.js.map +1 -0
- package/dist/utils/tests/fixture.d.ts +259 -0
- package/dist/utils/tests/fixture.d.ts.map +1 -0
- package/dist/utils/tests/fixture.js +33 -0
- package/dist/utils/tests/fixture.js.map +1 -0
- package/dist/utils/tests/mod.d.ts +3 -0
- package/dist/utils/tests/mod.d.ts.map +1 -0
- package/dist/utils/tests/mod.js +3 -0
- package/dist/utils/tests/mod.js.map +1 -0
- package/dist/utils/tests/otel.d.ts.map +1 -0
- package/dist/utils/tests/otel.js.map +1 -0
- package/package.json +17 -24
- package/src/ambient.d.ts +3 -0
- package/src/effect/LiveStore.ts +4 -4
- package/src/global-state.ts +5 -1
- package/src/index.ts +17 -4
- package/src/reactiveQueries/base-class.ts +1 -1
- package/src/reactiveQueries/sql.test.ts +2 -2
- package/src/reactiveQueries/sql.ts +5 -5
- package/src/row-query.ts +36 -16
- package/src/store-devtools.ts +5 -5
- package/src/store.ts +146 -78
- package/src/utils/dev.ts +1 -0
- package/src/{__tests__/react/utils → utils}/stack-info.test.ts +1 -1
- package/src/{react/utils → utils}/stack-info.ts +2 -12
- package/src/utils/tests/fixture.ts +77 -0
- package/src/utils/tests/mod.ts +2 -0
- package/tsconfig.json +1 -2
- package/vitest.config.js +0 -8
- package/dist/__tests__/react/fixture.d.ts +0 -461
- package/dist/__tests__/react/fixture.d.ts.map +0 -1
- package/dist/__tests__/react/fixture.js +0 -68
- package/dist/__tests__/react/fixture.js.map +0 -1
- package/dist/__tests__/react/utils/otel.d.ts.map +0 -1
- package/dist/__tests__/react/utils/otel.js.map +0 -1
- package/dist/__tests__/react/utils/stack-info.test.d.ts.map +0 -1
- package/dist/__tests__/react/utils/stack-info.test.js.map +0 -1
- package/dist/react/LiveStoreContext.d.ts +0 -7
- package/dist/react/LiveStoreContext.d.ts.map +0 -1
- package/dist/react/LiveStoreContext.js +0 -13
- package/dist/react/LiveStoreContext.js.map +0 -1
- package/dist/react/LiveStoreProvider.d.ts +0 -47
- package/dist/react/LiveStoreProvider.d.ts.map +0 -1
- package/dist/react/LiveStoreProvider.js +0 -169
- package/dist/react/LiveStoreProvider.js.map +0 -1
- package/dist/react/LiveStoreProvider.test.d.ts +0 -2
- package/dist/react/LiveStoreProvider.test.d.ts.map +0 -1
- package/dist/react/LiveStoreProvider.test.js +0 -62
- package/dist/react/LiveStoreProvider.test.js.map +0 -1
- package/dist/react/components/LiveList.d.ts +0 -21
- package/dist/react/components/LiveList.d.ts.map +0 -1
- package/dist/react/components/LiveList.js +0 -31
- package/dist/react/components/LiveList.js.map +0 -1
- package/dist/react/index.d.ts +0 -11
- package/dist/react/index.d.ts.map +0 -1
- package/dist/react/index.js +0 -10
- package/dist/react/index.js.map +0 -1
- package/dist/react/useAtom.d.ts +0 -10
- package/dist/react/useAtom.d.ts.map +0 -1
- package/dist/react/useAtom.js +0 -37
- package/dist/react/useAtom.js.map +0 -1
- package/dist/react/useLocalId.d.ts +0 -10
- package/dist/react/useLocalId.d.ts.map +0 -1
- package/dist/react/useLocalId.js +0 -21
- package/dist/react/useLocalId.js.map +0 -1
- package/dist/react/useQuery.d.ts +0 -9
- package/dist/react/useQuery.d.ts.map +0 -1
- package/dist/react/useQuery.js +0 -69
- package/dist/react/useQuery.js.map +0 -1
- package/dist/react/useQuery.test.d.ts +0 -2
- package/dist/react/useQuery.test.d.ts.map +0 -1
- package/dist/react/useQuery.test.js +0 -51
- package/dist/react/useQuery.test.js.map +0 -1
- package/dist/react/useRow.d.ts +0 -46
- package/dist/react/useRow.d.ts.map +0 -1
- package/dist/react/useRow.js +0 -94
- package/dist/react/useRow.js.map +0 -1
- package/dist/react/useRow.test.d.ts +0 -2
- package/dist/react/useRow.test.d.ts.map +0 -1
- package/dist/react/useRow.test.js +0 -562
- package/dist/react/useRow.test.js.map +0 -1
- package/dist/react/useTemporaryQuery.d.ts +0 -22
- package/dist/react/useTemporaryQuery.d.ts.map +0 -1
- package/dist/react/useTemporaryQuery.js +0 -70
- package/dist/react/useTemporaryQuery.js.map +0 -1
- package/dist/react/useTemporaryQuery.test.d.ts +0 -2
- package/dist/react/useTemporaryQuery.test.d.ts.map +0 -1
- package/dist/react/useTemporaryQuery.test.js +0 -37
- package/dist/react/useTemporaryQuery.test.js.map +0 -1
- package/dist/react/utils/stack-info.d.ts.map +0 -1
- package/dist/react/utils/stack-info.js.map +0 -1
- package/dist/react/utils/useStateRefWithReactiveInput.d.ts +0 -13
- package/dist/react/utils/useStateRefWithReactiveInput.d.ts.map +0 -1
- package/dist/react/utils/useStateRefWithReactiveInput.js +0 -38
- package/dist/react/utils/useStateRefWithReactiveInput.js.map +0 -1
- package/src/__tests__/react/fixture.tsx +0 -126
- package/src/react/LiveStoreContext.ts +0 -20
- package/src/react/LiveStoreProvider.test.tsx +0 -109
- package/src/react/LiveStoreProvider.tsx +0 -289
- package/src/react/components/LiveList.tsx +0 -84
- package/src/react/index.ts +0 -19
- package/src/react/useAtom.ts +0 -55
- package/src/react/useLocalId.ts +0 -33
- package/src/react/useQuery.test.tsx +0 -82
- package/src/react/useQuery.ts +0 -105
- package/src/react/useRow.test.tsx +0 -699
- package/src/react/useRow.ts +0 -180
- package/src/react/useTemporaryQuery.test.tsx +0 -56
- package/src/react/useTemporaryQuery.ts +0 -121
- package/src/react/utils/useStateRefWithReactiveInput.ts +0 -51
- /package/dist/{__tests__/react/utils → utils}/stack-info.test.d.ts +0 -0
- /package/dist/{__tests__/react/utils → utils/tests}/otel.d.ts +0 -0
- /package/dist/{__tests__/react/utils → utils/tests}/otel.js +0 -0
- /package/src/{__tests__/react/utils → utils/tests}/otel.ts +0 -0
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
import type { BootDb, BootStatus, IntentionalShutdownCause, StoreAdapterFactory } from '@livestore/common'
|
|
2
|
-
import { UnexpectedError } from '@livestore/common'
|
|
3
|
-
import type { LiveStoreSchema } from '@livestore/common/schema'
|
|
4
|
-
import { errorToString } from '@livestore/utils'
|
|
5
|
-
import { Effect, FiberSet, Logger, LogLevel, Schema } from '@livestore/utils/effect'
|
|
6
|
-
import type * as otel from '@opentelemetry/api'
|
|
7
|
-
import type { ReactElement, ReactNode } from 'react'
|
|
8
|
-
import React from 'react'
|
|
9
|
-
|
|
10
|
-
import type { BaseGraphQLContext, CreateStoreOptions, GraphQLOptions, OtelOptions } from '../store.js'
|
|
11
|
-
import { createStore } from '../store.js'
|
|
12
|
-
import type { LiveStoreContext as StoreContext_ } from '../store-context.js'
|
|
13
|
-
import { StoreAbort, StoreInterrupted } from '../store-context.js'
|
|
14
|
-
import { LiveStoreContext } from './LiveStoreContext.js'
|
|
15
|
-
|
|
16
|
-
interface LiveStoreProviderProps<GraphQLContext> {
|
|
17
|
-
schema: LiveStoreSchema
|
|
18
|
-
/**
|
|
19
|
-
* The `storeId` can be used to isolate multiple stores from each other.
|
|
20
|
-
* So it can be useful for multi-tenancy scenarios.
|
|
21
|
-
*
|
|
22
|
-
* The `storeId` is also used for persistence.
|
|
23
|
-
*
|
|
24
|
-
* @default 'default'
|
|
25
|
-
*/
|
|
26
|
-
storeId?: string
|
|
27
|
-
boot?: (db: BootDb, parentSpan: otel.Span) => void | Promise<void> | Effect.Effect<void, unknown, otel.Tracer>
|
|
28
|
-
graphQLOptions?: GraphQLOptions<GraphQLContext>
|
|
29
|
-
otelOptions?: OtelOptions
|
|
30
|
-
renderLoading: (status: BootStatus) => ReactElement
|
|
31
|
-
renderError?: (error: UnexpectedError | unknown) => ReactElement
|
|
32
|
-
renderShutdown?: (cause: IntentionalShutdownCause | StoreAbort) => ReactElement
|
|
33
|
-
adapter: StoreAdapterFactory
|
|
34
|
-
/**
|
|
35
|
-
* In order for LiveStore to apply multiple mutations in a single render,
|
|
36
|
-
* you need to pass the `batchUpdates` function from either `react-dom` or `react-native`.
|
|
37
|
-
*
|
|
38
|
-
* ```ts
|
|
39
|
-
* // With React DOM
|
|
40
|
-
* import { unstable_batchedUpdates as batchUpdates } from 'react-dom'
|
|
41
|
-
*
|
|
42
|
-
* // With React Native
|
|
43
|
-
* import { unstable_batchedUpdates as batchUpdates } from 'react-native'
|
|
44
|
-
* ```
|
|
45
|
-
*/
|
|
46
|
-
batchUpdates: (run: () => void) => void
|
|
47
|
-
disableDevtools?: boolean
|
|
48
|
-
signal?: AbortSignal
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const defaultRenderError = (error: UnexpectedError | unknown) => (
|
|
52
|
-
<>{Schema.is(UnexpectedError)(error) ? error.toString() : errorToString(error)}</>
|
|
53
|
-
)
|
|
54
|
-
const defaultRenderShutdown = (cause: IntentionalShutdownCause | StoreAbort) => {
|
|
55
|
-
const reason =
|
|
56
|
-
cause._tag === 'LiveStore.StoreAbort'
|
|
57
|
-
? 'abort signal'
|
|
58
|
-
: cause.reason === 'devtools-import'
|
|
59
|
-
? 'devtools import'
|
|
60
|
-
: cause.reason === 'devtools-reset'
|
|
61
|
-
? 'devtools reset'
|
|
62
|
-
: 'unknown reason'
|
|
63
|
-
|
|
64
|
-
return <>LiveStore Shutdown due to {reason}</>
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export const LiveStoreProvider = <GraphQLContext extends BaseGraphQLContext>({
|
|
68
|
-
renderLoading,
|
|
69
|
-
renderError = defaultRenderError,
|
|
70
|
-
renderShutdown = defaultRenderShutdown,
|
|
71
|
-
graphQLOptions,
|
|
72
|
-
otelOptions,
|
|
73
|
-
children,
|
|
74
|
-
schema,
|
|
75
|
-
storeId = 'default',
|
|
76
|
-
boot,
|
|
77
|
-
adapter,
|
|
78
|
-
batchUpdates,
|
|
79
|
-
disableDevtools,
|
|
80
|
-
signal,
|
|
81
|
-
}: LiveStoreProviderProps<GraphQLContext> & { children?: ReactNode }): JSX.Element => {
|
|
82
|
-
const storeCtx = useCreateStore({
|
|
83
|
-
storeId,
|
|
84
|
-
schema,
|
|
85
|
-
graphQLOptions,
|
|
86
|
-
otelOptions,
|
|
87
|
-
boot,
|
|
88
|
-
adapter,
|
|
89
|
-
batchUpdates,
|
|
90
|
-
disableDevtools,
|
|
91
|
-
signal,
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
if (storeCtx.stage === 'error') {
|
|
95
|
-
return renderError(storeCtx.error)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (storeCtx.stage === 'shutdown') {
|
|
99
|
-
return renderShutdown(storeCtx.cause)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (storeCtx.stage !== 'running') {
|
|
103
|
-
return renderLoading(storeCtx)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
window.__debugLiveStore ??= {}
|
|
107
|
-
window.__debugLiveStore[storeId] = storeCtx.store
|
|
108
|
-
|
|
109
|
-
return <LiveStoreContext.Provider value={storeCtx}>{children}</LiveStoreContext.Provider>
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
type SchemaKey = string
|
|
113
|
-
const semaphoreMap = new Map<SchemaKey, Effect.Semaphore>()
|
|
114
|
-
|
|
115
|
-
const withSemaphore = (schemaKey: SchemaKey) => {
|
|
116
|
-
let semaphore = semaphoreMap.get(schemaKey)
|
|
117
|
-
if (!semaphore) {
|
|
118
|
-
semaphore = Effect.makeSemaphore(1).pipe(Effect.runSync)
|
|
119
|
-
semaphoreMap.set(schemaKey, semaphore)
|
|
120
|
-
}
|
|
121
|
-
return semaphore.withPermits(1)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const useCreateStore = <GraphQLContext extends BaseGraphQLContext>({
|
|
125
|
-
schema,
|
|
126
|
-
storeId,
|
|
127
|
-
graphQLOptions,
|
|
128
|
-
otelOptions,
|
|
129
|
-
boot,
|
|
130
|
-
adapter,
|
|
131
|
-
batchUpdates,
|
|
132
|
-
disableDevtools,
|
|
133
|
-
reactivityGraph,
|
|
134
|
-
signal,
|
|
135
|
-
}: CreateStoreOptions<GraphQLContext, LiveStoreSchema> & { signal?: AbortSignal }) => {
|
|
136
|
-
const [_, rerender] = React.useState(0)
|
|
137
|
-
const ctxValueRef = React.useRef<{
|
|
138
|
-
value: StoreContext_ | BootStatus
|
|
139
|
-
fiberSet: FiberSet.FiberSet | undefined
|
|
140
|
-
counter: number
|
|
141
|
-
}>({
|
|
142
|
-
value: { stage: 'loading' },
|
|
143
|
-
fiberSet: undefined,
|
|
144
|
-
counter: 0,
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
// console.debug(`useCreateStore (${ctxValueRef.current.counter})`, ctxValueRef.current.value.stage)
|
|
148
|
-
|
|
149
|
-
const inputPropsCacheRef = React.useRef({
|
|
150
|
-
schema,
|
|
151
|
-
graphQLOptions,
|
|
152
|
-
otelOptions,
|
|
153
|
-
boot,
|
|
154
|
-
adapter,
|
|
155
|
-
batchUpdates,
|
|
156
|
-
disableDevtools,
|
|
157
|
-
reactivityGraph,
|
|
158
|
-
signal,
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
const interrupt = (fiberSet: FiberSet.FiberSet, error: StoreAbort | StoreInterrupted) =>
|
|
162
|
-
Effect.gen(function* () {
|
|
163
|
-
yield* FiberSet.clear(fiberSet)
|
|
164
|
-
yield* FiberSet.run(fiberSet, Effect.fail(error))
|
|
165
|
-
}).pipe(
|
|
166
|
-
Effect.tapErrorCause((cause) => Effect.logDebug(`[@livestore/livestore/react] interupting`, cause)),
|
|
167
|
-
Effect.runFork,
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
if (
|
|
171
|
-
inputPropsCacheRef.current.schema !== schema ||
|
|
172
|
-
inputPropsCacheRef.current.graphQLOptions !== graphQLOptions ||
|
|
173
|
-
inputPropsCacheRef.current.otelOptions !== otelOptions ||
|
|
174
|
-
inputPropsCacheRef.current.boot !== boot ||
|
|
175
|
-
inputPropsCacheRef.current.adapter !== adapter ||
|
|
176
|
-
inputPropsCacheRef.current.batchUpdates !== batchUpdates ||
|
|
177
|
-
inputPropsCacheRef.current.disableDevtools !== disableDevtools ||
|
|
178
|
-
inputPropsCacheRef.current.reactivityGraph !== reactivityGraph ||
|
|
179
|
-
inputPropsCacheRef.current.signal !== signal
|
|
180
|
-
) {
|
|
181
|
-
inputPropsCacheRef.current = {
|
|
182
|
-
schema,
|
|
183
|
-
graphQLOptions,
|
|
184
|
-
otelOptions,
|
|
185
|
-
boot,
|
|
186
|
-
adapter,
|
|
187
|
-
batchUpdates,
|
|
188
|
-
disableDevtools,
|
|
189
|
-
reactivityGraph,
|
|
190
|
-
signal,
|
|
191
|
-
}
|
|
192
|
-
if (ctxValueRef.current.fiberSet !== undefined) {
|
|
193
|
-
interrupt(ctxValueRef.current.fiberSet, new StoreInterrupted())
|
|
194
|
-
ctxValueRef.current.fiberSet = undefined
|
|
195
|
-
}
|
|
196
|
-
ctxValueRef.current = { value: { stage: 'loading' }, fiberSet: undefined, counter: ctxValueRef.current.counter + 1 }
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
React.useEffect(() => {
|
|
200
|
-
const counter = ctxValueRef.current.counter
|
|
201
|
-
|
|
202
|
-
const setContextValue = (value: StoreContext_ | BootStatus) => {
|
|
203
|
-
if (ctxValueRef.current.counter !== counter) return
|
|
204
|
-
ctxValueRef.current.value = value
|
|
205
|
-
rerender((c) => c + 1)
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
signal?.addEventListener('abort', () => {
|
|
209
|
-
if (ctxValueRef.current.fiberSet !== undefined && ctxValueRef.current.counter === counter) {
|
|
210
|
-
interrupt(ctxValueRef.current.fiberSet, new StoreAbort())
|
|
211
|
-
ctxValueRef.current.fiberSet = undefined
|
|
212
|
-
}
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
Effect.gen(function* () {
|
|
216
|
-
const fiberSet = yield* FiberSet.make<
|
|
217
|
-
unknown,
|
|
218
|
-
UnexpectedError | IntentionalShutdownCause | StoreAbort | StoreInterrupted
|
|
219
|
-
>()
|
|
220
|
-
|
|
221
|
-
ctxValueRef.current.fiberSet = fiberSet
|
|
222
|
-
|
|
223
|
-
yield* Effect.gen(function* () {
|
|
224
|
-
const store = yield* createStore({
|
|
225
|
-
fiberSet,
|
|
226
|
-
schema,
|
|
227
|
-
storeId,
|
|
228
|
-
graphQLOptions,
|
|
229
|
-
otelOptions,
|
|
230
|
-
boot,
|
|
231
|
-
adapter,
|
|
232
|
-
reactivityGraph,
|
|
233
|
-
batchUpdates,
|
|
234
|
-
disableDevtools,
|
|
235
|
-
onBootStatus: (status) => {
|
|
236
|
-
if (ctxValueRef.current.value.stage === 'running' || ctxValueRef.current.value.stage === 'error') return
|
|
237
|
-
setContextValue(status)
|
|
238
|
-
},
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
setContextValue({ stage: 'running', store })
|
|
242
|
-
|
|
243
|
-
yield* Effect.never
|
|
244
|
-
}).pipe(Effect.scoped, FiberSet.run(fiberSet))
|
|
245
|
-
|
|
246
|
-
const shutdownContext = (cause: IntentionalShutdownCause | StoreAbort) =>
|
|
247
|
-
Effect.sync(() => setContextValue({ stage: 'shutdown', cause }))
|
|
248
|
-
|
|
249
|
-
yield* FiberSet.join(fiberSet).pipe(
|
|
250
|
-
Effect.catchTag('LiveStore.IntentionalShutdownCause', (cause) => shutdownContext(cause)),
|
|
251
|
-
Effect.catchTag('LiveStore.StoreAbort', (cause) => shutdownContext(cause)),
|
|
252
|
-
Effect.tapError((error) => Effect.sync(() => setContextValue({ stage: 'error', error }))),
|
|
253
|
-
Effect.tapDefect((defect) => Effect.sync(() => setContextValue({ stage: 'error', error: defect }))),
|
|
254
|
-
Effect.exit,
|
|
255
|
-
)
|
|
256
|
-
}).pipe(
|
|
257
|
-
Effect.scoped,
|
|
258
|
-
// NOTE we're running the code above in a semaphore to make sure a previous store is always fully
|
|
259
|
-
// shutdown before a new one is created - especially when shutdown logic is async. You can't trust `React.useEffect`.
|
|
260
|
-
// Thank you to Mattia Manzati for this idea.
|
|
261
|
-
withSemaphore(storeId),
|
|
262
|
-
Effect.tapCauseLogPretty,
|
|
263
|
-
Effect.annotateLogs({ thread: 'window' }),
|
|
264
|
-
Effect.provide(Logger.pretty),
|
|
265
|
-
Logger.withMinimumLogLevel(LogLevel.Debug),
|
|
266
|
-
Effect.runFork,
|
|
267
|
-
)
|
|
268
|
-
|
|
269
|
-
return () => {
|
|
270
|
-
if (ctxValueRef.current.fiberSet !== undefined) {
|
|
271
|
-
interrupt(ctxValueRef.current.fiberSet, new StoreInterrupted())
|
|
272
|
-
ctxValueRef.current.fiberSet = undefined
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}, [
|
|
276
|
-
schema,
|
|
277
|
-
graphQLOptions,
|
|
278
|
-
otelOptions,
|
|
279
|
-
boot,
|
|
280
|
-
adapter,
|
|
281
|
-
batchUpdates,
|
|
282
|
-
disableDevtools,
|
|
283
|
-
signal,
|
|
284
|
-
reactivityGraph,
|
|
285
|
-
storeId,
|
|
286
|
-
])
|
|
287
|
-
|
|
288
|
-
return ctxValueRef.current.value
|
|
289
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
|
|
3
|
-
import type { LiveQuery } from '../../reactiveQueries/base-class.js'
|
|
4
|
-
import { computed } from '../../reactiveQueries/js.js'
|
|
5
|
-
import { useQuery } from '../useQuery.js'
|
|
6
|
-
import { useTemporaryQuery } from '../useTemporaryQuery.js'
|
|
7
|
-
|
|
8
|
-
/*
|
|
9
|
-
TODO:
|
|
10
|
-
- [ ] Bring back incremental rendering (see https://github.com/livestorejs/livestore/pull/55)
|
|
11
|
-
- [ ] Enable exit animations
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
export type LiveListProps<TItem> = {
|
|
15
|
-
items$: LiveQuery<ReadonlyArray<TItem>>
|
|
16
|
-
// TODO refactor render-flag to allow for transition animations on add/remove
|
|
17
|
-
renderItem: (item: TItem, opts: { index: number; isInitialListRender: boolean }) => React.ReactNode
|
|
18
|
-
/** Needs to be unique across all list items */
|
|
19
|
-
getKey: (item: TItem, index: number) => string | number
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* This component is a helper component for rendering a list of items for a LiveQuery of an array of items.
|
|
24
|
-
* The idea is that instead of letting React handle the rendering of the items array directly,
|
|
25
|
-
* we derive a item LiveQuery for each item which moves the reactivity to the item level when a single item changes.
|
|
26
|
-
*
|
|
27
|
-
* In the future we want to make this component even more efficient by using incremental rendering (https://github.com/livestorejs/livestore/pull/55)
|
|
28
|
-
* e.g. when an item is added/removed/moved to only re-render the affected DOM nodes.
|
|
29
|
-
*/
|
|
30
|
-
export const LiveList = <TItem,>({ items$, renderItem, getKey }: LiveListProps<TItem>): React.ReactNode => {
|
|
31
|
-
const [hasMounted, setHasMounted] = React.useState(false)
|
|
32
|
-
|
|
33
|
-
React.useEffect(() => setHasMounted(true), [])
|
|
34
|
-
|
|
35
|
-
const keysCb = React.useCallback(() => computed((get) => get(items$).map(getKey)), [getKey, items$])
|
|
36
|
-
const keys = useTemporaryQuery(keysCb, 'fixed')
|
|
37
|
-
const arr = React.useMemo(
|
|
38
|
-
() =>
|
|
39
|
-
keys.map(
|
|
40
|
-
(key) =>
|
|
41
|
-
// TODO figure out a way so that `item$` returns an ordered lookup map to more efficiently find the item by key
|
|
42
|
-
[key, computed((get) => get(items$).find((item) => getKey(item, 0) === key)!) as LiveQuery<TItem>] as const,
|
|
43
|
-
),
|
|
44
|
-
[getKey, items$, keys],
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
return (
|
|
48
|
-
<>
|
|
49
|
-
{arr.map(([key, item$], index) => (
|
|
50
|
-
<ItemWrapperMemo
|
|
51
|
-
key={key}
|
|
52
|
-
itemKey={key}
|
|
53
|
-
item$={item$}
|
|
54
|
-
opts={{ isInitialListRender: !hasMounted, index }}
|
|
55
|
-
renderItem={renderItem}
|
|
56
|
-
/>
|
|
57
|
-
))}
|
|
58
|
-
</>
|
|
59
|
-
)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const ItemWrapper = <TItem,>({
|
|
63
|
-
item$,
|
|
64
|
-
opts,
|
|
65
|
-
renderItem,
|
|
66
|
-
}: {
|
|
67
|
-
itemKey: string | number
|
|
68
|
-
item$: LiveQuery<TItem>
|
|
69
|
-
opts: { index: number; isInitialListRender: boolean }
|
|
70
|
-
renderItem: (item: TItem, opts: { index: number; isInitialListRender: boolean }) => React.ReactNode
|
|
71
|
-
}) => {
|
|
72
|
-
const item = useQuery(item$)
|
|
73
|
-
|
|
74
|
-
return <>{renderItem(item, opts)}</>
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const ItemWrapperMemo = React.memo(
|
|
78
|
-
ItemWrapper,
|
|
79
|
-
(prev, next) =>
|
|
80
|
-
prev.itemKey === next.itemKey &&
|
|
81
|
-
prev.renderItem === prev.renderItem &&
|
|
82
|
-
prev.opts.index === next.opts.index &&
|
|
83
|
-
prev.opts.isInitialListRender === next.opts.isInitialListRender,
|
|
84
|
-
) as typeof ItemWrapper
|
package/src/react/index.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export { LiveStoreContext, useStore } from './LiveStoreContext.js'
|
|
2
|
-
export { LiveStoreProvider } from './LiveStoreProvider.js'
|
|
3
|
-
export { useQuery } from './useQuery.js'
|
|
4
|
-
export { useTemporaryQuery } from './useTemporaryQuery.js'
|
|
5
|
-
export { useStackInfo } from './utils/stack-info.js'
|
|
6
|
-
export {
|
|
7
|
-
useRow,
|
|
8
|
-
type StateSetters,
|
|
9
|
-
type SetStateAction,
|
|
10
|
-
type Dispatch,
|
|
11
|
-
type UseRowResult as UseStateResult,
|
|
12
|
-
} from './useRow.js'
|
|
13
|
-
export { useAtom } from './useAtom.js'
|
|
14
|
-
export { useLocalId, getLocalId } from './useLocalId.js'
|
|
15
|
-
|
|
16
|
-
export { LiveList, type LiveListProps } from './components/LiveList.js'
|
|
17
|
-
|
|
18
|
-
// Needed to make TS happy
|
|
19
|
-
export type { TypedDocumentNode } from '@graphql-typed-document-node/core'
|
package/src/react/useAtom.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { type QueryInfoCol, type QueryInfoRow } from '@livestore/common'
|
|
2
|
-
import type { DbSchema } from '@livestore/common/schema'
|
|
3
|
-
import React from 'react'
|
|
4
|
-
|
|
5
|
-
import type { LiveQuery } from '../reactiveQueries/base-class.js'
|
|
6
|
-
import { useStore } from './LiveStoreContext.js'
|
|
7
|
-
import { useQueryRef } from './useQuery.js'
|
|
8
|
-
import type { Dispatch, SetStateAction } from './useRow.js'
|
|
9
|
-
|
|
10
|
-
export const useAtom = <
|
|
11
|
-
TQuery extends LiveQuery<any, QueryInfoRow<TTableDef> | QueryInfoCol<TTableDef, any>>,
|
|
12
|
-
TTableDef extends DbSchema.TableDef<
|
|
13
|
-
DbSchema.DefaultSqliteTableDefConstrained,
|
|
14
|
-
boolean,
|
|
15
|
-
DbSchema.TableOptions & { deriveMutations: { enabled: true } }
|
|
16
|
-
>,
|
|
17
|
-
>(
|
|
18
|
-
query$: TQuery,
|
|
19
|
-
): [value: TQuery['__result!'], setValue: Dispatch<SetStateAction<Partial<TQuery['__result!']>>>] => {
|
|
20
|
-
const query$Ref = useQueryRef(query$)
|
|
21
|
-
|
|
22
|
-
const { store } = useStore()
|
|
23
|
-
|
|
24
|
-
// TODO make API equivalent to useRow
|
|
25
|
-
const setValue = React.useMemo<Dispatch<SetStateAction<TQuery['__result!']>>>(() => {
|
|
26
|
-
return (newValueOrFn: any) => {
|
|
27
|
-
const newValue = typeof newValueOrFn === 'function' ? newValueOrFn(query$Ref.current) : newValueOrFn
|
|
28
|
-
|
|
29
|
-
if (query$.queryInfo._tag === 'Row') {
|
|
30
|
-
if (query$.queryInfo.table.options.isSingleton && query$.queryInfo.table.isSingleColumn) {
|
|
31
|
-
store.mutate(query$.queryInfo.table.update(newValue))
|
|
32
|
-
} else if (query$.queryInfo.table.options.isSingleColumn) {
|
|
33
|
-
store.mutate(
|
|
34
|
-
query$.queryInfo.table.update({ where: { id: query$.queryInfo.id }, values: { value: newValue } }),
|
|
35
|
-
)
|
|
36
|
-
} else {
|
|
37
|
-
store.mutate(query$.queryInfo.table.update({ where: { id: query$.queryInfo.id }, values: newValue }))
|
|
38
|
-
}
|
|
39
|
-
} else {
|
|
40
|
-
if (query$.queryInfo.table.options.isSingleton && query$.queryInfo.table.isSingleColumn) {
|
|
41
|
-
store.mutate(query$.queryInfo.table.update({ [query$.queryInfo.column]: newValue }))
|
|
42
|
-
} else {
|
|
43
|
-
store.mutate(
|
|
44
|
-
query$.queryInfo.table.update({
|
|
45
|
-
where: { id: query$.queryInfo.id },
|
|
46
|
-
values: { [query$.queryInfo.column]: newValue },
|
|
47
|
-
}),
|
|
48
|
-
)
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}, [query$.queryInfo, query$Ref, store])
|
|
53
|
-
|
|
54
|
-
return [query$Ref.current, setValue]
|
|
55
|
-
}
|
package/src/react/useLocalId.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { cuid } from '@livestore/utils/cuid'
|
|
2
|
-
import React from 'react'
|
|
3
|
-
|
|
4
|
-
type LocalIdOptions = {
|
|
5
|
-
key: string
|
|
6
|
-
storageType: 'session' | 'local'
|
|
7
|
-
storageKeyPrefix: string
|
|
8
|
-
makeId: () => string
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const useLocalId = (opts?: Partial<LocalIdOptions>) => React.useMemo(() => getLocalId(opts), [opts])
|
|
12
|
-
|
|
13
|
-
export const getLocalId = (opts?: Partial<LocalIdOptions>) => {
|
|
14
|
-
// TODO find a better way to handle this
|
|
15
|
-
// Currently `getLocalId` gets imported and called in some worker-side code
|
|
16
|
-
// during development where Vite isn't tree-shaking yet.
|
|
17
|
-
if (typeof window === 'undefined' || window.localStorage === undefined || window.sessionStorage === undefined) {
|
|
18
|
-
return ''
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const { key = '', storageType = 'session', storageKeyPrefix = 'livestore:localid:', makeId = cuid } = opts ?? {}
|
|
22
|
-
|
|
23
|
-
const storage = storageType === 'session' ? window.sessionStorage : window.localStorage
|
|
24
|
-
const fullKey = `${storageKeyPrefix}:${key}`
|
|
25
|
-
const storedKey = storage.getItem(fullKey)
|
|
26
|
-
|
|
27
|
-
if (storedKey) return storedKey
|
|
28
|
-
|
|
29
|
-
const newKey = makeId()
|
|
30
|
-
storage.setItem(fullKey, newKey)
|
|
31
|
-
|
|
32
|
-
return newKey
|
|
33
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { Effect, Schema } from '@livestore/utils/effect'
|
|
2
|
-
import { renderHook } from '@testing-library/react'
|
|
3
|
-
import React from 'react'
|
|
4
|
-
import { describe, expect, it } from 'vitest'
|
|
5
|
-
|
|
6
|
-
import { makeTodoMvc, tables, todos } from '../__tests__/react/fixture.js'
|
|
7
|
-
import { querySQL } from '../reactiveQueries/sql.js'
|
|
8
|
-
import * as LiveStoreReact from './index.js'
|
|
9
|
-
|
|
10
|
-
describe('useQuery', () => {
|
|
11
|
-
it('simple', () =>
|
|
12
|
-
Effect.gen(function* () {
|
|
13
|
-
const { wrapper, store, makeRenderCount } = yield* makeTodoMvc()
|
|
14
|
-
|
|
15
|
-
const renderCount = makeRenderCount()
|
|
16
|
-
|
|
17
|
-
const allTodos$ = querySQL(`select * from todos`, { schema: Schema.Array(tables.todos.schema) })
|
|
18
|
-
|
|
19
|
-
const { result } = renderHook(
|
|
20
|
-
() => {
|
|
21
|
-
renderCount.inc()
|
|
22
|
-
|
|
23
|
-
return LiveStoreReact.useQuery(allTodos$)
|
|
24
|
-
},
|
|
25
|
-
{ wrapper },
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
expect(result.current.length).toBe(0)
|
|
29
|
-
expect(renderCount.val).toBe(1)
|
|
30
|
-
|
|
31
|
-
React.act(() => store.mutate(todos.insert({ id: 't1', text: 'buy milk', completed: false })))
|
|
32
|
-
|
|
33
|
-
expect(result.current.length).toBe(1)
|
|
34
|
-
expect(result.current[0]!.text).toBe('buy milk')
|
|
35
|
-
expect(renderCount.val).toBe(2)
|
|
36
|
-
}).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
|
|
37
|
-
|
|
38
|
-
it('same `useQuery` hook invoked with different queries', () =>
|
|
39
|
-
Effect.gen(function* () {
|
|
40
|
-
const { wrapper, store, makeRenderCount } = yield* makeTodoMvc()
|
|
41
|
-
|
|
42
|
-
const renderCount = makeRenderCount()
|
|
43
|
-
|
|
44
|
-
const todo1$ = querySQL(`select * from todos where id = 't1'`, {
|
|
45
|
-
label: 'libraryTracksView1',
|
|
46
|
-
schema: Schema.Array(tables.todos.schema),
|
|
47
|
-
})
|
|
48
|
-
const todo2$ = querySQL(`select * from todos where id = 't2'`, {
|
|
49
|
-
label: 'libraryTracksView2',
|
|
50
|
-
schema: Schema.Array(tables.todos.schema),
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
store.mutate(
|
|
54
|
-
todos.insert({ id: 't1', text: 'buy milk', completed: false }),
|
|
55
|
-
todos.insert({ id: 't2', text: 'buy eggs', completed: false }),
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
const { result, rerender } = renderHook(
|
|
59
|
-
(todoId: string) => {
|
|
60
|
-
renderCount.inc()
|
|
61
|
-
|
|
62
|
-
const query$ = React.useMemo(() => (todoId === 't1' ? todo1$ : todo2$), [todoId])
|
|
63
|
-
|
|
64
|
-
return LiveStoreReact.useQuery(query$)[0]!.text
|
|
65
|
-
},
|
|
66
|
-
{ wrapper, initialProps: 't1' },
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
expect(result.current).toBe('buy milk')
|
|
70
|
-
expect(renderCount.val).toBe(1)
|
|
71
|
-
|
|
72
|
-
React.act(() => store.mutate(todos.update({ where: { id: 't1' }, values: { text: 'buy soy milk' } })))
|
|
73
|
-
|
|
74
|
-
expect(result.current).toBe('buy soy milk')
|
|
75
|
-
expect(renderCount.val).toBe(2)
|
|
76
|
-
|
|
77
|
-
rerender('t2')
|
|
78
|
-
|
|
79
|
-
expect(result.current).toBe('buy eggs')
|
|
80
|
-
expect(renderCount.val).toBe(3)
|
|
81
|
-
}).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
|
|
82
|
-
})
|
package/src/react/useQuery.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { deepEqual } from '@livestore/utils'
|
|
2
|
-
import * as otel from '@opentelemetry/api'
|
|
3
|
-
import React from 'react'
|
|
4
|
-
|
|
5
|
-
import type { GetResult, LiveQueryAny } from '../reactiveQueries/base-class.js'
|
|
6
|
-
import { useStore } from './LiveStoreContext.js'
|
|
7
|
-
import { extractStackInfoFromStackTrace, originalStackLimit } from './utils/stack-info.js'
|
|
8
|
-
import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInput.js'
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* NOTE Some folks have suggested to use `React.useSyncExternalStore`, however, it's not doing anything special
|
|
12
|
-
* for what's needed here, so we handle everything manually.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* This is needed because the `React.useMemo` call below, can sometimes be called multiple times 🤷,
|
|
17
|
-
* so we need to "cache" the fact that we've already started a span for this component.
|
|
18
|
-
* The map entry is being removed again in the `React.useEffect` call below.
|
|
19
|
-
*/
|
|
20
|
-
const spanAlreadyStartedCache = new Map<LiveQueryAny, { span: otel.Span; otelContext: otel.Context }>()
|
|
21
|
-
|
|
22
|
-
export const useQuery = <TQuery extends LiveQueryAny>(query: TQuery): GetResult<TQuery> => useQueryRef(query).current
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
*
|
|
26
|
-
*/
|
|
27
|
-
export const useQueryRef = <TQuery extends LiveQueryAny>(
|
|
28
|
-
query$: TQuery,
|
|
29
|
-
parentOtelContext?: otel.Context,
|
|
30
|
-
): React.MutableRefObject<GetResult<TQuery>> => {
|
|
31
|
-
const { store } = useStore()
|
|
32
|
-
|
|
33
|
-
React.useDebugValue(`LiveStore:useQuery:${query$.id}:${query$.label}`)
|
|
34
|
-
|
|
35
|
-
const stackInfo = React.useMemo(() => {
|
|
36
|
-
Error.stackTraceLimit = 10
|
|
37
|
-
// eslint-disable-next-line unicorn/error-message
|
|
38
|
-
const stack = new Error().stack!
|
|
39
|
-
Error.stackTraceLimit = originalStackLimit
|
|
40
|
-
return extractStackInfoFromStackTrace(stack)
|
|
41
|
-
}, [])
|
|
42
|
-
|
|
43
|
-
// The following `React.useMemo` and `React.useEffect` calls are used to start and end a span for the lifetime of this component.
|
|
44
|
-
const { span, otelContext } = React.useMemo(() => {
|
|
45
|
-
const existingSpan = spanAlreadyStartedCache.get(query$)
|
|
46
|
-
if (existingSpan !== undefined) return existingSpan
|
|
47
|
-
|
|
48
|
-
const span = store.otel.tracer.startSpan(
|
|
49
|
-
`LiveStore:useQuery:${query$.label}`,
|
|
50
|
-
{ attributes: { label: query$.label, stackInfo: JSON.stringify(stackInfo) } },
|
|
51
|
-
parentOtelContext ?? store.otel.queriesSpanContext,
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
55
|
-
|
|
56
|
-
spanAlreadyStartedCache.set(query$, { span, otelContext })
|
|
57
|
-
|
|
58
|
-
return { span, otelContext }
|
|
59
|
-
}, [parentOtelContext, query$, stackInfo, store.otel.queriesSpanContext, store.otel.tracer])
|
|
60
|
-
|
|
61
|
-
const initialResult = React.useMemo(
|
|
62
|
-
() =>
|
|
63
|
-
query$.run(otelContext, {
|
|
64
|
-
_tag: 'react',
|
|
65
|
-
api: 'useQuery',
|
|
66
|
-
label: query$.label,
|
|
67
|
-
stackInfo,
|
|
68
|
-
}),
|
|
69
|
-
[otelContext, query$, stackInfo],
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
// We know the query has a result by the time we use it; so we can synchronously populate a default state
|
|
73
|
-
const [valueRef, setValue] = useStateRefWithReactiveInput<GetResult<TQuery>>(initialResult)
|
|
74
|
-
|
|
75
|
-
React.useEffect(
|
|
76
|
-
() => () => {
|
|
77
|
-
spanAlreadyStartedCache.delete(query$)
|
|
78
|
-
span.end()
|
|
79
|
-
},
|
|
80
|
-
[query$, span],
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
// Subscribe to future updates for this query
|
|
84
|
-
React.useEffect(() => {
|
|
85
|
-
query$.activeSubscriptions.add(stackInfo)
|
|
86
|
-
|
|
87
|
-
return store.subscribe(
|
|
88
|
-
query$,
|
|
89
|
-
(newValue) => {
|
|
90
|
-
// NOTE: we return a reference to the result object within LiveStore;
|
|
91
|
-
// this implies that app code must not mutate the results, or else
|
|
92
|
-
// there may be weird reactivity bugs.
|
|
93
|
-
if (deepEqual(newValue, valueRef.current) === false) {
|
|
94
|
-
setValue(newValue)
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
() => {
|
|
98
|
-
query$.activeSubscriptions.delete(stackInfo)
|
|
99
|
-
},
|
|
100
|
-
{ label: query$.label, otelContext },
|
|
101
|
-
)
|
|
102
|
-
}, [stackInfo, query$, setValue, store, valueRef, otelContext, span])
|
|
103
|
-
|
|
104
|
-
return valueRef
|
|
105
|
-
}
|