# rpckit A modular TypeScript library for JSON-RPC communication with type-safe transports and utilities.
rpckit
A modular TypeScript library for JSON-RPC communication with type-safe transports, automatic batching, and built-in failover.
Get started Why rpckit?
:::code-group ```bash [npm] npm i @rpckit/core @rpckit/websocket ``` ```bash [pnpm] pnpm add @rpckit/core @rpckit/websocket ``` ```bash [yarn] yarn add @rpckit/core @rpckit/websocket ``` :::
version 2.2.0
typed 100%
license MIT
Modular
Pick only the transports you need - WebSocket, TCP, HTTP, or all three
Type-Safe
Full TypeScript support with schema-based typing for RPC methods
Resilient
Built-in failover with fallback and m-of-n quorum cluster transports
Efficient
Automatic request batching, connection pooling, and keep-alive
## Overview ```ts import { webSocket } from '@rpckit/websocket/electrum-cash' // Create a transport with protocol-specific defaults const transport = webSocket('wss://electrum.example.com:50004') // Make requests (handshake sent automatically) const tip = await transport.request('blockchain.headers.get_tip') // Subscribe to updates const unsubscribe = await transport.subscribe( 'blockchain.headers.subscribe', [], (header) => console.log('New block:', header) ) // Clean up await unsubscribe() await transport.close() ``` ## Features * **Multiple Transports** - WebSocket, TCP (with TLS), and HTTP support * **Meta-Transports** - Fallback for failover, Cluster for m-of-n quorum consensus * **Automatic Batching** - Combines multiple requests into single JSON-RPC batch calls * **Subscriptions** - First-class subscription support with automatic resubscription on reconnect * **Type Safety** - Define schemas for compile-time checking of method names and parameters * **Connection Management** - Keep-alive, reconnection with exponential backoff, connection pooling * **URL Parsing** - Create transports from URL strings with `parse('wss://...')` ## Packages | Package | Description | |---------|-------------| | `@rpckit/core` | Core types, `BatchScheduler`, `parse()`, `withRetry()` | | `@rpckit/websocket` | WebSocket transport with subscriptions and reconnection | | `@rpckit/tcp` | TCP transport with TLS support (Node.js) | | `@rpckit/http` | HTTP transport for stateless requests | | `@rpckit/fallback` | Meta-transport for failover across multiple transports | | `@rpckit/cluster` | Meta-transport for m-of-n quorum consensus |
# Changelog ## 2.2.0 ### Unwrap single-request batches on the wire When batching is enabled but only one request was aggregated, WebSocket and TCP transports now send the request as a plain JSON-RPC object instead of a 1-element array. This matches HTTP's existing behavior and what some servers expect when they advertise non-batch endpoints. * **WebSocket & TCP transports** — `sendBatch` now serializes `requests[0]` directly when the collected batch has length 1. * **Response parsing unchanged** — both transports already accept either the object or array form coming back from the server, so this is purely a send-path improvement. ## 2.1.0 ### Rate-limit aware retries Requests are already retried implicitly by every transport. With 2.1.0, when the server explicitly rate-limits the caller, the retry waits the server-supplied hint instead of falling back to exponential backoff. ```ts import { RateLimitError } from '@rpckit/core' try { await transport.request('eth_getBalance', ['0x...']) } catch (err) { if (err instanceof RateLimitError) { // All retries exhausted; the last hint is still surfaced. console.log(`rate-limited, server suggested ${err.retryAfterMs}ms`) } } ``` * **`RateLimitError`** — new exported error class carrying `retryAfterMs` (and optional `cause`). Thrown by transports when the server explicitly rate-limits the caller. The hint is honored automatically by the transport's existing retry wrapper; the error surfaces to userland only if retries are exhausted. * **Hint-aware retry delay** — when an attempt throws `RateLimitError`, the retry waits `retryAfterMs` instead of `retryDelay * 2^(attempt-1)`. Capped at 60s to keep a hostile/pathological hint from stalling the caller indefinitely. Negative hints clamp to 0. Mixed attempts (some generic `Error`, some `RateLimitError`) each use the appropriate delay shape. * **HTTP transport** — on `HTTP 429`, parses the retry hint from headers in priority order: 1. `retry-after-ms` — sub-second precision (used by services where a sub-second bucket refill would round to 0/1 under `Retry-After`'s integer-seconds-only encoding). 2. `Retry-After` — RFC 9110 §10.2.3 integer seconds → ms. 3. Conservative 1s fallback. HTTP-date form of `Retry-After` is intentionally not parsed — the millisecond-precision use case requires the delta form. * **WebSocket transport** — on an incoming JSON-RPC error frame with `code === 429` or `data.http_status === 429`, extracts `data.retry_after_ms` (1s fallback) and rejects the pending entry with `RateLimitError`. The existing per-request retry wrap handles the retry with a fresh request id. ## 2.0.0 ### Breaking: Explicit Params The `request()` and `subscribe()` APIs now take params as a single explicit argument instead of rest/spread parameters. This aligns the TypeScript API with the JSON-RPC 2.0 wire format and adds unambiguous support for both positional (array) and named (object) params. ```ts // Before (1.x) await transport.request('method', param1, param2) await transport.subscribe('method', param1, (data) => { ... }) // After (2.0) await transport.request('method', [param1, param2]) await transport.subscribe('method', [param1], (data) => { ... }) // Named params (new) await transport.request('daemon.passthrough', { method: 'foo', params: [] }) ``` #### Migration * **`request(method, ...params)`** → **`request(method, params?)`** — wrap positional args in an array. No-arg calls like `request('server.ping')` are unchanged. * **`subscribe(method, ...params, callback)`** → **`subscribe(method, params, callback)`** — params is always the second argument, callback is always the third. * **`SchemaEntry.params`** — now accepts `unknown[] | Record` to support named params. #### Other changes * Removed the auto-unwrap heuristic in WebSocket and TCP transports that guessed whether a single object argument was named params. The params argument now maps directly to the JSON-RPC `params` field — no ambiguity. * **`daemon.passthrough`** — Uses plain object params naturally in `ElectrumCashSchema`. ## 1.0.3 ### Subscription Dispatch Chain Subscription notification handlers are now serialized via a `dispatchChain` on each subscription entry, preventing concurrent handler execution and ensuring notifications are processed in order. * **WebSocket & TCP transports** — Notification handlers are dispatched through a promise chain, so each notification waits for the previous one's handlers to complete before firing. * **Error isolation** — A failing handler no longer breaks the dispatch chain for the subscription. Each handler is individually caught. * **Graceful cleanup** — `unsubscribe()` and `close()` now await in-flight dispatch chains, ensuring all pending handlers complete before teardown. ## 1.0.2 ### Batch Auto-Disable When a server can't handle batch requests (e.g. the batch is too large or the server doesn't support batching), the `BatchScheduler` now automatically falls back to sending requests individually and temporarily disables batching. After a cooldown period (default: 5 seconds), batching is re-enabled. * **`BatchScheduler`** — Added `sendSingle`, `isBatchRejection`, and `disabledCooldown` options. Added `disabled` property. * **WebSocket & TCP transports** — Now provide a `sendSingle` fallback to `BatchScheduler`, enabling transparent auto-disable on batch rejection. * **`parse()`** — Added `disabledCooldown` query parameter support (e.g. `wss://example.com?batchSize=10&disabledCooldown=10000`). * **`BatchConfig`** — Added `disabledCooldown` option. * **`bump.mjs`** — Skip package directories without `package.json`. ### Detection The following errors trigger auto-disable by default: * Batch timeout — server couldn't process the batch in time * Parse error (JSON-RPC code `-32700`) * Invalid request (JSON-RPC code `-32600`) Custom detection can be provided via the `isBatchRejection` option. ## 1.0.0 Initial release. ### Core * `Transport` interface with `request()`, `subscribe()`, `connect()`, `close()` * Spread-style parameters — `request('method', param1, param2)` instead of array wrapping * Schema-based generics for type-safe method names, parameters, and return types * `BatchScheduler` for automatic request batching with configurable batch size and wait time * `withRetry()` utility with exponential backoff * `parse()` for creating transports from URL strings (supports nested meta-transports and query-string options) * `createParse()` for building custom parse functions with overridden package maps * `createParseSync()` for building synchronous parse functions using pre-imported factory functions * `ElectrumCashSchema` type definitions (protocol v1.5 and v1.6) * `EthereumSchema` type definitions (EIP-1474 standard methods) ### Transports * **WebSocket** — Full-duplex communication with subscriptions, keep-alive, reconnection, connection pooling * **TCP** — Newline-delimited JSON-RPC with TLS support, keep-alive, reconnection (Node.js) * **HTTP** — Stateless JSON-RPC over HTTP POST with custom headers, fetch options, and raw mode All transports support lazy connections — no resources are allocated until the first `request()`, `subscribe()`, or `connect()` call. ### Meta-Transports * **Fallback** — Automatic failover across multiple transports with optional health-based ranking * Default `shouldThrow` stops fallback on deterministic JSON-RPC errors (parse error, invalid request, invalid params) * `eagerConnect` prioritizes the fastest-connecting transport * `onScores` listener and `scores` property for monitoring transport health * `onResponse` hook for observing requests across all transports * **Cluster** — m-of-n quorum consensus with deep-equality response matching * `onResponse` hook for observing individual transport responses * Single-element passthrough — `fallback([t])` and `cluster([t])` return the input transport directly ### Electrum Cash Variants Protocol-specific subpath imports (`@rpckit/*/electrum-cash`) with pre-configured defaults: * `server.version` handshake with configurable `clientName` and `protocolVersion` * `server.ping` keep-alive * `subscribe`/`unsubscribe` method convention * `Server-Version` HTTP header * Fallback variant with `server.ping` health probing and protocol-aware `shouldThrow` (retries transient server errors like OOM, warmup, syncing) ### Ethereum Variants Protocol-specific subpath imports (`@rpckit/*/ethereum`) for Ethereum JSON-RPC: * `eth_subscription` notification routing by subscription ID * `eth_unsubscribe` called automatically on cleanup * Subscription ID suppressed from callbacks (handled internally) * Custom `parse()` function for Ethereum transports ### Features * Automatic request batching with configurable batch size and wait time * Subscription support with automatic resubscription on reconnect * Handshake re-execution on reconnect (WebSocket and TCP) * Connection pooling with ref counting for WebSocket and TCP transports * Retry with exponential backoff on all transports * Raw mode for HTTP transport (return full JSON-RPC envelopes instead of throwing on error) * Request/response hooks on HTTP transport * Socket access via `getSocket()` and `getSocketAsync()` on WebSocket and TCP transports * Subscription sharing — multiple listeners on the same method+params share one server subscription * Smart unsubscribe — server unsubscribe only sent when last listener removes * Fresh data for new subscribers — new listeners receive most recent notification, not stale initial result * Race condition prevention — concurrent subscription calls safely coalesce * `notificationFilter` callback for protocol-specific notification routing * `transformInitialResult` option to normalize or suppress initial subscription results * HTTP transport includes response body in error messages for better debugging # Interactive Demo Try rpckit's transports live in your browser. Connect to real servers, execute RPC methods, and watch subscription events in real-time. ## How to Use ### 1. Choose Protocol Select the protocol at the top: * **Electrum Cash**: Connect to Electrum servers using the [Electrum Cash protocol](https://electrum-cash-protocol.readthedocs.io/). Supports subscriptions over WebSocket. * **BCHN**: Connect to a Bitcoin Cash Node using standard JSON-RPC. HTTP only, no subscriptions. ### 2. Configure Transport Choose your transport mode: * **Single**: Direct connection to one server * **Fallback**: Automatic failover across multiple servers (enable health ranking for smart routing) * **Cluster**: Require m-of-n quorum consensus for responses Add transports using the quick-select buttons or enter custom URLs. ### 3. Connect Click **Connect** to establish the connection. The status badge shows the current state. ### 4. Execute Methods Select an RPC method from the dropdown, fill in any required parameters, and click **Execute**. Results appear in the right panel with timing information. Available methods depend on the selected protocol: **Electrum Cash**: `server.ping`, `blockchain.headers.get_tip`, `blockchain.transaction.get`, `blockchain.address.get_balance`, `blockchain.address.get_history` **BCHN**: `getblockcount`, `getbestblockhash`, `getblockchaininfo`, `getmempoolinfo`, `getblock`, `getrawtransaction` ### 5. Subscriptions (Electrum + WebSocket only) * **Block Headers**: Click Subscribe to monitor new blocks. Events appear in the Live Events panel. * **Address Activity**: Enter a Bitcoin Cash address in CashAddr format (`bitcoincash:qp...`) to monitor for transaction activity. ## Default Servers ### Electrum Cash | Protocol | URL | |----------|-----| | WebSocket (secure) | `wss://fulcrum.pat.mn` | | WebSocket (insecure) | `ws://fulcrum.pat.mn:50003` | | HTTP | `https://fulcrum.pat.mn` | ### BCHN | Protocol | URL | |----------|-----| | HTTP | `https://bchn.pat.mn` | ## Notes * HTTP transport does not support subscriptions * BCHN protocol uses base transports (`@rpckit/http`), while Electrum uses protocol-specific variants (`@rpckit/websocket/electrum-cash`, `@rpckit/http/electrum-cash`) * Cluster mode requires multiple transports with matching quorum * Health ranking in Fallback mode automatically prioritizes faster, more reliable servers # Getting Started This guide will help you make your first JSON-RPC requests with rpckit. ## Installation ```bash npm i @rpckit/websocket ``` ## Basic Usage ### Create a Transport ```ts import { webSocket } from '@rpckit/websocket' const transport = webSocket('wss://my-jsonrpc-server.com') ``` ### Connect and Make Requests ```ts // Connect to the server await transport.connect() // Make a request const result = await transport.request('my.method', ['param1']) console.log(result) // Close when done await transport.close() ``` ### Subscribe to Updates ```ts // Subscribe returns an unsubscribe function const unsubscribe = await transport.subscribe( 'events.subscribe', ['channel-1'], (data) => { console.log('Event received:', data) } ) // Later, unsubscribe await unsubscribe() ``` ## Electrum Cash For Electrum Cash servers, use the protocol-specific variant which pre-configures handshake, keep-alive, and unsubscribe conventions: ```ts import { webSocket } from '@rpckit/websocket/electrum-cash' const transport = webSocket('wss://electrum.example.com:50004') // Handshake (server.version) is sent automatically on connect const tip = await transport.request('blockchain.headers.get_tip') console.log(tip) // { height: 875000, hex: '...' } // Unsubscribe method is derived automatically const unsub = await transport.subscribe( 'blockchain.headers.subscribe', [], (header) => console.log('New block:', header) ) await unsub() // Sends blockchain.headers.unsubscribe await transport.close() ``` Schema types are available for compile-time checking: ```ts import type { ElectrumCashSchema } from '@rpckit/core/electrum-cash' import { webSocket } from '@rpckit/websocket/electrum-cash' const transport = webSocket('wss://electrum.example.com:50004') const tip = await transport.request('blockchain.headers.get_tip') // tip: { height: number; hex: string } ``` ## Type-Safe Requests Define a schema for compile-time type checking of any JSON-RPC service: ```ts import { webSocket } from '@rpckit/websocket' import type { Schema } from '@rpckit/core' type MySchema = { requests: [ { method: 'getblockcount'; params: []; return: number }, { method: 'getblock'; params: [blockhash: string, verbosity?: number]; return: object } ] subscriptions: [] } const transport = webSocket('wss://example.com') // TypeScript knows the return type const count = await transport.request('getblockcount') // count: number ``` ## Configuration Options ### Timeout ```ts const transport = webSocket('wss://example.com', { timeout: 10000 // 10 seconds }) ``` ### Batching Transports will batch up requests over a given period and execute them in a single Batch JSON-RPC HTTP request. By default, this period is a zero delay meaning that the batch request will be executed at the end of the current JavaScript message queue. Consumers can specify a custom time period `wait` (in ms). Configure batch behavior with `{ batch: false }` or with extended config: ```ts const transport = webSocket('wss://example.com', { batch: { wait: 10, // Wait 10ms to collect requests batchSize: 50 // Max 50 requests per batch } }) ``` ### Keep-Alive ```ts const transport = webSocket('wss://example.com', { keepAlive: { interval: 30000, // Ping every 30 seconds method: 'server.ping' } }) ``` ### Reconnection ```ts const transport = webSocket('wss://example.com', { reconnect: { attempts: 5, // Max reconnect attempts after a disconnect delay: 1000 // Wait 1s between attempts } }) ``` ## Using Fallback For high availability, use multiple servers with fallback: ```ts import { webSocket } from '@rpckit/websocket' import { fallback } from '@rpckit/fallback' const transport = fallback([ webSocket('wss://primary.example.com'), webSocket('wss://backup1.example.com'), webSocket('wss://backup2.example.com') ]) // Requests automatically fail over to the next server const result = await transport.request('my.method') ``` ## Using URL Parsing Create transports from URL strings: ```ts import { parse } from '@rpckit/core' // Simple transport (base, protocol-agnostic) const ws = await parse('wss://example.com?timeout=5000') // Fallback transport const fb = await parse('fallback(wss://a.com,wss://b.com)?rank=true') // Cluster transport (2-of-3 quorum) const cl = await parse('cluster(2,wss://a.com,wss://b.com,wss://c.com)') ``` For Electrum Cash, use the pre-configured parse variant: ```ts import { parse } from '@rpckit/core/electrum-cash' const transport = await parse('wss://electrum.example.com:50004') // Handshake, keepAlive method, and unsubscribe conventions are pre-configured ``` ## Next Steps * Learn about [Transports](/docs/transports/overview) in detail * Explore [Utilities](/docs/utilities/parse) like `parse()` and `BatchScheduler` * See the [Fallback](/docs/transports/fallback) and [Cluster](/docs/transports/cluster) meta-transports # Installation rpckit is distributed as multiple packages. Install only what you need. ## Quick Start For most applications using WebSocket: :::code-group ```bash [npm] npm i @rpckit/websocket ``` ```bash [pnpm] pnpm add @rpckit/websocket ``` ```bash [yarn] yarn add @rpckit/websocket ``` ::: ## Packages ### Core The core package provides shared types and utilities. It's automatically installed as a dependency of all transport packages. ```bash npm i @rpckit/core ``` Exports: * `parse()` - Create transports from URL strings * `createParse()` - Factory for custom parse functions with overridden package maps * `BatchScheduler` - Request batching utility * `withRetry()` - Exponential backoff retry wrapper * TypeScript types and interfaces Electrum Cash types and a pre-configured `parse` are available via `@rpckit/core/electrum-cash`. ### WebSocket Transport WebSocket transport with subscriptions, reconnection, and keep-alive. ```bash npm i @rpckit/websocket ``` Works in browsers and Node.js. ### TCP Transport TCP transport with TLS support. Node.js only. ```bash npm i @rpckit/tcp ``` ### HTTP Transport Stateless HTTP transport. Works in browsers and Node.js. ```bash npm i @rpckit/http ``` ### Fallback Transport Meta-transport for failover across multiple transports. ```bash npm i @rpckit/fallback ``` ### Cluster Transport Meta-transport for m-of-n quorum consensus. ```bash npm i @rpckit/cluster ``` ## Full Installation To install all packages: ```bash npm i @rpckit/core @rpckit/websocket @rpckit/tcp @rpckit/http @rpckit/fallback @rpckit/cluster ``` ## Requirements * Node.js 18+ or modern browser * TypeScript 5.0+ (for type inference) # Why rpckit rpckit is a modular TypeScript library for JSON-RPC communication. It provides type-safe transports with automatic batching, subscriptions, and built-in failover mechanisms. ## The Problem When building applications that communicate with JSON-RPC servers (like Fulcrum, blockchain nodes, or custom services), you typically need to handle: * **Multiple transport protocols** - WebSocket for subscriptions, HTTP for stateless calls, TCP for performance * **Connection management** - Reconnection, keep-alive, timeouts * **Request batching** - Combining multiple requests to reduce round-trips * **Failover** - Switching to backup servers when the primary fails * **Type safety** - Ensuring correct method names and parameter types Most libraries solve one or two of these problems. rpckit solves all of them with a unified, composable API. ## The Solution ### Modular Design Install only what you need: ```bash # Just WebSocket npm i @rpckit/websocket # WebSocket + failover npm i @rpckit/websocket @rpckit/fallback # Everything npm i @rpckit/websocket @rpckit/tcp @rpckit/http @rpckit/fallback @rpckit/cluster ``` ### Unified Transport Interface All transports share the same interface: ```ts interface Transport { connect(): Promise request(method, params?): Promise subscribe(method, params, callback): Promise close(): Promise } ``` Switch between WebSocket, TCP, or HTTP without changing your application code. ### Type-Safe Schemas Define your RPC schema once, get compile-time checking everywhere: ```ts type MySchema = { requests: [ { method: 'getBalance'; params: [address: string]; return: number }, { method: 'getHistory'; params: [address: string]; return: Transaction[] } ] subscriptions: [ { method: 'subscribe'; params: [address: string]; return: string } ] } const transport = webSocket('wss://...') await transport.request('getBalance', ['addr123']) // Typed! await transport.request('unknownMethod') // Type error! ``` ### Automatic Batching Requests are automatically batched to reduce network overhead: ```ts // These three requests are sent as a single batch const [a, b, c] = await Promise.all([ transport.request('method1'), transport.request('method2'), transport.request('method3') ]) ``` ### Built-in Resilience Fallback transport tries servers in order until one succeeds: ```ts const transport = fallback([ webSocket('wss://primary.example.com'), webSocket('wss://backup.example.com'), tcp('tcp://fallback.example.com:50001') ]) ``` Cluster transport requires m-of-n servers to agree: ```ts const transport = cluster([ webSocket('wss://node1.example.com'), webSocket('wss://node2.example.com'), webSocket('wss://node3.example.com') ], { quorum: 2 }) // 2-of-3 must agree ``` ## When to Use rpckit rpckit is ideal for: * **Blockchain applications** communicating with Fulcrum or similar servers * **Microservices** using JSON-RPC for inter-service communication * **Real-time applications** that need subscriptions with automatic reconnection * **High-availability systems** requiring failover or consensus across multiple servers ## Comparison | Feature | rpckit | json-rpc-2.0 | jayson | |---------|--------|--------------|--------| | TypeScript-first | Yes | Partial | Partial | | Multiple transports | WS, TCP, HTTP | HTTP | HTTP, TCP | | Subscriptions | Yes | No | No | | Auto batching | Yes | Manual | Manual | | Failover | Yes | No | No | | Quorum consensus | Yes | No | No | | Schema typing | Yes | No | No | # Electrum Cash The Electrum Cash protocol variants provide pre-configured transports for communicating with [Electrum Cash](https://electrum-cash-protocol.readthedocs.io/) servers like Fulcrum. ## Overview Base transports (`@rpckit/websocket`, `@rpckit/tcp`, `@rpckit/http`) are protocol-agnostic. The Electrum Cash subpath variants add: * **Handshake** - `server.version` sent automatically on connect * **Keep-alive** - Uses `server.ping` as the ping method * **Unsubscribe** - Derives unsubscribe method from subscribe method (e.g., `blockchain.headers.subscribe` → `blockchain.headers.unsubscribe`) * **HTTP Header** - Includes `Server-Version` in request headers ## Installation ```bash npm i @rpckit/core @rpckit/websocket @rpckit/tcp @rpckit/http ``` ## Usage Import from the `/electrum-cash` subpath: ```ts import { webSocket } from '@rpckit/websocket/electrum-cash' import { tcp } from '@rpckit/tcp/electrum-cash' import { http } from '@rpckit/http/electrum-cash' ``` ### WebSocket ```ts import type { ElectrumCashSchema } from '@rpckit/core/electrum-cash' import { webSocket } from '@rpckit/websocket/electrum-cash' const transport = webSocket('wss://fulcrum.example.com:50004', { keepAlive: 30000, // Uses server.ping automatically clientName: 'myapp', // Client name in handshake (default: 'rpckit') protocolVersion: '1.6', // Protocol version (default: '1.6') }) // Handshake (server.version) sent automatically on connect const tip = await transport.request('blockchain.headers.get_tip') console.log(tip.height) // Typed as number ``` ### TCP ```ts import { tcp } from '@rpckit/tcp/electrum-cash' // Plain TCP const transport = tcp('tcp://electrum.example.com:50001') // TCP with TLS const tlsTransport = tcp('tcp+tls://electrum.example.com:50002') const tip = await transport.request('blockchain.headers.get_tip') ``` ### HTTP ```ts import { http } from '@rpckit/http/electrum-cash' const transport = http('https://electrum.example.com') // Server-Version header included automatically const tip = await transport.request('blockchain.headers.get_tip') ``` ## Configuration Options All Electrum Cash variants accept these additional options: | Option | Type | Default | Description | |--------|------|---------|-------------| | `clientName` | `string` | `'rpckit'` | Client name sent in `server.version` handshake | | `protocolVersion` | `string` | `'1.6'` | Protocol version for handshake | ## Subscriptions Electrum Cash supports subscriptions over WebSocket and TCP transports. The variants automatically handle unsubscription: ```ts import { webSocket } from '@rpckit/websocket/electrum-cash' const transport = webSocket('wss://fulcrum.example.com:50004') // Subscribe to block headers const unsubHeaders = await transport.subscribe( 'blockchain.headers.subscribe', [], (header) => { console.log('New block:', header.height) } ) // Subscribe to address activity const unsubAddress = await transport.subscribe( 'blockchain.address.subscribe', ['bitcoincash:qp...'], (status) => { console.log('Address status:', status) } ) // Unsubscribe calls blockchain.headers.unsubscribe automatically await unsubHeaders() await unsubAddress() ``` ## Type Safety Use `ElectrumCashSchema` for full type inference on method names, parameters, and return types: ```ts import type { ElectrumCashSchema } from '@rpckit/core/electrum-cash' import { webSocket } from '@rpckit/websocket/electrum-cash' const transport = webSocket('wss://...') // TypeScript knows: // - Method name: 'blockchain.headers.get_tip' // - No parameters // - Returns: { height: number, hex: string } const tip = await transport.request('blockchain.headers.get_tip') // TypeScript knows: // - Method name: 'blockchain.address.get_balance' // - Parameters: [address: string] // - Returns: { confirmed: number, unconfirmed: number } const balance = await transport.request( 'blockchain.address.get_balance', ['bitcoincash:qp...'] ) ``` ## Fallback with Ranking The `@rpckit/fallback/electrum-cash` variant uses `server.ping` as the default health check: ```ts import { fallback } from '@rpckit/fallback/electrum-cash' import { webSocket } from '@rpckit/websocket/electrum-cash' const transport = fallback([ webSocket('wss://server1.example.com:50004'), webSocket('wss://server2.example.com:50004'), webSocket('wss://server3.example.com:50004'), ], { rank: true }) // Requests routed to best-performing server const tip = await transport.request('blockchain.headers.get_tip') // Monitor server health transport.onScores((scores) => { console.log('Rankings:', scores.map(s => ({ url: s.transport.url, score: s.score.toFixed(2) }))) }) ``` ## URL Parsing Use the Electrum Cash-specific parse function: ```ts import { parse } from '@rpckit/core/electrum-cash' // Creates electrum-cash variant transports const ws = await parse('wss://fulcrum.example.com:50004?keepAlive=30000') const tcp = await parse('tcp+tls://electrum.example.com:50002') const http = await parse('https://electrum.example.com') // Fallback with ranking const fb = await parse('fallback(wss://s1.example.com,wss://s2.example.com)?rank=true') ``` ## Common Methods The Electrum Cash protocol includes methods for: | Category | Methods | |----------|---------| | **Server** | `server.version`, `server.ping`, `server.banner`, `server.features` | | **Blockchain** | `blockchain.headers.get_tip`, `blockchain.block.header`, `blockchain.block.headers` | | **Addresses** | `blockchain.address.get_balance`, `blockchain.address.get_history`, `blockchain.address.listunspent` | | **Transactions** | `blockchain.transaction.get`, `blockchain.transaction.broadcast` | | **Subscriptions** | `blockchain.headers.subscribe`, `blockchain.address.subscribe` | See the [Electrum Cash Protocol](https://electrum-cash-protocol.readthedocs.io/) specification for the complete method list. # Ethereum The Ethereum protocol variants provide pre-configured transports for communicating with Ethereum JSON-RPC nodes (Geth, Erigon, Infura, Alchemy, public nodes, etc.). ## Overview Base transports (`@rpckit/websocket`, `@rpckit/http`) are protocol-agnostic. The Ethereum subpath variants add: * **Subscription Routing** - `eth_subscription` notifications are routed to the correct callback by subscription ID * **Automatic Cleanup** - `eth_unsubscribe` is called automatically when unsubscribing * **ID Suppression** - Subscription IDs are handled internally and not passed to callbacks ## Installation ```bash npm i @rpckit/core @rpckit/websocket @rpckit/http ``` ## Usage Import from the `/ethereum` subpath: ```ts import { webSocket } from '@rpckit/websocket/ethereum' import { http } from '@rpckit/http/ethereum' ``` ### WebSocket ```ts import type { EthereumSchema } from '@rpckit/core/ethereum' import { webSocket } from '@rpckit/websocket/ethereum' const transport = webSocket('wss://ethereum-rpc.publicnode.com', { timeout: 30000, }) const blockNumber = await transport.request('eth_blockNumber') console.log(blockNumber) // '0x...' const balance = await transport.request( 'eth_getBalance', ['0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', 'latest'] ) ``` ### HTTP ```ts import { http } from '@rpckit/http/ethereum' const transport = http('https://ethereum-rpc.publicnode.com', { timeout: 30000, }) const chainId = await transport.request('eth_chainId') const gasPrice = await transport.request('eth_gasPrice') ``` ## Subscriptions Ethereum subscriptions use the `eth_subscribe` method with a subscription type as the first parameter. The Ethereum variant handles the subscription ID routing automatically: ### New Blocks ```ts import { webSocket } from '@rpckit/websocket/ethereum' const transport = webSocket('wss://ethereum-rpc.publicnode.com') const unsub = await transport.subscribe( 'eth_subscribe', ['newHeads'], (header) => { console.log('New block:', header.number, header.hash) } ) // Later: unsubscribe (calls eth_unsubscribe automatically) await unsub() ``` ### Pending Transactions ```ts const unsub = await transport.subscribe( 'eth_subscribe', ['newPendingTransactions'], (txHash) => { console.log('Pending tx:', txHash) } ) ``` ### Log Events Subscribe to contract events with filters: ```ts // Subscribe to USDC Transfer events const USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' const unsub = await transport.subscribe( 'eth_subscribe', ['logs', { address: USDC_ADDRESS, topics: [TRANSFER_TOPIC] }], (log) => { console.log('Transfer:', { from: log.topics[1], to: log.topics[2], block: log.blockNumber }) } ) ``` ### Syncing Status ```ts const unsub = await transport.subscribe( 'eth_subscribe', ['syncing'], (status) => { if (status === false) { console.log('Node is synced') } else { console.log('Syncing:', status.currentBlock, '/', status.highestBlock) } } ) ``` ## Type Safety Use `EthereumSchema` for full type inference: ```ts import type { EthereumSchema } from '@rpckit/core/ethereum' import { webSocket } from '@rpckit/websocket/ethereum' const transport = webSocket('wss://...') // TypeScript knows the return types const chainId = await transport.request('eth_chainId') // string const blockNumber = await transport.request('eth_blockNumber') // string const gasPrice = await transport.request('eth_gasPrice') // string // TypeScript validates parameters const balance = await transport.request( 'eth_getBalance', ['0x...', 'latest'] // [address, block tag] ) const block = await transport.request( 'eth_getBlockByNumber', ['latest', false] // [block tag, include transactions?] ) ``` ## Subscription ID Handling Unlike the base WebSocket transport, the Ethereum variant handles subscription IDs internally: ```ts // Base WebSocket - you receive the subscription ID const unsub = await baseTransport.subscribe('eth_subscribe', ['newHeads'], (data) => { // First call: data is the subscription ID ('0x...') // Subsequent calls: data is the notification }) // Ethereum variant - subscription ID is hidden const unsub = await ethereumTransport.subscribe('eth_subscribe', ['newHeads'], (header) => { // Every call: header is the block header object // Subscription ID is managed internally }) ``` ## URL Parsing Use the Ethereum-specific parse function: ```ts import { parse } from '@rpckit/core/ethereum' // Creates ethereum variant transports const ws = await parse('wss://ethereum-rpc.publicnode.com?timeout=30000') const http = await parse('https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY') // Fallback const fb = await parse('fallback(wss://node1.example.com,wss://node2.example.com)') ``` ## Common Methods The Ethereum JSON-RPC API includes: | Category | Methods | |----------|---------| | **Chain** | `eth_chainId`, `eth_blockNumber`, `eth_gasPrice`, `eth_feeHistory` | | **Accounts** | `eth_getBalance`, `eth_getTransactionCount`, `eth_getCode`, `eth_getStorageAt` | | **Blocks** | `eth_getBlockByNumber`, `eth_getBlockByHash`, `eth_getBlockReceipts` | | **Transactions** | `eth_getTransactionByHash`, `eth_getTransactionReceipt`, `eth_sendRawTransaction` | | **Calls** | `eth_call`, `eth_estimateGas`, `eth_createAccessList` | | **Logs** | `eth_getLogs`, `eth_newFilter`, `eth_getFilterChanges` | | **Subscriptions** | `eth_subscribe` (`newHeads`, `logs`, `newPendingTransactions`, `syncing`) | | **Network** | `net_version`, `net_listening`, `net_peerCount` | | **Debug** | `debug_traceTransaction`, `debug_traceCall`, `debug_traceBlockByNumber` | See the [Ethereum JSON-RPC Specification](https://ethereum.org/en/developers/docs/apis/json-rpc/) for the complete method list. ## Example: Monitor Token Transfers ```ts import type { EthereumSchema } from '@rpckit/core/ethereum' import { webSocket } from '@rpckit/websocket/ethereum' const transport = webSocket('wss://ethereum-rpc.publicnode.com') // ERC-20 Transfer event signature const TRANSFER_SIG = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' // Token addresses const TOKENS = { USDC: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', USDT: '0xdAC17F958D2ee523a2206206994597C13D831ec7', DAI: '0x6B175474E89094C44Da98b954EescdeCB5BE3830', } // Subscribe to transfers for multiple tokens const unsub = await transport.subscribe( 'eth_subscribe', ['logs', { address: Object.values(TOKENS), topics: [TRANSFER_SIG] }], (log) => { const token = Object.entries(TOKENS).find( ([, addr]) => addr.toLowerCase() === log.address.toLowerCase() )?.[0] ?? 'Unknown' console.log(`${token} transfer in block ${log.blockNumber}`) } ) // Cleanup on shutdown process.on('SIGINT', async () => { await unsub() await transport.close() }) ``` # Cluster Transport The Cluster transport sends requests to multiple transports in parallel and requires M-of-N responses to agree (quorum) before returning. ## Installation ```bash npm i @rpckit/cluster ``` You'll also need at least one base transport: ```bash npm i @rpckit/websocket ``` ## Basic Usage ```ts import { webSocket } from '@rpckit/websocket' import { cluster } from '@rpckit/cluster' const transport = cluster([ webSocket('wss://node1.example.com'), webSocket('wss://node2.example.com'), webSocket('wss://node3.example.com') ], { quorum: 2 }) // Waits for 2-of-3 nodes to return the same result const result = await transport.request('blockchain.transaction.get', [txid]) ``` ## Configuration ### Options ```ts const transport = cluster(transports, { quorum: 2, // Required number of matching responses timeout: 15000 // Max time to wait for quorum }) ``` | Option | Type | Default | Description | |--------|------|---------|-------------| | `quorum` | `number` | **required** | Minimum matching responses needed | | `timeout` | `number` | `10000` | Timeout in milliseconds | ### Quorum Rules * `quorum` must be at least 1 * `quorum` must not exceed the number of transports * Responses are compared using deep equality ## How It Works 1. Request is sent to all transports in parallel 2. As responses arrive, they're grouped by value (deep equality) 3. When any group reaches `quorum` size, that value is returned 4. If timeout occurs before quorum, an error is thrown ``` Request: getBalance(addr) Node1 → { confirmed: 100 } ─┐ Node2 → { confirmed: 100 } ─┼─→ Quorum reached (2 matching) → Return { confirmed: 100 } Node3 → { confirmed: 100 } ─┘ (Node3 response ignored, already resolved) ``` ## Observability ### onResponse Monitor individual transport responses: ```ts transport.onResponse((info) => { console.log(`${info.transport}: ${info.status}`) if (info.status === 'success') { console.log('Result:', info.response) } else { console.log('Error:', info.error) } }) ``` ## Extended Interface ```ts interface ClusterTransport extends Transport { transports: Transport[] onResponse(callback: (info: TransportResponse) => void): Unsubscribe } ``` ## Single Transport Optimization When given a single transport, `cluster()` returns it unwrapped: ```ts const single = cluster([webSocket('wss://example.com')], { quorum: 1 }) // single is the WebSocketTransport, not wrapped in ClusterTransport ``` ## Example: Byzantine Fault Tolerance For a system that can tolerate `f` faulty nodes, use `3f + 1` nodes with quorum `2f + 1`: ```ts import { webSocket } from '@rpckit/websocket' import { cluster } from '@rpckit/cluster' // Tolerate 1 faulty node: need 4 nodes, quorum of 3 const transport = cluster([ webSocket('wss://node1.example.com'), webSocket('wss://node2.example.com'), webSocket('wss://node3.example.com'), webSocket('wss://node4.example.com') ], { quorum: 3 }) // Even if one node returns incorrect data, the correct result wins const balance = await transport.request('getBalance', [address]) ``` ## Example: Cross-Validation Validate responses across different server implementations: ```ts import { webSocket } from '@rpckit/websocket' import { http } from '@rpckit/http' import { cluster } from '@rpckit/cluster' const transport = cluster([ webSocket('wss://electrum-cash-server.com'), http('https://fulcrum-server.com/rpc'), webSocket('wss://other-server.com') ], { quorum: 2 }) // Request is validated across different implementations const tx = await transport.request('blockchain.transaction.get', [txid]) ``` ## Subscriptions Cluster transport supports subscriptions, but note that: 1. Subscription is established on all transports 2. Notifications from any transport trigger the callback 3. There's no quorum check for notifications (first notification wins) ```ts const unsubscribe = await transport.subscribe( 'blockchain.headers.subscribe', [], (header) => { // Called when ANY node sends a notification console.log('New block:', header.height) } ) ``` For strict validation of subscription data, implement your own aggregation logic. # Fallback Transport The Fallback transport wraps multiple transports and provides automatic failover. When a request fails, it tries the next transport in the list. ## Installation ```bash npm i @rpckit/fallback ``` You'll also need at least one base transport: ```bash npm i @rpckit/websocket @rpckit/tcp ``` ## Basic Usage ```ts import { webSocket } from '@rpckit/websocket' import { tcp } from '@rpckit/tcp' import { fallback } from '@rpckit/fallback' const transport = fallback([ webSocket('wss://primary.example.com'), webSocket('wss://backup1.example.com'), tcp('tcp+tls://backup2.example.com:50002') ]) // Automatically tries next transport on failure const result = await transport.request('my.method') ``` ## Configuration ### Options ```ts const transport = fallback(transports, { shouldThrow: (error) => false, // Never throw, always try next rank: true, // Enable health-based ranking eagerConnect: true // Connect to all transports immediately }) ``` | Option | Type | Default | Description | |--------|------|---------|-------------| | `shouldThrow` | `(error: Error) => boolean \| undefined` | Throws on parse/invalid request/invalid params | Return `true` to throw, `false` to try next, `undefined` for default | | `rank` | `boolean \| RankConfig` | `false` | Enable health-based transport ranking | | `eagerConnect` | `boolean` | `false` | Connect to all transports in parallel (fastest is prioritized) | ### Health Ranking When enabled, the fallback transport monitors transport health and reorders them by performance. **Note:** Without a `ping` function, ranking has no way to probe transport health and will not collect samples — effectively making it a no-op. Provide a `ping` function appropriate for your protocol. ```ts const transport = fallback(transports, { rank: { interval: 5000, // Sample every 5 seconds ping: (t) => t.request('health.check'), // provide a ping function for your protocol sampleCount: 10, timeout: 1000, weights: { latency: 0.3, stability: 0.7 } } }) ``` | Rank Option | Type | Default | Description | |-------------|------|---------|-------------| | `interval` | `number` | `4000` | Sampling interval in ms | | `ping` | `(transport) => Promise` | - | Function to probe transport health (ranking is a no-op without this) | | `sampleCount` | `number` | `10` | Number of samples to average | | `timeout` | `number` | `1000` | Ping timeout in ms | | `weights.latency` | `number` | `0.3` | Weight for latency score (0-1) | | `weights.stability` | `number` | `0.7` | Weight for stability score (0-1) | ## Electrum Cash Variant The `@rpckit/fallback/electrum-cash` subpath provides a variant with `server.ping` as the default ping function and a protocol-aware `shouldThrow` that retries on transient server errors (internal error, OOM, warmup, syncing) while stopping on deterministic errors: ```ts import { fallback } from '@rpckit/fallback/electrum-cash' import { webSocket } from '@rpckit/websocket/electrum-cash' const transport = fallback([ webSocket('wss://server1.example.com:50004'), webSocket('wss://server2.example.com:50004'), ], { rank: true }) // Uses server.ping for health checks automatically ``` With the electrum-cash variant, `rank: true` works out of the box. With the base variant, you must provide a `ping` function. The `shouldThrow` function is also exported for custom use: ```ts import { shouldThrow } from '@rpckit/fallback/electrum-cash' ``` ## Observability ### onScores Subscribe to health score updates: ```ts const transport = fallback(transports, { rank: { ping: (t) => t.request('health.check') } }) transport.onScores((scores) => { for (const { transport, score, latency, stability } of scores) { console.log(`Transport score: ${score}, latency: ${latency}ms, stability: ${stability}`) } }) ``` ### onResponse Subscribe to individual transport responses: ```ts transport.onResponse((info) => { console.log(`${info.method} -> ${info.status}`) if (info.status === 'error') { console.log('Error:', info.error) } }) ``` ## Extended Interface ```ts interface FallbackTransport extends Transport { transports: Transport[] scores: TransportScore[] onScores(callback: (scores: TransportScore[]) => void): Unsubscribe onResponse(callback: (info: TransportResponse) => void): Unsubscribe } ``` ## Single Transport Optimization When given a single transport, `fallback()` returns it unwrapped: ```ts const single = fallback([webSocket('wss://example.com')]) // single is the WebSocketTransport, not wrapped in FallbackTransport ``` ## Example: High Availability Setup ```ts import { fallback } from '@rpckit/fallback' import { webSocket } from '@rpckit/websocket' import { tcp } from '@rpckit/tcp' // Create transports with different priorities const transport = fallback([ // Primary: fast WebSocket connection webSocket('wss://primary.example.com'), // Secondary: another WebSocket webSocket('wss://secondary.example.com'), // Tertiary: TCP fallback tcp('tcp+tls://fallback.example.com:50002') ], { rank: { interval: 10000, // Check health every 10 seconds ping: (t) => t.request('health.check'), weights: { latency: 0.4, stability: 0.6 } } }) // Monitor health transport.onScores((scores) => { const best = scores[0] console.log(`Best transport: latency=${best.latency}ms, stability=${best.stability}`) }) // Use normally - failover is automatic await transport.connect() const result = await transport.request('my.method') ``` ## Subscriptions Fallback transport supports subscriptions. The subscription is established on the first working transport: ```ts const unsubscribe = await transport.subscribe( 'events.subscribe', ['channel-1'], (data) => { console.log('Event:', data) } ) ``` If the subscribed transport fails, the subscription is re-established on the next available transport. # HTTP Transport The HTTP transport provides stateless JSON-RPC communication over HTTP/HTTPS. It does not support subscriptions. ## Installation ```bash npm i @rpckit/http ``` ## Basic Usage ```ts import { http } from '@rpckit/http' const transport = http('https://example.com/rpc') const result = await transport.request('method', [param1, param2]) ``` ## Configuration ### URL String ```ts const transport = http('https://example.com/rpc') ``` ### Configuration Object ```ts const transport = http({ url: 'https://example.com/rpc', headers: { 'Authorization': 'Bearer token', 'X-Custom-Header': 'value' }, timeout: 10000, batch: { wait: 10, batchSize: 50 } }) ``` ### Options | Option | Type | Default | Description | |--------|------|---------|-------------| | `url` | `string` | - | HTTP(S) URL | | `headers` | `Record` | - | Custom request headers | | `timeout` | `number` | `30000` | Request timeout in milliseconds | | `batch` | `BatchConfig \| false` | `{ batchSize: 100 }` | Batching configuration | | `fetchFn` | `typeof fetch` | `globalThis.fetch` | Custom fetch implementation | | `fetchOptions` | `RequestInit` | - | Additional fetch options | | `raw` | `boolean` | `false` | Return RPC errors as results instead of throwing | | `onRequest` | `(req) => void` | - | Hook called before each request | | `onResponse` | `(res) => void` | - | Hook called after each response | ### Custom Fetch Use a custom fetch implementation: ```ts import { http } from '@rpckit/http' import nodeFetch from 'node-fetch' const transport = http({ url: 'https://example.com/rpc', fetchFn: nodeFetch }) ``` ### Request/Response Hooks ```ts const transport = http({ url: 'https://example.com/rpc', onRequest: (request) => { console.log('Sending:', request) }, onResponse: (response) => { console.log('Received:', response) } }) ``` ## Electrum Cash Variant For Electrum Cash servers, use the `electrum-cash` subpath which pre-configures the `Server-Version` header: ```ts import { http } from '@rpckit/http/electrum-cash' const transport = http('https://electrum.example.com', { clientName: 'myapp', // Client name in Server-Version header (default: 'rpckit') protocolVersion: '1.6', // Default }) const tip = await transport.request('blockchain.headers.get_tip') ``` ## Subscriptions HTTP transport does **not** support subscriptions. Calling `subscribe()` throws an error: ```ts const transport = http('https://example.com/rpc') await transport.subscribe('method', [], () => {}) // Error: HTTP transport does not support subscriptions ``` Use WebSocket or TCP transport if you need subscriptions. ## Batching Multiple concurrent requests are automatically batched: ```ts const transport = http('https://example.com/rpc') // These are sent as a single HTTP request with a batch JSON-RPC payload const [a, b, c] = await Promise.all([ transport.request('method1'), transport.request('method2'), transport.request('method3') ]) ``` Configure batch behavior: ```ts const transport = http({ url: 'https://example.com/rpc', batch: { wait: 50, // Wait 50ms to collect more requests batchSize: 20 // Max 20 requests per batch } }) ``` Disable batching: ```ts const transport = http({ url: 'https://example.com/rpc', batch: false // Each request is sent individually }) ``` ## Raw Mode By default, JSON-RPC errors are thrown as exceptions. Use raw mode to receive them as results: ```ts const transport = http({ url: 'https://example.com/rpc', raw: true }) const result = await transport.request('unknownMethod') // result: { error: { code: -32601, message: 'Method not found' } } // No exception thrown ``` ## Example: API Client ```ts import { http } from '@rpckit/http' type ApiSchema = { requests: [ { method: 'users.get'; params: [id: string]; return: User }, { method: 'users.list'; params: []; return: User[] }, { method: 'users.create'; params: [data: CreateUserData]; return: User } ] subscriptions: [] } const api = http({ url: 'https://api.example.com/rpc', headers: { 'Authorization': `Bearer ${token}` } }) // Typed API calls const user = await api.request('users.get', ['user-123']) const users = await api.request('users.list') const newUser = await api.request('users.create', [{ name: 'Alice', email: 'alice@example.com' }]) ``` # Transports Overview Transports are the core abstraction in rpckit. They handle the low-level communication with JSON-RPC servers. ## Transport Interface All transports implement the same interface: ```ts interface Transport { connect(): Promise request(method: M, params?: Params): Promise> subscribe(method: M, params: Params, callback: (data: Return) => void): Promise close(): Promise } ``` ### Methods | Method | Description | |--------|-------------| | `connect()` | Establish connection to the server | | `request(method, params?)` | Send a JSON-RPC request and wait for response | | `subscribe(method, params, callback)` | Subscribe to notifications (WebSocket/TCP only) | | `close()` | Close the connection and clean up resources | ## Transport Types ### Base Transports | Transport | Package | Protocols | Subscriptions | Environment | |-----------|---------|-----------|---------------|-------------| | [WebSocket](/docs/transports/websocket) | `@rpckit/websocket` | `ws://`, `wss://` | Yes | Browser, Node.js | | [TCP](/docs/transports/tcp) | `@rpckit/tcp` | `tcp://`, `tcp+tls://` | Yes | Node.js only | | [HTTP](/docs/transports/http) | `@rpckit/http` | `http://`, `https://` | No | Browser, Node.js | ### Meta-Transports Meta-transports wrap other transports to add functionality: | Transport | Package | Purpose | |-----------|---------|---------| | [Fallback](/docs/transports/fallback) | `@rpckit/fallback` | Failover across multiple transports | | [Cluster](/docs/transports/cluster) | `@rpckit/cluster` | M-of-N quorum consensus | ## Common Configuration All transports support these common options: ### Timeout Maximum time to wait for a response: ```ts const transport = webSocket('wss://example.com', { timeout: 10000 // 10 seconds (default: 30000) }) ``` ### Batching Combine multiple requests into a single JSON-RPC batch: ```ts const transport = webSocket('wss://example.com', { batch: { wait: 10, // Wait 10ms to collect requests (default: 0) batchSize: 100 // Max requests per batch (default: 100) } }) ``` Set `batch: false` to disable batching entirely. ### Retry Every `request()`, `subscribe()`, and `connect()` is wrapped in an automatic retry with exponential backoff: ```ts const transport = webSocket('wss://example.com', { retryCount: 3, // Max attempts (default: 3) retryDelay: 150 // Base delay in ms (default: 150) }) ``` When the server explicitly rate-limits the caller (HTTP 429 or a JSON-RPC error frame with `code === 429` / `data.http_status === 429`), the retry waits the server-supplied hint instead of using the backoff. ## Type Safety Transports can be typed with a schema for compile-time checking: ```ts type MySchema = { requests: [ { method: 'add'; params: [a: number, b: number]; return: number } ] subscriptions: [ { method: 'updates'; params: [topic: string]; return: string } ] } const transport = webSocket('wss://example.com') // TypeScript enforces correct usage await transport.request('add', [1, 2]) // OK, returns number await transport.request('unknown') // Type error! await transport.request('add', ['a', 'b']) // Type error! ``` ## Connection Management ### Lazy vs Eager Connection By default, transports connect lazily on first request: ```ts const transport = webSocket('wss://example.com') // Not connected yet await transport.request('method') // Now connected ``` For explicit control, call `connect()`: ```ts const transport = webSocket('wss://example.com') await transport.connect() // Connect immediately ``` ### Connection Sharing Transports with the same configuration share connections: ```ts const t1 = webSocket('wss://example.com') const t2 = webSocket('wss://example.com') // t1 and t2 share the same WebSocket connection ``` ### Cleanup Always close transports when done: ```ts const transport = webSocket('wss://example.com') try { await transport.request('method') } finally { await transport.close() } ``` ## Subscriptions WebSocket and TCP transports support subscriptions: ```ts const unsubscribe = await transport.subscribe( 'events.subscribe', ['channel-1'], (data) => { console.log('Received:', data) } ) // The initial response is delivered to the callback // Subsequent notifications are also delivered to the callback // Later, unsubscribe await unsubscribe() ``` Subscriptions are automatically restored after reconnection. Configure server-side cleanup with `onUnsubscribe`: ```ts const transport = webSocket('wss://example.com', { onUnsubscribe: ({ request, method, params }) => { return request(method.replace('subscribe', 'unsubscribe'), params) } }) ``` ## Protocol Variants Base transports are protocol-agnostic. For protocol-specific defaults, use the subpath variants: ```ts // Base (generic JSON-RPC) import { webSocket } from '@rpckit/websocket' // Electrum Cash (handshake, keepAlive, unsubscribe pre-configured) import { webSocket } from '@rpckit/websocket/electrum-cash' ``` Available variants: `@rpckit/websocket/electrum-cash`, `@rpckit/tcp/electrum-cash`, `@rpckit/http/electrum-cash`, `@rpckit/fallback/electrum-cash` ## Error Handling Transports throw errors for: * Connection failures * Request timeouts * JSON-RPC errors from the server * Server-side rate limits — once `retryCount` is exhausted, the final attempt rejects with `RateLimitError` (carrying the server's `retryAfterMs` hint) ```ts import { RateLimitError } from '@rpckit/core' try { await transport.request('method') } catch (error) { if (error instanceof RateLimitError) { // Server is throttling; error.retryAfterMs holds the last hint } else if (error.code === -32600) { // Invalid request } else if (error.code === -32601) { // Method not found } } ``` # TCP Transport The TCP transport provides newline-delimited JSON-RPC communication with optional TLS encryption. Node.js only. ## Installation ```bash npm i @rpckit/tcp ``` ## Basic Usage ```ts import { tcp } from '@rpckit/tcp' const transport = tcp('tcp://example.com:50001') await transport.connect() const result = await transport.request('server.version', ['client', '1.4']) await transport.close() ``` ## Configuration ### URL String ```ts // Plain TCP const transport = tcp('tcp://example.com:50001') // TCP with TLS const transport = tcp('tcp+tls://example.com:50002') ``` ### Configuration Object ```ts const transport = tcp({ host: 'example.com', port: 50002, tls: true, timeout: 10000, batch: { wait: 10, batchSize: 50 }, keepAlive: { interval: 30000, method: 'server.ping' }, reconnect: { delay: 1000, attempts: 5 } }) ``` ### Options | Option | Type | Default | Description | |--------|------|---------|-------------| | `url` | `string` | - | TCP URL (`tcp://` or `tcp+tls://`) | | `host` | `string` | - | Server hostname (alternative to url) | | `port` | `number` | - | Server port (alternative to url) | | `tls` | `boolean \| TLSOptions` | `false` | Enable TLS encryption | | `timeout` | `number` | `30000` | Request timeout in milliseconds | | `connectTimeout` | `number` | - | Connection timeout in milliseconds | | `batch` | `BatchConfig \| false` | `{ batchSize: 100 }` | Batching configuration (`batchSize`, `wait`, `disabledCooldown`) | | `keepAlive` | `KeepAliveConfig` | - | Keep-alive ping configuration | | `reconnect` | `{ delay, attempts }` | - | Auto-reconnect after disconnect | ### TLS Configuration Simple TLS: ```ts const transport = tcp({ host: 'example.com', port: 50002, tls: true }) ``` Custom TLS options: ```ts const transport = tcp({ host: 'example.com', port: 50002, tls: { ca: fs.readFileSync('ca.pem'), cert: fs.readFileSync('client.pem'), key: fs.readFileSync('client-key.pem'), rejectUnauthorized: true } }) ``` ## Electrum Cash Variant For Electrum Cash servers, use the `electrum-cash` subpath: ```ts import { tcp } from '@rpckit/tcp/electrum-cash' const transport = tcp('tcp+tls://electrum.example.com:50002', { keepAlive: 60000, // Uses server.ping automatically clientName: 'myapp', // Client name in handshake (default: 'rpckit') protocolVersion: '1.6', // Default }) // server.version handshake is sent automatically // onUnsubscribe derives method from subscribe method ``` ## Subscriptions TCP transport supports subscriptions like WebSocket: ```ts const unsubscribe = await transport.subscribe( 'events.subscribe', ['channel-1'], (data) => { console.log('Event:', data) } ) // Later, unsubscribe await unsubscribe() ``` ### Subscription Sharing Multiple callers subscribing to the same method+params share a single server subscription. New subscribers receive the most recent notification data (not stale initial data). The server unsubscribe is only sent when the last listener unsubscribes. ```ts // Both callbacks share one server subscription const unsub1 = await transport.subscribe('events', [], callback1) const unsub2 = await transport.subscribe('events', [], callback2) await unsub1() // callback1 removed, server subscription stays active await unsub2() // callback2 removed, NOW server unsubscribe is sent ``` ## Protocol The TCP transport uses newline-delimited JSON: * Each JSON-RPC message is a single line terminated by `\n` * Messages are UTF-8 encoded * Batch requests are sent as a single JSON array on one line ## Extended Interface ```ts interface TcpTransport extends Transport { getSocket(): Socket | TLSSocket | null getSocketAsync(): Promise } ``` ## Example: Fulcrum Connection ```ts import { tcp } from '@rpckit/tcp/electrum-cash' const transport = tcp({ host: 'electrum.example.com', port: 50002, tls: true, keepAlive: 60000 }) await transport.connect() // Get chain tip (handshake already sent) const tip = await transport.request('blockchain.headers.get_tip') console.log('Block height:', tip.height) // Subscribe to block headers await transport.subscribe( 'blockchain.headers.subscribe', [], (header) => { console.log('New block:', header.height) } ) ``` # WebSocket Transport The WebSocket transport provides full-duplex communication with JSON-RPC servers, including support for subscriptions. ## Installation ```bash npm i @rpckit/websocket ``` ## Basic Usage ```ts import { webSocket } from '@rpckit/websocket' const transport = webSocket('wss://example.com:50004') await transport.connect() const result = await transport.request('server.version', ['client', '1.4']) await transport.close() ``` ## Configuration ### URL String ```ts const transport = webSocket('wss://example.com:50004') ``` ### Configuration Object ```ts const transport = webSocket({ url: 'wss://example.com:50004', timeout: 10000, batch: { wait: 10, batchSize: 50 }, keepAlive: { interval: 30000, method: 'server.ping' }, reconnect: { delay: 1000, attempts: 5 }, headers: { 'Authorization': 'Bearer token' } }) ``` ### Options | Option | Type | Default | Description | |--------|------|---------|-------------| | `url` | `string` | - | WebSocket URL (`ws://` or `wss://`) | | `timeout` | `number` | `30000` | Request timeout in milliseconds | | `connectTimeout` | `number` | - | Connection timeout in milliseconds | | `batch` | `BatchConfig \| false` | `{ batchSize: 100 }` | Batching configuration (`batchSize`, `wait`, `disabledCooldown`) | | `keepAlive` | `KeepAliveConfig` | - | Keep-alive ping configuration | | `reconnect` | `{ delay, attempts }` | - | Auto-reconnect after disconnect | | `headers` | `Record` | - | Headers for WebSocket handshake | | `handshake` | `HandshakeConfig` | - | Custom handshake after connection | ### Keep-Alive Send periodic pings to keep the connection alive: ```ts const transport = webSocket('wss://example.com', { keepAlive: { interval: 30000, // Ping every 30 seconds method: 'server.ping', // RPC method to call params: [] // Optional params } }) ``` ### Handshake Execute a custom handshake after connection: ```ts const transport = webSocket('wss://example.com', { handshake: { method: 'server.version', params: ['my-client', '1.4'] } }) ``` ### Unsubscribe Callback Configure how subscriptions are cleaned up on the server: ```ts const transport = webSocket('wss://example.com', { onUnsubscribe: ({ request, method, params }) => { // Derive unsubscribe method from subscribe method return request(method.replace('subscribe', 'unsubscribe'), params) } }) ``` Without `onUnsubscribe`, calling `unsub()` only removes the local listener. With it, the transport also notifies the server. ## Electrum Cash Variant For Electrum Cash servers, use the `electrum-cash` subpath which pre-configures protocol-specific defaults: ```ts import { webSocket } from '@rpckit/websocket/electrum-cash' const transport = webSocket('wss://electrum.example.com:50004', { keepAlive: 30000, // Uses server.ping automatically clientName: 'myapp', // Client name in handshake (default: 'rpckit') protocolVersion: '1.6', // Default }) // server.version handshake is sent automatically // onUnsubscribe derives method from subscribe method ``` ## Ethereum Variant For Ethereum JSON-RPC nodes, use the `ethereum` subpath which handles `eth_subscription` notification routing: ```ts import { webSocket } from '@rpckit/websocket/ethereum' const transport = webSocket('wss://ethereum-rpc.publicnode.com') // Standard requests const blockNumber = await transport.request('eth_blockNumber') // Subscriptions - notifications routed by subscription ID automatically const unsub = await transport.subscribe('eth_subscribe', ['newHeads'], (header) => { console.log('New block:', header.number) }) // eth_unsubscribe called automatically await unsub() ``` The Ethereum variant automatically: * Routes `eth_subscription` notifications to the correct callback by subscription ID * Calls `eth_unsubscribe` on cleanup * Suppresses subscription IDs from callbacks (handled internally) ## Subscriptions Subscribe to server notifications: ```ts const unsubscribe = await transport.subscribe( 'events.subscribe', ['channel-1'], (data) => { console.log('Event:', data) } ) // Later, unsubscribe await unsubscribe() ``` ### Subscription Behavior 1. The subscribe method sends the subscription request 2. The initial response is delivered to the callback 3. Subsequent notifications for the same subscription are delivered to the callback 4. Calling `unsubscribe()` invokes the `onUnsubscribe` callback if configured ### Subscription Sharing Multiple callers subscribing to the same method+params share a single server subscription. New subscribers receive the most recent notification data (not stale initial data). The server unsubscribe is only sent when the last listener unsubscribes. ```ts // Both callbacks share one server subscription const unsub1 = await transport.subscribe('events', [], callback1) const unsub2 = await transport.subscribe('events', [], callback2) await unsub1() // callback1 removed, server subscription stays active await unsub2() // callback2 removed, NOW server unsubscribe is sent ``` ### Automatic Resubscription Subscriptions are automatically restored after reconnection. The transport tracks active subscriptions and re-sends them when the connection is re-established. ## Extended Interface The WebSocket transport extends the base `Transport` interface: ```ts interface WebSocketTransport extends Transport { getSocket(): WebSocket | null getSocketAsync(): Promise } ``` ### getSocket() Returns the current WebSocket instance, or `null` if not connected: ```ts const socket = transport.getSocket() if (socket) { console.log('Ready state:', socket.readyState) } ``` ### getSocketAsync() Waits for connection and returns the WebSocket: ```ts const socket = await transport.getSocketAsync() console.log('Connected!') ``` ## Connection Pooling Transports with identical configuration share the same WebSocket connection: ```ts const t1 = webSocket('wss://example.com') const t2 = webSocket('wss://example.com') // t1 and t2 share the same underlying WebSocket // The connection is reference-counted and closed when all references are closed ``` ## Example: Full Application ```ts import { webSocket } from '@rpckit/websocket/electrum-cash' const transport = webSocket({ url: 'wss://electrum.example.com:50004', keepAlive: 30000, reconnect: { delay: 1000, attempts: 10, } }) // Connect (handshake sent automatically) await transport.connect() // Make typed requests const tip = await transport.request('blockchain.headers.get_tip') console.log(`Block height: ${tip.height}`) // Subscribe to address updates (unsubscribe method derived automatically) const unsubscribe = await transport.subscribe( 'blockchain.address.subscribe', [address], (status) => { console.log('Address status changed:', status) } ) // Keep running until shutdown process.on('SIGINT', async () => { await unsubscribe() await transport.close() }) ``` # BatchScheduler The `BatchScheduler` class collects individual JSON-RPC requests and sends them as batches. It's used internally by transports but can be used directly for custom batching logic. ## Installation ```bash npm i @rpckit/core ``` ## Basic Usage ```ts import { BatchScheduler } from '@rpckit/core' const scheduler = new BatchScheduler( { batchSize: 100, wait: 10 }, async (requests) => { // Send batch to server and return responses const response = await fetch('https://example.com/rpc', { method: 'POST', body: JSON.stringify(requests), headers: { 'Content-Type': 'application/json' } }) return response.json() } ) // Enqueue requests - they're batched automatically const result1 = scheduler.enqueue({ jsonrpc: '2.0', id: 1, method: 'method1', params: [] }) const result2 = scheduler.enqueue({ jsonrpc: '2.0', id: 2, method: 'method2', params: [] }) // Both requests are sent together const [r1, r2] = await Promise.all([result1, result2]) ``` ## Configuration ### Options | Option | Type | Default | Description | |--------|------|---------|-------------| | `batchSize` | `number` | `100` | Maximum requests per batch. When reached, batch is sent immediately. | | `wait` | `number` | `0` | Maximum time (ms) to wait before sending a batch. | | `raw` | `boolean` | `false` | Return full RPC response objects instead of just results. | | `disabledCooldown` | `number` | `5000` | Cooldown in ms before re-enabling batching after a server rejection. Set to `0` to disable auto-recovery. | | `sendSingle` | `function` | - | Callback to send a single request individually. Enables auto-disable on batch rejection. | | `isBatchRejection` | `function` | - | Custom predicate to detect batch rejection errors. | ### Batch Size The `batchSize` option controls how many requests are collected before sending: ```ts const scheduler = new BatchScheduler( { batchSize: 10 }, sendBatch ) // When 10 requests are enqueued, they're sent immediately for (let i = 0; i < 10; i++) { scheduler.enqueue({ jsonrpc: '2.0', id: i, method: 'ping', params: [] }) } // ^ Batch sent after 10th request ``` ### Wait Time The `wait` option sets a timer after the first request is enqueued: ```ts const scheduler = new BatchScheduler( { batchSize: 100, wait: 50 }, sendBatch ) scheduler.enqueue(request1) // Timer starts (50ms) scheduler.enqueue(request2) // Added to batch // ... 50ms later, batch is sent even if batchSize not reached ``` ### Raw Mode By default, `enqueue()` resolves with `response.result`. In raw mode, it resolves with the full response: ```ts const scheduler = new BatchScheduler( { raw: true }, sendBatch ) const response = await scheduler.enqueue(request) // response: { jsonrpc: '2.0', id: 1, result: 'value' } // or: { jsonrpc: '2.0', id: 1, error: { code: -32601, message: '...' } } ``` ## Methods ### enqueue(request) Adds a request to the batch queue. Returns a promise that resolves when the batch is sent and the response is received. ```ts const promise = scheduler.enqueue({ jsonrpc: '2.0', id: 1, method: 'getBalance', params: ['0x...'] }) const result = await promise ``` ### flush() Immediately sends any pending requests: ```ts scheduler.enqueue(request1) scheduler.enqueue(request2) // Don't wait for timer or batchSize - send now await scheduler.flush() ``` ## Auto-Disable on Batch Rejection When a server can't handle batch requests (e.g. the batch is too large, or the server doesn't support batching), the scheduler can automatically fall back to sending requests individually. This behavior is enabled when a `sendSingle` callback is provided. The built-in WebSocket and TCP transports provide this automatically. ### How It Works 1. A batch is sent to the server 2. The server rejects the batch (timeout, parse error, or invalid request) 3. The scheduler detects the rejection and disables batching 4. Failed requests are retried individually via `sendSingle` 5. Subsequent requests bypass the batch queue and are sent individually 6. After `disabledCooldown` ms (default: 5 seconds), batching is re-enabled ### Detection By default, the following errors trigger auto-disable: * **Batch timeout** — the server couldn't process the batch in time * **Parse error** (JSON-RPC code `-32700`) — the server couldn't parse the batch array * **Invalid request** (JSON-RPC code `-32600`) — the server rejected the batch format You can provide a custom `isBatchRejection` predicate for other error patterns: ```ts const scheduler = new BatchScheduler( { batchSize: 100, sendSingle: (req) => sendIndividualRequest(req), isBatchRejection: (error) => { // Custom detection logic return error instanceof Error && error.message.includes('rate limit') } }, sendBatch ) ``` ### Checking Status The `disabled` property indicates whether batching is currently disabled: ```ts if (scheduler.disabled) { console.log('Batching is temporarily disabled') } ``` ### Transport Integration The WebSocket and TCP transports enable auto-disable by default. Configure the cooldown via the `batch` option: ```ts import { webSocket } from '@rpckit/websocket/electrum-cash' const transport = webSocket('wss://electrum.example.com', { batch: { batchSize: 100, wait: 10, disabledCooldown: 10_000 // Re-enable after 10 seconds } }) // If the server rejects a batch, the transport transparently // falls back to individual requests and recovers automatically ``` ## Error Handling ### RPC Errors By default, RPC errors are thrown: ```ts try { const result = await scheduler.enqueue({ jsonrpc: '2.0', id: 1, method: 'unknownMethod', params: [] }) } catch (error) { // error: { code: -32601, message: 'Method not found' } } ``` With `raw: true`, errors are returned instead of thrown: ```ts const scheduler = new BatchScheduler({ raw: true }, sendBatch) const response = await scheduler.enqueue(request) if (response.error) { console.log('RPC error:', response.error) } else { console.log('Result:', response.result) } ``` ### Missing Responses If the server doesn't return a response for a request, an error is thrown: ```ts const result = await scheduler.enqueue(request) // Error: No response for request id 1, try reducing batch size ``` ### Network Errors Network errors reject all pending requests in the batch: ```ts const results = await Promise.allSettled([ scheduler.enqueue(request1), scheduler.enqueue(request2) ]) // Both rejected with the same network error ``` ## Example: Custom Transport ```ts import { BatchScheduler } from '@rpckit/core' import type { RpcRequest, RpcResponse } from '@rpckit/core' function createBatchingTransport(url: string) { const scheduler = new BatchScheduler( { batchSize: 50, wait: 10 }, async (requests: RpcRequest[]): Promise => { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requests) }) if (!response.ok) { throw new Error(`HTTP ${response.status}`) } return response.json() } ) let requestId = 0 return { async request(method: string, params: unknown[] = []) { return scheduler.enqueue({ jsonrpc: '2.0', id: ++requestId, method, params }) }, async flush() { await scheduler.flush() } } } // Usage const transport = createBatchingTransport('https://api.example.com/rpc') const [a, b, c] = await Promise.all([ transport.request('getA'), transport.request('getB'), transport.request('getC') ]) // All three sent in single HTTP request ``` ## How Batching Works 1. First `enqueue()` call starts a timer (if `wait > 0`) 2. Subsequent calls add to the queue 3. Batch is sent when either: * Queue reaches `batchSize` * Timer expires (`wait` ms elapsed) * `flush()` is called manually 4. Responses are matched to requests by `id` 5. Each enqueued promise resolves/rejects with its response # parse() The `parse()` utility creates transports from URL-like one-liner strings. It dynamically loads the required transport packages at runtime. ## Installation ```bash npm i @rpckit/core ``` You must also install the transport packages you want to use: ```bash npm i @rpckit/websocket @rpckit/tcp @rpckit/http @rpckit/fallback @rpckit/cluster ``` ## Basic Usage ```ts import { parse } from '@rpckit/core' const transport = await parse('wss://example.com') await transport.connect() const result = await transport.request('method', [param1, param2]) ``` ## Supported Schemes | Scheme | Transport | Package | |--------|-----------|---------| | `ws://`, `wss://` | WebSocket | `@rpckit/websocket` | | `tcp://`, `tcp+tls://` | TCP | `@rpckit/tcp` | | `http://`, `https://` | HTTP | `@rpckit/http` | | `fallback(...)` | Fallback | `@rpckit/fallback` | | `cluster(quorum,...)` | Cluster | `@rpckit/cluster` | ## Simple Transports ### WebSocket ```ts const ws = await parse('wss://example.com') const wss = await parse('wss://example.com:8443/path') ``` ### TCP ```ts const tcp = await parse('tcp://example.com:50001') const tcpTls = await parse('tcp+tls://example.com:50002') ``` ### HTTP ```ts const http = await parse('https://example.com/rpc') ``` ## Meta-Transports ### Fallback Creates a fallback transport that tries transports in order: ```ts const fb = await parse('fallback(wss://primary.com,wss://backup.com)') ``` With health ranking enabled: ```ts const ranked = await parse('fallback(wss://a.com,wss://b.com)?rank=true') ``` ### Cluster Creates a cluster transport requiring quorum consensus. The first argument is the quorum number: ```ts // Require 2-of-3 agreement const cluster = await parse('cluster(2,wss://node1.com,wss://node2.com,wss://node3.com)') ``` ## Options Options are specified as query parameters: ```ts const transport = await parse('wss://example.com?timeout=10000&keepAlive=30000') ``` ### Supported Options | Option | Type | Description | |--------|------|-------------| | `timeout` | `number` | Request timeout in milliseconds | | `keepAlive` | `number` | Keep-alive ping interval in milliseconds | | `batch` | `boolean` | Enable/disable batching | | `batchSize` | `number` | Maximum requests per batch | | `batchWait` | `number` | Maximum wait time before flushing batch (ms) | | `disabledCooldown` | `number` | Cooldown in ms before re-enabling batching after server rejection (default: `5000`) | | `rank` | `boolean` | Enable health ranking for fallback transport | | `eagerConnect` | `boolean` | Connect all fallback transports in parallel | | `retryCount` | `number` | Number of retry attempts | | `retryDelay` | `number` | Base delay between retries (ms) | | `clientName` | `string` | Client name for electrum-cash handshake (default: `'rpckit'`) | | `protocolVersion` | `string` | Protocol version for electrum-cash handshake (default: `'1.6'`) | ### Batching Options ```ts // Enable batching with custom settings const transport = await parse('wss://example.com?batchSize=10&batchWait=50') // Custom batch cooldown (re-enable batching after 10 seconds) const transport = await parse('wss://example.com?batchSize=10&disabledCooldown=10000') ``` ### Fallback Options ```ts // Ranked fallback with eager connection const transport = await parse('fallback(wss://a.com,wss://b.com)?rank=true&eagerConnect=true') ``` ### Cluster Options ```ts // Cluster with custom timeout const transport = await parse('cluster(2,wss://a.com,wss://b.com,wss://c.com)?timeout=5000') ``` ## Nested Transports Meta-transports can be nested: ```ts // Fallback with a cluster as backup const nested = await parse('fallback(wss://primary.com,cluster(2,wss://a.com,wss://b.com,wss://c.com))') ``` ## Protocol-Specific Parse ### createParse Use `createParse` to create a `parse` function that uses different packages for transport creation: ```ts import { createParse } from '@rpckit/core' const electrumParse = createParse({ websocket: '@rpckit/websocket/electrum-cash', tcp: '@rpckit/tcp/electrum-cash', http: '@rpckit/http/electrum-cash', }) // Transports created by electrumParse have Electrum Cash defaults const transport = await electrumParse('wss://electrum.example.com') ``` ### createParseSync Use `createParseSync` to create a synchronous `parse` function using pre-imported factory functions. Unlike `createParse` which uses dynamic imports, this accepts already-loaded factories: ```ts import { createParseSync } from '@rpckit/core' import { webSocket } from '@rpckit/websocket/electrum-cash' import { fallback } from '@rpckit/fallback' const parse = createParseSync({ webSocket, fallback }) const transport = parse('fallback(wss://a.com,wss://b.com)?eagerConnect=true') ``` Factory keys are normalized automatically (`webSocket` maps to `websocket`). ### Pre-Built Electrum Cash Parse A pre-configured parse for Electrum Cash is available: ```ts import { parse } from '@rpckit/core/electrum-cash' const transport = await parse('wss://electrum.example.com') // Handshake, keepAlive method, and unsubscribe conventions are pre-configured // Custom client name and protocol version const custom = await parse('wss://electrum.example.com?clientName=myapp&protocolVersion=1.5') ``` ### Pre-Built Ethereum Parse A pre-configured parse for Ethereum is available: ```ts import { parse } from '@rpckit/core/ethereum' const transport = await parse('wss://ethereum-rpc.publicnode.com') // Uses Ethereum transport variants with eth_subscription routing ``` ## Type Safety Use generics to type the transport: ```ts import { parse } from '@rpckit/core/electrum-cash' import type { ElectrumCashSchema } from '@rpckit/core/electrum-cash' const transport = await parse('wss://electrum.example.com') // Typed request const balance = await transport.request('blockchain.address.get_balance', [address]) ``` ## Error Handling If a required package is not installed, `parse()` throws an error: ```ts try { const transport = await parse('wss://example.com') } catch (error) { // Error: Package @rpckit/websocket is not installed. Run: npm install @rpckit/websocket } ``` Invalid URLs also throw errors: ```ts await parse('invalid://example.com') // Error: Unknown scheme: invalid ``` ## Example: Configuration-Driven Setup ```ts import { parse } from '@rpckit/core' // Configuration from environment or config file const config = { transport: process.env.RPC_TRANSPORT || 'wss://localhost:8080' } async function createClient() { const transport = await parse(config.transport) await transport.connect() return transport } // Switch between transports without code changes // RPC_TRANSPORT='fallback(wss://primary.com,wss://backup.com)?rank=true' // RPC_TRANSPORT='cluster(2,wss://a.com,wss://b.com,wss://c.com)' // RPC_TRANSPORT='tcp+tls://electrum.example.com:50002' ``` # withRetry() The `withRetry()` utility executes an async function with exponential backoff retry logic. ## Installation ```bash npm i @rpckit/core ``` ## Basic Usage ```ts import { withRetry } from '@rpckit/core' const result = await withRetry(async () => { const response = await fetch('https://api.example.com/data') if (!response.ok) throw new Error('Request failed') return response.json() }) ``` ## Configuration ### Options | Option | Type | Default | Description | |--------|------|---------|-------------| | `retryCount` | `number` | `3` | Number of retry attempts after initial failure | | `retryDelay` | `number` | `150` | Base delay in milliseconds between retries | ### Custom Retry Count ```ts const result = await withRetry( async () => fetchData(), { retryCount: 5 } // 5 retries = 6 total attempts ) ``` ### Custom Retry Delay ```ts const result = await withRetry( async () => fetchData(), { retryDelay: 1000 } // Start with 1 second delay ) ``` ## Exponential Backoff Delays increase exponentially with each retry: ``` Attempt 1: immediate Attempt 2: retryDelay * 1 (150ms default) Attempt 3: retryDelay * 2 (300ms) Attempt 4: retryDelay * 4 (600ms) Attempt 5: retryDelay * 8 (1200ms) ... ``` The formula is: `delay = retryDelay * 2^(attempt - 1)` ### Example Timeline With default options (`retryCount: 3`, `retryDelay: 150`): ``` 0ms - Attempt 1 (fails) 150ms - Attempt 2 (fails) 450ms - Attempt 3 (fails) 1050ms - Attempt 4 (final attempt) ``` With `retryDelay: 1000`: ``` 0ms - Attempt 1 (fails) 1000ms - Attempt 2 (fails) 3000ms - Attempt 3 (fails) 7000ms - Attempt 4 (final attempt) ``` ## Error Handling If all attempts fail, the last error is thrown: ```ts try { await withRetry(async () => { throw new Error('Always fails') }, { retryCount: 2 }) } catch (error) { // Error from the final (3rd) attempt console.log(error.message) // 'Always fails' } ``` ## Example: Retrying Transport Requests ```ts import { withRetry } from '@rpckit/core' import { webSocket } from '@rpckit/websocket' const transport = webSocket('wss://example.com') await transport.connect() // Retry failed requests const balance = await withRetry( () => transport.request('getBalance', ['0x...']), { retryCount: 3, retryDelay: 200 } ) ``` ## Example: Retrying Connection ```ts import { withRetry } from '@rpckit/core' import { webSocket } from '@rpckit/websocket' async function connectWithRetry() { const transport = webSocket('wss://example.com') await withRetry( () => transport.connect(), { retryCount: 5, retryDelay: 1000 } ) return transport } ``` ## Example: Conditional Retry Wrap your function to only retry specific errors: ```ts import { withRetry } from '@rpckit/core' class RetryableError extends Error { constructor(message: string) { super(message) this.name = 'RetryableError' } } const result = await withRetry(async () => { try { return await riskyOperation() } catch (error) { // Only retry network errors if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') { throw new RetryableError(error.message) } // Don't retry other errors - rethrow immediately throw error } }) ``` ## Example: With Logging ```ts import { withRetry } from '@rpckit/core' let attempt = 0 const result = await withRetry(async () => { attempt++ console.log(`Attempt ${attempt}...`) const response = await fetch('https://api.example.com/data') if (!response.ok) { throw new Error(`HTTP ${response.status}`) } return response.json() }, { retryCount: 3 }) // Output: // Attempt 1... // Attempt 2... (if first fails) // Attempt 3... (if second fails) // Attempt 4... (if third fails) ``` ## Comparison with Transport Retry Transports have built-in retry options. Use `withRetry()` when you need: * Custom retry logic * Retry for operations other than requests * Different retry settings per operation ```ts // Transport-level retry (applies to all requests) const transport = webSocket({ url: 'wss://example.com', retry: { retryCount: 3 } }) // Operation-level retry (for specific operations) const criticalData = await withRetry( () => transport.request('criticalMethod'), { retryCount: 10, retryDelay: 500 } ) ```