Skip to content
Logo

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

npm i @rpckit/fallback

You'll also need at least one base transport:

npm i @rpckit/websocket @rpckit/tcp

Basic Usage

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

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
})
OptionTypeDefaultDescription
shouldThrow(error: Error) => boolean | undefinedThrows on parse/invalid request/invalid paramsReturn true to throw, false to try next, undefined for default
rankboolean | RankConfigfalseEnable health-based transport ranking
eagerConnectbooleanfalseConnect 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.

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 OptionTypeDefaultDescription
intervalnumber4000Sampling interval in ms
ping(transport) => Promise<unknown>-Function to probe transport health (ranking is a no-op without this)
sampleCountnumber10Number of samples to average
timeoutnumber1000Ping timeout in ms
weights.latencynumber0.3Weight for latency score (0-1)
weights.stabilitynumber0.7Weight 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:

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:

import { shouldThrow } from '@rpckit/fallback/electrum-cash'

Observability

onScores

Subscribe to health score updates:

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:

transport.onResponse((info) => {
  console.log(`${info.method} -> ${info.status}`)
  if (info.status === 'error') {
    console.log('Error:', info.error)
  }
})

Extended Interface

interface FallbackTransport<S extends Schema> extends Transport<S> {
  transports: Transport<S>[]
  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:

const single = fallback([webSocket('wss://example.com')])
// single is the WebSocketTransport, not wrapped in FallbackTransport

Example: High Availability Setup

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:

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.