From c4ec7b9a91e96156b223b781a322074db5bf72c9 Mon Sep 17 00:00:00 2001 From: slinkydeveloper Date: Wed, 12 Aug 2026 10:33:38 +0200 Subject: [PATCH 1/3] feat(tunnel-native): embedded relay-tunnel receiver via napi-rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New, separate package `@restatedev/restate-sdk-tunnel-native` that serves a Restate SDK deployment as a relay *receiver* using the embedded native engine (restate-sdk-shared-core, `tunnel` feature) via napi-rs — the same Rust engine the Java SDK drives over FFM. The engine dials the relay on its own tokio runtime and bridges each forwarded request over a loopback socket into a local node:http2 server, so SDK request dispatch — and therefore BOTH the codegen SDK and the default promise API — is untouched. The napi boundary is control-plane only (start/status/stop); no per-request data crosses it. Layout (packages/libs/restate-sdk-tunnel-native): * Cargo.toml + build.rs + src/lib.rs: a napi crate exposing a `#[napi]` RelayTunnel class (start(configJson) / status() / stop()) over the engine's relay::{Config, Engine, Handle}. Depends on the shared-core `relay` branch with features=["tunnel"] (same git dep as the sdk-java cdylib). * src/index.ts: `serveTunnel({ services, relay })` — wires at the shared createEndpointHandler({ services }) layer (works with both programming models), boots a loopback-private node:http2 server on 127.0.0.1:0, and points the native engine at that port. Returns { localPort, status, stop }. * Dual ESM/CJS via tsdown; the napi `.cjs` loader + `.node` stay external and gitignored (built locally via `napi build` — the multi-platform prebuild matrix + publishing is deferred). napi-rs is set up from scratch here (the repo previously only had wasm-pack for the VM). Verified: the addon builds + loads; the wrapper type-checks against the real SDK types and builds; a full round-trip smoke test (fake relay h2 client -> native engine -> serveTunnel's http2 server running a real promise-API service -> forwarded /health -> 200 -> back, /whoami confirmed) passes. Design/rationale: development/relay-receiver-napi-plan.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- development/relay-receiver-napi-plan.md | 123 +++ .../libs/restate-sdk-tunnel-native/.gitignore | 12 + .../libs/restate-sdk-tunnel-native/Cargo.lock | 816 ++++++++++++++++++ .../libs/restate-sdk-tunnel-native/Cargo.toml | 26 + .../libs/restate-sdk-tunnel-native/README.md | 63 ++ .../libs/restate-sdk-tunnel-native/build.rs | 3 + .../restate-sdk-tunnel-native/package.json | 48 ++ .../restate-sdk-tunnel-native/src/index.ts | 162 ++++ .../libs/restate-sdk-tunnel-native/src/lib.rs | 54 ++ .../tsconfig.build.json | 8 + .../restate-sdk-tunnel-native/tsconfig.json | 7 + .../tsdown.config.ts | 15 + pnpm-lock.yaml | 16 + 13 files changed, 1353 insertions(+) create mode 100644 development/relay-receiver-napi-plan.md create mode 100644 packages/libs/restate-sdk-tunnel-native/.gitignore create mode 100644 packages/libs/restate-sdk-tunnel-native/Cargo.lock create mode 100644 packages/libs/restate-sdk-tunnel-native/Cargo.toml create mode 100644 packages/libs/restate-sdk-tunnel-native/README.md create mode 100644 packages/libs/restate-sdk-tunnel-native/build.rs create mode 100644 packages/libs/restate-sdk-tunnel-native/package.json create mode 100644 packages/libs/restate-sdk-tunnel-native/src/index.ts create mode 100644 packages/libs/restate-sdk-tunnel-native/src/lib.rs create mode 100644 packages/libs/restate-sdk-tunnel-native/tsconfig.build.json create mode 100644 packages/libs/restate-sdk-tunnel-native/tsconfig.json create mode 100644 packages/libs/restate-sdk-tunnel-native/tsdown.config.ts diff --git a/development/relay-receiver-napi-plan.md b/development/relay-receiver-napi-plan.md new file mode 100644 index 00000000..38e8ad31 --- /dev/null +++ b/development/relay-receiver-napi-plan.md @@ -0,0 +1,123 @@ +# Embedded native relay receiver (napi-rs) — implementation plan + +**Status:** proposed / in progress. **Date:** 2026-08-12. +**Scope:** a **new, separate** TypeScript package that embeds the relay receiver +as a **napi-rs** native addon, reusing the same Rust engine that the Java SDK +uses (`restate-sdk-shared-core`, branch `relay`, `tunnel` feature — +`relay::{Config, Engine, Handle}`). The FFI boundary is control-plane only; every +forwarded request rides a **loopback socket** into the SDK's own node:http2 +server, so request dispatch — and therefore **both the codegen SDK and the +default promise API** — is untouched. + +This is the TS sibling of `sdk-java`'s `RelayTunnel` / `RestateRelayServer` +(FFM) and of `sdk-shared-core/docs/relay-receiver-in-shared-core.md` (the engine). + +## Why a new module (not the existing `restate-sdk-tunnel`) + +`packages/libs/restate-sdk-tunnel` is a **pure-JS** reimplementation of the +receiver protocol targeting Restate Cloud's `/_/start-tunnel`. This work is the +**native** path (single-sourced Rust protocol via napi) targeting the generic +relay's `/whoami` mode. They are complementary; reconciling them is deferred. +We build a fresh package and leave `restate-sdk-tunnel` alone. + +## The two layers + +``` +packages/libs/restate-sdk-tunnel-native/ @restatedev/restate-sdk-tunnel-native +├── Cargo.toml # napi crate: napi + napi-derive + restate-sdk-shared-core(relay,tunnel) + serde_json +├── build.rs # napi_build::setup() +├── src/lib.rs # #[napi] RelayTunnel: start(configJson)->self, status()->string, stop() +├── index.js / index.d.ts # GENERATED by `napi build` (loads the .node); gitignored +├── *.node # GENERATED host artifact; gitignored (prebuild matrix deferred) +├── restate/ # the TS wrapper (public API), bundled by tsdown like every lib +│ └── index.ts # serveTunnel({ services, relay, env, tunnel, apiKey, ... }) +├── package.json # napi config + build scripts + exports +└── README.md +``` + +### 1. napi crate (`src/lib.rs`) — the thin binding + +Mirrors the Java C ABI, but idiomatic napi. A `#[napi]` class over the engine: + +```rust +#[napi] +pub struct RelayTunnel { handle: Option } + +#[napi] +impl RelayTunnel { + #[napi(factory)] + pub fn start(config_json: String) -> napi::Result { … Engine::start … } + #[napi] pub fn status(&self) -> String { … handle.status_json() … } + #[napi] pub fn stop(&mut self) { … handle.stop() … } +} +``` + +- Config crosses as a JSON string (the same `relay::Config` serde shape the Java + side builds); `status()` returns the `{"running","last_error"}` JSON. +- `Handle`'s `Drop` also stops the engine, so a GC'd `RelayTunnel` cleans up; the + explicit `stop()` (idempotent) is for deterministic shutdown. +- No `ThreadsafeFunction`, no per-request marshalling — data rides the loopback + socket, exactly like the FFM path. + +### 2. TS wrapper (`restate/index.ts`) — the node:http2 wiring + +The equivalent of Java's `RestateRelayServer.listen`. Works with **both** the +codegen SDK and the promise API because it wires at the shared `RestateEndpoint` +layer (what `endpoint().bind(...)` accepts from either model): + +```ts +import http2 from "node:http2"; +import { endpoint } from "@restatedev/restate-sdk/node"; +import { RelayTunnel } from "../index.js"; // the napi binding + +export async function serveTunnel(opts: ServeTunnelOptions): Promise { + const ep = endpoint(); + for (const s of opts.services) ep.bind(s); + const server = http2.createServer(ep.http2Handler({ bidirectional: true })); + const port: number = await new Promise((res, rej) => { + server.once("error", rej); + server.listen(0, "127.0.0.1", () => res((server.address() as any).port)); + }); + const tunnel = RelayTunnel.start(JSON.stringify({ + relay_addr: opts.relayAddress, env: opts.env, tunnel: opts.tunnel, + api_key: opts.apiKey, local_port: port, + connections: opts.connections, instance_id: opts.instanceId, + })); + // handle: status()/stop() + close the server; register on process signals. +} +``` + +- Binds `127.0.0.1` only (loopback) via a manual `http2.createServer` + + `listen(0, "127.0.0.1")` — `endpoint.listen()` binds all interfaces, so we use + `http2Handler()` directly to keep the server loopback-private. +- `bidirectional: true` → BIDI_STREAM, matching what the relay forwards. + +## napi-rs setup (from scratch — no existing precedent here) + +The repo builds Rust→TS via `wasm-pack` (the VM `sdk-shared-core-wasm-bindings`); +there is **no** napi-rs yet. This package adds: + +- `@napi-rs/cli` as a devDependency; `napi build --platform --release` produces + the host `.node` + `index.{js,d.ts}` loader. +- `napi`, `napi-derive`, `napi-build` crates in `Cargo.toml`. +- **For now:** host-target build only, `.node` gitignored (like wasm `pkg/`). + The multi-platform **prebuild matrix + optionalDependencies + CI** + (`napi create-npm-dirs` / `napi artifacts`) is **deferred** — that is the "deal + with it later" packaging problem. + +## Verification (v1) + +1. `napi build` produces a loadable `.node`; `RelayTunnel.start/status/stop` are + callable from Node. +2. The wrapper compiles (`tsc --noEmit`) + type-checks against `restate-sdk/node`. +3. A round-trip smoke test against a fake relay (mirrors shared-core's + `tests/relay_loopback.rs`): dial-in, `/whoami`, forwarded request → + loopback → node:http2 server → back. (The engine round-trip is already proven + in shared-core; here we prove the napi binding + node wiring.) + +## Deferred (documented, not in v1) + +- Multi-platform prebuild matrix + CI + `optionalDependencies` publishing. +- Restate Cloud `/_/start-tunnel` mode (the engine is `/whoami`-only, same as Java). +- Reconciling with the pure-JS `restate-sdk-tunnel` package. +- Swapping the shared-core git dep for a crates.io release once `tunnel` ships. diff --git a/packages/libs/restate-sdk-tunnel-native/.gitignore b/packages/libs/restate-sdk-tunnel-native/.gitignore new file mode 100644 index 00000000..69c121fb --- /dev/null +++ b/packages/libs/restate-sdk-tunnel-native/.gitignore @@ -0,0 +1,12 @@ +# Rust build output +/target + +# napi-rs generated host artifacts (built locally via `napi build`; the +# multi-platform prebuild matrix + publishing is deferred — see the plan doc). +*.node +/index.cjs +/index.d.cts + +# tsdown output +/dist +*.tsbuildinfo diff --git a/packages/libs/restate-sdk-tunnel-native/Cargo.lock b/packages/libs/restate-sdk-tunnel-native/Cargo.lock new file mode 100644 index 00000000..f5a764f0 --- /dev/null +++ b/packages/libs/restate-sdk-tunnel-native/Cargo.lock @@ -0,0 +1,816 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + +[[package]] +name = "cc" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "napi" +version = "2.16.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55740c4ae1d8696773c78fdafd5d0e5fe9bc9f1b071c7ba493ba5c413a9184f3" +dependencies = [ + "bitflags", + "ctor", + "napi-derive", + "napi-sys", + "once_cell", +] + +[[package]] +name = "napi-build" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60fdf9b392c50e7c4170fa633bd909490ed7835cea4c046776d1a4dd8d2ae0ab" + +[[package]] +name = "napi-derive" +version = "2.16.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c" +dependencies = [ + "cfg-if", + "convert_case", + "napi-derive-backend", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "napi-derive-backend" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf" +dependencies = [ + "convert_case", + "once_cell", + "proc-macro2", + "quote", + "regex", + "semver", + "syn 2.0.119", +] + +[[package]] +name = "napi-sys" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3" +dependencies = [ + "libloading", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "restate-sdk-shared-core" +version = "7.0.2" +source = "git+https://github.com/restatedev/sdk-shared-core.git?branch=relay#7789348906c6ae7d2ed6719043c4a2ff4b99b9d0" +dependencies = [ + "async-trait", + "base64", + "bytes", + "bytes-utils", + "h2", + "http", + "pastey", + "prost", + "rustls-pki-types", + "serde", + "serde_json", + "strum", + "thiserror", + "tokio", + "tokio-rustls", + "tracing", +] + +[[package]] +name = "restate-sdk-tunnel-native" +version = "0.0.0" +dependencies = [ + "napi", + "napi-build", + "napi-derive", + "restate-sdk-shared-core", + "serde_json", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/packages/libs/restate-sdk-tunnel-native/Cargo.toml b/packages/libs/restate-sdk-tunnel-native/Cargo.toml new file mode 100644 index 00000000..a43ed29f --- /dev/null +++ b/packages/libs/restate-sdk-tunnel-native/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "restate-sdk-tunnel-native" +version = "0.0.0" +edition = "2021" +publish = false +description = "napi-rs binding for the embedded relay-tunnel receiver (restate-sdk-shared-core relay engine)" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +# N-API bindings. `napi4` is a safe N-API version baseline (Node 10+); we only +# need plain sync class methods, so no tokio/async-runtime feature is enabled — +# the relay engine owns its own tokio runtime behind the FFI boundary. +napi = { version = "2", default-features = false, features = ["napi4"] } +napi-derive = "2" +# The relay engine (same branch + feature the Java FFM cdylib consumes). Swap to +# a crates.io version once the `tunnel` feature ships in a release. +restate-sdk-shared-core = { git = "https://github.com/restatedev/sdk-shared-core.git", branch = "relay", features = ["tunnel"] } +serde_json = "1" + +[build-dependencies] +napi-build = "2" + +[profile.release] +lto = true diff --git a/packages/libs/restate-sdk-tunnel-native/README.md b/packages/libs/restate-sdk-tunnel-native/README.md new file mode 100644 index 00000000..de0657d2 --- /dev/null +++ b/packages/libs/restate-sdk-tunnel-native/README.md @@ -0,0 +1,63 @@ +# @restatedev/restate-sdk-tunnel-native + +Serve a Restate SDK deployment as a **relay receiver** using the embedded +**native** engine (`restate-sdk-shared-core`, `tunnel` feature) via napi-rs — +the same Rust engine the Java SDK uses over FFM. + +The engine dials the relay on its own tokio runtime and bridges each forwarded +request over a **loopback socket** into a local `node:http2` server, so SDK +request dispatch — and therefore **both the codegen SDK and the default promise +API** — is untouched. The napi boundary is control-plane only +(`start`/`status`/`stop`); no per-request data crosses it. + +```ts +import { serveTunnel } from "@restatedev/restate-sdk-tunnel-native"; +import * as restate from "@restatedev/restate-sdk"; + +const greeter = restate.service({ + name: "greeter", + handlers: { greet: async () => "hi" }, +}); + +const tunnel = await serveTunnel({ + services: [greeter], + relay: { + address: "relay.example:8080", + env: "myenv", + tunnel: "mytunnel", + apiKey: process.env.RELAY_API_KEY!, + }, +}); + +// tunnel.status() -> { running, lastError } +// await tunnel.stop() on shutdown +``` + +## Status — early / experimental + +This is a fresh module (see `development/relay-receiver-napi-plan.md`). It is +**not** the pure-JS `@restatedev/restate-sdk-tunnel` package; the two are +complementary and will be reconciled later. + +- **Node only**, and needs a native addon built for the host platform. +- v1 targets the relay's `/whoami` mode (the generic relay), matching the shared + engine. Restate Cloud's `/_/start-tunnel` mode is not wired here yet. + +## Building the native addon + +The `.node` addon is **not** committed. Build it for the host platform with: + +```sh +pnpm --filter @restatedev/restate-sdk-tunnel-native run build:native # release +pnpm --filter @restatedev/restate-sdk-tunnel-native run _build # tsc + tsdown wrapper +``` + +`build:native` runs `napi build` and pins the shared-core engine to its `relay` +branch (git dependency in `Cargo.toml`) with the `tunnel` feature. + +## Deferred (packaging "later") + +- The multi-platform prebuild matrix + `optionalDependencies` publishing + CI + cross-compile (the standard napi-rs release flow). +- Swapping the shared-core git dependency for a crates.io release once the + `tunnel` feature ships. diff --git a/packages/libs/restate-sdk-tunnel-native/build.rs b/packages/libs/restate-sdk-tunnel-native/build.rs new file mode 100644 index 00000000..0f1b0100 --- /dev/null +++ b/packages/libs/restate-sdk-tunnel-native/build.rs @@ -0,0 +1,3 @@ +fn main() { + napi_build::setup(); +} diff --git a/packages/libs/restate-sdk-tunnel-native/package.json b/packages/libs/restate-sdk-tunnel-native/package.json new file mode 100644 index 00000000..93298c54 --- /dev/null +++ b/packages/libs/restate-sdk-tunnel-native/package.json @@ -0,0 +1,48 @@ +{ + "name": "@restatedev/restate-sdk-tunnel-native", + "version": "0.0.0", + "private": true, + "description": "Embedded native relay-tunnel receiver (napi-rs) for the Restate TypeScript SDK", + "author": "Restate Developers", + "email": "code@restate.dev", + "license": "MIT", + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.cts", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./package.json": "./package.json" + }, + "files": [ + "dist", + "index.cjs", + "index.d.cts", + "restate-sdk-tunnel-native.*.node", + "README.md" + ], + "napi": { + "name": "restate-sdk-tunnel-native" + }, + "scripts": { + "build:native": "napi build --platform --release --js index.cjs --dts index.d.cts", + "build:native:debug": "napi build --platform --js index.cjs --dts index.d.cts", + "_build": "tsc --noEmit --project tsconfig.build.json && tsdown", + "build": "turbo run _build --filter={.}...", + "check:types": "turbo run _check:types --filter={.}...", + "_check:types": "tsc --noEmit --project tsconfig.build.json", + "clean": "rm -rf dist *.tsbuildinfo .turbo", + "lint": "eslint ." + }, + "dependencies": {}, + "devDependencies": { + "@napi-rs/cli": "^2.18.4", + "@restatedev/restate-sdk": "workspace:*" + }, + "peerDependencies": { + "@restatedev/restate-sdk": "^1.15.0" + } +} diff --git a/packages/libs/restate-sdk-tunnel-native/src/index.ts b/packages/libs/restate-sdk-tunnel-native/src/index.ts new file mode 100644 index 00000000..2c971ef9 --- /dev/null +++ b/packages/libs/restate-sdk-tunnel-native/src/index.ts @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2023-2026 - Restate Software, Inc., Restate GmbH + * + * This file is part of the Restate SDK for Node.js/TypeScript, + * which is released under the MIT license. + * + * You can find a copy of the license in file LICENSE in the root + * directory of this repository or package, or at + * https://github.com/restatedev/sdk-typescript/blob/main/LICENSE + */ + +// @restatedev/restate-sdk-tunnel-native +// +// Serve a Restate SDK deployment as a relay *receiver*, using the embedded +// native engine (restate-sdk-shared-core, `tunnel` feature) via napi. The +// engine dials the relay on its own tokio runtime and bridges each forwarded +// request over a loopback socket into a local node:http2 server, so SDK request +// dispatch — and therefore BOTH the codegen SDK and the promise API — is +// untouched. The napi boundary is control-plane only (start/status/stop). +// +// import { serveTunnel } from "@restatedev/restate-sdk-tunnel-native"; +// +// const tunnel = await serveTunnel({ +// services: [greeter], +// relay: { +// address: "relay.example:8080", +// env: "myenv", +// tunnel: "mytunnel", +// apiKey: process.env.RELAY_API_KEY!, +// }, +// }); +// +// Node-only; requires a prebuilt native addon for the host platform. + +import * as http2 from "node:http2"; +import { + createEndpointHandler, + type NodeEndpointOptions, +} from "@restatedev/restate-sdk/node"; +import { RelayTunnel } from "../index.cjs"; + +/** Where and how to register with the relay. */ +export interface RelayConnectOptions { + /** `host:port` of the relay's receiver port (or the LB in front of it). */ + address: string; + /** The env (routing namespace) to register under. */ + env: string; + /** The tunnel to register under (scoped by `env`). */ + tunnel: string; + /** The API key presented in the `/whoami` handshake. */ + apiKey: string; + /** + * R4 multi-homing: how many connection slots to fan out across the relay + * nodes `address` resolves to. Omit for one per resolved node. + */ + connections?: number; + /** + * R5 receiver-instance id (affinity), held across reconnects. Omit to + * auto-generate an ephemeral, process-unique id. + */ + instanceId?: string; +} + +/** + * Options for {@link serveTunnel}. Extends the standard node endpoint options — + * `services`, `identityKeys`, etc. — so it accepts services from both the + * codegen SDK and the default promise API exactly like `serve(...)`, plus the + * relay connection coordinates. + */ +export interface ServeTunnelOptions extends NodeEndpointOptions { + relay: RelayConnectOptions; +} + +/** A snapshot of the tunnel's runtime status. */ +export interface TunnelStatus { + running: boolean; + lastError: string | null; +} + +/** A running tunnel + its private loopback server. */ +export interface TunnelHandle { + /** The loopback port the local server is bound to (127.0.0.1). */ + readonly localPort: number; + /** Current engine status. */ + status(): TunnelStatus; + /** Stop the tunnel (graceful) and close the loopback server. Idempotent. */ + stop(): Promise; +} + +/** + * Boot a loopback-private node:http2 (h2c) server for the given services and + * start a relay tunnel pointed at it. Resolves once the local server is bound + * and the tunnel engine has started; the receiver dials the relay in the + * background. + */ +export async function serveTunnel( + options: ServeTunnelOptions +): Promise { + const { relay, ...endpointOptions } = options; + + // The same dispatch handler `serve(...)` uses — unchanged for either API. + const handler = createEndpointHandler(endpointOptions); + // Bind 127.0.0.1 only (loopback-private): the relay reaches this server only + // through the native engine's loopback dial, never over the network. + const server = http2.createServer(handler); + + const localPort = await new Promise((resolve, reject) => { + const onError = (e: Error) => { + server.close(); + reject(e); + }; + server.once("error", onError); + server.listen(0, "127.0.0.1", () => { + server.removeListener("error", onError); + const address = server.address(); + if (address && typeof address === "object") { + resolve(address.port); + } else { + server.close(); + reject(new Error("relay tunnel: failed to bind the loopback server")); + } + }); + }); + + let tunnel: RelayTunnel; + try { + tunnel = RelayTunnel.start( + JSON.stringify({ + relay_addr: relay.address, + env: relay.env, + tunnel: relay.tunnel, + api_key: relay.apiKey, + local_port: localPort, + // undefined keys are dropped by JSON.stringify → the engine treats them + // as absent (serde `Option::None`). + connections: relay.connections, + instance_id: relay.instanceId, + }) + ); + } catch (e) { + await new Promise((res) => server.close(() => res())); + throw e; + } + + return { + localPort, + status(): TunnelStatus { + const raw = JSON.parse(tunnel.status()) as { + running?: boolean; + last_error?: string | null; + }; + return { + running: raw.running ?? false, + lastError: raw.last_error ?? null, + }; + }, + async stop(): Promise { + tunnel.stop(); + await new Promise((res) => server.close(() => res())); + }, + }; +} diff --git a/packages/libs/restate-sdk-tunnel-native/src/lib.rs b/packages/libs/restate-sdk-tunnel-native/src/lib.rs new file mode 100644 index 00000000..af7188fb --- /dev/null +++ b/packages/libs/restate-sdk-tunnel-native/src/lib.rs @@ -0,0 +1,54 @@ +//! napi-rs binding for the embedded relay-tunnel receiver. +//! +//! A thin control-plane wrapper around `restate_sdk_shared_core::relay` +//! (`Engine`/`Handle`) — the same engine the sdk-java FFM cdylib drives. Every +//! forwarded request rides a loopback socket inside the engine's own tokio +//! runtime; this binding only exposes start / status / stop. The high-level +//! node:http2 wiring lives in the TypeScript wrapper (`restate/index.ts`). + +use napi_derive::napi; +use restate_sdk_shared_core::relay::{Config, Engine, Handle}; + +/// A running relay tunnel. Construct with [`RelayTunnel::start`]; stop with +/// [`RelayTunnel::stop`] (idempotent) or by letting it be garbage-collected +/// (the underlying engine's `Drop` also shuts down). +#[napi] +pub struct RelayTunnel { + handle: Option, +} + +#[napi] +impl RelayTunnel { + /// Parse the JSON config (the `relay::Config` shape: `relay_addr`, `env`, + /// `tunnel`, `api_key`, `local_port`, optional `connections`/`instance_id`/ + /// `tls`/…) and start the engine. Returns immediately; the receiver dials + /// the relay in the background. + #[napi(factory)] + pub fn start(config_json: String) -> napi::Result { + let config: Config = serde_json::from_str(&config_json) + .map_err(|e| napi::Error::from_reason(format!("invalid tunnel config JSON: {e}")))?; + let handle = Engine::start(config).map_err(|e| napi::Error::from_reason(e.to_string()))?; + Ok(RelayTunnel { + handle: Some(handle), + }) + } + + /// Engine status as JSON: `{"running": bool, "last_error": string|null}`. + #[napi] + pub fn status(&self) -> String { + match &self.handle { + Some(h) => h.status_json(), + None => r#"{"running":false,"last_error":null}"#.to_string(), + } + } + + /// Signal a graceful shutdown and join the engine's runtime. Idempotent — + /// a second call (or GC after this) is a no-op. Blocks briefly while the + /// runtime drains, so call it off any hot path. + #[napi] + pub fn stop(&mut self) { + if let Some(mut h) = self.handle.take() { + h.stop(); + } + } +} diff --git a/packages/libs/restate-sdk-tunnel-native/tsconfig.build.json b/packages/libs/restate-sdk-tunnel-native/tsconfig.build.json new file mode 100644 index 00000000..dde202cb --- /dev/null +++ b/packages/libs/restate-sdk-tunnel-native/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "composite": true + }, + "include": ["src/**/*", "index.d.cts"] +} diff --git a/packages/libs/restate-sdk-tunnel-native/tsconfig.json b/packages/libs/restate-sdk-tunnel-native/tsconfig.json new file mode 100644 index 00000000..bb7c8330 --- /dev/null +++ b/packages/libs/restate-sdk-tunnel-native/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "composite": false + }, + "include": ["src/**/*", "index.d.cts"] +} diff --git a/packages/libs/restate-sdk-tunnel-native/tsdown.config.ts b/packages/libs/restate-sdk-tunnel-native/tsdown.config.ts new file mode 100644 index 00000000..017ee3ee --- /dev/null +++ b/packages/libs/restate-sdk-tunnel-native/tsdown.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "tsdown"; + +export default defineConfig({ + entry: ["src/index.ts"], + platform: "node", + exports: true, + format: ["esm", "cjs"], + dts: true, + clean: true, + ignoreWatch: ["dist", ".turbo", "*.tsbuildinfo"], + // Keep the SDK peer + the napi binding out of the bundle. The generated + // `../index.cjs` (napi loader) + its `.node` are shipped alongside `dist/` + // and required at runtime; bundling them would break the native load. + external: ["@restatedev/restate-sdk", "../index.cjs"], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac12af5e..79d1c74d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -324,6 +324,15 @@ importers: specifier: workspace:* version: link:../restate-sdk + packages/libs/restate-sdk-tunnel-native: + devDependencies: + '@napi-rs/cli': + specifier: ^2.18.4 + version: 2.18.4 + '@restatedev/restate-sdk': + specifier: workspace:* + version: link:../restate-sdk + packages/libs/restate-sdk-zod: devDependencies: '@restatedev/restate-sdk-core': @@ -1144,6 +1153,11 @@ packages: '@microsoft/tsdoc@0.16.0': resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + '@napi-rs/cli@2.18.4': + resolution: {integrity: sha512-SgJeA4df9DE2iAEpr3M2H0OKl/yjtg1BnRI5/JyowS71tUWhrfSu2LT0V3vlHET+g1hBVlrO60PmEXwUEKp8Mg==} + engines: {node: '>= 10'} + hasBin: true + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -5338,6 +5352,8 @@ snapshots: '@microsoft/tsdoc@0.16.0': {} + '@napi-rs/cli@2.18.4': {} + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.9.2 From bd92edd411162d35b73c7287db749c9e186b73af Mon Sep 17 00:00:00 2001 From: slinkydeveloper Date: Wed, 12 Aug 2026 11:39:59 +0200 Subject: [PATCH 2/3] fix(tunnel-native): opt out of the shared turbo pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The package's `_build` (tsc + tsdown) ran inside the repo-wide `turbo run _build`, but CI never runs `napi build`, so the napi-generated `../index.cjs` (+ its `.d.cts`) are absent and `tsc` failed with TS2307 — cascading to every job that builds first. An embedded native addon can't be built in CI without a native build step (which doesn't exist yet — the prebuild matrix is deferred). So drop the turbo task scripts (`_build`/`_check:types`/`_test`/`lint`) from this package: turbo then skips it in the shared build/verify runs. Keep manual `build:native` + `build:wrapper` for local/prebuild use. The package rejoins the pipeline once its native CI lands. Verified: `pnpm build` (turbo _build over ./packages/libs/*) and `turbo run _check:types lint` are green with the package skipped. Co-Authored-By: Claude Opus 4.8 (1M context) --- development/relay-receiver-napi-plan.md | 5 +++++ packages/libs/restate-sdk-tunnel-native/package.json | 9 +++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/development/relay-receiver-napi-plan.md b/development/relay-receiver-napi-plan.md index 38e8ad31..bad26445 100644 --- a/development/relay-receiver-napi-plan.md +++ b/development/relay-receiver-napi-plan.md @@ -118,6 +118,11 @@ there is **no** napi-rs yet. This package adds: ## Deferred (documented, not in v1) - Multi-platform prebuild matrix + CI + `optionalDependencies` publishing. + Until that lands, the package deliberately defines **no** turbo task scripts + (`_build`/`_check:types`/`_test`/`lint`), so it is skipped by the repo-wide + `turbo run` (build/verify) — CI can't run `napi build`, so it must not try to + `tsc`/`tsdown` a wrapper whose native binding isn't present. Build it manually + with `build:native` then `build:wrapper`. - Restate Cloud `/_/start-tunnel` mode (the engine is `/whoami`-only, same as Java). - Reconciling with the pure-JS `restate-sdk-tunnel` package. - Swapping the shared-core git dep for a crates.io release once `tunnel` ships. diff --git a/packages/libs/restate-sdk-tunnel-native/package.json b/packages/libs/restate-sdk-tunnel-native/package.json index 93298c54..d8d66ecb 100644 --- a/packages/libs/restate-sdk-tunnel-native/package.json +++ b/packages/libs/restate-sdk-tunnel-native/package.json @@ -27,15 +27,12 @@ "napi": { "name": "restate-sdk-tunnel-native" }, + "//scripts": "This package needs a native `napi build` step that the shared CI pipeline does not run yet, so it deliberately does NOT define the turbo task scripts (_build / _check:types / _test / lint). That opts it out of the repo-wide `turbo run` (build/verify) until the prebuild matrix + native CI land — see development/relay-receiver-napi-plan.md. Build it manually with the scripts below (build:native first, then build:wrapper).", "scripts": { "build:native": "napi build --platform --release --js index.cjs --dts index.d.cts", "build:native:debug": "napi build --platform --js index.cjs --dts index.d.cts", - "_build": "tsc --noEmit --project tsconfig.build.json && tsdown", - "build": "turbo run _build --filter={.}...", - "check:types": "turbo run _check:types --filter={.}...", - "_check:types": "tsc --noEmit --project tsconfig.build.json", - "clean": "rm -rf dist *.tsbuildinfo .turbo", - "lint": "eslint ." + "build:wrapper": "tsc --noEmit --project tsconfig.build.json && tsdown", + "clean": "rm -rf dist *.tsbuildinfo .turbo" }, "dependencies": {}, "devDependencies": { From 70ae59adf79d712972c3b0511e1e3b4be6eae504 Mon Sep 17 00:00:00 2001 From: slinkydeveloper Date: Thu, 29 Oct 2026 11:46:13 +0100 Subject: [PATCH 3/3] feat(tunnel-native): relay.version, sent in /whoami `RelayConnectOptions.version` goes to the engine config: the relay routes each version apart and, with auto-registration, registers each as its own deployment. Bumps the shared-core `relay` engine that carries it. Co-Authored-By: Claude Opus 5.5 --- packages/libs/restate-sdk-tunnel-native/Cargo.lock | 2 +- packages/libs/restate-sdk-tunnel-native/src/index.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/libs/restate-sdk-tunnel-native/Cargo.lock b/packages/libs/restate-sdk-tunnel-native/Cargo.lock index f5a764f0..0f36842f 100644 --- a/packages/libs/restate-sdk-tunnel-native/Cargo.lock +++ b/packages/libs/restate-sdk-tunnel-native/Cargo.lock @@ -391,7 +391,7 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "restate-sdk-shared-core" version = "7.0.2" -source = "git+https://github.com/restatedev/sdk-shared-core.git?branch=relay#7789348906c6ae7d2ed6719043c4a2ff4b99b9d0" +source = "git+https://github.com/restatedev/sdk-shared-core.git?branch=relay#74b116983caa675a5bbae313288fe47fbbad8f26" dependencies = [ "async-trait", "base64", diff --git a/packages/libs/restate-sdk-tunnel-native/src/index.ts b/packages/libs/restate-sdk-tunnel-native/src/index.ts index 2c971ef9..e06c586d 100644 --- a/packages/libs/restate-sdk-tunnel-native/src/index.ts +++ b/packages/libs/restate-sdk-tunnel-native/src/index.ts @@ -59,6 +59,13 @@ export interface RelayConnectOptions { * auto-generate an ephemeral, process-unique id. */ instanceId?: string; + /** + * The version of the code this process serves (e.g. `1.2.3` or a build id; + * `[A-Za-z0-9._-]`, at most 64 chars). The relay routes each version apart + * and, with auto-registration on, registers each version as its own + * deployment. Omit for an unversioned receiver. + */ + version?: string; } /** @@ -135,6 +142,7 @@ export async function serveTunnel( // as absent (serde `Option::None`). connections: relay.connections, instance_id: relay.instanceId, + version: relay.version, }) ); } catch (e) {