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.
@@ -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(7)
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
- // // change minAge to 29
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
- expect(reRenders).toBe(1)
309
- expect(result1.current?.[0]?.length).toBe(undefined)
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
- expect(reRenders).toBe(3)
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
  })