@livestore/react 0.4.0-dev.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/StoreRegistryContext.d.ts +56 -0
- package/dist/StoreRegistryContext.d.ts.map +1 -0
- package/dist/StoreRegistryContext.js +61 -0
- package/dist/StoreRegistryContext.js.map +1 -0
- package/dist/__tests__/fixture.d.ts +12 -283
- package/dist/__tests__/fixture.d.ts.map +1 -1
- package/dist/__tests__/fixture.js +12 -86
- package/dist/__tests__/fixture.js.map +1 -1
- package/dist/experimental/components/LiveList.d.ts +4 -2
- package/dist/experimental/components/LiveList.d.ts.map +1 -1
- package/dist/experimental/components/LiveList.js +10 -8
- package/dist/experimental/components/LiveList.js.map +1 -1
- package/dist/mod.d.ts +8 -5
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +6 -4
- package/dist/mod.js.map +1 -1
- package/dist/useClientDocument.d.ts +12 -4
- package/dist/useClientDocument.d.ts.map +1 -1
- package/dist/useClientDocument.js +4 -18
- package/dist/useClientDocument.js.map +1 -1
- package/dist/useClientDocument.test.js +21 -9
- package/dist/useClientDocument.test.js.map +1 -1
- package/dist/useQuery.d.ts +29 -8
- package/dist/useQuery.d.ts.map +1 -1
- package/dist/useQuery.js +43 -70
- package/dist/useQuery.js.map +1 -1
- package/dist/useQuery.test.js +57 -13
- package/dist/useQuery.test.js.map +1 -1
- package/dist/useRcResource.d.ts +1 -1
- package/dist/useRcResource.d.ts.map +1 -1
- package/dist/useRcResource.js +41 -34
- package/dist/useRcResource.js.map +1 -1
- package/dist/useRcResource.test.js +72 -50
- package/dist/useRcResource.test.js.map +1 -1
- package/dist/useStore.d.ts +74 -7
- package/dist/useStore.d.ts.map +1 -1
- package/dist/useStore.js +82 -16
- package/dist/useStore.js.map +1 -1
- package/dist/useStore.test.d.ts +2 -0
- package/dist/useStore.test.d.ts.map +1 -0
- package/dist/useStore.test.js +241 -0
- package/dist/useStore.test.js.map +1 -0
- 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 +69 -26
- package/src/StoreRegistryContext.tsx +70 -0
- package/src/__snapshots__/useClientDocument.test.tsx.snap +112 -78
- package/src/__snapshots__/useQuery.test.tsx.snap +12 -12
- package/src/__tests__/fixture.tsx +29 -133
- package/src/experimental/components/LiveList.tsx +23 -10
- package/src/mod.ts +8 -12
- package/src/useClientDocument.test.tsx +90 -79
- package/src/useClientDocument.ts +19 -38
- package/src/useQuery.test.tsx +99 -13
- package/src/useQuery.ts +74 -89
- package/src/useRcResource.test.tsx +115 -59
- package/src/useRcResource.ts +51 -37
- package/src/useStore.test.tsx +347 -0
- package/src/useStore.ts +115 -22
- package/src/useSyncStatus.ts +34 -0
- package/dist/LiveStoreContext.d.ts +0 -13
- package/dist/LiveStoreContext.d.ts.map +0 -1
- package/dist/LiveStoreContext.js +0 -3
- package/dist/LiveStoreContext.js.map +0 -1
- package/dist/LiveStoreProvider.d.ts +0 -65
- package/dist/LiveStoreProvider.d.ts.map +0 -1
- package/dist/LiveStoreProvider.js +0 -221
- package/dist/LiveStoreProvider.js.map +0 -1
- package/dist/LiveStoreProvider.test.d.ts +0 -2
- package/dist/LiveStoreProvider.test.d.ts.map +0 -1
- package/dist/LiveStoreProvider.test.js +0 -117
- package/dist/LiveStoreProvider.test.js.map +0 -1
- 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/LiveStoreContext.ts +0 -14
- package/src/LiveStoreProvider.test.tsx +0 -248
- package/src/LiveStoreProvider.tsx +0 -413
- package/src/ambient.d.ts +0 -1
- package/src/utils/stack-info.ts +0 -13
|
@@ -1,131 +1,44 @@
|
|
|
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
|
-
import * as LiveStoreReact from '../mod.
|
|
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'
|
|
18
|
+
// Re-export shared types and schema
|
|
19
|
+
export { events, schema, tables }
|
|
20
|
+
export type { AppState, Filter, Todo }
|
|
20
21
|
|
|
21
|
-
export type
|
|
22
|
-
|
|
23
|
-
filter: Filter
|
|
22
|
+
export type MakeTodoMvcReactOptions = CreateTodoMvcStoreOptions & {
|
|
23
|
+
strictMode?: boolean | undefined
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const
|
|
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 })
|
|
97
|
-
|
|
98
|
-
export const makeTodoMvcReact: ({
|
|
99
|
-
otelTracer,
|
|
100
|
-
otelContext,
|
|
101
|
-
strictMode,
|
|
102
|
-
}?: {
|
|
103
|
-
otelTracer?: otel.Tracer
|
|
104
|
-
otelContext?: otel.Context
|
|
105
|
-
strictMode?: boolean
|
|
106
|
-
}) => Effect.Effect<
|
|
26
|
+
export const makeTodoMvcReact: (opts?: MakeTodoMvcReactOptions) => Effect.Effect<
|
|
107
27
|
{
|
|
108
28
|
wrapper: ({ children }: any) => React.JSX.Element
|
|
109
|
-
store: Store<
|
|
29
|
+
store: Store<typeof schema> & LiveStoreReact.ReactApi
|
|
110
30
|
renderCount: { readonly val: number; inc: () => void }
|
|
111
31
|
},
|
|
112
|
-
|
|
32
|
+
UnknownError,
|
|
113
33
|
Scope.Scope
|
|
114
|
-
> = ({
|
|
115
|
-
otelTracer,
|
|
116
|
-
otelContext,
|
|
117
|
-
strictMode,
|
|
118
|
-
}: {
|
|
119
|
-
otelTracer?: otel.Tracer
|
|
120
|
-
otelContext?: otel.Context
|
|
121
|
-
strictMode?: boolean
|
|
122
|
-
} = {}) =>
|
|
34
|
+
> = (opts: MakeTodoMvcReactOptions = {}) =>
|
|
123
35
|
Effect.gen(function* () {
|
|
36
|
+
const { strictMode } = opts
|
|
124
37
|
const makeRenderCount = () => {
|
|
125
38
|
let val = 0
|
|
126
39
|
|
|
127
40
|
const inc = () => {
|
|
128
|
-
val += strictMode ? 0.5 : 1
|
|
41
|
+
val += strictMode === true ? 0.5 : 1
|
|
129
42
|
}
|
|
130
43
|
|
|
131
44
|
return {
|
|
@@ -136,32 +49,15 @@ export const makeTodoMvcReact: ({
|
|
|
136
49
|
}
|
|
137
50
|
}
|
|
138
51
|
|
|
139
|
-
const store
|
|
140
|
-
schema,
|
|
141
|
-
storeId: 'default',
|
|
142
|
-
adapter: makeInMemoryAdapter(),
|
|
143
|
-
debug: { instanceId: 'test' },
|
|
144
|
-
})
|
|
52
|
+
const store = yield* createTodoMvcStore(opts)
|
|
145
53
|
|
|
146
54
|
const storeWithReactApi = LiveStoreReact.withReactApi(store)
|
|
147
55
|
|
|
148
|
-
|
|
149
|
-
const storeContext = {
|
|
150
|
-
stage: 'running' as const,
|
|
151
|
-
store: storeWithReactApi,
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const MaybeStrictMode = strictMode ? React.StrictMode : React.Fragment
|
|
56
|
+
const MaybeStrictMode = strictMode === true ? React.StrictMode : React.Fragment
|
|
155
57
|
|
|
156
|
-
const wrapper = ({ children }: any) =>
|
|
157
|
-
<MaybeStrictMode>
|
|
158
|
-
<LiveStoreReact.LiveStoreContext.Provider value={storeContext}>
|
|
159
|
-
{children}
|
|
160
|
-
</LiveStoreReact.LiveStoreContext.Provider>
|
|
161
|
-
</MaybeStrictMode>
|
|
162
|
-
)
|
|
58
|
+
const wrapper = ({ children }: any) => <MaybeStrictMode>{children}</MaybeStrictMode>
|
|
163
59
|
|
|
164
60
|
const renderCount = makeRenderCount()
|
|
165
61
|
|
|
166
62
|
return { wrapper, store: storeWithReactApi, renderCount }
|
|
167
|
-
})
|
|
63
|
+
})
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { LiveQueryDef } from '@livestore/livestore'
|
|
1
|
+
import type { LiveQueryDef, Store } from '@livestore/livestore'
|
|
2
2
|
import { computed } from '@livestore/livestore'
|
|
3
3
|
import React from 'react'
|
|
4
4
|
|
|
5
|
-
import { useQuery } from '../../useQuery.
|
|
5
|
+
import { useQuery } from '../../useQuery.ts'
|
|
6
6
|
|
|
7
7
|
/*
|
|
8
8
|
TODO:
|
|
@@ -16,6 +16,8 @@ export type LiveListProps<TItem> = {
|
|
|
16
16
|
renderItem: (item: TItem, opts: { index: number; isInitialListRender: boolean }) => React.ReactNode
|
|
17
17
|
/** Needs to be unique across all list items */
|
|
18
18
|
getKey: (item: TItem, index: number) => string | number
|
|
19
|
+
/** The store instance to use for queries */
|
|
20
|
+
store: Store<any, any>
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/**
|
|
@@ -26,12 +28,15 @@ export type LiveListProps<TItem> = {
|
|
|
26
28
|
* In the future we want to make this component even more efficient by using incremental rendering (https://github.com/livestorejs/livestore/pull/55)
|
|
27
29
|
* e.g. when an item is added/removed/moved to only re-render the affected DOM nodes.
|
|
28
30
|
*/
|
|
29
|
-
export const LiveList = <TItem,>({ items$, renderItem, getKey }: LiveListProps<TItem>): React.ReactNode => {
|
|
31
|
+
export const LiveList = <TItem,>({ items$, renderItem, getKey, store }: LiveListProps<TItem>): React.ReactNode => {
|
|
30
32
|
const [hasMounted, setHasMounted] = React.useState(false)
|
|
31
33
|
|
|
32
34
|
React.useEffect(() => setHasMounted(true), [])
|
|
33
35
|
|
|
34
|
-
const keys = useQuery(
|
|
36
|
+
const keys = useQuery(
|
|
37
|
+
computed((get) => get(items$).map(getKey)),
|
|
38
|
+
{ store },
|
|
39
|
+
)
|
|
35
40
|
const arr = React.useMemo(
|
|
36
41
|
() =>
|
|
37
42
|
keys.map(
|
|
@@ -54,7 +59,9 @@ export const LiveList = <TItem,>({ items$, renderItem, getKey }: LiveListProps<T
|
|
|
54
59
|
key={key}
|
|
55
60
|
itemKey={key}
|
|
56
61
|
item$={item$}
|
|
57
|
-
|
|
62
|
+
store={store}
|
|
63
|
+
index={index}
|
|
64
|
+
isInitialListRender={!hasMounted}
|
|
58
65
|
renderItem={renderItem}
|
|
59
66
|
/>
|
|
60
67
|
))}
|
|
@@ -64,15 +71,20 @@ export const LiveList = <TItem,>({ items$, renderItem, getKey }: LiveListProps<T
|
|
|
64
71
|
|
|
65
72
|
const ItemWrapper = <TItem,>({
|
|
66
73
|
item$,
|
|
67
|
-
|
|
74
|
+
index,
|
|
75
|
+
isInitialListRender,
|
|
68
76
|
renderItem,
|
|
77
|
+
store,
|
|
69
78
|
}: {
|
|
70
79
|
itemKey: string | number
|
|
71
80
|
item$: LiveQueryDef<TItem>
|
|
72
|
-
|
|
81
|
+
index: number
|
|
82
|
+
isInitialListRender: boolean
|
|
73
83
|
renderItem: (item: TItem, opts: { index: number; isInitialListRender: boolean }) => React.ReactNode
|
|
84
|
+
store: Store<any, any>
|
|
74
85
|
}) => {
|
|
75
|
-
const item = useQuery(item
|
|
86
|
+
const item = useQuery(item$, { store })
|
|
87
|
+
const opts = React.useMemo(() => ({ index, isInitialListRender }), [index, isInitialListRender])
|
|
76
88
|
|
|
77
89
|
return <>{renderItem(item, opts)}</>
|
|
78
90
|
}
|
|
@@ -82,6 +94,7 @@ const ItemWrapperMemo = React.memo(
|
|
|
82
94
|
(prev, next) =>
|
|
83
95
|
prev.itemKey === next.itemKey &&
|
|
84
96
|
prev.renderItem === next.renderItem &&
|
|
85
|
-
prev.
|
|
86
|
-
prev.
|
|
97
|
+
prev.store === next.store &&
|
|
98
|
+
prev.index === next.index &&
|
|
99
|
+
prev.isInitialListRender === next.isInitialListRender,
|
|
87
100
|
) as typeof ItemWrapper
|
package/src/mod.ts
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
type StateSetters,
|
|
8
|
-
type UseClientDocumentResult,
|
|
9
|
-
useClientDocument,
|
|
10
|
-
} from './useClientDocument.ts'
|
|
1
|
+
export type { Dispatch, SetStateAction, SetStateActionPartial, StateSetters } from '@livestore/framework-toolkit'
|
|
2
|
+
export { captureStackInfo } from '@livestore/framework-toolkit'
|
|
3
|
+
export { StoreRegistry, storeOptions } from '@livestore/livestore'
|
|
4
|
+
export { LiveList, type LiveListProps } from './experimental/components/LiveList.tsx'
|
|
5
|
+
export * from './StoreRegistryContext.tsx'
|
|
6
|
+
export { type UseClientDocumentResult, useClientDocument } from './useClientDocument.ts'
|
|
11
7
|
export { useQuery, useQueryRef } from './useQuery.ts'
|
|
12
|
-
export { useStore, withReactApi } from './useStore.ts'
|
|
13
|
-
export {
|
|
8
|
+
export { type ReactApi, useStore, withReactApi } from './useStore.ts'
|
|
9
|
+
export { useSyncStatus } from './useSyncStatus.ts'
|
|
@@ -1,18 +1,19 @@
|
|
|
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 { StoreInternalsSymbol } from '@livestore/livestore'
|
|
4
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'
|
|
8
9
|
import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
|
|
9
10
|
import * as ReactTesting from '@testing-library/react'
|
|
10
|
-
import
|
|
11
|
+
import * as 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)
|
|
@@ -119,25 +120,35 @@ Vitest.describe('useClientDocument', () => {
|
|
|
119
120
|
renderCount.inc()
|
|
120
121
|
|
|
121
122
|
const [state, setState] = store.useClientDocument(tables.AppRouterSchema, 'singleton')
|
|
123
|
+
const setCurrentTaskId = React.useCallback((taskId: string) => setState({ currentTaskId: taskId }), [setState])
|
|
122
124
|
|
|
123
125
|
globalSetState = setState
|
|
124
126
|
|
|
125
127
|
return (
|
|
126
128
|
<div>
|
|
127
|
-
<TasksList setTaskId={
|
|
129
|
+
<TasksList setTaskId={setCurrentTaskId} />
|
|
128
130
|
<div role="current-id">Current Task Id: {state.currentTaskId ?? '-'}</div>
|
|
129
|
-
{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>}
|
|
130
132
|
</div>
|
|
131
133
|
)
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
const TasksList: React.FC<{ setTaskId: (_: string) => void }> = ({ setTaskId }) => {
|
|
135
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
|
+
)
|
|
136
147
|
|
|
137
148
|
return (
|
|
138
149
|
<div>
|
|
139
150
|
{allTodos.map((_) => (
|
|
140
|
-
<div key={_.id}
|
|
151
|
+
<div key={_.id} data-task-id={_.id} onClick={handleTaskClick}>
|
|
141
152
|
{_.id}
|
|
142
153
|
</div>
|
|
143
154
|
))}
|
|
@@ -253,81 +264,81 @@ Vitest.describe('useClientDocument', () => {
|
|
|
253
264
|
)
|
|
254
265
|
|
|
255
266
|
Vitest.describe('otel', () => {
|
|
256
|
-
it.each([
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
267
|
+
it.each([
|
|
268
|
+
{ strictMode: true },
|
|
269
|
+
{ strictMode: false },
|
|
270
|
+
])('should update the data based on component key strictMode=%s', async ({ strictMode }) => {
|
|
271
|
+
const exporter = new InMemorySpanExporter()
|
|
272
|
+
|
|
273
|
+
const provider = new BasicTracerProvider({
|
|
274
|
+
spanProcessors: [new SimpleSpanProcessor(exporter)],
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
const otelTracer = provider.getTracer(`testing-${strictMode !== undefined ? 'strict' : 'non-strict'}`)
|
|
278
|
+
|
|
279
|
+
const span = otelTracer.startSpan('test-root')
|
|
280
|
+
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
281
|
+
|
|
282
|
+
await Effect.gen(function* () {
|
|
283
|
+
const { wrapper, store, renderCount } = yield* makeTodoMvcReact({
|
|
284
|
+
otelContext,
|
|
285
|
+
otelTracer,
|
|
286
|
+
strictMode,
|
|
263
287
|
})
|
|
264
288
|
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
269
|
-
|
|
270
|
-
await Effect.gen(function* () {
|
|
271
|
-
const { wrapper, store, renderCount } = yield* makeTodoMvcReact({
|
|
272
|
-
otelContext,
|
|
273
|
-
otelTracer,
|
|
274
|
-
strictMode,
|
|
275
|
-
})
|
|
276
|
-
|
|
277
|
-
const { result, rerender, unmount } = ReactTesting.renderHook(
|
|
278
|
-
(userId: string) => {
|
|
279
|
-
renderCount.inc()
|
|
289
|
+
const { result, rerender, unmount } = ReactTesting.renderHook(
|
|
290
|
+
(userId: string) => {
|
|
291
|
+
renderCount.inc()
|
|
280
292
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
293
|
+
const [state, setState, id] = store.useClientDocument(tables.userInfo, userId)
|
|
294
|
+
return { state, setState, id }
|
|
295
|
+
},
|
|
296
|
+
{ wrapper, initialProps: 'u1' },
|
|
297
|
+
)
|
|
286
298
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
299
|
+
expect(result.current.id).toBe('u1')
|
|
300
|
+
expect(result.current.state.username).toBe('')
|
|
301
|
+
expect(renderCount.val).toBe(1)
|
|
302
|
+
|
|
303
|
+
// For u2 we'll make sure that the row already exists,
|
|
304
|
+
// so the lazy `insert` will be skipped
|
|
305
|
+
ReactTesting.act(() => store.commit(events.UserInfoSet({ username: 'username_u2' }, 'u2')))
|
|
306
|
+
|
|
307
|
+
rerender('u2')
|
|
308
|
+
|
|
309
|
+
expect(result.current.id).toBe('u2')
|
|
310
|
+
expect(result.current.state.username).toBe('username_u2')
|
|
311
|
+
expect(renderCount.val).toBe(2)
|
|
312
|
+
|
|
313
|
+
unmount()
|
|
314
|
+
span.end()
|
|
315
|
+
}).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise)
|
|
316
|
+
|
|
317
|
+
await provider.forceFlush()
|
|
318
|
+
|
|
319
|
+
const mapAttributes = (attributes: otel.Attributes) => {
|
|
320
|
+
return ReadonlyRecord.map(attributes, (val, key) => {
|
|
321
|
+
if (key === 'code.stacktrace') {
|
|
322
|
+
return '<STACKTRACE>'
|
|
323
|
+
} else if (key === 'firstStackInfo') {
|
|
324
|
+
const stackInfo = JSON.parse(val as string) as LiveStore.StackInfo
|
|
325
|
+
// stackInfo.frames.shift() // Removes `renderHook.wrapper` from the stack
|
|
326
|
+
stackInfo.frames.forEach((_) => {
|
|
327
|
+
if (_.name.includes('renderHook.wrapper') === true) {
|
|
328
|
+
_.name = 'renderHook.wrapper'
|
|
329
|
+
}
|
|
330
|
+
_.filePath = '__REPLACED_FOR_SNAPSHOT__'
|
|
331
|
+
})
|
|
332
|
+
return JSON.stringify(stackInfo)
|
|
333
|
+
}
|
|
334
|
+
return val
|
|
335
|
+
})
|
|
336
|
+
}
|
|
325
337
|
|
|
326
|
-
|
|
327
|
-
|
|
338
|
+
expect(getSimplifiedRootSpan(exporter, 'createStore', mapAttributes)).toMatchSnapshot()
|
|
339
|
+
expect(getAllSimplifiedRootSpans(exporter, 'LiveStore:commit', mapAttributes)).toMatchSnapshot()
|
|
328
340
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
)
|
|
341
|
+
await provider.shutdown()
|
|
342
|
+
})
|
|
332
343
|
})
|
|
333
344
|
})
|
package/src/useClientDocument.ts
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
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
|
-
import { LiveStoreContext } from './LiveStoreContext.ts'
|
|
10
11
|
import { useQueryRef } from './useQuery.ts'
|
|
11
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Return type of `useClientDocument` hook.
|
|
15
|
+
*
|
|
16
|
+
* A tuple providing React-style state access to a client-document table row:
|
|
17
|
+
* - `[0]` row: The current value (decoded according to the table schema)
|
|
18
|
+
* - `[1]` setRow: Setter function to update the document
|
|
19
|
+
* - `[2]` id: The document's ID (resolved from `SessionIdSymbol` if applicable)
|
|
20
|
+
* - `[3]` query$: The underlying `LiveQuery` for advanced use cases
|
|
21
|
+
*
|
|
22
|
+
* @typeParam TTableDef - The client-document table definition type
|
|
23
|
+
*/
|
|
12
24
|
export type UseClientDocumentResult<TTableDef extends State.SQLite.ClientDocumentTableDef.TraitAny> = [
|
|
13
25
|
row: TTableDef['Value'],
|
|
14
26
|
setRow: StateSetters<TTableDef>,
|
|
@@ -104,20 +116,15 @@ export const useClientDocument: {
|
|
|
104
116
|
|
|
105
117
|
const tableName = table.sqliteDef.name
|
|
106
118
|
|
|
107
|
-
const store =
|
|
108
|
-
storeArg?.store ?? // biome-ignore lint/correctness/useHookAtTopLevel: store is stable
|
|
109
|
-
React.useContext(LiveStoreContext)?.store ??
|
|
110
|
-
shouldNeverHappen(`No store provided to useClientDocument`)
|
|
119
|
+
const store = storeArg?.store ?? shouldNeverHappen(`No store provided to useClientDocument`)
|
|
111
120
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const idStr: string = id === SessionIdSymbol ? store.clientSession.sessionId : id
|
|
121
|
+
const idStr: string = id === SessionIdSymbol ? store.sessionId : id
|
|
115
122
|
|
|
116
123
|
type QueryDef = LiveQueryDef<TTableDef['Value']>
|
|
117
124
|
const queryDef: QueryDef = React.useMemo(
|
|
118
125
|
() =>
|
|
119
|
-
queryDb(table.get(id
|
|
120
|
-
deps: [idStr
|
|
126
|
+
queryDb(table.get(id, { default: defaultValues! }), {
|
|
127
|
+
deps: [idStr, table.sqliteDef.name, JSON.stringify(defaultValues)],
|
|
121
128
|
}),
|
|
122
129
|
[table, id, defaultValues, idStr],
|
|
123
130
|
)
|
|
@@ -132,36 +139,10 @@ export const useClientDocument: {
|
|
|
132
139
|
const newValue = typeof newValueOrFn === 'function' ? newValueOrFn(queryRef.valueRef.current) : newValueOrFn
|
|
133
140
|
if (queryRef.valueRef.current === newValue) return
|
|
134
141
|
|
|
135
|
-
store.commit(table.set(removeUndefinedValues(newValue), id
|
|
142
|
+
store.commit(table.set(removeUndefinedValues(newValue), id))
|
|
136
143
|
},
|
|
137
144
|
[id, queryRef.valueRef, store, table],
|
|
138
145
|
)
|
|
139
146
|
|
|
140
147
|
return [queryRef.valueRef.current, setState, idStr, queryRef.queryRcRef.value]
|
|
141
148
|
}
|
|
142
|
-
|
|
143
|
-
export type Dispatch<A> = (action: A) => void
|
|
144
|
-
export type SetStateActionPartial<S> = Partial<S> | ((previousValue: S) => Partial<S>)
|
|
145
|
-
export type SetStateAction<S> = S | ((previousValue: S) => S)
|
|
146
|
-
|
|
147
|
-
export type StateSetters<TTableDef extends State.SQLite.ClientDocumentTableDef.TraitAny> = Dispatch<
|
|
148
|
-
TTableDef[State.SQLite.ClientDocumentTableDefSymbol]['options']['partialSet'] extends false
|
|
149
|
-
? SetStateAction<TTableDef['Value']>
|
|
150
|
-
: SetStateActionPartial<TTableDef['Value']>
|
|
151
|
-
>
|
|
152
|
-
|
|
153
|
-
const validateTableOptions = (table: State.SQLite.TableDef<any, any>) => {
|
|
154
|
-
if (State.SQLite.tableIsClientDocumentTable(table) === false) {
|
|
155
|
-
return shouldNeverHappen(
|
|
156
|
-
`useClientDocument called on table "${table.sqliteDef.name}" which is not a client document table`,
|
|
157
|
-
)
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const removeUndefinedValues = (value: any) => {
|
|
162
|
-
if (typeof value === 'object' && value !== null) {
|
|
163
|
-
return Object.fromEntries(Object.entries(value).filter(([_, v]) => v !== undefined))
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return value
|
|
167
|
-
}
|