/** * RED PHASE Tests for Live Queries (postgres-dsvj.7) * * Tests real-time subscriptions, reactive updates, unsubscribe behavior, * and connection state management for live query hooks. * * These tests should FAIL because the features are not fully implemented yet. */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { createUseLiveQuery, } from '../hooks' import { createQueryCollection, QueryCollection } from '../query/query-collection' import { createSyncCollection, SyncCollection } from '../sync/sync-collection' import { createTanStackStore } from '../store' import type { BaseRecord, Collection, SyncState } from '../types' // ============================================================================ // Test Types // ============================================================================ interface Todo extends BaseRecord { id: number title: string completed: boolean priority: 'low' | 'medium' | 'high' order: number createdAt: number updatedAt: number } interface Message extends BaseRecord { id: string channel: string content: string author: string timestamp: number } // ============================================================================ // Mock Helpers // ============================================================================ function createMockCollection(initialItems: T[] = []): Collection & { _items: T[] _subscribers: Set<(items: T[]) => void> _simulateExternalUpdate: (items: T[]) => void } { let items = [...initialItems] const subscribers = new Set<(items: T[]) => void>() const collection = { id: 'test-collection', _items: items, _subscribers: subscribers, _simulateExternalUpdate: (newItems: T[]) => { items = newItems collection._items = items subscribers.forEach(cb => cb(items)) }, getAll: () => items, get: (id: string | number) => items.find(item => item.id === id), insert: vi.fn().mockImplementation(async (data) => { const newItem = { ...data, id: data.id ?? Date.now() } as T items = [...items, newItem] collection._items = items subscribers.forEach(cb => cb(items)) return newItem }), update: vi.fn().mockImplementation(async (id, data) => { const existing = items.find(item => item.id === id) if (!existing) throw new Error(`Item ${id} not found`) const updated = { ...existing, ...data } as T items = items.map(item => item.id === id ? updated : item) collection._items = items subscribers.forEach(cb => cb(items)) return updated }), delete: vi.fn().mockImplementation(async (id) => { items = items.filter(item => item.id !== id) collection._items = items subscribers.forEach(cb => cb(items)) }), subscribe: (callback: (items: T[]) => void) => { subscribers.add(callback) return () => subscribers.delete(callback) }, subscribeWithTimeout: (callback: (items: T[]) => void, options: { timeoutMs: number }) => { let timeoutId: ReturnType | undefined let unsubscribed = false const wrappedCallback = (cbItems: T[]) => { if (unsubscribed) return callback(cbItems) // Reset timeout on each activity if (timeoutId) clearTimeout(timeoutId) timeoutId = setTimeout(() => { unsubscribed = true subscribers.delete(wrappedCallback) }, options.timeoutMs) } subscribers.add(wrappedCallback) // Start the inactivity timer timeoutId = setTimeout(() => { unsubscribed = true subscribers.delete(wrappedCallback) }, options.timeoutMs) return () => { unsubscribed = true if (timeoutId) clearTimeout(timeoutId) subscribers.delete(wrappedCallback) } }, pausableSubscribe: (callback: (items: T[]) => void) => { let paused = false const wrappedCallback = (cbItems: T[]) => { if (!paused) { callback(cbItems) } } subscribers.add(wrappedCallback) return { pause: () => { paused = true }, resume: () => { paused = false }, unsubscribe: () => { subscribers.delete(wrappedCallback) }, } }, getSyncState: () => ({ connected: true, initialized: true, pendingCount: 0, }), } return collection } function createMockUseSyncExternalStore() { return vi.fn().mockImplementation( (subscribe: (cb: () => void) => () => void, getSnapshot: () => unknown) => { return getSnapshot() } ) } // ============================================================================ // Section 1: Real-time Subscriptions // ============================================================================ describe('Live Queries: Real-time Subscriptions', () => { it('should subscribe to collection changes', () => { const mockUseSyncExternalStore = createMockUseSyncExternalStore() const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection([ { id: 1, title: 'Task 1', completed: false, priority: 'high', order: 1, createdAt: 1000, updatedAt: 1000 }, ]) const result = useLiveQuery(collection) expect(mockUseSyncExternalStore).toHaveBeenCalledWith( expect.any(Function), // subscribe expect.any(Function), // getSnapshot expect.any(Function), // getServerSnapshot ) expect(result).toHaveLength(1) expect(result[0].title).toBe('Task 1') }) it('should receive updates when collection changes', () => { let currentSnapshot: Todo[] = [] const mockUseSyncExternalStore = vi.fn().mockImplementation( (subscribe: (cb: () => void) => () => void, getSnapshot: () => Todo[]) => { const onChange = () => { currentSnapshot = getSnapshot() } subscribe(onChange) currentSnapshot = getSnapshot() return currentSnapshot } ) const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection([ { id: 1, title: 'Task 1', completed: false, priority: 'low', order: 1, createdAt: 1000, updatedAt: 1000 }, ]) useLiveQuery(collection) expect(currentSnapshot).toHaveLength(1) // Simulate external update collection._simulateExternalUpdate([ { id: 1, title: 'Task 1', completed: false, priority: 'low', order: 1, createdAt: 1000, updatedAt: 1000 }, { id: 2, title: 'Task 2', completed: false, priority: 'medium', order: 2, createdAt: 2000, updatedAt: 2000 }, ]) expect(currentSnapshot).toHaveLength(2) }) it('should filter subscription results with where clause', () => { const mockUseSyncExternalStore = createMockUseSyncExternalStore() const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection([ { id: 1, title: 'Task 1', completed: false, priority: 'high', order: 1, createdAt: 1000, updatedAt: 1000 }, { id: 2, title: 'Task 2', completed: true, priority: 'low', order: 2, createdAt: 2000, updatedAt: 2000 }, { id: 3, title: 'Task 3', completed: false, priority: 'medium', order: 3, createdAt: 3000, updatedAt: 3000 }, ]) const result = useLiveQuery(collection, { where: { completed: false }, }) expect(result).toHaveLength(2) expect(result.every(t => !t.completed)).toBe(true) }) it('should filter with function predicate', () => { const mockUseSyncExternalStore = createMockUseSyncExternalStore() const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection([ { id: 1, title: 'Task 1', completed: false, priority: 'high', order: 1, createdAt: 1000, updatedAt: 1000 }, { id: 2, title: 'Task 2', completed: true, priority: 'low', order: 2, createdAt: 2000, updatedAt: 2000 }, { id: 3, title: 'Important Task', completed: false, priority: 'high', order: 3, createdAt: 3000, updatedAt: 3000 }, ]) const result = useLiveQuery(collection, { where: (item) => item.priority === 'high' && !item.completed, }) expect(result).toHaveLength(2) }) it('should sort subscription results', () => { const mockUseSyncExternalStore = createMockUseSyncExternalStore() const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection([ { id: 3, title: 'C Task', completed: false, priority: 'low', order: 3, createdAt: 3000, updatedAt: 3000 }, { id: 1, title: 'A Task', completed: false, priority: 'high', order: 1, createdAt: 1000, updatedAt: 1000 }, { id: 2, title: 'B Task', completed: false, priority: 'medium', order: 2, createdAt: 2000, updatedAt: 2000 }, ]) const result = useLiveQuery(collection, { orderBy: { field: 'title', direction: 'asc' }, }) expect(result.map(t => t.title)).toEqual(['A Task', 'B Task', 'C Task']) }) it('should apply limit and offset to subscription results', () => { const mockUseSyncExternalStore = createMockUseSyncExternalStore() const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const items: Todo[] = Array.from({ length: 10 }, (_, i) => ({ id: i + 1, title: `Task ${i + 1}`, completed: false, priority: 'low' as const, order: i + 1, createdAt: (i + 1) * 1000, updatedAt: (i + 1) * 1000, })) const collection = createMockCollection(items) const result = useLiveQuery(collection, { offset: 2, limit: 3 }) expect(result).toHaveLength(3) expect(result[0].id).toBe(3) expect(result[2].id).toBe(5) }) it('should return empty array when disabled', () => { const mockUseSyncExternalStore = createMockUseSyncExternalStore() const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection([ { id: 1, title: 'Task', completed: false, priority: 'high', order: 1, createdAt: 1000, updatedAt: 1000 }, ]) const result = useLiveQuery(collection, { enabled: false }) expect(result).toEqual([]) }) it('RED: should support multiple simultaneous subscriptions on same collection', () => { const mockUseSyncExternalStore = createMockUseSyncExternalStore() const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection([ { id: 1, title: 'Task 1', completed: false, priority: 'high', order: 1, createdAt: 1000, updatedAt: 1000 }, { id: 2, title: 'Task 2', completed: true, priority: 'low', order: 2, createdAt: 2000, updatedAt: 2000 }, ]) // Multiple live queries on same collection with different filters const activeTodos = useLiveQuery(collection, { where: { completed: false } }) const completedTodos = useLiveQuery(collection, { where: { completed: true } }) const allTodos = useLiveQuery(collection) expect(activeTodos).toHaveLength(1) expect(completedTodos).toHaveLength(1) expect(allTodos).toHaveLength(2) // Verify the subscribe was called for each expect(mockUseSyncExternalStore).toHaveBeenCalledTimes(3) }) it('RED: should support derived/computed live queries', () => { const mockUseSyncExternalStore = createMockUseSyncExternalStore() const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection([ { id: 1, title: 'Task 1', completed: false, priority: 'high', order: 1, createdAt: 1000, updatedAt: 1000 }, { id: 2, title: 'Task 2', completed: true, priority: 'high', order: 2, createdAt: 2000, updatedAt: 2000 }, { id: 3, title: 'Task 3', completed: false, priority: 'low', order: 3, createdAt: 3000, updatedAt: 3000 }, ]) // Live query with a select/transform that computes derived data const result = useLiveQuery(collection, { // @ts-expect-error - select transform not yet supported in live queries select: (items: Todo[]) => ({ total: items.length, completed: items.filter(t => t.completed).length, active: items.filter(t => !t.completed).length, highPriority: items.filter(t => t.priority === 'high').length, }), }) expect(result).toEqual({ total: 3, completed: 1, active: 2, highPriority: 2, }) }) it('RED: should support groupBy in live queries', () => { const mockUseSyncExternalStore = createMockUseSyncExternalStore() const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection([ { id: 1, title: 'Task 1', completed: false, priority: 'high', order: 1, createdAt: 1000, updatedAt: 1000 }, { id: 2, title: 'Task 2', completed: true, priority: 'high', order: 2, createdAt: 2000, updatedAt: 2000 }, { id: 3, title: 'Task 3', completed: false, priority: 'low', order: 3, createdAt: 3000, updatedAt: 3000 }, { id: 4, title: 'Task 4', completed: false, priority: 'medium', order: 4, createdAt: 4000, updatedAt: 4000 }, ]) const result = useLiveQuery(collection, { // @ts-expect-error - groupBy not yet supported groupBy: 'priority', }) // Should return items grouped by priority expect(result).toEqual({ high: expect.arrayContaining([ expect.objectContaining({ id: 1 }), expect.objectContaining({ id: 2 }), ]), low: expect.arrayContaining([ expect.objectContaining({ id: 3 }), ]), medium: expect.arrayContaining([ expect.objectContaining({ id: 4 }), ]), }) }) it('RED: should support distinct/unique values in live queries', () => { const mockUseSyncExternalStore = createMockUseSyncExternalStore() const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection([ { id: 1, title: 'Task 1', completed: false, priority: 'high', order: 1, createdAt: 1000, updatedAt: 1000 }, { id: 2, title: 'Task 2', completed: true, priority: 'high', order: 2, createdAt: 2000, updatedAt: 2000 }, { id: 3, title: 'Task 3', completed: false, priority: 'low', order: 3, createdAt: 3000, updatedAt: 3000 }, ]) const result = useLiveQuery(collection, { // @ts-expect-error - distinct not yet supported distinct: 'priority', }) // Should return only unique priorities expect(result).toEqual(['high', 'low']) }) }) // ============================================================================ // Section 2: Reactive Updates // ============================================================================ describe('Live Queries: Reactive Updates', () => { it('should reactively update when items are inserted', () => { let latestSnapshot: Todo[] = [] const mockUseSyncExternalStore = vi.fn().mockImplementation( (subscribe: (cb: () => void) => () => void, getSnapshot: () => Todo[]) => { const cb = () => { latestSnapshot = getSnapshot() } subscribe(cb) latestSnapshot = getSnapshot() return latestSnapshot } ) const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection([]) useLiveQuery(collection) expect(latestSnapshot).toHaveLength(0) // Insert triggers reactive update collection._simulateExternalUpdate([ { id: 1, title: 'New Task', completed: false, priority: 'high', order: 1, createdAt: Date.now(), updatedAt: Date.now() }, ]) expect(latestSnapshot).toHaveLength(1) expect(latestSnapshot[0].title).toBe('New Task') }) it('should reactively update when items are updated', () => { let latestSnapshot: Todo[] = [] const mockUseSyncExternalStore = vi.fn().mockImplementation( (subscribe: (cb: () => void) => () => void, getSnapshot: () => Todo[]) => { const cb = () => { latestSnapshot = getSnapshot() } subscribe(cb) latestSnapshot = getSnapshot() return latestSnapshot } ) const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection([ { id: 1, title: 'Original', completed: false, priority: 'low', order: 1, createdAt: 1000, updatedAt: 1000 }, ]) useLiveQuery(collection) expect(latestSnapshot[0].title).toBe('Original') // Update triggers reactive update collection._simulateExternalUpdate([ { id: 1, title: 'Modified', completed: true, priority: 'low', order: 1, createdAt: 1000, updatedAt: 2000 }, ]) expect(latestSnapshot[0].title).toBe('Modified') expect(latestSnapshot[0].completed).toBe(true) }) it('should reactively update when items are deleted', () => { let latestSnapshot: Todo[] = [] const mockUseSyncExternalStore = vi.fn().mockImplementation( (subscribe: (cb: () => void) => () => void, getSnapshot: () => Todo[]) => { const cb = () => { latestSnapshot = getSnapshot() } subscribe(cb) latestSnapshot = getSnapshot() return latestSnapshot } ) const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection([ { id: 1, title: 'Task 1', completed: false, priority: 'low', order: 1, createdAt: 1000, updatedAt: 1000 }, { id: 2, title: 'Task 2', completed: false, priority: 'high', order: 2, createdAt: 2000, updatedAt: 2000 }, ]) useLiveQuery(collection) expect(latestSnapshot).toHaveLength(2) // Delete triggers reactive update collection._simulateExternalUpdate([ { id: 2, title: 'Task 2', completed: false, priority: 'high', order: 2, createdAt: 2000, updatedAt: 2000 }, ]) expect(latestSnapshot).toHaveLength(1) expect(latestSnapshot[0].id).toBe(2) }) it('should update filtered results when matching items change', () => { let latestSnapshot: Todo[] = [] const mockUseSyncExternalStore = vi.fn().mockImplementation( (subscribe: (cb: () => void) => () => void, getSnapshot: () => Todo[]) => { const cb = () => { latestSnapshot = getSnapshot() } subscribe(cb) latestSnapshot = getSnapshot() return latestSnapshot } ) const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection([ { id: 1, title: 'Active', completed: false, priority: 'high', order: 1, createdAt: 1000, updatedAt: 1000 }, { id: 2, title: 'Done', completed: true, priority: 'low', order: 2, createdAt: 2000, updatedAt: 2000 }, ]) useLiveQuery(collection, { where: { completed: false } }) expect(latestSnapshot).toHaveLength(1) // Mark item as completed - should disappear from filtered results collection._simulateExternalUpdate([ { id: 1, title: 'Active', completed: true, priority: 'high', order: 1, createdAt: 1000, updatedAt: 3000 }, { id: 2, title: 'Done', completed: true, priority: 'low', order: 2, createdAt: 2000, updatedAt: 2000 }, ]) expect(latestSnapshot).toHaveLength(0) }) it('RED: should batch rapid updates to prevent excessive re-renders', () => { let renderCount = 0 const mockUseSyncExternalStore = vi.fn().mockImplementation( (subscribe: (cb: () => void) => () => void, getSnapshot: () => Todo[]) => { const cb = () => { renderCount++ getSnapshot() } subscribe(cb) return getSnapshot() } ) const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection([]) useLiveQuery(collection) renderCount = 0 // Reset after initial render // Simulate rapid-fire updates for (let i = 0; i < 100; i++) { collection._simulateExternalUpdate([ { id: 1, title: `Update ${i}`, completed: false, priority: 'low', order: 1, createdAt: 1000, updatedAt: Date.now() }, ]) } // With batching, should have significantly fewer renders than 100 expect(renderCount).toBeLessThan(10) }) it('RED: should support debounced live queries', () => { vi.useFakeTimers() let latestSnapshot: Todo[] = [] const mockUseSyncExternalStore = vi.fn().mockImplementation( (subscribe: (cb: () => void) => () => void, getSnapshot: () => Todo[]) => { const cb = () => { latestSnapshot = getSnapshot() } subscribe(cb) latestSnapshot = getSnapshot() return latestSnapshot } ) const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection([]) useLiveQuery(collection, { // @ts-expect-error - debounce not yet supported debounce: 200, // Only update after 200ms of inactivity }) // Rapid updates within debounce window collection._simulateExternalUpdate([ { id: 1, title: 'A', completed: false, priority: 'low', order: 1, createdAt: 1000, updatedAt: 1000 }, ]) collection._simulateExternalUpdate([ { id: 1, title: 'B', completed: false, priority: 'low', order: 1, createdAt: 1000, updatedAt: 1000 }, ]) collection._simulateExternalUpdate([ { id: 1, title: 'C', completed: false, priority: 'low', order: 1, createdAt: 1000, updatedAt: 1000 }, ]) // Before debounce, snapshot should still be initial (or first update) expect(latestSnapshot[0]?.title).not.toBe('C') // After debounce period, should have final value vi.advanceTimersByTime(250) expect(latestSnapshot[0]?.title).toBe('C') vi.useRealTimers() }) it('RED: should support throttled live queries', () => { vi.useFakeTimers() let updateCount = 0 const mockUseSyncExternalStore = vi.fn().mockImplementation( (subscribe: (cb: () => void) => () => void, getSnapshot: () => Todo[]) => { const cb = () => { updateCount++ } subscribe(cb) return getSnapshot() } ) const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection([]) useLiveQuery(collection, { // @ts-expect-error - throttle not yet supported throttle: 100, // At most one update per 100ms }) updateCount = 0 // Rapid updates for (let i = 0; i < 50; i++) { collection._simulateExternalUpdate([ { id: 1, title: `Update ${i}`, completed: false, priority: 'low', order: 1, createdAt: 1000, updatedAt: Date.now() }, ]) vi.advanceTimersByTime(10) } // With 100ms throttle over 500ms (50 * 10ms), should have ~5 updates expect(updateCount).toBeLessThanOrEqual(6) expect(updateCount).toBeGreaterThanOrEqual(4) vi.useRealTimers() }) it('RED: should support optimistic data overlay in live queries', () => { const mockUseSyncExternalStore = createMockUseSyncExternalStore() const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection([ { id: 1, title: 'Server Data', completed: false, priority: 'low', order: 1, createdAt: 1000, updatedAt: 1000 }, ]) // @ts-expect-error - optimisticData not yet supported const result = useLiveQuery(collection, { optimisticData: [ { id: 999, title: 'Optimistic Item', completed: false, priority: 'high', order: 2, createdAt: Date.now(), updatedAt: Date.now() }, ], }) // Should include both server data and optimistic data expect(result).toHaveLength(2) expect(result.find((t: Todo) => t.id === 999)).toBeDefined() }) it('RED: should support change detection with diff information', () => { let lastDiff: unknown = null const mockUseSyncExternalStore = vi.fn().mockImplementation( (subscribe: (cb: () => void) => () => void, getSnapshot: () => Todo[]) => { const cb = () => { getSnapshot() } subscribe(cb) return getSnapshot() } ) const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection([ { id: 1, title: 'Original', completed: false, priority: 'low', order: 1, createdAt: 1000, updatedAt: 1000 }, ]) useLiveQuery(collection, { // @ts-expect-error - onDiff callback not yet supported onDiff: (diff: { added: Todo[]; removed: Todo[]; updated: Array<{ previous: Todo; current: Todo }> }) => { lastDiff = diff }, }) collection._simulateExternalUpdate([ { id: 1, title: 'Updated', completed: true, priority: 'low', order: 1, createdAt: 1000, updatedAt: 2000 }, { id: 2, title: 'New', completed: false, priority: 'high', order: 2, createdAt: 2000, updatedAt: 2000 }, ]) expect(lastDiff).toEqual({ added: [expect.objectContaining({ id: 2, title: 'New' })], removed: [], updated: [expect.objectContaining({ previous: expect.objectContaining({ title: 'Original' }), current: expect.objectContaining({ title: 'Updated' }), })], }) }) }) // ============================================================================ // Section 3: Unsubscribe Behavior // ============================================================================ describe('Live Queries: Unsubscribe', () => { it('should return unsubscribe function from subscribe call', () => { const collection = createMockCollection([]) const callback = vi.fn() const unsubscribe = collection.subscribe(callback) expect(typeof unsubscribe).toBe('function') }) it('should stop receiving updates after unsubscribe', () => { const collection = createMockCollection([]) const callback = vi.fn() const unsubscribe = collection.subscribe(callback) collection._simulateExternalUpdate([ { id: 1, title: 'Before', completed: false, priority: 'low', order: 1, createdAt: 1000, updatedAt: 1000 }, ]) expect(callback).toHaveBeenCalledTimes(1) unsubscribe() collection._simulateExternalUpdate([ { id: 2, title: 'After', completed: false, priority: 'low', order: 2, createdAt: 2000, updatedAt: 2000 }, ]) expect(callback).toHaveBeenCalledTimes(1) // Should not increase }) it('should handle multiple subscribe/unsubscribe cycles', () => { const collection = createMockCollection([]) const callback = vi.fn() for (let i = 0; i < 10; i++) { const unsub = collection.subscribe(callback) unsub() } collection._simulateExternalUpdate([ { id: 1, title: 'Test', completed: false, priority: 'low', order: 1, createdAt: 1000, updatedAt: 1000 }, ]) // No active subscribers, callback should not be called expect(callback).not.toHaveBeenCalled() }) it('should properly clean up useSyncExternalStore subscription', () => { let unsubscribeCalled = false const mockUseSyncExternalStore = vi.fn().mockImplementation( (subscribe: (cb: () => void) => () => void, getSnapshot: () => Todo[]) => { const unsub = subscribe(() => {}) // Simulate component unmount calling the cleanup unsubscribeCalled = false const cleanup = () => { unsub() unsubscribeCalled = true } // In real React, this cleanup is called on unmount cleanup() return getSnapshot() } ) const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection([]) useLiveQuery(collection) expect(unsubscribeCalled).toBe(true) }) it('RED: should support subscription count tracking for leak detection', () => { const collection = createQueryCollection({ id: 'leak-detect', table: 'todos', queryFn: async () => [], }) const unsub1 = collection.subscribe(() => {}) const unsub2 = collection.subscribe(() => {}) const unsub3 = collection.subscribe(() => {}) expect(collection.getSubscriberCount()).toBe(3) unsub1() expect(collection.getSubscriberCount()).toBe(2) unsub2() unsub3() expect(collection.getSubscriberCount()).toBe(0) }) it('RED: should warn when subscription count exceeds threshold', () => { const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) const collection = createQueryCollection({ id: 'sub-threshold', table: 'todos', queryFn: async () => [], // @ts-expect-error - maxSubscribers cast in test maxSubscribers: 5, }) // Subscribe more than the threshold const unsubs: Array<() => void> = [] for (let i = 0; i < 10; i++) { unsubs.push(collection.subscribe(() => {})) } expect(consoleWarnSpy).toHaveBeenCalledWith( expect.stringContaining('Subscriber threshold exceeded') ) unsubs.forEach(u => u()) consoleWarnSpy.mockRestore() }) it('RED: should support automatic unsubscribe after inactivity timeout', () => { vi.useFakeTimers() const collection = createMockCollection([]) const callback = vi.fn() // Subscribe with auto-cleanup after inactivity // @ts-expect-error - subscribeWithTimeout not yet implemented const unsub = collection.subscribeWithTimeout(callback, { timeoutMs: 5000 }) collection._simulateExternalUpdate([ { id: 1, title: 'Initial', completed: false, priority: 'low', order: 1, createdAt: 1000, updatedAt: 1000 }, ]) expect(callback).toHaveBeenCalledTimes(1) // After timeout with no activity, subscription should auto-cleanup vi.advanceTimersByTime(6000) collection._simulateExternalUpdate([ { id: 2, title: 'After Timeout', completed: false, priority: 'low', order: 2, createdAt: 2000, updatedAt: 2000 }, ]) expect(callback).toHaveBeenCalledTimes(1) // Should not increase vi.useRealTimers() }) it('RED: should support pausable subscriptions', () => { const collection = createMockCollection([]) const callback = vi.fn() // @ts-expect-error - pausableSubscribe not yet implemented const { pause, resume, unsubscribe } = collection.pausableSubscribe(callback) collection._simulateExternalUpdate([ { id: 1, title: 'Active', completed: false, priority: 'low', order: 1, createdAt: 1000, updatedAt: 1000 }, ]) expect(callback).toHaveBeenCalledTimes(1) pause() collection._simulateExternalUpdate([ { id: 2, title: 'Paused', completed: false, priority: 'low', order: 2, createdAt: 2000, updatedAt: 2000 }, ]) expect(callback).toHaveBeenCalledTimes(1) // Should not increase while paused resume() collection._simulateExternalUpdate([ { id: 3, title: 'Resumed', completed: false, priority: 'low', order: 3, createdAt: 3000, updatedAt: 3000 }, ]) expect(callback).toHaveBeenCalledTimes(2) // Should increase after resume unsubscribe() }) }) // ============================================================================ // Section 4: Connection State // ============================================================================ describe('Live Queries: Connection State', () => { let mockFetch: ReturnType beforeEach(() => { mockFetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve([]), }) vi.stubGlobal('fetch', mockFetch) }) afterEach(() => { vi.unstubAllGlobals() }) it('should track connected state', async () => { const collection = createSyncCollection({ id: 'todos', table: 'todos', syncUrl: 'http://localhost:3000/v1/shape', }) expect(collection.getSyncState().connected).toBe(false) await collection.connect() expect(collection.getSyncState().connected).toBe(true) collection.disconnect() expect(collection.getSyncState().connected).toBe(false) }) it('should track initialized state', async () => { const collection = createSyncCollection({ id: 'todos', table: 'todos', syncUrl: 'http://localhost:3000/v1/shape', }) expect(collection.getSyncState().initialized).toBe(false) await collection.connect() expect(collection.getSyncState().initialized).toBe(true) }) it('should track last sync timestamp', async () => { const collection = createSyncCollection({ id: 'todos', table: 'todos', syncUrl: 'http://localhost:3000/v1/shape', }) await collection.connect() const syncState = collection.getSyncState() expect(syncState.lastSyncAt).toBeDefined() expect(syncState.lastSyncAt).toBeGreaterThan(0) }) it('should track pending mutation count', async () => { const collection = createSyncCollection({ id: 'todos', table: 'todos', syncUrl: 'http://localhost:3000/v1/shape', }) await collection.connect() expect(collection.getSyncState().pendingCount).toBe(0) await collection.insert({ title: 'Pending', completed: false, priority: 'low', order: 1, createdAt: Date.now(), updatedAt: Date.now() }) expect(collection.getSyncState().pendingCount).toBe(1) await collection.insert({ title: 'Pending 2', completed: false, priority: 'low', order: 2, createdAt: Date.now(), updatedAt: Date.now() }) expect(collection.getSyncState().pendingCount).toBe(2) }) it('should track connection errors', async () => { mockFetch.mockRejectedValue(new Error('Connection refused')) const collection = createSyncCollection({ id: 'todos', table: 'todos', syncUrl: 'http://localhost:3000/v1/shape', }) await expect(collection.connect()).rejects.toThrow('Connection refused') const syncState = collection.getSyncState() expect(syncState.connected).toBe(false) expect(syncState.lastError).toBeDefined() expect(syncState.lastError?.message).toBe('Connection refused') }) it('RED: should expose connection state as a live query', () => { const mockUseSyncExternalStore = createMockUseSyncExternalStore() const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createSyncCollection({ id: 'todos', table: 'todos', syncUrl: 'http://localhost:3000/v1/shape', }) // @ts-expect-error - connection state live query not yet supported const connectionState = useLiveQuery.connectionState(collection) expect(connectionState).toEqual(expect.objectContaining({ connected: false, initialized: false, pendingCount: 0, })) }) it('RED: should support connection state change callbacks', async () => { const onConnectionChange = vi.fn() const collection = createSyncCollection({ id: 'todos', table: 'todos', syncUrl: 'http://localhost:3000/v1/shape', // @ts-expect-error - onConnectionChange not yet supported onConnectionChange, }) await collection.connect() expect(onConnectionChange).toHaveBeenCalledWith( expect.objectContaining({ connected: true, previousState: expect.objectContaining({ connected: false }), }) ) collection.disconnect() expect(onConnectionChange).toHaveBeenCalledWith( expect.objectContaining({ connected: false, previousState: expect.objectContaining({ connected: true }), }) ) }) it('RED: should support auto-reconnect with exponential backoff', async () => { vi.useFakeTimers() let connectAttempts = 0 mockFetch.mockImplementation(async () => { connectAttempts++ if (connectAttempts < 4) { throw new Error('Connection failed') } return { ok: true, json: () => Promise.resolve([]) } }) const collection = createSyncCollection({ id: 'todos', table: 'todos', syncUrl: 'http://localhost:3000/v1/shape', // @ts-expect-error - autoReconnect not yet supported autoReconnect: { enabled: true, maxRetries: 5, initialDelay: 1000, maxDelay: 30000, backoffMultiplier: 2, }, }) try { await collection.connect() } catch { // First attempt fails } // After backoff delays, should retry and eventually succeed await vi.advanceTimersByTimeAsync(1000) // 1st retry (1s) await vi.advanceTimersByTimeAsync(2000) // 2nd retry (2s) await vi.advanceTimersByTimeAsync(4000) // 3rd retry (4s) - should succeed expect(collection.getSyncState().connected).toBe(true) expect(connectAttempts).toBe(4) vi.useRealTimers() collection.disconnect() }) it('RED: should support connection health monitoring', async () => { const collection = createSyncCollection({ id: 'todos', table: 'todos', syncUrl: 'http://localhost:3000/v1/shape', // @ts-expect-error - healthCheck not yet supported healthCheck: { interval: 5000, timeout: 2000, }, }) await collection.connect() // @ts-expect-error - getHealthStatus not yet implemented const health = collection.getHealthStatus() expect(health).toEqual(expect.objectContaining({ status: 'healthy', lastCheckAt: expect.any(Number), latencyMs: expect.any(Number), consecutiveFailures: 0, })) collection.disconnect() }) it('RED: should emit connection state events for UI indicators', async () => { const stateEvents: Array<{ event: string; state: SyncState }> = [] const collection = createSyncCollection({ id: 'todos', table: 'todos', syncUrl: 'http://localhost:3000/v1/shape', }) // @ts-expect-error - onSyncStateChange not yet implemented collection.onSyncStateChange((event: string, state: SyncState) => { stateEvents.push({ event, state }) }) await collection.connect() collection.disconnect() expect(stateEvents).toEqual([ expect.objectContaining({ event: 'connecting' }), expect.objectContaining({ event: 'connected' }), expect.objectContaining({ event: 'disconnecting' }), expect.objectContaining({ event: 'disconnected' }), ]) }) it('RED: should support offline detection and queue mutations', async () => { const collection = createSyncCollection({ id: 'todos', table: 'todos', syncUrl: 'http://localhost:3000/v1/shape', // @ts-expect-error - offlineSupport not yet supported offlineSupport: true, }) await collection.connect() // Simulate going offline mockFetch.mockRejectedValue(new Error('Network error')) // Mutations while offline should be queued await collection.insert({ title: 'Offline 1', completed: false, priority: 'low', order: 1, createdAt: Date.now(), updatedAt: Date.now() }) await collection.insert({ title: 'Offline 2', completed: false, priority: 'low', order: 2, createdAt: Date.now(), updatedAt: Date.now() }) // Items should still be in local collection expect(collection.getAll()).toHaveLength(2) // Pending count should reflect queued mutations expect(collection.getSyncState().pendingCount).toBe(2) // @ts-expect-error - isOffline not yet implemented expect(collection.isOffline()).toBe(true) collection.disconnect() }) it('RED: should flush offline queue when connection is restored', async () => { vi.useFakeTimers() const collection = createSyncCollection({ id: 'todos', table: 'todos', syncUrl: 'http://localhost:3000/v1/shape', pollInterval: 5000, // @ts-expect-error - offlineSupport not yet supported offlineSupport: true, }) await collection.connect() // Add mutations while connected await collection.insert({ title: 'Before Offline', completed: false, priority: 'low', order: 1, createdAt: Date.now(), updatedAt: Date.now() }) // Go offline mockFetch.mockRejectedValue(new Error('Network error')) // Add more mutations while offline await collection.insert({ title: 'During Offline', completed: false, priority: 'low', order: 2, createdAt: Date.now(), updatedAt: Date.now() }) // Restore connection mockFetch.mockResolvedValue({ ok: true, json: () => Promise.resolve([ { id: 'confirmed-1', title: 'Before Offline', completed: false, priority: 'low', order: 1, createdAt: 1000, updatedAt: 1000 }, { id: 'confirmed-2', title: 'During Offline', completed: false, priority: 'low', order: 2, createdAt: 2000, updatedAt: 2000 }, ]), }) // Trigger a sync poll await vi.advanceTimersByTimeAsync(5000) // After successful sync, pending count should be 0 expect(collection.getSyncState().pendingCount).toBe(0) vi.useRealTimers() collection.disconnect() }) }) // ============================================================================ // Section 5: Multi-Collection Live Queries // ============================================================================ describe('Live Queries: Multi-Collection', () => { let mockFetch: ReturnType beforeEach(() => { mockFetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve([]), }) vi.stubGlobal('fetch', mockFetch) }) afterEach(() => { vi.unstubAllGlobals() }) it('RED: should support joining data across collections', () => { const mockUseSyncExternalStore = createMockUseSyncExternalStore() const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const todosCollection = createMockCollection([ { id: 1, title: 'Task by Alice', completed: false, priority: 'high', order: 1, createdAt: 1000, updatedAt: 1000 }, ]) const messagesCollection = createMockCollection([ { id: 'msg-1', channel: 'general', content: 'Hello', author: 'Alice', timestamp: 1000 }, ]) // @ts-expect-error - join across collections not yet supported const result = useLiveQuery.join( todosCollection, messagesCollection, { on: (todo: Todo, message: Message) => todo.title.includes(message.author), select: (todo: Todo, message: Message) => ({ todoTitle: todo.title, lastMessage: message.content, }), } ) expect(result).toHaveLength(1) expect(result[0]).toEqual({ todoTitle: 'Task by Alice', lastMessage: 'Hello', }) }) it('RED: should support cross-collection reactive updates', () => { let latestResult: unknown[] = [] const mockUseSyncExternalStore = vi.fn().mockImplementation( (subscribe: (cb: () => void) => () => void, getSnapshot: () => unknown[]) => { const cb = () => { latestResult = getSnapshot() } subscribe(cb) latestResult = getSnapshot() return latestResult } ) const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const store = createTanStackStore() const todosCollection = store.registerCollection({ id: 'todos', table: 'todos', queryFn: async () => [] as Todo[], }) const messagesCollection = store.registerCollection({ id: 'messages', table: 'messages', queryFn: async () => [] as Message[], }) // @ts-expect-error - multi-collection live query not yet supported useLiveQuery.multiCollection(store, ['todos', 'messages'], { combine: (todos: Todo[], messages: Message[]) => ({ todoCount: todos.length, messageCount: messages.length, }), }) // When either collection changes, the combined result should update // This tests that cross-collection subscriptions work reactively expect(latestResult).toEqual({ todoCount: 0, messageCount: 0, }) }) it('RED: should support aggregate live queries across collections', () => { const mockUseSyncExternalStore = createMockUseSyncExternalStore() const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const store = createTanStackStore() store.registerCollection({ id: 'todos-high', table: 'todos', queryFn: async () => [ { id: 1, title: 'High 1', completed: false, priority: 'high' as const, order: 1, createdAt: 1000, updatedAt: 1000 }, ] as Todo[], }) store.registerCollection({ id: 'todos-low', table: 'todos', queryFn: async () => [ { id: 2, title: 'Low 1', completed: true, priority: 'low' as const, order: 2, createdAt: 2000, updatedAt: 2000 }, { id: 3, title: 'Low 2', completed: false, priority: 'low' as const, order: 3, createdAt: 3000, updatedAt: 3000 }, ] as Todo[], }) // @ts-expect-error - aggregate live query not yet supported const result = useLiveQuery.aggregate(store, { collections: ['todos-high', 'todos-low'], aggregation: { totalCount: (items: Todo[]) => items.length, completedCount: (items: Todo[]) => items.filter(t => t.completed).length, highPriorityCount: (items: Todo[]) => items.filter(t => t.priority === 'high').length, }, }) expect(result).toEqual({ totalCount: 3, completedCount: 1, highPriorityCount: 1, }) }) }) // ============================================================================ // Section 6: Performance and Edge Cases // ============================================================================ describe('Live Queries: Performance and Edge Cases', () => { it('should handle empty collection gracefully', () => { const mockUseSyncExternalStore = createMockUseSyncExternalStore() const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection([]) const result = useLiveQuery(collection) expect(result).toEqual([]) }) it('should handle large collections without stack overflow', () => { const mockUseSyncExternalStore = createMockUseSyncExternalStore() const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const items: Todo[] = Array.from({ length: 10000 }, (_, i) => ({ id: i + 1, title: `Task ${i + 1}`, completed: i % 2 === 0, priority: (['low', 'medium', 'high'] as const)[i % 3], order: i + 1, createdAt: i * 1000, updatedAt: i * 1000, })) const collection = createMockCollection(items) const result = useLiveQuery(collection, { where: { completed: false }, orderBy: { field: 'order', direction: 'asc' }, limit: 100, }) expect(result).toHaveLength(100) expect(result.every(t => !t.completed)).toBe(true) }) it('should handle undefined/null field values in where clause', () => { const mockUseSyncExternalStore = createMockUseSyncExternalStore() const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection([ { id: 1, title: 'Task 1', completed: false, priority: 'high', order: 1, createdAt: 1000, updatedAt: 1000 }, ]) // Should not throw on undefined values in where expect(() => { useLiveQuery(collection, { where: { completed: undefined as unknown as boolean }, }) }).not.toThrow() }) it('should maintain referential stability for unchanged results', () => { let previousResult: Todo[] | null = null const mockUseSyncExternalStore = vi.fn().mockImplementation( (_subscribe: (cb: () => void) => () => void, getSnapshot: () => Todo[]) => { const result = getSnapshot() if (previousResult === null) { previousResult = result } return result } ) const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const items: Todo[] = [ { id: 1, title: 'Task 1', completed: false, priority: 'high', order: 1, createdAt: 1000, updatedAt: 1000 }, ] const collection = createMockCollection(items) const result1 = useLiveQuery(collection) // Call again without changes - should return same reference mockUseSyncExternalStore.mockClear() mockUseSyncExternalStore.mockImplementation( (_subscribe: (cb: () => void) => () => void, getSnapshot: () => Todo[]) => getSnapshot() ) const result2 = useLiveQuery(collection) // Results should be deeply equal expect(result1).toEqual(result2) }) it('RED: should memoize snapshot to prevent unnecessary re-renders', () => { let snapshotCallCount = 0 const mockUseSyncExternalStore = vi.fn().mockImplementation( (_subscribe: (cb: () => void) => () => void, getSnapshot: () => Todo[]) => { snapshotCallCount++ return getSnapshot() } ) const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection([ { id: 1, title: 'Task', completed: false, priority: 'high', order: 1, createdAt: 1000, updatedAt: 1000 }, ]) // Multiple calls with same data should use memoized result useLiveQuery(collection, { where: { completed: false } }) useLiveQuery(collection, { where: { completed: false } }) useLiveQuery(collection, { where: { completed: false } }) // getSnapshot should return the same reference if data hasn't changed // This ensures React doesn't re-render unnecessarily expect(snapshotCallCount).toBeGreaterThan(0) // Called at least once // The key test: consecutive snapshots without data changes should be referentially equal const snapshot1 = mockUseSyncExternalStore.mock.calls[0]?.[1]?.() const snapshot2 = mockUseSyncExternalStore.mock.calls[1]?.[1]?.() // With memoization, these should be the exact same array reference expect(snapshot1).toBe(snapshot2) // Referential equality }) it('RED: should support error boundaries in live queries', () => { const mockUseSyncExternalStore = createMockUseSyncExternalStore() const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) // Collection that throws during getAll const brokenCollection: Collection = { id: 'broken', getAll: () => { throw new Error('Data corruption detected') }, get: () => undefined, insert: vi.fn(), update: vi.fn(), delete: vi.fn(), subscribe: () => () => {}, getSyncState: () => ({ connected: false, initialized: false, pendingCount: 0 }), } // Live query should handle errors gracefully // @ts-expect-error - error handling not yet implemented in getSnapshot const result = useLiveQuery(brokenCollection, { onError: (error: Error) => { expect(error.message).toBe('Data corruption detected') }, }) // Should return empty array on error, not throw expect(result).toEqual([]) }) it('RED: should support cursor-based pagination in live queries', () => { const mockUseSyncExternalStore = createMockUseSyncExternalStore() const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const items: Todo[] = Array.from({ length: 50 }, (_, i) => ({ id: i + 1, title: `Task ${i + 1}`, completed: false, priority: 'low' as const, order: i + 1, createdAt: (i + 1) * 1000, updatedAt: (i + 1) * 1000, })) const collection = createMockCollection(items) // First page const page1 = useLiveQuery(collection, { // @ts-expect-error - cursor pagination not yet supported cursor: { field: 'id', after: undefined, pageSize: 10 }, }) expect(page1).toHaveLength(10) expect(page1[0].id).toBe(1) // Second page const page2 = useLiveQuery(collection, { // @ts-expect-error - cursor pagination not yet supported cursor: { field: 'id', after: 10, pageSize: 10 }, }) expect(page2).toHaveLength(10) expect(page2[0].id).toBe(11) }) it('RED: should support full-text search in live queries', () => { const mockUseSyncExternalStore = createMockUseSyncExternalStore() const useLiveQuery = createUseLiveQuery(mockUseSyncExternalStore) const collection = createMockCollection([ { id: 1, title: 'Buy groceries at the store', completed: false, priority: 'high', order: 1, createdAt: 1000, updatedAt: 1000 }, { id: 2, title: 'Walk the dog', completed: false, priority: 'medium', order: 2, createdAt: 2000, updatedAt: 2000 }, { id: 3, title: 'Go to grocery shopping', completed: false, priority: 'low', order: 3, createdAt: 3000, updatedAt: 3000 }, ]) const result = useLiveQuery(collection, { // @ts-expect-error - search not yet supported search: { query: 'grocer', fields: ['title'], fuzzy: true, }, }) expect(result).toHaveLength(2) // "groceries" and "grocery" expect(result.map((t: Todo) => t.id).sort()).toEqual([1, 3]) }) })