muya 2.5.4 → 2.5.6

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 (123) 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 +14 -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/scheduler.js +1 -0
  16. package/dist/esm/select.js +1 -0
  17. package/{types → dist/types}/create-state.d.ts +1 -0
  18. package/dist/types/create-state.d.ts.map +1 -0
  19. package/{types → dist/types}/create.d.ts +2 -4
  20. package/dist/types/create.d.ts.map +1 -0
  21. package/dist/types/debug/development-tools.d.ts +13 -0
  22. package/dist/types/debug/development-tools.d.ts.map +1 -0
  23. package/{types → dist/types}/index.d.ts +3 -1
  24. package/dist/types/index.d.ts.map +1 -0
  25. package/dist/types/scheduler.d.ts +25 -0
  26. package/dist/types/scheduler.d.ts.map +1 -0
  27. package/{types → dist/types}/select.d.ts +1 -0
  28. package/dist/types/select.d.ts.map +1 -0
  29. package/{types → dist/types}/types.d.ts +1 -0
  30. package/dist/types/types.d.ts.map +1 -0
  31. package/{types → dist/types}/use-value-loadable.d.ts +1 -0
  32. package/dist/types/use-value-loadable.d.ts.map +1 -0
  33. package/{types → dist/types}/use-value.d.ts +2 -1
  34. package/dist/types/use-value.d.ts.map +1 -0
  35. package/{types → dist/types}/utils/common.d.ts +1 -0
  36. package/dist/types/utils/common.d.ts.map +1 -0
  37. package/{types → dist/types}/utils/create-emitter.d.ts +1 -0
  38. package/dist/types/utils/create-emitter.d.ts.map +1 -0
  39. package/{types → dist/types}/utils/id.d.ts +1 -0
  40. package/dist/types/utils/id.d.ts.map +1 -0
  41. package/{types → dist/types}/utils/is.d.ts +1 -0
  42. package/dist/types/utils/is.d.ts.map +1 -0
  43. package/{types → dist/types}/utils/shallow.d.ts +1 -0
  44. package/dist/types/utils/shallow.d.ts.map +1 -0
  45. package/package.json +31 -8
  46. package/src/create.ts +7 -2
  47. package/src/debug/development-tools.ts +5 -40
  48. package/src/index.ts +2 -1
  49. package/src/scheduler.ts +77 -71
  50. package/src/select.ts +7 -2
  51. package/src/use-value.ts +1 -1
  52. package/tsconfig.build.json +12 -0
  53. package/cjs/index.js +0 -1
  54. package/esm/__tests__/test-utils.js +0 -1
  55. package/esm/create.js +0 -1
  56. package/esm/debug/development-tools.js +0 -1
  57. package/esm/index.js +0 -1
  58. package/esm/scheduler.js +0 -1
  59. package/esm/select.js +0 -1
  60. package/esm/sqlite/__tests__/create-sqlite.test.js +0 -1
  61. package/esm/sqlite/__tests__/map-deque.test.js +0 -1
  62. package/esm/sqlite/__tests__/table.test.js +0 -1
  63. package/esm/sqlite/__tests__/tokenizer.test.js +0 -1
  64. package/esm/sqlite/__tests__/where.test.js +0 -1
  65. package/esm/sqlite/create-sqlite.js +0 -1
  66. package/esm/sqlite/index.js +0 -1
  67. package/esm/sqlite/table/backend.js +0 -1
  68. package/esm/sqlite/table/bun-backend.js +0 -1
  69. package/esm/sqlite/table/index.js +0 -1
  70. package/esm/sqlite/table/map-deque.js +0 -1
  71. package/esm/sqlite/table/table.js +0 -43
  72. package/esm/sqlite/table/table.types.js +0 -0
  73. package/esm/sqlite/table/tokenizer.js +0 -1
  74. package/esm/sqlite/table/where.js +0 -1
  75. package/esm/sqlite/use-sqlite-count.js +0 -1
  76. package/esm/sqlite/use-sqlite.js +0 -1
  77. package/esm/utils/__tests__/is.test.js +0 -1
  78. package/esm/utils/__tests__/shallow.test.js +0 -1
  79. package/src/sqlite/__tests__/create-sqlite.test.ts +0 -264
  80. package/src/sqlite/__tests__/map-deque.test.ts +0 -61
  81. package/src/sqlite/__tests__/table.test.ts +0 -351
  82. package/src/sqlite/__tests__/tokenizer.test.ts +0 -43
  83. package/src/sqlite/__tests__/use-slite-count.test.tsx +0 -96
  84. package/src/sqlite/__tests__/use-sqlite.more.test.tsx +0 -637
  85. package/src/sqlite/__tests__/use-sqlite.test.tsx +0 -1008
  86. package/src/sqlite/__tests__/where.test.ts +0 -234
  87. package/src/sqlite/create-sqlite.ts +0 -164
  88. package/src/sqlite/index.ts +0 -4
  89. package/src/sqlite/table/backend.ts +0 -21
  90. package/src/sqlite/table/bun-backend.ts +0 -47
  91. package/src/sqlite/table/index.ts +0 -6
  92. package/src/sqlite/table/map-deque.ts +0 -29
  93. package/src/sqlite/table/table.ts +0 -353
  94. package/src/sqlite/table/table.types.ts +0 -129
  95. package/src/sqlite/table/tokenizer.ts +0 -35
  96. package/src/sqlite/table/where.ts +0 -207
  97. package/src/sqlite/use-sqlite-count.ts +0 -69
  98. package/src/sqlite/use-sqlite.ts +0 -250
  99. package/types/__tests__/test-utils.d.ts +0 -25
  100. package/types/debug/development-tools.d.ts +0 -8
  101. package/types/scheduler.d.ts +0 -20
  102. package/types/sqlite/create-sqlite.d.ts +0 -31
  103. package/types/sqlite/index.d.ts +0 -4
  104. package/types/sqlite/table/backend.d.ts +0 -20
  105. package/types/sqlite/table/bun-backend.d.ts +0 -6
  106. package/types/sqlite/table/index.d.ts +0 -6
  107. package/types/sqlite/table/map-deque.d.ts +0 -5
  108. package/types/sqlite/table/table.d.ts +0 -21
  109. package/types/sqlite/table/table.types.d.ts +0 -91
  110. package/types/sqlite/table/tokenizer.d.ts +0 -11
  111. package/types/sqlite/table/where.d.ts +0 -37
  112. package/types/sqlite/use-sqlite-count.d.ts +0 -17
  113. package/types/sqlite/use-sqlite.d.ts +0 -39
  114. /package/{src/__tests__ → __tests__}/test-utils.ts +0 -0
  115. /package/{esm → dist/esm}/create-state.js +0 -0
  116. /package/{esm → dist/esm}/types.js +0 -0
  117. /package/{esm → dist/esm}/use-value-loadable.js +0 -0
  118. /package/{esm → dist/esm}/use-value.js +0 -0
  119. /package/{esm → dist/esm}/utils/common.js +0 -0
  120. /package/{esm → dist/esm}/utils/create-emitter.js +0 -0
  121. /package/{esm → dist/esm}/utils/id.js +0 -0
  122. /package/{esm → dist/esm}/utils/is.js +0 -0
  123. /package/{esm → dist/esm}/utils/shallow.js +0 -0
@@ -1,637 +0,0 @@
1
- import { act, renderHook } from '@testing-library/react-hooks'
2
- import { createSqliteState } from '../create-sqlite'
3
- import { useSqliteValue } from '../use-sqlite'
4
- import { waitFor } from '@testing-library/react'
5
- import { bunMemoryBackend } from '../table/bun-backend'
6
- import { StrictMode, Suspense } from 'react'
7
- import type { Key } from '../table'
8
-
9
- const backend = bunMemoryBackend()
10
-
11
- interface Person {
12
- id: string
13
- name: string
14
- age: number
15
- }
16
-
17
- /**
18
- * Wrapper component to provide necessary React context for testing.
19
- * @param props - The props object containing children.
20
- * @param props.children - The children to render inside the wrapper.
21
- * @returns The wrapped children with React context.
22
- */
23
- function Wrapper({ children }: Readonly<{ children: React.ReactNode }>) {
24
- return (
25
- <StrictMode>
26
- <Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
27
- </StrictMode>
28
- )
29
- }
30
-
31
- describe('use-sqlite edge cases', () => {
32
- it('should remove an item and verify it is removed', async () => {
33
- const sql = createSqliteState<Person>({ backend, tableName: 'RemoveTest', key: 'id' })
34
- await sql.batchSet([
35
- { id: '1', name: 'Alice', age: 30 },
36
- { id: '2', name: 'Bob', age: 25 },
37
- ])
38
-
39
- const { result } = renderHook(() => useSqliteValue(sql, {}, []), { wrapper: Wrapper })
40
-
41
- await waitFor(() => {
42
- expect(result.current[0]).toEqual([
43
- { id: '1', name: 'Alice', age: 30 },
44
- { id: '2', name: 'Bob', age: 25 },
45
- ])
46
- expect(result.current[1].keysIndex).toEqual(
47
- new Map([
48
- ['1', 0],
49
- ['2', 1],
50
- ]),
51
- )
52
- })
53
-
54
- act(() => {
55
- sql.delete('1')
56
- })
57
-
58
- await waitFor(() => {
59
- expect(result.current[0]).toEqual([{ id: '2', name: 'Bob', age: 25 }])
60
- expect(result.current[1].keysIndex).toEqual(new Map([['2', 0]]))
61
- })
62
-
63
- act(() => {
64
- sql.delete('2')
65
- })
66
-
67
- await waitFor(() => {
68
- expect(result.current[0]).toEqual([])
69
- })
70
- })
71
- it('should handle deleting a non-existent item gracefully', async () => {
72
- const sql = createSqliteState<Person>({ backend, tableName: 'EdgeCaseTest', key: 'id' })
73
- await sql.batchSet([
74
- { id: '1', name: 'Alice', age: 30 },
75
- { id: '2', name: 'Bob', age: 25 },
76
- ])
77
-
78
- const { result } = renderHook(() => useSqliteValue(sql, {}, []), { wrapper: Wrapper })
79
-
80
- await waitFor(() => {
81
- expect(result.current[0]).toEqual([
82
- { id: '1', name: 'Alice', age: 30 },
83
- { id: '2', name: 'Bob', age: 25 },
84
- ])
85
- })
86
-
87
- act(() => {
88
- sql.delete('3') // Attempt to delete a non-existent item
89
- })
90
-
91
- await waitFor(() => {
92
- expect(result.current[0]).toEqual([
93
- { id: '1', name: 'Alice', age: 30 },
94
- { id: '2', name: 'Bob', age: 25 },
95
- ]) // State should remain unchanged
96
- })
97
- })
98
-
99
- it('should handle deleting all items', async () => {
100
- const sql = createSqliteState<Person>({ backend, tableName: 'DeleteAllTest', key: 'id' })
101
- await sql.batchSet([
102
- { id: '1', name: 'Alice', age: 30 },
103
- { id: '2', name: 'Bob', age: 25 },
104
- ])
105
-
106
- const { result } = renderHook(() => useSqliteValue(sql, {}, []), { wrapper: Wrapper })
107
-
108
- await waitFor(() => {
109
- expect(result.current[0]).toEqual([
110
- { id: '1', name: 'Alice', age: 30 },
111
- { id: '2', name: 'Bob', age: 25 },
112
- ])
113
- })
114
-
115
- act(() => {
116
- sql.delete('1')
117
- sql.delete('2')
118
- })
119
-
120
- await waitFor(() => {
121
- expect(result.current[0]).toEqual([]) // All items should be removed
122
- })
123
- })
124
-
125
- it('should handle concurrent operations', async () => {
126
- const sql = createSqliteState<Person>({ backend, tableName: 'ConcurrentOpsTest', key: 'id' })
127
- await sql.batchSet([
128
- { id: '1', name: 'Alice', age: 30 },
129
- { id: '2', name: 'Bob', age: 25 },
130
- ])
131
-
132
- const { result } = renderHook(() => useSqliteValue(sql, {}, []), { wrapper: Wrapper })
133
-
134
- await waitFor(() => {
135
- expect(result.current[0]).toEqual([
136
- { id: '1', name: 'Alice', age: 30 },
137
- { id: '2', name: 'Bob', age: 25 },
138
- ])
139
- })
140
-
141
- act(() => {
142
- sql.delete('1')
143
- sql.set({ id: '3', name: 'Carol', age: 40 })
144
- })
145
-
146
- await waitFor(() => {
147
- expect(result.current[0]).toEqual([
148
- { id: '2', name: 'Bob', age: 25 },
149
- { id: '3', name: 'Carol', age: 40 },
150
- ]) // State should reflect both operations
151
- })
152
- })
153
-
154
- it('should handle repeated updates, removals, and insertions in a loop', async () => {
155
- const sql = createSqliteState<Person>({ backend, tableName: 'LoopTest', key: 'id' })
156
- sql.clear()
157
- // Initial batch set
158
- await sql.batchSet([
159
- { id: '1', name: 'Alice', age: 30 },
160
- { id: '2', name: 'Bob', age: 25 },
161
- ])
162
-
163
- const { result } = renderHook(() => useSqliteValue(sql, {}, []), { wrapper: Wrapper })
164
-
165
- await waitFor(() => {
166
- expect(result.current[0]).toEqual([
167
- { id: '1', name: 'Alice', age: 30 },
168
- { id: '2', name: 'Bob', age: 25 },
169
- ])
170
- })
171
-
172
- // Perform updates, removals, and insertions in a loop
173
- act(() => {
174
- for (let index = 0; index < 10; index++) {
175
- sql.set({ id: `new-${index}`, name: `Person ${index}`, age: 20 + index }) // Insert new item
176
- sql.delete('1') // Remove an existing item
177
- sql.set({ id: '2', name: `Updated Bob ${index}`, age: 25 + index }) // Update an existing item
178
- }
179
- // fetch next
180
- result.current[1].nextPage()
181
- })
182
-
183
- await waitFor(() => {
184
- expect(result.current[0]).toEqual([
185
- { id: '2', name: 'Updated Bob 9', age: 34 },
186
- { id: 'new-0', name: 'Person 0', age: 20 },
187
- { id: 'new-1', name: 'Person 1', age: 21 },
188
- { id: 'new-2', name: 'Person 2', age: 22 },
189
- { id: 'new-3', name: 'Person 3', age: 23 },
190
- { id: 'new-4', name: 'Person 4', age: 24 },
191
- { id: 'new-5', name: 'Person 5', age: 25 },
192
- { id: 'new-6', name: 'Person 6', age: 26 },
193
- { id: 'new-7', name: 'Person 7', age: 27 },
194
- { id: 'new-8', name: 'Person 8', age: 28 },
195
- { id: 'new-9', name: 'Person 9', age: 29 },
196
- ])
197
- })
198
- })
199
- it('should handle concurrent insertions and deletions', async () => {
200
- const sql = createSqliteState<Person>({ backend, tableName: 'ConcurrentInsertDeleteTest', key: 'id' })
201
- await sql.batchSet([
202
- { id: '1', name: 'Alice', age: 30 },
203
- { id: '2', name: 'Bob', age: 25 },
204
- ])
205
-
206
- const { result } = renderHook(() => useSqliteValue(sql, {}, []), { wrapper: Wrapper })
207
-
208
- await waitFor(() => {
209
- expect(result.current[0]).toEqual([
210
- { id: '1', name: 'Alice', age: 30 },
211
- { id: '2', name: 'Bob', age: 25 },
212
- ])
213
- })
214
-
215
- act(() => {
216
- sql.set({ id: '3', name: 'Carol', age: 40 })
217
- sql.delete('1')
218
- })
219
-
220
- await waitFor(() => {
221
- expect(result.current[0]).toEqual([
222
- { id: '2', name: 'Bob', age: 25 },
223
- { id: '3', name: 'Carol', age: 40 },
224
- ])
225
- })
226
- })
227
-
228
- it('should handle pagination with empty pages', async () => {
229
- const sql = createSqliteState<Person>({ backend, tableName: 'EmptyPaginationTest', key: 'id' })
230
- sql.clear()
231
-
232
- const { result } = renderHook(() => useSqliteValue(sql, {}, []), { wrapper: Wrapper })
233
-
234
- await waitFor(() => {
235
- expect(result.current[0]).toEqual([])
236
- })
237
-
238
- act(() => {
239
- result.current[1].nextPage()
240
- })
241
-
242
- await waitFor(() => {
243
- expect(result.current[0]).toEqual([]) // Still empty
244
- })
245
- })
246
-
247
- it('should handle duplicate key insertions gracefully', async () => {
248
- const sql = createSqliteState<Person>({ backend, tableName: 'DuplicateKeyTest', key: 'id' })
249
- await sql.batchSet([{ id: '1', name: 'Alice', age: 30 }])
250
-
251
- const { result } = renderHook(() => useSqliteValue(sql, {}, []), { wrapper: Wrapper })
252
-
253
- await waitFor(() => {
254
- expect(result.current[0]).toEqual([{ id: '1', name: 'Alice', age: 30 }])
255
- })
256
-
257
- act(() => {
258
- sql.set({ id: '1', name: 'Updated Alice', age: 35 })
259
- })
260
-
261
- await waitFor(() => {
262
- expect(result.current[0]).toEqual([{ id: '1', name: 'Updated Alice', age: 35 }])
263
- })
264
- })
265
-
266
- it('should handle reset during pagination', async () => {
267
- const sql = createSqliteState<Person>({ backend, tableName: 'ResetDuringPaginationTest', key: 'id' })
268
- await sql.batchSet([
269
- { id: '1', name: 'Alice', age: 30 },
270
- { id: '2', name: 'Bob', age: 25 },
271
- ])
272
-
273
- const { result } = renderHook(() => useSqliteValue(sql, {}, []), { wrapper: Wrapper })
274
-
275
- await waitFor(() => {
276
- expect(result.current[0]).toEqual([
277
- { id: '1', name: 'Alice', age: 30 },
278
- { id: '2', name: 'Bob', age: 25 },
279
- ])
280
- })
281
-
282
- act(() => {
283
- result.current[1].reset()
284
- result.current[1].nextPage()
285
- })
286
-
287
- await waitFor(() => {
288
- expect(result.current[0]).toEqual([
289
- { id: '1', name: 'Alice', age: 30 },
290
- { id: '2', name: 'Bob', age: 25 },
291
- ])
292
- })
293
- })
294
-
295
- it('should handle invalid key deletion gracefully', async () => {
296
- const sql = createSqliteState<Person>({ backend, tableName: 'InvalidKeyDeletionTest', key: 'id' })
297
- await sql.batchSet([
298
- { id: '1', name: 'Alice', age: 30 },
299
- { id: '2', name: 'Bob', age: 25 },
300
- ])
301
-
302
- const { result } = renderHook(() => useSqliteValue(sql, {}, []), { wrapper: Wrapper })
303
-
304
- await waitFor(() => {
305
- expect(result.current[0]).toEqual([
306
- { id: '1', name: 'Alice', age: 30 },
307
- { id: '2', name: 'Bob', age: 25 },
308
- ])
309
- })
310
-
311
- act(() => {
312
- sql.delete('non-existent-key')
313
- })
314
-
315
- await waitFor(() => {
316
- expect(result.current[0]).toEqual([
317
- { id: '1', name: 'Alice', age: 30 },
318
- { id: '2', name: 'Bob', age: 25 },
319
- ])
320
- })
321
- })
322
- it('should update a visible document', async () => {
323
- const sql = createSqliteState<Person>({ backend, tableName: 'UpdateVisibleTest', key: 'id' })
324
- await sql.batchSet([
325
- { id: '1', name: 'Alice', age: 30 },
326
- { id: '2', name: 'Bob', age: 25 },
327
- ])
328
-
329
- const { result } = renderHook(() => useSqliteValue(sql, {}, []), { wrapper: Wrapper })
330
-
331
- await waitFor(() => {
332
- expect(result.current[0]).toEqual([
333
- { id: '1', name: 'Alice', age: 30 },
334
- { id: '2', name: 'Bob', age: 25 },
335
- ])
336
- })
337
-
338
- act(() => {
339
- sql.set({ id: '1', name: 'Updated Alice', age: 35 })
340
- })
341
-
342
- await waitFor(() => {
343
- expect(result.current[0]).toEqual([
344
- { id: '1', name: 'Updated Alice', age: 35 },
345
- { id: '2', name: 'Bob', age: 25 },
346
- ])
347
- })
348
- })
349
-
350
- it('should update a non-visible document', async () => {
351
- const sql = createSqliteState<Person>({ backend, tableName: 'UpdateNonVisibleTest', key: 'id' })
352
- await sql.batchSet([
353
- { id: '1', name: 'Alice', age: 30 },
354
- { id: '2', name: 'Bob', age: 25 },
355
- { id: '3', name: 'Carol', age: 40 },
356
- ])
357
-
358
- const { result } = renderHook(() => useSqliteValue(sql, { pageSize: 2 }, []), { wrapper: Wrapper })
359
-
360
- await waitFor(() => {
361
- expect(result.current[0]).toEqual([
362
- { id: '1', name: 'Alice', age: 30 },
363
- { id: '2', name: 'Bob', age: 25 },
364
- ])
365
- })
366
-
367
- act(() => {
368
- sql.set({ id: '3', name: 'Updated Carol', age: 45 })
369
- })
370
-
371
- await waitFor(() => {
372
- expect(result.current[0]).toEqual([
373
- { id: '1', name: 'Alice', age: 30 },
374
- { id: '2', name: 'Bob', age: 25 },
375
- ]) // No change in visible items
376
- })
377
-
378
- act(() => {
379
- result.current[1].nextPage()
380
- })
381
-
382
- await waitFor(() => {
383
- expect(result.current[0]).toEqual([
384
- { id: '1', name: 'Alice', age: 30 },
385
- { id: '2', name: 'Bob', age: 25 },
386
- { id: '3', name: 'Updated Carol', age: 45 },
387
- ])
388
- })
389
- })
390
-
391
- it('should handle updates during pagination', async () => {
392
- const sql = createSqliteState<Person>({ backend, tableName: 'UpdateDuringPaginationTest', key: 'id' })
393
- await sql.batchSet([
394
- { id: '1', name: 'Alice', age: 30 },
395
- { id: '2', name: 'Bob', age: 25 },
396
- { id: '3', name: 'Carol', age: 40 },
397
- ])
398
-
399
- const { result } = renderHook(() => useSqliteValue(sql, { pageSize: 2 }, []), { wrapper: Wrapper })
400
-
401
- await waitFor(() => {
402
- expect(result.current[0]).toEqual([
403
- { id: '1', name: 'Alice', age: 30 },
404
- { id: '2', name: 'Bob', age: 25 },
405
- ])
406
- })
407
-
408
- act(() => {
409
- result.current[1].nextPage()
410
- })
411
-
412
- await waitFor(() => {
413
- expect(result.current[0]).toEqual([
414
- { id: '1', name: 'Alice', age: 30 },
415
- { id: '2', name: 'Bob', age: 25 },
416
- { id: '3', name: 'Carol', age: 40 },
417
- ])
418
- })
419
-
420
- act(() => {
421
- sql.set({ id: '2', name: 'Updated Bob', age: 35 })
422
- })
423
-
424
- await waitFor(() => {
425
- expect(result.current[0]).toEqual([
426
- { id: '1', name: 'Alice', age: 30 },
427
- { id: '2', name: 'Updated Bob', age: 35 },
428
- { id: '3', name: 'Carol', age: 40 },
429
- ])
430
- })
431
- })
432
-
433
- it('should handle rapid consecutive updates without losing state', async () => {
434
- const sql = createSqliteState<Person>({ backend, tableName: 'RaceState', key: 'id' })
435
-
436
- // Insert initial document
437
- await sql.set({ id: '1', name: 'Alice', age: 30 })
438
-
439
- const { result } = renderHook(() => useSqliteValue(sql, {}, []))
440
-
441
- await waitFor(() => {
442
- expect(result.current[0]).toEqual([{ id: '1', name: 'Alice', age: 30 }])
443
- })
444
-
445
- // Now simulate fast consecutive updates to the same key
446
- // These will hit the subscription handler while it's still awaiting `state.get`
447
- await Promise.all([
448
- sql.set({ id: '1', name: 'AliceV2', age: 31 }),
449
- sql.set({ id: '1', name: 'AliceV3', age: 32 }),
450
- sql.set({ id: '1', name: 'AliceV4', age: 33 }),
451
- ])
452
-
453
- // Wait for hook to stabilize
454
- await waitFor(() => {
455
- // eslint-disable-next-line prefer-destructuring, unicorn/prevent-abbreviations
456
- const docs = result.current[0]
457
- expect(docs?.length).toBe(1)
458
- // Expect the *latest* update to win
459
- expect(docs?.[0]).toEqual({ id: '1', name: 'AliceV4', age: 33 })
460
- })
461
- })
462
-
463
- it('should not overwrite newer updates with stale state.get results', async () => {
464
- const sql = createSqliteState<Person>({ backend, tableName: 'RaceFailState', key: 'id' })
465
- await sql.set({ id: '1', name: 'Initial', age: 20 })
466
-
467
- // Delay the first get call artificially to simulate slow DB
468
- let callCount = 0
469
- const originalGet = sql.get
470
- // @ts-expect-error - We mocking the get method for testing
471
- sql.get = async (key: Key, selector: ((document: Person) => Person) | undefined) => {
472
- callCount++
473
- if (callCount === 1) {
474
- // Simulate slow resolution for first update
475
- await new Promise((resolve) => setTimeout(resolve, 50))
476
- }
477
- return originalGet(key, selector)
478
- }
479
-
480
- const { result } = renderHook(() => useSqliteValue(sql, {}, []))
481
-
482
- await waitFor(() => {
483
- expect(result.current[0]).toEqual([{ id: '1', name: 'Initial', age: 20 }])
484
- })
485
-
486
- // Trigger two consecutive updates
487
- await sql.set({ id: '1', name: 'AliceV1', age: 21 })
488
- await sql.set({ id: '1', name: 'AliceV2', age: 22 })
489
-
490
- // Wait for hook to stabilize
491
- await waitFor(() => {
492
- // eslint-disable-next-line prefer-destructuring, unicorn/prevent-abbreviations
493
- const docs = result.current[0]
494
- expect(docs?.length).toBe(1)
495
- // 🔥 Correct behavior: should end with the *latest* version (AliceV2)
496
- // ❌ Buggy behavior: may still show AliceV1 because the delayed get resolves last
497
- expect(docs?.[0]).toEqual({ id: '1', name: 'AliceV2', age: 22 })
498
- })
499
- })
500
- it('should reset correctly during pagination', async () => {
501
- const sql = createSqliteState<Person>({ backend, tableName: 'ResetMid', key: 'id' })
502
- const people = Array.from({ length: 200 }, (_, index) => ({ id: `${index + 1}`, name: `P${index + 1}`, age: index }))
503
- await sql.batchSet(people)
504
-
505
- const { result } = renderHook(() => useSqliteValue(sql, { pageSize: 50 }, []))
506
-
507
- await waitFor(() => {
508
- expect(result.current[0]?.length).toBe(50)
509
- })
510
-
511
- // Load next page
512
- await act(async () => {
513
- await result.current[1].nextPage()
514
- })
515
- expect(result.current[0]?.length).toBe(100)
516
-
517
- // Reset
518
- await act(async () => {
519
- await result.current[1].reset()
520
- })
521
- await waitFor(() => {
522
- // Should go back to first page only
523
- expect(result.current[0]?.length).toBe(50)
524
- expect(result.current[0]?.[0]?.id).toBe('1')
525
- })
526
- })
527
- it('should overwrite duplicate keys instead of duplicating items', async () => {
528
- const sql = createSqliteState<Person>({ backend, tableName: 'DupTest', key: 'id' })
529
- await sql.set({ id: '1', name: 'Alice', age: 30 })
530
- await sql.set({ id: '1', name: 'Alice2', age: 35 }) // overwrite
531
-
532
- const { result } = renderHook(() => useSqliteValue(sql, {}, []))
533
-
534
- await waitFor(() => {
535
- expect(result.current[0]).toEqual([{ id: '1', name: 'Alice2', age: 35 }])
536
- })
537
- })
538
- it('should not update after unmount', async () => {
539
- const sql = createSqliteState<Person>({ backend, tableName: 'UnmountTest', key: 'id' })
540
- const { unmount, result } = renderHook(() => useSqliteValue(sql, {}, []))
541
-
542
- unmount()
543
- await sql.set({ id: '1', name: 'ShouldNotAppear', age: 99 })
544
-
545
- // wait briefly to give subscription a chance
546
- await new Promise((r) => setTimeout(r, 20))
547
-
548
- expect(result.current[0]).toBeNull() // no state change after unmount
549
- })
550
- it('should restart iterator when sortBy changes', async () => {
551
- const sql = createSqliteState<Person>({ backend, tableName: 'SortChange', key: 'id' })
552
- await sql.batchSet([
553
- { id: '1', name: 'A', age: 40 },
554
- { id: '2', name: 'B', age: 20 },
555
- ])
556
-
557
- const { result, rerender } = renderHook(({ sortBy }) => useSqliteValue(sql, { sortBy }, [sortBy]), {
558
- initialProps: { sortBy: 'age' as keyof Person },
559
- })
560
-
561
- await waitFor(() => {
562
- expect(result.current[0]?.[0]?.age).toBe(20)
563
- })
564
-
565
- act(() => {
566
- rerender({ sortBy: 'name' })
567
- })
568
-
569
- await waitFor(() => {
570
- expect(result.current[0]?.[0]?.name).toBe('A') // sorted by name asc
571
- })
572
- })
573
-
574
- it('should return done when nextPage is called on empty table', async () => {
575
- const sql = createSqliteState<Person>({ backend, tableName: 'EmptyNext', key: 'id' })
576
- const { result } = renderHook(() => useSqliteValue(sql, {}, []))
577
-
578
- // @ts-expect-error - Testing internal method
579
- const isDone = await act(result.current[1].nextPage)
580
- expect(isDone).toBe(true)
581
- expect(result.current[0]).toEqual([])
582
- })
583
-
584
- it('should handle deletion of non-visible item gracefully', async () => {
585
- const sql = createSqliteState<Person>({ backend, tableName: 'DeleteNonVisible', key: 'id' })
586
- const people = Array.from({ length: 200 }, (_, index) => ({ id: `${index + 1}`, name: `P${index + 1}`, age: index }))
587
- await sql.batchSet(people)
588
- let renderCount = 0
589
- const { result } = renderHook(() => {
590
- renderCount++
591
- return useSqliteValue(sql, { pageSize: 50 }, [])
592
- })
593
- await waitFor(() => {
594
- expect(result.current[0]?.length).toBe(50)
595
- })
596
- const initialRenders = renderCount
597
-
598
- // Delete item outside current page
599
- await act(async () => {
600
- await sql.delete('150')
601
- })
602
-
603
- await waitFor(() => {
604
- expect(result.current[0]?.length).toBe(50) // unchanged page size
605
- })
606
- // No re-render for non-visible item deletion
607
- expect(renderCount).toBe(initialRenders)
608
- })
609
- it('should not rerender when using select if the selected value does not change', async () => {
610
- const sql = createSqliteState<Person>({ backend, tableName: 'SelectNoReRender', key: 'id' })
611
- await sql.set({ id: '1', name: 'Alice', age: 30 })
612
-
613
- let renders = 0
614
- const { result } = renderHook(() => {
615
- renders++
616
- // Only project `name`
617
- return useSqliteValue(sql, { select: (p) => p.name }, [])
618
- })
619
-
620
- await waitFor(() => {
621
- expect(result.current[0]).toEqual(['Alice'])
622
- })
623
- const initialRenders = renders
624
-
625
- // Update age (not part of select projection)
626
- await act(async () => {
627
- await sql.set({ id: '1', name: 'Alice', age: 31 })
628
- })
629
-
630
- // Wait a bit to let subscription flush
631
- await new Promise((r) => setTimeout(r, 20))
632
-
633
- // No re-render since selected value "Alice" didn't change
634
- expect(result.current[0]).toEqual(['Alice'])
635
- expect(renders).toBe(initialRenders)
636
- })
637
- })