Skip to content

feat(flags-core): add header-driven Vercel mode on the shared cache - #511

Merged
luismeyer merged 47 commits into
codex/flags-cache-foundationfrom
codex/flags-header-source
Oct 9, 2026
Merged

luismeyer merged 47 commits into
codex/flags-cache-foundationfrom
codex/flags-header-source

Conversation

@luismeyer

@luismeyer luismeyer commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

Stacked on #510.

Adds header-driven reads on Vercel, enabled by default when VERCEL=1. Initialization loads provided or bundled definitions. Evaluations and getDatafile() share project-specific header checks, stale-while-revalidate, blocking refresh, stale-if-error, and source fallback. Provider discovery and experimental_reportOverride continue using the public getDatafile() method.

Header mode requires a valid positive version for the client's own project and a valid cached config version. Missing, malformed, or unrelated entries switch that client to streaming when enabled, otherwise polling. Clients select sources independently within a request. A cold read first discovers project identity through the shared cache fetch; a failed cold fetch rejects without switching sources and can be retried. Subsequent source assessments return an object containing data, cache status, and a source-error indicator. Assessment errors trigger fallback without renewing the fetch-failure deadline, including when stale-if-error has expired.

The cache owns freshness, serving decisions, deduplicated refreshes, cancellation, and failure deadlines. Accepted stream updates and valid confirmations cancel superseded HTTP work; waiting reads use confirmed data, and late responses cannot change cache or authorization state.

  • Every transition to polling uses initializePolling(). Reads share the immediate poll and wait for its completion or the configured initialization timeout, even with cached data. Timeout fallback respects stale-if-error while polling continues. Detached disconnect startup handles rejection, while waiting reads still receive errors.
  • Ping timeouts reconnect quietly, including after runtime suspension. Genuine disconnects start exactly one immediate shared poll. Stream recovery cancels superseded HTTP work, stops polling, and releases waiting reads.
  • Streaming becomes stale after 60 seconds and expires after 90 seconds. Polling becomes stale after its interval plus the ten-second fetch deadline and expires after two intervals plus that deadline (40/70 seconds by default).
  • Accepted updates, valid confirmations, and stream pings reset cache age and clear failure without rewriting persisted fetchedAt. Initialization timeouts and repeated errors do not renew failure allowances.
  • staleWhileRevalidate defaults to ten seconds and staleIfError to Infinity. Public durations accept fractional seconds; zero disables the corresponding stale allowance. Build/offline loading retains its existing policy.

Validation: all 1,012 core unit tests pass on Node 22 and Node 24. TypeScript, changed-file Biome, and diff checks pass. Coverage includes multi-project clients, unified reads and refresh sharing, failed cold-fetch retries, polling completion and timeout fallback, quiet ping recovery, stream reconnection, and shutdown with and without waiting reads. Live-service integration tests were excluded.

@vercel

vercel Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
flags-playground Ready Ready Preview, v0 Oct 9, 2026 1:08pm UTC
flags-sdk-dev Ready Ready Preview, v0 Oct 9, 2026 1:08pm UTC
flags-sdk-flagsmith Ready Ready Preview, v0 Oct 9, 2026 1:08pm UTC
flags-sdk-growthbook Ready Ready Preview, v0 Oct 9, 2026 1:08pm UTC
flags-sdk-launchdarkly Ready Ready Preview, v0 Oct 9, 2026 1:08pm UTC
flags-sdk-openfeature Ready Ready Preview, v0 Oct 9, 2026 1:08pm UTC
flags-sdk-posthog Ready Ready Preview, v0 Oct 9, 2026 1:08pm UTC
flags-sdk-reflag Ready Ready Preview, v0 Oct 9, 2026 1:08pm UTC
flags-sdk-snippets Ready Ready Preview, v0 Oct 9, 2026 1:08pm UTC
flags-sdk-statsig Ready Ready Preview, v0 Oct 9, 2026 1:08pm UTC
flags-sdk-sveltekit-snippets Ready Ready Preview, v0 Oct 9, 2026 1:08pm UTC
shirt-shop Ready Ready Preview, v0 Oct 9, 2026 1:08pm UTC
shirt-shop-api Ready Ready Preview, v0 Oct 9, 2026 1:08pm UTC

Comment thread packages/vercel-flags-core/src/controller/datafile-cache.ts Outdated
Comment thread packages/vercel-flags-core/src/controller/header-source.ts Outdated
Comment thread packages/vercel-flags-core/src/controller/index.ts Outdated
Comment thread packages/vercel-flags-core/src/controller/datafile-cache.ts Outdated
Comment thread packages/vercel-flags-core/src/controller/index.ts Outdated
Comment thread packages/vercel-flags-core/src/controller/header-source.ts Outdated
Comment thread packages/vercel-flags-core/src/controller/datafile-cache.ts Outdated
Comment thread packages/vercel-flags-core/src/controller/index.ts Outdated
luismeyer and others added 29 commits October 9, 2026 15:07
…ce response as recovery

Polling now starts only once the stream has given up for good (retries
exhausted, 401, or token failure); transient disconnects and startup
timeouts keep serving the cache while the stream reconnects. Stream and
polling freshness extend their fresh window by staleWhileRevalidate
before reads block. getDatafile() is a snapshot again and never starts a
source. Any successful source response renews cache age and clears the
failure deadline even when the version guard keeps the stored data, a
last-resort fetch clears failure too, and clear() resets the deadline so
reinitialization after shutdown starts clean and rewires source events.
A 401 on an empty cache no longer disables the client permanently.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WjWjQyvVvunuerU51GvB9M
…out polling

With a recorded failure the cache otherwise waits for the live source to
recover, but a stream-only client has no source left once the stream gives
up. Reads in that state now revalidate over HTTP, including after a failed
refresh.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WjWjQyvVvunuerU51GvB9M
…t getDatafile

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WjWjQyvVvunuerU51GvB9M
…s while degraded

The controller no longer tracks a separate streamExhausted flag: stream startup reports connected/connecting/failed and the state plus StreamSource.active carry the rest. Degraded now means no live source is active, and degraded reads apply stale-while-revalidate over HTTP on the configured source's schedule, treating unknown age as stale so data recovers once the stream stops for good. Polling gives up on a 401 and enters degraded, and a retried initialization after a failed first poll keeps the active polling source instead of leaving it in degraded.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…staleIfError only

Degraded reads no longer borrow the stream or polling schedules. Data refreshed within staleWhileRevalidate is served as is, older data is served while a background refresh runs, and data older than staleWhileRevalidate plus staleIfError waits for the refresh. Unknown age refreshes in the background.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…after suspension

While streaming, stale reads serve the cache and expired reads wait for the stream to confirm instead of starting an HTTP fetch that the next ping would cancel. StreamSource.revalidate() drops a connection silent beyond the fresh window (keeping a young replacement), and waits up to the fetch deadline for the next message; CacheReadPolicy.revalidate routes stale and expired reads through it. Polling, header mode and degraded reads keep refreshing over HTTP.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…equest

Module-scope initialization still starts no network activity in Vercel mode. Inside a request, initialize() now prepares the cache the way the first evaluation would: a matching version header confirms it, a newer version or an empty cache fetches, and a missing entry starts streaming or polling. It rejects only when no definitions are available. Runtime setup happens once per lifecycle, so repeated calls in later requests only repeat the preparation. Lazy initialization from reads passes prepareRequest: false, because the read resolves the request itself and must keep accurate cache metrics.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Streaming data now stays stale until five minutes after the last message, or until the fresh window plus staleWhileRevalidate if that is longer. A runtime resuming from an ordinary suspension serves the cache while the ping watchdog reconnects in the background, instead of blocking the read on a stream reconnect. Reads only wait for the stream once the connection has been silent beyond that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Streaming reads now always serve the cache, reporting data older than one missed ping as stale, and never start HTTP work while the stream is live. Recovery stays with the stream: the ping watchdog reconnects silent connections, including right after a suspended runtime resumes, and exhausted retries fall back to polling or HTTP revalidation. Production metrics showed rare ~2s (up to 10s) warm precompute() waits from the blocking path.

This removes the stream revalidation wait (StreamSource.revalidate, the per-connection reconnect handle and the cache's resolveThroughSource) and the five-minute streaming expiry, which only existed to bound that wait.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot mentioned this pull request Oct 9, 2026

This branch was successfully deployed

14 active deployments
Preview – flags-sdk-dev — 1a66589f Deployed Oct 9, 2026 by vercel[bot]
Preview – flags-sdk-snippets — 1a66589f Deployed Oct 9, 2026 by vercel[bot]
Preview – flags-sdk-vercel — 1a66589f Deployed Oct 9, 2026 by vercel[bot]
Preview – flags-sdk-posthog — 1a66589f Deployed Oct 9, 2026 by vercel[bot]
Preview – flags-sdk-flagsmith — 1a66589f Deployed Oct 9, 2026 by vercel[bot]
Preview – flags-playground — 1a66589f Deployed Oct 9, 2026 by vercel[bot]
Preview – flags-sdk-growthbook — 1a66589f Deployed Oct 9, 2026 by vercel[bot]
Preview – flags-sdk-launchdarkly — 1a66589f Deployed Oct 9, 2026 by vercel[bot]
Preview – flags-sdk-reflag — 1a66589f Deployed Oct 9, 2026 by vercel[bot]
Preview – flags-sdk-sveltekit-snippets — 1a66589f Deployed Oct 9, 2026 by vercel[bot]
Preview – flags-sdk-openfeature — 1a66589f Deployed Oct 9, 2026 by vercel[bot]
Preview – flags-sdk-statsig — 1a66589f Deployed Oct 9, 2026 by vercel[bot]
Preview – shirt-shop-api — 1a66589f Deployed Oct 9, 2026 by vercel[bot]
Preview – shirt-shop — 1a66589f Deployed Oct 9, 2026 by vercel[bot]
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.

3 participants