⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content

Conversation

@KyleAMathews
Copy link
Contributor

@KyleAMathews KyleAMathews commented Jan 15, 2026

Summary

Fixes cache buster generation for CDN stale response retries - each retry now gets a unique cache buster value instead of potentially reusing the same one, ensuring reliable cache bypass across multiple retry attempts.

Reviewer Guidance

Root Cause

The bug occurred because the cache buster was generated in #onInitialResponse before throwing StaleCacheError, but the retry loop in the error handler would call #requestShape() without generating a new value. If multiple retries occurred in quick succession (within the same millisecond, or with same random seed state), they could get identical cache busters - defeating the purpose of the retry mechanism.

Approach

Moved cache buster generation from the error detection site (#onInitialResponse) to the retry site (the StaleCacheError catch block):

if (e instanceof StaleCacheError) {
  // Generate a NEW random cache buster for each retry
  this.#staleCacheBuster = `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
  return this.#requestShape()
}

This ensures a fresh timestamp and random suffix for every retry attempt.

Key Invariants

  • Each retry request MUST have a unique cache buster value
  • Cache buster must be generated immediately before the retry request, not before
  • The retry count and max retries behavior remains unchanged

Non-goals

  • Not changing the cache buster format or algorithm
  • Not adding additional retry strategies or backoff
  • Not modifying other error handling paths

Trade-offs

Alternative considered: Pass the cache buster as a parameter through the error. Rejected because it would complicate the error interface and the current approach is simpler - generate right before use.

Verification

pnpm --filter @electric-sql/client test

The test should retry with cache buster when receiving stale cache response with expired handle now verifies that all cache busters across retry attempts are unique.

Files changed

  • packages/typescript-client/src/client.ts - Moved cache buster generation from #onInitialResponse to the StaleCacheError catch block
  • packages/typescript-client/test/expired-shapes-cache.test.ts - Added assertion that all cache busters are unique
  • .changeset/fix-cache-buster-retry.md - Changeset for the patch release

Fixes #3723

Summary by CodeRabbit

  • Bug Fixes
    • Fixed CDN retry behavior to generate a unique cache buster value for each retry attempt, preventing stale cache issues from reoccurring on subsequent retries.

✏️ Tip: You can customize this high-level summary in your review settings.

When the client detects a stale cached response from a CDN with an
expired shape handle, it retries with a cache buster parameter to
bypass the cache. Previously, the cache buster was generated in
#onInitialResponse before throwing the error, which could result in
the same value being used across retries.

Now the cache buster is generated right before each retry request
in the StaleCacheError handler, ensuring a unique value for every
retry attempt. This guarantees each retry has a different cache
buster to bypass different cache layers.

Also updated the test to verify that all cache busters are unique
across retry attempts.

Fixes #3723
@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

📝 Walkthrough

Walkthrough

This change ensures the cache-buster value is generated immediately before each CDN retry instead of being reused, by throwing on stale initial responses and generating a fresh timestamp-and-random suffix at retry time (fix for issue #3723).

Changes

Cohort / File(s) Summary
Changelog
\.changeset/fix-cache-buster-retry.md
New changelog entry describing the fix for cache buster generation on retry, referencing issue #3723.
Client logic
packages/typescript-client/src/client.ts
Altered stale-response flow: initial stale detection now throws StaleCacheError instead of setting a stale buster; retry handler generates a new timestamp+random cache buster immediately before each retry.
Tests
packages/typescript-client/test/expired-shapes-cache.test.ts
Test updated to collect cache buster values from retry URLs, assert non-nullity and assert uniqueness across retries using a Set-based check.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant RetryHandler
  participant CDN
  participant Cache

  Client->>CDN: Request shape (no or stale buster)
  CDN->>Client: Stale handle / stale response
  Client->>RetryHandler: throw StaleCacheError
  RetryHandler->>RetryHandler: generate new cache-buster (timestamp + random)
  RetryHandler->>CDN: Retry request with fresh cache-buster
  CDN->>Client: Fresh response
  Client->>Cache: update cache with fresh shape
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • thruflo
  • kevin-dp

Poem

🐰 A buster once born, then reused each try,
Retries echoed back with the same old cry.
Now I hop in fresh, timestamp and spin,
Each retry a new dance, a chance to win! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Change cache buster on every request' clearly and concisely describes the main change in the PR: modifying the cache buster generation to occur before each retry rather than once upfront.
Linked Issues check ✅ Passed The PR implementation directly addresses issue #3723 by moving cache buster generation from the initial response handler to the retry error handler, ensuring a new value is generated for each retry attempt.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the cache buster retry issue: the client logic change, test updates, and changelog entry are all focused on the stated objective.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings


📜 Recent review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0b8e508 and c3441dd.

📒 Files selected for processing (1)
  • packages/typescript-client/test/expired-shapes-cache.test.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Mock fetch client in tests via shapeOptions.fetchClient parameter

Files:

  • packages/typescript-client/test/expired-shapes-cache.test.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Use @tanstack/{angular,react,solid,svelte,vue}-db framework-specific imports instead of generic TanStack DB

Files:

  • packages/typescript-client/test/expired-shapes-cache.test.ts
🧠 Learnings (2)
📚 Learning: 2026-01-14T14:45:05.838Z
Learnt from: CR
Repo: electric-sql/electric PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-14T14:45:05.838Z
Learning: Applies to **/*.{test,spec}.{ts,tsx} : Mock fetch client in tests via `shapeOptions.fetchClient` parameter

Applied to files:

  • packages/typescript-client/test/expired-shapes-cache.test.ts
📚 Learning: 2026-01-14T14:45:05.838Z
Learnt from: CR
Repo: electric-sql/electric PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-14T14:45:05.838Z
Learning: Applies to **/collections/**/*.{ts,tsx} : Use collection factory functions to create dynamic shapes (shapes are immutable per subscription)

Applied to files:

  • packages/typescript-client/test/expired-shapes-cache.test.ts
🧬 Code graph analysis (1)
packages/typescript-client/test/expired-shapes-cache.test.ts (2)
packages/typescript-client/src/client.ts (1)
  • url (847-1022)
packages/typescript-client/src/constants.ts (1)
  • CACHE_BUSTER_QUERY_PARAM (31-31)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Test packages/react-hooks w/ sync-service
  • GitHub Check: Test packages/experimental w/ sync-service
  • GitHub Check: Test packages/typescript-client w/ sync-service
  • GitHub Check: Run Lux integration tests
🔇 Additional comments (1)
packages/typescript-client/test/expired-shapes-cache.test.ts (1)

517-526: LGTM! Test correctly validates unique cache busters per retry.

The extraction logic properly skips the initial request (which shouldn't have a cache buster) and the Set-based uniqueness check is an appropriate way to verify the fix for issue #3723.

Minor style note: placing the expect(cacheBuster).not.toBeNull() assertion inside the map callback is functional but unconventional—test assertions within transformation functions can make failures harder to trace. Consider extracting the assertion to a separate loop if this pattern is used elsewhere. Not blocking.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 15, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@electric-sql/react@3724
npm i https://pkg.pr.new/@electric-sql/client@3724
npm i https://pkg.pr.new/@electric-sql/y-electric@3724

commit: c3441dd

@codecov
Copy link

codecov bot commented Jan 15, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.36%. Comparing base (5a767e6) to head (c3441dd).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3724      +/-   ##
==========================================
+ Coverage   84.01%   87.36%   +3.35%     
==========================================
  Files          44       23      -21     
  Lines        2834     2011     -823     
  Branches      534      533       -1     
==========================================
- Hits         2381     1757     -624     
+ Misses        451      252     -199     
  Partials        2        2              
Flag Coverage Δ
elixir ?
elixir-client ?
packages/experimental 87.73% <ø> (ø)
packages/react-hooks 86.48% <ø> (ø)
packages/start 82.83% <ø> (ø)
packages/typescript-client 93.47% <100.00%> (ø)
packages/y-electric 56.05% <ø> (ø)
typescript 87.36% <100.00%> (ø)
unit-tests 87.36% <100.00%> (+3.35%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Replace imperative for loop with declarative slice/map pattern and
remove redundant searchParams.has() check since get() + toBeNull
assertion already validates existence.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cache buster failing for expired shape handles

3 participants