|
1 | 1 | /** |
2 | | - * Progressive Mode Test Suite (Electric only) |
| 2 | + * Progressive Mode Test Suite |
3 | 3 | * |
4 | 4 | * Tests progressive sync mode behavior including: |
5 | 5 | * - Snapshot loading during initial sync |
|
11 | 11 | import { describe, expect, it } from "vitest" |
12 | 12 | import { createLiveQueryCollection, eq, gt } from "@tanstack/db" |
13 | 13 | import { waitFor, waitForQueryData } from "../utils/helpers" |
14 | | -import type { E2ETestConfig } from "../types" |
| 14 | +import type { E2ETestConfig, User } from "../types" |
| 15 | +import type { Collection } from "@tanstack/db" |
| 16 | +import type { ElectricCollectionUtils } from "@tanstack/electric-db-collection" |
15 | 17 |
|
16 | 18 | export function createProgressiveTestSuite( |
17 | 19 | getConfig: () => Promise<E2ETestConfig> |
18 | 20 | ) { |
19 | | - describe(`Progressive Mode Suite (Electric only)`, () => { |
| 21 | + describe(`Progressive Mode Suite`, () => { |
20 | 22 | describe(`Basic Progressive Mode`, () => { |
21 | 23 | it(`should explicitly validate snapshot phase and atomic swap transition`, async () => { |
22 | 24 | const config = await getConfig() |
@@ -406,6 +408,145 @@ export function createProgressiveTestSuite( |
406 | 408 | }) |
407 | 409 | }) |
408 | 410 |
|
| 411 | + describe(`Txid Tracking Behavior (Electric only)`, () => { |
| 412 | + it(`should not track txids during snapshot phase but track them after atomic swap`, async () => { |
| 413 | + const config = await getConfig() |
| 414 | + if ( |
| 415 | + !config.collections.progressive || |
| 416 | + !config.mutations?.insertUser || |
| 417 | + !config.getTxid |
| 418 | + ) { |
| 419 | + return // Skip if progressive collections, mutations, or getTxid not available |
| 420 | + } |
| 421 | + const progressiveUsers = config.collections.progressive |
| 422 | + .users as Collection<User, string, ElectricCollectionUtils> |
| 423 | + |
| 424 | + // awaitTxId is guaranteed to exist on ElectricCollectionUtils |
| 425 | + // This test is Electric-only via the describe block name |
| 426 | + |
| 427 | + // Start sync but don't release yet (stay in snapshot phase) |
| 428 | + progressiveUsers.startSyncImmediate() |
| 429 | + await new Promise((resolve) => setTimeout(resolve, 100)) |
| 430 | + |
| 431 | + // Should be in loading state (snapshot phase) |
| 432 | + if (progressiveUsers.status !== `loading`) { |
| 433 | + console.log( |
| 434 | + `Collection already ready, cannot test snapshot phase txid behavior` |
| 435 | + ) |
| 436 | + return |
| 437 | + } |
| 438 | + |
| 439 | + // === PHASE 1: INSERT DURING SNAPSHOT PHASE === |
| 440 | + const snapshotPhaseUser = { |
| 441 | + id: crypto.randomUUID(), |
| 442 | + name: `Snapshot Phase User`, |
| 443 | + |
| 444 | + age: 28, |
| 445 | + isActive: true, |
| 446 | + createdAt: new Date(), |
| 447 | + metadata: null, |
| 448 | + deletedAt: null, |
| 449 | + } |
| 450 | + |
| 451 | + // Insert user and track when awaitTxId completes |
| 452 | + let txidResolved = false |
| 453 | + |
| 454 | + // Start the insert |
| 455 | + await config.mutations.insertUser(snapshotPhaseUser) |
| 456 | + |
| 457 | + // Get the txid from postgres |
| 458 | + const txid = await config.getTxid() |
| 459 | + |
| 460 | + if (!txid) { |
| 461 | + console.log(`Could not get txid, skipping txid tracking validation`) |
| 462 | + config.progressiveTestControl?.releaseInitialSync() |
| 463 | + return |
| 464 | + } |
| 465 | + |
| 466 | + // Start awaiting the txid (should NOT resolve during snapshot phase) |
| 467 | + progressiveUsers.utils.awaitTxId(txid, 60000).then(() => { |
| 468 | + txidResolved = true |
| 469 | + }) |
| 470 | + |
| 471 | + // Wait a moment for sync to process |
| 472 | + await new Promise((resolve) => setTimeout(resolve, 500)) |
| 473 | + |
| 474 | + // Txid should NOT have resolved yet (snapshot phase, txids not tracked) |
| 475 | + expect(txidResolved).toBe(false) |
| 476 | + |
| 477 | + // Query for the user (triggers fetchSnapshot with this user) |
| 478 | + const query = createLiveQueryCollection((q) => |
| 479 | + q |
| 480 | + .from({ user: progressiveUsers }) |
| 481 | + .where(({ user }) => eq(user.id, snapshotPhaseUser.id)) |
| 482 | + ) |
| 483 | + |
| 484 | + await query.preload() |
| 485 | + await waitForQueryData(query, { minSize: 1, timeout: 10000 }) |
| 486 | + |
| 487 | + // User should be in snapshot data |
| 488 | + expect(query.size).toBe(1) |
| 489 | + expect(query.get(snapshotPhaseUser.id)).toBeDefined() |
| 490 | + |
| 491 | + // But collection is still in snapshot phase |
| 492 | + expect(progressiveUsers.status).toBe(`loading`) |
| 493 | + |
| 494 | + // Txid should STILL not have resolved (snapshot doesn't track txids) |
| 495 | + expect(txidResolved).toBe(false) |
| 496 | + |
| 497 | + // === PHASE 2: TRIGGER ATOMIC SWAP === |
| 498 | + if (config.progressiveTestControl) { |
| 499 | + config.progressiveTestControl.releaseInitialSync() |
| 500 | + } |
| 501 | + |
| 502 | + // Wait for atomic swap to complete |
| 503 | + await waitFor(() => progressiveUsers.status === `ready`, { |
| 504 | + timeout: 30000, |
| 505 | + message: `Progressive collection did not complete sync`, |
| 506 | + }) |
| 507 | + |
| 508 | + // NOW txid should resolve (buffered messages include txids) |
| 509 | + await waitFor(() => txidResolved, { |
| 510 | + timeout: 5000, |
| 511 | + message: `Txid did not resolve after atomic swap`, |
| 512 | + }) |
| 513 | + |
| 514 | + expect(txidResolved).toBe(true) |
| 515 | + |
| 516 | + // === PHASE 3: VERIFY TXID TRACKING POST-SWAP === |
| 517 | + // User should still be present after atomic swap |
| 518 | + expect(progressiveUsers.get(snapshotPhaseUser.id)).toBeDefined() |
| 519 | + |
| 520 | + // Now insert another user and verify txid tracking works |
| 521 | + const postSwapUser = { |
| 522 | + id: crypto.randomUUID(), |
| 523 | + name: `Post Swap User`, |
| 524 | + |
| 525 | + age: 29, |
| 526 | + isActive: true, |
| 527 | + createdAt: new Date(), |
| 528 | + metadata: null, |
| 529 | + deletedAt: null, |
| 530 | + } |
| 531 | + |
| 532 | + await config.mutations.insertUser(postSwapUser) |
| 533 | + |
| 534 | + // Wait for incremental update (txid tracking should work now) |
| 535 | + if (config.hasReplicationLag) { |
| 536 | + await waitFor(() => progressiveUsers.has(postSwapUser.id), { |
| 537 | + timeout: 10000, |
| 538 | + message: `Post-swap user not synced via incremental update`, |
| 539 | + }) |
| 540 | + } |
| 541 | + |
| 542 | + // Both users should be present |
| 543 | + expect(progressiveUsers.get(snapshotPhaseUser.id)).toBeDefined() |
| 544 | + expect(progressiveUsers.get(postSwapUser.id)).toBeDefined() |
| 545 | + |
| 546 | + await query.cleanup() |
| 547 | + }) |
| 548 | + }) |
| 549 | + |
409 | 550 | describe(`Progressive Mode Resilience`, () => { |
410 | 551 | it(`should handle cleanup and restart during snapshot phase`, async () => { |
411 | 552 | const config = await getConfig() |
|
0 commit comments