muya 2.5.3 → 2.5.5

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 (134) hide show
  1. package/{src/__tests__ → __tests__}/bench.test.tsx +4 -4
  2. package/{src/__tests__ → __tests__}/compare.test.tsx +8 -6
  3. package/{src/__tests__ → __tests__}/create.test.tsx +2 -2
  4. package/{src/utils/__tests__ → __tests__}/is.test.ts +3 -3
  5. package/{src/__tests__ → __tests__}/scheduler.test.tsx +1 -1
  6. package/{src/__tests__ → __tests__}/select.test.tsx +5 -5
  7. package/{src/utils/__tests__ → __tests__}/shallow.test.ts +1 -1
  8. package/{src/__tests__ → __tests__}/use-value-loadable.test.tsx +3 -3
  9. package/{src/__tests__ → __tests__}/use-value.test.tsx +8 -8
  10. package/build.ts +67 -0
  11. package/dist/cjs/index.js +1 -0
  12. package/dist/esm/create.js +1 -0
  13. package/dist/esm/debug/development-tools.js +1 -0
  14. package/dist/esm/index.js +1 -0
  15. package/dist/esm/select.js +1 -0
  16. package/{types → dist/types}/create-state.d.ts +1 -0
  17. package/dist/types/create-state.d.ts.map +1 -0
  18. package/{types → dist/types}/create.d.ts +1 -0
  19. package/dist/types/create.d.ts.map +1 -0
  20. package/dist/types/debug/development-tools.d.ts +13 -0
  21. package/dist/types/debug/development-tools.d.ts.map +1 -0
  22. package/{types → dist/types}/index.d.ts +3 -1
  23. package/dist/types/index.d.ts.map +1 -0
  24. package/{types → dist/types}/scheduler.d.ts +1 -0
  25. package/dist/types/scheduler.d.ts.map +1 -0
  26. package/{types → dist/types}/select.d.ts +1 -0
  27. package/dist/types/select.d.ts.map +1 -0
  28. package/{types → dist/types}/types.d.ts +1 -0
  29. package/dist/types/types.d.ts.map +1 -0
  30. package/{types → dist/types}/use-value-loadable.d.ts +1 -0
  31. package/dist/types/use-value-loadable.d.ts.map +1 -0
  32. package/{types → dist/types}/use-value.d.ts +2 -1
  33. package/dist/types/use-value.d.ts.map +1 -0
  34. package/{types → dist/types}/utils/common.d.ts +1 -0
  35. package/dist/types/utils/common.d.ts.map +1 -0
  36. package/{types → dist/types}/utils/create-emitter.d.ts +1 -0
  37. package/dist/types/utils/create-emitter.d.ts.map +1 -0
  38. package/{types → dist/types}/utils/id.d.ts +1 -0
  39. package/dist/types/utils/id.d.ts.map +1 -0
  40. package/{types → dist/types}/utils/is.d.ts +1 -0
  41. package/dist/types/utils/is.d.ts.map +1 -0
  42. package/{types → dist/types}/utils/shallow.d.ts +1 -0
  43. package/dist/types/utils/shallow.d.ts.map +1 -0
  44. package/package.json +23 -8
  45. package/src/create-state.d.ts.map +1 -0
  46. package/src/create.d.ts.map +1 -0
  47. package/src/create.ts +7 -2
  48. package/src/debug/development-tools.d.ts.map +1 -0
  49. package/src/debug/development-tools.ts +5 -40
  50. package/src/index.d.ts.map +1 -0
  51. package/src/index.ts +2 -1
  52. package/src/scheduler.d.ts.map +1 -0
  53. package/src/select.d.ts.map +1 -0
  54. package/src/select.ts +7 -2
  55. package/src/types.d.ts.map +1 -0
  56. package/src/use-value-loadable.d.ts.map +1 -0
  57. package/src/use-value.d.ts.map +1 -0
  58. package/src/use-value.ts +1 -1
  59. package/src/utils/common.d.ts.map +1 -0
  60. package/src/utils/create-emitter.d.ts.map +1 -0
  61. package/src/utils/id.d.ts.map +1 -0
  62. package/src/utils/is.d.ts.map +1 -0
  63. package/src/utils/shallow.d.ts.map +1 -0
  64. package/tsconfig.build.json +12 -0
  65. package/cjs/index.js +0 -1
  66. package/esm/__tests__/test-utils.js +0 -1
  67. package/esm/create.js +0 -1
  68. package/esm/debug/development-tools.js +0 -1
  69. package/esm/index.js +0 -1
  70. package/esm/select.js +0 -1
  71. package/esm/sqlite/__tests__/create-sqlite.test.js +0 -1
  72. package/esm/sqlite/__tests__/map-deque.test.js +0 -1
  73. package/esm/sqlite/__tests__/table.test.js +0 -1
  74. package/esm/sqlite/__tests__/tokenizer.test.js +0 -1
  75. package/esm/sqlite/__tests__/where.test.js +0 -1
  76. package/esm/sqlite/create-sqlite.js +0 -1
  77. package/esm/sqlite/index.js +0 -1
  78. package/esm/sqlite/table/backend.js +0 -1
  79. package/esm/sqlite/table/bun-backend.js +0 -1
  80. package/esm/sqlite/table/index.js +0 -1
  81. package/esm/sqlite/table/map-deque.js +0 -1
  82. package/esm/sqlite/table/table.js +0 -43
  83. package/esm/sqlite/table/table.types.js +0 -0
  84. package/esm/sqlite/table/tokenizer.js +0 -1
  85. package/esm/sqlite/table/where.js +0 -1
  86. package/esm/sqlite/use-sqlite-count.js +0 -1
  87. package/esm/sqlite/use-sqlite.js +0 -1
  88. package/esm/utils/__tests__/is.test.js +0 -1
  89. package/esm/utils/__tests__/shallow.test.js +0 -1
  90. package/src/sqlite/__tests__/create-sqlite.test.ts +0 -264
  91. package/src/sqlite/__tests__/map-deque.test.ts +0 -61
  92. package/src/sqlite/__tests__/table.test.ts +0 -351
  93. package/src/sqlite/__tests__/tokenizer.test.ts +0 -43
  94. package/src/sqlite/__tests__/use-slite-count.test.tsx +0 -96
  95. package/src/sqlite/__tests__/use-sqlite.more.test.tsx +0 -637
  96. package/src/sqlite/__tests__/use-sqlite.test.tsx +0 -1008
  97. package/src/sqlite/__tests__/where.test.ts +0 -234
  98. package/src/sqlite/create-sqlite.ts +0 -164
  99. package/src/sqlite/index.ts +0 -4
  100. package/src/sqlite/table/backend.ts +0 -21
  101. package/src/sqlite/table/bun-backend.ts +0 -47
  102. package/src/sqlite/table/index.ts +0 -6
  103. package/src/sqlite/table/map-deque.ts +0 -29
  104. package/src/sqlite/table/table.ts +0 -353
  105. package/src/sqlite/table/table.types.ts +0 -129
  106. package/src/sqlite/table/tokenizer.ts +0 -35
  107. package/src/sqlite/table/where.ts +0 -207
  108. package/src/sqlite/use-sqlite-count.ts +0 -69
  109. package/src/sqlite/use-sqlite.ts +0 -250
  110. package/types/__tests__/test-utils.d.ts +0 -25
  111. package/types/debug/development-tools.d.ts +0 -8
  112. package/types/sqlite/create-sqlite.d.ts +0 -31
  113. package/types/sqlite/index.d.ts +0 -4
  114. package/types/sqlite/table/backend.d.ts +0 -20
  115. package/types/sqlite/table/bun-backend.d.ts +0 -6
  116. package/types/sqlite/table/index.d.ts +0 -6
  117. package/types/sqlite/table/map-deque.d.ts +0 -5
  118. package/types/sqlite/table/table.d.ts +0 -21
  119. package/types/sqlite/table/table.types.d.ts +0 -91
  120. package/types/sqlite/table/tokenizer.d.ts +0 -11
  121. package/types/sqlite/table/where.d.ts +0 -37
  122. package/types/sqlite/use-sqlite-count.d.ts +0 -17
  123. package/types/sqlite/use-sqlite.d.ts +0 -39
  124. /package/{src/__tests__ → __tests__}/test-utils.ts +0 -0
  125. /package/{esm → dist/esm}/create-state.js +0 -0
  126. /package/{esm → dist/esm}/scheduler.js +0 -0
  127. /package/{esm → dist/esm}/types.js +0 -0
  128. /package/{esm → dist/esm}/use-value-loadable.js +0 -0
  129. /package/{esm → dist/esm}/use-value.js +0 -0
  130. /package/{esm → dist/esm}/utils/common.js +0 -0
  131. /package/{esm → dist/esm}/utils/create-emitter.js +0 -0
  132. /package/{esm → dist/esm}/utils/id.js +0 -0
  133. /package/{esm → dist/esm}/utils/is.js +0 -0
  134. /package/{esm → dist/esm}/utils/shallow.js +0 -0
@@ -1,1008 +0,0 @@
1
- /* eslint-disable jsdoc/require-jsdoc */
2
- import { act, renderHook } from '@testing-library/react-hooks'
3
- import { createSqliteState } from '../create-sqlite'
4
- import { useSqliteValue } from '../use-sqlite'
5
- import { waitFor } from '@testing-library/react'
6
- import { bunMemoryBackend } from '../table/bun-backend'
7
- import { StrictMode, Suspense, useState } from 'react'
8
- import { DEFAULT_PAGE_SIZE } from '../table/table'
9
-
10
- const backend = bunMemoryBackend()
11
- interface Person {
12
- id: string
13
- name: string
14
- age: number
15
- }
16
-
17
- function Wrapper({ children }: Readonly<{ children: React.ReactNode }>) {
18
- return (
19
- <StrictMode>
20
- <Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
21
- </StrictMode>
22
- )
23
- }
24
-
25
- /**
26
- * Generate mock people for testing
27
- * @param count Number of people to generate
28
- * @returns Array of Person objects
29
- */
30
- function generatePeople(count: number): Person[] {
31
- return Array.from({ length: count }, (_, index) => ({
32
- id: `person-${index}`,
33
- name: `Person ${index}`,
34
- age: 20 + (index % 60),
35
- }))
36
- }
37
-
38
- describe('use-sqlite-state', () => {
39
- it('should get basic value states', async () => {
40
- const sql = createSqliteState<Person>({ backend, tableName: 'State1', key: 'id' })
41
- let reRenders = 0
42
- const { result, rerender } = renderHook(
43
- () => {
44
- reRenders++
45
- const aha = useSqliteValue(sql)
46
- return aha
47
- },
48
- { wrapper: Wrapper },
49
- )
50
-
51
- // Initial sync render = 1
52
- expect(reRenders).toBe(1)
53
-
54
- // Wait for initial data load
55
- await waitFor(() => {
56
- expect(result.current[0]).toEqual([])
57
- })
58
- const initialRenders = reRenders
59
-
60
- act(() => {
61
- sql.set({ id: '1', name: 'Alice', age: 30 })
62
- })
63
- await waitFor(() => {
64
- expect(result.current[0]).toEqual([{ id: '1', name: 'Alice', age: 30 }])
65
- })
66
- const afterFirstSet = reRenders
67
-
68
- act(() => {
69
- sql.set({ id: '1', name: 'Alice2', age: 30 })
70
- })
71
- await waitFor(() => {
72
- expect(result.current[0]).toEqual([{ id: '1', name: 'Alice2', age: 30 }])
73
- })
74
-
75
- // delete item
76
- act(() => {
77
- sql.delete('1')
78
- })
79
- await waitFor(() => {
80
- expect(result.current[0]).toEqual([])
81
- })
82
-
83
- // add two items
84
- act(() => {
85
- sql.set({ id: '1', name: 'Alice', age: 30 })
86
- sql.set({ id: '2', name: 'Bob', age: 25 })
87
- })
88
- await waitFor(() => {
89
- expect(result.current[0]?.length).toBe(2)
90
- })
91
-
92
- const beforeManualRerender = reRenders
93
- act(() => {
94
- rerender()
95
- })
96
- await waitFor(() => {
97
- expect(reRenders).toBe(beforeManualRerender + 1)
98
- expect(result.current[0]?.length).toBe(2)
99
- })
100
-
101
- // Verify re-renders happened (at least initial + operations)
102
- expect(afterFirstSet).toBeGreaterThan(initialRenders)
103
- })
104
-
105
- it('should use where clause changed via state', async () => {
106
- const sql = createSqliteState<Person>({ backend, tableName: 'State2', key: 'id' })
107
- await sql.batchSet([
108
- { id: '1', name: 'Alice', age: 30 },
109
- { id: '2', name: 'Bob', age: 25 },
110
- { id: '3', name: 'Carol', age: 40 },
111
- ])
112
- let reRenders = 0
113
- const { result } = renderHook(() => {
114
- reRenders++
115
- const [minAge, setMinAge] = useState(20)
116
- return [useSqliteValue(sql, { where: { age: { gt: minAge } }, sortBy: 'age' }, [minAge]), setMinAge] as const
117
- })
118
-
119
- await waitFor(() => {
120
- const names = result.current?.[0][0]?.map((p) => p.name)
121
- expect(names).toEqual(['Bob', 'Alice', 'Carol'])
122
- })
123
- const initialRenders = reRenders
124
-
125
- // change minAge to 29
126
- act(() => {
127
- result.current[1](29)
128
- })
129
- await waitFor(() => {
130
- const names = result.current?.[0][0]?.map((p) => p.name)
131
- expect(names).toEqual(['Alice', 'Carol'])
132
- })
133
- // Deps change should trigger re-renders (stale + data load)
134
- expect(reRenders).toBeGreaterThan(initialRenders)
135
- })
136
-
137
- it('should handle rapid dependency changes without excessive re-renders', async () => {
138
- const sql = createSqliteState<Person>({ backend, tableName: 'RapidDepsChange', key: 'id' })
139
- await sql.batchSet([
140
- { id: '1', name: 'Alice', age: 30 },
141
- { id: '2', name: 'Bob', age: 25 },
142
- { id: '3', name: 'Carol', age: 40 },
143
- { id: '4', name: 'Dave', age: 35 },
144
- ])
145
-
146
- let reRenders = 0
147
- const staleLog: Array<{ render: number; isStale: boolean; filterAge: number; dataLength: number | undefined }> = []
148
-
149
- const { result } = renderHook(() => {
150
- reRenders++
151
- const [filterAge, setFilterAge] = useState(20)
152
- const [data, actions] = useSqliteValue(sql, { where: { age: { gt: filterAge } }, sortBy: 'age' }, [filterAge])
153
- staleLog.push({ render: reRenders, isStale: actions.isStale, filterAge, dataLength: data?.length })
154
- return { data, isStale: actions.isStale, setFilterAge }
155
- })
156
-
157
- await waitFor(() => {
158
- expect(result.current.data?.length).toBe(4)
159
- expect(result.current.isStale).toBe(false)
160
- })
161
- const beforeRapidChanges = reRenders
162
- staleLog.length = 0 // Clear log for the interesting part
163
-
164
- // Change deps rapidly: 20 -> 25 -> 30 -> 35 in quick succession (within same act)
165
- act(() => {
166
- result.current.setFilterAge(25)
167
- result.current.setFilterAge(30)
168
- result.current.setFilterAge(35)
169
- })
170
-
171
- // isStale should be true immediately after deps change
172
- expect(result.current.isStale).toBe(true)
173
-
174
- // Wait for final data to load
175
- await waitFor(() => {
176
- expect(result.current.isStale).toBe(false)
177
- // Only Carol (age 40) is > 35
178
- expect(result.current.data?.length).toBe(1)
179
- expect(result.current.data?.[0]?.name).toBe('Carol')
180
- })
181
-
182
- const afterRapidChanges = reRenders
183
- const rendersForRapidChanges = afterRapidChanges - beforeRapidChanges
184
-
185
- // eslint-disable-next-line no-console
186
- console.log(`🔄 Rapid dep changes (3 setState calls in 1 act): ${rendersForRapidChanges} renders`)
187
- // eslint-disable-next-line no-console
188
- console.log('📋 Stale log:', staleLog)
189
-
190
- // With batched setState, React batches the 3 calls into 1 render with filterAge=35
191
- // Then we get: 1 render (batched setState) + 1 render (setSettledDeps) = 2 renders
192
- // This demonstrates the optimization: rapid changes don't cause multiple stale cycles
193
- expect(rendersForRapidChanges).toBeLessThanOrEqual(4)
194
- })
195
-
196
- it('should handle sequential dependency changes with proper stale tracking', async () => {
197
- const sql = createSqliteState<Person>({ backend, tableName: 'SequentialDepsChange', key: 'id' })
198
- await sql.batchSet([
199
- { id: '1', name: 'Alice', age: 30 },
200
- { id: '2', name: 'Bob', age: 25 },
201
- { id: '3', name: 'Carol', age: 40 },
202
- ])
203
-
204
- let reRenders = 0
205
- const { result } = renderHook(() => {
206
- reRenders++
207
- const [filterAge, setFilterAge] = useState(20)
208
- const [data, actions] = useSqliteValue(sql, { where: { age: { gt: filterAge } }, sortBy: 'age' }, [filterAge])
209
- return { data, isStale: actions.isStale, setFilterAge }
210
- })
211
-
212
- await waitFor(() => {
213
- expect(result.current.data?.length).toBe(3)
214
- expect(result.current.isStale).toBe(false)
215
- })
216
- const afterInitialLoad = reRenders
217
-
218
- // First change: 20 -> 25
219
- act(() => {
220
- result.current.setFilterAge(25)
221
- })
222
- expect(result.current.isStale).toBe(true)
223
-
224
- await waitFor(() => {
225
- expect(result.current.isStale).toBe(false)
226
- expect(result.current.data?.length).toBe(2) // Alice (30) and Carol (40)
227
- })
228
- const afterFirstChange = reRenders
229
-
230
- // Second change: 25 -> 35
231
- act(() => {
232
- result.current.setFilterAge(35)
233
- })
234
- expect(result.current.isStale).toBe(true)
235
-
236
- await waitFor(() => {
237
- expect(result.current.isStale).toBe(false)
238
- expect(result.current.data?.length).toBe(1) // Only Carol (40)
239
- })
240
- const afterSecondChange = reRenders
241
-
242
- // Each dep change should cause ~2 re-renders: 1 for setState + 1 for setSettledDeps
243
- const rendersPerChange1 = afterFirstChange - afterInitialLoad
244
- const rendersPerChange2 = afterSecondChange - afterFirstChange
245
-
246
- // eslint-disable-next-line no-console
247
- console.log(`🔄 Sequential dep changes: ${rendersPerChange1} renders for 1st, ${rendersPerChange2} renders for 2nd`)
248
-
249
- // Should be 2-3 renders per change (setState + data load + setSettledDeps)
250
- expect(rendersPerChange1).toBeLessThanOrEqual(4)
251
- expect(rendersPerChange2).toBeLessThanOrEqual(4)
252
- })
253
-
254
- it('should support like in where clause and update results', async () => {
255
- const sql = createSqliteState<Person>({ backend, tableName: 'State3Hook', key: 'id' })
256
- await sql.batchSet([
257
- { id: '1', name: 'Alice', age: 30 },
258
- { id: '2', name: 'Alicia', age: 25 },
259
- { id: '3', name: 'Bob', age: 40 },
260
- ])
261
- let reRenders = 0
262
- const { result, rerender } = renderHook(
263
- ({ like }) => {
264
- reRenders++
265
- return useSqliteValue(sql, { where: { name: { like } } }, [like])
266
- },
267
- { initialProps: { like: '%Ali%' } },
268
- )
269
- await waitFor(() => {
270
- expect(result.current?.[0]?.map((p) => p.name)).toEqual(['Alice', 'Alicia'])
271
- })
272
- act(() => {
273
- rerender({ like: '%Bob%' })
274
- })
275
- await waitFor(() => {
276
- expect(result.current?.[0]?.map((p) => p.name)).toEqual(['Bob'])
277
- })
278
- expect(reRenders).toBeGreaterThanOrEqual(2)
279
- })
280
-
281
- it('should update results when changing order and limit options 1', async () => {
282
- const sql = createSqliteState<Person>({ backend, tableName: 'State44Hook', key: 'id' })
283
- await sql.batchSet([
284
- { id: '1', name: 'Alice', age: 30 },
285
- { id: '2', name: 'Bob', age: 25 },
286
- { id: '3', name: 'Carol', age: 40 },
287
- ])
288
- const { result, rerender } = renderHook(
289
- ({ order, limit }) => useSqliteValue(sql, { sortBy: 'age', order, limit }, [order, limit]),
290
- { initialProps: { order: 'asc' as 'asc' | 'desc', limit: 2 } },
291
- )
292
- await waitFor(() => {
293
- expect(result.current?.[0]?.map((p) => p.name)).toEqual(['Bob', 'Alice'])
294
- })
295
- act(() => {
296
- rerender({ order: 'desc', limit: 2 })
297
- })
298
- await waitFor(() => {
299
- expect(result.current?.[0]?.map((p) => p.name)).toEqual(['Carol', 'Alice'])
300
- })
301
- act(() => {
302
- rerender({ order: 'desc', limit: 1 })
303
- })
304
- await waitFor(() => {
305
- expect(result.current?.[0]?.map((p) => p.name)).toEqual(['Carol'])
306
- })
307
- })
308
-
309
- it('should support actions.next and actions.refresh', async () => {
310
- const sql = createSqliteState<Person>({ backend, tableName: 'State5Hook', key: 'id' })
311
- await sql.batchSet([
312
- { id: '1', name: 'Alice', age: 30 },
313
- { id: '2', name: 'Bob', age: 25 },
314
- ])
315
- const { result } = renderHook(() => useSqliteValue(sql, {}, []))
316
- // actions.next and actions.refresh should be functions
317
- await waitFor(() => {
318
- expect(typeof result.current[1].nextPage).toBe('function')
319
- expect(typeof result.current[1].reset).toBe('function')
320
- })
321
- })
322
- it('should handle thousands of records Here', async () => {
323
- const sql = createSqliteState<Person>({ backend, tableName: 'State6Hook', key: 'id' })
324
- const people: Person[] = []
325
- const ITEMS_COUNT = 1000
326
- for (let index = 1; index <= ITEMS_COUNT; index++) {
327
- people.push({ id: index.toString(), name: `Person${index}`, age: 20 + (index % 50) })
328
- }
329
- await sql.batchSet(people)
330
- const { result } = renderHook(() => useSqliteValue(sql, {}, []))
331
- await waitFor(() => {
332
- expect(result.current?.[0]?.length ?? 0).toBe(DEFAULT_PAGE_SIZE)
333
- })
334
-
335
- // // loop until we have all ITEMS_COUNT items
336
- for (let index = 0; index < ITEMS_COUNT / DEFAULT_PAGE_SIZE; index++) {
337
- act(() => {
338
- result.current[1].nextPage()
339
- })
340
- await waitFor(() => {
341
- expect(result.current?.[0]?.length).toBe(Math.min(DEFAULT_PAGE_SIZE * (index + 2), ITEMS_COUNT))
342
- })
343
- }
344
-
345
- act(() => {
346
- result.current[1].reset()
347
- })
348
- await waitFor(() => {
349
- expect(result.current?.[0]?.length).toBe(DEFAULT_PAGE_SIZE)
350
- })
351
- })
352
-
353
- it('should handle thousands of records with single update', async () => {
354
- const sql = createSqliteState<Person>({ backend, tableName: 'State6Hook', key: 'id' })
355
- const people: Person[] = []
356
- const ITEMS_COUNT = 10_000
357
- const pageSize = 500
358
- for (let index = 1; index <= ITEMS_COUNT; index++) {
359
- people.push({ id: index.toString(), name: `Person${index}`, age: 20 + (index % 50) })
360
- }
361
- await sql.batchSet(people)
362
- let reRenders = 0
363
- const { result } = renderHook(() => {
364
- reRenders++
365
- return useSqliteValue(sql, { pageSize }, [])
366
- })
367
- await waitFor(() => {
368
- expect(result.current?.[0]?.length).toBe(pageSize)
369
- })
370
- const initialRenders = reRenders
371
-
372
- act(() => {
373
- for (let index = 0; index < (ITEMS_COUNT - pageSize) / pageSize; index++) {
374
- result.current[1].nextPage()
375
- }
376
- })
377
-
378
- await waitFor(() => {
379
- expect(result.current?.[0]?.length).toBe(ITEMS_COUNT)
380
- })
381
- const afterPagination = reRenders
382
-
383
- act(() => {
384
- result.current[1].reset()
385
- })
386
- await waitFor(() => {
387
- expect(result.current?.[0]?.length).toBe(pageSize)
388
- })
389
-
390
- // Verify pagination and reset caused re-renders
391
- expect(afterPagination).toBeGreaterThan(initialRenders)
392
- expect(reRenders).toBeGreaterThan(afterPagination)
393
- })
394
- it('should change ordering', async () => {
395
- const sql = createSqliteState<Person>({ backend, tableName: 'State7', key: 'id', indexes: ['age'] })
396
- const people: Person[] = []
397
- for (let index = 1; index <= 100; index++) {
398
- people.push({ id: index.toString(), name: `Person${index}`, age: 20 + (index % 50) })
399
- }
400
- await sql.batchSet(people)
401
- const { result, rerender } = renderHook(({ order }) => useSqliteValue(sql, { sortBy: 'age', order }, [order]), {
402
- initialProps: { order: 'asc' as 'asc' | 'desc' },
403
- })
404
- await waitFor(() => {
405
- expect(result.current?.[0]?.[0]?.age).toBe(20)
406
- })
407
- act(() => {
408
- rerender({ order: 'desc' })
409
- })
410
- await waitFor(() => {
411
- expect(result.current?.[0]?.[0]?.age).toBe(69)
412
- })
413
- })
414
-
415
- it('should support selector in options', async () => {
416
- const sql = createSqliteState<Person>({ backend, tableName: 'State8', key: 'id' })
417
- await sql.batchSet([
418
- { id: '1', name: 'Alice', age: 30 },
419
- { id: '2', name: 'Bob', age: 25 },
420
- { id: '3', name: 'Carol', age: 40 },
421
- ])
422
- const { result } = renderHook(() =>
423
- useSqliteValue(
424
- sql,
425
- {
426
- sortBy: 'age',
427
- select: (d) => d.name,
428
- },
429
- [],
430
- ),
431
- )
432
- await waitFor(() => {
433
- expect(result.current[0]).toEqual(['Bob', 'Alice', 'Carol'])
434
- })
435
- })
436
- it('should add 50 documents and then load with another hook', async () => {
437
- const sql = createSqliteState<Person>({ backend, tableName: 'State9', key: 'id' })
438
- let reRenders = 0
439
- const { result: result1 } = renderHook(() => {
440
- reRenders++
441
- return useSqliteValue(
442
- sql,
443
- {
444
- sortBy: 'age',
445
- order: 'desc',
446
- },
447
- [],
448
- )
449
- })
450
- await waitFor(() => {
451
- // Initially empty
452
- expect(result1.current?.[0]?.length ?? 0).toBeLessThanOrEqual(1)
453
- })
454
- const initialRenders = reRenders
455
-
456
- const people: Person[] = []
457
- for (let index = 1; index <= 50; index++) {
458
- people.push({ id: index.toString(), name: `Person${index}`, age: 20 + (index % 50) })
459
- }
460
- await sql.batchSet(people)
461
- await waitFor(() => {
462
- expect(result1.current?.[0]?.length).toBe(50)
463
- })
464
- // Data load should trigger re-renders
465
- expect(reRenders).toBeGreaterThan(initialRenders)
466
-
467
- const { result: result2 } = renderHook(() => useSqliteValue(sql, {}, []))
468
- await waitFor(() => {
469
- expect(result2.current?.[0]?.length).toBe(50)
470
- })
471
- })
472
-
473
- it('should handle update of deep fields with deep id', async () => {
474
- interface DeepItem {
475
- person: {
476
- id: string
477
- name: string
478
- age: number
479
- }
480
- }
481
- const sql = createSqliteState<DeepItem>({ backend, tableName: 'State10', key: 'person.id' })
482
- let reRenders = 0
483
- const { result } = renderHook(() => {
484
- reRenders++
485
- return useSqliteValue(sql, { sortBy: 'person.age' }, [])
486
- })
487
-
488
- await waitFor(() => {
489
- expect(result.current?.[0]?.length).toBe(0)
490
- })
491
- const initialRenders = reRenders
492
-
493
- act(() => {
494
- sql.set({ person: { id: 'some_id', name: 'Alice', age: 30 } })
495
- })
496
- await waitFor(() => {
497
- expect(result.current[0]).toEqual([{ person: { id: 'some_id', name: 'Alice', age: 30 } }])
498
- })
499
- const afterFirstSet = reRenders
500
-
501
- // update deep field
502
- act(() => {
503
- sql.set({ person: { id: 'some_id', name: 'Alice', age: 31 } })
504
- })
505
- await waitFor(() => {
506
- expect(result.current[0]).toEqual([{ person: { id: 'some_id', name: 'Alice', age: 31 } }])
507
- })
508
-
509
- // Each set should trigger re-renders
510
- expect(afterFirstSet).toBeGreaterThan(initialRenders)
511
- expect(reRenders).toBeGreaterThan(afterFirstSet)
512
-
513
- // update same field
514
- act(() => {
515
- sql.set({ person: { id: 'some_id', name: 'Alice', age: 31 } })
516
- })
517
- // should not re-render
518
- await waitFor(() => {
519
- expect(result.current[0]).toEqual([{ person: { id: 'some_id', name: 'Alice', age: 31 } }])
520
- })
521
-
522
- // add another item
523
- })
524
- it('should test reset', async () => {
525
- const sql = createSqliteState<Person>({ backend, tableName: 'State11', key: 'id' })
526
- let reRenders = 0
527
-
528
- await sql.set({ id: 'initial', name: 'initial', age: 1 })
529
- const { result } = renderHook(() => {
530
- reRenders++
531
- // eslint-disable-next-line unicorn/prevent-abbreviations
532
- const res = useSqliteValue(sql, {}, [])
533
- return res
534
- })
535
-
536
- await waitFor(() => {
537
- expect(result.current?.[0]?.length).toBe(1)
538
- })
539
- const initialRenders = reRenders
540
-
541
- act(() => {
542
- sql.set({ id: '1', name: 'Alice', age: 30 })
543
- })
544
- await waitFor(() => {
545
- expect(result.current?.[0]?.length).toBe(2)
546
- })
547
- const afterSet = reRenders
548
-
549
- act(() => {
550
- result.current[1].reset()
551
- })
552
- await waitFor(() => {
553
- expect(result.current?.[0]?.length).toBe(2)
554
- })
555
-
556
- // Set and reset should trigger re-renders
557
- expect(afterSet).toBeGreaterThan(initialRenders)
558
- expect(reRenders).toBeGreaterThan(afterSet)
559
- })
560
-
561
- it('should handle no items in the database', async () => {
562
- const sql = createSqliteState<Person>({ backend, tableName: 'EmptyState', key: 'id' })
563
- const { result } = renderHook(() => useSqliteValue(sql, {}, []))
564
-
565
- await waitFor(() => {
566
- expect(result.current[0]).toEqual([])
567
- })
568
- })
569
-
570
- it('should handle fewer items than page size', async () => {
571
- const sql = createSqliteState<Person>({ backend, tableName: 'FewItemsState', key: 'id' })
572
- await sql.batchSet([
573
- { id: '1', name: 'Alice', age: 30 },
574
- { id: '2', name: 'Bob', age: 25 },
575
- ])
576
-
577
- const { result } = renderHook(() => useSqliteValue(sql, {}, []))
578
-
579
- await waitFor(() => {
580
- expect(result.current[0]).toEqual([
581
- { id: '1', name: 'Alice', age: 30 },
582
- { id: '2', name: 'Bob', age: 25 },
583
- ])
584
- })
585
- })
586
-
587
- it('should handle exactly page size items', async () => {
588
- const sql = createSqliteState<Person>({ backend, tableName: 'ExactPageSizeState', key: 'id' })
589
- const items = Array.from({ length: DEFAULT_PAGE_SIZE }, (_, index) => ({
590
- id: `${index + 1}`,
591
- name: `Person${index + 1}`,
592
- age: 20 + (index % 50),
593
- }))
594
- await sql.batchSet(items)
595
-
596
- const { result } = renderHook(() => useSqliteValue(sql, {}, []))
597
-
598
- await waitFor(() => {
599
- expect(result.current[0]?.length).toBe(DEFAULT_PAGE_SIZE)
600
- })
601
- })
602
-
603
- it('should have thousands items, and update in middle check', async () => {
604
- let reRenders = 0
605
- const sql = createSqliteState<Person>({ backend, tableName: 'ManyItemsState', key: 'id' })
606
- const ITEMS_COUNT = 1000
607
- const people: Person[] = []
608
- for (let index = 1; index <= ITEMS_COUNT; index++) {
609
- people.push({ id: index.toString(), name: `Person${index}`, age: 20 + (index % 50) })
610
- }
611
- await sql.batchSet(people)
612
-
613
- const { result } = renderHook(() => {
614
- reRenders++
615
- return useSqliteValue(sql, { pageSize: 100 }, [])
616
- })
617
-
618
- await waitFor(() => {
619
- expect(result.current[0]?.length).toBe(100)
620
- })
621
- const initialRenders = reRenders
622
-
623
- act(() => {
624
- for (let index = 0; index < (ITEMS_COUNT - 100) / 100; index++) {
625
- result.current[1].nextPage()
626
- }
627
- })
628
- await waitFor(() => {
629
- expect(result.current[0]?.length).toBe(ITEMS_COUNT)
630
- })
631
- const afterPagination = reRenders
632
-
633
- act(() => {
634
- sql.set({ id: '500', name: 'UpdatedPerson500', age: 99 })
635
- })
636
-
637
- await waitFor(() => {
638
- const updated = result.current[0]?.find((p) => p.id === '500')
639
- expect(updated).toEqual({ id: '500', name: 'UpdatedPerson500', age: 99 })
640
- expect(result.current[0]?.length).toBe(ITEMS_COUNT)
641
- })
642
-
643
- // Pagination and update should cause re-renders
644
- expect(afterPagination).toBeGreaterThan(initialRenders)
645
- expect(reRenders).toBeGreaterThan(afterPagination)
646
- })
647
- it("should test batch delete and its impact on the hook's results", async () => {
648
- const sql = createSqliteState<Person>({ backend, tableName: 'BatchDeleteState', key: 'id' })
649
- const people: Person[] = []
650
- for (let index = 1; index <= 20; index++) {
651
- people.push({ id: index.toString(), name: `Person${index}`, age: 20 + (index % 50) })
652
- }
653
- await sql.batchSet(people)
654
-
655
- let reRenders = 0
656
- const { result } = renderHook(() => {
657
- reRenders++
658
- return useSqliteValue(sql, {}, [])
659
- })
660
-
661
- await waitFor(() => {
662
- expect(result.current[0]?.length).toBe(20)
663
- })
664
- const initialRenders = reRenders
665
-
666
- act(() => {
667
- sql.batchDelete(['5', '10', '15'])
668
- })
669
-
670
- await waitFor(() => {
671
- expect(result.current[0]?.length).toBe(17)
672
- expect(result.current[0]?.find((p) => p.id === '5')).toBeUndefined()
673
- expect(result.current[0]?.find((p) => p.id === '10')).toBeUndefined()
674
- expect(result.current[0]?.find((p) => p.id === '15')).toBeUndefined()
675
- })
676
- // Batch delete should trigger re-render
677
- expect(reRenders).toBeGreaterThan(initialRenders)
678
- })
679
- })
680
-
681
- /* eslint-disable no-console */
682
- describe('use-sqlite-state performance benchmarks', () => {
683
- describe('timing benchmarks', () => {
684
- it('benchmark: initial load 100 items', async () => {
685
- const testBackend = bunMemoryBackend()
686
- const sql = createSqliteState<Person>({ backend: testBackend, tableName: 'Bench100', key: 'id', indexes: ['age'] })
687
- await sql.batchSet(generatePeople(100))
688
-
689
- const start = performance.now()
690
- const { result } = renderHook(() => useSqliteValue(sql, { pageSize: 100 }, []))
691
-
692
- await waitFor(() => {
693
- expect(result.current[0]?.length).toBe(100)
694
- })
695
-
696
- const duration = performance.now() - start
697
- console.log('📊 100 items initial load:', duration.toFixed(2), 'ms')
698
- expect(duration).toBeLessThan(500)
699
- })
700
-
701
- it('benchmark: initial load 1000 items', async () => {
702
- const testBackend = bunMemoryBackend()
703
- const sql = createSqliteState<Person>({ backend: testBackend, tableName: 'Bench1000', key: 'id', indexes: ['age'] })
704
- await sql.batchSet(generatePeople(1000))
705
-
706
- const start = performance.now()
707
- const { result } = renderHook(() => useSqliteValue(sql, { pageSize: 1000 }, []))
708
-
709
- await waitFor(() => {
710
- expect(result.current[0]?.length).toBe(1000)
711
- })
712
-
713
- const duration = performance.now() - start
714
- console.log('📊 1000 items initial load:', duration.toFixed(2), 'ms')
715
- expect(duration).toBeLessThan(2000)
716
- })
717
-
718
- it('benchmark: initial load 5000 items', async () => {
719
- const testBackend = bunMemoryBackend()
720
- const sql = createSqliteState<Person>({ backend: testBackend, tableName: 'Bench5000', key: 'id', indexes: ['age'] })
721
- await sql.batchSet(generatePeople(5000))
722
-
723
- const start = performance.now()
724
- const { result } = renderHook(() => useSqliteValue(sql, { pageSize: 5000 }, []))
725
-
726
- await waitFor(() => {
727
- expect(result.current[0]?.length).toBe(5000)
728
- })
729
-
730
- const duration = performance.now() - start
731
- console.log('📊 5000 items initial load:', duration.toFixed(2), 'ms')
732
- expect(duration).toBeLessThan(5000)
733
- })
734
-
735
- it('benchmark: where clause filtering on 5000 items', async () => {
736
- const testBackend = bunMemoryBackend()
737
- const sql = createSqliteState<Person>({ backend: testBackend, tableName: 'BenchWhere5000', key: 'id', indexes: ['age'] })
738
- await sql.batchSet(generatePeople(5000))
739
-
740
- const start = performance.now()
741
- const { result } = renderHook(() => useSqliteValue(sql, { where: { age: { gte: 50 } }, pageSize: 5000 }, []))
742
-
743
- await waitFor(() => {
744
- expect(result.current[0]?.length).toBeGreaterThan(0)
745
- })
746
-
747
- const duration = performance.now() - start
748
- console.log('📊 5000 items WHERE filter:', duration.toFixed(2), 'ms', '| matched:', result.current[0]?.length)
749
- expect(duration).toBeLessThan(2000)
750
- })
751
-
752
- it('benchmark: rapid sequential updates', async () => {
753
- const testBackend = bunMemoryBackend()
754
- const sql = createSqliteState<Person>({ backend: testBackend, tableName: 'BenchRapid', key: 'id', indexes: ['age'] })
755
- await sql.batchSet(generatePeople(100))
756
-
757
- const { result } = renderHook(() => useSqliteValue(sql, { pageSize: 200 }, []))
758
-
759
- await waitFor(() => {
760
- expect(result.current[0]?.length).toBe(100)
761
- })
762
-
763
- const updateCount = 50
764
- const start = performance.now()
765
-
766
- for (let index = 0; index < updateCount; index++) {
767
- await act(async () => {
768
- await sql.set({ id: `rapid-${index}`, name: `Rapid ${index}`, age: 25 })
769
- })
770
- }
771
-
772
- await waitFor(() => {
773
- expect(result.current[0]?.length).toBe(150)
774
- })
775
-
776
- const duration = performance.now() - start
777
- console.log(
778
- '📊',
779
- updateCount,
780
- 'rapid inserts:',
781
- duration.toFixed(2),
782
- 'ms',
783
- '| avg:',
784
- (duration / updateCount).toFixed(2),
785
- 'ms/op',
786
- )
787
- expect(duration).toBeLessThan(3000)
788
- })
789
-
790
- it('benchmark: pagination load all pages', async () => {
791
- const testBackend = bunMemoryBackend()
792
- const sql = createSqliteState<Person>({ backend: testBackend, tableName: 'BenchPagination', key: 'id', indexes: ['age'] })
793
- const totalItems = 500
794
- const pageSize = 50
795
- await sql.batchSet(generatePeople(totalItems))
796
-
797
- const { result } = renderHook(() => useSqliteValue(sql, { pageSize }, []))
798
-
799
- await waitFor(() => {
800
- expect(result.current[0]?.length).toBe(pageSize)
801
- })
802
-
803
- const start = performance.now()
804
- const totalPages = Math.ceil(totalItems / pageSize) - 1
805
-
806
- for (let page = 0; page < totalPages; page++) {
807
- await act(async () => {
808
- await result.current[1].nextPage()
809
- })
810
- }
811
-
812
- await waitFor(() => {
813
- expect(result.current[0]?.length).toBe(totalItems)
814
- })
815
-
816
- const duration = performance.now() - start
817
- console.log(
818
- '📊',
819
- totalPages,
820
- 'page loads:',
821
- duration.toFixed(2),
822
- 'ms',
823
- '| avg:',
824
- (duration / totalPages).toFixed(2),
825
- 'ms/page',
826
- )
827
- expect(duration).toBeLessThan(2000)
828
- })
829
- })
830
-
831
- describe('re-render analysis', () => {
832
- it('analyze: re-renders on initial load', async () => {
833
- const testBackend = bunMemoryBackend()
834
- const sql = createSqliteState<Person>({ backend: testBackend, tableName: 'RenderInit', key: 'id' })
835
- await sql.batchSet(generatePeople(50))
836
-
837
- let renderCount = 0
838
- const { result } = renderHook(() => {
839
- renderCount++
840
- return useSqliteValue(sql, { pageSize: 50 }, [])
841
- })
842
-
843
- await waitFor(() => {
844
- expect(result.current[0]?.length).toBe(50)
845
- })
846
-
847
- console.log('🔄 Initial load renders:', renderCount)
848
- expect(renderCount).toBeLessThanOrEqual(3)
849
- })
850
-
851
- it('analyze: re-renders on single insert', async () => {
852
- const testBackend = bunMemoryBackend()
853
- const sql = createSqliteState<Person>({ backend: testBackend, tableName: 'RenderInsert', key: 'id' })
854
- await sql.batchSet(generatePeople(10))
855
-
856
- let renderCount = 0
857
- const { result } = renderHook(() => {
858
- renderCount++
859
- return useSqliteValue(sql, { pageSize: 50 }, [])
860
- })
861
-
862
- await waitFor(() => {
863
- expect(result.current[0]?.length).toBe(10)
864
- })
865
-
866
- const rendersBefore = renderCount
867
-
868
- await act(async () => {
869
- await sql.set({ id: 'new-item', name: 'New Person', age: 30 })
870
- })
871
-
872
- await waitFor(() => {
873
- expect(result.current[0]?.length).toBe(11)
874
- })
875
-
876
- const rendersForInsert = renderCount - rendersBefore
877
- console.log('🔄 Single insert renders:', rendersForInsert)
878
- expect(rendersForInsert).toBeLessThanOrEqual(2)
879
- })
880
-
881
- it('analyze: re-renders on update with same data (shallow equal)', async () => {
882
- const testBackend = bunMemoryBackend()
883
- const sql = createSqliteState<Person>({ backend: testBackend, tableName: 'RenderShallow', key: 'id' })
884
- await sql.set({ id: 'test-1', name: 'Test Person', age: 30 })
885
-
886
- let renderCount = 0
887
- const { result } = renderHook(() => {
888
- renderCount++
889
- return useSqliteValue(sql, { pageSize: 50 }, [])
890
- })
891
-
892
- await waitFor(() => {
893
- expect(result.current[0]?.length).toBe(1)
894
- })
895
-
896
- const rendersBefore = renderCount
897
-
898
- // Update with identical data
899
- await act(async () => {
900
- await sql.set({ id: 'test-1', name: 'Test Person', age: 30 })
901
- })
902
-
903
- // Small delay to ensure any async effects complete
904
- await new Promise((resolve) => setTimeout(resolve, 50))
905
-
906
- const rendersForSameData = renderCount - rendersBefore
907
- console.log('🔄 Same data update renders:', rendersForSameData, '(should be 0)')
908
- expect(rendersForSameData).toBe(0)
909
- })
910
-
911
- it('analyze: re-renders on update with different data', async () => {
912
- const testBackend = bunMemoryBackend()
913
- const sql = createSqliteState<Person>({ backend: testBackend, tableName: 'RenderDiff', key: 'id' })
914
- await sql.set({ id: 'test-1', name: 'Original', age: 30 })
915
-
916
- let renderCount = 0
917
- const { result } = renderHook(() => {
918
- renderCount++
919
- return useSqliteValue(sql, { pageSize: 50 }, [])
920
- })
921
-
922
- await waitFor(() => {
923
- expect(result.current[0]?.[0]?.name).toBe('Original')
924
- })
925
-
926
- const rendersBefore = renderCount
927
-
928
- await act(async () => {
929
- await sql.set({ id: 'test-1', name: 'Updated', age: 31 })
930
- })
931
-
932
- await waitFor(() => {
933
- expect(result.current[0]?.[0]?.name).toBe('Updated')
934
- })
935
-
936
- const rendersForUpdate = renderCount - rendersBefore
937
- console.log('🔄 Different data update renders:', rendersForUpdate)
938
- expect(rendersForUpdate).toBeLessThanOrEqual(2)
939
- })
940
-
941
- it('analyze: re-renders on batch operations', async () => {
942
- const testBackend = bunMemoryBackend()
943
- const sql = createSqliteState<Person>({ backend: testBackend, tableName: 'RenderBatch', key: 'id' })
944
- await sql.batchSet(generatePeople(20))
945
-
946
- let renderCount = 0
947
- const { result } = renderHook(() => {
948
- renderCount++
949
- return useSqliteValue(sql, { pageSize: 50 }, [])
950
- })
951
-
952
- await waitFor(() => {
953
- expect(result.current[0]?.length).toBe(20)
954
- })
955
-
956
- const rendersBefore = renderCount
957
-
958
- // Batch delete 5 items
959
- await act(async () => {
960
- await sql.batchDelete(['person-0', 'person-1', 'person-2', 'person-3', 'person-4'])
961
- })
962
-
963
- await waitFor(() => {
964
- expect(result.current[0]?.length).toBe(15)
965
- })
966
-
967
- const rendersForBatchDelete = renderCount - rendersBefore
968
- console.log('🔄 Batch delete (5 items) renders:', rendersForBatchDelete)
969
- // Batch operations should ideally cause minimal re-renders
970
- expect(rendersForBatchDelete).toBeLessThanOrEqual(3)
971
- })
972
-
973
- it('analyze: re-renders on deps change', async () => {
974
- const testBackend = bunMemoryBackend()
975
- const sql = createSqliteState<Person>({ backend: testBackend, tableName: 'RenderDeps', key: 'id', indexes: ['age'] })
976
- await sql.batchSet(generatePeople(100))
977
-
978
- let renderCount = 0
979
- const { result, rerender } = renderHook(
980
- ({ minAge }) => {
981
- renderCount++
982
- return useSqliteValue(sql, { where: { age: { gte: minAge } }, pageSize: 100 }, [minAge])
983
- },
984
- { initialProps: { minAge: 50 } },
985
- )
986
-
987
- await waitFor(() => {
988
- expect(result.current[0]?.length).toBeGreaterThan(0)
989
- })
990
-
991
- const rendersBefore = renderCount
992
-
993
- act(() => {
994
- rerender({ minAge: 60 })
995
- })
996
-
997
- await waitFor(() => {
998
- // eslint-disable-next-line sonarjs/no-nested-functions
999
- const minAgeInResults = Math.min(...(result.current[0]?.map((p) => p.age) ?? [0]))
1000
- expect(minAgeInResults).toBeGreaterThanOrEqual(60)
1001
- })
1002
-
1003
- const rendersForDepsChange = renderCount - rendersBefore
1004
- console.log('🔄 Deps change renders:', rendersForDepsChange)
1005
- expect(rendersForDepsChange).toBeLessThanOrEqual(3)
1006
- })
1007
- })
1008
- })