Skip to content

feat: TypeScript RPC client#5

Closed
0xpolarzero wants to merge 6 commits into
rpc-structured-commandfrom
feature/typescript-client
Closed

feat: TypeScript RPC client#5
0xpolarzero wants to merge 6 commits into
rpc-structured-commandfrom
feature/typescript-client

Conversation

@0xpolarzero
Copy link
Copy Markdown
Owner

@0xpolarzero 0xpolarzero commented May 22, 2026

Warning

This PR is part of a stacked PR chain.

Upstream merge PRs:
wevm/incur#144 ->
wevm/incur#145 ->
wevm/incur#143 ->
wevm/incur#147

Correctly based review stack in this fork:
#3 ->
#4 ->
#5 ->
#2

Overview

Adds a typed TypeScript client for incur RPC servers, including async-generator streaming support for generated clients and the structured RPC route.

OpenAPI-mounted fetch gateways are supported as typed client commands because the OpenAPI spec gives incur operation names, input shapes, and response types. Raw fetch gateways are not typed client commands because they accept arbitrary HTTP requests, not a known command schema. Calling a raw fetch gateway through structured RPC returns FETCH_GATEWAY_UNSUPPORTED.

Maintainer Note: Client Error Compatibility

ClientError is now exported, but its data and error fields intentionally stay broad at the class boundary. Existing client failures can carry non-ideal shapes: malformed JSON leaves raw response text in data, malformed envelopes leave arbitrary payloads in data, and failed RPC envelopes may expose whatever the server returned in error.

That compatibility slop is deliberate for this PR. We can remove it later by making ClientError only expose strongly typed RPC envelopes/errors, but that would be a breaking change for callers that already inspect the current thrown shape.

Maintainer Note: Stream Detection

Streaming typegen detection is based on actual async *run handlers. A wrapper like run() { return stream() } is not emitted as stream: true.

API

import { createClient } from 'incur'

// Generated by `incur typegen`; this gives the client its command names,
// input schemas, and output types without a runtime import.
import type { Commands } from './incur.generated.js'

// `baseUrl` points at the incur server exposing `/_incur/rpc`.
const client = createClient<Commands>({ baseUrl: 'https://api.example.com' })

const result = await client('project deploy')({
  // `args` and `options` are typed from the command schemas.
  args: { id: 'p1' },
  options: { dryRun: true },
})
//    ^? project deploy output

// Commands declared with `async *run` return typed async iterables.
const stream = await client('project logs')({
  args: { id: 'p1' },
})
//    ^? AsyncIterable<{ line: string }>

for await (const chunk of stream) {
  //             ^? { line: string }
  console.log(chunk.line)
}

Changes

  • Add createClient({ baseUrl, fetch? }).
  • Make createClient generic over a generated command map.
  • Extend typegen to export Commands while preserving Register.commands augmentation.
  • Extend typegen to emit command output types for client return values.
  • Generate typed command entries for OpenAPI-mounted operations.
  • Make OpenAPI-mounted command generation synchronous during .command(..., { fetch, openapi }) registration, so Typegen.fromCli(cli) and incur gen see those operations without first serving the CLI. This moves local spec dereferencing/schema construction to CLI construction time for OpenAPI mounts.
  • Skip aliases and raw fetch gateways in generated client-callable command entries.
  • Detect async generator command handlers and emit stream: true metadata in typegen.
  • Type streaming client calls as async iterables while preserving existing non-streaming call types.
  • Add application/x-ndjson handling for RPC stream responses.
  • Preserve returned CTA/error records from async generator RPC streams.
  • Cancel the response body when consumers stop reading a stream early.

Tests

  • Adds runtime tests for createClient.
  • Adds client tests for NDJSON chunks, stream errors, malformed streams, missing completion records, and cancellation.
  • Adds type tests for command-name, input, output, and streaming return inference.
  • Adds OpenAPI-mounted command map and typegen coverage.
  • Adds generated client round-trip coverage.
  • Adds typegen snapshot coverage for generated stream metadata.
  • Adds RPC route coverage for async generator streams preserving returned CTA/error records.
  • Updates typegen snapshots for generated command map declarations.

@0xpolarzero 0xpolarzero force-pushed the feature/typescript-client branch 2 times, most recently from 87299d9 to 2bd19e0 Compare May 22, 2026 20:49
@0xpolarzero 0xpolarzero force-pushed the rpc-structured-command branch from e49c1d1 to 01d974e Compare May 22, 2026 20:49
@0xpolarzero 0xpolarzero force-pushed the feature/typescript-client branch from 2bd19e0 to 80462ea Compare May 22, 2026 20:55
@0xpolarzero 0xpolarzero changed the title feat(3): add generated TypeScript client feat: TypeScript RPC client May 24, 2026
@0xpolarzero 0xpolarzero deleted the feature/typescript-client branch May 25, 2026 16:36
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