diff --git a/.changeset/stream-ws-capability-gate.md b/.changeset/stream-ws-capability-gate.md new file mode 100644 index 000000000..4e05d6a9c --- /dev/null +++ b/.changeset/stream-ws-capability-gate.md @@ -0,0 +1,5 @@ +--- +'@workflow/world-vercel': patch +--- + +Add the default-off stream WebSocket capability gate. diff --git a/docs/content/docs/v5/configuration/worlds.mdx b/docs/content/docs/v5/configuration/worlds.mdx index ed49ee51f..60d2c8736 100644 --- a/docs/content/docs/v5/configuration/worlds.mdx +++ b/docs/content/docs/v5/configuration/worlds.mdx @@ -281,6 +281,14 @@ Platform-provided values such as `VERCEL_DEPLOYMENT_ID`, `VERCEL_PROJECT_ID`, an - Default: `1000` - 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` - Factory option: none diff --git a/docs/content/worlds/v5/vercel.mdx b/docs/content/worlds/v5/vercel.mdx index 29c0ec026..65c85f665 100644 --- a/docs/content/worlds/v5/vercel.mdx +++ b/docs/content/worlds/v5/vercel.mdx @@ -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`. +### `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 run events ship to the Vercel World over a WebSocket instead of one HTTP request each. Default: `ws`. diff --git a/packages/world-vercel/src/ws-streams-transport-enabled.test.ts b/packages/world-vercel/src/ws-streams-transport-enabled.test.ts new file mode 100644 index 000000000..18952f797 --- /dev/null +++ b/packages/world-vercel/src/ws-streams-transport-enabled.test.ts @@ -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); + }); +}); diff --git a/packages/world-vercel/src/ws-transport-enabled.ts b/packages/world-vercel/src/ws-transport-enabled.ts index 40ac9d4d2..d3dad89b6 100644 --- a/packages/world-vercel/src/ws-transport-enabled.ts +++ b/packages/world-vercel/src/ws-transport-enabled.ts @@ -53,3 +53,20 @@ export function isWsEventsTransportStrict(): boolean { const raw = process.env.WORKFLOW_INTERNAL_EVENTS_TRANSPORT_STRICT; 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'; +}