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

farvex

v2.1.0

Published

Web SDK for Callpad voice sessions

Readme

farvex

Web SDK for Callpad voice sessions. It wraps the generated session HTTP client, Centrifugo realtime updates, a current browser session controller, React hooks, and LiveKit helpers.

Exports

| Entry | Purpose | | ---------------- | ----------------------------------------- | | farvex | Client factories and SDK/API types. | | farvex/react | React provider and focused session hooks. | | farvex/livekit | LiveKit re-exports and SessionRoom. |

Install

npm install farvex livekit-client @livekit/components-react

livekit-client, @livekit/components-react, react, and react-dom are peer dependencies. Install the LiveKit packages only in apps that render media.

Client Setup

import { createHostClient } from "farvex";

const client = createHostClient({
  apiUrl: "https://api.callpad.example.com",
  vendor: "acme",
  userId: user.id,
  auth: {
    getAccessToken,
    onUnauthorized: () => auth.signOut(),
  },
});

await client.connect();
await client.sessions.sync(undefined, { mode: "replaceVisible" });

getAccessToken is called before REST requests and realtime token refreshes. SDK methods throw CallpadError for API problem responses.

API Shape

Use client.session for the current browser media session lifecycle:

const current = await client.session.start({
  target: { type: "customer", userId: customer.id },
});

await client.session.end();

Use client.sessions for the low-level collection/data API:

const sessions = await client.sessions.sync();
const session = client.sessions.get(current.sessionId);
const canRecord = session ? client.sessions.can(session, "startRecording") : false;

The backend can support many live sessions. The browser SDK defaults to one current media session per client instance. client.session.start() and client.session.accept() use rejectWhileBusy by default, so starting or accepting while a non-terminal current session exists fails locally with session.busy. Pass { behavior: "allowParallel" } only when intentionally creating and selecting another backend session.

StartedSession.join from the generated API is nullable. The high-level client.session methods require a media grant by default and fail with session.join_unavailable if the backend creates/accepts a session but does not return a browser media grant.

React Usage

Import farvex/react from client components.

"use client";

import { useMemo, type ReactNode } from "react";
import { createHostClient, type HostClient } from "farvex";
import { CallpadProvider } from "farvex/react";

export const VoiceProvider = ({ children }: { children: ReactNode }) => {
  const client = useMemo<HostClient>(
    () =>
      createHostClient({
        apiUrl: process.env.NEXT_PUBLIC_CALLPAD_API_URL!,
        vendor: "acme",
        userId: 42,
        auth: { getAccessToken },
      }),
    [],
  );

  return (
    <CallpadProvider client={client} connect disposeOnUnmount>
      {children}
    </CallpadProvider>
  );
};

Hooks:

| Hook | Purpose | | ---------------------------- | ------------------------------------------- | | useCallpad() | Current SDK client. | | useStatus() | Realtime connection status. | | useSessions() | Visible session collection. | | useSession(id) | Exact session lookup. Pass null for none. | | useCurrentSession() | Current browser media-session snapshot. | | useCurrentSessionActions() | Current session commands and media events. | | useIncomingInvites() | Pending invites for the current user. | | useIncomingInvite() | First pending invite shortcut. | | useCan(id, action) | UI helper for low-level session actions. | | useSessionDuration(id) | Live elapsed time for an exact session ID. |

Starting a session:

import { useCallpad, useCurrentSession } from "farvex/react";

const StartButton = () => {
  const client = useCallpad<HostClient>();
  const { busy } = useCurrentSession();

  const start = () =>
    client.session.start({
      target: { type: "phone", phone: "+15551234567" },
    });

  return (
    <button type="button" disabled={busy} onClick={start}>
      Start session
    </button>
  );
};

Accepting an invite:

import { useCallpad, useIncomingInvite } from "farvex/react";

const IncomingInvite = () => {
  const client = useCallpad<HostClient>();
  const invite = useIncomingInvite();

  if (!invite) {
    return null;
  }

  return (
    <>
      <button
        type="button"
        onClick={() =>
          client.session.accept({
            sessionId: invite.session.id,
            participantId: invite.participant.id,
          })
        }
      >
        Answer
      </button>
      <button
        type="button"
        onClick={() =>
          client.sessions.reject({
            sessionId: invite.session.id,
            participantId: invite.participant.id,
          })
        }
      >
        Decline
      </button>
    </>
  );
};

Overlay and media should use the same current session ID:

import { RoomAudioRenderer, SessionRoom } from "farvex/livekit";
import { useCurrentSession, useCurrentSessionActions, useSession } from "farvex/react";

const SessionOverlay = () => {
  const snapshot = useCurrentSession();
  const actions = useCurrentSessionActions();
  const session = useSession(snapshot.current?.sessionId ?? null);

  return (
    <>
      <SessionRoom session={snapshot.current} controls={actions}>
        <RoomAudioRenderer />
      </SessionRoom>
      {snapshot.current && session ? (
        <SessionPanel session={session} phase={snapshot.current.phase} />
      ) : null}
    </>
  );
};

LiveKit

farvex/livekit re-exports first-party LiveKit primitives and adds SessionRoom. Use SessionRoom for the normal SDK flow; use raw LiveKitRoom only for advanced media ownership.

<SessionRoom session={current} controls={client.session} audio video={false}>
  <RoomAudioRenderer />
</SessionRoom>

SessionRoom renders nothing without a current session or join grant. It passes session.join.url and session.join.token to LiveKit and reports connected, disconnected, and error events back to the session controller when controls is provided.

Ringing Timeout

The SDK may hide expired incoming invites locally for immediate UX, but backend REST/realtime state is authoritative for final session state. Apps should render terminal state from VoiceSession.state, realtime events, and REST reconciliation rather than treating a local timer as final.

Migration Notes

Replace no-ID useSession():

const { current } = useCurrentSession();
const session = useSession(current?.sessionId ?? null);

Replace local raw join-grant state with client.session and SessionRoom. The current session stores { sessionId, participantId, join }, so the overlay, media room, controls, and participant UI all target the same session.

SDK source should use the exported Nullable<T> alias for nullable types instead of direct null unions in public SDK types.

Development

From websdk/:

bun run generate
bun run check
bun run build

HTTP calls must go through the generated Hey API client in src/api/generated/. Do not hand-edit generated files.

Release

Changesets lives in websdk/.changeset. From websdk/:

bun run changeset
bun run version
bun run release