feat(streams): add WebSocket capability gate (#3764)

## Summary & Motivation

`WORKFLOW_STREAMS_TRANSPORT=ws` advertises client support for `workflow-stream-ws/v1` on stream writes; anything else keeps HTTP. It's a capability signal only — the server decides each upgrade, so there's no version or tenant-policy heuristic on the client side.

## Test Plan

Unit tests for the gate's accepted values; typecheck, build, and lint pass locally.
This commit is contained in:
Alex Langenfeld
2026-09-09 11:48:08 -05:00
committed by GitHub
parent 9a5660fbd6
commit fdeb642270
5 changed files with 62 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@workflow/world-vercel': patch
---
Add the default-off stream WebSocket capability gate.
@@ -281,6 +281,14 @@ Platform-provided values such as `VERCEL_DEPLOYMENT_ID`, `VERCEL_PROJECT_ID`, an
- Default: `1000` - Default: `1000`
- Maximum stream chunks written in one Vercel World request. Larger batches are split. - Maximum stream chunks written in one Vercel World request. Larger batches are split.
### `WORKFLOW_STREAMS_TRANSPORT`
- Factory option: none
- CLI flag: none
- Default: `http`
- Experimental stream-write transport capability. Set to exactly `ws` to advertise support for `workflow-stream-ws/v1` when it becomes available. The server authoritatively accepts or declines an upgrade; a decline uses HTTP directly. Stream reads remain HTTP and demand-driven.
- This is not tenant rollout policy or a package-version check. HTTP remains the compatibility path. `/websockets/v1` is independent of REST v2/v4 and persisted workflow `specVersion` values.
### `WORKFLOW_DISABLE_ANALYTICS_READS` ### `WORKFLOW_DISABLE_ANALYTICS_READS`
- Factory option: none - Factory option: none
+6
View File
@@ -200,6 +200,12 @@ Below the floor the deadline starts canceling requests that would have succeeded
Maximum stream chunks written in one Vercel World request. Larger batches are split across multiple requests. Default: `1000`. Minimum: `1`. Maximum stream chunks written in one Vercel World request. Larger batches are split across multiple requests. Default: `1000`. Minimum: `1`.
### `WORKFLOW_STREAMS_TRANSPORT`
Experimental stream-write transport capability. Default: `http`. Set `WORKFLOW_STREAMS_TRANSPORT=ws` to advertise support for `workflow-stream-ws/v1` when it becomes available. The server authoritatively accepts or declines every upgrade; a decline goes directly to HTTP. Reads remain HTTP and demand-driven.
This setting does not own tenant rollout policy and does not infer server support from package versions. HTTP remains the compatibility path. The protocol route (`/websockets/v1`) is versioned independently from REST v2/v4 and persisted workflow `specVersion` values. Drain/de-opt control semantics are a rollout prerequisite and are intentionally not part of this capability gate.
### `WORKFLOW_EVENTS_TRANSPORT` ### `WORKFLOW_EVENTS_TRANSPORT`
Workflow run events ship to the Vercel World over a WebSocket instead of one HTTP request each. Default: `ws`. Workflow run events ship to the Vercel World over a WebSocket instead of one HTTP request each. Default: `ws`.
@@ -0,0 +1,26 @@
import { afterEach, describe, expect, it } from 'vitest';
import { isWsStreamsTransportEnabled } from './ws-transport-enabled.js';
afterEach(() => {
delete process.env.WORKFLOW_STREAMS_TRANSPORT;
});
describe('isWsStreamsTransportEnabled', () => {
it.each([
[undefined, false],
['', false],
['http', false],
['HTTP', false],
['ws ', false],
['WS', false],
['ws', true],
])('advertises v1 only for the exact ws value: %j', (value, expected) => {
if (value === undefined) {
delete process.env.WORKFLOW_STREAMS_TRANSPORT;
} else {
process.env.WORKFLOW_STREAMS_TRANSPORT = value;
}
expect(isWsStreamsTransportEnabled()).toBe(expected);
});
});
@@ -53,3 +53,20 @@ export function isWsEventsTransportStrict(): boolean {
const raw = process.env.WORKFLOW_INTERNAL_EVENTS_TRANSPORT_STRICT; const raw = process.env.WORKFLOW_INTERNAL_EVENTS_TRANSPORT_STRICT;
return raw === '1' || raw === 'true'; return raw === '1' || raw === 'true';
} }
/**
* Advertise the experimental v1 stream-write protocol only when explicitly
* requested. This is a client capability signal, not an entitlement: the
* server authoritatively accepts or declines every upgrade, and a decline
* falls back directly to the HTTP stream writer.
*
* HTTP is the compatibility path and the default. Unlike the default-on events
* gate above, this opt-in is exact-match: a typo must fail toward HTTP rather
* than unexpectedly enabling an experimental transport. This deliberately has
* no package-version or tenant-policy heuristic; rollout policy belongs to the
* server. v1 is `/websockets/v1`, independently versioned from REST v2/v4 and
* persisted workflow spec versions.
*/
export function isWsStreamsTransportEnabled(): boolean {
return process.env.WORKFLOW_STREAMS_TRANSPORT === 'ws';
}