Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/polite-eels-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@tanstack/electric-db-collection": patch
"@tanstack/db": patch
---

Fix repeated renders when markReady called when the collection was already ready. This would occur after each long poll on a Electric collection.
10 changes: 5 additions & 5 deletions packages/db/src/collection/lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,12 @@ export class CollectionLifecycleManager<
this.onFirstReadyCallbacks = []
callbacks.forEach((callback) => callback())
}
}

// Always notify dependents when markReady is called, after status is set
// This ensures live queries get notified when their dependencies become ready
if (this.changes.changeSubscriptions.size > 0) {
this.changes.emitEmptyReadyEvent()
// Notify dependents when markReady is called, after status is set
// This ensures live queries get notified when their dependencies become ready
if (this.changes.changeSubscriptions.size > 0) {
this.changes.emitEmptyReadyEvent()
}
}
}

Expand Down
99 changes: 99 additions & 0 deletions packages/electric-db-collection/tests/electric-live-query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ describe.each([
subscriber(messages)
}

function simulateUpToDateOnly() {
// Send only an up-to-date message with no data changes
subscriber([{ headers: { control: `up-to-date` } }])
}

beforeEach(() => {
electricCollection = createElectricUsersCollection()
})
Expand Down Expand Up @@ -338,4 +343,98 @@ describe.each([
expect(testElectricCollection.status).toBe(`ready`)
expect(liveQuery.status).toBe(`ready`)
})

it.only(`should not emit changes on up-to-date messages with no data changes`, async () => {
// Test to verify that up-to-date messages without actual data changes
// don't trigger unnecessary renders in live query collections

// Create a live query collection
const liveQuery = createLiveQueryCollection({
startSync: true,
query: (q) =>
q
.from({ user: electricCollection })
.where(({ user }) => eq(user.active, true))
.select(({ user }) => ({
id: user.id,
name: user.name,
active: user.active,
})),
})

// Track changes emitted by the live query
const changeNotifications: Array<any> = []
const subscription = liveQuery.subscribeChanges((changes) => {
changeNotifications.push(changes)
})

// Initial sync with data
simulateInitialSync()
expect(liveQuery.status).toBe(`ready`)
expect(liveQuery.size).toBe(3) // Only active users

// Clear any initial change notifications
changeNotifications.length = 0

// Send an up-to-date message with no data changes
// This simulates the scenario where Electric sends up-to-date
// but there are no actual changes to the data
simulateUpToDateOnly()

// Wait a tick to ensure any async operations complete
await new Promise((resolve) => setTimeout(resolve, 0))

// The live query should not have emitted any changes
// because there were no actual data changes
expect(changeNotifications).toHaveLength(0)

// Verify the collection is still in ready state
expect(liveQuery.status).toBe(`ready`)
expect(liveQuery.size).toBe(3)

// Clean up
subscription.unsubscribe()
})

it(`should not emit changes on multiple consecutive up-to-date messages with no data changes`, async () => {
// Test to verify that multiple consecutive up-to-date messages
// without data changes don't accumulate unnecessary renders

const liveQuery = createLiveQueryCollection({
startSync: true,
query: (q) => q.from({ user: electricCollection }),
})

// Track changes emitted by the live query
const changeNotifications: Array<any> = []
const subscription = liveQuery.subscribeChanges((changes) => {
changeNotifications.push(changes)
})

// Initial sync
simulateInitialSync()
expect(liveQuery.status).toBe(`ready`)
expect(liveQuery.size).toBe(4)

// Clear initial change notifications
changeNotifications.length = 0

// Send multiple up-to-date messages with no data changes
simulateUpToDateOnly()
simulateUpToDateOnly()
simulateUpToDateOnly()

// Wait for any async operations
await new Promise((resolve) => setTimeout(resolve, 0))

// Should not have emitted any changes despite multiple up-to-date messages
expect(changeNotifications).toHaveLength(0)

// Verify collection state is still correct
expect(liveQuery.status).toBe(`ready`)
expect(liveQuery.size).toBe(4)

// Clean up
subscription.unsubscribe()
})
})
Loading