@livestore/react 0.3.0-dev.9 → 0.3.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.
Files changed (96) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/LiveStoreContext.d.ts +10 -4
  3. package/dist/LiveStoreContext.d.ts.map +1 -1
  4. package/dist/LiveStoreContext.js +1 -11
  5. package/dist/LiveStoreContext.js.map +1 -1
  6. package/dist/LiveStoreProvider.d.ts +29 -12
  7. package/dist/LiveStoreProvider.d.ts.map +1 -1
  8. package/dist/LiveStoreProvider.js +84 -55
  9. package/dist/LiveStoreProvider.js.map +1 -1
  10. package/dist/LiveStoreProvider.test.js +80 -29
  11. package/dist/LiveStoreProvider.test.js.map +1 -1
  12. package/dist/__tests__/fixture.d.ts +122 -556
  13. package/dist/__tests__/fixture.d.ts.map +1 -1
  14. package/dist/__tests__/fixture.js +71 -30
  15. package/dist/__tests__/fixture.js.map +1 -1
  16. package/dist/experimental/components/LiveList.d.ts +2 -2
  17. package/dist/experimental/components/LiveList.d.ts.map +1 -1
  18. package/dist/experimental/components/LiveList.js +10 -6
  19. package/dist/experimental/components/LiveList.js.map +1 -1
  20. package/dist/mod.d.ts +4 -5
  21. package/dist/mod.d.ts.map +1 -1
  22. package/dist/mod.js +4 -5
  23. package/dist/mod.js.map +1 -1
  24. package/dist/useClientDocument.d.ts +61 -0
  25. package/dist/useClientDocument.d.ts.map +1 -0
  26. package/dist/useClientDocument.js +79 -0
  27. package/dist/useClientDocument.js.map +1 -0
  28. package/dist/useClientDocument.test.d.ts +2 -0
  29. package/dist/useClientDocument.test.d.ts.map +1 -0
  30. package/dist/useClientDocument.test.js +175 -0
  31. package/dist/useClientDocument.test.js.map +1 -0
  32. package/dist/useQuery.d.ts +25 -3
  33. package/dist/useQuery.d.ts.map +1 -1
  34. package/dist/useQuery.js +67 -47
  35. package/dist/useQuery.js.map +1 -1
  36. package/dist/useQuery.test.d.ts +1 -1
  37. package/dist/useQuery.test.d.ts.map +1 -1
  38. package/dist/useQuery.test.js +86 -24
  39. package/dist/useQuery.test.js.map +1 -1
  40. package/dist/useRcResource.d.ts +76 -0
  41. package/dist/useRcResource.d.ts.map +1 -0
  42. package/dist/useRcResource.js +152 -0
  43. package/dist/useRcResource.js.map +1 -0
  44. package/dist/useRcResource.test.d.ts +2 -0
  45. package/dist/useRcResource.test.d.ts.map +1 -0
  46. package/dist/useRcResource.test.js +122 -0
  47. package/dist/useRcResource.test.js.map +1 -0
  48. package/dist/useStore.d.ts +9 -0
  49. package/dist/useStore.d.ts.map +1 -0
  50. package/dist/useStore.js +28 -0
  51. package/dist/useStore.js.map +1 -0
  52. package/dist/utils/useStateRefWithReactiveInput.d.ts.map +1 -1
  53. package/package.json +19 -13
  54. package/src/LiveStoreContext.ts +11 -16
  55. package/src/LiveStoreProvider.test.tsx +176 -37
  56. package/src/LiveStoreProvider.tsx +156 -81
  57. package/src/__snapshots__/useClientDocument.test.tsx.snap +613 -0
  58. package/src/__snapshots__/useQuery.test.tsx.snap +2011 -0
  59. package/src/__tests__/fixture.tsx +74 -47
  60. package/src/experimental/components/LiveList.tsx +10 -7
  61. package/src/mod.ts +5 -6
  62. package/src/useClientDocument.test.tsx +306 -0
  63. package/src/useClientDocument.ts +157 -0
  64. package/src/useQuery.test.tsx +182 -71
  65. package/src/useQuery.ts +95 -58
  66. package/src/useRcResource.test.tsx +167 -0
  67. package/src/useRcResource.ts +182 -0
  68. package/src/useStore.ts +36 -0
  69. package/dist/useAtom.d.ts +0 -5
  70. package/dist/useAtom.d.ts.map +0 -1
  71. package/dist/useAtom.js +0 -38
  72. package/dist/useAtom.js.map +0 -1
  73. package/dist/useRow.d.ts +0 -50
  74. package/dist/useRow.d.ts.map +0 -1
  75. package/dist/useRow.js +0 -93
  76. package/dist/useRow.js.map +0 -1
  77. package/dist/useRow.test.d.ts +0 -2
  78. package/dist/useRow.test.d.ts.map +0 -1
  79. package/dist/useRow.test.js +0 -202
  80. package/dist/useRow.test.js.map +0 -1
  81. package/dist/useScopedQuery.d.ts +0 -33
  82. package/dist/useScopedQuery.d.ts.map +0 -1
  83. package/dist/useScopedQuery.js +0 -87
  84. package/dist/useScopedQuery.js.map +0 -1
  85. package/dist/useScopedQuery.test.d.ts +0 -2
  86. package/dist/useScopedQuery.test.d.ts.map +0 -1
  87. package/dist/useScopedQuery.test.js +0 -60
  88. package/dist/useScopedQuery.test.js.map +0 -1
  89. package/src/__snapshots__/useRow.test.tsx.snap +0 -360
  90. package/src/useAtom.ts +0 -52
  91. package/src/useRow.test.tsx +0 -344
  92. package/src/useRow.ts +0 -188
  93. package/src/useScopedQuery.test.tsx +0 -96
  94. package/src/useScopedQuery.ts +0 -143
  95. package/tsconfig.json +0 -20
  96. package/vitest.config.js +0 -17
@@ -1,9 +1,9 @@
1
+ import { makeInMemoryAdapter } from '@livestore/adapter-web'
1
2
  import { provideOtel } from '@livestore/common'
2
- import { DbSchema, makeSchema } from '@livestore/common/schema'
3
- import type { LiveStoreContextRunning } from '@livestore/livestore'
4
- import { createStore, globalReactivityGraph, makeReactivityGraph } from '@livestore/livestore'
5
- import { Effect } from '@livestore/utils/effect'
6
- import { makeInMemoryAdapter } from '@livestore/web'
3
+ import { Events, makeSchema, State } from '@livestore/common/schema'
4
+ import type { Store } from '@livestore/livestore'
5
+ import { createStore } from '@livestore/livestore'
6
+ import { Effect, Schema } from '@livestore/utils/effect'
7
7
  import type * as otel from '@opentelemetry/api'
8
8
  import React from 'react'
9
9
 
@@ -22,55 +22,78 @@ export type AppState = {
22
22
  filter: Filter
23
23
  }
24
24
 
25
- export const todos = DbSchema.table(
26
- 'todos',
27
- {
28
- id: DbSchema.text({ primaryKey: true }),
29
- text: DbSchema.text({ default: '', nullable: false }),
30
- completed: DbSchema.boolean({ default: false, nullable: false }),
25
+ const todos = State.SQLite.table({
26
+ name: 'todos',
27
+ columns: {
28
+ id: State.SQLite.text({ primaryKey: true }),
29
+ text: State.SQLite.text({ default: '', nullable: false }),
30
+ completed: State.SQLite.boolean({ default: false, nullable: false }),
31
31
  },
32
- { deriveMutations: true, isSingleton: false },
33
- )
34
-
35
- export const app = DbSchema.table(
36
- 'app',
37
- {
38
- id: DbSchema.text({ primaryKey: true, default: 'static' }),
39
- newTodoText: DbSchema.text({ default: '', nullable: true }),
40
- filter: DbSchema.text({ default: 'all', nullable: false }),
32
+ })
33
+
34
+ const app = State.SQLite.table({
35
+ name: 'app',
36
+ columns: {
37
+ id: State.SQLite.text({ primaryKey: true, default: 'static' }),
38
+ newTodoText: State.SQLite.text({ default: '', nullable: true }),
39
+ filter: State.SQLite.text({ default: 'all', nullable: false }),
41
40
  },
42
- { isSingleton: true },
43
- )
44
-
45
- export const AppComponentSchema = DbSchema.table(
46
- 'UserInfo',
47
- {
48
- username: DbSchema.text({ default: '' }),
49
- text: DbSchema.text({ default: '' }),
41
+ })
42
+
43
+ const userInfo = State.SQLite.clientDocument({
44
+ name: 'UserInfo',
45
+ schema: Schema.Struct({
46
+ username: Schema.String,
47
+ text: Schema.String,
48
+ }),
49
+ default: { value: { username: '', text: '' } },
50
+ })
51
+
52
+ const AppRouterSchema = State.SQLite.clientDocument({
53
+ name: 'AppRouter',
54
+ schema: Schema.Struct({
55
+ currentTaskId: Schema.String.pipe(Schema.NullOr),
56
+ }),
57
+ default: {
58
+ value: { currentTaskId: null },
59
+ id: 'singleton',
50
60
  },
51
- { deriveMutations: true },
52
- )
61
+ })
62
+
63
+ export const events = {
64
+ todoCreated: Events.synced({
65
+ name: 'todoCreated',
66
+ schema: Schema.Struct({ id: Schema.String, text: Schema.String, completed: Schema.Boolean }),
67
+ }),
68
+ todoUpdated: Events.synced({
69
+ name: 'todoUpdated',
70
+ schema: Schema.Struct({
71
+ id: Schema.String,
72
+ text: Schema.String.pipe(Schema.optional),
73
+ completed: Schema.Boolean.pipe(Schema.optional),
74
+ }),
75
+ }),
76
+ AppRouterSet: AppRouterSchema.set,
77
+ UserInfoSet: userInfo.set,
78
+ }
53
79
 
54
- export const AppRouterSchema = DbSchema.table(
55
- 'AppRouter',
56
- {
57
- currentTaskId: DbSchema.text({ default: null, nullable: true }),
58
- },
59
- { isSingleton: true, deriveMutations: true },
60
- )
80
+ const materializers = State.SQLite.materializers(events, {
81
+ todoCreated: ({ id, text, completed }) => todos.insert({ id, text, completed }),
82
+ todoUpdated: ({ id, text, completed }) => todos.update({ completed, text }).where({ id }),
83
+ })
61
84
 
62
- export const tables = { todos, app, AppComponentSchema, AppRouterSchema }
63
- export const schema = makeSchema({ tables })
85
+ export const tables = { todos, app, userInfo, AppRouterSchema }
86
+
87
+ const state = State.SQLite.makeState({ tables, materializers })
88
+ export const schema = makeSchema({ state, events })
64
89
 
65
90
  export const makeTodoMvcReact = ({
66
91
  otelTracer,
67
92
  otelContext,
68
- useGlobalReactivityGraph = true,
69
93
  strictMode,
70
94
  }: {
71
95
  otelTracer?: otel.Tracer
72
96
  otelContext?: otel.Context
73
- useGlobalReactivityGraph?: boolean
74
97
  strictMode?: boolean
75
98
  } = {}) =>
76
99
  Effect.gen(function* () {
@@ -89,18 +112,20 @@ export const makeTodoMvcReact = ({
89
112
  }
90
113
  }
91
114
 
92
- const reactivityGraph = useGlobalReactivityGraph ? globalReactivityGraph : makeReactivityGraph()
93
-
94
- const store = yield* createStore({
115
+ const store: Store<any> = yield* createStore({
95
116
  schema,
96
117
  storeId: 'default',
97
118
  adapter: makeInMemoryAdapter(),
98
- reactivityGraph,
99
119
  debug: { instanceId: 'test' },
100
120
  })
101
121
 
122
+ const storeWithReactApi = LiveStoreReact.withReactApi(store)
123
+
102
124
  // TODO improve typing of `LiveStoreContext`
103
- const storeContext = { stage: 'running', store } as any as LiveStoreContextRunning
125
+ const storeContext = {
126
+ stage: 'running' as const,
127
+ store: storeWithReactApi,
128
+ }
104
129
 
105
130
  const MaybeStrictMode = strictMode ? React.StrictMode : React.Fragment
106
131
 
@@ -112,5 +137,7 @@ export const makeTodoMvcReact = ({
112
137
  </MaybeStrictMode>
113
138
  )
114
139
 
115
- return { wrapper, store, reactivityGraph, makeRenderCount }
140
+ const renderCount = makeRenderCount()
141
+
142
+ return { wrapper, store: storeWithReactApi, renderCount }
116
143
  }).pipe(provideOtel({ parentSpanContext: otelContext, otelTracer }))
@@ -1,9 +1,8 @@
1
- import type { LiveQuery } from '@livestore/livestore'
1
+ import type { LiveQueryDef } from '@livestore/livestore'
2
2
  import { computed } from '@livestore/livestore'
3
3
  import React from 'react'
4
4
 
5
5
  import { useQuery } from '../../useQuery.js'
6
- import { useScopedQuery } from '../../useScopedQuery.js'
7
6
 
8
7
  /*
9
8
  TODO:
@@ -12,7 +11,7 @@ TODO:
12
11
  */
13
12
 
14
13
  export type LiveListProps<TItem> = {
15
- items$: LiveQuery<ReadonlyArray<TItem>>
14
+ items$: LiveQueryDef<ReadonlyArray<TItem>>
16
15
  // TODO refactor render-flag to allow for transition animations on add/remove
17
16
  renderItem: (item: TItem, opts: { index: number; isInitialListRender: boolean }) => React.ReactNode
18
17
  /** Needs to be unique across all list items */
@@ -32,14 +31,18 @@ export const LiveList = <TItem,>({ items$, renderItem, getKey }: LiveListProps<T
32
31
 
33
32
  React.useEffect(() => setHasMounted(true), [])
34
33
 
35
- const keysCb = React.useCallback(() => computed((get) => get(items$).map(getKey)), [getKey, items$])
36
- const keys = useScopedQuery(keysCb, 'fixed')
34
+ const keys = useQuery(computed((get) => get(items$).map(getKey)))
37
35
  const arr = React.useMemo(
38
36
  () =>
39
37
  keys.map(
40
38
  (key) =>
41
39
  // TODO figure out a way so that `item$` returns an ordered lookup map to more efficiently find the item by key
42
- [key, computed((get) => get(items$).find((item) => getKey(item, 0) === key)!) as LiveQuery<TItem>] as const,
40
+ [
41
+ key,
42
+ computed((get) => get(items$).find((item) => getKey(item, 0) === key)!, {
43
+ deps: [key],
44
+ }) as LiveQueryDef<TItem>,
45
+ ] as const,
43
46
  ),
44
47
  [getKey, items$, keys],
45
48
  )
@@ -65,7 +68,7 @@ const ItemWrapper = <TItem,>({
65
68
  renderItem,
66
69
  }: {
67
70
  itemKey: string | number
68
- item$: LiveQuery<TItem>
71
+ item$: LiveQueryDef<TItem>
69
72
  opts: { index: number; isInitialListRender: boolean }
70
73
  renderItem: (item: TItem, opts: { index: number; isInitialListRender: boolean }) => React.ReactNode
71
74
  }) => {
package/src/mod.ts CHANGED
@@ -1,13 +1,12 @@
1
- export { LiveStoreContext, useStore } from './LiveStoreContext.js'
1
+ export { LiveStoreContext, type ReactApi } from './LiveStoreContext.js'
2
+ export { useStore, withReactApi } from './useStore.js'
2
3
  export { LiveStoreProvider } from './LiveStoreProvider.js'
3
- export { useQuery } from './useQuery.js'
4
- export { useScopedQuery } from './useScopedQuery.js'
5
4
  export { useStackInfo } from './utils/stack-info.js'
5
+ export { useQuery, useQueryRef } from './useQuery.js'
6
6
  export {
7
- useRow,
7
+ useClientDocument,
8
8
  type StateSetters,
9
9
  type SetStateAction,
10
10
  type Dispatch,
11
11
  type UseRowResult as UseStateResult,
12
- } from './useRow.js'
13
- export { useAtom } from './useAtom.js'
12
+ } from './useClientDocument.js'
@@ -0,0 +1,306 @@
1
+ import * as LiveStore from '@livestore/livestore'
2
+ import { getSimplifiedRootSpan } from '@livestore/livestore/internal/testing-utils'
3
+ import { Effect, ReadonlyRecord, Schema } from '@livestore/utils/effect'
4
+ import { Vitest } from '@livestore/utils-dev/node-vitest'
5
+ import * as otel from '@opentelemetry/api'
6
+ import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
7
+ import * as ReactTesting from '@testing-library/react'
8
+ import React from 'react'
9
+ import { beforeEach, expect, it } from 'vitest'
10
+
11
+ import { events, makeTodoMvcReact, tables } from './__tests__/fixture.js'
12
+ import type * as LiveStoreReact from './mod.js'
13
+ import { __resetUseRcResourceCache } from './useRcResource.js'
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
+ beforeEach(() => {
20
+ __resetUseRcResourceCache()
21
+ })
22
+
23
+ Vitest.scopedLive('should update the data based on component key', () =>
24
+ Effect.gen(function* () {
25
+ const { wrapper, store, renderCount } = yield* makeTodoMvcReact({})
26
+
27
+ const { result, rerender } = ReactTesting.renderHook(
28
+ (userId: string) => {
29
+ renderCount.inc()
30
+
31
+ const [state, setState, id] = store.useClientDocument(tables.userInfo, userId)
32
+ return { state, setState, id }
33
+ },
34
+ { wrapper, initialProps: 'u1' },
35
+ )
36
+
37
+ expect(result.current.id).toBe('u1')
38
+ expect(result.current.state.username).toBe('')
39
+ expect(renderCount.val).toBe(1)
40
+ expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
41
+ store.commit(tables.userInfo.set({ username: 'username_u2' }, 'u2'))
42
+
43
+ rerender('u2')
44
+
45
+ expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
46
+ expect(result.current.id).toBe('u2')
47
+ expect(result.current.state.username).toBe('username_u2')
48
+ expect(renderCount.val).toBe(2)
49
+ }),
50
+ )
51
+
52
+ // TODO add a test that makes sure React doesn't re-render when a setter is used to set the same value
53
+
54
+ Vitest.scopedLive('should update the data reactively - via setState', () =>
55
+ Effect.gen(function* () {
56
+ const { wrapper, store, renderCount } = yield* makeTodoMvcReact({})
57
+
58
+ const { result } = ReactTesting.renderHook(
59
+ (userId: string) => {
60
+ renderCount.inc()
61
+
62
+ const [state, setState, id] = store.useClientDocument(tables.userInfo, userId)
63
+ return { state, setState, id }
64
+ },
65
+ { wrapper, initialProps: 'u1' },
66
+ )
67
+
68
+ expect(result.current.id).toBe('u1')
69
+ expect(result.current.state.username).toBe('')
70
+ expect(renderCount.val).toBe(1)
71
+
72
+ ReactTesting.act(() => result.current.setState({ username: 'username_u1_hello' }))
73
+
74
+ expect(result.current.id).toBe('u1')
75
+ expect(result.current.state.username).toBe('username_u1_hello')
76
+ expect(renderCount.val).toBe(2)
77
+ }),
78
+ )
79
+
80
+ Vitest.scopedLive('should update the data reactively - via raw store commit', () =>
81
+ Effect.gen(function* () {
82
+ const { wrapper, store, renderCount } = yield* makeTodoMvcReact({})
83
+
84
+ const { result } = ReactTesting.renderHook(
85
+ (userId: string) => {
86
+ renderCount.inc()
87
+
88
+ const [state, setState, id] = store.useClientDocument(tables.userInfo, userId)
89
+ return { state, setState, id }
90
+ },
91
+ { wrapper, initialProps: 'u1' },
92
+ )
93
+
94
+ expect(result.current.id).toBe('u1')
95
+ expect(result.current.state.username).toBe('')
96
+ expect(renderCount.val).toBe(1)
97
+
98
+ ReactTesting.act(() => store.commit(events.UserInfoSet({ username: 'username_u1_hello' }, 'u1')))
99
+
100
+ expect(result.current.id).toBe('u1')
101
+ expect(result.current.state.username).toBe('username_u1_hello')
102
+ expect(renderCount.val).toBe(2)
103
+ }),
104
+ )
105
+
106
+ Vitest.scopedLive('should work for a larger app', () =>
107
+ Effect.gen(function* () {
108
+ const { wrapper, store, renderCount } = yield* makeTodoMvcReact({})
109
+
110
+ const allTodos$ = LiveStore.queryDb(
111
+ { query: `select * from todos`, schema: Schema.Array(tables.todos.rowSchema) },
112
+ { label: 'allTodos' },
113
+ )
114
+
115
+ let globalSetState: LiveStoreReact.StateSetters<typeof tables.AppRouterSchema> | undefined
116
+ const AppRouter: React.FC = () => {
117
+ renderCount.inc()
118
+
119
+ const [state, setState] = store.useClientDocument(tables.AppRouterSchema, 'singleton')
120
+
121
+ globalSetState = setState
122
+
123
+ return (
124
+ <div>
125
+ <TasksList setTaskId={(taskId) => setState({ currentTaskId: taskId })} />
126
+ <div role="current-id">Current Task Id: {state.currentTaskId ?? '-'}</div>
127
+ {state.currentTaskId ? <TaskDetails id={state.currentTaskId} /> : <div>Click on a task to see details</div>}
128
+ </div>
129
+ )
130
+ }
131
+
132
+ const TasksList: React.FC<{ setTaskId: (_: string) => void }> = ({ setTaskId }) => {
133
+ const allTodos = store.useQuery(allTodos$)
134
+
135
+ return (
136
+ <div>
137
+ {allTodos.map((_) => (
138
+ <div key={_.id} onClick={() => setTaskId(_.id)}>
139
+ {_.id}
140
+ </div>
141
+ ))}
142
+ </div>
143
+ )
144
+ }
145
+
146
+ const TaskDetails: React.FC<{ id: string }> = ({ id }) => {
147
+ const todo = store.useQuery(LiveStore.queryDb(tables.todos.where({ id }).first(), { deps: id }))
148
+ return <div role="content">{JSON.stringify(todo)}</div>
149
+ }
150
+
151
+ const renderResult = ReactTesting.render(<AppRouter />, { wrapper })
152
+
153
+ expect(renderCount.val).toBe(1)
154
+
155
+ ReactTesting.act(() =>
156
+ store.commit(
157
+ LiveStore.rawSqlEvent({
158
+ sql: LiveStore.sql`INSERT INTO todos (id, text, completed) VALUES ('t1', 'buy milk', 0)`,
159
+ }),
160
+ ),
161
+ )
162
+
163
+ expect(renderCount.val).toBe(1)
164
+ expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: -"')
165
+
166
+ ReactTesting.act(() => globalSetState!({ currentTaskId: 't1' }))
167
+
168
+ expect(renderCount.val).toBe(2)
169
+ expect(renderResult.getByRole('content').innerHTML).toMatchInlineSnapshot(
170
+ `"{"id":"t1","text":"buy milk","completed":false}"`,
171
+ )
172
+
173
+ expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: t1"')
174
+
175
+ ReactTesting.act(() =>
176
+ store.commit(
177
+ events.todoCreated({ id: 't2', text: 'buy eggs', completed: false }),
178
+ events.AppRouterSet({ currentTaskId: 't2' }),
179
+ events.todoCreated({ id: 't3', text: 'buy bread', completed: false }),
180
+ ),
181
+ )
182
+
183
+ expect(renderCount.val).toBe(3)
184
+ expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: t2"')
185
+ }),
186
+ )
187
+
188
+ Vitest.scopedLive('should work for a useClientDocument query chained with a useTemporary query', () =>
189
+ Effect.gen(function* () {
190
+ const { store, wrapper, renderCount } = yield* makeTodoMvcReact({})
191
+
192
+ store.commit(
193
+ events.todoCreated({ id: 't1', text: 'buy milk', completed: false }),
194
+ events.todoCreated({ id: 't2', text: 'buy bread', completed: false }),
195
+ )
196
+
197
+ const { result, unmount, rerender } = ReactTesting.renderHook(
198
+ (userId: string) => {
199
+ renderCount.inc()
200
+
201
+ const [_row, _setRow, _id, rowState$] = store.useClientDocument(tables.userInfo, userId)
202
+ const todos = store.useQuery(
203
+ LiveStore.queryDb(
204
+ (get) => tables.todos.where('text', 'LIKE', `%${get(rowState$).text}%`),
205
+ // TODO find a way where explicit `userId` is not needed here
206
+ // possibly by automatically understanding the `get(rowState$)` dependency
207
+ { label: 'todosFiltered', deps: userId },
208
+ ),
209
+ // TODO introduce a `deps` array which is only needed when a query is parametric
210
+ )
211
+
212
+ return { todos }
213
+ },
214
+ { wrapper, initialProps: 'u1' },
215
+ )
216
+
217
+ ReactTesting.act(() => store.commit(events.UserInfoSet({ username: 'username_u2', text: 'milk' }, 'u2')))
218
+
219
+ expect(result.current.todos.length).toBe(2)
220
+ expect(renderCount.val).toBe(1)
221
+
222
+ rerender('u2')
223
+
224
+ expect(result.current.todos.length).toBe(1)
225
+ expect(renderCount.val).toBe(2)
226
+
227
+ unmount()
228
+ }),
229
+ )
230
+
231
+ Vitest.describe('otel', () => {
232
+ it.each([{ strictMode: true }, { strictMode: false }])(
233
+ 'should update the data based on component key strictMode=%s',
234
+ async ({ strictMode }) => {
235
+ const exporter = new InMemorySpanExporter()
236
+
237
+ const provider = new BasicTracerProvider({
238
+ spanProcessors: [new SimpleSpanProcessor(exporter)],
239
+ })
240
+
241
+ const otelTracer = provider.getTracer(`testing-${strictMode ? 'strict' : 'non-strict'}`)
242
+
243
+ const span = otelTracer.startSpan('test-root')
244
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
245
+
246
+ await Effect.gen(function* () {
247
+ const { wrapper, store, renderCount } = yield* makeTodoMvcReact({
248
+ otelContext,
249
+ otelTracer,
250
+ strictMode,
251
+ })
252
+
253
+ const { result, rerender, unmount } = ReactTesting.renderHook(
254
+ (userId: string) => {
255
+ renderCount.inc()
256
+
257
+ const [state, setState, id] = store.useClientDocument(tables.userInfo, userId)
258
+ return { state, setState, id }
259
+ },
260
+ { wrapper, initialProps: 'u1' },
261
+ )
262
+
263
+ expect(result.current.id).toBe('u1')
264
+ expect(result.current.state.username).toBe('')
265
+ expect(renderCount.val).toBe(1)
266
+
267
+ // For u2 we'll make sure that the row already exists,
268
+ // so the lazy `insert` will be skipped
269
+ ReactTesting.act(() => store.commit(events.UserInfoSet({ username: 'username_u2' }, 'u2')))
270
+
271
+ rerender('u2')
272
+
273
+ expect(result.current.id).toBe('u2')
274
+ expect(result.current.state.username).toBe('username_u2')
275
+ expect(renderCount.val).toBe(2)
276
+
277
+ unmount()
278
+ span.end()
279
+ }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise)
280
+
281
+ await provider.forceFlush()
282
+
283
+ const mapAttributes = (attributes: otel.Attributes) => {
284
+ return ReadonlyRecord.map(attributes, (val, key) => {
285
+ if (key === 'firstStackInfo') {
286
+ const stackInfo = JSON.parse(val as string) as LiveStore.StackInfo
287
+ // stackInfo.frames.shift() // Removes `renderHook.wrapper` from the stack
288
+ stackInfo.frames.forEach((_) => {
289
+ if (_.name.includes('renderHook.wrapper')) {
290
+ _.name = 'renderHook.wrapper'
291
+ }
292
+ _.filePath = '__REPLACED_FOR_SNAPSHOT__'
293
+ })
294
+ return JSON.stringify(stackInfo)
295
+ }
296
+ return val
297
+ })
298
+ }
299
+
300
+ expect(getSimplifiedRootSpan(exporter, mapAttributes)).toMatchSnapshot()
301
+
302
+ await provider.shutdown()
303
+ },
304
+ )
305
+ })
306
+ })