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 —
sendBatchnow serializesrequests[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.
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 carryingretryAfterMs(and optionalcause). 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 waitsretryAfterMsinstead ofretryDelay * 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 genericError, someRateLimitError) each use the appropriate delay shape. -
HTTP transport — on
HTTP 429, parses the retry hint from headers in priority order:retry-after-ms— sub-second precision (used by services where a sub-second bucket refill would round to 0/1 underRetry-After's integer-seconds-only encoding).Retry-After— RFC 9110 §10.2.3 integer seconds → ms.- Conservative 1s fallback.
HTTP-date form of
Retry-Afteris intentionally not parsed — the millisecond-precision use case requires the delta form. -
WebSocket transport — on an incoming JSON-RPC error frame with
code === 429ordata.http_status === 429, extractsdata.retry_after_ms(1s fallback) and rejects the pending entry withRateLimitError. 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.
// 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 likerequest('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 acceptsunknown[] | Record<string, unknown>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
paramsfield — no ambiguity. daemon.passthrough— Uses plain object params naturally inElectrumCashSchema.
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()andclose()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— AddedsendSingle,isBatchRejection, anddisabledCooldownoptions. Addeddisabledproperty.- WebSocket & TCP transports — Now provide a
sendSinglefallback toBatchScheduler, enabling transparent auto-disable on batch rejection. parse()— AddeddisabledCooldownquery parameter support (e.g.wss://example.com?batchSize=10&disabledCooldown=10000).BatchConfig— AddeddisabledCooldownoption.bump.mjs— Skip package directories withoutpackage.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
Transportinterface withrequest(),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
BatchSchedulerfor automatic request batching with configurable batch size and wait timewithRetry()utility with exponential backoffparse()for creating transports from URL strings (supports nested meta-transports and query-string options)createParse()for building custom parse functions with overridden package mapscreateParseSync()for building synchronous parse functions using pre-imported factory functionsElectrumCashSchematype definitions (protocol v1.5 and v1.6)EthereumSchematype 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
shouldThrowstops fallback on deterministic JSON-RPC errors (parse error, invalid request, invalid params) eagerConnectprioritizes the fastest-connecting transportonScoreslistener andscoresproperty for monitoring transport healthonResponsehook for observing requests across all transports
- Default
- Cluster — m-of-n quorum consensus with deep-equality response matching
onResponsehook for observing individual transport responses
- Single-element passthrough —
fallback([t])andcluster([t])return the input transport directly
Electrum Cash Variants
Protocol-specific subpath imports (@rpckit/*/electrum-cash) with pre-configured defaults:
server.versionhandshake with configurableclientNameandprotocolVersionserver.pingkeep-alivesubscribe/unsubscribemethod conventionServer-VersionHTTP header- Fallback variant with
server.pinghealth probing and protocol-awareshouldThrow(retries transient server errors like OOM, warmup, syncing)
Ethereum Variants
Protocol-specific subpath imports (@rpckit/*/ethereum) for Ethereum JSON-RPC:
eth_subscriptionnotification routing by subscription IDeth_unsubscribecalled 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()andgetSocketAsync()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
notificationFiltercallback for protocol-specific notification routingtransformInitialResultoption to normalize or suppress initial subscription results- HTTP transport includes response body in error messages for better debugging