Skip to content

Commit 1c54b1b

Browse files
committed
setWindow returns a promise when it triggers loading subset
1 parent f8cd6fe commit 1c54b1b

2 files changed

Lines changed: 177 additions & 2 deletions

File tree

packages/db/src/query/live/collection-config-builder.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@ export type LiveQueryCollectionUtils = UtilsRecord & {
3939
/**
4040
* Sets the offset and limit of an ordered query.
4141
* Is a no-op if the query is not ordered.
42+
*
43+
* @returns `true` if no subset loading was triggered, or `Promise<void>` that resolves when the subset has been loaded
4244
*/
43-
setWindow: (options: WindowOptions) => void
45+
setWindow: (options: WindowOptions) => true | Promise<void>
4446
}
4547

4648
type PendingGraphRun = {
@@ -189,13 +191,32 @@ export class CollectionConfigBuilder<
189191
}
190192
}
191193

192-
setWindow(options: WindowOptions) {
194+
setWindow(options: WindowOptions): true | Promise<void> {
193195
if (!this.windowFn) {
194196
throw new SetWindowRequiresOrderByError()
195197
}
196198

197199
this.windowFn(options)
198200
this.maybeRunGraphFn?.()
201+
202+
// Check if loading a subset was triggered
203+
if (this.liveQueryCollection?.isLoadingSubset) {
204+
// Loading was triggered, return a promise that resolves when it completes
205+
return new Promise<void>((resolve) => {
206+
const unsubscribe = this.liveQueryCollection!.on(
207+
`loadingSubset:change`,
208+
(event) => {
209+
if (!event.isLoadingSubset) {
210+
unsubscribe()
211+
resolve()
212+
}
213+
}
214+
)
215+
})
216+
}
217+
218+
// No loading was triggered
219+
return true
199220
}
200221

201222
/**

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

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1551,5 +1551,159 @@ describe(`createLiveQueryCollection`, () => {
15511551
`Post F`,
15521552
])
15531553
})
1554+
1555+
it(`setWindow returns true when no subset loading is triggered`, async () => {
1556+
const extendedUsers = createCollection(
1557+
mockSyncCollectionOptions<User>({
1558+
id: `users-no-loading`,
1559+
getKey: (user) => user.id,
1560+
initialData: [
1561+
{ id: 1, name: `Alice`, active: true },
1562+
{ id: 2, name: `Bob`, active: true },
1563+
{ id: 3, name: `Charlie`, active: true },
1564+
],
1565+
})
1566+
)
1567+
1568+
const activeUsers = createLiveQueryCollection((q) =>
1569+
q
1570+
.from({ user: extendedUsers })
1571+
.where(({ user }) => eq(user.active, true))
1572+
.orderBy(({ user }) => user.name, `asc`)
1573+
.limit(2)
1574+
)
1575+
1576+
await activeUsers.preload()
1577+
1578+
// setWindow should return true when no loading is triggered
1579+
const result = activeUsers.utils.setWindow({ offset: 1, limit: 2 })
1580+
expect(result).toBe(true)
1581+
})
1582+
1583+
it(`setWindow returns and resolves a Promise when async loading is triggered`, async () => {
1584+
// This is an integration test that validates the full async flow:
1585+
// 1. setWindow triggers loading more data
1586+
// 2. Returns a Promise (not true)
1587+
// 3. The Promise waits for loading to complete
1588+
// 4. The Promise resolves once loading is done
1589+
1590+
vi.useFakeTimers()
1591+
1592+
try {
1593+
let loadSubsetCallCount = 0
1594+
1595+
const sourceCollection = createCollection<{
1596+
id: number
1597+
value: number
1598+
}>({
1599+
id: `source-async-subset-loading`,
1600+
getKey: (item) => item.id,
1601+
syncMode: `on-demand`,
1602+
startSync: true,
1603+
sync: {
1604+
sync: ({ markReady, begin, write, commit }) => {
1605+
// Provide minimal initial data
1606+
begin()
1607+
write({ type: `insert`, value: { id: 1, value: 1 } })
1608+
write({ type: `insert`, value: { id: 2, value: 2 } })
1609+
write({ type: `insert`, value: { id: 3, value: 3 } })
1610+
commit()
1611+
markReady()
1612+
1613+
return {
1614+
loadSubset: () => {
1615+
loadSubsetCallCount++
1616+
1617+
// First call is for the initial window request
1618+
if (loadSubsetCallCount === 1) {
1619+
return true
1620+
}
1621+
1622+
// Second call (triggered by setWindow) returns a promise
1623+
const loadPromise = new Promise<void>((resolve) => {
1624+
// Simulate async data loading with a delay
1625+
setTimeout(() => {
1626+
begin()
1627+
// Load additional items that would be needed for the new window
1628+
write({ type: `insert`, value: { id: 4, value: 4 } })
1629+
write({ type: `insert`, value: { id: 5, value: 5 } })
1630+
write({ type: `insert`, value: { id: 6, value: 6 } })
1631+
commit()
1632+
resolve()
1633+
}, 50)
1634+
})
1635+
1636+
return loadPromise
1637+
},
1638+
}
1639+
},
1640+
},
1641+
})
1642+
1643+
const liveQuery = createLiveQueryCollection({
1644+
query: (q) =>
1645+
q
1646+
.from({ item: sourceCollection })
1647+
.orderBy(({ item }) => item.value, `asc`)
1648+
.limit(2)
1649+
.offset(0),
1650+
startSync: true,
1651+
})
1652+
1653+
await liveQuery.preload()
1654+
1655+
// Initial state: should have 2 items (values 1, 2)
1656+
expect(liveQuery.size).toBe(2)
1657+
expect(liveQuery.isLoadingSubset).toBe(false)
1658+
expect(loadSubsetCallCount).toBe(1)
1659+
1660+
// Move window to offset 3, which requires loading more data
1661+
// This should trigger loadSubset and return a Promise
1662+
const result = liveQuery.utils.setWindow({ offset: 3, limit: 2 })
1663+
1664+
// CRITICAL VALIDATION: result should be a Promise, not true
1665+
expect(result).toBeInstanceOf(Promise)
1666+
expect(result).not.toBe(true)
1667+
1668+
// Advance just a bit to let the scheduler execute and trigger loadSubset
1669+
await vi.advanceTimersByTimeAsync(1)
1670+
1671+
// Verify that loading was triggered and is in progress
1672+
expect(loadSubsetCallCount).toBeGreaterThan(1)
1673+
expect(liveQuery.isLoadingSubset).toBe(true)
1674+
1675+
// Track when the promise resolves
1676+
let promiseResolved = false
1677+
if (result !== true) {
1678+
result.then(() => {
1679+
promiseResolved = true
1680+
})
1681+
}
1682+
1683+
// Promise should NOT be resolved yet because loading is still in progress
1684+
await vi.advanceTimersByTimeAsync(10)
1685+
expect(promiseResolved).toBe(false)
1686+
expect(liveQuery.isLoadingSubset).toBe(true)
1687+
1688+
// Now advance time to complete the loading (50ms total from loadSubset call)
1689+
await vi.advanceTimersByTimeAsync(40)
1690+
1691+
// Wait for the promise to resolve
1692+
if (result !== true) {
1693+
await result
1694+
}
1695+
1696+
// CRITICAL VALIDATION: Promise has resolved and loading is complete
1697+
expect(promiseResolved).toBe(true)
1698+
expect(liveQuery.isLoadingSubset).toBe(false)
1699+
1700+
// Verify the window was successfully moved and has the right data
1701+
expect(liveQuery.size).toBe(2)
1702+
const items = liveQuery.toArray
1703+
expect(items.map((i) => i.value)).toEqual([4, 5])
1704+
} finally {
1705+
vi.useRealTimers()
1706+
}
1707+
})
15541708
})
15551709
})

0 commit comments

Comments
 (0)