@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.
Files changed (157) hide show
  1. package/README.md +1 -117
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/effect/LiveStore.d.ts +3 -3
  4. package/dist/effect/LiveStore.d.ts.map +1 -1
  5. package/dist/effect/LiveStore.js +1 -1
  6. package/dist/effect/LiveStore.js.map +1 -1
  7. package/dist/global-state.d.ts.map +1 -1
  8. package/dist/global-state.js +2 -1
  9. package/dist/global-state.js.map +1 -1
  10. package/dist/index.d.ts +8 -6
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +4 -2
  13. package/dist/index.js.map +1 -1
  14. package/dist/reactiveQueries/base-class.d.ts +1 -1
  15. package/dist/reactiveQueries/base-class.d.ts.map +1 -1
  16. package/dist/reactiveQueries/base-class.js.map +1 -1
  17. package/dist/reactiveQueries/sql.d.ts +1 -1
  18. package/dist/reactiveQueries/sql.d.ts.map +1 -1
  19. package/dist/reactiveQueries/sql.js +4 -4
  20. package/dist/reactiveQueries/sql.js.map +1 -1
  21. package/dist/reactiveQueries/sql.test.js +2 -2
  22. package/dist/reactiveQueries/sql.test.js.map +1 -1
  23. package/dist/row-query.d.ts +3 -2
  24. package/dist/row-query.d.ts.map +1 -1
  25. package/dist/row-query.js +18 -10
  26. package/dist/row-query.js.map +1 -1
  27. package/dist/store-devtools.d.ts +2 -2
  28. package/dist/store-devtools.d.ts.map +1 -1
  29. package/dist/store-devtools.js +3 -3
  30. package/dist/store-devtools.js.map +1 -1
  31. package/dist/store.d.ts +23 -19
  32. package/dist/store.d.ts.map +1 -1
  33. package/dist/store.js +90 -59
  34. package/dist/store.js.map +1 -1
  35. package/dist/utils/dev.d.ts.map +1 -1
  36. package/dist/utils/dev.js +1 -0
  37. package/dist/utils/dev.js.map +1 -1
  38. package/dist/{react/utils → utils}/stack-info.d.ts +1 -2
  39. package/dist/utils/stack-info.d.ts.map +1 -0
  40. package/dist/{react/utils → utils}/stack-info.js +1 -9
  41. package/dist/utils/stack-info.js.map +1 -0
  42. package/dist/utils/stack-info.test.d.ts.map +1 -0
  43. package/dist/{__tests__/react/utils → utils}/stack-info.test.js +1 -1
  44. package/dist/utils/stack-info.test.js.map +1 -0
  45. package/dist/utils/tests/fixture.d.ts +259 -0
  46. package/dist/utils/tests/fixture.d.ts.map +1 -0
  47. package/dist/utils/tests/fixture.js +33 -0
  48. package/dist/utils/tests/fixture.js.map +1 -0
  49. package/dist/utils/tests/mod.d.ts +3 -0
  50. package/dist/utils/tests/mod.d.ts.map +1 -0
  51. package/dist/utils/tests/mod.js +3 -0
  52. package/dist/utils/tests/mod.js.map +1 -0
  53. package/dist/utils/tests/otel.d.ts.map +1 -0
  54. package/dist/utils/tests/otel.js.map +1 -0
  55. package/package.json +17 -24
  56. package/src/ambient.d.ts +3 -0
  57. package/src/effect/LiveStore.ts +4 -4
  58. package/src/global-state.ts +5 -1
  59. package/src/index.ts +17 -4
  60. package/src/reactiveQueries/base-class.ts +1 -1
  61. package/src/reactiveQueries/sql.test.ts +2 -2
  62. package/src/reactiveQueries/sql.ts +5 -5
  63. package/src/row-query.ts +36 -16
  64. package/src/store-devtools.ts +5 -5
  65. package/src/store.ts +146 -78
  66. package/src/utils/dev.ts +1 -0
  67. package/src/{__tests__/react/utils → utils}/stack-info.test.ts +1 -1
  68. package/src/{react/utils → utils}/stack-info.ts +2 -12
  69. package/src/utils/tests/fixture.ts +77 -0
  70. package/src/utils/tests/mod.ts +2 -0
  71. package/tsconfig.json +1 -2
  72. package/vitest.config.js +0 -8
  73. package/dist/__tests__/react/fixture.d.ts +0 -461
  74. package/dist/__tests__/react/fixture.d.ts.map +0 -1
  75. package/dist/__tests__/react/fixture.js +0 -68
  76. package/dist/__tests__/react/fixture.js.map +0 -1
  77. package/dist/__tests__/react/utils/otel.d.ts.map +0 -1
  78. package/dist/__tests__/react/utils/otel.js.map +0 -1
  79. package/dist/__tests__/react/utils/stack-info.test.d.ts.map +0 -1
  80. package/dist/__tests__/react/utils/stack-info.test.js.map +0 -1
  81. package/dist/react/LiveStoreContext.d.ts +0 -7
  82. package/dist/react/LiveStoreContext.d.ts.map +0 -1
  83. package/dist/react/LiveStoreContext.js +0 -13
  84. package/dist/react/LiveStoreContext.js.map +0 -1
  85. package/dist/react/LiveStoreProvider.d.ts +0 -47
  86. package/dist/react/LiveStoreProvider.d.ts.map +0 -1
  87. package/dist/react/LiveStoreProvider.js +0 -169
  88. package/dist/react/LiveStoreProvider.js.map +0 -1
  89. package/dist/react/LiveStoreProvider.test.d.ts +0 -2
  90. package/dist/react/LiveStoreProvider.test.d.ts.map +0 -1
  91. package/dist/react/LiveStoreProvider.test.js +0 -62
  92. package/dist/react/LiveStoreProvider.test.js.map +0 -1
  93. package/dist/react/components/LiveList.d.ts +0 -21
  94. package/dist/react/components/LiveList.d.ts.map +0 -1
  95. package/dist/react/components/LiveList.js +0 -31
  96. package/dist/react/components/LiveList.js.map +0 -1
  97. package/dist/react/index.d.ts +0 -11
  98. package/dist/react/index.d.ts.map +0 -1
  99. package/dist/react/index.js +0 -10
  100. package/dist/react/index.js.map +0 -1
  101. package/dist/react/useAtom.d.ts +0 -10
  102. package/dist/react/useAtom.d.ts.map +0 -1
  103. package/dist/react/useAtom.js +0 -37
  104. package/dist/react/useAtom.js.map +0 -1
  105. package/dist/react/useLocalId.d.ts +0 -10
  106. package/dist/react/useLocalId.d.ts.map +0 -1
  107. package/dist/react/useLocalId.js +0 -21
  108. package/dist/react/useLocalId.js.map +0 -1
  109. package/dist/react/useQuery.d.ts +0 -9
  110. package/dist/react/useQuery.d.ts.map +0 -1
  111. package/dist/react/useQuery.js +0 -69
  112. package/dist/react/useQuery.js.map +0 -1
  113. package/dist/react/useQuery.test.d.ts +0 -2
  114. package/dist/react/useQuery.test.d.ts.map +0 -1
  115. package/dist/react/useQuery.test.js +0 -51
  116. package/dist/react/useQuery.test.js.map +0 -1
  117. package/dist/react/useRow.d.ts +0 -46
  118. package/dist/react/useRow.d.ts.map +0 -1
  119. package/dist/react/useRow.js +0 -94
  120. package/dist/react/useRow.js.map +0 -1
  121. package/dist/react/useRow.test.d.ts +0 -2
  122. package/dist/react/useRow.test.d.ts.map +0 -1
  123. package/dist/react/useRow.test.js +0 -562
  124. package/dist/react/useRow.test.js.map +0 -1
  125. package/dist/react/useTemporaryQuery.d.ts +0 -22
  126. package/dist/react/useTemporaryQuery.d.ts.map +0 -1
  127. package/dist/react/useTemporaryQuery.js +0 -70
  128. package/dist/react/useTemporaryQuery.js.map +0 -1
  129. package/dist/react/useTemporaryQuery.test.d.ts +0 -2
  130. package/dist/react/useTemporaryQuery.test.d.ts.map +0 -1
  131. package/dist/react/useTemporaryQuery.test.js +0 -37
  132. package/dist/react/useTemporaryQuery.test.js.map +0 -1
  133. package/dist/react/utils/stack-info.d.ts.map +0 -1
  134. package/dist/react/utils/stack-info.js.map +0 -1
  135. package/dist/react/utils/useStateRefWithReactiveInput.d.ts +0 -13
  136. package/dist/react/utils/useStateRefWithReactiveInput.d.ts.map +0 -1
  137. package/dist/react/utils/useStateRefWithReactiveInput.js +0 -38
  138. package/dist/react/utils/useStateRefWithReactiveInput.js.map +0 -1
  139. package/src/__tests__/react/fixture.tsx +0 -126
  140. package/src/react/LiveStoreContext.ts +0 -20
  141. package/src/react/LiveStoreProvider.test.tsx +0 -109
  142. package/src/react/LiveStoreProvider.tsx +0 -289
  143. package/src/react/components/LiveList.tsx +0 -84
  144. package/src/react/index.ts +0 -19
  145. package/src/react/useAtom.ts +0 -55
  146. package/src/react/useLocalId.ts +0 -33
  147. package/src/react/useQuery.test.tsx +0 -82
  148. package/src/react/useQuery.ts +0 -105
  149. package/src/react/useRow.test.tsx +0 -699
  150. package/src/react/useRow.ts +0 -180
  151. package/src/react/useTemporaryQuery.test.tsx +0 -56
  152. package/src/react/useTemporaryQuery.ts +0 -121
  153. package/src/react/utils/useStateRefWithReactiveInput.ts +0 -51
  154. /package/dist/{__tests__/react/utils → utils}/stack-info.test.d.ts +0 -0
  155. /package/dist/{__tests__/react/utils → utils/tests}/otel.d.ts +0 -0
  156. /package/dist/{__tests__/react/utils → utils/tests}/otel.js +0 -0
  157. /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
@@ -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'
@@ -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
- }
@@ -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
- })
@@ -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
- }