Files
Pranay Prakash 5f0b845211 RFC: compress serialized payload refs — zstd (gzip fallback), specVersion 5 (#2394)
* feat(core,world): gzip-compress serialized payloads behind specVersion 5

Add a composable 'gzip' format prefix layer to the serialization
pipeline (compress before encrypt: encr(gzip(devl))), cutting stored
payload bytes by ~70-87% on real-world-style workloads. Compression is
gated on run specVersion 5 (new SPEC_VERSION_SUPPORTS_COMPRESSION) and
on target-deployment capabilities for cross-deployment writes; payloads
under 1KB or that don't compress meaningfully are stored unchanged.
Reads dispatch on the format prefix so both compressed and uncompressed
data are always readable. WORKFLOW_DISABLE_COMPRESSION=1 disables
writes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(core): add CPU/perf compression benchmark + shared workloads

Split the compression benchmark into reproducible size and CPU scripts
sharing deterministic workloads (lib/workloads.mjs). The CPU benchmark
measures serialize/deserialize overhead per payload, total CPU across
thousands of events, and compares gzip levels/brotli/deflate. Documents
how to run the size, CPU, and end-to-end (bench.bench.ts) benchmarks
against local and Vercel in scripts/README.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(world-vercel): advertise specVersion 5 to enable compression on Vercel

Now that workflow-server declares spec-5 support (vercel/workflow-server#520),
bump the Vercel world's advertised specVersion from 4 to 5 so new Vercel runs
are stamped spec 5 and become eligible for gzip payload compression. Payloads
stay opaque to the server (compression is client-side); spec 5 is a superset of
spec 4, so initial run attributes still work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(core): emit OTel span attributes for compression impact

Track gzip payload compression on both the serialize (write) and
deserialize (read) paths via span attributes:
workflow.serialization.{operation,compressed,uncompressed_bytes,
stored_bytes,compression_ratio}. Sizes are measured at the compression
boundary (pre-encryption), so they reflect compression's effect rather
than the at-rest size.

The compression codec stays pure — compress/decompress optionally
populate a CompressionStats sink, threaded through CodecOptions to the
mode serializers and read by the dehydrate/hydrate wrappers, which set
attributes on the active span. Telemetry failures are swallowed so they
can never break the serialize/deserialize data path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(core,web-shared): prefer zstd compression codec (gzip fallback)

Switch the payload compression codec to zstd, which benchmarks 3–7×
faster than gzip at an equal-or-better ratio on representative workloads
(compression runs at every step boundary, so the write CPU is a per-step
tax). zstd uses node:zlib (>= 22.15); gzip via the portable
CompressionStream remains the fallback when zstd is unavailable, and
WORKFLOW_COMPRESSION_CODEC=gzip forces it. Reads dispatch on the format
prefix, so 'zstd' and 'gzip' payloads are both always decodable.

zstd is Node-only (Web CompressionStream has no zstd), so the browser
o11y read path registers a WASM-backed decoder (@tootallnate/zstd-wasm)
via a new registerZstdDecoder hook; node:zlib handles Node-side reads
(runtime replay, CLI, server o11y). A new workflow.serialization.codec
span attribute reports which codec applied. gzip and zstd read support
co-ship, so the existing specVersion-5 capability gate is unchanged.

Verified end-to-end: spec-5 runs store zstd-prefixed payloads on disk
and replay/complete correctly; the WASM decoder round-trips node:zlib
zstd output. Benchmarks updated to compare zstd vs gzip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 23:27:47 +00:00

84 lines
2.3 KiB
JSON

{
"name": "@workflow/web-shared",
"description": "Shared components for Workflow Observability UI",
"version": "5.0.0-beta.17",
"private": false,
"files": [
"dist",
"src"
],
"publishConfig": {
"access": "public"
},
"license": "Apache-2.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./components": {
"types": "./dist/components/index.d.ts",
"default": "./dist/components/index.js"
},
"./styles.css": "./dist/styles.css"
},
"typesVersions": {
"*": {
"components": [
"dist/components/index.d.ts"
]
}
},
"repository": {
"type": "git",
"url": "https://github.com/vercel/workflow.git",
"directory": "packages/web-shared"
},
"scripts": {
"build": "tsc && cp -r src/components/trace-viewer/*.css dist/components/trace-viewer/ && cp src/components/new-trace-viewer/components/*.module.css dist/components/new-trace-viewer/components/ && cp src/styles.css dist/styles.css",
"dev": "tsc --watch",
"clean": "tsc --build --clean && rm -r dist ||:",
"test": "vitest run",
"typecheck": "tsc --noEmit",
"lint": "biome check",
"format": "biome format --write"
},
"dependencies": {
"@radix-ui/react-slot": "1.1.1",
"@radix-ui/react-tooltip": "1.2.8",
"@tailwindcss/postcss": "4",
"@tootallnate/zstd-wasm": "0.0.2",
"@workflow/core": "workspace:*",
"@workflow/utils": "workspace:*",
"@workflow/world": "workspace:*",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"date-fns": "4.1.0",
"lucide-react": "0.575.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-inspector": "9.0.0",
"react-use-measure": "2.1.1",
"react-virtuoso": "4.18.1",
"shiki": "4.0.0",
"sonner": "2.0.7",
"streamdown": "2.3.0",
"tailwind-merge": "3.5.0",
"tailwindcss": "4"
},
"devDependencies": {
"@biomejs/biome": "catalog:",
"@types/node": "catalog:",
"@types/react": "19",
"@types/react-dom": "19",
"@workflow/errors": "workspace:*",
"@workflow/tsconfig": "workspace:*",
"ai": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
}
}