npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@novasamatech/host-substrate-chain-connection

v0.7.9-3

Published

Chain connection pool with ref counting and provider branching for Polkadot API

Readme

@novasamatech/host-substrate-chain-connection

Reference-counted connection pool for polkadot-api. Connections are created on first use, shared across callers, and destroyed when the last caller releases them.

  • Shared connections - one underlying WebSocket (or light client) per chain, multiplexed across consumers
  • Automatic lifecycle - ref-counted; opens on first acquire, closes when the last caller releases
  • Flexible resolution - transform the raw PolkadotClient into any app-specific type via resolve
  • Metadata caching - optional persistent cache so polkadot-api skips re-fetching metadata on reconnect
  • Status tracking - subscribe to per-chain connection status changes

Install

npm install @novasamatech/host-substrate-chain-connection

Quick start

import {
  createChainConnection,
  createWsJsonRpcProvider,
  type ChainConfig,
} from '@novasamatech/host-substrate-chain-connection';
import { dot } from '@polkadot-api/descriptors';

// `ChainConfig` only requires `genesisHash` — extend it with whatever fields
// your app needs (here, the WebSocket endpoints to dial).
type Chain = ChainConfig & { nodes: string[] };

const polkadot: Chain = {
  genesisHash: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3',
  nodes: ['wss://rpc.polkadot.io'],
};

const chains = createChainConnection<Chain>({
  createProvider: (chain, onStatusChanged) =>
    createWsJsonRpcProvider({
      endpoints: chain.nodes,
      onStatusChanged,
    }),
});

// Acquire a connection, run a query, then release
const { api: client, unlock } = await chains.lockApi(polkadot);
const api = client.getTypedApi(dot);
const account = await api.query.System.Account.getValue('5GrwvaEF...');
unlock();

Table of contents

API

createChainConnection(config)

Creates a connection pool. Generic over your chain config C and resolved API type T.

function createChainConnection<C extends ChainConfig, T = PolkadotClient>(
  config: ChainConnectionConfig<C, T>,
): ChainConnection<C, T>;

ChainConnectionConfig<C, T>

| Field | Type | Description | |---|---|---| | createProvider | (chain: C, onStatusChanged: (status: ConnectionStatus) => void) => JsonRpcProvider | Factory for the underlying JSON-RPC transport. Called once per chain. Use onStatusChanged to feed connection status back into the pool. | | clientOptions | (chain: C) => ClientOptions | Optional. Returns polkadot-api client options - typically metadata cache hooks (getMetadata / setMetadata). | | resolve | (chain: C, client: PolkadotClient) => Promise<T> | Optional. Transforms the raw PolkadotClient into your app's API type. The result is cached per chain. If omitted, T defaults to PolkadotClient. | | destroyDelay | number | Optional. Milliseconds to wait before destroying a connection after the last caller releases. Defaults to 0 (destroy immediately). Useful to avoid reconnect churn when callers release and re-acquire in quick succession. |

ChainConfig - minimum shape your chain objects must satisfy:

type ChainConfig = {
  genesisHash: string;
};

Extend it with whatever fields your createProvider / resolve callbacks need (e.g. nodes, name, specName).

Returns a ChainConnection<C, T> with the methods below.


lockApi(chain)

Acquires a connection and holds it until unlock() is called. Use for subscriptions or multi-step flows where you need the connection to stay alive.

Signature:

function lockApi(chain: C): Promise<{ api: T; unlock: VoidFunction }>

Example:

const { api: client, unlock } = await chains.lockApi(polkadot);

try {
  const api = client.getTypedApi(dot);

  const sub = api.query.System.Account.watchValue('5GrwvaEF...').subscribe({
    next: (value) => console.info('Balance:', value.data.free),
  });

  // later...
  sub.unsubscribe();
} finally {
  unlock();
}

getProvider(chain)

Returns a JsonRpcProvider backed by the shared pooled connection. The provider holds a ref-counted branch - the underlying connection opens when the provider starts and closes when disconnect() is called.

Useful for passing a provider to an iframe, webview, or any library that expects a raw JsonRpcProvider.

Signature:

function getProvider(chain: C): JsonRpcProvider

Example:

const provider = chains.getProvider(polkadot);
// Connection is released when the consumer calls disconnect()

status(genesisHash) / onStatusChanged(genesisHash, callback)

Read or subscribe to connection status. Both take a genesisHash string. Returns 'disconnected' for chains that have never been connected.

Signature:

function status(genesisHash: string): ConnectionStatus
// ConnectionStatus = 'connecting' | 'connected' | 'disconnected'

function onStatusChanged(genesisHash: string, callback: (status: ConnectionStatus) => void): VoidFunction

Example:

const currentStatus = chains.status(polkadot.genesisHash);

const unsubscribe = chains.onStatusChanged(polkadot.genesisHash, (status) => {
  console.info('Polkadot:', status);
});

// Stop listening:
unsubscribe();

pauseAll / resumeAll

Drops the inner socket of every active provider that supports pausing (e.g. providers built via createWsJsonRpcProvider). Pooled clients and ref counts are preserved; tracked subscriptions are re-sent on resumeAll() via the replay wrapper.

Use this when the host process goes to background (mobile / OS suspend) and you want to release sockets without tearing down callers.

Signature:

function pauseAll(): void
function resumeAll(): void

Example:

document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    chains.pauseAll();
  } else {
    chains.resumeAll();
  }
});

createWsJsonRpcProvider(options)

WebSocket provider factory. Wraps polkadot-api's getWsProvider and translates WebSocket events to ConnectionStatus. Active JSON-RPC subscriptions are automatically replayed after a reconnect — consumers using getProvider don't need to handle reconnects manually.

The returned provider is pausable: pause() drops the inner socket and resume() reopens it. The connection pool calls these via pauseAll() / resumeAll().

Signature:

function createWsJsonRpcProvider(options: {
  endpoints: string[];
  onStatusChanged?: (status: ConnectionStatus) => void;
  websocketClass?: typeof WebSocket;
  heartbeatTimeout?: number;
  connectionTimeout?: number;
  logger?: SocketLoggerFn;
}): PausableJsonRpcProvider;

type PausableJsonRpcProvider = JsonRpcProvider & {
  pause(): void;
  resume(): void;
};

| Field | Description | |---|---| | endpoints | One or more WebSocket URLs. Failover is handled by the underlying getWsProvider. | | onStatusChanged | Optional. Called with 'connecting' \| 'connected' \| 'disconnected' on socket events. | | websocketClass | Optional. Override the WebSocket implementation (e.g. ws in Node). | | heartbeatTimeout | Optional. Milliseconds without a server message before the socket is considered dead. Defaults to the underlying library's 40 s. | | connectionTimeout | Optional. Milliseconds to wait for the initial connection before giving up. | | logger | Optional. A SocketLoggerFn from @polkadot-api/ws-provider for tracing socket events. |

Example:

const provider = createWsJsonRpcProvider({
  endpoints: ['wss://rpc.polkadot.io', 'wss://polkadot-rpc.dwellir.com'],
  onStatusChanged: (status) => console.info(status),
});

createMetadataCache(options)

Caches chain metadata in memory, with optional persistence via a StorageAdapter. Wire it into clientOptions so polkadot-api skips re-fetching metadata on reconnect.

Signature:

function createMetadataCache(options?: { storage?: StorageAdapter }): MetadataCache;

type MetadataCache = {
  forChain(genesisHash: string): ClientOptions;
};

forChain returns an object with getMetadata / setMetadata methods that polkadot-api's createClient accepts as its second argument.

Example:

import { createMetadataCache } from '@novasamatech/host-substrate-chain-connection';
import { createLocalStorageAdapter } from '@novasamatech/storage-adapter';

// In-memory only
const cache = createMetadataCache();

// With localStorage persistence (survives page reloads)
const persistedCache = createMetadataCache({
  storage: createLocalStorageAdapter('chain-metadata'),
});

withSubscriptionReplay(provider, onReconnect)

Wraps any JsonRpcProvider so that active JSON-RPC subscriptions are automatically re-sent after the underlying transport reconnects. createWsJsonRpcProvider applies this internally — use it directly only when building a custom provider that needs the same behavior.

Signature:

function withSubscriptionReplay(
  provider: JsonRpcProvider,
  onReconnect: (callback: VoidFunction) => VoidFunction,
): JsonRpcProvider;

onReconnect is a subscription primitive: call the supplied callback whenever your transport finishes reconnecting, and return a teardown function that removes the listener.

Note: After a reconnect the server assigns new subscription IDs. Always unsubscribe with the most recently received ID — stale IDs from a previous connection silently fail.

Recipes

Custom resolve

The resolve callback transforms the raw PolkadotClient into whatever your app needs. The result is cached per chain and shared across all callers.

import { type PolkadotClient, type TypedApi } from 'polkadot-api';
import { dot, type DotDescriptor } from '@polkadot-api/descriptors';

type Chain = ChainConfig & { nodes: string[] };

type ResolvedApi = {
  api: TypedApi<DotDescriptor>;
  client: PolkadotClient;
};

const chains = createChainConnection<Chain, ResolvedApi>({
  createProvider(chain, onStatusChanged) {
    return createWsJsonRpcProvider({
      endpoints: chain.nodes,
      onStatusChanged,
    });
  },

  async resolve (_chain, client) {
    return {
      api: client.getTypedApi(dot),
      client,
    };
  },
});

// Now lockApi returns ResolvedApi instead of PolkadotClient
const { api: resolved, unlock } = await chains.lockApi(polkadot);
const account = await resolved.api.query.System.Account.getValue('5GrwvaEF...');
unlock();

Metadata caching

import { createLocalStorageAdapter } from '@novasamatech/storage-adapter';

type Chain = ChainConfig & { nodes: string[] };

const metadataCache = createMetadataCache({
  storage: createLocalStorageAdapter('chain-metadata'),
});

const chains = createChainConnection<Chain>({
  createProvider: (chain, onStatusChanged) =>
    createWsJsonRpcProvider({
      endpoints: chain.nodes,
      onStatusChanged,
    }),
  clientOptions: (chain) => metadataCache.forChain(chain.genesisHash),
});

Smoldot light client

Smoldot syncs chain state directly in the browser without trusting a remote RPC node. It works for well-known relay chains (Polkadot, Kusama, Westend) - parachains fall back to WebSocket.

import { type JsonRpcProvider } from '@polkadot-api/json-rpc-provider';
import { getSmProvider } from 'polkadot-api/sm-provider';
import { type Client as SmoldotClient, start as startSmoldot } from 'polkadot-api/smoldot';

type MyChain = ChainConfig & {
  name: string;
  nodes: string[];
  lightClient?: boolean;
};

// Chain specs for each relay chain - polkadot-api ships these built-in.
const lightClientChainSpecs: Record<string, () => Promise<{ chainSpec: string }>> = {
  '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3': () => import('polkadot-api/chains/polkadot'),
  '0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe': () => import('polkadot-api/chains/ksmcc3'),
  '0xe143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e': () => import('polkadot-api/chains/westend2'),
};

// Smoldot instance is created lazily and shared across all chains.
let smoldot: SmoldotClient | null = null;

const createLightClientProvider = (chain: MyChain): JsonRpcProvider => {
  const getChainSpec = lightClientChainSpecs[chain.genesisHash];
  if (!getChainSpec) {
    throw new Error(`Light client for chain "${chain.name}" is not supported`);
  }

  const smoldotChain = getChainSpec().then(({ chainSpec }) => {
    if (!smoldot) {
      smoldot = startSmoldot();
    }
    return smoldot.addChain({ chainSpec });
  });

  return getSmProvider(smoldotChain);
};

const chains = createChainConnection<MyChain>({
  createProvider: (chain, onStatusChanged) => {
    if (chain.lightClient && chain.genesisHash in lightClientChainSpecs) {
      // Light clients report connected immediately - Smoldot handles syncing internally.
      onStatusChanged('connected');
      return createLightClientProvider(chain);
    }

    return createWsJsonRpcProvider({
      endpoints: chain.nodes,
      onStatusChanged,
    });
  },
});

Multiple chains

When your app connects to several chains with different descriptors, resolve receives the chain object so you can return the right typed API for each.

Common additions beyond api and client:

  • Pre-resolved compatibilityToken - avoids repeated async lookups at every call site
  • Typed codecs via getTypedCodecs(descriptor) - for encoding/decoding extrinsics without going through the API layer
  • Descriptor key - a string discriminant so you can narrow the typed API at runtime
import { dot, ksm, type DotDescriptor, type KsmDescriptor } from '@polkadot-api/descriptors';
import { type ChainDefinition, type CompatibilityToken, type PolkadotClient, type TypedApi, getTypedCodecs } from 'polkadot-api';

type Chain = ChainConfig & { nodes: string[] };

const POLKADOT_GENESIS = '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3';
const KUSAMA_GENESIS = '0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe';

const descriptorMap: Record<string, ChainDefinition> = {
  [POLKADOT_GENESIS]: dot,
  [KUSAMA_GENESIS]: ksm,
};

type ResolvedApi = {
  api: TypedApi<DotDescriptor> | TypedApi<KsmDescriptor>;
  client: PolkadotClient;
  codecs: Awaited<ReturnType<typeof getTypedCodecs>>;
  compatibilityToken: CompatibilityToken;
};

const chains = createChainConnection<Chain, ResolvedApi>({
  createProvider: (chain, onStatusChanged) =>
    createWsJsonRpcProvider({
      endpoints: chain.nodes,
      onStatusChanged,
    }),

  resolve: async (chain, client) => {
    const descriptor = descriptorMap[chain.genesisHash];
    const api = client.getTypedApi(descriptor);

    // Pre-resolve once - these require async metadata fetches
    // that you don't want repeated at every call site.
    const [compatibilityToken, codecs] = await Promise.all([
      api.compatibilityToken,
      getTypedCodecs(descriptor),
    ]);

    return { api, client, codecs, compatibilityToken };
  },
});

Raw JSON-RPC subscriptions

getProvider returns a raw JsonRpcProvider. When used with subscription methods, active subscriptions are automatically resent after a WebSocket reconnect — no manual reconnect handling needed.

const provider = chains.getProvider(polkadot);

const conn = provider(message => {
  const parsed = JSON.parse(message);
  if (parsed.method === 'chain_newHead') {
    console.info('New head:', parsed.params.result);
  }
});

// Subscribe to new block heads
conn.send(JSON.stringify({ id: 1, method: 'chain_subscribeNewHeads', params: [] }));

// If the WebSocket drops and reconnects, the subscription is automatically
// resent. The server assigns a new subscription ID and notifications resume.

// Cleanup
conn.disconnect();

Note: Use the most recently received server-assigned subscription ID when unsubscribing. IDs from before a reconnect are no longer valid.


How it works

graph LR
  subgraph Products ["Products (iframe / webview)"]
    P1["Product A"]
    P2["Product B"]
  end

  subgraph Host App
    HA["lockApi()"]
    Container
  end

  P1 -- "remote_chain_*" --> Container
  P2 -- "remote_chain_*" --> Container
  Container -- "getProvider()" --> Pool

  HA --> Pool

  subgraph Pool ["Connection Pool"]
    C1["Polkadot"]
  end

  C1 --> W1["wss://rpc.polkadot.io"]

Products embedded in iframes or webviews don't connect to RPC nodes directly. They send remote_chain_* requests to the Container, which obtains a provider from the Connection Pool via getProvider(chain).

The host app's own code uses the same pool through lockApi and getProvider. Everyone shares the same underlying connections - one per chain. The pool opens a connection on first use and closes it when the last consumer releases.

Full example

Production-grade setup matching the architecture of polkadot-desktop. Includes Smoldot light client fallback, metadata caching, specName-based descriptor resolution, and a rich resolved API type.

import {
  createChainConnection,
  createMetadataCache,
  createWsJsonRpcProvider,
  type ChainConfig,
} from '@novasamatech/host-substrate-chain-connection';
import { createLocalStorageAdapter } from '@novasamatech/storage-adapter';
import { type JsonRpcProvider } from '@polkadot-api/json-rpc-provider';
import {
  type ChainDefinition,
  type CompatibilityToken,
  type PolkadotClient,
  type TypedApi,
  getTypedCodecs,
} from 'polkadot-api';
import { getSmProvider } from 'polkadot-api/sm-provider';
import { type Client as SmoldotClient, start as startSmoldot } from 'polkadot-api/smoldot';

import { dot, dot_ah, dot_ppl, ksm, ksm_ah, wnd, wnd_ah } from '@polkadot-api/descriptors';

// ---------------------------------------------------------------------------
// 1. Chain config
//
//    Extend ChainConfig with app-specific fields. `specName` is used to
//    select the right descriptor for each chain.
// ---------------------------------------------------------------------------

type Chain = ChainConfig & {
  name: string;
  nodes: string[];
  specName: string;
};

// ---------------------------------------------------------------------------
// 2. Descriptor resolution
//
//    Maps specName → default descriptor, with genesisHash overrides for
//    parachains that share a specName with their relay chain.
// ---------------------------------------------------------------------------

type Descriptor = { type: string; def: ChainDefinition };

const parachainOverrides: Record<string, Descriptor> = {
  // Polkadot Asset Hub
  '0x68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f': { type: 'dot_ah', def: dot_ah },
  // Polkadot People
  '0x67fa177a097bfa18f77ea95ab56e9bcdfeb0e5b8a40e46298bb93e16b6fc5008': { type: 'dot_ppl', def: dot_ppl },
  // Kusama Asset Hub
  '0x48239ef607d7928874027a43a67689209727dfb3d3dc5e5b03a39bdc2eda771a': { type: 'ksm_ah', def: ksm_ah },
  // Westend Asset Hub
  '0x67f9723393ef76214df0118c34bbbd3dbebc8ed46a10973a8c969d48fe7598c9': { type: 'wnd_ah', def: wnd_ah },
};

const specNameDefaults: Record<string, Descriptor> = {
  polkadot: { type: 'dot', def: dot },
  kusama:   { type: 'ksm', def: ksm },
  westend:  { type: 'wnd', def: wnd },
};

const getDescriptor = (chain: Chain): Descriptor => {
  return parachainOverrides[chain.genesisHash]
    ?? specNameDefaults[chain.specName]
    ?? { type: 'dot', def: dot };
};

// ---------------------------------------------------------------------------
// 3. Smoldot light client
//
//    Used for relay chains; parachains fall back to WebSocket.
// ---------------------------------------------------------------------------

const lightClientChainSpecs: Record<string, () => Promise<{ chainSpec: string }>> = {
  '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3': () => import('polkadot-api/chains/polkadot'),
  '0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe': () => import('polkadot-api/chains/ksmcc3'),
  '0xe143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e': () => import('polkadot-api/chains/westend2'),
};

let smoldot: SmoldotClient | null = null;

const createLightClientProvider = (genesisHash: string): JsonRpcProvider => {
  const getChainSpec = lightClientChainSpecs[genesisHash]!;

  const smoldotChain = getChainSpec().then(({ chainSpec }) => {
    if (!smoldot) {
      smoldot = startSmoldot();
    }
    return smoldot.addChain({ chainSpec });
  });

  return getSmProvider(smoldotChain);
};

// ---------------------------------------------------------------------------
// 4. Metadata cache
// ---------------------------------------------------------------------------

const metadataCache = createMetadataCache({
  storage: createLocalStorageAdapter('chain-metadata'),
});

// ---------------------------------------------------------------------------
// 5. Resolved API type
//
//    `resolve` pre-computes everything callers need so they don't have to
//    repeat async lookups (compatibilityToken, codecs) at every call site.
// ---------------------------------------------------------------------------

type TypedClient = {
  type: string;
  api: TypedApi<ChainDefinition>;
  codecs: Awaited<ReturnType<typeof getTypedCodecs>>;
  compatibilityToken: CompatibilityToken;
  client: PolkadotClient;
};

// ---------------------------------------------------------------------------
// 6. Create the connection pool
// ---------------------------------------------------------------------------

const chains = createChainConnection<Chain, TypedClient>({
  createProvider: (chain, onStatusChanged) => {
    if (chain.genesisHash in lightClientChainSpecs) {
      onStatusChanged('connected');
      return createLightClientProvider(chain.genesisHash);
    }

    return createWsJsonRpcProvider({
      endpoints: chain.nodes,
      onStatusChanged,
    });
  },

  clientOptions: (chain) => metadataCache.forChain(chain.genesisHash),

  resolve: async (chain, client) => {
    const { type, def } = getDescriptor(chain);
    const api = client.getTypedApi(def);

    const [compatibilityToken, codecs] = await Promise.all([
      api.compatibilityToken,
      getTypedCodecs(def),
    ]);

    return { type, api, codecs, compatibilityToken, client };
  },
});

// ---------------------------------------------------------------------------
// 7. Usage
// ---------------------------------------------------------------------------

const polkadot: Chain = {
  genesisHash: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3',
  specName: 'polkadot',
  name: 'Polkadot',
  nodes: ['wss://rpc.polkadot.io', 'wss://polkadot-rpc.dwellir.com'],
};

// One-shot query
const { api: queried, unlock: unlockQuery } = await chains.lockApi(polkadot);
const account = await queried.api.query.System.Account.getValue('5GrwvaEF...');
unlockQuery();

// Long-lived subscription
const { api: resolved, unlock } = await chains.lockApi(polkadot);

const sub = resolved.api.query.System.Account.watchValue('5GrwvaEF...').subscribe({
  next: (value) => console.info('Balance:', value.data.free),
});

// Cleanup
sub.unsubscribe();
unlock();

// Connection status
const unsubscribe = chains.onStatusChanged(polkadot.genesisHash, (status) => {
  console.info(`${polkadot.name}:`, status);
});

// Pass provider to an iframe or webview
const provider = chains.getProvider(polkadot);