Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

88 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

capnweb-go

Warning

Early-stage and under active development. The API and wire behavior may change between releases — pin a version and expect breaking changes before v1.

Go implementation of Cloudflare's Cap'n Web RPC protocol — wire-compatible with the TypeScript reference, validated by a cross-language test suite against capnweb 0.8.0.

No code generation: export any Go struct with methods as an RPC target and call remote methods through typed stubs.

Protocol support

Complete implementation of the capnweb 0.8.0 wire protocol — all eight message types and every value type in the reference serialize.ts, validated against the TypeScript implementation in both directions.

Transports

Original Ported Go API
WebSocket (newWebSocketRpcSession) Yes WSDial / WSAccept
HTTP batch (newHttpBatchRpcSession) Yes BatchHandler / BatchClient
MessagePort (newMessagePortRpcSession) No browser/Worker-only; no Go analog

Custom framing is supported via the pluggable Transport interface.

RPC features (all supported)

  • Bidirectional RPC — either side can export objects and call the other
  • Promise pipelining — chain dependent calls in one round trip
  • Pass-by-reference stubs with automatic reference counting
  • Server-side .map() (remap), including captures and nested instructions
  • Streaming — ReadableStream / WritableStream over one connection

Serializable value types (all supported)

  • Primitives (string / number / boolean / null), undefined
  • Infinity / -Infinity / NaN, BigInt, Date (including invalid)
  • Uint8Array (bytes), Blob
  • Error — with own enumerable properties, cause, and AggregateError errors
  • Headers, Request, Response
  • Arrays (escaped) and objects, recursively
  • Capability stubs (import / export / promise / pipeline) and streams (readable / writable)

Not ported

  • The MessagePort transport (browser/Worker-only).
  • capnweb-validate, the reference's build-time TypeScript type-validator, has no Go analog. Wire conformance here is enforced instead by golden wire-vectors and the cross-language interop suite.

Install

go get go.flaticols.dev/capnweb-go

Examples

Go Server (WebSocket)

package main

import (
	"context"
	"net/http"

	capnweb "go.flaticols.dev/capnweb-go"
)

type Greeter struct {
	capnweb.RpcTargetBase
}

func (g *Greeter) Greet(_ context.Context, name string) (string, error) {
	return "Hello, " + name + "!", nil
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
		tr, _ := capnweb.WSAccept(w, r, &capnweb.WSAcceptOptions{Origins: []string{"*"}})
		sess := capnweb.NewSession(tr, &Greeter{})
		sess.Run(r.Context())
	})
	http.ListenAndServe(":8080", mux)
}

Go Server (HTTP Batch)

mux.Handle("/rpc", capnweb.BatchHandler(&Greeter{}))

Each HTTP POST carries a batch of NDJSON messages — all calls and their responses fit in one round trip.

Go Client (WebSocket)

ctx := context.Background()
tr, _ := capnweb.WSDial(ctx, "ws://localhost:8080/ws", nil)

client := capnweb.NewSession(tr, nil)
go client.Run(ctx)
defer client.Close()

main := client.Main()
result, _ := capnweb.Call[string](ctx, main, "Greet", "World")
// result == "Hello, World!"

Go Client (HTTP Batch)

bc := &capnweb.BatchClient{URL: "http://localhost:8080/rpc"}
msgs, _ := bc.Do(ctx, []capnweb.Message{
	capnweb.PushMsg{Expr: json.RawMessage(`["import",0,["Greet"],["World"]]`)},
	capnweb.PullMsg{ImportID: 1},
})

TypeScript Client (WebSocket)

import { newWebSocketRpcSession } from "capnweb";

const stub = newWebSocketRpcSession("ws://localhost:8080/ws");
const result = await stub.Greet("World");
// result === "Hello, World!"

TypeScript Server + Go Client

// server.ts
import { RpcTarget, newWebSocketRpcSession } from "capnweb";

class Greeter extends RpcTarget {
  greet(name: string) { return `Hello, ${name}!`; }
}

// ... set up WebSocketServer, call newWebSocketRpcSession(ws, new Greeter())
// client.go
tr, _ := capnweb.WSDial(ctx, "ws://localhost:3000", nil)
client := capnweb.NewSession(tr, nil)
go client.Run(ctx)

main := client.Main()
result, _ := capnweb.Call[string](ctx, main, "greet", "World")

Pass-by-Reference

Methods that return an RpcTarget are automatically passed by reference:

type Calculator struct{ capnweb.RpcTargetBase }
func (c *Calculator) Add(_ context.Context, a, b float64) (float64, error) {
	return a + b, nil
}

type MathService struct{ capnweb.RpcTargetBase }
func (s *MathService) GetCalculator(_ context.Context) (*Calculator, error) {
	return &Calculator{}, nil
}
// Client
main := client.Main()
calc, _ := capnweb.Call[*capnweb.Stub](ctx, main, "GetCalculator")
result, _ := capnweb.Call[float64](ctx, calc, "Add", 3.0, 4.0)
defer calc.Release(ctx)

Promise Pipelining

Chain calls without waiting for intermediate results:

main := client.Main()
calc, _ := main.Pipeline(ctx, "GetCalculator")   // push only, no wait
result, _ := capnweb.Call[float64](ctx, calc, "Add", 3.0, 4.0) // push + pull
defer calc.Release(ctx)

Streaming

// Client: create pipe, write chunks
writer, readable, _ := client.CreatePipe(ctx)
go func() {
	main := client.Main()
	capnweb.Call[string](ctx, main, "Collect", readable)
}()
writer.Write(ctx, "chunk1")
writer.Write(ctx, "chunk2")
writer.Close(ctx)

// Server: read stream
func (s *Service) Collect(_ context.Context, reader *capnweb.StreamReader) (string, error) {
	var buf strings.Builder
	for {
		chunk, err := reader.Read(context.Background())
		if err == io.EOF { break }
		buf.WriteString(chunk.(string))
	}
	return buf.String(), nil
}

Status

See the issue tracker for known gaps and follow-ups.

Go Reference CI Release

License

MIT

About

Go implementation of Cloudflare's Cap'n Web RPC protocol

Topics

Resources

Stars

Watchers

Forks

Releases

Contributors

Languages