Skip to content

Commit 95df0cd

Browse files
committed
additional tests relarted to #370
1 parent 967f9e0 commit 95df0cd

File tree

4 files changed

+222
-1
lines changed

4 files changed

+222
-1
lines changed

packages/svelte-db/src/useLiveQuery.svelte.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { untrack } from "svelte"
1+
import { flushSync, untrack } from "svelte"
22
import { createLiveQueryCollection } from "@tanstack/db"
33
import { SvelteMap } from "svelte/reactivity"
44
import type {
@@ -315,6 +315,15 @@ export function useLiveQuery(
315315
// Initialize data array in correct order
316316
syncDataFromCollection(currentCollection)
317317

318+
// Listen for the first ready event to catch status transitions
319+
// that might not trigger change events (fixes async status transition bug)
320+
currentCollection.onFirstReady(() => {
321+
// Use flushSync to ensure Svelte reactivity updates properly
322+
flushSync(() => {
323+
status = currentCollection.status
324+
})
325+
})
326+
318327
// Subscribe to collection changes with granular updates
319328
currentUnsubscribe = currentCollection.subscribeChanges(
320329
(changes: Array<ChangeMessage<any>>) => {

packages/svelte-db/tests/useLiveQuery.svelte.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1190,6 +1190,72 @@ describe(`Query Collections`, () => {
11901190
expect(query.isReady).toBe(true)
11911191
})
11921192
})
1193+
1194+
it(`should handle status transitions correctly with onFirstReady`, () => {
1195+
// This test verifies that the onFirstReady callback properly updates status
1196+
let beginFn: (() => void) | undefined
1197+
let commitFn: (() => void) | undefined
1198+
let markReadyFn: (() => void) | undefined
1199+
1200+
const collection = createCollection<Person>({
1201+
id: `onfirstready-test`,
1202+
getKey: (person: Person) => person.id,
1203+
startSync: false,
1204+
sync: {
1205+
sync: ({ begin, commit, markReady }) => {
1206+
beginFn = begin
1207+
commitFn = commit
1208+
markReadyFn = markReady
1209+
// Don't sync immediately
1210+
},
1211+
},
1212+
onInsert: () => Promise.resolve(),
1213+
onUpdate: () => Promise.resolve(),
1214+
onDelete: () => Promise.resolve(),
1215+
})
1216+
1217+
cleanup = $effect.root(() => {
1218+
const query = useLiveQuery((q) =>
1219+
q
1220+
.from({ collection })
1221+
.where(({ collection: c }) => gt(c.age, 30))
1222+
.select(({ collection: c }) => ({
1223+
id: c.id,
1224+
name: c.name,
1225+
}))
1226+
)
1227+
1228+
// Initially should be loading
1229+
expect(query.isLoading).toBe(true)
1230+
expect(query.isReady).toBe(false)
1231+
1232+
// Start sync manually
1233+
collection.preload()
1234+
1235+
// Trigger the first commit to make collection ready
1236+
if (beginFn && commitFn && markReadyFn) {
1237+
beginFn()
1238+
commitFn()
1239+
markReadyFn()
1240+
}
1241+
1242+
// Insert data
1243+
collection.insert({
1244+
id: `1`,
1245+
name: `John Doe`,
1246+
age: 35,
1247+
1248+
isActive: true,
1249+
team: `team1`,
1250+
})
1251+
1252+
// Wait for the status to transition correctly
1253+
flushSync()
1254+
expect(query.isLoading).toBe(false)
1255+
expect(query.isReady).toBe(true)
1256+
expect(query.status).toBe(`ready`)
1257+
})
1258+
})
11931259
})
11941260

11951261
it(`should accept config object with pre-built QueryBuilder instance`, async () => {

packages/vue-db/src/useLiveQuery.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
computed,
33
getCurrentInstance,
4+
nextTick,
45
onUnmounted,
56
reactive,
67
ref,
@@ -298,6 +299,15 @@ export function useLiveQuery(
298299
// Initialize data array in correct order
299300
syncDataFromCollection(currentCollection)
300301

302+
// Listen for the first ready event to catch status transitions
303+
// that might not trigger change events (fixes async status transition bug)
304+
currentCollection.onFirstReady(() => {
305+
// Use nextTick to ensure Vue reactivity updates properly
306+
nextTick(() => {
307+
status.value = currentCollection.status
308+
})
309+
})
310+
301311
// Subscribe to collection changes with granular updates
302312
currentUnsubscribe = currentCollection.subscribeChanges(
303313
(changes: Array<ChangeMessage<any>>) => {

packages/vue-db/tests/useLiveQuery.test.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,6 +1258,142 @@ describe(`Query Collections`, () => {
12581258
minAge.value = 25
12591259
await waitFor(() => expect(isReady.value).toBe(true))
12601260
})
1261+
1262+
it(`should handle async status transitions correctly`, async () => {
1263+
let beginFn: (() => void) | undefined
1264+
let commitFn: (() => void) | undefined
1265+
let markReadyFn: (() => void) | undefined
1266+
1267+
const collection = createCollection<Person>({
1268+
id: `async-status-transition-test`,
1269+
getKey: (person: Person) => person.id,
1270+
startSync: false,
1271+
sync: {
1272+
sync: ({ begin, commit, markReady }) => {
1273+
beginFn = begin
1274+
commitFn = commit
1275+
markReadyFn = markReady
1276+
// Don't sync immediately
1277+
},
1278+
},
1279+
onInsert: () => Promise.resolve(),
1280+
onUpdate: () => Promise.resolve(),
1281+
onDelete: () => Promise.resolve(),
1282+
})
1283+
1284+
const { isLoading, isReady, status } = useLiveQuery((q) =>
1285+
q
1286+
.from({ collection })
1287+
.where(({ collection: c }) => gt(c.age, 30))
1288+
.select(({ collection: c }) => ({
1289+
id: c.id,
1290+
name: c.name,
1291+
}))
1292+
)
1293+
1294+
// Initially should be loading
1295+
expect(isLoading.value).toBe(true)
1296+
expect(isReady.value).toBe(false)
1297+
expect(status.value).toBe(`loading`)
1298+
1299+
// Start sync manually
1300+
collection.preload()
1301+
1302+
// Trigger the first commit to make collection ready
1303+
if (beginFn && commitFn && markReadyFn) {
1304+
beginFn()
1305+
commitFn()
1306+
// Simulate async delay before marking ready
1307+
await new Promise((resolve) => setTimeout(resolve, 10))
1308+
markReadyFn()
1309+
}
1310+
1311+
// Insert data
1312+
collection.insert({
1313+
id: `1`,
1314+
name: `John Doe`,
1315+
age: 35,
1316+
1317+
isActive: true,
1318+
team: `team1`,
1319+
})
1320+
1321+
// Wait for the status to transition correctly
1322+
await waitFor(() => {
1323+
expect(isLoading.value).toBe(false)
1324+
expect(isReady.value).toBe(true)
1325+
expect(status.value).toBe(`ready`)
1326+
})
1327+
})
1328+
1329+
it(`should handle status transitions without change events`, async () => {
1330+
// This test reproduces the bug where status gets stuck in 'initialCommit'
1331+
// when the collection status changes without triggering change events
1332+
let beginFn: (() => void) | undefined
1333+
let commitFn: (() => void) | undefined
1334+
let markReadyFn: (() => void) | undefined
1335+
1336+
const collection = createCollection<Person>({
1337+
id: `status-stuck-test`,
1338+
getKey: (person: Person) => person.id,
1339+
startSync: false,
1340+
sync: {
1341+
sync: ({ begin, commit, markReady }) => {
1342+
beginFn = begin
1343+
commitFn = commit
1344+
markReadyFn = markReady
1345+
// Don't sync immediately
1346+
},
1347+
},
1348+
onInsert: () => Promise.resolve(),
1349+
onUpdate: () => Promise.resolve(),
1350+
onDelete: () => Promise.resolve(),
1351+
})
1352+
1353+
const { isLoading, isReady, status } = useLiveQuery((q) =>
1354+
q
1355+
.from({ collection })
1356+
.where(({ collection: c }) => gt(c.age, 30))
1357+
.select(({ collection: c }) => ({
1358+
id: c.id,
1359+
name: c.name,
1360+
}))
1361+
)
1362+
1363+
// Initially should be loading
1364+
expect(isLoading.value).toBe(true)
1365+
expect(isReady.value).toBe(false)
1366+
1367+
// Start sync manually
1368+
collection.preload()
1369+
1370+
// Trigger the first commit to make collection ready
1371+
if (beginFn && commitFn && markReadyFn) {
1372+
beginFn()
1373+
commitFn()
1374+
// Simulate async delay before marking ready
1375+
await new Promise((resolve) => setTimeout(resolve, 10))
1376+
markReadyFn()
1377+
}
1378+
1379+
// Insert data
1380+
collection.insert({
1381+
id: `1`,
1382+
name: `John Doe`,
1383+
age: 35,
1384+
1385+
isActive: true,
1386+
team: `team1`,
1387+
})
1388+
1389+
// Wait for the status to transition correctly
1390+
// This should work even if no change events are fired
1391+
await waitFor(() => {
1392+
expect(isLoading.value).toBe(false)
1393+
expect(isReady.value).toBe(true)
1394+
expect(status.value).toBe(`ready`)
1395+
})
1396+
})
12611397
})
12621398

12631399
it(`should accept config object with pre-built QueryBuilder instance`, async () => {

0 commit comments

Comments
 (0)