From 4e3656b2b17a43309e2aa9741e07a6c2235e886d Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 14:20:44 +0000 Subject: [PATCH 1/2] test: verify progressive mode avoids requestSnapshot in joins and queries (issue #811) Add regression tests to verify that progressive mode collections properly avoid calling requestSnapshot() after initial sync completes. This ensures joins between progressive collections and ordered queries work correctly. Tests verify: - Joins between two progressive collections after initial sync - Joins when joined collection is still in buffering phase - Ordered queries on progressive collections - Ordered queries with limit Co-Authored-By: Claude Opus 4.6 --- .../tests/electric-progressive-join.test.ts | 361 ++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 packages/electric-db-collection/tests/electric-progressive-join.test.ts diff --git a/packages/electric-db-collection/tests/electric-progressive-join.test.ts b/packages/electric-db-collection/tests/electric-progressive-join.test.ts new file mode 100644 index 000000000..0fee4feff --- /dev/null +++ b/packages/electric-db-collection/tests/electric-progressive-join.test.ts @@ -0,0 +1,361 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { + createCollection, + createLiveQueryCollection, + eq, +} from '@tanstack/db' +import { electricCollectionOptions } from '../src/electric' +import type { Message } from '@electric-sql/client' + +// Sample types for tests +type User = { + id: number + name: string + department_id: number +} + +type Department = { + id: number + name: string +} + +// Sample data +const sampleUsers: Array = [ + { id: 1, name: `Alice`, department_id: 1 }, + { id: 2, name: `Bob`, department_id: 1 }, + { id: 3, name: `Charlie`, department_id: 2 }, +] + +const sampleDepartments: Array = [ + { id: 1, name: `Engineering` }, + { id: 2, name: `Sales` }, +] + +// Mock the ShapeStream module - we need separate mocks for different collections +const mockUsersSubscribe = vi.fn() +const mockUsersRequestSnapshot = vi.fn() +const mockUsersFetchSnapshot = vi.fn() +const mockUsersStream = { + subscribe: mockUsersSubscribe, + requestSnapshot: mockUsersRequestSnapshot, + fetchSnapshot: mockUsersFetchSnapshot, +} + +const mockDepartmentsSubscribe = vi.fn() +const mockDepartmentsRequestSnapshot = vi.fn() +const mockDepartmentsFetchSnapshot = vi.fn() +const mockDepartmentsStream = { + subscribe: mockDepartmentsSubscribe, + requestSnapshot: mockDepartmentsRequestSnapshot, + fetchSnapshot: mockDepartmentsFetchSnapshot, +} + +// Track which collection is being created to return the right mock stream +let creatingCollection: `users` | `departments` = `users` + +vi.mock(`@electric-sql/client`, async () => { + const actual = await vi.importActual(`@electric-sql/client`) + return { + ...actual, + ShapeStream: vi.fn(() => { + if (creatingCollection === `users`) { + return mockUsersStream + } + return mockDepartmentsStream + }), + } +}) + +describe(`Electric Collection - Progressive mode with joins`, () => { + let usersSubscriber: (messages: Array>) => void + let departmentsSubscriber: (messages: Array>) => void + + function createProgressiveUsersCollection() { + creatingCollection = `users` + + mockUsersSubscribe.mockImplementation((callback) => { + usersSubscriber = callback + return () => {} + }) + + // Make requestSnapshot throw the error we see in the issue + mockUsersRequestSnapshot.mockImplementation(() => { + throw new Error( + `Snapshot requests are not supported in full mode, as the consumer is guaranteed to observe all data`, + ) + }) + + mockUsersFetchSnapshot.mockResolvedValue({ + metadata: {}, + data: [], + }) + + const options = electricCollectionOptions({ + id: `progressive-users`, + shapeOptions: { + url: `http://test-url`, + params: { table: `users` }, + }, + syncMode: `progressive`, + getKey: (user: User) => user.id, + }) + + return createCollection({ + ...options, + startSync: true, + autoIndex: `eager` as const, + }) + } + + function createProgressiveDepartmentsCollection() { + creatingCollection = `departments` + + mockDepartmentsSubscribe.mockImplementation((callback) => { + departmentsSubscriber = callback + return () => {} + }) + + // Make requestSnapshot throw the error we see in the issue + mockDepartmentsRequestSnapshot.mockImplementation(() => { + throw new Error( + `Snapshot requests are not supported in full mode, as the consumer is guaranteed to observe all data`, + ) + }) + + mockDepartmentsFetchSnapshot.mockResolvedValue({ + metadata: {}, + data: [], + }) + + const options = electricCollectionOptions({ + id: `progressive-departments`, + shapeOptions: { + url: `http://test-url`, + params: { table: `departments` }, + }, + syncMode: `progressive`, + getKey: (dept: Department) => dept.id, + }) + + return createCollection({ + ...options, + startSync: true, + autoIndex: `eager` as const, + }) + } + + function simulateUsersSync(users: Array = sampleUsers) { + const messages: Array> = users.map((user) => ({ + key: user.id.toString(), + value: user, + headers: { operation: `insert` }, + })) + messages.push({ headers: { control: `up-to-date` } }) + usersSubscriber(messages) + } + + function simulateDepartmentsSync( + departments: Array = sampleDepartments, + ) { + const messages: Array> = departments.map((dept) => ({ + key: dept.id.toString(), + value: dept, + headers: { operation: `insert` }, + })) + messages.push({ headers: { control: `up-to-date` } }) + departmentsSubscriber(messages) + } + + beforeEach(() => { + vi.clearAllMocks() + }) + + it(`should handle join between two progressive collections without calling requestSnapshot`, async () => { + // Create two progressive collections + const usersCollection = createProgressiveUsersCollection() + const departmentsCollection = createProgressiveDepartmentsCollection() + + // Complete initial sync for both collections + simulateUsersSync() + simulateDepartmentsSync() + + expect(usersCollection.status).toBe(`ready`) + expect(departmentsCollection.status).toBe(`ready`) + expect(usersCollection.size).toBe(3) + expect(departmentsCollection.size).toBe(2) + + // Create a live query that joins both collections + // This should NOT throw an error about "Snapshot requests are not supported in full mode" + const joinQuery = createLiveQueryCollection({ + id: `users-with-departments`, + startSync: true, + query: (q) => + q + .from({ user: usersCollection }) + .join( + { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id), + `left`, + ) + .select(({ user, dept }) => ({ + user_name: user.name, + department_name: dept?.name, + })), + }) + + // Wait for the live query to process + await new Promise((resolve) => setTimeout(resolve, 10)) + + // The join query should have all users with their departments + expect(joinQuery.status).toBe(`ready`) + expect(joinQuery.size).toBe(3) // All 3 users + + // Verify that requestSnapshot was NOT called on either collection + // because progressive mode should not use requestSnapshot after initial sync + expect(mockUsersRequestSnapshot).not.toHaveBeenCalled() + expect(mockDepartmentsRequestSnapshot).not.toHaveBeenCalled() + + // Verify the data is correct + const results = joinQuery.toArray + const alice = results.find((r) => r.user_name === `Alice`) + expect(alice).toMatchObject({ + user_name: `Alice`, + department_name: `Engineering`, + }) + + const charlie = results.find((r) => r.user_name === `Charlie`) + expect(charlie).toMatchObject({ + user_name: `Charlie`, + department_name: `Sales`, + }) + }) + + it(`should handle join when joined collection is still in buffering phase`, async () => { + // Create two progressive collections + const usersCollection = createProgressiveUsersCollection() + const departmentsCollection = createProgressiveDepartmentsCollection() + + // Complete sync for users but NOT for departments (still buffering) + simulateUsersSync() + // Don't call simulateDepartmentsSync() - departments is still buffering + + expect(usersCollection.status).toBe(`ready`) + expect(departmentsCollection.status).toBe(`loading`) // Still buffering + + // Mock fetchSnapshot to return department data (this is what progressive mode should use during buffering) + mockDepartmentsFetchSnapshot.mockResolvedValueOnce({ + metadata: {}, + data: sampleDepartments.map((dept) => ({ + key: dept.id.toString(), + value: dept, + headers: { operation: `insert` }, + })), + }) + + // Create a live query that joins both collections + // The departments collection is lazy and still buffering, so it should use fetchSnapshot + const joinQuery = createLiveQueryCollection({ + id: `users-with-departments-buffering`, + startSync: true, + query: (q) => + q + .from({ user: usersCollection }) + .join( + { dept: departmentsCollection }, + ({ user, dept }) => eq(user.department_id, dept.id), + `left`, + ) + .select(({ user, dept }) => ({ + user_name: user.name, + department_name: dept?.name, + })), + }) + + // Wait for the live query to process + await new Promise((resolve) => setTimeout(resolve, 10)) + + // Verify joinQuery exists (avoid unused variable error) + expect(joinQuery).toBeDefined() + + // requestSnapshot should NOT have been called + expect(mockDepartmentsRequestSnapshot).not.toHaveBeenCalled() + + // fetchSnapshot SHOULD have been called (because departments is still buffering) + expect(mockDepartmentsFetchSnapshot).toHaveBeenCalled() + }) + + it(`should handle ordered query on progressive collection after initial sync`, async () => { + // This test reproduces the scenario from the issue comment + // A simple ordered query (no join) on a progressive collection + const usersCollection = createProgressiveUsersCollection() + + // Complete initial sync + simulateUsersSync() + + expect(usersCollection.status).toBe(`ready`) + expect(usersCollection.size).toBe(3) + + // Create an ordered live query (like the useLiveSuspenseQuery example in the issue) + const orderedQuery = createLiveQueryCollection({ + id: `ordered-users`, + startSync: true, + query: (q) => + q + .from({ user: usersCollection }) + .orderBy(({ user }) => user.id, `desc`), + }) + + // Wait for the live query to process + await new Promise((resolve) => setTimeout(resolve, 10)) + + // The query should work without throwing + expect(orderedQuery.status).toBe(`ready`) + expect(orderedQuery.size).toBe(3) + + // requestSnapshot should NOT have been called + expect(mockUsersRequestSnapshot).not.toHaveBeenCalled() + + // Verify ordering is correct (descending by id) + const results = orderedQuery.toArray + expect(results[0]?.name).toBe(`Charlie`) // id: 3 + expect(results[1]?.name).toBe(`Bob`) // id: 2 + expect(results[2]?.name).toBe(`Alice`) // id: 1 + }) + + it(`should handle ordered query with limit on progressive collection after initial sync`, async () => { + // Test ordered query with limit + const usersCollection = createProgressiveUsersCollection() + + // Complete initial sync + simulateUsersSync() + + expect(usersCollection.status).toBe(`ready`) + expect(usersCollection.size).toBe(3) + + // Create an ordered live query with limit + const orderedQuery = createLiveQueryCollection({ + id: `ordered-limited-users`, + startSync: true, + query: (q) => + q + .from({ user: usersCollection }) + .orderBy(({ user }) => user.id, `desc`) + .limit(2), + }) + + // Wait for the live query to process + await new Promise((resolve) => setTimeout(resolve, 10)) + + // The query should work without throwing + expect(orderedQuery.status).toBe(`ready`) + expect(orderedQuery.size).toBe(2) + + // requestSnapshot should NOT have been called + expect(mockUsersRequestSnapshot).not.toHaveBeenCalled() + + // Verify ordering and limit are correct + const results = orderedQuery.toArray + expect(results[0]?.name).toBe(`Charlie`) // id: 3 + expect(results[1]?.name).toBe(`Bob`) // id: 2 + }) +}) From 23ec45e96192a11d2bd0628f36053b5cd659f26d Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 14:22:33 +0000 Subject: [PATCH 2/2] ci: apply automated fixes --- .../tests/electric-progressive-join.test.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/electric-db-collection/tests/electric-progressive-join.test.ts b/packages/electric-db-collection/tests/electric-progressive-join.test.ts index 0fee4feff..14a8c83dc 100644 --- a/packages/electric-db-collection/tests/electric-progressive-join.test.ts +++ b/packages/electric-db-collection/tests/electric-progressive-join.test.ts @@ -1,9 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import { - createCollection, - createLiveQueryCollection, - eq, -} from '@tanstack/db' +import { createCollection, createLiveQueryCollection, eq } from '@tanstack/db' import { electricCollectionOptions } from '../src/electric' import type { Message } from '@electric-sql/client'