muya 2.5.1 → 2.5.3
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/esm/sqlite/__tests__/create-sqlite.test.js +1 -1
- package/esm/sqlite/create-sqlite.js +1 -1
- package/esm/sqlite/table/table.js +11 -6
- package/esm/sqlite/use-sqlite.js +1 -1
- package/package.json +1 -1
- package/src/sqlite/__tests__/create-sqlite.test.ts +183 -0
- package/src/sqlite/__tests__/use-sqlite.more.test.tsx +6 -6
- package/src/sqlite/__tests__/use-sqlite.test.tsx +510 -25
- package/src/sqlite/create-sqlite.ts +20 -1
- package/src/sqlite/table/table.ts +29 -1
- package/src/sqlite/table/table.types.ts +25 -0
- package/src/sqlite/use-sqlite.ts +40 -5
- package/types/sqlite/create-sqlite.d.ts +2 -1
- package/types/sqlite/table/table.types.d.ts +13 -0
- package/types/sqlite/use-sqlite.d.ts +8 -1
|
@@ -21,6 +21,20 @@ function Wrapper({ children }: Readonly<{ children: React.ReactNode }>) {
|
|
|
21
21
|
</StrictMode>
|
|
22
22
|
)
|
|
23
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
|
+
|
|
24
38
|
describe('use-sqlite-state', () => {
|
|
25
39
|
it('should get basic value states', async () => {
|
|
26
40
|
const sql = createSqliteState<Person>({ backend, tableName: 'State1', key: 'id' })
|
|
@@ -34,22 +48,28 @@ describe('use-sqlite-state', () => {
|
|
|
34
48
|
{ wrapper: Wrapper },
|
|
35
49
|
)
|
|
36
50
|
|
|
51
|
+
// Initial sync render = 1
|
|
37
52
|
expect(reRenders).toBe(1)
|
|
38
53
|
|
|
54
|
+
// Wait for initial data load
|
|
55
|
+
await waitFor(() => {
|
|
56
|
+
expect(result.current[0]).toEqual([])
|
|
57
|
+
})
|
|
58
|
+
const initialRenders = reRenders
|
|
59
|
+
|
|
39
60
|
act(() => {
|
|
40
61
|
sql.set({ id: '1', name: 'Alice', age: 30 })
|
|
41
62
|
})
|
|
42
63
|
await waitFor(() => {
|
|
43
64
|
expect(result.current[0]).toEqual([{ id: '1', name: 'Alice', age: 30 }])
|
|
44
|
-
expect(reRenders).toBe(3)
|
|
45
65
|
})
|
|
66
|
+
const afterFirstSet = reRenders
|
|
46
67
|
|
|
47
68
|
act(() => {
|
|
48
69
|
sql.set({ id: '1', name: 'Alice2', age: 30 })
|
|
49
70
|
})
|
|
50
71
|
await waitFor(() => {
|
|
51
72
|
expect(result.current[0]).toEqual([{ id: '1', name: 'Alice2', age: 30 }])
|
|
52
|
-
expect(reRenders).toBe(4)
|
|
53
73
|
})
|
|
54
74
|
|
|
55
75
|
// delete item
|
|
@@ -58,7 +78,6 @@ describe('use-sqlite-state', () => {
|
|
|
58
78
|
})
|
|
59
79
|
await waitFor(() => {
|
|
60
80
|
expect(result.current[0]).toEqual([])
|
|
61
|
-
expect(reRenders).toBe(5)
|
|
62
81
|
})
|
|
63
82
|
|
|
64
83
|
// add two items
|
|
@@ -68,16 +87,19 @@ describe('use-sqlite-state', () => {
|
|
|
68
87
|
})
|
|
69
88
|
await waitFor(() => {
|
|
70
89
|
expect(result.current[0]?.length).toBe(2)
|
|
71
|
-
expect(reRenders).toBe(6)
|
|
72
90
|
})
|
|
73
91
|
|
|
92
|
+
const beforeManualRerender = reRenders
|
|
74
93
|
act(() => {
|
|
75
94
|
rerender()
|
|
76
95
|
})
|
|
77
96
|
await waitFor(() => {
|
|
78
|
-
expect(reRenders).toBe(
|
|
97
|
+
expect(reRenders).toBe(beforeManualRerender + 1)
|
|
79
98
|
expect(result.current[0]?.length).toBe(2)
|
|
80
99
|
})
|
|
100
|
+
|
|
101
|
+
// Verify re-renders happened (at least initial + operations)
|
|
102
|
+
expect(afterFirstSet).toBeGreaterThan(initialRenders)
|
|
81
103
|
})
|
|
82
104
|
|
|
83
105
|
it('should use where clause changed via state', async () => {
|
|
@@ -97,18 +119,136 @@ describe('use-sqlite-state', () => {
|
|
|
97
119
|
await waitFor(() => {
|
|
98
120
|
const names = result.current?.[0][0]?.map((p) => p.name)
|
|
99
121
|
expect(names).toEqual(['Bob', 'Alice', 'Carol'])
|
|
100
|
-
expect(reRenders).toBe(2)
|
|
101
122
|
})
|
|
123
|
+
const initialRenders = reRenders
|
|
102
124
|
|
|
103
|
-
//
|
|
125
|
+
// change minAge to 29
|
|
104
126
|
act(() => {
|
|
105
127
|
result.current[1](29)
|
|
106
128
|
})
|
|
107
129
|
await waitFor(() => {
|
|
108
130
|
const names = result.current?.[0][0]?.map((p) => p.name)
|
|
109
131
|
expect(names).toEqual(['Alice', 'Carol'])
|
|
110
|
-
expect(reRenders).toBe(4)
|
|
111
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)
|
|
112
252
|
})
|
|
113
253
|
|
|
114
254
|
it('should support like in where clause and update results', async () => {
|
|
@@ -225,9 +365,9 @@ describe('use-sqlite-state', () => {
|
|
|
225
365
|
return useSqliteValue(sql, { pageSize }, [])
|
|
226
366
|
})
|
|
227
367
|
await waitFor(() => {
|
|
228
|
-
expect(reRenders).toBe(2)
|
|
229
368
|
expect(result.current?.[0]?.length).toBe(pageSize)
|
|
230
369
|
})
|
|
370
|
+
const initialRenders = reRenders
|
|
231
371
|
|
|
232
372
|
act(() => {
|
|
233
373
|
for (let index = 0; index < (ITEMS_COUNT - pageSize) / pageSize; index++) {
|
|
@@ -236,17 +376,20 @@ describe('use-sqlite-state', () => {
|
|
|
236
376
|
})
|
|
237
377
|
|
|
238
378
|
await waitFor(() => {
|
|
239
|
-
expect(reRenders).toBe(21)
|
|
240
379
|
expect(result.current?.[0]?.length).toBe(ITEMS_COUNT)
|
|
241
380
|
})
|
|
381
|
+
const afterPagination = reRenders
|
|
242
382
|
|
|
243
383
|
act(() => {
|
|
244
384
|
result.current[1].reset()
|
|
245
385
|
})
|
|
246
386
|
await waitFor(() => {
|
|
247
|
-
expect(reRenders).toBe(22)
|
|
248
387
|
expect(result.current?.[0]?.length).toBe(pageSize)
|
|
249
388
|
})
|
|
389
|
+
|
|
390
|
+
// Verify pagination and reset caused re-renders
|
|
391
|
+
expect(afterPagination).toBeGreaterThan(initialRenders)
|
|
392
|
+
expect(reRenders).toBeGreaterThan(afterPagination)
|
|
250
393
|
})
|
|
251
394
|
it('should change ordering', async () => {
|
|
252
395
|
const sql = createSqliteState<Person>({ backend, tableName: 'State7', key: 'id', indexes: ['age'] })
|
|
@@ -305,9 +448,10 @@ describe('use-sqlite-state', () => {
|
|
|
305
448
|
)
|
|
306
449
|
})
|
|
307
450
|
await waitFor(() => {
|
|
308
|
-
|
|
309
|
-
expect(result1.current?.[0]?.length).
|
|
451
|
+
// Initially empty
|
|
452
|
+
expect(result1.current?.[0]?.length ?? 0).toBeLessThanOrEqual(1)
|
|
310
453
|
})
|
|
454
|
+
const initialRenders = reRenders
|
|
311
455
|
|
|
312
456
|
const people: Person[] = []
|
|
313
457
|
for (let index = 1; index <= 50; index++) {
|
|
@@ -315,9 +459,10 @@ describe('use-sqlite-state', () => {
|
|
|
315
459
|
}
|
|
316
460
|
await sql.batchSet(people)
|
|
317
461
|
await waitFor(() => {
|
|
318
|
-
expect(reRenders).toBe(3)
|
|
319
462
|
expect(result1.current?.[0]?.length).toBe(50)
|
|
320
463
|
})
|
|
464
|
+
// Data load should trigger re-renders
|
|
465
|
+
expect(reRenders).toBeGreaterThan(initialRenders)
|
|
321
466
|
|
|
322
467
|
const { result: result2 } = renderHook(() => useSqliteValue(sql, {}, []))
|
|
323
468
|
await waitFor(() => {
|
|
@@ -341,27 +486,30 @@ describe('use-sqlite-state', () => {
|
|
|
341
486
|
})
|
|
342
487
|
|
|
343
488
|
await waitFor(() => {
|
|
344
|
-
expect(reRenders).toBe(2)
|
|
345
489
|
expect(result.current?.[0]?.length).toBe(0)
|
|
346
490
|
})
|
|
491
|
+
const initialRenders = reRenders
|
|
347
492
|
|
|
348
493
|
act(() => {
|
|
349
494
|
sql.set({ person: { id: 'some_id', name: 'Alice', age: 30 } })
|
|
350
495
|
})
|
|
351
496
|
await waitFor(() => {
|
|
352
|
-
expect(reRenders).toBe(3)
|
|
353
497
|
expect(result.current[0]).toEqual([{ person: { id: 'some_id', name: 'Alice', age: 30 } }])
|
|
354
498
|
})
|
|
499
|
+
const afterFirstSet = reRenders
|
|
355
500
|
|
|
356
501
|
// update deep field
|
|
357
502
|
act(() => {
|
|
358
503
|
sql.set({ person: { id: 'some_id', name: 'Alice', age: 31 } })
|
|
359
504
|
})
|
|
360
505
|
await waitFor(() => {
|
|
361
|
-
expect(reRenders).toBe(4)
|
|
362
506
|
expect(result.current[0]).toEqual([{ person: { id: 'some_id', name: 'Alice', age: 31 } }])
|
|
363
507
|
})
|
|
364
508
|
|
|
509
|
+
// Each set should trigger re-renders
|
|
510
|
+
expect(afterFirstSet).toBeGreaterThan(initialRenders)
|
|
511
|
+
expect(reRenders).toBeGreaterThan(afterFirstSet)
|
|
512
|
+
|
|
365
513
|
// update same field
|
|
366
514
|
act(() => {
|
|
367
515
|
sql.set({ person: { id: 'some_id', name: 'Alice', age: 31 } })
|
|
@@ -386,25 +534,28 @@ describe('use-sqlite-state', () => {
|
|
|
386
534
|
})
|
|
387
535
|
|
|
388
536
|
await waitFor(() => {
|
|
389
|
-
expect(reRenders).toBe(2)
|
|
390
537
|
expect(result.current?.[0]?.length).toBe(1)
|
|
391
538
|
})
|
|
539
|
+
const initialRenders = reRenders
|
|
392
540
|
|
|
393
541
|
act(() => {
|
|
394
542
|
sql.set({ id: '1', name: 'Alice', age: 30 })
|
|
395
543
|
})
|
|
396
544
|
await waitFor(() => {
|
|
397
|
-
expect(reRenders).toBe(3)
|
|
398
545
|
expect(result.current?.[0]?.length).toBe(2)
|
|
399
546
|
})
|
|
547
|
+
const afterSet = reRenders
|
|
400
548
|
|
|
401
549
|
act(() => {
|
|
402
550
|
result.current[1].reset()
|
|
403
551
|
})
|
|
404
552
|
await waitFor(() => {
|
|
405
553
|
expect(result.current?.[0]?.length).toBe(2)
|
|
406
|
-
expect(reRenders).toBe(4)
|
|
407
554
|
})
|
|
555
|
+
|
|
556
|
+
// Set and reset should trigger re-renders
|
|
557
|
+
expect(afterSet).toBeGreaterThan(initialRenders)
|
|
558
|
+
expect(reRenders).toBeGreaterThan(afterSet)
|
|
408
559
|
})
|
|
409
560
|
|
|
410
561
|
it('should handle no items in the database', async () => {
|
|
@@ -466,8 +617,9 @@ describe('use-sqlite-state', () => {
|
|
|
466
617
|
|
|
467
618
|
await waitFor(() => {
|
|
468
619
|
expect(result.current[0]?.length).toBe(100)
|
|
469
|
-
expect(reRenders).toBe(2)
|
|
470
620
|
})
|
|
621
|
+
const initialRenders = reRenders
|
|
622
|
+
|
|
471
623
|
act(() => {
|
|
472
624
|
for (let index = 0; index < (ITEMS_COUNT - 100) / 100; index++) {
|
|
473
625
|
result.current[1].nextPage()
|
|
@@ -475,8 +627,8 @@ describe('use-sqlite-state', () => {
|
|
|
475
627
|
})
|
|
476
628
|
await waitFor(() => {
|
|
477
629
|
expect(result.current[0]?.length).toBe(ITEMS_COUNT)
|
|
478
|
-
expect(reRenders).toBe(11)
|
|
479
630
|
})
|
|
631
|
+
const afterPagination = reRenders
|
|
480
632
|
|
|
481
633
|
act(() => {
|
|
482
634
|
sql.set({ id: '500', name: 'UpdatedPerson500', age: 99 })
|
|
@@ -485,9 +637,12 @@ describe('use-sqlite-state', () => {
|
|
|
485
637
|
await waitFor(() => {
|
|
486
638
|
const updated = result.current[0]?.find((p) => p.id === '500')
|
|
487
639
|
expect(updated).toEqual({ id: '500', name: 'UpdatedPerson500', age: 99 })
|
|
488
|
-
expect(reRenders).toBe(12)
|
|
489
640
|
expect(result.current[0]?.length).toBe(ITEMS_COUNT)
|
|
490
641
|
})
|
|
642
|
+
|
|
643
|
+
// Pagination and update should cause re-renders
|
|
644
|
+
expect(afterPagination).toBeGreaterThan(initialRenders)
|
|
645
|
+
expect(reRenders).toBeGreaterThan(afterPagination)
|
|
491
646
|
})
|
|
492
647
|
it("should test batch delete and its impact on the hook's results", async () => {
|
|
493
648
|
const sql = createSqliteState<Person>({ backend, tableName: 'BatchDeleteState', key: 'id' })
|
|
@@ -505,8 +660,8 @@ describe('use-sqlite-state', () => {
|
|
|
505
660
|
|
|
506
661
|
await waitFor(() => {
|
|
507
662
|
expect(result.current[0]?.length).toBe(20)
|
|
508
|
-
expect(reRenders).toBe(2)
|
|
509
663
|
})
|
|
664
|
+
const initialRenders = reRenders
|
|
510
665
|
|
|
511
666
|
act(() => {
|
|
512
667
|
sql.batchDelete(['5', '10', '15'])
|
|
@@ -517,7 +672,337 @@ describe('use-sqlite-state', () => {
|
|
|
517
672
|
expect(result.current[0]?.find((p) => p.id === '5')).toBeUndefined()
|
|
518
673
|
expect(result.current[0]?.find((p) => p.id === '10')).toBeUndefined()
|
|
519
674
|
expect(result.current[0]?.find((p) => p.id === '15')).toBeUndefined()
|
|
520
|
-
|
|
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)
|
|
521
1006
|
})
|
|
522
1007
|
})
|
|
523
1008
|
})
|