|
/** |
|
* `useLiveSuspenseQuery` never releases over an `on-demand` collection whose |
|
* `loadSubset` returns a Promise. |
|
* |
|
* npm install && npx vitest run # real Chromium |
|
* |
|
* The collection is plain `@tanstack/db` — no persistence, no Electric, no |
|
* `orderBy`, no `limit`. The only variable is whether `loadSubset` answers |
|
* with the synchronous `true` that `LoadSubsetFn` allows, or with a Promise. |
|
*/ |
|
import { createCollection, eq } from "@tanstack/db"; |
|
import { useLiveSuspenseQuery } from "@tanstack/react-db"; |
|
import { StrictMode, Suspense } from "react"; |
|
import { createRoot } from "react-dom/client"; |
|
import { expect, test } from "vitest"; |
|
|
|
const ROWS = [ |
|
{ id: `1`, listId: `a`, text: `first` }, |
|
{ id: `2`, listId: `a`, text: `second` }, |
|
]; |
|
|
|
/** |
|
* `mode: "sync"` answers `true`; `mode: "async"` resolves after `delayMs`. |
|
* Both deliver exactly the same rows. |
|
*/ |
|
const makeCollection = ({ mode, delayMs = 0 }) => |
|
createCollection({ |
|
id: `items-${mode}-${delayMs}`, |
|
syncMode: `on-demand`, |
|
getKey: (row) => row.id, |
|
sync: { |
|
sync: ({ begin, write, commit, markReady }) => { |
|
begin(); |
|
commit(); |
|
markReady(); |
|
|
|
const deliver = () => { |
|
begin(); |
|
for (const row of ROWS) write({ type: `insert`, value: row }); |
|
commit(); |
|
}; |
|
|
|
return { |
|
loadSubset: () => { |
|
if (mode === `sync`) { |
|
deliver(); |
|
return true; |
|
} |
|
|
|
return new Promise((resolve) => { |
|
setTimeout(() => { |
|
deliver(); |
|
resolve(); |
|
}, delayMs); |
|
}); |
|
}, |
|
}; |
|
}, |
|
}, |
|
}); |
|
|
|
const probe = async ({ mode, delayMs = 0, strict = true, queryKey = false }) => { |
|
const collection = makeCollection({ mode, delayMs }); |
|
const renders = { count: 0 }; |
|
|
|
const List = () => { |
|
renders.count++; |
|
|
|
const query = (q) => q.from({ row: collection }).where(({ row }) => eq(row.listId, `a`)); |
|
const { data } = useLiveSuspenseQuery( |
|
queryKey ? { queryKey: [mode, delayMs], query } : { query }, |
|
); |
|
|
|
return <span>rows:{data.length}</span>; |
|
}; |
|
|
|
const host = document.createElement(`div`); |
|
document.body.appendChild(host); |
|
const root = createRoot(host); |
|
|
|
const tree = ( |
|
<Suspense fallback={<span>loading</span>}> |
|
<List /> |
|
</Suspense> |
|
); |
|
|
|
root.render(strict ? <StrictMode>{tree}</StrictMode> : tree); |
|
|
|
const deadline = Date.now() + 5_000; |
|
|
|
while (Date.now() < deadline && !host.textContent.startsWith(`rows:`)) { |
|
await new Promise((resolve) => setTimeout(resolve, 50)); |
|
} |
|
|
|
const result = { text: host.textContent, renders: renders.count }; |
|
|
|
root.unmount(); |
|
host.remove(); |
|
|
|
return result; |
|
}; |
|
|
|
test(`a synchronous loadSubset releases the boundary`, async () => { |
|
const result = await probe({ mode: `sync` }); |
|
|
|
expect(result.text).toBe(`rows:2`); |
|
expect(result.renders).toBeLessThan(10); |
|
}, 12_000); |
|
|
|
// Everything below is the same collection with an async `loadSubset`. |
|
for (const delayMs of [0, 50, 500]) { |
|
test( |
|
`an async loadSubset resolving after ${delayMs}ms releases the boundary`, |
|
async () => { |
|
const result = await probe({ mode: `async`, delayMs }); |
|
|
|
expect({ text: result.text, renders: result.renders }).toEqual({ |
|
text: `rows:2`, |
|
renders: result.renders, |
|
}); |
|
expect(result.renders).toBeLessThan(100); |
|
}, |
|
12_000, |
|
); |
|
} |
|
|
|
// Two things a reviewer would reasonably suspect, ruled out. |
|
test(`an explicit queryKey does not change the outcome`, async () => { |
|
const result = await probe({ mode: `async`, delayMs: 500, queryKey: true }); |
|
|
|
expect(result.text).toBe(`rows:2`); |
|
}, 12_000); |
|
|
|
test(`StrictMode is not the cause`, async () => { |
|
const result = await probe({ mode: `async`, delayMs: 500, strict: false }); |
|
|
|
expect(result.text).toBe(`rows:2`); |
|
}, 12_000); |