@livestore/react 0.4.0-dev.2 → 0.4.0-dev.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/LiveStoreContext.d.ts +27 -0
- package/dist/LiveStoreContext.d.ts.map +1 -1
- package/dist/LiveStoreContext.js +18 -0
- package/dist/LiveStoreContext.js.map +1 -1
- package/dist/LiveStoreProvider.d.ts +14 -8
- package/dist/LiveStoreProvider.d.ts.map +1 -1
- package/dist/LiveStoreProvider.js +40 -24
- package/dist/LiveStoreProvider.js.map +1 -1
- package/dist/LiveStoreProvider.test.js +7 -7
- package/dist/LiveStoreProvider.test.js.map +1 -1
- package/dist/__tests__/fixture.d.ts +34 -12
- package/dist/__tests__/fixture.d.ts.map +1 -1
- package/dist/__tests__/fixture.js +13 -5
- package/dist/__tests__/fixture.js.map +1 -1
- package/dist/experimental/components/LiveList.js +1 -1
- package/dist/experimental/mod.d.ts +1 -0
- package/dist/experimental/mod.d.ts.map +1 -1
- package/dist/experimental/mod.js +1 -0
- package/dist/experimental/mod.js.map +1 -1
- package/dist/experimental/multi-store/StoreRegistry.d.ts +105 -0
- package/dist/experimental/multi-store/StoreRegistry.d.ts.map +1 -0
- package/dist/experimental/multi-store/StoreRegistry.js +184 -0
- package/dist/experimental/multi-store/StoreRegistry.js.map +1 -0
- package/dist/experimental/multi-store/StoreRegistry.test.d.ts +2 -0
- package/dist/experimental/multi-store/StoreRegistry.test.d.ts.map +1 -0
- package/dist/experimental/multi-store/StoreRegistry.test.js +381 -0
- package/dist/experimental/multi-store/StoreRegistry.test.js.map +1 -0
- package/dist/experimental/multi-store/StoreRegistryContext.d.ts +10 -0
- package/dist/experimental/multi-store/StoreRegistryContext.d.ts.map +1 -0
- package/dist/experimental/multi-store/StoreRegistryContext.js +15 -0
- package/dist/experimental/multi-store/StoreRegistryContext.js.map +1 -0
- package/dist/experimental/multi-store/mod.d.ts +6 -0
- package/dist/experimental/multi-store/mod.d.ts.map +1 -0
- package/dist/experimental/multi-store/mod.js +6 -0
- package/dist/experimental/multi-store/mod.js.map +1 -0
- package/dist/experimental/multi-store/storeOptions.d.ts +4 -0
- package/dist/experimental/multi-store/storeOptions.d.ts.map +1 -0
- package/dist/experimental/multi-store/storeOptions.js +4 -0
- package/dist/experimental/multi-store/storeOptions.js.map +1 -0
- package/dist/experimental/multi-store/types.d.ts +25 -0
- package/dist/experimental/multi-store/types.d.ts.map +1 -0
- package/dist/experimental/multi-store/types.js +2 -0
- package/dist/experimental/multi-store/types.js.map +1 -0
- package/dist/experimental/multi-store/useStore.d.ts +11 -0
- package/dist/experimental/multi-store/useStore.d.ts.map +1 -0
- package/dist/experimental/multi-store/useStore.js +16 -0
- package/dist/experimental/multi-store/useStore.js.map +1 -0
- package/dist/experimental/multi-store/useStore.test.d.ts +2 -0
- package/dist/experimental/multi-store/useStore.test.d.ts.map +1 -0
- package/dist/experimental/multi-store/useStore.test.js +198 -0
- package/dist/experimental/multi-store/useStore.test.js.map +1 -0
- package/dist/mod.d.ts +1 -1
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js.map +1 -1
- package/dist/useClientDocument.d.ts +43 -13
- package/dist/useClientDocument.d.ts.map +1 -1
- package/dist/useClientDocument.js +4 -5
- package/dist/useClientDocument.js.map +1 -1
- package/dist/useClientDocument.test.js +29 -7
- package/dist/useClientDocument.test.js.map +1 -1
- package/dist/useQuery.d.ts +28 -6
- package/dist/useQuery.d.ts.map +1 -1
- package/dist/useQuery.js +63 -18
- package/dist/useQuery.js.map +1 -1
- package/dist/useQuery.test.js +35 -11
- package/dist/useQuery.test.js.map +1 -1
- package/dist/useRcResource.test.js +1 -1
- package/dist/useStore.d.ts +53 -1
- package/dist/useStore.d.ts.map +1 -1
- package/dist/useStore.js +52 -1
- package/dist/useStore.js.map +1 -1
- package/package.json +14 -14
- package/src/LiveStoreContext.ts +27 -0
- package/src/LiveStoreProvider.test.tsx +7 -7
- package/src/LiveStoreProvider.tsx +67 -45
- package/src/__snapshots__/useClientDocument.test.tsx.snap +208 -100
- package/src/__snapshots__/useQuery.test.tsx.snap +400 -128
- package/src/__tests__/fixture.tsx +23 -24
- package/src/experimental/components/LiveList.tsx +1 -1
- package/src/experimental/mod.ts +1 -0
- package/src/experimental/multi-store/StoreRegistry.test.ts +518 -0
- package/src/experimental/multi-store/StoreRegistry.ts +253 -0
- package/src/experimental/multi-store/StoreRegistryContext.tsx +23 -0
- package/src/experimental/multi-store/mod.ts +5 -0
- package/src/experimental/multi-store/storeOptions.ts +8 -0
- package/src/experimental/multi-store/types.ts +37 -0
- package/src/experimental/multi-store/useStore.test.tsx +269 -0
- package/src/experimental/multi-store/useStore.ts +26 -0
- package/src/mod.ts +2 -1
- package/src/useClientDocument.test.tsx +105 -75
- package/src/useClientDocument.ts +58 -13
- package/src/useQuery.test.tsx +62 -11
- package/src/useQuery.ts +98 -27
- package/src/useRcResource.test.tsx +1 -1
- package/src/useStore.ts +55 -3
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** biome-ignore-all lint/a11y/useValidAriaRole: not needed for testing */
|
|
2
2
|
/** biome-ignore-all lint/a11y/noStaticElementInteractions: not needed for testing */
|
|
3
3
|
import * as LiveStore from '@livestore/livestore'
|
|
4
|
-
import {
|
|
4
|
+
import { StoreInternalsSymbol } from '@livestore/livestore'
|
|
5
|
+
import { getAllSimplifiedRootSpans, getSimplifiedRootSpan } from '@livestore/livestore/internal/testing-utils'
|
|
5
6
|
import { Effect, ReadonlyRecord, Schema } from '@livestore/utils/effect'
|
|
6
7
|
import { Vitest } from '@livestore/utils-dev/node-vitest'
|
|
7
8
|
import * as otel from '@opentelemetry/api'
|
|
@@ -10,9 +11,9 @@ import * as ReactTesting from '@testing-library/react'
|
|
|
10
11
|
import type React from 'react'
|
|
11
12
|
import { beforeEach, expect, it } from 'vitest'
|
|
12
13
|
|
|
13
|
-
import { events, makeTodoMvcReact, tables } from './__tests__/fixture.
|
|
14
|
-
import type * as LiveStoreReact from './mod.
|
|
15
|
-
import { __resetUseRcResourceCache } from './useRcResource.
|
|
14
|
+
import { events, makeTodoMvcReact, tables } from './__tests__/fixture.tsx'
|
|
15
|
+
import type * as LiveStoreReact from './mod.ts'
|
|
16
|
+
import { __resetUseRcResourceCache } from './useRcResource.ts'
|
|
16
17
|
|
|
17
18
|
// const strictMode = process.env.REACT_STRICT_MODE !== undefined
|
|
18
19
|
|
|
@@ -39,12 +40,12 @@ Vitest.describe('useClientDocument', () => {
|
|
|
39
40
|
expect(result.current.id).toBe('u1')
|
|
40
41
|
expect(result.current.state.username).toBe('')
|
|
41
42
|
expect(renderCount.val).toBe(1)
|
|
42
|
-
expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
43
|
+
expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
43
44
|
store.commit(tables.userInfo.set({ username: 'username_u2' }, 'u2'))
|
|
44
45
|
|
|
45
46
|
rerender('u2')
|
|
46
47
|
|
|
47
|
-
expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
48
|
+
expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
48
49
|
expect(result.current.id).toBe('u2')
|
|
49
50
|
expect(result.current.state.username).toBe('username_u2')
|
|
50
51
|
expect(renderCount.val).toBe(2)
|
|
@@ -224,81 +225,110 @@ Vitest.describe('useClientDocument', () => {
|
|
|
224
225
|
}),
|
|
225
226
|
)
|
|
226
227
|
|
|
227
|
-
Vitest.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
async ({ strictMode }) => {
|
|
231
|
-
const exporter = new InMemorySpanExporter()
|
|
228
|
+
Vitest.scopedLive('kv client document overwrites value (Schema.Any, no partial merge)', () =>
|
|
229
|
+
Effect.gen(function* () {
|
|
230
|
+
const { wrapper, store, renderCount } = yield* makeTodoMvcReact({})
|
|
232
231
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
232
|
+
const { result } = ReactTesting.renderHook(
|
|
233
|
+
(id: string) => {
|
|
234
|
+
renderCount.inc()
|
|
235
|
+
|
|
236
|
+
const [state, setState] = store.useClientDocument(tables.kv, id)
|
|
237
|
+
return { state, setState, id }
|
|
238
|
+
},
|
|
239
|
+
{ wrapper, initialProps: 'k1' },
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
expect(result.current.id).toBe('k1')
|
|
243
|
+
expect(result.current.state).toBe(null)
|
|
244
|
+
expect(renderCount.val).toBe(1)
|
|
236
245
|
|
|
237
|
-
|
|
246
|
+
ReactTesting.act(() => result.current.setState(1))
|
|
247
|
+
expect(result.current.state).toEqual(1)
|
|
248
|
+
expect(renderCount.val).toBe(2)
|
|
249
|
+
|
|
250
|
+
ReactTesting.act(() => result.current.setState({ b: 2 }))
|
|
251
|
+
expect(result.current.state).toEqual({ b: 2 })
|
|
252
|
+
expect(renderCount.val).toBe(3)
|
|
253
|
+
}),
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
Vitest.describe('otel', () => {
|
|
257
|
+
it.each([
|
|
258
|
+
{ strictMode: true },
|
|
259
|
+
{ strictMode: false },
|
|
260
|
+
])('should update the data based on component key strictMode=%s', async ({ strictMode }) => {
|
|
261
|
+
const exporter = new InMemorySpanExporter()
|
|
262
|
+
|
|
263
|
+
const provider = new BasicTracerProvider({
|
|
264
|
+
spanProcessors: [new SimpleSpanProcessor(exporter)],
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
const otelTracer = provider.getTracer(`testing-${strictMode ? 'strict' : 'non-strict'}`)
|
|
268
|
+
|
|
269
|
+
const span = otelTracer.startSpan('test-root')
|
|
270
|
+
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
271
|
+
|
|
272
|
+
await Effect.gen(function* () {
|
|
273
|
+
const { wrapper, store, renderCount } = yield* makeTodoMvcReact({
|
|
274
|
+
otelContext,
|
|
275
|
+
otelTracer,
|
|
276
|
+
strictMode,
|
|
277
|
+
})
|
|
238
278
|
|
|
239
|
-
const
|
|
240
|
-
|
|
279
|
+
const { result, rerender, unmount } = ReactTesting.renderHook(
|
|
280
|
+
(userId: string) => {
|
|
281
|
+
renderCount.inc()
|
|
241
282
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
})
|
|
283
|
+
const [state, setState, id] = store.useClientDocument(tables.userInfo, userId)
|
|
284
|
+
return { state, setState, id }
|
|
285
|
+
},
|
|
286
|
+
{ wrapper, initialProps: 'u1' },
|
|
287
|
+
)
|
|
248
288
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
289
|
+
expect(result.current.id).toBe('u1')
|
|
290
|
+
expect(result.current.state.username).toBe('')
|
|
291
|
+
expect(renderCount.val).toBe(1)
|
|
292
|
+
|
|
293
|
+
// For u2 we'll make sure that the row already exists,
|
|
294
|
+
// so the lazy `insert` will be skipped
|
|
295
|
+
ReactTesting.act(() => store.commit(events.UserInfoSet({ username: 'username_u2' }, 'u2')))
|
|
296
|
+
|
|
297
|
+
rerender('u2')
|
|
298
|
+
|
|
299
|
+
expect(result.current.id).toBe('u2')
|
|
300
|
+
expect(result.current.state.username).toBe('username_u2')
|
|
301
|
+
expect(renderCount.val).toBe(2)
|
|
302
|
+
|
|
303
|
+
unmount()
|
|
304
|
+
span.end()
|
|
305
|
+
}).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise)
|
|
306
|
+
|
|
307
|
+
await provider.forceFlush()
|
|
308
|
+
|
|
309
|
+
const mapAttributes = (attributes: otel.Attributes) => {
|
|
310
|
+
return ReadonlyRecord.map(attributes, (val, key) => {
|
|
311
|
+
if (key === 'code.stacktrace') {
|
|
312
|
+
return '<STACKTRACE>'
|
|
313
|
+
} else if (key === 'firstStackInfo') {
|
|
314
|
+
const stackInfo = JSON.parse(val as string) as LiveStore.StackInfo
|
|
315
|
+
// stackInfo.frames.shift() // Removes `renderHook.wrapper` from the stack
|
|
316
|
+
stackInfo.frames.forEach((_) => {
|
|
317
|
+
if (_.name.includes('renderHook.wrapper')) {
|
|
318
|
+
_.name = 'renderHook.wrapper'
|
|
319
|
+
}
|
|
320
|
+
_.filePath = '__REPLACED_FOR_SNAPSHOT__'
|
|
321
|
+
})
|
|
322
|
+
return JSON.stringify(stackInfo)
|
|
323
|
+
}
|
|
324
|
+
return val
|
|
325
|
+
})
|
|
326
|
+
}
|
|
252
327
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
},
|
|
256
|
-
{ wrapper, initialProps: 'u1' },
|
|
257
|
-
)
|
|
328
|
+
expect(getSimplifiedRootSpan(exporter, 'createStore', mapAttributes)).toMatchSnapshot()
|
|
329
|
+
expect(getAllSimplifiedRootSpans(exporter, 'LiveStore:commit', mapAttributes)).toMatchSnapshot()
|
|
258
330
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
expect(renderCount.val).toBe(1)
|
|
262
|
-
|
|
263
|
-
// For u2 we'll make sure that the row already exists,
|
|
264
|
-
// so the lazy `insert` will be skipped
|
|
265
|
-
ReactTesting.act(() => store.commit(events.UserInfoSet({ username: 'username_u2' }, 'u2')))
|
|
266
|
-
|
|
267
|
-
rerender('u2')
|
|
268
|
-
|
|
269
|
-
expect(result.current.id).toBe('u2')
|
|
270
|
-
expect(result.current.state.username).toBe('username_u2')
|
|
271
|
-
expect(renderCount.val).toBe(2)
|
|
272
|
-
|
|
273
|
-
unmount()
|
|
274
|
-
span.end()
|
|
275
|
-
}).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise)
|
|
276
|
-
|
|
277
|
-
await provider.forceFlush()
|
|
278
|
-
|
|
279
|
-
const mapAttributes = (attributes: otel.Attributes) => {
|
|
280
|
-
return ReadonlyRecord.map(attributes, (val, key) => {
|
|
281
|
-
if (key === 'code.stacktrace') {
|
|
282
|
-
return '<STACKTRACE>'
|
|
283
|
-
} else if (key === 'firstStackInfo') {
|
|
284
|
-
const stackInfo = JSON.parse(val as string) as LiveStore.StackInfo
|
|
285
|
-
// stackInfo.frames.shift() // Removes `renderHook.wrapper` from the stack
|
|
286
|
-
stackInfo.frames.forEach((_) => {
|
|
287
|
-
if (_.name.includes('renderHook.wrapper')) {
|
|
288
|
-
_.name = 'renderHook.wrapper'
|
|
289
|
-
}
|
|
290
|
-
_.filePath = '__REPLACED_FOR_SNAPSHOT__'
|
|
291
|
-
})
|
|
292
|
-
return JSON.stringify(stackInfo)
|
|
293
|
-
}
|
|
294
|
-
return val
|
|
295
|
-
})
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
expect(getSimplifiedRootSpan(exporter, mapAttributes)).toMatchSnapshot()
|
|
299
|
-
|
|
300
|
-
await provider.shutdown()
|
|
301
|
-
},
|
|
302
|
-
)
|
|
331
|
+
await provider.shutdown()
|
|
332
|
+
})
|
|
303
333
|
})
|
|
304
334
|
})
|
package/src/useClientDocument.ts
CHANGED
|
@@ -3,13 +3,24 @@ import { SessionIdSymbol } from '@livestore/common'
|
|
|
3
3
|
import { State } from '@livestore/common/schema'
|
|
4
4
|
import type { LiveQuery, LiveQueryDef, Store } from '@livestore/livestore'
|
|
5
5
|
import { queryDb } from '@livestore/livestore'
|
|
6
|
-
import { shouldNeverHappen } from '@livestore/utils'
|
|
6
|
+
import { omitUndefineds, shouldNeverHappen } from '@livestore/utils'
|
|
7
7
|
import React from 'react'
|
|
8
8
|
|
|
9
9
|
import { LiveStoreContext } from './LiveStoreContext.ts'
|
|
10
10
|
import { useQueryRef } from './useQuery.ts'
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Return type of `useClientDocument` hook.
|
|
14
|
+
*
|
|
15
|
+
* A tuple providing React-style state access to a client-document table row:
|
|
16
|
+
* - `[0]` row: The current value (decoded according to the table schema)
|
|
17
|
+
* - `[1]` setRow: Setter function to update the document
|
|
18
|
+
* - `[2]` id: The document's ID (resolved from `SessionIdSymbol` if applicable)
|
|
19
|
+
* - `[3]` query$: The underlying `LiveQuery` for advanced use cases
|
|
20
|
+
*
|
|
21
|
+
* @typeParam TTableDef - The client-document table definition type
|
|
22
|
+
*/
|
|
23
|
+
export type UseClientDocumentResult<TTableDef extends State.SQLite.ClientDocumentTableDef.TraitAny> = [
|
|
13
24
|
row: TTableDef['Value'],
|
|
14
25
|
setRow: StateSetters<TTableDef>,
|
|
15
26
|
id: string,
|
|
@@ -54,13 +65,17 @@ export const useClientDocument: {
|
|
|
54
65
|
any,
|
|
55
66
|
any,
|
|
56
67
|
any,
|
|
57
|
-
{
|
|
68
|
+
{
|
|
69
|
+
partialSet: boolean
|
|
70
|
+
/** Default value to use instead of the default value from the table definition */
|
|
71
|
+
default: any
|
|
72
|
+
}
|
|
58
73
|
>,
|
|
59
74
|
>(
|
|
60
75
|
table: TTableDef,
|
|
61
76
|
id?: State.SQLite.ClientDocumentTableDef.DefaultIdType<TTableDef> | SessionIdSymbol,
|
|
62
77
|
options?: Partial<RowQuery.GetOrCreateOptions<TTableDef>>,
|
|
63
|
-
):
|
|
78
|
+
): UseClientDocumentResult<TTableDef>
|
|
64
79
|
|
|
65
80
|
// case: no default id → id arg is required
|
|
66
81
|
<
|
|
@@ -68,20 +83,24 @@ export const useClientDocument: {
|
|
|
68
83
|
any,
|
|
69
84
|
any,
|
|
70
85
|
any,
|
|
71
|
-
{
|
|
86
|
+
{
|
|
87
|
+
partialSet: boolean
|
|
88
|
+
/** Default value to use instead of the default value from the table definition */
|
|
89
|
+
default: any
|
|
90
|
+
}
|
|
72
91
|
>,
|
|
73
92
|
>(
|
|
74
93
|
table: TTableDef,
|
|
75
94
|
// TODO adjust so it works with arbitrary primary keys or unique constraints
|
|
76
95
|
id: State.SQLite.ClientDocumentTableDef.DefaultIdType<TTableDef> | string | SessionIdSymbol,
|
|
77
96
|
options?: Partial<RowQuery.GetOrCreateOptions<TTableDef>>,
|
|
78
|
-
):
|
|
97
|
+
): UseClientDocumentResult<TTableDef>
|
|
79
98
|
} = <TTableDef extends State.SQLite.ClientDocumentTableDef.Any>(
|
|
80
99
|
table: TTableDef,
|
|
81
100
|
idOrOptions?: string | SessionIdSymbol,
|
|
82
101
|
options_?: Partial<RowQuery.GetOrCreateOptions<TTableDef>>,
|
|
83
102
|
storeArg?: { store?: Store },
|
|
84
|
-
):
|
|
103
|
+
): UseClientDocumentResult<TTableDef> => {
|
|
85
104
|
const id =
|
|
86
105
|
typeof idOrOptions === 'string' || idOrOptions === SessionIdSymbol
|
|
87
106
|
? idOrOptions
|
|
@@ -97,14 +116,13 @@ export const useClientDocument: {
|
|
|
97
116
|
const tableName = table.sqliteDef.name
|
|
98
117
|
|
|
99
118
|
const store =
|
|
100
|
-
storeArg?.store ??
|
|
101
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: store is stable
|
|
119
|
+
storeArg?.store ?? // biome-ignore lint/correctness/useHookAtTopLevel: store is stable
|
|
102
120
|
React.useContext(LiveStoreContext)?.store ??
|
|
103
121
|
shouldNeverHappen(`No store provided to useClientDocument`)
|
|
104
122
|
|
|
105
123
|
// console.debug('useClientDocument', tableName, id)
|
|
106
124
|
|
|
107
|
-
const idStr: string = id === SessionIdSymbol ? store.
|
|
125
|
+
const idStr: string = id === SessionIdSymbol ? store.sessionId : id
|
|
108
126
|
|
|
109
127
|
type QueryDef = LiveQueryDef<TTableDef['Value']>
|
|
110
128
|
const queryDef: QueryDef = React.useMemo(
|
|
@@ -117,7 +135,7 @@ export const useClientDocument: {
|
|
|
117
135
|
|
|
118
136
|
const queryRef = useQueryRef(queryDef, {
|
|
119
137
|
otelSpanName: `LiveStore:useClientDocument:${tableName}:${idStr}`,
|
|
120
|
-
store: storeArg?.store,
|
|
138
|
+
...omitUndefineds({ store: storeArg?.store }),
|
|
121
139
|
})
|
|
122
140
|
|
|
123
141
|
const setState = React.useMemo<StateSetters<TTableDef>>(
|
|
@@ -133,11 +151,38 @@ export const useClientDocument: {
|
|
|
133
151
|
return [queryRef.valueRef.current, setState, idStr, queryRef.queryRcRef.value]
|
|
134
152
|
}
|
|
135
153
|
|
|
154
|
+
/**
|
|
155
|
+
* A function that dispatches an action. Mirrors React's `Dispatch` type.
|
|
156
|
+
* @typeParam A - The action type
|
|
157
|
+
*/
|
|
136
158
|
export type Dispatch<A> = (action: A) => void
|
|
137
|
-
export type SetStateAction<S> = Partial<S> | ((previousValue: S) => Partial<S>)
|
|
138
159
|
|
|
160
|
+
/**
|
|
161
|
+
* A state update that can be either a partial value or a function returning a partial value.
|
|
162
|
+
* Used when the client-document table has `partialSet: true`.
|
|
163
|
+
* @typeParam S - The state type
|
|
164
|
+
*/
|
|
165
|
+
export type SetStateActionPartial<S> = Partial<S> | ((previousValue: S) => Partial<S>)
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* A state update that can be either a full value or a function returning a full value.
|
|
169
|
+
* Mirrors React's `SetStateAction` type.
|
|
170
|
+
* @typeParam S - The state type
|
|
171
|
+
*/
|
|
172
|
+
export type SetStateAction<S> = S | ((previousValue: S) => S)
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The setter function type for `useClientDocument`, determined by the table's `partialSet` option.
|
|
176
|
+
*
|
|
177
|
+
* - If `partialSet: false` (default), requires full state replacement
|
|
178
|
+
* - If `partialSet: true`, accepts partial updates merged with existing state
|
|
179
|
+
*
|
|
180
|
+
* @typeParam TTableDef - The client-document table definition type
|
|
181
|
+
*/
|
|
139
182
|
export type StateSetters<TTableDef extends State.SQLite.ClientDocumentTableDef.TraitAny> = Dispatch<
|
|
140
|
-
|
|
183
|
+
TTableDef[State.SQLite.ClientDocumentTableDefSymbol]['options']['partialSet'] extends false
|
|
184
|
+
? SetStateAction<TTableDef['Value']>
|
|
185
|
+
: SetStateActionPartial<TTableDef['Value']>
|
|
141
186
|
>
|
|
142
187
|
|
|
143
188
|
const validateTableOptions = (table: State.SQLite.TableDef<any, any>) => {
|
package/src/useQuery.test.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** biome-ignore-all lint/a11y: test */
|
|
2
2
|
import * as LiveStore from '@livestore/livestore'
|
|
3
|
-
import { queryDb, signal } from '@livestore/livestore'
|
|
3
|
+
import { queryDb, StoreInternalsSymbol, signal } from '@livestore/livestore'
|
|
4
4
|
import { RG } from '@livestore/livestore/internal/testing-utils'
|
|
5
5
|
import { Effect, Schema } from '@livestore/utils/effect'
|
|
6
6
|
import { Vitest } from '@livestore/utils-dev/node-vitest'
|
|
@@ -10,8 +10,8 @@ import React from 'react'
|
|
|
10
10
|
import * as ReactWindow from 'react-window'
|
|
11
11
|
import { expect } from 'vitest'
|
|
12
12
|
|
|
13
|
-
import { events, makeTodoMvcReact, tables } from './__tests__/fixture.
|
|
14
|
-
import { __resetUseRcResourceCache } from './useRcResource.
|
|
13
|
+
import { events, makeTodoMvcReact, tables } from './__tests__/fixture.tsx'
|
|
14
|
+
import { __resetUseRcResourceCache } from './useRcResource.ts'
|
|
15
15
|
|
|
16
16
|
Vitest.describe.each([{ strictMode: true }, { strictMode: false }] as const)(
|
|
17
17
|
'useQuery (strictMode=%s)',
|
|
@@ -38,14 +38,14 @@ Vitest.describe.each([{ strictMode: true }, { strictMode: false }] as const)(
|
|
|
38
38
|
|
|
39
39
|
expect(result.current.length).toBe(0)
|
|
40
40
|
expect(renderCount.val).toBe(1)
|
|
41
|
-
expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
41
|
+
expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
42
42
|
|
|
43
43
|
ReactTesting.act(() => store.commit(events.todoCreated({ id: 't1', text: 'buy milk', completed: false })))
|
|
44
44
|
|
|
45
45
|
expect(result.current.length).toBe(1)
|
|
46
46
|
expect(result.current[0]!.text).toBe('buy milk')
|
|
47
47
|
expect(renderCount.val).toBe(2)
|
|
48
|
-
expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
48
|
+
expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
49
49
|
}),
|
|
50
50
|
)
|
|
51
51
|
|
|
@@ -80,19 +80,25 @@ Vitest.describe.each([{ strictMode: true }, { strictMode: false }] as const)(
|
|
|
80
80
|
|
|
81
81
|
expect(result.current).toBe('buy milk')
|
|
82
82
|
expect(renderCount.val).toBe(1)
|
|
83
|
-
expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot(
|
|
83
|
+
expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot(
|
|
84
|
+
'1: after first render',
|
|
85
|
+
)
|
|
84
86
|
|
|
85
87
|
ReactTesting.act(() => store.commit(events.todoUpdated({ id: 't1', text: 'buy soy milk' })))
|
|
86
88
|
|
|
87
89
|
expect(result.current).toBe('buy soy milk')
|
|
88
90
|
expect(renderCount.val).toBe(2)
|
|
89
|
-
expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot(
|
|
91
|
+
expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot(
|
|
92
|
+
'2: after first commit',
|
|
93
|
+
)
|
|
90
94
|
|
|
91
95
|
rerender('t2')
|
|
92
96
|
|
|
93
97
|
expect(result.current).toBe('buy eggs')
|
|
94
98
|
expect(renderCount.val).toBe(3)
|
|
95
|
-
expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot(
|
|
99
|
+
expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot(
|
|
100
|
+
'3: after forced rerender',
|
|
101
|
+
)
|
|
96
102
|
}),
|
|
97
103
|
)
|
|
98
104
|
|
|
@@ -120,19 +126,19 @@ Vitest.describe.each([{ strictMode: true }, { strictMode: false }] as const)(
|
|
|
120
126
|
|
|
121
127
|
expect(result.current).toBe('buy milk')
|
|
122
128
|
expect(renderCount.val).toBe(1)
|
|
123
|
-
expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
129
|
+
expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
124
130
|
|
|
125
131
|
ReactTesting.act(() => store.commit(events.todoUpdated({ id: 't1', text: 'buy soy milk' })))
|
|
126
132
|
|
|
127
133
|
expect(result.current).toBe('buy soy milk')
|
|
128
134
|
expect(renderCount.val).toBe(2)
|
|
129
|
-
expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
135
|
+
expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
130
136
|
|
|
131
137
|
ReactTesting.act(() => store.setSignal(filter$, 't2'))
|
|
132
138
|
|
|
133
139
|
expect(result.current).toBe('buy eggs')
|
|
134
140
|
expect(renderCount.val).toBe(3)
|
|
135
|
-
expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
141
|
+
expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
136
142
|
}),
|
|
137
143
|
)
|
|
138
144
|
|
|
@@ -187,5 +193,50 @@ Vitest.describe.each([{ strictMode: true }, { strictMode: false }] as const)(
|
|
|
187
193
|
expect(result.current).toBe(1)
|
|
188
194
|
}),
|
|
189
195
|
)
|
|
196
|
+
|
|
197
|
+
Vitest.scopedLive('supports query builders directly', () =>
|
|
198
|
+
Effect.gen(function* () {
|
|
199
|
+
const { wrapper, store } = yield* makeTodoMvcReact({ strictMode })
|
|
200
|
+
|
|
201
|
+
store.commit(
|
|
202
|
+
events.todoCreated({ id: 't1', text: 'buy milk', completed: false }),
|
|
203
|
+
events.todoCreated({ id: 't2', text: 'buy eggs', completed: true }),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
const todosWhereIncomplete = tables.todos.where({ completed: false })
|
|
207
|
+
|
|
208
|
+
const { result } = ReactTesting.renderHook(() => store.useQuery(todosWhereIncomplete).map((todo) => todo.id), {
|
|
209
|
+
wrapper,
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
expect(result.current).toEqual(['t1'])
|
|
213
|
+
}),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
Vitest.scopedLive('union of different result types with useQuery', () =>
|
|
217
|
+
Effect.gen(function* () {
|
|
218
|
+
const { wrapper, store, renderCount } = yield* makeTodoMvcReact({ strictMode })
|
|
219
|
+
|
|
220
|
+
const str$ = signal('hello', { label: 'str' })
|
|
221
|
+
const num$ = signal(123, { label: 'num' })
|
|
222
|
+
|
|
223
|
+
const { result, rerender } = ReactTesting.renderHook(
|
|
224
|
+
(useNum: boolean) => {
|
|
225
|
+
renderCount.inc()
|
|
226
|
+
const query$ = React.useMemo(() => (useNum ? num$ : str$), [useNum])
|
|
227
|
+
return store.useQuery(query$)
|
|
228
|
+
},
|
|
229
|
+
{ wrapper, initialProps: false },
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
expect(result.current).toBe('hello')
|
|
233
|
+
expect(renderCount.val).toBe(1)
|
|
234
|
+
|
|
235
|
+
rerender(true)
|
|
236
|
+
|
|
237
|
+
expect(result.current).toBe(123)
|
|
238
|
+
expect(renderCount.val).toBe(2)
|
|
239
|
+
}),
|
|
240
|
+
)
|
|
190
241
|
},
|
|
191
242
|
)
|