@livestore/solid 0.4.0-dev.22 → 0.4.0-dev.24
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/StoreRegistryContext.d.ts +56 -0
- package/dist/StoreRegistryContext.d.ts.map +1 -0
- package/dist/StoreRegistryContext.jsx +60 -0
- package/dist/StoreRegistryContext.jsx.map +1 -0
- package/dist/__tests__/fixture.d.ts +14 -0
- package/dist/__tests__/fixture.d.ts.map +1 -0
- package/dist/__tests__/fixture.jsx +13 -0
- package/dist/__tests__/fixture.jsx.map +1 -0
- package/dist/experimental/components/LiveList.d.ts +24 -0
- package/dist/experimental/components/LiveList.d.ts.map +1 -0
- package/dist/experimental/components/LiveList.jsx +24 -0
- package/dist/experimental/components/LiveList.jsx.map +1 -0
- package/dist/experimental/mod.d.ts +2 -0
- package/dist/experimental/mod.d.ts.map +1 -0
- package/dist/experimental/mod.js +2 -0
- package/dist/experimental/mod.js.map +1 -0
- package/dist/mod.d.ts +6 -2
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +4 -2
- package/dist/mod.js.map +1 -1
- package/dist/useClientDocument.client.test.d.ts +2 -0
- package/dist/useClientDocument.client.test.d.ts.map +1 -0
- package/dist/useClientDocument.client.test.jsx +177 -0
- package/dist/useClientDocument.client.test.jsx.map +1 -0
- package/dist/useClientDocument.d.ts +71 -0
- package/dist/useClientDocument.d.ts.map +1 -0
- package/dist/useClientDocument.js +74 -0
- package/dist/useClientDocument.js.map +1 -0
- package/dist/useClientDocument.server.test.d.ts +6 -0
- package/dist/useClientDocument.server.test.d.ts.map +1 -0
- package/dist/useClientDocument.server.test.jsx +76 -0
- package/dist/useClientDocument.server.test.jsx.map +1 -0
- package/dist/useQuery.client.test.d.ts +2 -0
- package/dist/useQuery.client.test.d.ts.map +1 -0
- package/dist/useQuery.client.test.jsx +165 -0
- package/dist/useQuery.client.test.jsx.map +1 -0
- package/dist/useQuery.d.ts +32 -0
- package/dist/useQuery.d.ts.map +1 -0
- package/dist/useQuery.js +64 -0
- package/dist/useQuery.js.map +1 -0
- package/dist/useQuery.server.test.d.ts +6 -0
- package/dist/useQuery.server.test.d.ts.map +1 -0
- package/dist/useQuery.server.test.jsx +88 -0
- package/dist/useQuery.server.test.jsx.map +1 -0
- package/dist/useStore.client.test.d.ts +2 -0
- package/dist/useStore.client.test.d.ts.map +1 -0
- package/dist/useStore.client.test.jsx +438 -0
- package/dist/useStore.client.test.jsx.map +1 -0
- package/dist/useStore.d.ts +91 -0
- package/dist/useStore.d.ts.map +1 -0
- package/dist/useStore.js +94 -0
- package/dist/useStore.js.map +1 -0
- package/dist/useStore.server.test.d.ts +6 -0
- package/dist/useStore.server.test.d.ts.map +1 -0
- package/dist/useStore.server.test.jsx +56 -0
- package/dist/useStore.server.test.jsx.map +1 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +7 -0
- package/dist/utils.js.map +1 -0
- package/dist/whenever.d.ts +32 -0
- package/dist/whenever.d.ts.map +1 -0
- package/dist/whenever.js +51 -0
- package/dist/whenever.js.map +1 -0
- package/package.json +64 -16
- package/src/StoreRegistryContext.tsx +70 -0
- package/src/__snapshots__/useClientDocument.client.test.tsx.snap +570 -0
- package/src/__snapshots__/useQuery.client.test.tsx.snap +1550 -0
- package/src/__tests__/fixture.tsx +42 -0
- package/src/experimental/components/LiveList.tsx +54 -0
- package/src/experimental/mod.ts +1 -0
- package/src/mod.ts +6 -2
- package/src/useClientDocument.client.test.tsx +299 -0
- package/src/useClientDocument.server.test.tsx +107 -0
- package/src/useClientDocument.ts +146 -0
- package/src/useQuery.client.test.tsx +293 -0
- package/src/useQuery.server.test.tsx +128 -0
- package/src/useQuery.ts +115 -0
- package/src/useStore.client.test.tsx +632 -0
- package/src/useStore.server.test.tsx +70 -0
- package/src/useStore.ts +179 -0
- package/src/utils.ts +10 -0
- package/src/whenever.ts +80 -0
- package/dist/query.d.ts +0 -4
- package/dist/query.d.ts.map +0 -1
- package/dist/query.js +0 -15
- package/dist/query.js.map +0 -1
- package/dist/store.d.ts +0 -6
- package/dist/store.d.ts.map +0 -1
- package/dist/store.js +0 -99
- package/dist/store.js.map +0 -1
- package/src/query.ts +0 -22
- package/src/store.ts +0 -196
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type * as Solid from 'solid-js'
|
|
2
|
+
|
|
3
|
+
import type { UnknownError } from '@livestore/common'
|
|
4
|
+
import {
|
|
5
|
+
type AppState,
|
|
6
|
+
type CreateTodoMvcStoreOptions,
|
|
7
|
+
createTodoMvcStore,
|
|
8
|
+
events,
|
|
9
|
+
type Filter,
|
|
10
|
+
schema,
|
|
11
|
+
type Todo,
|
|
12
|
+
tables,
|
|
13
|
+
} from '@livestore/framework-toolkit/testing'
|
|
14
|
+
import type { Store } from '@livestore/livestore'
|
|
15
|
+
import { StoreInternalsSymbol } from '@livestore/livestore'
|
|
16
|
+
import { Effect, type Scope } from '@livestore/utils/effect'
|
|
17
|
+
|
|
18
|
+
import * as LiveStoreSolid from '../mod.ts'
|
|
19
|
+
|
|
20
|
+
// Re-export shared types, schema, and StoreInternalsSymbol for tests
|
|
21
|
+
export { events, schema, StoreInternalsSymbol, tables }
|
|
22
|
+
export type { AppState, Filter, Todo }
|
|
23
|
+
|
|
24
|
+
export const makeTodoMvcSolid = (
|
|
25
|
+
opts: CreateTodoMvcStoreOptions = {},
|
|
26
|
+
): Effect.Effect<
|
|
27
|
+
{
|
|
28
|
+
wrapper: ({ children }: any) => Solid.JSX.Element
|
|
29
|
+
store: Store<typeof schema> & LiveStoreSolid.SolidApi
|
|
30
|
+
},
|
|
31
|
+
UnknownError,
|
|
32
|
+
Scope.Scope
|
|
33
|
+
> =>
|
|
34
|
+
Effect.gen(function* () {
|
|
35
|
+
const store = yield* createTodoMvcStore(opts)
|
|
36
|
+
|
|
37
|
+
const storeWithSolidApi = LiveStoreSolid.withSolidApi(store)
|
|
38
|
+
|
|
39
|
+
const wrapper = (props: Solid.ParentProps) => <>{props.children}</>
|
|
40
|
+
|
|
41
|
+
return { wrapper, store: storeWithSolidApi }
|
|
42
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Accessor, JSX } from 'solid-js'
|
|
2
|
+
import * as Solid from 'solid-js'
|
|
3
|
+
|
|
4
|
+
import type { LiveQueryDef, Store } from '@livestore/livestore'
|
|
5
|
+
import { computed } from '@livestore/livestore'
|
|
6
|
+
|
|
7
|
+
import { useQuery } from '../../useQuery.ts'
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
TODO:
|
|
11
|
+
- [ ] Bring back incremental rendering (see https://github.com/livestorejs/livestore/pull/55)
|
|
12
|
+
- [ ] Enable exit animations
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export type LiveListProps<TItem> = {
|
|
16
|
+
items$: LiveQueryDef<ReadonlyArray<TItem>>
|
|
17
|
+
// TODO refactor render-flag to allow for transition animations on add/remove
|
|
18
|
+
renderItem: (item: Accessor<TItem>, index: Accessor<number>) => JSX.Element
|
|
19
|
+
/** Needs to be unique across all list items */
|
|
20
|
+
getKey: (item: TItem, index: number) => string | number
|
|
21
|
+
/** The store instance to use for queries */
|
|
22
|
+
store: Store<any, any>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* This component is a helper component for rendering a list of items for a LiveQuery of an array of items.
|
|
27
|
+
* The idea is that instead of letting Solid handle the rendering of the items array directly,
|
|
28
|
+
* we derive a item LiveQuery for each item which moves the reactivity to the item level when a single item changes.
|
|
29
|
+
*
|
|
30
|
+
* In the future we want to make this component even more efficient by using incremental rendering (https://github.com/livestorejs/livestore/pull/55)
|
|
31
|
+
* e.g. when an item is added/removed/moved to only re-render the affected DOM nodes.
|
|
32
|
+
*/
|
|
33
|
+
export const LiveList = <TItem,>(props: LiveListProps<TItem>): JSX.Element => {
|
|
34
|
+
const [config] = Solid.splitProps(props, ['store'])
|
|
35
|
+
const keys = useQuery(
|
|
36
|
+
() => computed((get) => get(props.items$).map((item, index) => props.getKey(item, index))),
|
|
37
|
+
config,
|
|
38
|
+
)
|
|
39
|
+
return <Solid.For each={keys()}>{(key, index) => <ItemWrapper {...props} key={key} index={index} />}</Solid.For>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const ItemWrapper = <TItem,>(
|
|
43
|
+
props: { key: string | number; index: Accessor<number> } & LiveListProps<TItem>,
|
|
44
|
+
) => {
|
|
45
|
+
const [config] = Solid.splitProps(props, ['store'])
|
|
46
|
+
const item = useQuery(
|
|
47
|
+
() =>
|
|
48
|
+
computed((get) => get(props.items$).find((item, index) => props.getKey(item, index) === props.key)!, {
|
|
49
|
+
deps: [props.key],
|
|
50
|
+
}),
|
|
51
|
+
config,
|
|
52
|
+
)
|
|
53
|
+
return <>{props.renderItem(item, props.index)}</>
|
|
54
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { LiveList, type LiveListProps } from './components/LiveList.tsx'
|
package/src/mod.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
1
|
+
export type { Dispatch, SetStateAction, SetStateActionPartial, StateSetters } from '@livestore/framework-toolkit'
|
|
2
|
+
export { StoreRegistry, storeOptions } from '@livestore/livestore'
|
|
3
|
+
export { LiveList, type LiveListProps } from './experimental/components/LiveList.tsx'
|
|
4
|
+
export * from './StoreRegistryContext.tsx'
|
|
5
|
+
export type { UseClientDocumentResult } from './useClientDocument.ts'
|
|
6
|
+
export { type SolidApi, useStore, withSolidApi } from './useStore.ts'
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/** biome-ignore-all lint/a11y/useValidAriaRole: not needed for testing */
|
|
2
|
+
/** biome-ignore-all lint/a11y/noStaticElementInteractions: not needed for testing */
|
|
3
|
+
import * as LiveStore from '@livestore/livestore'
|
|
4
|
+
import { getAllSimplifiedRootSpans, getSimplifiedRootSpan } from '@livestore/livestore/internal/testing-utils'
|
|
5
|
+
import { Effect, ReadonlyRecord, Schema } from '@livestore/utils/effect'
|
|
6
|
+
import { Vitest } from '@livestore/utils-dev/node-vitest'
|
|
7
|
+
import * as otel from '@opentelemetry/api'
|
|
8
|
+
import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
|
|
9
|
+
import * as SolidTesting from '@solidjs/testing-library'
|
|
10
|
+
import * as Solid from 'solid-js'
|
|
11
|
+
|
|
12
|
+
import { events, makeTodoMvcSolid, StoreInternalsSymbol, tables } from './__tests__/fixture.tsx'
|
|
13
|
+
import type * as LiveStoreSolid from './mod.ts'
|
|
14
|
+
|
|
15
|
+
// const strictMode = process.env.REACT_STRICT_MODE !== undefined
|
|
16
|
+
|
|
17
|
+
// NOTE running tests concurrently doesn't work with the default global db graph
|
|
18
|
+
Vitest.describe('useClientDocument', () => {
|
|
19
|
+
Vitest.scopedLive('should update the data based on component key', () =>
|
|
20
|
+
Effect.gen(function* () {
|
|
21
|
+
const { wrapper, store } = yield* makeTodoMvcSolid({})
|
|
22
|
+
|
|
23
|
+
const [userId, setUserId] = Solid.createSignal('u1')
|
|
24
|
+
|
|
25
|
+
const { result } = SolidTesting.renderHook(
|
|
26
|
+
() => {
|
|
27
|
+
const [state, setState, id] = store.useClientDocument(tables.userInfo, userId)
|
|
28
|
+
return { state, setState, id }
|
|
29
|
+
},
|
|
30
|
+
{ wrapper },
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
Vitest.expect(result.id()).toBe('u1')
|
|
34
|
+
Vitest.expect(result.state().username).toBe('')
|
|
35
|
+
Vitest.expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
36
|
+
store.commit(tables.userInfo.set({ username: 'username_u2' }, 'u2'))
|
|
37
|
+
|
|
38
|
+
setUserId('u2')
|
|
39
|
+
|
|
40
|
+
Vitest.expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
41
|
+
Vitest.expect(result.id()).toBe('u2')
|
|
42
|
+
Vitest.expect(result.state().username).toBe('username_u2')
|
|
43
|
+
}),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
// TODO add a test that makes sure Solid doesn't re-render when a setter is used to set the same value
|
|
47
|
+
|
|
48
|
+
Vitest.scopedLive('should update the data reactively - via setState', () =>
|
|
49
|
+
Effect.gen(function* () {
|
|
50
|
+
const { wrapper, store } = yield* makeTodoMvcSolid({})
|
|
51
|
+
|
|
52
|
+
const { result } = SolidTesting.renderHook(
|
|
53
|
+
() => {
|
|
54
|
+
const [state, setState, id] = store.useClientDocument(tables.userInfo, 'u1')
|
|
55
|
+
return { state, setState, id }
|
|
56
|
+
},
|
|
57
|
+
{ wrapper },
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
Vitest.expect(result.id()).toBe('u1')
|
|
61
|
+
Vitest.expect(result.state().username).toBe('')
|
|
62
|
+
|
|
63
|
+
result.setState({ username: 'username_u1_hello' })
|
|
64
|
+
|
|
65
|
+
Vitest.expect(result.id()).toBe('u1')
|
|
66
|
+
Vitest.expect(result.state().username).toBe('username_u1_hello')
|
|
67
|
+
}),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
Vitest.scopedLive('should update the data reactively - via raw store commit', () =>
|
|
71
|
+
Effect.gen(function* () {
|
|
72
|
+
const { wrapper, store } = yield* makeTodoMvcSolid({})
|
|
73
|
+
|
|
74
|
+
const { result } = SolidTesting.renderHook(
|
|
75
|
+
() => {
|
|
76
|
+
const [state, setState, id] = store.useClientDocument(tables.userInfo, 'u1')
|
|
77
|
+
return { state, setState, id }
|
|
78
|
+
},
|
|
79
|
+
{ wrapper },
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
Vitest.expect(result.id()).toBe('u1')
|
|
83
|
+
Vitest.expect(result.state().username).toBe('')
|
|
84
|
+
|
|
85
|
+
store.commit(events.UserInfoSet({ username: 'username_u1_hello' }, 'u1'))
|
|
86
|
+
|
|
87
|
+
Vitest.expect(result.id()).toBe('u1')
|
|
88
|
+
Vitest.expect(result.state().username).toBe('username_u1_hello')
|
|
89
|
+
}),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
Vitest.scopedLive('should work for a larger app', () =>
|
|
93
|
+
Effect.gen(function* () {
|
|
94
|
+
const { wrapper, store } = yield* makeTodoMvcSolid({})
|
|
95
|
+
|
|
96
|
+
const allTodos$ = LiveStore.queryDb(
|
|
97
|
+
{ query: `select * from todos`, schema: Schema.Array(tables.todos.rowSchema) },
|
|
98
|
+
{ label: 'allTodos' },
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
let globalSetState: LiveStoreSolid.StateSetters<typeof tables.AppRouterSchema> | undefined
|
|
102
|
+
const AppRouter = () => {
|
|
103
|
+
const [state, setState] = store.useClientDocument(
|
|
104
|
+
() => tables.AppRouterSchema,
|
|
105
|
+
() => 'singleton',
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
globalSetState = setState
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div>
|
|
112
|
+
<TasksList />
|
|
113
|
+
<div role={'current-id' as any}>Current Task Id: {state().currentTaskId ?? '-'}</div>
|
|
114
|
+
<Solid.Show when={state().currentTaskId} fallback={'Click on a task to see details'}>
|
|
115
|
+
{(id: Solid.Accessor<string>) => <TaskDetails id={id()} />}
|
|
116
|
+
</Solid.Show>
|
|
117
|
+
</div>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const TasksList = () => {
|
|
122
|
+
const allTodos = store.useQuery(() => allTodos$)
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div>
|
|
126
|
+
<Solid.For each={allTodos()}>{(todo) => <div>{todo.id}</div>}</Solid.For>
|
|
127
|
+
</div>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const TaskDetails = (props: { id: string }) => {
|
|
132
|
+
const todo = store.useQuery(() =>
|
|
133
|
+
LiveStore.queryDb(tables.todos.where({ id: props.id }).first(), { deps: props.id }),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return <div role={'content' as any}>{JSON.stringify(todo())}</div>
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const { getByRole } = SolidTesting.render(() => <AppRouter />, { wrapper })
|
|
140
|
+
|
|
141
|
+
store.commit(events.todoCreated({ id: 't1', text: 'buy milk', completed: false }))
|
|
142
|
+
|
|
143
|
+
Vitest.expect(getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: -"')
|
|
144
|
+
|
|
145
|
+
globalSetState!({ currentTaskId: 't1' })
|
|
146
|
+
|
|
147
|
+
Vitest.expect(getByRole('content').innerHTML).toMatchInlineSnapshot(
|
|
148
|
+
`"{"id":"t1","text":"buy milk","completed":false}"`,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
Vitest.expect(getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: t1"')
|
|
152
|
+
|
|
153
|
+
store.commit(
|
|
154
|
+
events.todoCreated({ id: 't2', text: 'buy eggs', completed: false }),
|
|
155
|
+
events.AppRouterSet({ currentTaskId: 't2' }),
|
|
156
|
+
events.todoCreated({ id: 't3', text: 'buy bread', completed: false }),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
Vitest.expect(getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: t2"')
|
|
160
|
+
}),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
Vitest.scopedLive('should work for a useClientDocument query chained with a useTemporary query', () =>
|
|
164
|
+
Effect.gen(function* () {
|
|
165
|
+
const { store, wrapper } = yield* makeTodoMvcSolid({})
|
|
166
|
+
|
|
167
|
+
store.commit(
|
|
168
|
+
events.todoCreated({ id: 't1', text: 'buy milk', completed: false }),
|
|
169
|
+
events.todoCreated({ id: 't2', text: 'buy bread', completed: false }),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
const [userId, setUserId] = Solid.createSignal('u1')
|
|
173
|
+
|
|
174
|
+
const { result } = SolidTesting.renderHook(
|
|
175
|
+
() => {
|
|
176
|
+
const [_row, _setRow, _id, rowState$] = store.useClientDocument(tables.userInfo, userId)
|
|
177
|
+
const todos = store.useQuery(
|
|
178
|
+
() =>
|
|
179
|
+
LiveStore.queryDb(
|
|
180
|
+
(get) => tables.todos.where('text', 'LIKE', `%${get(rowState$()).text}%`),
|
|
181
|
+
// TODO find a way where explicit `userId` is not needed here
|
|
182
|
+
// possibly by automatically understanding the `get(rowState$)` dependency
|
|
183
|
+
{ label: 'todosFiltered', deps: userId() },
|
|
184
|
+
),
|
|
185
|
+
// TODO introduce a `deps` array which is only needed when a query is parametric
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return { todos }
|
|
189
|
+
},
|
|
190
|
+
{ wrapper },
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
Vitest.expect(result.todos()?.length).toBe(2)
|
|
194
|
+
|
|
195
|
+
// Set text filter for u2 and test with second user
|
|
196
|
+
store.commit(events.UserInfoSet({ username: 'username_u2', text: 'milk' }, 'u2'))
|
|
197
|
+
|
|
198
|
+
setUserId('u2')
|
|
199
|
+
|
|
200
|
+
Vitest.expect(result.todos()?.length).toBe(1)
|
|
201
|
+
}),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
Vitest.scopedLive('kv client document overwrites value (Schema.Any, no partial merge)', () =>
|
|
205
|
+
Effect.gen(function* () {
|
|
206
|
+
const { wrapper, store } = yield* makeTodoMvcSolid({})
|
|
207
|
+
|
|
208
|
+
const { result } = SolidTesting.renderHook(
|
|
209
|
+
() => {
|
|
210
|
+
const [state, setState] = store.useClientDocument(tables.kv, 'k1')
|
|
211
|
+
return { state, setState }
|
|
212
|
+
},
|
|
213
|
+
{ wrapper },
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
Vitest.expect(result.state()).toBe(null)
|
|
217
|
+
|
|
218
|
+
result.setState(1)
|
|
219
|
+
Vitest.expect(result.state()).toEqual(1)
|
|
220
|
+
|
|
221
|
+
result.setState({ b: 2 })
|
|
222
|
+
Vitest.expect(result.state()).toEqual({ b: 2 })
|
|
223
|
+
}),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
Vitest.describe('otel', () => {
|
|
227
|
+
Vitest.it('should update the data based on component key', async () => {
|
|
228
|
+
const exporter = new InMemorySpanExporter()
|
|
229
|
+
|
|
230
|
+
const provider = new BasicTracerProvider({
|
|
231
|
+
spanProcessors: [new SimpleSpanProcessor(exporter)],
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
const otelTracer = provider.getTracer(`testing-solid`)
|
|
235
|
+
|
|
236
|
+
const span = otelTracer.startSpan('test-root')
|
|
237
|
+
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
238
|
+
|
|
239
|
+
await Effect.gen(function* () {
|
|
240
|
+
const { wrapper, store } = yield* makeTodoMvcSolid({
|
|
241
|
+
otelContext,
|
|
242
|
+
otelTracer,
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
const [userId, setUserId] = Solid.createSignal('u1')
|
|
246
|
+
|
|
247
|
+
// Test with first user
|
|
248
|
+
const { result } = SolidTesting.renderHook(
|
|
249
|
+
() => {
|
|
250
|
+
const [state, setState, id] = store.useClientDocument(tables.userInfo, userId)
|
|
251
|
+
return { state, setState, id }
|
|
252
|
+
},
|
|
253
|
+
{ wrapper },
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
Vitest.expect(result.id()).toBe('u1')
|
|
257
|
+
Vitest.expect(result.state().username).toBe('')
|
|
258
|
+
|
|
259
|
+
// For u2 we'll make sure that the row already exists,
|
|
260
|
+
// so the lazy `insert` will be skipped
|
|
261
|
+
store.commit(events.UserInfoSet({ username: 'username_u2' }, 'u2'))
|
|
262
|
+
|
|
263
|
+
// Test with second user (new hook instance)
|
|
264
|
+
setUserId('u2')
|
|
265
|
+
|
|
266
|
+
Vitest.expect(result.id()).toBe('u2')
|
|
267
|
+
Vitest.expect(result.state().username).toBe('username_u2')
|
|
268
|
+
|
|
269
|
+
span.end()
|
|
270
|
+
}).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise)
|
|
271
|
+
|
|
272
|
+
await provider.forceFlush()
|
|
273
|
+
|
|
274
|
+
const mapAttributes = (attributes: otel.Attributes) => {
|
|
275
|
+
return ReadonlyRecord.map(attributes, (val, key) => {
|
|
276
|
+
if (key === 'code.stacktrace') {
|
|
277
|
+
return '<STACKTRACE>'
|
|
278
|
+
} else if (key === 'firstStackInfo') {
|
|
279
|
+
const stackInfo = JSON.parse(val as string) as LiveStore.StackInfo
|
|
280
|
+
// stackInfo.frames.shift() // Removes `renderHook.wrapper` from the stack
|
|
281
|
+
stackInfo.frames.forEach((_) => {
|
|
282
|
+
if (_.name.includes('renderHook.wrapper') === true) {
|
|
283
|
+
_.name = 'renderHook.wrapper'
|
|
284
|
+
}
|
|
285
|
+
_.filePath = '__REPLACED_FOR_SNAPSHOT__'
|
|
286
|
+
})
|
|
287
|
+
return JSON.stringify(stackInfo)
|
|
288
|
+
}
|
|
289
|
+
return val
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
Vitest.expect(getSimplifiedRootSpan(exporter, 'createStore', mapAttributes)).toMatchSnapshot()
|
|
294
|
+
Vitest.expect(getAllSimplifiedRootSpans(exporter, 'LiveStore:commit', mapAttributes)).toMatchSnapshot()
|
|
295
|
+
|
|
296
|
+
await provider.shutdown()
|
|
297
|
+
})
|
|
298
|
+
})
|
|
299
|
+
})
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSR tests for useClientDocument
|
|
3
|
+
* These tests run in node environment with SSR JSX transform using renderToString.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { isServer, renderToString } from 'solid-js/web'
|
|
7
|
+
import { describe, expect, it } from 'vitest'
|
|
8
|
+
|
|
9
|
+
import { provideOtel } from '@livestore/common'
|
|
10
|
+
import * as LiveStore from '@livestore/livestore'
|
|
11
|
+
import { Effect, Schema } from '@livestore/utils/effect'
|
|
12
|
+
|
|
13
|
+
import { events, makeTodoMvcSolid, tables } from './__tests__/fixture.tsx'
|
|
14
|
+
|
|
15
|
+
describe('environment', () => {
|
|
16
|
+
it('runs on server', () => {
|
|
17
|
+
// Use 'window' in globalThis to avoid TypeScript error without DOM lib
|
|
18
|
+
expect('window' in globalThis).toBe(false)
|
|
19
|
+
expect(isServer).toBe(true)
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('useClientDocument SSR', () => {
|
|
24
|
+
it('renders client document with default value to string', async () => {
|
|
25
|
+
await Effect.gen(function* () {
|
|
26
|
+
const { store } = yield* makeTodoMvcSolid({})
|
|
27
|
+
|
|
28
|
+
const UserDisplay = () => {
|
|
29
|
+
const [state] = store.useClientDocument(tables.userInfo, 'u1')
|
|
30
|
+
return <div>Username: {state().username || 'anonymous'}</div>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const html = renderToString(() => <UserDisplay />)
|
|
34
|
+
|
|
35
|
+
expect(html).toContain('Username:')
|
|
36
|
+
}).pipe(provideOtel({}), Effect.scoped, Effect.runPromise)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('renders client document with committed value to string', async () => {
|
|
40
|
+
await Effect.gen(function* () {
|
|
41
|
+
const { store } = yield* makeTodoMvcSolid({})
|
|
42
|
+
|
|
43
|
+
store.commit(events.UserInfoSet({ username: 'ssr-user' }, 'u1'))
|
|
44
|
+
|
|
45
|
+
const UserDisplay = () => {
|
|
46
|
+
const [state] = store.useClientDocument(tables.userInfo, 'u1')
|
|
47
|
+
return <div>Username: {state().username}</div>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const html = renderToString(() => <UserDisplay />)
|
|
51
|
+
|
|
52
|
+
expect(html).toContain('Username:')
|
|
53
|
+
expect(html).toContain('ssr-user')
|
|
54
|
+
}).pipe(provideOtel({}), Effect.scoped, Effect.runPromise)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('renders larger app with useClientDocument and useQuery to string', async () => {
|
|
58
|
+
await Effect.gen(function* () {
|
|
59
|
+
const { store } = yield* makeTodoMvcSolid({})
|
|
60
|
+
|
|
61
|
+
const allTodos$ = LiveStore.queryDb(
|
|
62
|
+
{ query: `select * from todos`, schema: Schema.Array(tables.todos.rowSchema) },
|
|
63
|
+
{ label: 'allTodos' },
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
store.commit(
|
|
67
|
+
events.todoCreated({ id: 't1', text: 'buy milk', completed: false }),
|
|
68
|
+
events.todoCreated({ id: 't2', text: 'buy eggs', completed: true }),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const App = () => {
|
|
72
|
+
const [routerState] = store.useClientDocument(tables.AppRouterSchema, 'singleton')
|
|
73
|
+
const allTodos = store.useQuery(allTodos$)
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div>
|
|
77
|
+
<div>Current Task: {routerState().currentTaskId ?? 'none'}</div>
|
|
78
|
+
<div>Total Tasks: {allTodos()?.length}</div>
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const html = renderToString(() => <App />)
|
|
84
|
+
|
|
85
|
+
expect(html).toContain('Current Task:')
|
|
86
|
+
expect(html).toContain('none')
|
|
87
|
+
expect(html).toContain('Total Tasks:')
|
|
88
|
+
expect(html).toContain('2')
|
|
89
|
+
}).pipe(provideOtel({}), Effect.scoped, Effect.runPromise)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('renders kv client document to string', async () => {
|
|
93
|
+
await Effect.gen(function* () {
|
|
94
|
+
const { store } = yield* makeTodoMvcSolid({})
|
|
95
|
+
|
|
96
|
+
const KVDisplay = () => {
|
|
97
|
+
const [state] = store.useClientDocument(tables.kv, 'k1')
|
|
98
|
+
return <div>Value: {JSON.stringify(state())}</div>
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const html = renderToString(() => <KVDisplay />)
|
|
102
|
+
|
|
103
|
+
expect(html).toContain('Value:')
|
|
104
|
+
expect(html).toContain('null')
|
|
105
|
+
}).pipe(provideOtel({}), Effect.scoped, Effect.runPromise)
|
|
106
|
+
})
|
|
107
|
+
})
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import * as Solid from 'solid-js'
|
|
2
|
+
|
|
3
|
+
import type { RowQuery } from '@livestore/common'
|
|
4
|
+
import { SessionIdSymbol } from '@livestore/common'
|
|
5
|
+
import { State } from '@livestore/common/schema'
|
|
6
|
+
import { removeUndefinedValues, type StateSetters, validateTableOptions } from '@livestore/framework-toolkit'
|
|
7
|
+
import type { LiveQuery, LiveQueryDef, Store } from '@livestore/livestore'
|
|
8
|
+
import { queryDb } from '@livestore/livestore'
|
|
9
|
+
|
|
10
|
+
import { useQueryRef } from './useQuery.ts'
|
|
11
|
+
import { type AccessorMaybe, resolve } from './utils.ts'
|
|
12
|
+
|
|
13
|
+
export type UseClientDocumentResult<TTableDef extends State.SQLite.ClientDocumentTableDef.TraitAny> = [
|
|
14
|
+
row: Solid.Accessor<TTableDef['Value']>,
|
|
15
|
+
setRow: StateSetters<TTableDef>,
|
|
16
|
+
id: Solid.Accessor<string>,
|
|
17
|
+
query$: Solid.Accessor<LiveQuery<TTableDef['Value']>>,
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Type for useClientDocument that enforces id requirement based on table definition.
|
|
22
|
+
* If table has a default id → id parameter is optional.
|
|
23
|
+
* If table has no default id → id parameter is required.
|
|
24
|
+
*/
|
|
25
|
+
export interface UseClientDocument {
|
|
26
|
+
// case: table has default id → id is optional
|
|
27
|
+
<
|
|
28
|
+
TTableDef extends State.SQLite.ClientDocumentTableDef.Trait<
|
|
29
|
+
any,
|
|
30
|
+
any,
|
|
31
|
+
any,
|
|
32
|
+
{
|
|
33
|
+
partialSet: boolean
|
|
34
|
+
default: { id: string | SessionIdSymbol; value: any }
|
|
35
|
+
}
|
|
36
|
+
>,
|
|
37
|
+
>(
|
|
38
|
+
table: AccessorMaybe<TTableDef>,
|
|
39
|
+
id: AccessorMaybe<State.SQLite.ClientDocumentTableDef.DefaultIdType<TTableDef> | SessionIdSymbol> | undefined,
|
|
40
|
+
options: Partial<RowQuery.GetOrCreateOptions<TTableDef>> | undefined,
|
|
41
|
+
config: { store: Store<any, any> },
|
|
42
|
+
): UseClientDocumentResult<TTableDef>
|
|
43
|
+
|
|
44
|
+
// case: table has no default id → id is required
|
|
45
|
+
<
|
|
46
|
+
TTableDef extends State.SQLite.ClientDocumentTableDef.Trait<
|
|
47
|
+
any,
|
|
48
|
+
any,
|
|
49
|
+
any,
|
|
50
|
+
{ partialSet: boolean; default: { id: undefined; value: any } }
|
|
51
|
+
>,
|
|
52
|
+
>(
|
|
53
|
+
table: AccessorMaybe<TTableDef>,
|
|
54
|
+
id: AccessorMaybe<string | SessionIdSymbol>,
|
|
55
|
+
options: Partial<RowQuery.GetOrCreateOptions<TTableDef>> | undefined,
|
|
56
|
+
config: { store: Store<any, any> },
|
|
57
|
+
): UseClientDocumentResult<TTableDef>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Similar to `Solid.createSignal` but returns a tuple of `[state, setState, id, query$]` for a given table where ...
|
|
62
|
+
*
|
|
63
|
+
* - `state` is the current value of the row (fully decoded according to the table schema)
|
|
64
|
+
* - `setState` is a function that can be used to update the document
|
|
65
|
+
* - `id` is the id of the document
|
|
66
|
+
* - `query$` is a `LiveQuery` that e.g. can be used to subscribe to changes to the document
|
|
67
|
+
*
|
|
68
|
+
* `useClientDocument` only works for client-document tables:
|
|
69
|
+
*
|
|
70
|
+
* ```tsx
|
|
71
|
+
* const MyState = State.SQLite.clientDocument({
|
|
72
|
+
* name: 'MyState',
|
|
73
|
+
* schema: Schema.Struct({
|
|
74
|
+
* showSidebar: Schema.Boolean,
|
|
75
|
+
* }),
|
|
76
|
+
* default: { id: SessionIdSymbol, value: { showSidebar: true } },
|
|
77
|
+
* })
|
|
78
|
+
*
|
|
79
|
+
* const MyComponent = () => {
|
|
80
|
+
* const [{ showSidebar }, setState] = useClientDocument(MyState)
|
|
81
|
+
* return (
|
|
82
|
+
* <div onClick={() => setState({ showSidebar: !showSidebar })}>
|
|
83
|
+
* {showSidebar ? 'Sidebar is open' : 'Sidebar is closed'}
|
|
84
|
+
* </div>
|
|
85
|
+
* )
|
|
86
|
+
* }
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* If the table has a default id, `useClientDocument` can be called without an `id` argument. Otherwise, the `id` argument is required.
|
|
90
|
+
*/
|
|
91
|
+
export const useClientDocument: UseClientDocument = <TTableDef extends State.SQLite.ClientDocumentTableDef.Any>(
|
|
92
|
+
table: AccessorMaybe<TTableDef>,
|
|
93
|
+
_id: AccessorMaybe<string | SessionIdSymbol> | undefined,
|
|
94
|
+
options: Partial<RowQuery.GetOrCreateOptions<TTableDef>> | undefined,
|
|
95
|
+
config: { store: Store<any, any> },
|
|
96
|
+
): UseClientDocumentResult<TTableDef> => {
|
|
97
|
+
const id = (): string | SessionIdSymbol => {
|
|
98
|
+
const id = resolve(_id)
|
|
99
|
+
return typeof id === 'string' || id === SessionIdSymbol
|
|
100
|
+
? id
|
|
101
|
+
: resolve(table)[State.SQLite.ClientDocumentTableDefSymbol].options.default.id
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const serializedId = () => {
|
|
105
|
+
const _id = id()
|
|
106
|
+
return typeof _id === 'string' ? _id : config.store.sessionId
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
Solid.createComputed(() => validateTableOptions(resolve(table)))
|
|
110
|
+
|
|
111
|
+
type QueryDef = LiveQueryDef<TTableDef['Value']>
|
|
112
|
+
const queryDef = Solid.createMemo<QueryDef>(() =>
|
|
113
|
+
queryDb(
|
|
114
|
+
resolve(table).get(
|
|
115
|
+
id(),
|
|
116
|
+
options?.default !== undefined
|
|
117
|
+
? {
|
|
118
|
+
default: options.default,
|
|
119
|
+
}
|
|
120
|
+
: undefined,
|
|
121
|
+
),
|
|
122
|
+
{
|
|
123
|
+
deps: [serializedId(), resolve(table).sqliteDef.name, JSON.stringify(options?.default)],
|
|
124
|
+
},
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
const queryRef = useQueryRef(queryDef, {
|
|
129
|
+
get otelSpanName() {
|
|
130
|
+
return `LiveStore:useClientDocument:${resolve(table).sqliteDef.name}:${serializedId()}`
|
|
131
|
+
},
|
|
132
|
+
get store() {
|
|
133
|
+
return config.store
|
|
134
|
+
},
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const setState = (newValueOrFn: TTableDef['Value']) => {
|
|
138
|
+
const newValue = typeof newValueOrFn === 'function' ? newValueOrFn(queryRef.valueRef()) : newValueOrFn
|
|
139
|
+
|
|
140
|
+
if (queryRef.valueRef() === newValue) return
|
|
141
|
+
|
|
142
|
+
config.store.commit(resolve(table).set(removeUndefinedValues(newValue), id()))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return [queryRef.valueRef, setState, serializedId, () => queryRef.queryRcRef().value]
|
|
146
|
+
}
|