Skip to content

Add @polyvariant/smithy-ts-runtime: out-of-the-box transports - #2

Merged
kubukoz merged 2 commits into
mainfrom
runtime-transports
Aug 19, 2026
Merged

Add @polyvariant/smithy-ts-runtime: out-of-the-box transports#2
kubukoz merged 2 commits into
mainfrom
runtime-transports

Conversation

@kubukoz

@kubukoz kubukoz commented Aug 19, 2026

Copy link
Copy Markdown
Member

Closes the README TODO: the Transport / StreamTransport implementations had to be written by hand in every consuming project — a near-identical fetch wrapper each time, plus a hand-rolled ndjson read loop wherever streaming was used. This publishes that as @polyvariant/smithy-ts-runtime.

What ships

fetchTransport implements both halves of the generated contract from one object:

const transport = fetchTransport({ baseUrl: '/api' })
const directory = new DirectoryClient(transport)
const feed = new FeedClient(transport, transport)   // streaming ops
  • Unary — baseUrl joining, query building (dropping undefined), lowercased response headers, empty/non-JSON body tolerance.
  • Streaming — ndjson and binary framing in both directions, lazy in both (one line deframed per pull, so a long-lived stream never buffers). A streamed request body uses duplex: 'half' where the platform supports it and buffers where it doesn't.
  • Non-2xx is returned, not thrown — the generated client needs the status and body to dispatch declared errors: [...]. 401 is the exception; pass your generated error class via unauthenticated to keep existing instanceof checks working.
  • Middlewarechain / around / mapRequest / withHeaders / tap, plus interceptorStack for handlers registered and removed after construction (what a React effect needs).
  • Framing primitives (encodeNdjson, decodeNdjson, …) are exported for transports this package doesn't ship.

The package is zero-dependency and ESM-only.

Type compatibility is checked, not assumed

The library declares its own structural copies of the transport types and imports nothing from generated code, so generated.ts stays self-contained. typecheck/src/runtimeUsage.ts drives the real generated clients with the library's transport, so if the codegen changes the contract and the library isn't updated, it stops compiling.

Build

TypeScript moves into a pnpm workspace (runtime/ + typecheck/). nix flake check now builds and unit-tests the library (39 node:test cases covering framing round-trips, chunk-split multi-byte characters, laziness, the transport against a fetch double, and middleware ordering) before typechecking the generated sample. pnpm check runs the same without nix.

Publishing

.github/workflows/npm-publish.yml publishes on a v* tag, so one tag ships both halves at the same version. The tag is the only source of version truth — runtime/package.json keeps a placeholder 0.0.0 the workflow overwrites, so there's no version to bump by hand. It runs the full check before publishing, and uses npm provenance.

Hand-written rather than folded into ci.yml, since that file is generated by sbt-typelevel and CI fails if it drifts.

Needs an NPM_TOKEN secret with publish rights on the @polyvariant scope before the first tag.

Verification

nix flake check passes: library build + typecheck + 39 tests, then the generated-sample typecheck including the compatibility file. Package packs cleanly with no @types/node leakage into the published .d.ts.

🤖 Generated with Claude Code

The codegen emits transport *interfaces* and leaves the implementation to
consumers, so every consuming project ends up hand-rolling the same fetch
wrapper, and the same ndjson read loop wherever streaming is used. This
publishes that as a library.

`fetchTransport` implements both halves of the contract: unary requests,
plus ndjson and binary framing in both directions for streaming ops. It
returns non-2xx as a response rather than throwing, since the generated
client needs the status and body to dispatch declared errors; 401 is the
exception, and the generated error class can be passed in so existing
`instanceof` checks keep working.

Middleware (`chain` / `around` / `tap` / `withHeaders`) covers cross-cutting
concerns like tracing, auth headers and error reporting, and
`interceptorStack` covers the add-and-remove-later case a React effect
needs. The per-call options blob the codegen already threads through
untouched is what carries framework-specific knobs, so the package needs no
HTTP client dependency of its own and stays dependency-free.

The library imports nothing from generated code: it declares structural
copies of the transport types. typecheck/src/runtimeUsage.ts compiles the
two against each other so that pairing can't drift silently.

TypeScript moves into a pnpm workspace (runtime/ + typecheck/); nix flake
check now builds and unit-tests the library before typechecking the sample.
A v* tag publishes the package to npm alongside the JVM artifacts, with the
tag as the only source of version truth.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@kubukoz
kubukoz force-pushed the runtime-transports branch from da47ba4 to d5424dd Compare August 19, 2026 14:40
OIDC means no NPM_TOKEN secret to create, store or rotate. The publish
step moves from pnpm to npm because the OIDC exchange is implemented in
the npm CLI; everything before it still runs under pnpm.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@kubukoz
kubukoz merged commit 396d57a into main Aug 19, 2026
10 checks passed
Sign up for free to 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.

1 participant