uninet-client
v0.1.2
Published
HTTP, WebSocket, and unified real-time client for uninet servers — no server dependency
Maintainers
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-clientOptional (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 bodyFile 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 stringbodyPrepared (optional) — Pre-serialized body string. When set, used as-is (no
JSON.stringify). Use for hot paths / repeated identical payloads; takes precedence overbody.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) —
AbortSignalfor cancellationdisableHttpFallback (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. UsemaxQueuedEmits(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
Arun — LinkedIn · GitHub · pluskode.com
