') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); [core] Add wire-level framing for byte streams by TooTallNate · Pull Request #1853 · vercel/workflow · GitHub
Skip to content

[core] Add wire-level framing for byte streams - #1853

Merged
VaguelySerious merged 14 commits into
mainfrom
nate/byte-stream-framing
Jun 12, 2026
Merged

[core] Add wire-level framing for byte streams#1853
VaguelySerious merged 14 commits into
mainfrom
nate/byte-stream-framing

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Apr 25, 2026

Copy link
Copy Markdown
Member

Summary

Wraps each chunk of type: 'bytes' ReadableStreams in a 4-byte big-endian length prefix on the wire, so consumers can identify chunk boundaries. The user-facing API is unchanged — getReader() still yields raw Uint8Array chunks; only the wire envelope is new.

This enables byte-stream auto-reconnect to be added in a follow-up: with frames on the wire, a reconnecting reader can count completed frames and resume from startIndex + consumed after a transient error — the same trick the object-stream reconnecting reader uses.

#1854 (which surfaced workflowCoreVersion from healthCheck()) has merged, so this PR is now based directly on main.

Mechanism

The framing decision is made per-stream at serialization time and recorded in the stream ref:

ReadableStream:
|{ name: string; type?: 'bytes'; startIndex?: number; framing?: 'raw'|'framed-v1'}|{bodyInit: any};

Readers dispatch on the field. Absent or 'raw' → existing legacy behavior (no unframing). 'framed-v1' → pipe through getByteUnframingStream() to strip the length prefix before handing chunks to the user.

Capability gating

Producers consult a new framedByteStreams capability in getRunCapabilities(), keyed on the target run's workflowCoreVersion, and bake the choice into the ref so consumers don't re-do the lookup. Same-deployment/same-run producers always frame; cross-deployment start() probes the target via healthCheck() (2s timeout) and falls back to raw on miss/timeout. Older runs whose refs lack framing are read as raw bytes.

Relationship to the reconnect work

Independent of, and not blocking, the object-stream reconnect PRs (#1847 on stable and its main forward-port), which deliberately opt byte streams out. This PR lays the groundwork so a later follow-up can extend the same frame-counting reconnect to framing: 'framed-v1' byte streams without touching the legacy raw path. See the cross-PR comment for ordering.

Tests

  • byte-stream-framing.test.ts (new): framing/unframing prefix shape, empty-chunk drop, large chunks, split/coalesced frames across reads, mid-frame truncation + oversized-frame guards, and end-to-end round-trips for both framedByteStreams on/off.
  • capabilities.test.ts (extended): framedByteStreams false for invalid/old versions, true from the cutoff onward.

Notes

The minVersion for framedByteStreams is set based on the next beta release — update if the actual ship version differs. The 'framed-v1' identifier is opaque so future framing variants can be added without breaking existing consumers.

CopilotAI review requested due to automatic review settings April 25, 2026 07:21
@changeset-bot

changeset-botBot commented Apr 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2dbcbb7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
NameType
@workflow/coreMinor
workflowMinor
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/world-testingPatch
@workflow/aiMajor
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

@vercel

vercelBot commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.041s (-2.4%)1.006s (~)0.965s101.00x
💻 LocalNitro0.047s (+11.3% 🔺)1.007s (~)0.959s101.14x
🐘 PostgresExpress0.062s (+1.0%)1.012s (~)0.951s101.50x
💻 LocalNext.js (Turbopack)0.065s (+3.5%)1.006s (~)0.941s101.57x
🐘 PostgresNitro0.067s (+1.1%)1.013s (~)0.947s101.61x
🐘 PostgresNext.js (Turbopack)0.103s (+38.1% 🔺)1.029s (+1.3%)0.925s102.50x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.274s (+1.4%)2.049s (-20.6% 🟢)1.775s101.00x
▲ VercelNext.js (Turbopack)0.397s (+5.5% 🔺)2.290s (-5.7% 🟢)1.892s101.45x
▲ VercelNitro0.513s (+35.7% 🔺)2.730s (+30.0% 🔺)2.217s101.87x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.092s (~)2.006s (~)0.914s101.00x
💻 LocalNitro1.104s (+1.2%)2.007s (~)0.903s101.01x
🐘 PostgresNitro1.116s (~)2.011s (~)0.895s101.02x
🐘 PostgresExpress1.117s (~)2.011s (~)0.894s101.02x
💻 LocalNext.js (Turbopack)1.136s (+0.8%)2.007s (~)0.870s101.04x
🐘 PostgresNext.js (Turbopack)1.243s (+8.7% 🔺)2.050s (+2.0%)0.808s101.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.581s (-9.9% 🟢)3.837s (+3.4%)2.257s101.00x
▲ VercelNitro1.675s (~)3.724s (+8.0% 🔺)2.049s101.06x
▲ VercelNext.js (Turbopack)1.722s (+2.6%)3.590s (-1.3%)1.869s101.09x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express10.523s (~)11.022s (~)0.500s31.00x
💻 LocalNitro10.544s (~)11.024s (~)0.480s31.00x
🐘 PostgresExpress10.573s (~)11.018s (~)0.446s31.00x
🐘 PostgresNitro10.575s (~)11.019s (~)0.444s31.01x
💻 LocalNext.js (Turbopack)10.830s (~)11.022s (~)0.192s31.03x
🐘 PostgresNext.js (Turbopack)11.466s (+4.1%)12.361s (+5.8% 🔺)0.895s31.09x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro13.766s (+3.2%)16.290s (+7.6% 🔺)2.524s21.00x
▲ VercelExpress13.943s (+3.9%)16.256s (+3.3%)2.313s21.01x
▲ VercelNext.js (Turbopack)14.375s (+2.0%)15.997s (-1.0%)1.622s21.04x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.698s (-1.1%)14.027s (~)0.329s51.00x
🐘 PostgresNitro13.818s (~)14.020s (~)0.202s51.01x
💻 LocalNitro13.897s (~)14.028s (-1.4%)0.131s51.01x
🐘 PostgresExpress13.921s (+0.6%)14.019s (~)0.098s51.02x
💻 LocalNext.js (Turbopack)14.517s (~)15.030s (~)0.514s41.06x
🐘 PostgresNext.js (Turbopack)14.711s (+1.5%)15.020s (~)0.309s41.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro21.305s (-5.0%)23.620s (-1.9%)2.315s31.00x
▲ VercelExpress22.452s (+4.5%)24.209s (+4.9%)1.757s31.05x
▲ VercelNext.js (Turbopack)22.939s (+6.2% 🔺)24.668s (+5.7% 🔺)1.729s31.08x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express12.418s (-0.7%)13.025s (~)0.608s71.00x
🐘 PostgresNitro12.563s (~)13.020s (~)0.457s71.01x
🐘 PostgresExpress12.620s (-1.0%)13.020s (~)0.400s71.02x
💻 LocalNitro12.657s (+1.5%)13.025s (~)0.368s71.02x
💻 LocalNext.js (Turbopack)13.900s (+1.6%)14.026s (~)0.126s71.12x
🐘 PostgresNext.js (Turbopack)14.332s (+2.6%)14.787s (+3.4%)0.455s71.15x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro29.660s (-6.5% 🟢)32.293s (-3.0%)2.633s31.00x
▲ VercelExpress30.017s (+1.2%)32.987s (+5.9% 🔺)2.971s31.01x
▲ VercelNext.js (Turbopack)33.331s (+16.3% 🔺)35.152s (+15.8% 🔺)1.820s31.12x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.183s (-0.6%)2.006s (~)0.823s151.00x
💻 LocalNitro1.206s (-1.4%)2.006s (~)0.800s151.02x
🐘 PostgresNitro1.209s (~)2.007s (~)0.799s151.02x
🐘 PostgresExpress1.225s (+1.9%)2.008s (~)0.783s151.04x
💻 LocalNext.js (Turbopack)1.327s (-1.6%)2.006s (~)0.680s151.12x
🐘 PostgresNext.js (Turbopack)1.471s (+16.9% 🔺)2.142s (+6.6% 🔺)0.670s151.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.358s (-7.6% 🟢)4.099s (-8.9% 🟢)1.741s81.00x
▲ VercelNitro2.392s (-35.3% 🟢)4.421s (-14.7% 🟢)2.029s71.01x
▲ VercelNext.js (Turbopack)2.713s (-10.9% 🟢)4.561s (-0.7%)1.848s71.15x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.356s (+7.4% 🔺)2.393s (+19.2% 🔺)1.037s131.00x
🐘 PostgresNitro1.397s (+11.0% 🔺)2.511s (+25.1% 🔺)1.114s121.03x
🐘 PostgresNext.js (Turbopack)1.580s (+12.8% 🔺)2.222s (+10.7% 🔺)0.642s141.17x
💻 LocalNitro1.659s (-2.6%)2.006s (~)0.348s151.22x
💻 LocalExpress1.771s (-3.8%)2.073s (-3.6%)0.301s151.31x
💻 LocalNext.js (Turbopack)1.877s (~)2.150s (~)0.274s141.38x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.933s (-99.0% 🟢)4.682s (-98.5% 🟢)1.749s71.00x
▲ VercelNitro3.317s (-46.8% 🟢)5.133s (-33.7% 🟢)1.816s61.13x
▲ VercelExpress3.863s (-12.6% 🟢)5.894s (-3.6%)2.031s61.32x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.597s (+13.5% 🔺)4.011s (+99.7% 🔺)2.414s81.00x
🐘 PostgresExpress1.659s (+18.4% 🔺)4.012s (+99.8% 🔺)2.353s81.04x
🐘 PostgresNext.js (Turbopack)2.871s (+59.2% 🔺)3.768s (+64.0% 🔺)0.897s81.80x
💻 LocalNitro4.394s (-16.3% 🟢)4.869s (-19.1% 🟢)0.475s72.75x
💻 LocalNext.js (Turbopack)4.607s (-19.2% 🟢)5.346s (-14.0% 🟢)0.739s62.88x
💻 LocalExpress4.729s (-12.2% 🟢)5.513s (-10.8% 🟢)0.784s62.96x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.379s (-74.2% 🟢)6.443s (-64.8% 🟢)2.064s51.00x
▲ VercelExpress4.479s (-50.9% 🟢)6.540s (-42.4% 🟢)2.061s51.02x
▲ VercelNext.js (Turbopack)5.584s (-7.2% 🟢)8.084s (-4.2%)2.500s41.28x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.215s (+2.0%)2.007s (~)0.793s151.00x
🐘 PostgresExpress1.218s (+0.5%)2.007s (~)0.789s151.00x
🐘 PostgresNext.js (Turbopack)1.453s (+14.9% 🔺)2.055s (+2.3%)0.603s151.20x
💻 LocalNext.js (Turbopack)1.471s (+8.8% 🔺)2.075s (+3.4%)0.604s151.21x
💻 LocalNitro1.583s (+4.9%)2.007s (~)0.424s151.30x
💻 LocalExpress1.633s (-1.5%)2.074s (~)0.440s151.34x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.277s (-15.7% 🟢)3.903s (-17.2% 🟢)1.626s81.00x
▲ VercelNext.js (Turbopack)2.371s (-42.2% 🟢)3.689s (-34.5% 🟢)1.319s91.04x
▲ VercelNitro2.626s (+1.8%)4.289s (+1.3%)1.663s81.15x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.414s (+9.0% 🔺)2.394s (+19.1% 🔺)0.980s131.00x
🐘 PostgresNitro1.430s (+12.5% 🔺)2.223s (+10.7% 🔺)0.793s141.01x
💻 LocalNitro1.919s (-11.6% 🟢)2.315s (-10.7% 🟢)0.396s131.36x
💻 LocalExpress1.963s (-9.0% 🟢)2.395s (-10.5% 🟢)0.432s131.39x
🐘 PostgresNext.js (Turbopack)2.012s (+43.3% 🔺)2.708s (+34.8% 🔺)0.695s121.42x
💻 LocalNext.js (Turbopack)2.154s (+5.3% 🔺)2.827s (-6.0% 🟢)0.673s111.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.076s (-15.6% 🟢)5.075s (-2.7%)1.999s71.00x
▲ VercelExpress3.077s (-10.2% 🟢)5.130s (+0.6%)2.053s61.00x
▲ VercelNitro3.522s (-27.0% 🟢)6.573s (+2.6%)3.051s51.14x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.732s (+23.5% 🔺)4.136s (+105.7% 🔺)2.404s81.00x
🐘 PostgresNitro1.825s (+31.4% 🔺)4.441s (+121.1% 🔺)2.616s71.05x
🐘 PostgresNext.js (Turbopack)3.911s (+116.3% 🔺)4.732s (+112.8% 🔺)0.821s72.26x
💻 LocalNitro4.879s (-10.3% 🟢)5.682s (-5.5% 🟢)0.803s62.82x
💻 LocalNext.js (Turbopack)5.415s (-10.6% 🟢)6.016s (-11.8% 🟢)0.602s53.13x
💻 LocalExpress6.388s (~)7.015s (~)0.627s53.69x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.409s (-33.2% 🟢)5.597s (-22.7% 🟢)2.188s61.00x
▲ VercelNitro3.945s (-43.7% 🟢)6.263s (-27.2% 🟢)2.319s51.16x
▲ VercelNext.js (Turbopack)4.328s (-46.1% 🟢)6.403s (-34.6% 🟢)2.075s51.27x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.598s (-2.3%)1.005s (~)0.407s601.00x
🐘 PostgresExpress0.602s (~)1.006s (~)0.404s601.01x
🐘 PostgresNitro0.603s (+4.0%)1.023s (+1.7%)0.421s591.01x
💻 LocalNitro0.606s (+1.0%)1.005s (~)0.399s601.01x
🐘 PostgresNext.js (Turbopack)0.902s (+5.5% 🔺)1.235s (+20.6% 🔺)0.333s491.51x
💻 LocalNext.js (Turbopack)0.908s (+5.2% 🔺)1.076s (+5.4% 🔺)0.169s561.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.073s (+22.4% 🔺)7.867s (+20.8% 🔺)1.794s81.00x
▲ VercelNext.js (Turbopack)6.355s (+21.7% 🔺)7.993s (+16.5% 🔺)1.639s81.05x
▲ VercelExpress6.419s (-36.7% 🟢)8.218s (-30.2% 🟢)1.798s81.06x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.436s (~)2.053s (+1.1%)0.616s441.00x
🐘 PostgresExpress1.495s (+4.3%)2.054s (+2.3%)0.559s441.04x
💻 LocalExpress1.512s (-2.7%)2.006s (-1.1%)0.494s451.05x
💻 LocalNitro1.529s (+1.7%)2.006s (~)0.477s451.06x
🐘 PostgresNext.js (Turbopack)2.053s (+1.2%)2.505s (-5.7% 🟢)0.452s371.43x
💻 LocalNext.js (Turbopack)2.247s (+7.0% 🔺)3.008s (~)0.762s301.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.556s (+11.1% 🔺)16.437s (+13.7% 🔺)1.882s61.00x
▲ VercelExpress14.809s (+3.1%)16.891s (+4.6%)2.082s61.02x
▲ VercelNext.js (Turbopack)15.611s (+17.4% 🔺)17.495s (+15.9% 🔺)1.884s61.07x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.740s (-2.0%)3.137s (-1.0%)0.397s391.00x
🐘 PostgresExpress2.829s (+0.7%)3.167s (+1.0%)0.338s381.03x
💻 LocalExpress3.243s (-1.3%)4.010s (~)0.767s301.18x
💻 LocalNitro3.274s (-0.7%)4.009s (~)0.735s301.19x
💻 LocalNext.js (Turbopack)4.592s (+5.7% 🔺)5.012s (~)0.420s241.68x
🐘 PostgresNext.js (Turbopack)4.875s (+22.4% 🔺)5.326s (+22.9% 🔺)0.450s231.78x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro27.871s (-4.3%)30.809s (~)2.938s41.00x
▲ VercelExpress28.878s (+10.1% 🔺)31.688s (+12.3% 🔺)2.810s41.04x
▲ VercelNext.js (Turbopack)30.740s (-13.9% 🟢)33.226s (-11.3% 🟢)2.486s41.10x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.243s (+10.7% 🔺)1.006s (~)0.763s601.00x
🐘 PostgresExpress0.290s (+24.6% 🔺)1.007s (~)0.717s601.19x
💻 LocalExpress0.414s (~)1.004s (~)0.590s601.70x
💻 LocalNitro0.445s (+3.6%)1.022s (~)0.576s591.83x
🐘 PostgresNext.js (Turbopack)0.597s (+115.1% 🔺)1.227s (+21.9% 🔺)0.630s492.46x
💻 LocalNext.js (Turbopack)0.673s (+23.2% 🔺)1.059s (+5.4% 🔺)0.386s572.77x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.979s (-22.9% 🟢)3.879s (-5.6% 🟢)1.899s161.00x
▲ VercelNitro2.168s (-23.2% 🟢)3.826s (-11.4% 🟢)1.658s161.10x
▲ VercelNext.js (Turbopack)2.485s (-44.3% 🟢)4.206s (-32.4% 🟢)1.721s151.26x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.420s (+17.2% 🔺)1.029s (+2.2%)0.608s881.00x
🐘 PostgresExpress0.449s (+19.9% 🔺)1.053s (+4.7%)0.605s861.07x
🐘 PostgresNext.js (Turbopack)0.757s (+51.8% 🔺)1.392s (+38.3% 🔺)0.634s651.80x
💻 LocalNitro2.017s (-7.6% 🟢)2.581s (-4.9%)0.564s354.80x
💻 LocalExpress2.167s (+6.4% 🔺)2.736s (+7.9% 🔺)0.569s335.16x
💻 LocalNext.js (Turbopack)2.717s (+10.9% 🔺)3.420s (+8.6% 🔺)0.702s276.46x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.095s (-42.8% 🟢)4.926s (-30.4% 🟢)1.831s191.00x
▲ VercelNitro3.387s (-63.4% 🟢)5.234s (-51.8% 🟢)1.847s181.09x
▲ VercelNext.js (Turbopack)3.678s (-52.3% 🟢)5.382s (-42.7% 🟢)1.704s171.19x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.785s (+15.2% 🔺)1.367s (+35.8% 🔺)0.582s891.00x
🐘 PostgresExpress0.852s (+22.4% 🔺)1.489s (+48.0% 🔺)0.637s811.09x
🐘 PostgresNext.js (Turbopack)2.283s (+127.1% 🔺)3.005s (+62.0% 🔺)0.722s402.91x
💻 LocalNitro9.112s (-1.8%)9.639s (-1.6%)0.527s1311.61x
💻 LocalExpress9.444s (+1.4%)10.026s (+1.6%)0.582s1212.04x
💻 LocalNext.js (Turbopack)11.847s (+8.3% 🔺)12.835s (+11.8% 🔺)0.989s1015.10x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.460s (-65.8% 🟢)8.398s (-59.1% 🟢)1.938s151.00x
▲ VercelNitro7.521s (-61.0% 🟢)9.362s (-57.7% 🟢)1.841s131.16x
▲ VercelNext.js (Turbopack)8.654s (-60.9% 🟢)10.433s (-57.2% 🟢)1.779s121.34x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.159s (-0.6%)2.005s (~)0.010s (~)2.018s (~)0.859s101.00x
💻 LocalExpress1.160s (-0.7%)2.005s (~)0.010s (-12.0% 🟢)2.017s (~)0.857s101.00x
🐘 PostgresNitro1.166s (-0.6%)2.000s (~)0.001s (-7.7% 🟢)2.011s (~)0.845s101.01x
🐘 PostgresExpress1.170s (-0.7%)2.001s (~)0.001s (-8.3% 🟢)2.011s (~)0.841s101.01x
💻 LocalNext.js (Turbopack)1.220s (+0.7%)2.003s (~)0.012s (-2.4%)2.020s (~)0.800s101.05x
🐘 PostgresNext.js (Turbopack)1.306s (+4.9%)2.001s (~)0.001s (-23.1% 🟢)2.013s (~)0.707s101.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.434s (+12.3% 🔺)3.537s (+10.4% 🔺)1.442s (+58.7% 🔺)5.481s (+19.7% 🔺)3.046s101.00x
▲ VercelNext.js (Turbopack)2.440s (+8.7% 🔺)3.341s (+3.4%)1.674s (+46.7% 🔺)5.451s (+11.7% 🔺)3.011s101.00x
▲ VercelNitro2.718s (+11.1% 🔺)3.825s (+13.7% 🔺)1.570s (+65.4% 🔺)6.020s (+26.3% 🔺)3.302s101.12x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.574s (~)2.010s (~)0.011s (-8.0% 🟢)2.023s (~)0.449s301.00x
💻 LocalNitro1.584s (-0.7%)2.009s (~)0.013s (+5.3% 🔺)2.025s (~)0.441s301.01x
🐘 PostgresNitro1.592s (-1.3%)2.008s (~)0.005s (+9.4% 🔺)2.029s (~)0.437s301.01x
🐘 PostgresExpress1.625s (+1.5%)2.006s (~)0.005s (-2.6%)2.026s (~)0.401s301.03x
💻 LocalNext.js (Turbopack)1.815s (+4.4%)2.044s (+1.7%)0.014s (+14.1% 🔺)2.062s (+1.8%)0.247s301.15x
🐘 PostgresNext.js (Turbopack)2.614s (+46.3% 🔺)3.059s (+52.1% 🔺)0.004s (-14.0% 🟢)3.096s (+52.7% 🔺)0.482s201.66x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.956s (-1.9%)7.319s (~)0.262s (-0.5%)8.091s (+1.2%)2.135s81.00x
▲ VercelNext.js (Turbopack)6.162s (-4.4%)7.444s (-3.3%)0.382s (-31.8% 🟢)8.265s (-6.9% 🟢)2.103s81.03x
▲ VercelNitro6.165s (+4.1%)7.645s (+8.7% 🔺)0.785s (+67.3% 🔺)8.975s (+12.4% 🔺)2.810s71.04x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.760s (+7.2% 🔺)1.030s (-1.3%)0.000s (-100.0% 🟢)1.050s (-1.3%)0.290s581.00x
🐘 PostgresExpress0.840s (+18.8% 🔺)1.101s (+8.6% 🔺)0.000s (+321.4% 🔺)1.115s (+8.4% 🔺)0.275s561.11x
💻 LocalNitro1.409s (-2.2%)2.014s (~)0.000s (+40.0% 🔺)2.016s (~)0.607s301.85x
💻 LocalExpress1.414s (+4.8%)2.014s (~)0.000s (+250.0% 🔺)2.016s (~)0.602s301.86x
🐘 PostgresNext.js (Turbopack)1.554s (+80.3% 🔺)2.096s (+92.1% 🔺)0.000s (-100.0% 🟢)2.123s (+93.2% 🔺)0.569s302.05x
💻 LocalNext.js (Turbopack)1.565s (+6.7% 🔺)2.014s (~)0.000s (-73.3% 🟢)2.018s (~)0.453s302.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.016s (-20.7% 🟢)4.702s (-2.6%)0.000s (NaN%)5.264s (+0.8%)2.248s121.00x
▲ VercelExpress3.170s (-5.7% 🟢)4.608s (+0.7%)0.000s (-100.0% 🟢)5.098s (+1.2%)1.927s121.05x
▲ VercelNext.js (Turbopack)3.300s (-10.8% 🟢)4.662s (-6.3% 🟢)0.000s (-100.0% 🟢)5.117s (-6.5% 🟢)1.817s121.09x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.596s (+19.7% 🔺)2.141s (+5.6% 🔺)0.000s (+Infinity% 🔺)2.164s (+4.9%)0.568s281.00x
🐘 PostgresExpress1.751s (+14.8% 🔺)2.221s (~)0.000s (~)2.233s (-0.7%)0.482s271.10x
💻 LocalNitro3.055s (-3.6%)3.730s (-2.8%)0.001s (+245.1% 🔺)3.736s (-2.7%)0.681s171.91x
💻 LocalExpress3.137s (+1.3%)3.964s (+8.0% 🔺)0.001s (+15.9% 🔺)3.968s (+7.9% 🔺)0.831s161.97x
💻 LocalNext.js (Turbopack)3.156s (+10.7% 🔺)3.778s (+10.6% 🔺)0.001s (+256.3% 🔺)3.783s (+10.7% 🔺)0.627s161.98x
🐘 PostgresNext.js (Turbopack)3.396s (+96.3% 🔺)4.003s (+77.0% 🔺)0.000s (NaN%)4.035s (+77.7% 🔺)0.638s152.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.311s (-41.5% 🟢)6.245s (-21.3% 🟢)0.000s (+Infinity% 🔺)6.806s (-24.6% 🟢)2.495s91.00x
▲ VercelNext.js (Turbopack)4.390s (-50.5% 🟢)5.819s (-51.2% 🟢)0.000s (-100.0% 🟢)6.247s (-50.0% 🟢)1.857s101.02x
▲ VercelExpress4.406s (-17.2% 🟢)6.240s (-5.2% 🟢)0.000s (NaN%)6.810s (-5.3% 🟢)2.404s91.02x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress11/21
🐘 PostgresNitro15/21
▲ VercelExpress10/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres12/21
Next.js (Turbopack)🐘 Postgres11/21
Nitro🐘 Postgres15/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Redis + BullMQ: Community world (local development)
  • 🌐 Cloudflare: Community world (local development)
  • 🌐 MySQL: Community world (local development)
  • 🌐 Azure: Community world (local development)
  • 🌐 NATS JetStream: Community world (local development)
  • 🌐 Upstash: Community world (local development)

📋 View full workflow run

@github-actions

github-actionsBot commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production137602191595
✅ 💻 Local Development181102192030
✅ 📦 Local Production181102192030
✅ 🐘 Local Postgres179702332030
✅ 🪟 Windows14500145
✅ 📋 Other83701781015
Total7777010688845

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro119026
✅ example119026
✅ express119026
✅ fastify119026
✅ hono119026
✅ nextjs-turbopack14302
✅ nextjs-webpack14302
✅ nitro119026
✅ nuxt119026
✅ sveltekit13807
✅ vite119026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable119026
✅ express-stable119026
✅ fastify-stable119026
✅ hono-stable119026
✅ nextjs-turbopack-canary125020
✅ nextjs-turbopack-stable-lazy-discovery-disabled14401
✅ nextjs-turbopack-stable-lazy-discovery-enabled14401
✅ nextjs-webpack-canary125020
✅ nextjs-webpack-stable-lazy-discovery-disabled14401
✅ nextjs-webpack-stable-lazy-discovery-enabled14401
✅ nitro-stable119026
✅ nuxt-stable119026
✅ sveltekit-stable13807
✅ vite-stable119026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack14500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable120025
✅ e2e-local-dev-tanstack-start-120025
✅ e2e-local-postgres-nest-stable119026
✅ e2e-local-postgres-tanstack-start-119026
✅ e2e-local-prod-nest-stable120025
✅ e2e-local-prod-tanstack-start-120025
✅ e2e-vercel-prod-tanstack-start119026

📋 View full workflow run

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds opt-in wire-level length-prefix framing for type: 'bytes'ReadableStreams, gated by target run capabilities, to preserve chunk boundaries on the wire and enable future transparent auto-reconnect.

Changes:

  • Introduces byte-stream framing/unframing transforms and carries a per-stream framing field through serialization refs and VM symbols.
  • Adds framedByteStreams capability detection (via workflowCoreVersion) and uses a cross-deployment healthCheck probe in start() to decide framing.
  • Updates producer/consumer sites (step return values, getWritable, resumeHook, workflow argument dehydration) and adds dedicated tests + changeset.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
packages/core/src/symbols.tsAdds STREAM_FRAMING_SYMBOL to persist framing choice through the VM boundary.
packages/core/src/step/writable-stream.tsForces framed byte-stream support when serializing values written from steps (same-deployment assumption).
packages/core/src/serialization.tsImplements byte framing/unframing, adds framing to serialized stream refs, and threads framedByteStreams through reducers/revivers.
packages/core/src/runtime/step-handler.tsAlways enables framed byte streams for step return value dehydration (same-deployment assumption).
packages/core/src/runtime/start.tsAdds capability probing for cross-deployment start() to decide whether to frame byte streams.
packages/core/src/runtime/resume-hook.tsUses run capabilities to decide whether to emit framed byte streams in hook resume payloads.
packages/core/src/runtime/helpers.tsParses and surfaces workflowCoreVersion from health check responses.
packages/core/src/capabilities.tsAdds framedByteStreams capability with semver cutoff logic.
packages/core/src/capabilities.test.tsAdds coverage for framedByteStreams cutoff/invalid-version behavior.
packages/core/src/byte-stream-framing.test.tsAdds unit + e2e tests for framing/unframing and ref behavior.
.changeset/byte-stream-wire-framing.mdDeclares a minor release for the new wire framing capability and behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/core/src/serialization.ts
Comment threadpackages/core/src/serialization.ts Outdated
Comment threadpackages/core/src/byte-stream-framing.test.ts Outdated
Comment threadpackages/core/src/serialization.ts
The responding deployment already advertises its `@workflow/core` version
on the wire via `handleHealthCheckMessage` (`helpers.ts:109`), but it was
dropped at the parsing step. Surface it on `HealthCheckResult` so
callers can derive capability metadata about a target deployment before
sending it work — for example, version-gated decisions via
`getRunCapabilities()`.
Backwards compatible:
- Older deployments with plain-text health responses (specVersion < 3)
yield `{ healthy: true }` with no version metadata, as before.
- Older deployments with JSON responses but no `workflowCoreVersion`
field continue to return `{ healthy, specVersion? }`; the field is
simply omitted from the result.
- Defensive type check rejects non-string values rather than passing
them through as garbage.
Tests cover all four cases (workflowCoreVersion present, missing,
wrong type, plain-text legacy response) via a small in-memory mock
world plus a new e2e assertion.

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review: no blocking issues

Comment threadpackages/core/src/step/writable-stream.ts
Comment threadpackages/core/src/serialization.ts
Comment threadpackages/core/src/serialization.ts Outdated
@VaguelySerious

Copy link
Copy Markdown
Member

(AI) Cross-PR context & merge order

Together these make run.getReadable() transparently reconnect when a stream's underlying connection ends mid-stream (e.g. the periodic server-side max-duration cutoff), instead of surfacing a truncated stream to the consumer.

How the pieces fit:

Behavioural note: the reconnecting reader only reopens on a connection error — a clean close means "complete". So the client change is inert on its own and only takes effect once paired with the coordinated server-side change that ends a timed-out connection with an error rather than a silent close (handled separately). Shipping the client first is therefore safe and must precede that server change.

Suggested order:

  1. [core] Move stream reconnect logic to getReadable level #1847stable (client reconnect; a no-op until the paired server change ships).
  2. [core] Forward-port stream reconnect to getReadable level #2318main (same, on main). Release both so deployed apps gain reconnect.
  3. The coordinated server-side timeout change — only after the reconnect-capable client is released.

#1853 is independent and can land on its own schedule; it unblocks byte-stream reconnect as a future follow-up.

The placeholder cutoff (5.0.0-beta.3) predates several published betas
that do not contain the unframing reader. A producer probing a
beta.3..beta.13 target would wrongly write framed bytes that the
consumer cannot decode. Pin the capability to the next unpublished
beta and leave a TODO(release) marker to re-verify at release time.
TooTallNateand others added 4 commits June 11, 2026 13:00
5.0.0-beta.14 was published (Version Packages #2326) without this PR,
so beta.14 consumers have no unframing reader. Move the cutoff to the
next unpublished beta per the TODO(release) marker.
…test rename
- getByteFramingStream now rejects chunks over MAX_FRAME_SIZE at write
time (shared constant with the unframer), so an oversized chunk fails
where the error is actionable instead of producing a frame the
consumer refuses to decode
- Document the one-frame-per-stored-chunk invariant that future
byte-stream reconnect resume arithmetic depends on
- Rename the wire-level round-trip test to describe what it exercises
(the ref-dispatch claim it previously made is covered by the
dehydrateStepReturnValue round-trip tests above it)

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(AI) AI review: no blocking issues

Re-reviewed on the current tip (merge of main is resolved and the branch merges into main cleanly). Conflicts that existed earlier — from the #2318 reconnect work touching the same type: 'bytes' reviver branch — are resolved, and the earlier review nits are addressed (write-time MAX_FRAME_SIZE cap, one-frame-per-stored-chunk invariant doc, test rename, version cutoff bumped).

Validated:

  • Core suites pass locally — byte-stream-framing (17), capabilities (28), reconnecting-framed-stream (11), serialization (406); CI shows no failing checks.
  • Framer/unframer round-trip is correct under adversarial transport re-chunking, including the 4-byte length header split across reads and reads straddling a frame boundary (checked with an extra one-off). Symmetric size caps on both sides; flush errors on truncated trailing data; empty-chunk drop is intentional and avoids the [0,0,0,0] sniff collision.
  • Capability gating is backward-compatible: same-deployment → framed; cross-deployment probes healthCheck with a tight timeout and falls back to raw on miss/old version, so a run started against an older deployment emits raw bytes it can read. The ref carries framing only when capable; absent/raw refs read unchanged.
  • Post-merge reviver coexists correctly with reconnect: byte streams stay on the raw path (no reconnect), object streams use the reconnecting wrapper.

AI Review: Note (non-blocking, fine as follow-ups)

  • appendToBuffer in the unframer reallocates the pending buffer on every transport read (O(n²) with many tiny chunks). Only matters for pathological chunking on the opt-in framed path.
  • The cross-deployment probe adds a synchronous (≤2s on a cached miss) round-trip to start(); acceptable given the raw fallback, but worth caching per-deployment if cross-deployment start() becomes hot.

@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2380. Merge conflicts were resolved by AI — please review carefully. (backport job run)

Sign up for freeto 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

@TooTallNate@VaguelySerious