[core] [world] Gate CBOR queue transport on specVersion#1627
[core] [world] Gate CBOR queue transport on specVersion#1627VaguelySerious merged 12 commits intomainfrom
Conversation
Bump SPEC_VERSION_CURRENT to 3 and fix isLegacySpecVersion to check against SPEC_VERSION_LEGACY instead of SPEC_VERSION_CURRENT, preventing v2 runs from being incorrectly treated as legacy when the version bumps. Add JsonTransport and DualTransport to world-vercel queue: the handler uses DualTransport (CBOR-first, JSON fallback) to accept messages from both old and new deployments, while the send path selects CBOR or JSON based on the target run's specVersion. Pass specVersion through queue opts from start(), reenqueueRun(), wakeUpRun(), and resumeHook(). Only include runInput in the queue payload for specVersion >= 3, creating clean separation between old and new behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bump specVersion to 3. Use JSON transport for queue messages targeting older deployments (specVersion < 3), CBOR for new ones. Handler uses dual transport that deserializes both formats, so new deployments can receive messages from old and new senders. Fixes replay/reenqueue from the dashboard to older deployments that expect JSON queue messages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: ff8a79c The changes in this PR will be included in the next version bump. This PR includes changesets to release 20 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📊 Benchmark Results
workflow with no steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) workflow with 1 step💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) workflow with 10 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) workflow with 25 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro workflow with 50 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) Promise.all with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) Promise.all with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) Promise.all with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) Promise.race with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro Promise.race with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) Promise.race with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro workflow with 10 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) workflow with 25 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) workflow with 50 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) workflow with 10 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) workflow with 25 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) workflow with 50 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro Stream Benchmarks (includes TTFB metrics)workflow with stream💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) stream pipeline with 5 transform steps (1MB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) 10 parallel streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) fan-out fan-in 10 streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) SummaryFastest Framework by WorldWinner determined by most benchmark wins
Fastest World by FrameworkWinner determined by most benchmark wins
Column Definitions
Worlds:
❌ Some benchmark jobs failed:
Check the workflow run for details. |
🧪 E2E Test Results❌ Some tests failed Summary
❌ Failed Tests🌍 Community Worlds (60 failed)mongodb-dev (1 failed):
redis-dev (1 failed):
turso-dev (1 failed):
turso (57 failed):
Details by Category✅ ▲ Vercel Production
✅ 💻 Local Development
✅ 📦 Local Production
✅ 🐘 Local Postgres
✅ 🪟 Windows
❌ 🌍 Community Worlds
✅ 📋 Other
|
Replace SPEC_VERSION_CURRENT comparisons with named constants (SPEC_VERSION_SUPPORTS_CBOR_QUEUE_TRANSPORT, SPEC_VERSION_SUPPORTS_EVENT_SOURCING) so future SPEC_VERSION_CURRENT bumps don't shift existing feature gates. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
…d, causing `toEqual` to fail due to exact equality mismatch.
This commit fixes the issue reported at packages/core/e2e/e2e.test.ts:1550
**Bug explanation:**
The `withHealthCheck` handler in `packages/core/src/runtime/helpers.ts` returns a JSON response with three fields: `{ healthy: true, endpoint: url.pathname, specVersion: SPEC_VERSION_CURRENT }`. However, the HTTP-based health check e2e test in `packages/core/e2e/e2e.test.ts` (lines 1549-1553 and 1567-1571) uses `toEqual` to assert against objects that only have two fields: `{ healthy: true, specVersion: SPEC_VERSION_CURRENT }`, missing the `endpoint` field.
In Vitest, `toEqual` performs exact deep equality checking. When the actual response object contains `{ healthy: true, endpoint: "/.well-known/workflow/v1/flow", specVersion: "..." }` but the expected object only has `{ healthy: true, specVersion: "..." }`, the assertion fails because the actual object has an extra `endpoint` property not present in the expected object.
This affects both the flow endpoint health check assertion and the step endpoint health check assertion.
**Fix explanation:**
Added the missing `endpoint` field to both `toEqual` assertions:
- For the flow endpoint: `endpoint: '/.well-known/workflow/v1/flow'` (the pathname portion of `/.well-known/workflow/v1/flow?__health`)
- For the step endpoint: `endpoint: '/.well-known/workflow/v1/step'` (the pathname portion of `/.well-known/workflow/v1/step?__health`)
These values match what `url.pathname` returns in the handler (the `?__health` query parameter is not part of the pathname). This makes the test assertions exactly match the actual response structure from the handler.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: VaguelySerious <mittgfu@gmail.com>
Update HealthCheckResult to include specVersion and parse it from the stream response, so observability tools can query deployment capabilities through both the HTTP and queue-based health check paths. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use Record<string, unknown> instead of narrower casts that tsc rejects as insufficient overlap. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
TooTallNate
left a comment
There was a problem hiding this comment.
This is a well-structured backwards-compatibility fix for the CBOR transport introduced in #1537. The approach is sound: gate send-side transport on the target run's specVersion, and use a dual CBOR-first/JSON-fallback transport on the receive side.
What I verified:
spec-version.ts:
isLegacySpecVersionnow correctly checksv <= SPEC_VERSION_LEGACY(1) instead ofv < SPEC_VERSION_CURRENT. This is the right fix — without it, bumpingSPEC_VERSION_CURRENTto 3 would cause specVersion 2 runs to be incorrectly treated as v1-compat (direct entity mutation instead of event-sourced). All 6 callsites ofisLegacySpecVersionare compatible with this semantic change.- Named constants (
SPEC_VERSION_SUPPORTS_EVENT_SOURCING,SPEC_VERSION_SUPPORTS_CBOR_QUEUE_TRANSPORT) clearly document which features each version introduced.
world-vercel/queue.ts:
- Send path creates a per-call
QueueClientwith the right transport based onopts.specVersion.undefineddefaults toSPEC_VERSION_CURRENT, which is correct — callers without specVersion are new runs. DualTransport.deserialize()tries CBOR decode first, catches on failure, falls back to JSON. Correct — valid CBOR will never accidentally parse as JSON since CBOR's first byte (typically 0xa or 0xb map/array major types) would be invalid JSON.- The handler uses
DualTransport(receives both formats); send-side usesCborTransportfor new runs (handler re-enqueues target the same new deployment). Correct.
Queue callsite coverage:
start.ts— passesspecVersionvia opts. Also gatesrunInputon>= SPEC_VERSION_SUPPORTS_CBOR_QUEUE_TRANSPORTsinceUint8Arraydoesn't survive JSON. Correct.reenqueueRun(runs.ts:120) — passesrun.specVersion ?? SPEC_VERSION_LEGACY. Correct.wakeUpRun(runs.ts:213) — same pattern. Correct.resumeHook(resume-hook.ts:195) — passesworkflowRun.specVersion ?? SPEC_VERSION_LEGACY. Correct.healthCheck(helpers.ts:228) — no opts, defaults toSPEC_VERSION_CURRENT. Correct — health checks target the current deployment.
Health check response format:
- Changed from
text/plaintoapplication/jsonwith{ healthy, endpoint, specVersion }. This is a minor breaking change for anything parsing the response, but the e2e tests are updated and the JSON format is strictly more useful (enables version detection from the dashboard).
Changeset: patch for all three packages. Correct.
LGTM.
| const buffer = Buffer.concat(chunks); | ||
| try { | ||
| return decode(buffer); | ||
| } catch { |
There was a problem hiding this comment.
Non-blocking observation: The CBOR-first, JSON-fallback strategy is safe because CBOR's binary encoding always starts with a type-length byte (e.g. 0xA2 for a 2-entry map) that is never valid as the first byte of a JSON string (which must start with {, [, ", digit, t, f, n, or whitespace in ASCII). So there's no ambiguity — cbor-x.decode() will throw on JSON input, and JSON.parse() will succeed.
The only theoretical concern is a corrupted message that happens to be valid CBOR but not valid JSON — but that's a data integrity issue unrelated to the dual transport.
| * @param v - The spec version number, or undefined/null for legacy runs | ||
| * @returns true if the run is a legacy run | ||
| */ | ||
| export function isLegacySpecVersion(v: number | undefined | null): boolean { |
There was a problem hiding this comment.
Non-blocking: This is the key correctness fix. The old v < SPEC_VERSION_CURRENT would have made specVersion 2 runs legacy after the bump to 3, breaking event-sourced replay for all existing v2 runs. v <= SPEC_VERSION_LEGACY is the right semantic — only version 1 is truly legacy (direct entity mutation).
Summary
PR #1537 changed the world-vercel queue transport from JSON to CBOR. This breaks replay/reenqueue from the dashboard (and any queue operation targeting an older deployment) because old deployments expect JSON messages.
specVersionto 3 — new runs use CBOR queue transportqueue()selects transport based onopts.specVersion: JSON for < 3, CBOR for >= 3DualTransportthat deserializes CBOR first, falls back to JSON — so new deployments handle messages from both old and new sendersrunInput(resilient start payload) only included for specVersion >= 3, since old deployments ignore it anyway and Uint8Array doesn't survive plain JSON serializationisLegacySpecVersionto check againstSPEC_VERSION_LEGACY(1) instead ofSPEC_VERSION_CURRENT, preventing v2 runs from being incorrectly treated as v1-compat after the bumpworld.queue()call sites now passspecVersionin optionsTest plan
packages/coretests pass (580/580)packages/world-verceltests pass (21/21)🤖 Generated with Claude Code