@drm3labs-oss/rpc-pool
v0.2.8
Published
Browser + Node WASM binding for drm3-rpc-pool: a resilient JSON-RPC failover pool driven by the platform `fetch`.
Maintainers
Readme
@drm3labs-oss/rpc-pool
Resilient JSON-RPC failover pool for any EVM chain, compiled to WebAssembly.
The pool's failover, per-endpoint health tracking, exponential backoff,
capability routing, and client-side rate limiting all run in WASM. The actual
network call is done by the platform fetch, so the same package works in the
browser, in Web Workers, and in Node 18+.
It is a WASM binding over the Rust crate
drm3-rpc-pool. No reqwest, no
tokio - just fetch.
Install
npm install @drm3labs-oss/rpc-poolBrowser (ES modules / Vite / bundler)
With the --target web build you must call the default init() once to load
the .wasm, then use the RpcPool class.
import init, { RpcPool } from "@drm3labs-oss/rpc-pool";
await init(); // loads the .wasm
const pool = new RpcPool({
max_retries: 0, // 0 = try every healthy, capable endpoint
endpoints: [
{ url: "https://eth.llamarpc.com", priority: 0 },
{ url: "https://rpc.ankr.com/eth", priority: 1 },
{
url: "https://eth-mainnet.example/v2/KEY",
priority: 2,
auth: { type: "bearer", token: "..." },
},
],
});
// Resolves the JSON-RPC `result`; fails over on 429 / transport errors.
const blockHex: string = await pool.call("eth_blockNumber", []);
console.log(parseInt(blockHex, 16));
// Live per-endpoint health snapshot.
console.log(pool.status());Node 18+
Node ships a global fetch, so the same API works. Use the web build with a
manual wasm read, or build a --target nodejs package (npm run build:node)
which needs no init():
// nodejs target (no init needed):
import { RpcPool } from "@drm3labs-oss/rpc-pool";
const pool = new RpcPool({
endpoints: [{ url: "https://eth.llamarpc.com" }],
});
const chainId = await pool.call("eth_chainId", []);
console.log(chainId);Config shape (TypeScript)
Types are generated by wasm-bindgen into drm3_rpc_pool_wasm.d.ts.
new RpcPool({
request_timeout_ms?: number;
max_retries?: number; // 0 = try every healthy candidate
endpoints: Array<{
url: string;
label?: string;
priority?: number; // lower priority is tried first
capabilities?: string[]; // e.g. ["eth_call"]; empty/omitted = supports all
max_rps?: number; // client-side throttle; throttled endpoint is skipped, not awaited
auth?:
| { type: "none" }
| { type: "url_key" }
| { type: "header"; name: string; value: string }
| { type: "bearer"; token: string };
}>;
});
pool.call(method: string, params: any): Promise<any>;
pool.status(): any;
pool.length: number;Failover behavior
- Endpoints are tried in
priorityorder (ties broken by config order). - An endpoint that lacks the method's capability is skipped.
- On a 429, non-2xx, transport error, or unparseable body, the call fails over to the next endpoint. A well-formed JSON-RPC error result is returned as-is (it is a valid answer, not a transport failure).
- Repeated failures demote an endpoint into an exponentially-growing cooldown; it is skipped while cooling down and reinstated automatically.
call(...)rejects only when every candidate has failed.
Notes / limitations
- Backoff is time-gated, not sleep-based. The pool never blocks on a wasm
timer: it records failure timestamps and skips an endpoint while it is within
its cooldown window, comparing against
web-time's wasm clock. This is the same logic as the native build; nothing about failover is "simplified" in wasm. (gloo-timersis available as the documented wasm timer should a future sleep-based path be added.) request_timeout_msis advisory in this build. The native crate enforces it via the reqwest client timeout; the wasmFetchTransportdoes not yet wire anAbortControllerdeadline ontofetch. Per-request timeouts therefore rely on the platform's defaultfetchtimeout. (Adding an abort-based deadline is a small follow-up.)- The pool runs single-threaded in wasm (the
Transport/Metricstraits drop theirSend + Syncbounds onwasm32); this matches the JS event loop.
License
MIT
