Skip to content

Commit acf11cc

Browse files
committed
Add regression coverage for ordered on-demand paging
1 parent 7ca011d commit acf11cc

2 files changed

Lines changed: 237 additions & 0 deletions

File tree

‎packages/db-collection-e2e/src/suites/pagination.suite.ts‎

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,110 @@ export function createPaginationTestSuite(
264264
await query2.cleanup()
265265
})
266266

267+
it(`should load distinct windows across multiple live queries with multi-column orderBy`, async () => {
268+
const config = await getConfig()
269+
const usersCollection = config.collections.onDemand.users
270+
271+
const page1 = createLiveQueryCollection((q) =>
272+
q
273+
.from({ user: usersCollection })
274+
.orderBy(({ user }) => user.isActive, `desc`)
275+
.orderBy(({ user }) => user.age, `asc`)
276+
.limit(10)
277+
.offset(0),
278+
)
279+
280+
const page2 = createLiveQueryCollection((q) =>
281+
q
282+
.from({ user: usersCollection })
283+
.orderBy(({ user }) => user.isActive, `desc`)
284+
.orderBy(({ user }) => user.age, `asc`)
285+
.limit(10)
286+
.offset(10),
287+
)
288+
289+
await page1.preload()
290+
await waitForQueryData(page1, { minSize: 10 })
291+
await page2.preload()
292+
await waitForQueryData(page2, { minSize: 10 })
293+
294+
const page1Results = Array.from(page1.state.values())
295+
const page2Results = Array.from(page2.state.values())
296+
297+
expect(page1Results).toHaveLength(10)
298+
expect(page2Results).toHaveLength(10)
299+
300+
const page1Ids = new Set(page1Results.map((user) => user.id))
301+
for (const user of page2Results) {
302+
expect(page1Ids.has(user.id)).toBe(false)
303+
}
304+
305+
for (const page of [page1Results, page2Results]) {
306+
for (let i = 1; i < page.length; i++) {
307+
const prev = page[i - 1]!
308+
const curr = page[i]!
309+
310+
if (prev.isActive !== curr.isActive) {
311+
expect(prev.isActive ? 1 : 0).toBeGreaterThanOrEqual(
312+
curr.isActive ? 1 : 0,
313+
)
314+
} else {
315+
expect(prev.age).toBeLessThanOrEqual(curr.age)
316+
}
317+
}
318+
}
319+
320+
await page1.cleanup()
321+
await page2.cleanup()
322+
})
323+
324+
it(`should allow paging a second live query without affecting the first`, async () => {
325+
const config = await getConfig()
326+
const usersCollection = config.collections.onDemand.users
327+
328+
const baseQuery = createLiveQueryCollection((q) =>
329+
q
330+
.from({ user: usersCollection })
331+
.orderBy(({ user }) => user.isActive, `desc`)
332+
.orderBy(({ user }) => user.age, `asc`)
333+
.limit(10)
334+
.offset(0),
335+
)
336+
337+
const pagedQuery = createLiveQueryCollection((q) =>
338+
q
339+
.from({ user: usersCollection })
340+
.orderBy(({ user }) => user.isActive, `desc`)
341+
.orderBy(({ user }) => user.age, `asc`)
342+
.limit(10)
343+
.offset(0),
344+
)
345+
346+
await baseQuery.preload()
347+
await waitForQueryData(baseQuery, { minSize: 10 })
348+
await pagedQuery.preload()
349+
await waitForQueryData(pagedQuery, { minSize: 10 })
350+
351+
const baseIds = new Set(
352+
Array.from(baseQuery.state.values()).map((user) => user.id),
353+
)
354+
355+
const moveResult = pagedQuery.utils.setWindow({ offset: 10, limit: 10 })
356+
if (moveResult !== true) {
357+
await moveResult
358+
}
359+
await waitForQueryData(pagedQuery, { minSize: 10 })
360+
361+
const pagedResults = Array.from(pagedQuery.state.values())
362+
expect(pagedResults).toHaveLength(10)
363+
for (const user of pagedResults) {
364+
expect(baseIds.has(user.id)).toBe(false)
365+
}
366+
367+
await baseQuery.cleanup()
368+
await pagedQuery.cleanup()
369+
})
370+
267371
it(`should handle multi-column orderBy with duplicate values in first column`, async () => {
268372
const config = await getConfig()
269373
const usersCollection = config.collections.onDemand.users

‎packages/db/tests/query/live-query-collection.test.ts‎

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2120,6 +2120,139 @@ describe(`createLiveQueryCollection`, () => {
21202120
vi.useRealTimers()
21212121
}
21222122
})
2123+
2124+
it(`advances offset when async loadSubset fills an initially empty window`, async () => {
2125+
type Item = { id: number; value: number }
2126+
const remoteData: Array<Item> = [
2127+
{ id: 1, value: 1 },
2128+
{ id: 2, value: 2 },
2129+
{ id: 3, value: 3 },
2130+
{ id: 4, value: 4 },
2131+
]
2132+
const loadOffsets: Array<number | undefined> = []
2133+
2134+
const sourceCollection = createCollection<Item>({
2135+
id: `offset-advances-async`,
2136+
getKey: (item) => item.id,
2137+
syncMode: `on-demand`,
2138+
startSync: true,
2139+
autoIndex: `eager`,
2140+
sync: {
2141+
sync: ({ begin, write, commit, markReady }) => {
2142+
markReady()
2143+
return {
2144+
loadSubset: (options: LoadSubsetOptions) => {
2145+
loadOffsets.push(options.offset)
2146+
return new Promise<void>((resolve) => {
2147+
setTimeout(() => {
2148+
begin()
2149+
const start = options.offset ?? 0
2150+
const end = options.limit
2151+
? start + options.limit
2152+
: remoteData.length
2153+
remoteData.slice(start, end).forEach((item) => {
2154+
write({ type: `insert`, value: item })
2155+
})
2156+
commit()
2157+
resolve()
2158+
}, 0)
2159+
})
2160+
},
2161+
}
2162+
},
2163+
},
2164+
})
2165+
2166+
const liveQuery = createLiveQueryCollection((q) =>
2167+
q
2168+
.from({ item: sourceCollection })
2169+
.orderBy(({ item }) => item.value, `asc`)
2170+
.limit(2)
2171+
.offset(0),
2172+
)
2173+
2174+
await liveQuery.preload()
2175+
expect(liveQuery.toArray.map((item) => item.value)).toEqual([1, 2])
2176+
expect(loadOffsets[0]).toBe(0)
2177+
2178+
const moveResult = liveQuery.utils.setWindow({ offset: 2, limit: 2 })
2179+
if (moveResult !== true) {
2180+
await moveResult
2181+
}
2182+
2183+
expect(loadOffsets).toEqual([0, 2])
2184+
expect(liveQuery.toArray.map((item) => item.value)).toEqual([3, 4])
2185+
})
2186+
2187+
it(`requests new offsets when window moves across identical orderBy values`, async () => {
2188+
type Item = { id: number; rank: number }
2189+
const remoteData: Array<Item> = [
2190+
{ id: 1, rank: 1 },
2191+
{ id: 2, rank: 1 },
2192+
{ id: 3, rank: 1 },
2193+
{ id: 4, rank: 1 },
2194+
{ id: 5, rank: 1 },
2195+
{ id: 6, rank: 1 },
2196+
]
2197+
const loadOffsets: Array<number | undefined> = []
2198+
2199+
const sourceCollection = createCollection<Item>({
2200+
id: `offset-moves-constant-orderby`,
2201+
getKey: (item) => item.id,
2202+
syncMode: `on-demand`,
2203+
startSync: true,
2204+
autoIndex: `eager`,
2205+
sync: {
2206+
sync: ({ begin, write, commit, markReady }) => {
2207+
markReady()
2208+
return {
2209+
loadSubset: (options: LoadSubsetOptions) => {
2210+
loadOffsets.push(options.offset)
2211+
const start = options.offset ?? 0
2212+
const end = options.limit
2213+
? start + options.limit
2214+
: remoteData.length
2215+
begin()
2216+
remoteData.slice(start, end).forEach((item) => {
2217+
write({ type: `insert`, value: item })
2218+
})
2219+
commit()
2220+
return true
2221+
},
2222+
}
2223+
},
2224+
},
2225+
})
2226+
2227+
const liveQuery = createLiveQueryCollection((q) =>
2228+
q
2229+
.from({ item: sourceCollection })
2230+
.orderBy(({ item }) => item.rank, `asc`)
2231+
.limit(2)
2232+
.offset(0),
2233+
)
2234+
2235+
await liveQuery.preload()
2236+
await flushPromises()
2237+
expect(loadOffsets[0]).toBe(0)
2238+
expect(liveQuery.toArray.map((item) => item.id)).toEqual([1, 2])
2239+
2240+
const moveFirst = liveQuery.utils.setWindow({ offset: 2, limit: 2 })
2241+
if (moveFirst !== true) {
2242+
await moveFirst
2243+
}
2244+
await flushPromises()
2245+
expect(loadOffsets).toEqual([0, 2])
2246+
expect(liveQuery.toArray.map((item) => item.id)).toEqual([3, 4])
2247+
2248+
const moveSecond = liveQuery.utils.setWindow({ offset: 4, limit: 2 })
2249+
if (moveSecond !== true) {
2250+
await moveSecond
2251+
}
2252+
await flushPromises()
2253+
expect(loadOffsets).toEqual([0, 2, 4])
2254+
expect(liveQuery.toArray.map((item) => item.id)).toEqual([5, 6])
2255+
})
21232256
})
21242257

21252258
describe(`custom getKey with joins error handling`, () => {

0 commit comments

Comments
 (0)