Skip to content

EXP-3418: skip init wait when the routed config version is already loaded - #489

Draft
luismeyer wants to merge 1 commit into
mainfrom
exp-3418-edge-cfg-vers
Draft

EXP-3418: skip init wait when the routed config version is already loaded#489
luismeyer wants to merge 1 commit into
mainfrom
exp-3418-edge-cfg-vers

Conversation

@luismeyer

@luismeyer luismeyer commented Sep 2, 2026

Copy link
Copy Markdown
Member

EXP-3418

@vercel/flags-core waits for a stream confirmation or a first poll on every cold start, even when the definitions it already has in hand are current. Vercel already tells us which config version a request was routed to, so that wait is avoidable.

What changed

  • Parsersrc/utils/edge-config-versions.ts parses the existing x-vercel-edge-config-versions request header, a semicolon-separated map of store name to version (flags_prj_123=1758000000000;ecfg_abc=1757000000000). It selects only the exact flags_<projectId> entry — no prefix, suffix or substring matching — and accepts only non-negative safe integers.
  • Decisionsrc/controller/routed-init.ts reads the header from the existing Vercel request context (utils/request-context.ts), derives the key from the projectId of the loaded data, and compares the local configUpdatedAt against the routed version.
  • Controller — after local data loads (provided datafile or bundled definitions) but before awaiting stream/poll, initialize() returns immediately when local configUpdatedAt >= routed version, while the stream (or poll + interval) keeps updating in the background.

No config id is involved and no new header is requested — only the header that is already on the request context.

Preserved behavior

All of these keep waiting up to initTimeoutMs exactly as before:

  • no request context, or a context without headers
  • no projectId on the loaded data
  • no exact entry for this project (including keys that merely overlap)
  • malformed or unsafe versions: empty, non-numeric, signed, fractional, exponent/hex, NaN/Infinity, beyond Number.MAX_SAFE_INTEGER
  • the routed key appearing more than once (reported, never resolved to one of them)
  • loaded data without a usable configUpdatedAt

Two more invariants are covered by tests:

  • No connection is reported before one exists. The controller stays in initializing:* after an immediate init, so reads report connectionState: 'disconnected' and mode: 'offline' until the source emits connected.
  • Newer state is never replaced by equal or older data. The existing configUpdatedAt guard still applies to the background stream/poll updates.

Metrics

FLAGS_CONFIG_READ events carry a new configRoutedInit field — one of immediate, behind, invalid, duplicate, unknown-local. It is a fixed low-cardinality enum, carries no project ids or header values, and is omitted entirely when no routed version applied, so existing event payloads are unchanged.

Tests

  • src/utils/edge-config-versions.test.ts — 18 focused parser tests (exact match, whitespace, duplicates, malformed/unsafe versions)
  • src/controller/routed-init.test.ts — 31 focused decision tests (missing context/project/match, comparison, unsafe values on both sides)
  • src/black-box.test.ts — 23 tests through the public API: immediate init for provided datafile, equal version, bundled definitions and polling mode; the full preserve-behavior table; connection reporting; the equal/older guard; and the metric assertions including a negative check that no ids or header values are ingested
pnpm test        565 passed (15 files), was 493
pnpm type-check  clean
pnpm check       clean (1 pre-existing warning, unchanged from main)
pnpm build       success

Repo-wide pnpm type-check (18/18) and pnpm test (17/17) also pass.

Notes

  • cacheStatus stays STALE during the immediate-init window because it is derived from the connection state, matching the existing timeout-fallback and polling-with-data paths. Changing that is a separate metrics decision.
  • Biome's autofix wanted to rewrite an unrelated mock in src/index.make.test.ts in a way its own comment forbids; that change was reverted and is not part of this PR.

🤖 Generated with Claude Code

EXP-3418

Reads the existing `x-vercel-edge-config-versions` request context header,
selects the exact `flags_<projectId>` entry derived from the loaded
definitions, and resolves `initialize()` right away when the local
`configUpdatedAt` is at or ahead of that version, while the stream or poll
keeps updating in the background.

No config id and no new header are involved. A missing request context,
project or entry, a malformed/unsafe version, a duplicated entry, and local
data without a usable `configUpdatedAt` all preserve the previous behavior of
waiting up to `initTimeoutMs`. The controller stays in `initializing:*` until
the source actually connects, so no connection is reported before it exists,
and the existing `configUpdatedAt` guard still keeps background updates from
replacing newer definitions with equal or older ones.

The low cardinality outcome is attached to `FLAGS_CONFIG_READ` events as
`configRoutedInit` and never carries ids or header values.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Co-Authored-By: Luis Meyer <luis.meyer@vercel.com>
@vercel

vercel Bot commented Sep 2, 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 Sep 2, 2026 9:19am UTC
shirt-shop Ready Ready Preview, v0 Sep 2, 2026 9:19am UTC
shirt-shop-api Ready Ready Preview, v0 Sep 2, 2026 9:19am UTC

@luismeyer
luismeyer marked this pull request as draft September 2, 2026 13:33
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.

1 participant