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

uninet-client

v0.1.2

Published

HTTP, WebSocket, and unified real-time client for uninet servers — no server dependency

Readme

uninet-client

HTTP, WebSocket, and unified real-time client for uninet servers. No dependency on the server package (uninet). Sockress-style API: REST (get/post/put/patch/delete) + realtime (emit/on) with .body responses and upload progress.

Install

npm install uninet-client

Optional (WebSocket in Node): npm install ws

Docs & examples: Features-example.md — production-ready examples for every feature (5+ per feature); platform support table and React/React Native checklist included.

| README section | Feature examples | |----------------|------------------| | REST methods, Response object, Request options | uninetClient — REST + Realtime | | Events (realtime), Custom realtime (emit / on), Typed realtime events | uninetClient, createUninetClient | | File uploads, Upload progress | uninetClient examples | | Low-level API — fetch | fetch — HTTP client | | Low-level API — createWebSocket | createWebSocket — Raw WebSocket | | Low-level API — createUninetClient | createUninetClient | | Node.js, React Native, Browser | Platform support & checklist |

Quick start

import { uninetClient } from "uninet-client";

const api = uninetClient({
  baseUrl: "http://localhost:5051",
  autoConnect: true,
  preferSocket: true,
});

// GET request
const users = await api.get("/api/users");
console.log(users.body);

// POST request
const response = await api.post("/api/auth/login", {
  body: { email: "[email protected]", password: "secret" },
});
console.log(response.body.token);

// Generic request
const res = await api.request({
  path: "/api/users",
  method: "GET",
  headers: { Authorization: "Bearer token" },
  query: { page: 1, limit: 10 },
  body: { name: "John" },
  timeout: 5000,
  signal: abortController.signal,
  disableHttpFallback: false,
});

REST methods

api.get(path, options?);
const response = await api.get("/api/users", {
  query: { page: 1 },
  headers: { Authorization: "Bearer token" },
});

api.post(path, options?);
const response = await api.post("/api/users", {
  body: { name: "John", email: "[email protected]" },
});

api.put(path, options?);
const response = await api.put("/api/users/123", {
  body: { name: "Jane" },
});

api.patch(path, options?);
const response = await api.patch("/api/users/123", {
  body: { email: "[email protected]" },
});

api.delete(path, options?);
const response = await api.delete("/api/users/123");

Response object

All methods return an ApiResponse<T>:

interface ApiResponse<T> {
  status: number;           // HTTP status code
  ok: boolean;              // true if status 200–299
  headers: Record<string, string>;
  body: T;                  // parsed response body
  json<R = T>(): R;        // type-safe body as JSON
  text(): string;           // body as string
  raw(): T;                 // raw body (same as .body)
}

Example:

const response = await api.get("/api/users");
console.log(response.status);   // 200
console.log(response.ok);       // true
console.log(response.body);     // parsed JSON
console.log(response.json());   // same as body
console.log(response.text());   // JSON string
console.log(response.raw());    // raw body

File uploads

Upload files using FormData:

const formData = new FormData();
formData.append("avatar", file);
formData.append("name", "John Doe");

const response = await api.post("/api/profile/avatar", {
  body: formData,
  headers: { Authorization: "Bearer token" },
});
console.log(response.body.avatarUrl);

The client uses native FormData for HTTP; for realtime over WebSocket, the server must support the same semantics.

Upload progress

Track upload progress with onProgress (or onUploadProgress):

const formData = new FormData();
formData.append("video", largeVideoFile);

const response = await api.post("/api/upload", {
  body: formData,
  onProgress: (progress) => {
    console.log(`Uploaded: ${progress.loaded} / ${progress.total} bytes`);
    console.log(`Progress: ${progress.percentage}%`);
  },
});

onProgress receives { loaded, total, percentage }. Note: progress is supported for non-FormData bodies (e.g. JSON) via the underlying fetch; for FormData the host fetch implementation may not report progress.

Events (realtime)

Listen to socket lifecycle events:

// Socket opened
api.on("connect", () => {
  console.log("Socket connected");
});

// Socket closed
api.on("disconnect", () => {
  console.log("Socket closed");
});

// Custom events from server
api.on("message", (payload) => {
  console.log("message event:", payload);
});
api.on("user_typing", (data) => {
  console.log("user_typing:", data);
});

Custom realtime (emit / on)

Send and receive arbitrary events over the WebSocket transport:

// Client -> Server
api.emit("message", "hello");
api.emit("any_payload", { ok: true, list: [1, 2, 3] });

// With acknowledgement
api.emit("save", { name: "doc" }, (err, data) => {
  if (err) console.error(err);
  else console.log("saved", data);
});

Remove listeners:

const handler = () => console.log("connected");
api.on("connect", handler);
// Later
api.off("connect", handler);

Configuration

const api = uninetClient({
  baseUrl: "http://localhost:5051",   // required
  socketPath: "/",                    // WebSocket path (default: "/")
  headers: { "X-Custom": "value" },   // default headers
  timeout: 15_000,                    // request timeout ms (default: 15000)
  reconnectInterval: 1_000,           // initial reconnect delay (default: 1000)
  maxReconnectInterval: 5_000,        // max reconnect delay (default: 5000)
  autoConnect: true,                  // auto-connect on creation (default: true)
  preferSocket: true,                 // prefer WebSocket over HTTP (default: true)
  credentials: "include",            // fetch credentials (default: "include")
  fetchImpl: fetch,                   // custom fetch implementation
  wsFactory: (url) => new WebSocket(url),  // custom WebSocket factory
});

Node.js usage

In Node.js, provide fetch and WebSocket if needed:

import fetch from "node-fetch";
import WebSocket from "ws";
import { uninetClient } from "uninet-client";

const api = uninetClient({
  baseUrl: "https://api.example.com",
  fetchImpl: fetch as typeof globalThis.fetch,
  wsFactory: (url) => new WebSocket(url) as unknown as WebSocket,
});

Manual connection

const api = uninetClient({
  baseUrl: "http://localhost:5051",
  autoConnect: false,
});

await api.connect();
// ...
api.close();  // or api.disconnect()

Request options

  • path (required) — Request path (e.g. /api/users)

  • method (optional) — GET, POST, PUT, PATCH, DELETE, etc. (default: GET)

  • headers (optional) — Request headers

  • query (optional) — Query params (strings, numbers, booleans, arrays)

    await api.get("/api/users", {
      query: { page: 1, limit: 10, tags: ["js", "ts"], active: true },
    });
  • body (optional) — Request body: plain object (JSON), FormData, Blob, ArrayBuffer, URLSearchParams, or string

  • bodyPrepared (optional) — Pre-serialized body string. When set, used as-is (no JSON.stringify). Use for hot paths / repeated identical payloads; takes precedence over body.

    const payload = JSON.stringify({ name, email });
    api.post("/api/users", { bodyPrepared: payload });
  • onProgress (optional) — Upload progress: (progress: { loaded, total, percentage }) => void. For FormData uploads in the browser, progress is reported via XHR.

  • timeout (optional) — Request timeout in ms

  • signal (optional) — AbortSignal for cancellation

  • disableHttpFallback (optional) — If true, do not fall back to HTTP when socket is unavailable (realtime only; default: false)

  • retry (optional) — Number of retries on 5xx or network error (default: 0). Uses exponential backoff.

  • retryDelay (optional) — Initial retry delay in ms (default: 1000).

Retry & interceptors

Retry with exponential backoff on 5xx or network errors:

const res = await api.get("/api/users", { retry: 3, retryDelay: 1000 });

Request/response interceptors (auth, logging):

const api = uninetClient({
  baseUrl: "http://localhost:5051",
  interceptors: {
    request: (config) => {
      config.headers["Authorization"] = "Bearer " + token;
      return config;
    },
    response: (res) => {
      console.log(res.status, res.body);
      return res;
    },
  },
});

Batch requests (api.all)

Run multiple requests in parallel:

const [users, posts] = await api.all(api.get("/api/users"), api.get("/api/posts"));

Error handling

try {
  const response = await api.post("/api/users", {
    body: { name: "John" },
  });
  console.log(response.body);
} catch (error) {
  console.error("Request failed", error);
}

Typed realtime events (type-safe event map)

Define a ServerEvents interface so on/emit infer payload types:

import { uninetClient, type ServerEvents } from "uninet-client";

interface MyServerEvents extends ServerEvents {
  message: string;
  user_typing: { id: string; name?: string };
  welcome: { id: string; message: string };
}

const api = uninetClient<MyServerEvents>({ baseUrl: "http://localhost:3000" });

api.on("message", (data) => {
  console.log(data); // string
});
api.on("user_typing", (data) => {
  console.log(data.id, data.name); // typed
});
api.emit("message", "hello"); // data must be string
api.emit("user_typing", { id: "1", name: "Alice" });

Same with createUninetClient<MyServerEvents>(url, options) for realtime-only usage.

Offline queue & cookie persistence

  • queueEmitsWhenDisconnected — When true, emit() while disconnected is queued and flushed on connect. Use maxQueuedEmits (default 100) to cap the queue.
  • cookieFile (Node only) — Path to a JSON file; cookies are loaded on init and saved when the server sends Set-Cookie. Use for scripts/CLI that need to retain session across restarts.
const client = createUninetClient("http://localhost:3000", {
  queueEmitsWhenDisconnected: true,
  maxQueuedEmits: 50,
  cookieFile: ".cookies.json", // Node only
});

WebSocket binary mode

Use binary: true so the message callback receives ArrayBuffer:

const ws = createWebSocket("ws://localhost:3000/stream", {
  binary: true,
  message(data) {
    // data is ArrayBuffer when binary: true
    console.log(new Uint8Array(data as ArrayBuffer));
  },
});

Request deduplication

When dedupe: true, in-flight requests with the same method and URL share one promise; duplicate callers get the same result.

const res = await api.get("/api/users", { dedupe: true });

Streaming response (api.stream)

For NDJSON or chunked responses, use api.stream(path, options?):

const res = await api.stream("/api/events");
for await (const line of res.textLines()) {
  const data = JSON.parse(line); // NDJSON
  console.log(data);
}
// Or raw chunks:
for await (const chunk of res.body) {
  console.log(new TextDecoder().decode(chunk));
}

SWR cache (stale-while-revalidate)

Return cached body immediately when fresh; revalidate in background:

const res = await api.get("/api/users", { cache: "swr", ttl: 60_000 });
// First call: fetches. Subsequent calls within 60s: return cached, revalidate in background.

Reconnect jitter

Reconnect delay uses random jitter (50–100% of base delay) by default so many clients don’t reconnect at once. Disable with reconnectJitter: false in config.

Timeout

Every request supports timeout in options; the client aborts the request after the given ms. See request options above.

TypeScript

Full type safety:

interface User {
  id: number;
  name: string;
  email: string;
}

const response = await api.get<User[]>("/api/users");
const users: User[] = response.body;

Low-level API

  • fetch(url, options) — Axios-like HTTP client (see FetchOptions, FetchResponse). Use for one-off requests without the unified client.
  • createWebSocket(url, options) — Raw WebSocket client with open/message/close/error callbacks.
  • createUninetClient(url, options) — Low-level unified client (WS-first, HTTP fallback, reconnect). Use when you only need realtime (on/off/emit) without REST helpers.

Packages

  • uninet — Server only (createServer, HTTP, WS, SSE, middleware, upload, etc.)
  • uninet-client — Client only (uninetClient, fetch, createWebSocket, createUninetClient)

No dependency between them. Use uninet to run the server and uninet-client in your app or in another repo.

Author

ArunLinkedIn · GitHub · pluskode.com