@livestore/react 0.4.0-dev.22 → 0.4.0-dev.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/StoreRegistryContext.d.ts +1 -1
- package/dist/StoreRegistryContext.d.ts.map +1 -1
- package/dist/StoreRegistryContext.js +2 -2
- package/dist/StoreRegistryContext.js.map +1 -1
- package/dist/__tests__/fixture.d.ts +8 -280
- package/dist/__tests__/fixture.d.ts.map +1 -1
- package/dist/__tests__/fixture.js +8 -78
- package/dist/__tests__/fixture.js.map +1 -1
- package/dist/experimental/components/LiveList.d.ts.map +1 -1
- package/dist/experimental/components/LiveList.js +5 -4
- package/dist/experimental/components/LiveList.js.map +1 -1
- package/dist/mod.d.ts +4 -2
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +3 -2
- package/dist/mod.js.map +1 -1
- package/dist/useClientDocument.d.ts +1 -26
- package/dist/useClientDocument.d.ts.map +1 -1
- package/dist/useClientDocument.js +2 -13
- package/dist/useClientDocument.js.map +1 -1
- package/dist/useClientDocument.test.js +12 -4
- package/dist/useClientDocument.test.js.map +1 -1
- package/dist/useQuery.d.ts +3 -4
- package/dist/useQuery.d.ts.map +1 -1
- package/dist/useQuery.js +10 -80
- package/dist/useQuery.js.map +1 -1
- package/dist/useQuery.test.js +7 -8
- package/dist/useQuery.test.js.map +1 -1
- package/dist/useRcResource.d.ts.map +1 -1
- package/dist/useRcResource.js +9 -5
- package/dist/useRcResource.js.map +1 -1
- package/dist/useRcResource.test.js +1 -1
- package/dist/useRcResource.test.js.map +1 -1
- package/dist/useStore.d.ts +12 -1
- package/dist/useStore.d.ts.map +1 -1
- package/dist/useStore.js +21 -13
- package/dist/useStore.js.map +1 -1
- package/dist/useStore.test.js +53 -8
- package/dist/useStore.test.js.map +1 -1
- package/dist/useSyncStatus.d.ts +22 -0
- package/dist/useSyncStatus.d.ts.map +1 -0
- package/dist/useSyncStatus.js +28 -0
- package/dist/useSyncStatus.js.map +1 -0
- package/package.json +68 -25
- package/src/StoreRegistryContext.tsx +4 -3
- package/src/__snapshots__/useClientDocument.test.tsx.snap +112 -78
- package/src/__tests__/fixture.tsx +22 -105
- package/src/experimental/components/LiveList.tsx +9 -5
- package/src/mod.ts +4 -9
- package/src/useClientDocument.test.tsx +16 -6
- package/src/useClientDocument.ts +6 -56
- package/src/useQuery.test.tsx +8 -8
- package/src/useQuery.ts +28 -113
- package/src/useRcResource.test.tsx +1 -1
- package/src/useRcResource.ts +10 -5
- package/src/useStore.test.tsx +85 -9
- package/src/useStore.ts +30 -17
- package/src/useSyncStatus.ts +34 -0
- package/dist/utils/stack-info.d.ts +0 -4
- package/dist/utils/stack-info.d.ts.map +0 -1
- package/dist/utils/stack-info.js +0 -10
- package/dist/utils/stack-info.js.map +0 -1
- package/src/ambient.d.ts +0 -1
- package/src/utils/stack-info.ts +0 -13
|
@@ -1,110 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import type { UnknownError } from '@livestore/common'
|
|
2
|
+
import {
|
|
3
|
+
type AppState,
|
|
4
|
+
type CreateTodoMvcStoreOptions,
|
|
5
|
+
createTodoMvcStore,
|
|
6
|
+
events,
|
|
7
|
+
type Filter,
|
|
8
|
+
schema,
|
|
9
|
+
type Todo,
|
|
10
|
+
tables,
|
|
11
|
+
} from '@livestore/framework-toolkit/testing'
|
|
12
|
+
import type { Store } from '@livestore/livestore'
|
|
13
|
+
import { Effect, type Scope } from '@livestore/utils/effect'
|
|
9
14
|
import React from 'react'
|
|
10
15
|
|
|
11
16
|
import * as LiveStoreReact from '../mod.ts'
|
|
12
17
|
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
completed: boolean
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export type Filter = 'all' | 'active' | 'completed'
|
|
20
|
-
|
|
21
|
-
export type AppState = {
|
|
22
|
-
newTodoText: string
|
|
23
|
-
filter: Filter
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const todos = State.SQLite.table({
|
|
27
|
-
name: 'todos',
|
|
28
|
-
columns: {
|
|
29
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
30
|
-
text: State.SQLite.text({ default: '', nullable: false }),
|
|
31
|
-
completed: State.SQLite.boolean({ default: false, nullable: false }),
|
|
32
|
-
},
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
const app = State.SQLite.table({
|
|
36
|
-
name: 'app',
|
|
37
|
-
columns: {
|
|
38
|
-
id: State.SQLite.text({ primaryKey: true, default: 'static' }),
|
|
39
|
-
newTodoText: State.SQLite.text({ default: '', nullable: true }),
|
|
40
|
-
filter: State.SQLite.text({ default: 'all', nullable: false }),
|
|
41
|
-
},
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
const userInfo = State.SQLite.clientDocument({
|
|
45
|
-
name: 'UserInfo',
|
|
46
|
-
schema: Schema.Struct({
|
|
47
|
-
username: Schema.String,
|
|
48
|
-
text: Schema.String,
|
|
49
|
-
}),
|
|
50
|
-
default: { value: { username: '', text: '' } },
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
const AppRouterSchema = State.SQLite.clientDocument({
|
|
54
|
-
name: 'AppRouter',
|
|
55
|
-
schema: Schema.Struct({
|
|
56
|
-
currentTaskId: Schema.String.pipe(Schema.NullOr),
|
|
57
|
-
}),
|
|
58
|
-
default: {
|
|
59
|
-
value: { currentTaskId: null },
|
|
60
|
-
id: 'singleton',
|
|
61
|
-
},
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
const kv = State.SQLite.clientDocument({
|
|
65
|
-
name: 'Kv',
|
|
66
|
-
schema: Schema.Any,
|
|
67
|
-
default: { value: null },
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
export const events = {
|
|
71
|
-
todoCreated: Events.synced({
|
|
72
|
-
name: 'todoCreated',
|
|
73
|
-
schema: Schema.Struct({ id: Schema.String, text: Schema.String, completed: Schema.Boolean }),
|
|
74
|
-
}),
|
|
75
|
-
todoUpdated: Events.synced({
|
|
76
|
-
name: 'todoUpdated',
|
|
77
|
-
schema: Schema.Struct({
|
|
78
|
-
id: Schema.String,
|
|
79
|
-
text: Schema.String.pipe(Schema.optional),
|
|
80
|
-
completed: Schema.Boolean.pipe(Schema.optional),
|
|
81
|
-
}),
|
|
82
|
-
}),
|
|
83
|
-
AppRouterSet: AppRouterSchema.set,
|
|
84
|
-
UserInfoSet: userInfo.set,
|
|
85
|
-
KvSet: kv.set,
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const materializers = State.SQLite.materializers(events, {
|
|
89
|
-
todoCreated: ({ id, text, completed }) => todos.insert({ id, text, completed }),
|
|
90
|
-
todoUpdated: ({ id, text, completed }) => todos.update({ ...omitUndefineds({ completed, text }) }).where({ id }),
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
export const tables = { todos, app, userInfo, AppRouterSchema, kv }
|
|
94
|
-
|
|
95
|
-
const state = State.SQLite.makeState({ tables, materializers })
|
|
96
|
-
export const schema = makeSchema({ state, events })
|
|
18
|
+
// Re-export shared types and schema
|
|
19
|
+
export { events, schema, tables }
|
|
20
|
+
export type { AppState, Filter, Todo }
|
|
97
21
|
|
|
98
|
-
export type MakeTodoMvcReactOptions = {
|
|
99
|
-
otelTracer?: otel.Tracer | undefined
|
|
100
|
-
otelContext?: otel.Context | undefined
|
|
22
|
+
export type MakeTodoMvcReactOptions = CreateTodoMvcStoreOptions & {
|
|
101
23
|
strictMode?: boolean | undefined
|
|
102
24
|
}
|
|
103
25
|
|
|
104
26
|
export const makeTodoMvcReact: (opts?: MakeTodoMvcReactOptions) => Effect.Effect<
|
|
105
27
|
{
|
|
106
28
|
wrapper: ({ children }: any) => React.JSX.Element
|
|
107
|
-
store: Store<
|
|
29
|
+
store: Store<typeof schema> & LiveStoreReact.ReactApi
|
|
108
30
|
renderCount: { readonly val: number; inc: () => void }
|
|
109
31
|
},
|
|
110
32
|
UnknownError,
|
|
@@ -116,7 +38,7 @@ export const makeTodoMvcReact: (opts?: MakeTodoMvcReactOptions) => Effect.Effect
|
|
|
116
38
|
let val = 0
|
|
117
39
|
|
|
118
40
|
const inc = () => {
|
|
119
|
-
val += strictMode ? 0.5 : 1
|
|
41
|
+
val += strictMode === true ? 0.5 : 1
|
|
120
42
|
}
|
|
121
43
|
|
|
122
44
|
return {
|
|
@@ -127,20 +49,15 @@ export const makeTodoMvcReact: (opts?: MakeTodoMvcReactOptions) => Effect.Effect
|
|
|
127
49
|
}
|
|
128
50
|
}
|
|
129
51
|
|
|
130
|
-
const store
|
|
131
|
-
schema,
|
|
132
|
-
storeId: 'default',
|
|
133
|
-
adapter: makeInMemoryAdapter(),
|
|
134
|
-
debug: { instanceId: 'test' },
|
|
135
|
-
})
|
|
52
|
+
const store = yield* createTodoMvcStore(opts)
|
|
136
53
|
|
|
137
54
|
const storeWithReactApi = LiveStoreReact.withReactApi(store)
|
|
138
55
|
|
|
139
|
-
const MaybeStrictMode = strictMode ? React.StrictMode : React.Fragment
|
|
56
|
+
const MaybeStrictMode = strictMode === true ? React.StrictMode : React.Fragment
|
|
140
57
|
|
|
141
58
|
const wrapper = ({ children }: any) => <MaybeStrictMode>{children}</MaybeStrictMode>
|
|
142
59
|
|
|
143
60
|
const renderCount = makeRenderCount()
|
|
144
61
|
|
|
145
62
|
return { wrapper, store: storeWithReactApi, renderCount }
|
|
146
|
-
})
|
|
63
|
+
})
|
|
@@ -60,7 +60,8 @@ export const LiveList = <TItem,>({ items$, renderItem, getKey, store }: LiveList
|
|
|
60
60
|
itemKey={key}
|
|
61
61
|
item$={item$}
|
|
62
62
|
store={store}
|
|
63
|
-
|
|
63
|
+
index={index}
|
|
64
|
+
isInitialListRender={!hasMounted}
|
|
64
65
|
renderItem={renderItem}
|
|
65
66
|
/>
|
|
66
67
|
))}
|
|
@@ -70,17 +71,20 @@ export const LiveList = <TItem,>({ items$, renderItem, getKey, store }: LiveList
|
|
|
70
71
|
|
|
71
72
|
const ItemWrapper = <TItem,>({
|
|
72
73
|
item$,
|
|
73
|
-
|
|
74
|
+
index,
|
|
75
|
+
isInitialListRender,
|
|
74
76
|
renderItem,
|
|
75
77
|
store,
|
|
76
78
|
}: {
|
|
77
79
|
itemKey: string | number
|
|
78
80
|
item$: LiveQueryDef<TItem>
|
|
79
|
-
|
|
81
|
+
index: number
|
|
82
|
+
isInitialListRender: boolean
|
|
80
83
|
renderItem: (item: TItem, opts: { index: number; isInitialListRender: boolean }) => React.ReactNode
|
|
81
84
|
store: Store<any, any>
|
|
82
85
|
}) => {
|
|
83
86
|
const item = useQuery(item$, { store })
|
|
87
|
+
const opts = React.useMemo(() => ({ index, isInitialListRender }), [index, isInitialListRender])
|
|
84
88
|
|
|
85
89
|
return <>{renderItem(item, opts)}</>
|
|
86
90
|
}
|
|
@@ -91,6 +95,6 @@ const ItemWrapperMemo = React.memo(
|
|
|
91
95
|
prev.itemKey === next.itemKey &&
|
|
92
96
|
prev.renderItem === next.renderItem &&
|
|
93
97
|
prev.store === next.store &&
|
|
94
|
-
prev.
|
|
95
|
-
prev.
|
|
98
|
+
prev.index === next.index &&
|
|
99
|
+
prev.isInitialListRender === next.isInitialListRender,
|
|
96
100
|
) as typeof ItemWrapper
|
package/src/mod.ts
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
|
+
export type { Dispatch, SetStateAction, SetStateActionPartial, StateSetters } from '@livestore/framework-toolkit'
|
|
2
|
+
export { captureStackInfo } from '@livestore/framework-toolkit'
|
|
1
3
|
export { StoreRegistry, storeOptions } from '@livestore/livestore'
|
|
2
4
|
export { LiveList, type LiveListProps } from './experimental/components/LiveList.tsx'
|
|
3
5
|
export * from './StoreRegistryContext.tsx'
|
|
4
|
-
export {
|
|
5
|
-
type Dispatch,
|
|
6
|
-
type SetStateAction,
|
|
7
|
-
type SetStateActionPartial,
|
|
8
|
-
type StateSetters,
|
|
9
|
-
type UseClientDocumentResult,
|
|
10
|
-
useClientDocument,
|
|
11
|
-
} from './useClientDocument.ts'
|
|
6
|
+
export { type UseClientDocumentResult, useClientDocument } from './useClientDocument.ts'
|
|
12
7
|
export { useQuery, useQueryRef } from './useQuery.ts'
|
|
13
8
|
export { type ReactApi, useStore, withReactApi } from './useStore.ts'
|
|
14
|
-
export {
|
|
9
|
+
export { useSyncStatus } from './useSyncStatus.ts'
|
|
@@ -8,7 +8,7 @@ import { Vitest } from '@livestore/utils-dev/node-vitest'
|
|
|
8
8
|
import * as otel from '@opentelemetry/api'
|
|
9
9
|
import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
|
|
10
10
|
import * as ReactTesting from '@testing-library/react'
|
|
11
|
-
import
|
|
11
|
+
import * as React from 'react'
|
|
12
12
|
import { beforeEach, expect, it } from 'vitest'
|
|
13
13
|
|
|
14
14
|
import { events, makeTodoMvcReact, tables } from './__tests__/fixture.tsx'
|
|
@@ -120,25 +120,35 @@ Vitest.describe('useClientDocument', () => {
|
|
|
120
120
|
renderCount.inc()
|
|
121
121
|
|
|
122
122
|
const [state, setState] = store.useClientDocument(tables.AppRouterSchema, 'singleton')
|
|
123
|
+
const setCurrentTaskId = React.useCallback((taskId: string) => setState({ currentTaskId: taskId }), [setState])
|
|
123
124
|
|
|
124
125
|
globalSetState = setState
|
|
125
126
|
|
|
126
127
|
return (
|
|
127
128
|
<div>
|
|
128
|
-
<TasksList setTaskId={
|
|
129
|
+
<TasksList setTaskId={setCurrentTaskId} />
|
|
129
130
|
<div role="current-id">Current Task Id: {state.currentTaskId ?? '-'}</div>
|
|
130
|
-
{state.currentTaskId ? <TaskDetails id={state.currentTaskId} /> : <div>Click on a task to see details</div>}
|
|
131
|
+
{state.currentTaskId !== null ? <TaskDetails id={state.currentTaskId} /> : <div>Click on a task to see details</div>}
|
|
131
132
|
</div>
|
|
132
133
|
)
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
const TasksList: React.FC<{ setTaskId: (_: string) => void }> = ({ setTaskId }) => {
|
|
136
137
|
const allTodos = store.useQuery(allTodos$)
|
|
138
|
+
const handleTaskClick = React.useCallback(
|
|
139
|
+
(event: React.MouseEvent<HTMLDivElement>) => {
|
|
140
|
+
const taskId = event.currentTarget.dataset.taskId
|
|
141
|
+
if (taskId !== undefined) {
|
|
142
|
+
setTaskId(taskId)
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
[setTaskId],
|
|
146
|
+
)
|
|
137
147
|
|
|
138
148
|
return (
|
|
139
149
|
<div>
|
|
140
150
|
{allTodos.map((_) => (
|
|
141
|
-
<div key={_.id}
|
|
151
|
+
<div key={_.id} data-task-id={_.id} onClick={handleTaskClick}>
|
|
142
152
|
{_.id}
|
|
143
153
|
</div>
|
|
144
154
|
))}
|
|
@@ -264,7 +274,7 @@ Vitest.describe('useClientDocument', () => {
|
|
|
264
274
|
spanProcessors: [new SimpleSpanProcessor(exporter)],
|
|
265
275
|
})
|
|
266
276
|
|
|
267
|
-
const otelTracer = provider.getTracer(`testing-${strictMode ? 'strict' : 'non-strict'}`)
|
|
277
|
+
const otelTracer = provider.getTracer(`testing-${strictMode !== undefined ? 'strict' : 'non-strict'}`)
|
|
268
278
|
|
|
269
279
|
const span = otelTracer.startSpan('test-root')
|
|
270
280
|
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
@@ -314,7 +324,7 @@ Vitest.describe('useClientDocument', () => {
|
|
|
314
324
|
const stackInfo = JSON.parse(val as string) as LiveStore.StackInfo
|
|
315
325
|
// stackInfo.frames.shift() // Removes `renderHook.wrapper` from the stack
|
|
316
326
|
stackInfo.frames.forEach((_) => {
|
|
317
|
-
if (_.name.includes('renderHook.wrapper')) {
|
|
327
|
+
if (_.name.includes('renderHook.wrapper') === true) {
|
|
318
328
|
_.name = 'renderHook.wrapper'
|
|
319
329
|
}
|
|
320
330
|
_.filePath = '__REPLACED_FOR_SNAPSHOT__'
|
package/src/useClientDocument.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
1
3
|
import type { RowQuery } from '@livestore/common'
|
|
2
4
|
import { SessionIdSymbol } from '@livestore/common'
|
|
3
5
|
import { State } from '@livestore/common/schema'
|
|
6
|
+
import { removeUndefinedValues, type StateSetters, validateTableOptions } from '@livestore/framework-toolkit'
|
|
4
7
|
import type { LiveQuery, LiveQueryDef, Store } from '@livestore/livestore'
|
|
5
8
|
import { queryDb } from '@livestore/livestore'
|
|
6
9
|
import { omitUndefineds, shouldNeverHappen } from '@livestore/utils'
|
|
7
|
-
import React from 'react'
|
|
8
10
|
|
|
9
11
|
import { useQueryRef } from './useQuery.ts'
|
|
10
12
|
|
|
@@ -116,15 +118,13 @@ export const useClientDocument: {
|
|
|
116
118
|
|
|
117
119
|
const store = storeArg?.store ?? shouldNeverHappen(`No store provided to useClientDocument`)
|
|
118
120
|
|
|
119
|
-
// console.debug('useClientDocument', tableName, id)
|
|
120
|
-
|
|
121
121
|
const idStr: string = id === SessionIdSymbol ? store.sessionId : id
|
|
122
122
|
|
|
123
123
|
type QueryDef = LiveQueryDef<TTableDef['Value']>
|
|
124
124
|
const queryDef: QueryDef = React.useMemo(
|
|
125
125
|
() =>
|
|
126
|
-
queryDb(table.get(id
|
|
127
|
-
deps: [idStr
|
|
126
|
+
queryDb(table.get(id, { default: defaultValues! }), {
|
|
127
|
+
deps: [idStr, table.sqliteDef.name, JSON.stringify(defaultValues)],
|
|
128
128
|
}),
|
|
129
129
|
[table, id, defaultValues, idStr],
|
|
130
130
|
)
|
|
@@ -139,60 +139,10 @@ export const useClientDocument: {
|
|
|
139
139
|
const newValue = typeof newValueOrFn === 'function' ? newValueOrFn(queryRef.valueRef.current) : newValueOrFn
|
|
140
140
|
if (queryRef.valueRef.current === newValue) return
|
|
141
141
|
|
|
142
|
-
store.commit(table.set(removeUndefinedValues(newValue), id
|
|
142
|
+
store.commit(table.set(removeUndefinedValues(newValue), id))
|
|
143
143
|
},
|
|
144
144
|
[id, queryRef.valueRef, store, table],
|
|
145
145
|
)
|
|
146
146
|
|
|
147
147
|
return [queryRef.valueRef.current, setState, idStr, queryRef.queryRcRef.value]
|
|
148
148
|
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* A function that dispatches an action. Mirrors React's `Dispatch` type.
|
|
152
|
-
* @typeParam A - The action type
|
|
153
|
-
*/
|
|
154
|
-
export type Dispatch<A> = (action: A) => void
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* A state update that can be either a partial value or a function returning a partial value.
|
|
158
|
-
* Used when the client-document table has `partialSet: true`.
|
|
159
|
-
* @typeParam S - The state type
|
|
160
|
-
*/
|
|
161
|
-
export type SetStateActionPartial<S> = Partial<S> | ((previousValue: S) => Partial<S>)
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* A state update that can be either a full value or a function returning a full value.
|
|
165
|
-
* Mirrors React's `SetStateAction` type.
|
|
166
|
-
* @typeParam S - The state type
|
|
167
|
-
*/
|
|
168
|
-
export type SetStateAction<S> = S | ((previousValue: S) => S)
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* The setter function type for `useClientDocument`, determined by the table's `partialSet` option.
|
|
172
|
-
*
|
|
173
|
-
* - If `partialSet: false` (default), requires full state replacement
|
|
174
|
-
* - If `partialSet: true`, accepts partial updates merged with existing state
|
|
175
|
-
*
|
|
176
|
-
* @typeParam TTableDef - The client-document table definition type
|
|
177
|
-
*/
|
|
178
|
-
export type StateSetters<TTableDef extends State.SQLite.ClientDocumentTableDef.TraitAny> = Dispatch<
|
|
179
|
-
TTableDef[State.SQLite.ClientDocumentTableDefSymbol]['options']['partialSet'] extends false
|
|
180
|
-
? SetStateAction<TTableDef['Value']>
|
|
181
|
-
: SetStateActionPartial<TTableDef['Value']>
|
|
182
|
-
>
|
|
183
|
-
|
|
184
|
-
const validateTableOptions = (table: State.SQLite.TableDef<any, any>) => {
|
|
185
|
-
if (State.SQLite.tableIsClientDocumentTable(table) === false) {
|
|
186
|
-
return shouldNeverHappen(
|
|
187
|
-
`useClientDocument called on table "${table.sqliteDef.name}" which is not a client document table`,
|
|
188
|
-
)
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const removeUndefinedValues = (value: any) => {
|
|
193
|
-
if (typeof value === 'object' && value !== null) {
|
|
194
|
-
return Object.fromEntries(Object.entries(value).filter(([_, v]) => v !== undefined))
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return value
|
|
198
|
-
}
|
package/src/useQuery.test.tsx
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
+
import * as ReactTesting from '@testing-library/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import * as ReactWindow from 'react-window'
|
|
4
|
+
import { expect } from 'vitest'
|
|
5
|
+
|
|
1
6
|
/** biome-ignore-all lint/a11y: test */
|
|
2
7
|
import * as LiveStore from '@livestore/livestore'
|
|
3
8
|
import { queryDb, StoreInternalsSymbol, signal } from '@livestore/livestore'
|
|
4
9
|
import { RG } from '@livestore/livestore/internal/testing-utils'
|
|
5
|
-
import { Effect, Schema } from '@livestore/utils/effect'
|
|
6
10
|
import { Vitest } from '@livestore/utils-dev/node-vitest'
|
|
7
|
-
import
|
|
8
|
-
import React from 'react'
|
|
9
|
-
// @ts-expect-error no types
|
|
10
|
-
import * as ReactWindow from 'react-window'
|
|
11
|
-
import { expect } from 'vitest'
|
|
11
|
+
import { Effect, Schema } from '@livestore/utils/effect'
|
|
12
12
|
|
|
13
13
|
import { events, makeTodoMvcReact, tables } from './__tests__/fixture.tsx'
|
|
14
14
|
import { __resetUseRcResourceCache } from './useRcResource.ts'
|
|
@@ -156,7 +156,7 @@ Vitest.describe.each([{ strictMode: true }, { strictMode: false }] as const)(
|
|
|
156
156
|
width={100}
|
|
157
157
|
itemSize={10}
|
|
158
158
|
itemCount={numItems}
|
|
159
|
-
itemData={Array.from({ length: numItems }, (_, i) => i).
|
|
159
|
+
itemData={Array.from({ length: numItems }, (_, i) => i).toReversed()}
|
|
160
160
|
>
|
|
161
161
|
{ListItem}
|
|
162
162
|
</ReactWindow.FixedSizeList>
|
|
@@ -223,7 +223,7 @@ Vitest.describe.each([{ strictMode: true }, { strictMode: false }] as const)(
|
|
|
223
223
|
const { result, rerender } = ReactTesting.renderHook(
|
|
224
224
|
(useNum: boolean) => {
|
|
225
225
|
renderCount.inc()
|
|
226
|
-
const query$ = React.useMemo(() => (useNum ? num$ : str$), [useNum])
|
|
226
|
+
const query$ = React.useMemo(() => (useNum === true ? num$ : str$), [useNum])
|
|
227
227
|
return store.useQuery(query$)
|
|
228
228
|
},
|
|
229
229
|
{ wrapper, initialProps: false },
|
package/src/useQuery.ts
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import type * as otel from '@opentelemetry/api'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
3
4
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from '@livestore/livestore'
|
|
5
|
+
captureStackInfo,
|
|
6
|
+
computeRcRefKey,
|
|
7
|
+
createQueryResource,
|
|
8
|
+
type NormalizedQueryable,
|
|
9
|
+
normalizeQueryable,
|
|
10
|
+
runInitialQuery,
|
|
11
|
+
} from '@livestore/framework-toolkit'
|
|
12
|
+
import type { LiveQuery, Queryable, Store } from '@livestore/livestore'
|
|
12
13
|
import type { LiveQueries } from '@livestore/livestore/internal'
|
|
13
|
-
import { deepEqual,
|
|
14
|
-
import * as otel from '@opentelemetry/api'
|
|
15
|
-
import React from 'react'
|
|
14
|
+
import { deepEqual, shouldNeverHappen } from '@livestore/utils'
|
|
16
15
|
|
|
17
16
|
import { useRcResource } from './useRcResource.ts'
|
|
18
|
-
import { originalStackLimit } from './utils/stack-info.ts'
|
|
19
17
|
import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInput.ts'
|
|
20
18
|
|
|
21
19
|
/**
|
|
@@ -73,124 +71,42 @@ export const useQueryRef = <TQueryable extends Queryable<any>>(
|
|
|
73
71
|
const store = options?.store ?? shouldNeverHappen(`No store provided to useQuery`)
|
|
74
72
|
|
|
75
73
|
type TResult = Queryable.Result<TQueryable>
|
|
76
|
-
type NormalizedQueryable =
|
|
77
|
-
| { _tag: 'definition'; def: LiveQueryDef<TResult> | SignalDef<TResult> }
|
|
78
|
-
| { _tag: 'live-query'; query$: LiveQuery<TResult> }
|
|
79
|
-
|
|
80
|
-
const normalized = React.useMemo<NormalizedQueryable>(() => {
|
|
81
|
-
if (!isQueryable(queryable)) {
|
|
82
|
-
return shouldNeverHappen('useQuery expected a Queryable value')
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (isQueryBuilder(queryable)) {
|
|
86
|
-
return { _tag: 'definition', def: queryDb(queryable) }
|
|
87
|
-
}
|
|
88
74
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return { _tag: 'definition', def: queryable as LiveQueryDef<TResult> | SignalDef<TResult> }
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return { _tag: 'live-query', query$: queryable as LiveQuery<TResult> }
|
|
97
|
-
}, [queryable])
|
|
98
|
-
|
|
99
|
-
// It's important to use all "aspects" of a store instance here, otherwise we get unexpected cache mappings
|
|
100
|
-
const rcRefKey = React.useMemo(() => {
|
|
101
|
-
const base = `${store.storeId}_${store.clientId}_${store.sessionId}`
|
|
102
|
-
|
|
103
|
-
if (normalized._tag === 'definition') {
|
|
104
|
-
return `${base}:def:${normalized.def.hash}`
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return `${base}:instance:${normalized.query$.id}`
|
|
108
|
-
}, [normalized, store.clientId, store.sessionId, store.storeId])
|
|
75
|
+
const normalized = React.useMemo<NormalizedQueryable<TResult>>(
|
|
76
|
+
() => normalizeQueryable(queryable as Queryable<TResult>),
|
|
77
|
+
[queryable],
|
|
78
|
+
)
|
|
109
79
|
|
|
110
|
-
const
|
|
80
|
+
const rcRefKey = React.useMemo(() => computeRcRefKey(store, normalized), [normalized, store])
|
|
111
81
|
|
|
112
|
-
const stackInfo = React.useMemo(() =>
|
|
113
|
-
Error.stackTraceLimit = 10
|
|
114
|
-
const stack = new Error().stack!
|
|
115
|
-
Error.stackTraceLimit = originalStackLimit
|
|
116
|
-
return extractStackInfoFromStackTrace(stack)
|
|
117
|
-
}, [])
|
|
82
|
+
const stackInfo = React.useMemo(() => captureStackInfo(), [])
|
|
118
83
|
|
|
119
84
|
const { queryRcRef, span, otelContext } = useRcResource(
|
|
120
85
|
rcRefKey,
|
|
121
|
-
() =>
|
|
122
|
-
|
|
123
|
-
options?.otelSpanName
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
129
|
-
|
|
130
|
-
const queryRcRef =
|
|
131
|
-
normalized._tag === 'definition'
|
|
132
|
-
? normalized.def.make(store[StoreInternalsSymbol].reactivityGraph.context!, otelContext)
|
|
133
|
-
: ({
|
|
134
|
-
value: normalized.query$,
|
|
135
|
-
deref: () => {},
|
|
136
|
-
rc: Number.POSITIVE_INFINITY,
|
|
137
|
-
} satisfies LiveQueries.RcRef<LiveQuery<TResult>>)
|
|
138
|
-
|
|
139
|
-
return { queryRcRef, span, otelContext }
|
|
140
|
-
},
|
|
86
|
+
() =>
|
|
87
|
+
createQueryResource(store, normalized, stackInfo, {
|
|
88
|
+
otelSpanName: options?.otelSpanName,
|
|
89
|
+
otelContext: options?.otelContext,
|
|
90
|
+
}),
|
|
141
91
|
// We need to keep the queryRcRef alive a bit longer, so we have a second `useRcResource` below
|
|
142
92
|
// which takes care of disposing the queryRcRef
|
|
143
93
|
() => {},
|
|
144
94
|
)
|
|
145
95
|
|
|
146
|
-
|
|
147
|
-
// const queryRcRef.value.get()
|
|
148
|
-
// }
|
|
149
|
-
|
|
150
|
-
const query$ = queryRcRef.value as LiveQuery<TResult>
|
|
96
|
+
const query$ = queryRcRef.value
|
|
151
97
|
|
|
152
98
|
React.useDebugValue(`LiveStore:useQuery:${query$.id}:${query$.label}`)
|
|
153
|
-
// console.debug(`LiveStore:useQuery:${query$.id}:${query$.label}`)
|
|
154
|
-
|
|
155
|
-
const initialResult = React.useMemo(() => {
|
|
156
|
-
try {
|
|
157
|
-
return query$.run({
|
|
158
|
-
otelContext,
|
|
159
|
-
debugRefreshReason: {
|
|
160
|
-
_tag: 'react',
|
|
161
|
-
api: 'useQuery',
|
|
162
|
-
label: `useQuery:initial-run:${query$.label}`,
|
|
163
|
-
stackInfo,
|
|
164
|
-
},
|
|
165
|
-
})
|
|
166
|
-
} catch (cause: any) {
|
|
167
|
-
console.error('[@livestore/react:useQuery] Error running query', cause)
|
|
168
|
-
throw new Error(
|
|
169
|
-
`\
|
|
170
|
-
[@livestore/react:useQuery] Error running query: ${cause.name}
|
|
171
99
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
${indent(stackInfoToString(stackInfo), 4)}
|
|
177
|
-
|
|
178
|
-
Stack trace:
|
|
179
|
-
`,
|
|
180
|
-
{ cause },
|
|
181
|
-
)
|
|
182
|
-
}
|
|
183
|
-
}, [otelContext, query$, stackInfo])
|
|
100
|
+
const initialResult = React.useMemo(
|
|
101
|
+
() => runInitialQuery(query$, otelContext, stackInfo, 'react'),
|
|
102
|
+
[otelContext, query$, stackInfo],
|
|
103
|
+
)
|
|
184
104
|
|
|
185
105
|
// We know the query has a result by the time we use it; so we can synchronously populate a default state
|
|
186
106
|
const [valueRef, setValue] = useStateRefWithReactiveInput<TResult>(initialResult)
|
|
187
107
|
|
|
188
|
-
// TODO we probably need to change the order of `useEffect` calls, so we destroy the query at the end
|
|
189
|
-
// before calling the LS `onEffect` on it
|
|
190
|
-
|
|
191
108
|
// Subscribe to future updates for this query
|
|
192
109
|
React.useEffect(() => {
|
|
193
|
-
// TODO double check whether we still need `activeSubscriptions`
|
|
194
110
|
query$.activeSubscriptions.add(stackInfo)
|
|
195
111
|
|
|
196
112
|
// Dynamic queries only set their actual label after they've been run the first time,
|
|
@@ -221,7 +137,6 @@ Stack trace:
|
|
|
221
137
|
rcRefKey,
|
|
222
138
|
() => ({ queryRcRef, span }),
|
|
223
139
|
({ queryRcRef, span }) => {
|
|
224
|
-
// console.debug('deref', queryRcRef.value.id, queryRcRef.value.label)
|
|
225
140
|
queryRcRef.deref()
|
|
226
141
|
span.end()
|
|
227
142
|
},
|
|
@@ -9,7 +9,7 @@ describe.each([{ strictMode: true }, { strictMode: false }])('useRcResource (str
|
|
|
9
9
|
__resetUseRcResourceCache()
|
|
10
10
|
})
|
|
11
11
|
|
|
12
|
-
const wrapper = strictMode ? React.StrictMode : React.Fragment
|
|
12
|
+
const wrapper = strictMode === true ? React.StrictMode : React.Fragment
|
|
13
13
|
|
|
14
14
|
it('should create a stateful entity using make and call cleanup on unmount', () => {
|
|
15
15
|
const makeSpy = vi.fn(() => Symbol('statefulResource'))
|
package/src/useRcResource.ts
CHANGED
|
@@ -79,11 +79,16 @@ export const useRcResource = <T>(
|
|
|
79
79
|
): T => {
|
|
80
80
|
const keyRef = React.useRef<string | undefined>(undefined)
|
|
81
81
|
const didDisposeInMemo = React.useRef(false)
|
|
82
|
+
const createRef = React.useRef(create)
|
|
83
|
+
const disposeRef = React.useRef(dispose)
|
|
84
|
+
|
|
85
|
+
createRef.current = create
|
|
86
|
+
disposeRef.current = dispose
|
|
82
87
|
|
|
83
88
|
// biome-ignore lint/correctness/useExhaustiveDependencies: Dependency is deliberately limited to `key` to avoid unintended re-creations.
|
|
84
89
|
const resource = React.useMemo(() => {
|
|
85
90
|
// console.debug('useMemo', key)
|
|
86
|
-
if (didDisposeInMemo.current) {
|
|
91
|
+
if (didDisposeInMemo.current === true) {
|
|
87
92
|
// console.debug('useMemo', key, 'skip')
|
|
88
93
|
const cachedItem = cache.get(key)
|
|
89
94
|
if (cachedItem !== undefined && cachedItem._tag === 'active') {
|
|
@@ -104,7 +109,7 @@ export const useRcResource = <T>(
|
|
|
104
109
|
|
|
105
110
|
if (cachedItemForPreviousKey.rc === 0) {
|
|
106
111
|
// Clean up the stateful resource if no longer referenced
|
|
107
|
-
|
|
112
|
+
disposeRef.current(cachedItemForPreviousKey.resource)
|
|
108
113
|
cache.set(previousKey, { _tag: 'destroyed' })
|
|
109
114
|
didDisposeInMemo.current = true
|
|
110
115
|
}
|
|
@@ -122,7 +127,7 @@ export const useRcResource = <T>(
|
|
|
122
127
|
}
|
|
123
128
|
|
|
124
129
|
// Create a new stateful resource if not cached
|
|
125
|
-
const resource =
|
|
130
|
+
const resource = createRef.current()
|
|
126
131
|
cache.set(key, { _tag: 'active', rc: 1, resource })
|
|
127
132
|
return resource
|
|
128
133
|
}, [key])
|
|
@@ -130,7 +135,7 @@ export const useRcResource = <T>(
|
|
|
130
135
|
// biome-ignore lint/correctness/useExhaustiveDependencies: We assume the `dispose` function is stable and won't change across renders
|
|
131
136
|
React.useEffect(() => {
|
|
132
137
|
return () => {
|
|
133
|
-
if (didDisposeInMemo.current) {
|
|
138
|
+
if (didDisposeInMemo.current === true) {
|
|
134
139
|
// console.debug('unmount', keyRef.current, 'skip')
|
|
135
140
|
didDisposeInMemo.current = false
|
|
136
141
|
return
|
|
@@ -146,7 +151,7 @@ export const useRcResource = <T>(
|
|
|
146
151
|
// console.debug('rc--', cachedItem.rc, ...(_options?.debugPrint?.(cachedItem.resource) ?? []))
|
|
147
152
|
|
|
148
153
|
if (cachedItem.rc === 0) {
|
|
149
|
-
|
|
154
|
+
disposeRef.current(cachedItem.resource)
|
|
150
155
|
cache.delete(key)
|
|
151
156
|
}
|
|
152
157
|
}
|