@livestore/react 0.0.0-snapshot-8d3edf87cb1e88c7b67c5f3ea9d0b307253c33df

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 (87) hide show
  1. package/README.md +1 -0
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/LiveStoreContext.d.ts +7 -0
  4. package/dist/LiveStoreContext.d.ts.map +1 -0
  5. package/dist/LiveStoreContext.js +13 -0
  6. package/dist/LiveStoreContext.js.map +1 -0
  7. package/dist/LiveStoreProvider.d.ts +49 -0
  8. package/dist/LiveStoreProvider.d.ts.map +1 -0
  9. package/dist/LiveStoreProvider.js +168 -0
  10. package/dist/LiveStoreProvider.js.map +1 -0
  11. package/dist/LiveStoreProvider.test.d.ts +2 -0
  12. package/dist/LiveStoreProvider.test.d.ts.map +1 -0
  13. package/dist/LiveStoreProvider.test.js +62 -0
  14. package/dist/LiveStoreProvider.test.js.map +1 -0
  15. package/dist/__tests__/fixture.d.ts +567 -0
  16. package/dist/__tests__/fixture.d.ts.map +1 -0
  17. package/dist/__tests__/fixture.js +61 -0
  18. package/dist/__tests__/fixture.js.map +1 -0
  19. package/dist/experimental/components/LiveList.d.ts +21 -0
  20. package/dist/experimental/components/LiveList.d.ts.map +1 -0
  21. package/dist/experimental/components/LiveList.js +31 -0
  22. package/dist/experimental/components/LiveList.js.map +1 -0
  23. package/dist/experimental/mod.d.ts +2 -0
  24. package/dist/experimental/mod.d.ts.map +1 -0
  25. package/dist/experimental/mod.js +2 -0
  26. package/dist/experimental/mod.js.map +1 -0
  27. package/dist/mod.d.ts +8 -0
  28. package/dist/mod.d.ts.map +1 -0
  29. package/dist/mod.js +8 -0
  30. package/dist/mod.js.map +1 -0
  31. package/dist/useAtom.d.ts +10 -0
  32. package/dist/useAtom.d.ts.map +1 -0
  33. package/dist/useAtom.js +37 -0
  34. package/dist/useAtom.js.map +1 -0
  35. package/dist/useQuery.d.ts +9 -0
  36. package/dist/useQuery.d.ts.map +1 -0
  37. package/dist/useQuery.js +88 -0
  38. package/dist/useQuery.js.map +1 -0
  39. package/dist/useQuery.test.d.ts +2 -0
  40. package/dist/useQuery.test.d.ts.map +1 -0
  41. package/dist/useQuery.test.js +51 -0
  42. package/dist/useQuery.test.js.map +1 -0
  43. package/dist/useRow.d.ts +46 -0
  44. package/dist/useRow.d.ts.map +1 -0
  45. package/dist/useRow.js +96 -0
  46. package/dist/useRow.js.map +1 -0
  47. package/dist/useRow.test.d.ts +2 -0
  48. package/dist/useRow.test.d.ts.map +1 -0
  49. package/dist/useRow.test.js +212 -0
  50. package/dist/useRow.test.js.map +1 -0
  51. package/dist/useTemporaryQuery.d.ts +22 -0
  52. package/dist/useTemporaryQuery.d.ts.map +1 -0
  53. package/dist/useTemporaryQuery.js +75 -0
  54. package/dist/useTemporaryQuery.js.map +1 -0
  55. package/dist/useTemporaryQuery.test.d.ts +2 -0
  56. package/dist/useTemporaryQuery.test.d.ts.map +1 -0
  57. package/dist/useTemporaryQuery.test.js +59 -0
  58. package/dist/useTemporaryQuery.test.js.map +1 -0
  59. package/dist/utils/stack-info.d.ts +4 -0
  60. package/dist/utils/stack-info.d.ts.map +1 -0
  61. package/dist/utils/stack-info.js +11 -0
  62. package/dist/utils/stack-info.js.map +1 -0
  63. package/dist/utils/useStateRefWithReactiveInput.d.ts +13 -0
  64. package/dist/utils/useStateRefWithReactiveInput.d.ts.map +1 -0
  65. package/dist/utils/useStateRefWithReactiveInput.js +38 -0
  66. package/dist/utils/useStateRefWithReactiveInput.js.map +1 -0
  67. package/package.json +54 -0
  68. package/src/LiveStoreContext.ts +19 -0
  69. package/src/LiveStoreProvider.test.tsx +109 -0
  70. package/src/LiveStoreProvider.tsx +295 -0
  71. package/src/__snapshots__/useRow.test.tsx.snap +359 -0
  72. package/src/__tests__/fixture.tsx +119 -0
  73. package/src/ambient.d.ts +2 -0
  74. package/src/experimental/components/LiveList.tsx +84 -0
  75. package/src/experimental/mod.ts +1 -0
  76. package/src/mod.ts +13 -0
  77. package/src/useAtom.ts +55 -0
  78. package/src/useQuery.test.tsx +82 -0
  79. package/src/useQuery.ts +122 -0
  80. package/src/useRow.test.tsx +346 -0
  81. package/src/useRow.ts +182 -0
  82. package/src/useTemporaryQuery.test.tsx +98 -0
  83. package/src/useTemporaryQuery.ts +131 -0
  84. package/src/utils/stack-info.ts +13 -0
  85. package/src/utils/useStateRefWithReactiveInput.ts +51 -0
  86. package/tsconfig.json +20 -0
  87. package/vitest.config.js +17 -0
@@ -0,0 +1,82 @@
1
+ import { querySQL } from '@livestore/livestore'
2
+ import { Effect, Schema } from '@livestore/utils/effect'
3
+ import { renderHook } from '@testing-library/react'
4
+ import React from 'react'
5
+ import { describe, expect, it } from 'vitest'
6
+
7
+ import { makeTodoMvcReact, tables, todos } from './__tests__/fixture.js'
8
+ import * as LiveStoreReact from './mod.js'
9
+
10
+ describe('useQuery', () => {
11
+ it('simple', () =>
12
+ Effect.gen(function* () {
13
+ const { wrapper, store, makeRenderCount } = yield* makeTodoMvcReact()
14
+
15
+ const renderCount = makeRenderCount()
16
+
17
+ const allTodos$ = querySQL(`select * from todos`, { schema: Schema.Array(tables.todos.schema) })
18
+
19
+ const { result } = renderHook(
20
+ () => {
21
+ renderCount.inc()
22
+
23
+ return LiveStoreReact.useQuery(allTodos$)
24
+ },
25
+ { wrapper },
26
+ )
27
+
28
+ expect(result.current.length).toBe(0)
29
+ expect(renderCount.val).toBe(1)
30
+
31
+ React.act(() => store.mutate(todos.insert({ id: 't1', text: 'buy milk', completed: false })))
32
+
33
+ expect(result.current.length).toBe(1)
34
+ expect(result.current[0]!.text).toBe('buy milk')
35
+ expect(renderCount.val).toBe(2)
36
+ }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
37
+
38
+ it('same `useQuery` hook invoked with different queries', () =>
39
+ Effect.gen(function* () {
40
+ const { wrapper, store, makeRenderCount } = yield* makeTodoMvcReact()
41
+
42
+ const renderCount = makeRenderCount()
43
+
44
+ const todo1$ = querySQL(`select * from todos where id = 't1'`, {
45
+ label: 'libraryTracksView1',
46
+ schema: Schema.Array(tables.todos.schema),
47
+ })
48
+ const todo2$ = querySQL(`select * from todos where id = 't2'`, {
49
+ label: 'libraryTracksView2',
50
+ schema: Schema.Array(tables.todos.schema),
51
+ })
52
+
53
+ store.mutate(
54
+ todos.insert({ id: 't1', text: 'buy milk', completed: false }),
55
+ todos.insert({ id: 't2', text: 'buy eggs', completed: false }),
56
+ )
57
+
58
+ const { result, rerender } = renderHook(
59
+ (todoId: string) => {
60
+ renderCount.inc()
61
+
62
+ const query$ = React.useMemo(() => (todoId === 't1' ? todo1$ : todo2$), [todoId])
63
+
64
+ return LiveStoreReact.useQuery(query$)[0]!.text
65
+ },
66
+ { wrapper, initialProps: 't1' },
67
+ )
68
+
69
+ expect(result.current).toBe('buy milk')
70
+ expect(renderCount.val).toBe(1)
71
+
72
+ React.act(() => store.mutate(todos.update({ where: { id: 't1' }, values: { text: 'buy soy milk' } })))
73
+
74
+ expect(result.current).toBe('buy soy milk')
75
+ expect(renderCount.val).toBe(2)
76
+
77
+ rerender('t2')
78
+
79
+ expect(result.current).toBe('buy eggs')
80
+ expect(renderCount.val).toBe(3)
81
+ }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
82
+ })
@@ -0,0 +1,122 @@
1
+ import type { GetResult, LiveQueryAny } from '@livestore/livestore'
2
+ import { extractStackInfoFromStackTrace, stackInfoToString } from '@livestore/livestore'
3
+ import { deepEqual, indent } from '@livestore/utils'
4
+ import * as otel from '@opentelemetry/api'
5
+ import React from 'react'
6
+
7
+ import { useStore } from './LiveStoreContext.js'
8
+ import { originalStackLimit } from './utils/stack-info.js'
9
+ import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInput.js'
10
+
11
+ /**
12
+ * NOTE Some folks have suggested to use `React.useSyncExternalStore`, however, it's not doing anything special
13
+ * for what's needed here, so we handle everything manually.
14
+ */
15
+
16
+ /**
17
+ * This is needed because the `React.useMemo` call below, can sometimes be called multiple times 🤷,
18
+ * so we need to "cache" the fact that we've already started a span for this component.
19
+ * The map entry is being removed again in the `React.useEffect` call below.
20
+ */
21
+ const spanAlreadyStartedCache = new Map<LiveQueryAny, { span: otel.Span; otelContext: otel.Context }>()
22
+
23
+ export const useQuery = <TQuery extends LiveQueryAny>(query: TQuery): GetResult<TQuery> => useQueryRef(query).current
24
+
25
+ /**
26
+ *
27
+ */
28
+ export const useQueryRef = <TQuery extends LiveQueryAny>(
29
+ query$: TQuery,
30
+ parentOtelContext?: otel.Context,
31
+ ): React.MutableRefObject<GetResult<TQuery>> => {
32
+ const { store } = useStore()
33
+
34
+ React.useDebugValue(`LiveStore:useQuery:${query$.id}:${query$.label}`)
35
+ // console.debug(`LiveStore:useQuery:${query$.id}:${query$.label}`)
36
+
37
+ const stackInfo = React.useMemo(() => {
38
+ Error.stackTraceLimit = 10
39
+ // eslint-disable-next-line unicorn/error-message
40
+ const stack = new Error().stack!
41
+ Error.stackTraceLimit = originalStackLimit
42
+ return extractStackInfoFromStackTrace(stack)
43
+ }, [])
44
+
45
+ // The following `React.useMemo` and `React.useEffect` calls are used to start and end a span for the lifetime of this component.
46
+ const { span, otelContext } = React.useMemo(() => {
47
+ const existingSpan = spanAlreadyStartedCache.get(query$)
48
+ if (existingSpan !== undefined) return existingSpan
49
+
50
+ const span = store.otel.tracer.startSpan(
51
+ `LiveStore:useQuery:${query$.label}`,
52
+ { attributes: { label: query$.label, stackInfo: JSON.stringify(stackInfo) } },
53
+ parentOtelContext ?? store.otel.queriesSpanContext,
54
+ )
55
+
56
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
57
+
58
+ spanAlreadyStartedCache.set(query$, { span, otelContext })
59
+
60
+ return { span, otelContext }
61
+ }, [parentOtelContext, query$, stackInfo, store.otel.queriesSpanContext, store.otel.tracer])
62
+
63
+ const initialResult = React.useMemo(() => {
64
+ try {
65
+ return query$.run(otelContext, {
66
+ _tag: 'react',
67
+ api: 'useQuery',
68
+ label: query$.label,
69
+ stackInfo,
70
+ })
71
+ } catch (cause: any) {
72
+ throw new Error(
73
+ `\
74
+ [@livestore/react:useQuery] Error running query: ${cause.name}
75
+
76
+ Query: ${query$.label}
77
+
78
+ React trace:
79
+
80
+ ${indent(stackInfoToString(stackInfo), 4)}
81
+
82
+ Stack trace:
83
+ `,
84
+ { cause },
85
+ )
86
+ }
87
+ }, [otelContext, query$, stackInfo])
88
+
89
+ // We know the query has a result by the time we use it; so we can synchronously populate a default state
90
+ const [valueRef, setValue] = useStateRefWithReactiveInput<GetResult<TQuery>>(initialResult)
91
+
92
+ React.useEffect(
93
+ () => () => {
94
+ spanAlreadyStartedCache.delete(query$)
95
+ span.end()
96
+ },
97
+ [query$, span],
98
+ )
99
+
100
+ // Subscribe to future updates for this query
101
+ React.useEffect(() => {
102
+ query$.activeSubscriptions.add(stackInfo)
103
+
104
+ return store.subscribe(
105
+ query$,
106
+ (newValue) => {
107
+ // NOTE: we return a reference to the result object within LiveStore;
108
+ // this implies that app code must not mutate the results, or else
109
+ // there may be weird reactivity bugs.
110
+ if (deepEqual(newValue, valueRef.current) === false) {
111
+ setValue(newValue)
112
+ }
113
+ },
114
+ () => {
115
+ query$.activeSubscriptions.delete(stackInfo)
116
+ },
117
+ { label: query$.label, otelContext },
118
+ )
119
+ }, [stackInfo, query$, setValue, store, valueRef, otelContext, span])
120
+
121
+ return valueRef
122
+ }
@@ -0,0 +1,346 @@
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 * as otel from '@opentelemetry/api'
5
+ import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
6
+ import { render, renderHook } from '@testing-library/react'
7
+ import React from 'react'
8
+ import { describe, expect, it } from 'vitest'
9
+
10
+ import { AppComponentSchema, AppRouterSchema, makeTodoMvcReact, tables, todos } from './__tests__/fixture.js'
11
+ import * as LiveStoreReact from './mod.js'
12
+
13
+ // NOTE running tests concurrently doesn't work with the default global db graph
14
+ describe('useRow', () => {
15
+ it('should update the data based on component key', () =>
16
+ Effect.gen(function* () {
17
+ const { wrapper, store, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
18
+ useGlobalReactivityGraph: false,
19
+ })
20
+
21
+ const renderCount = makeRenderCount()
22
+
23
+ const { result, rerender } = renderHook(
24
+ (userId: string) => {
25
+ renderCount.inc()
26
+
27
+ const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
28
+ return { state, setState }
29
+ },
30
+ { wrapper, initialProps: 'u1' },
31
+ )
32
+
33
+ expect(result.current.state.id).toBe('u1')
34
+ expect(result.current.state.username).toBe('')
35
+ expect(renderCount.val).toBe(1)
36
+
37
+ React.act(() =>
38
+ store.mutate(
39
+ LiveStore.rawSqlMutation({
40
+ sql: LiveStore.sql`INSERT INTO UserInfo (id, username) VALUES ('u2', 'username_u2')`,
41
+ }),
42
+ ),
43
+ )
44
+
45
+ rerender('u2')
46
+
47
+ expect(result.current.state.id).toBe('u2')
48
+ expect(result.current.state.username).toBe('username_u2')
49
+ expect(renderCount.val).toBe(2)
50
+ }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
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
+ it('should update the data reactively - via setState', () =>
55
+ Effect.gen(function* () {
56
+ const { wrapper, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
57
+ useGlobalReactivityGraph: false,
58
+ })
59
+
60
+ const renderCount = makeRenderCount()
61
+
62
+ const { result } = renderHook(
63
+ (userId: string) => {
64
+ renderCount.inc()
65
+
66
+ const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
67
+ return { state, setState }
68
+ },
69
+ { wrapper, initialProps: 'u1' },
70
+ )
71
+
72
+ expect(result.current.state.id).toBe('u1')
73
+ expect(result.current.state.username).toBe('')
74
+ expect(renderCount.val).toBe(1)
75
+
76
+ React.act(() => result.current.setState.username('username_u1_hello'))
77
+
78
+ expect(result.current.state.id).toBe('u1')
79
+ expect(result.current.state.username).toBe('username_u1_hello')
80
+ expect(renderCount.val).toBe(2)
81
+ }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
82
+
83
+ it('should update the data reactively - via raw store mutation', () =>
84
+ Effect.gen(function* () {
85
+ const { wrapper, store, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
86
+ useGlobalReactivityGraph: false,
87
+ })
88
+
89
+ const renderCount = makeRenderCount()
90
+
91
+ const { result } = renderHook(
92
+ (userId: string) => {
93
+ renderCount.inc()
94
+
95
+ const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
96
+ return { state, setState }
97
+ },
98
+ { wrapper, initialProps: 'u1' },
99
+ )
100
+
101
+ expect(result.current.state.id).toBe('u1')
102
+ expect(result.current.state.username).toBe('')
103
+ expect(renderCount.val).toBe(1)
104
+
105
+ React.act(() =>
106
+ store.mutate(
107
+ LiveStore.rawSqlMutation({
108
+ sql: LiveStore.sql`UPDATE UserInfo SET username = 'username_u1_hello' WHERE id = 'u1';`,
109
+ }),
110
+ ),
111
+ )
112
+
113
+ expect(result.current.state.id).toBe('u1')
114
+ expect(result.current.state.username).toBe('username_u1_hello')
115
+ expect(renderCount.val).toBe(2)
116
+ }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
117
+
118
+ it('should work for a larger app', () =>
119
+ Effect.gen(function* () {
120
+ const { wrapper, store, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
121
+ useGlobalReactivityGraph: false,
122
+ })
123
+
124
+ const allTodos$ = LiveStore.querySQL(`select * from todos`, {
125
+ label: 'allTodos',
126
+ schema: Schema.Array(tables.todos.schema),
127
+ reactivityGraph,
128
+ })
129
+
130
+ const appRouterRenderCount = makeRenderCount()
131
+ let globalSetState: LiveStoreReact.StateSetters<typeof AppRouterSchema> | undefined
132
+ const AppRouter: React.FC = () => {
133
+ appRouterRenderCount.inc()
134
+
135
+ const [state, setState] = LiveStoreReact.useRow(AppRouterSchema, { reactivityGraph })
136
+
137
+ globalSetState = setState
138
+
139
+ return (
140
+ <div>
141
+ <TasksList setTaskId={setState.currentTaskId} />
142
+ <div role="current-id">Current Task Id: {state.currentTaskId ?? '-'}</div>
143
+ {state.currentTaskId ? <TaskDetails id={state.currentTaskId} /> : <div>Click on a task to see details</div>}
144
+ </div>
145
+ )
146
+ }
147
+
148
+ const TasksList: React.FC<{ setTaskId: (_: string) => void }> = ({ setTaskId }) => {
149
+ const allTodos = LiveStoreReact.useQuery(allTodos$)
150
+
151
+ return (
152
+ <div>
153
+ {allTodos.map((_) => (
154
+ <div key={_.id} onClick={() => setTaskId(_.id)}>
155
+ {_.id}
156
+ </div>
157
+ ))}
158
+ </div>
159
+ )
160
+ }
161
+
162
+ const TaskDetails: React.FC<{ id: string }> = ({ id }) => {
163
+ const [todo] = LiveStoreReact.useRow(todos, id, { reactivityGraph })
164
+ return <div role="content">{JSON.stringify(todo)}</div>
165
+ }
166
+
167
+ const renderResult = render(<AppRouter />, { wrapper })
168
+
169
+ expect(appRouterRenderCount.val).toBe(1)
170
+
171
+ React.act(() =>
172
+ store.mutate(
173
+ LiveStore.rawSqlMutation({
174
+ sql: LiveStore.sql`INSERT INTO todos (id, text, completed) VALUES ('t1', 'buy milk', 0)`,
175
+ }),
176
+ ),
177
+ )
178
+
179
+ expect(appRouterRenderCount.val).toBe(1)
180
+ expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: -"')
181
+
182
+ React.act(() => globalSetState!.currentTaskId('t1'))
183
+
184
+ expect(appRouterRenderCount.val).toBe(2)
185
+ expect(renderResult.getByRole('content').innerHTML).toMatchInlineSnapshot(
186
+ `"{"id":"t1","text":"buy milk","completed":false}"`,
187
+ )
188
+
189
+ expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: t1"')
190
+
191
+ React.act(() =>
192
+ store.mutate(
193
+ LiveStore.rawSqlMutation({
194
+ sql: LiveStore.sql`INSERT INTO todos (id, text, completed) VALUES ('t2', 'buy eggs', 0)`,
195
+ }),
196
+ AppRouterSchema.update({ where: { id: 'singleton' }, values: { currentTaskId: 't2' } }),
197
+ LiveStore.rawSqlMutation({
198
+ sql: LiveStore.sql`INSERT INTO todos (id, text, completed) VALUES ('t3', 'buy bread', 0)`,
199
+ }),
200
+ ),
201
+ )
202
+
203
+ expect(appRouterRenderCount.val).toBe(3)
204
+ expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: t2"')
205
+ }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
206
+
207
+ it('should work for a useRow query chained with a useTemporary query', () =>
208
+ Effect.gen(function* () {
209
+ const { store, wrapper, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
210
+ useGlobalReactivityGraph: false,
211
+ })
212
+ const renderCount = makeRenderCount()
213
+
214
+ store.mutate(
215
+ todos.insert({ id: 't1', text: 'buy milk', completed: false }),
216
+ todos.insert({ id: 't2', text: 'buy bread', completed: false }),
217
+ )
218
+
219
+ const { result, unmount, rerender } = renderHook(
220
+ (userId: string) => {
221
+ renderCount.inc()
222
+
223
+ const [_row, _setRow, rowState$] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
224
+ const todos = LiveStoreReact.useTemporaryQuery(
225
+ () =>
226
+ LiveStore.querySQL(
227
+ (get) => LiveStore.sql`select * from todos where text like '%${get(rowState$).text}%'`,
228
+ {
229
+ schema: Schema.Array(tables.todos.schema),
230
+ reactivityGraph,
231
+ label: 'todosFiltered',
232
+ },
233
+ ),
234
+ userId,
235
+ )
236
+
237
+ return { todos }
238
+ },
239
+ { wrapper, initialProps: 'u1' },
240
+ )
241
+
242
+ React.act(() =>
243
+ store.mutate(
244
+ LiveStore.rawSqlMutation({
245
+ sql: LiveStore.sql`INSERT INTO UserInfo (id, username, text) VALUES ('u2', 'username_u2', 'milk')`,
246
+ }),
247
+ ),
248
+ )
249
+
250
+ expect(result.current.todos.length).toBe(2)
251
+ // expect(result.current.state.username).toBe('')
252
+ expect(renderCount.val).toBe(1)
253
+
254
+ rerender('u2')
255
+
256
+ expect(result.current.todos.length).toBe(1)
257
+ expect(renderCount.val).toBe(2)
258
+
259
+ unmount()
260
+ }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
261
+
262
+ let cachedProvider: BasicTracerProvider | undefined
263
+
264
+ describe('otel', () => {
265
+ const exporter = new InMemorySpanExporter()
266
+
267
+ const provider = cachedProvider ?? new BasicTracerProvider()
268
+ cachedProvider = provider
269
+ provider.addSpanProcessor(new SimpleSpanProcessor(exporter))
270
+ provider.register()
271
+
272
+ const otelTracer = otel.trace.getTracer('test')
273
+
274
+ const span = otelTracer.startSpan('test')
275
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
276
+
277
+ it('should update the data based on component key', async () => {
278
+ const { strictMode } = await Effect.gen(function* () {
279
+ const { wrapper, store, reactivityGraph, makeRenderCount, strictMode } = yield* makeTodoMvcReact({
280
+ useGlobalReactivityGraph: false,
281
+ otelContext,
282
+ otelTracer,
283
+ })
284
+
285
+ const renderCount = makeRenderCount()
286
+
287
+ const { result, rerender, unmount } = renderHook(
288
+ (userId: string) => {
289
+ renderCount.inc()
290
+
291
+ const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
292
+ return { state, setState }
293
+ },
294
+ { wrapper, initialProps: 'u1' },
295
+ )
296
+
297
+ expect(result.current.state.id).toBe('u1')
298
+ expect(result.current.state.username).toBe('')
299
+ expect(renderCount.val).toBe(1)
300
+
301
+ React.act(() =>
302
+ store.mutate(
303
+ LiveStore.rawSqlMutation({
304
+ sql: LiveStore.sql`INSERT INTO UserInfo (id, username) VALUES ('u2', 'username_u2')`,
305
+ }),
306
+ ),
307
+ )
308
+
309
+ rerender('u2')
310
+
311
+ expect(result.current.state.id).toBe('u2')
312
+ expect(result.current.state.username).toBe('username_u2')
313
+ expect(renderCount.val).toBe(2)
314
+
315
+ unmount()
316
+ span.end()
317
+
318
+ return { strictMode }
319
+ }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise)
320
+
321
+ const mapAttributes = (attributes: otel.Attributes) => {
322
+ return ReadonlyRecord.map(attributes, (val, key) => {
323
+ if (key === 'stackInfo') {
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')) {
328
+ _.name = 'renderHook.wrapper'
329
+ }
330
+ _.filePath = '__REPLACED_FOR_SNAPSHOT__'
331
+ })
332
+ return JSON.stringify(stackInfo)
333
+ }
334
+ return val
335
+ })
336
+ }
337
+
338
+ // TODO improve testing setup so "obsolete" warning is avoided
339
+ if (strictMode) {
340
+ expect(getSimplifiedRootSpan(exporter, mapAttributes)).toMatchSnapshot('strictMode=true')
341
+ } else {
342
+ expect(getSimplifiedRootSpan(exporter, mapAttributes)).toMatchSnapshot('strictMode=false')
343
+ }
344
+ })
345
+ })
346
+ })