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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@soffinal/rpc

v0.1.2

Published

Universal type-safe RPC with reactive streams for any transport boundary

Downloads

12

Readme

@soffinal/rpc

npm version TypeScript License: MIT

Universal Type-Safe RPC with Reactive Streams

A groundbreaking RPC library that provides perfect type inference, universal transport support, and reactive stream-based response handling. Built for any boundary: HTTP, WebSocket, Workers, or any message-passing interface.

Features

  • 🎯 Perfect Type Inference - Zero configuration, complete IntelliSense
  • 🌐 Universal Transport - HTTP, WebSocket, Workers, any message-passing boundary
  • 📡 Reactive Streams - Stream-based response handling with per-action filtering
  • 🔒 Context-Aware - Type-safe context injection per action
  • 📚 Documentation-as-Distribution - Copy-paste transport integrations embedded in JSDoc, no separate packages needed. Hover over Action.handle() and Client.create() in your IDE to see all available integrations
  • Zero Dependencies - Lightweight with optional stream integration
  • 🛠️ Framework Agnostic - Works with any server framework
  • 📘 Full TypeScript - End-to-end type safety

Quick Start

Installation

npm install @soffinal/rpc
# or
bun add @soffinal/rpc

Optional: Advanced Stream Transformations

For advanced stream operations like filter, map, merge, and other reactive primitives:

npm install @soffinal/stream
# or
bun add @soffinal/stream

Server Setup

import { Action } from "@soffinal/rpc";

// Actions are simple functions that return Action.Response ( {data} | {error} )
function addUser(ctx: undefined, name: string, age: number) {
  if (name.length < 2) {
    return { error: "Name too short" as const };
  }
  if (age < 18) {
    return { error: "Must be 18 or older" as const };
  }
  return { data: { id: Math.random(), name, age } };
}

function getUserProfile(ctx: { userId: string }, profileId: string) {
  if (ctx.userId !== profileId) {
    return { error: "Unauthorized" as const };
  }
  return { data: { id: profileId, name: "John Doe" } };
}

// Or use Action.create helper for convenience
const addUser = Action.create((ctx: undefined, name: string, age: number) => {
  if (name.length < 2) {
    return Action.error("Name too short" as const);
  }
  if (age < 18) {
    return Action.error("Must be 18 or older" as const);
  }
  return Action.success({ id: Math.random(), name, age });
});

// Create action registry
const actions = { addUser, getUserProfile };
export type Actions = typeof actions;

// HTTP Server (Bun example)
const server = Bun.serve({
  port: 3000,
  async fetch(req) {
    // ... do the logic to get the contexts if needed

    const response = await Action.handle(await req.json(), actions, {
      getUserProfileContext: { userId: "current-user" },
    });
    return Response.json(response);
  },
});

Client Setup

import { Client } from "@soffinal/rpc";
import type { Actions } from "./server";

// Create client with HTTP transport
const client = Client.create<Actions>(async (request) => {
  const response = await fetch("http://localhost:3000/rpc", {
    method: "POST",
    body: JSON.stringify(request),
  });
  return response.json();
});

// Use as functions with perfect type inference
const user = await client.addUser("John", 25);
//    ^ { data: { id: number, name: string, age: number } } | { error: "Name too short" | "Must be 18 or older" }

// Use as reactive streams
client.addUser.data.listen((user) => {
  //                         ^ { id: number, name: string, age: number }
  console.log("User created:", user);
});

client.addUser.error.listen((error) => {
  //                          ^ "Name too short" | "Must be 18 or older"
  console.error("Failed to create user:", error);
});

// Global response monitoring
client.responses.listen((response) => {
  //                      ^ { actionName: "addUser", data?: { id: number, name: string, age: number }, error?: "Name too short" | "Must be 18 or older" } | { actionName: "getUserProfile", data?: { id: string, name: string }, error?: "Unauthorized" }
  console.log(`Action ${response.actionName}:`, response);
});

Core Concepts

Actions

Actions are type-safe functions that define your RPC endpoints:

// Raw action function
const myAction = (context: MyContext, arg1: string, arg2: number): Action.Response<MyData, MyError> => {
  // Your logic here
  if (success) {
    return { data: myData };
  } else {
    return { error: myError };
  }
};

// Or use Action.create helper
const myAction = Action.create((context: MyContext, arg1: string, arg2: number) => {
  // Your logic here
  if (success) {
    return Action.success(myData);
  } else {
    return Action.error(myError);
  }
});

Context-Aware Type System

Actions can require specific context, enforced at compile time:

// Action requiring authentication context
const authenticatedAction = Action.create((ctx: { userId: string }, data: any) => {
  return Action.success(`User ${ctx.userId} processed ${data}`);
});

// TypeScript enforces required context
await Action.handle(request, actions, {
  authenticatedActionContext: { userId: "user123" }, // Required!
});

Reactive Streams

Every action becomes both a function and a reactive stream:

// Call as function
const result = await client.myAction(arg1, arg2);

// Listen as stream
client.myAction.data.listen((data) => console.log("Success:", data));
//                          ^ MyData
client.myAction.error.listen((error) => console.log("Error:", error));
//                           ^ MyError

// Global streams
client.responses.listen((response) => {
  if (response.actionName === "myAction") {
    //  ^ TypeScript narrows to myAction response type
    // Handle specific action responses
  }
});

Advanced Stream Transformations

For complex reactive patterns, install @soffinal/stream for additional primitives:

import { filter, map, merge } from "@soffinal/stream";

// Transform and filter responses
client.addUser.data
  .pipe(filter({}, (_, user) => [user.age > 21, {}]))
  //                   ^ { id: number, name: string, age: number }
  .pipe(map({}, (_, user) => [`Welcome ${user.name}!`, {}]))
  .listen((message) => console.log(message));
//       ^ string

// Centralized error handling with filtering
client.error
  .pipe(filter({}, (_, response) => [response.actionName === "addUser", {}]))
  .listen(({ error }) => showUserError(error));

// Merge multiple action streams
const allUserActions = client.addUser.data.pipe(merge(client.updateUser.data, client.deleteUser.data));

Transport Integrations

The library includes copy-paste ready integrations for popular frameworks. Hover over Action.handle() and Client.create() in your IDE to see all available integrations:

HTTP Frameworks

Express.js:

app.post("/rpc", async (req, res) => {
  const response = await Action.handle(req.body, actions, {
    // Your context here
  });
  res.json(response);
});

Next.js API Routes:

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === "POST") {
    const response = await Action.handle(req.body, actions, {
      // Your context here
    });
    res.json(response);
  }
}

WebSocket

ws Library:

wss.on("connection", (ws) => {
  ws.on("message", async (data) => {
    const { id, request } = JSON.parse(data.toString());
    const response = await Action.handle(request, actions, {});
    ws.send(JSON.stringify({ requestId: id, response }));
  });
});

Workers

Web Worker:

// Client
const client = Client.create<Actions>((request) => {
  return new Promise((resolve) => {
    const handler = (event) => {
      worker.removeEventListener("message", handler);
      resolve(event.data);
    };
    worker.addEventListener("message", handler);
    worker.postMessage(request);
  });
});

// Worker
self.onmessage = async (event) => {
  const response = await Action.handle(event.data, actions, {});
  self.postMessage(response);
};

Node.js Worker Threads:

import { parentPort } from "worker_threads";

parentPort?.on("message", async (request) => {
  const response = await Action.handle(request, actions, {});
  parentPort?.postMessage(response);
});

Advanced Examples

Database Integration

const createPost = Action.create(async (ctx: { db: Database; userId: string }, title: string, content: string) => {
  try {
    const post = await ctx.db.posts.create({
      title,
      content,
      authorId: ctx.userId,
    });
    return Action.success(post);
  } catch (error) {
    return Action.error("Database error");
  }
});

Validation with Zod

import { z } from "zod";

const userSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
  age: z.number().min(18),
});

const createUser = Action.create((ctx: undefined, userData: unknown) => {
  const result = userSchema.safeParse(userData);
  if (!result.success) {
    return Action.error(result.error.issues);
  }

  return Action.success({ id: Math.random(), ...result.data });
});

API Reference

Action Namespace

  • Action.create<CTX, ARGS, DATA, ERROR>(fn) - Create typed action
  • Action.handle(request, actions, context) - Handle RPC requests
  • Action.success<DATA>(data) - Create success response
  • Action.error<ERROR>(error) - Create error response

Client Namespace

  • Client.create<ACTIONS>(transport) - Create typed client
  • client.actionName(...args) - Call action as function
  • client.actionName.data - Success stream for action
  • client.actionName.error - Error stream for action
  • client.responses - All responses stream
  • client.data - All success responses stream
  • client.error - All error responses stream

Runtime Support

  • Node.js 16+
  • Bun 1.0+
  • Deno 1.0+
  • Modern browsers with ES2020+
  • Cloudflare Workers
  • Vercel Edge Runtime

License

MIT © Soffinal

Contact