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

@mantlejs/client

v0.1.0

Published

Type-safe client for Mantle JS APIs — call services from browsers and Node.js

Readme

@mantlejs/client

Official JavaScript/TypeScript client SDK for Mantle JS — call Mantle services from browsers, Node.js (18+), and React Native over REST, with optional real-time events over Socket.IO.


Installation

npm install @mantlejs/client

# Optional — only needed for real-time service events
npm install socket.io-client

The client has zero required dependencies: REST calls use the native Fetch API. socket.io-client is an optional peer dependency loaded lazily on the first .on() call.


Concepts

The same Service<T> surface, on the wire

A ServiceClient<T> exposes the six method names the server-side Service<T> contract uses — find, get, create, update, patch, remove — dispatched as REST calls (GET/POST/PUT/PATCH/DELETE /:service). Switching between server and client code involves no context switching.

Query serialization

params.query is serialized into the bracket-notation query string every Mantle HTTP transport parses back into the identical object (parseQueryString in @mantlejs/mantle):

await api.service("users").find({ query: { age: { $gt: 21 }, $limit: 10, $sort: { name: "asc" } } });
// → GET /users?age[$gt]=21&$limit=10&$sort[name]=asc

Values arrive server-side as strings — pair services with RepositoryService and a schema for type coercion. undefined values are dropped; null serializes as an empty string, so IS NULL queries need server-side coercion.

Authentication and token rotation

client.authenticate({ strategy, ...credentials }) posts to /authentication and stores the returned accessToken/refreshToken in the configured TokenStorage (default: localStorage in the browser, in-memory elsewhere). Every REST request carries Authorization: Bearer <accessToken>.

On a 401, the client attempts one token rotation — POST /authentication with { strategy: "refresh", refreshToken } — then retries the original request. Concurrent 401s share a single refresh (the server's rotation treats a reused refresh token as theft and revokes the whole family). If the refresh fails, tokens are cleared, 'logout' is emitted, and the original 401 error is thrown.

Restoring a session on startup

getAccessToken() is synchronous, reading only the in-memory copy — on a fresh page load (or a new MantleClient instance) that copy is empty until something hydrates it from storage, which normally only happens as a side effect of the first authenticated REST call. A UI that checks getAccessToken() before making any request — the natural thing to do to decide whether to render a logged-in or logged-out view on mount — will see undefined and show its logged-out state on every refresh, even with a valid session sitting in storage. Use isAuthenticated() instead, which hydrates first:

const [authenticated, setAuthenticated] = useState<boolean | undefined>(undefined); // undefined = "checking"
useEffect(() => {
  void api.isAuthenticated().then(setAuthenticated);
}, []);

setTokens({ accessToken, refreshToken }) hydrates the client from a token pair obtained outside authenticate() — e.g. an OAuth provider's redirect-back callback landing tokens in a URL fragment. It stores them and emits 'authenticated', the same as a successful authenticate().

Real-time events

When the socket option is configured, service.on("created" | "updated" | "patched" | "removed", handler) subscribes to the server's Socket.IO broadcasts ("<path> <event>"). The socket connects lazily on the first .on() call, all services share one connection, and multiple handlers for the same event multiplex over a single underlying socket listener. Calling .on() without the socket option throws a GeneralError-shaped MantleClientError.

Event delivery is at-most-once (see the @mantlejs/sync README) — the client emits a 'reconnect' event on every re-connect so callers (e.g. @mantlejs/react) can refetch and bound the staleness from any missed events.

Batch coalescing

With the batch option enabled, service calls made within the same coalescing window (default: the same microtask tick) are queued and sent as one POST /batch request instead of N separate REST calls — the transports mount that endpoint by default. Each caller's promise still resolves or rejects independently from its own entry in the batched response, so application code is unchanged:

const api = mantle({ url: "http://localhost:3030", batch: true });

// One HTTP round trip, three independent promises — the dominant AI-agent call pattern
const [user, posts, tags] = await Promise.all([
  api.service("users").get(1),
  api.service("posts").find({ query: { authorId: 1 } }),
  api.service("tags").find(),
]);

windowMs widens the window (setTimeout-based) beyond the same tick; queues longer than maxSize (default 25 — match the server's max batch size) split into multiple requests. Calls with per-request headers bypass coalescing and go out individually, as does similar(). If every entry fails with a 401 (expired token), the client performs its usual single token refresh and retries just those entries once.

Errors

Non-2xx responses are deserialized into MantleClientErrorname ("BadRequest", "NotFound", …), code (HTTP status), data, errors, and the server's actionable hint when present. Non-JSON bodies (gateway errors) fall back to the HTTP status. Network failures propagate as the native fetch TypeError, unwrapped.


Quick start

import { mantle } from "@mantlejs/client";

interface Message {
  id: number;
  text: string;
}

const api = mantle({ url: "http://localhost:3030", socket: {} });

// Authenticate (against @mantlejs/auth + a strategy such as auth-local)
await api.authenticate({ strategy: "local", email: "[email protected]", password: "secret" });

// CRUD — same names as the server-side Service<T>
const messages = api.service<Message>("messages");
const page = await messages.find({ query: { $limit: 10, $sort: { id: "desc" } } });
const one = await messages.get(1);
const created = await messages.create({ text: "Hello" });

// Real-time
messages.on("created", (message) => console.log("new message", message.text));
api.on("reconnect", () => console.log("socket reconnected — refetch anything stale"));

Vector search, against a service that registers the similar custom method (VectorRepositoryService):

const hits = await api.service<Doc>("docs").similar({ vector: [0.1, 0.2, 0.3], topK: 5 });
// hits: Array<Doc & { _score: number }>

API

mantle(options)

Creates a MantleClient. Throws TypeError if url is missing.

| Option | Type | Default | Description | | --------- | ------------------------ | ---------------------------------- | ---------------------------------------------------------------------------------------------- | | url | string | — (required) | Base URL of the Mantle server, e.g. "http://localhost:3030" | | storage | TokenStorage | localStorage browser / in-memory | Token persistence. Any object with getItem/setItem/removeItem (sync or async) works | | socket | SocketOptions | undefined | Socket.IO connection options, passed to io(url, options). Omit to disable real-time features | | headers | Record<string, string> | {} | Default headers appended to every REST request (per-request params.headers win) | | batch | boolean \| BatchOptions | false | Coalesce same-window service calls into one POST /batch request (see Batch coalescing) |

SocketOptions.io optionally overrides the socket factory itself — inject a stub in tests, or supply a pre-bundled io when dynamic import of the optional peer is undesirable.

MantleClient

| Member | Description | | --------------------------- | ------------------------------------------------------------------------------------ | | service<T>(path) | Returns the (cached) ServiceClient<T> for a service path | | authenticate(credentials) | POST /authentication, stores tokens, emits 'authenticated', returns AuthResult | | setTokens(tokens) | Stores a { accessToken, refreshToken? } pair obtained outside authenticate(), emits 'authenticated' | | isAuthenticated() | Promise<boolean> — hydrates from storage first if needed. Use on startup, not getAccessToken() (see "Restoring a session on startup") | | logout() | Clears tokens, emits 'logout', fires a best-effort POST /authentication/logout | | getAccessToken() | Current access token (synchronous, from the in-memory copy — may be undefined until something hydrates it, even with a valid session in storage) | | on(event, handler) | Client events: 'authenticated', 'logout', 'reconnect' | | off(event, handler) | Remove a client event handler |

ServiceClient<T>

| Member | HTTP | Description | | --------------------------- | ------------------------ | ----------------------------------------------------------------------------- | | find(params?) | GET /:service | Promise<T[] \| Paginated<T>> | | get(id, params?) | GET /:service/:id | Promise<T> | | create(data, params?) | POST /:service | Promise<T> | | update(id, data, params?) | PUT /:service/:id | Promise<T> | | patch(id, data, params?) | PATCH /:service/:id | Promise<T> | | remove(id, params?) | DELETE /:service/:id | Promise<T> | | similar(data, params?) | POST /:service/similar | Vector-search convention — Promise<Array<T & { _score }>> | | on(event, handler) | — | Subscribe to 'created' \| 'updated' \| 'patched' \| 'removed' | | off(event, handler) | — | Unsubscribe; the socket listener detaches with the last handler | | realtime | — | true when the client has the socket option — on()/off() are available |


Types

interface ClientParams {
  query?: Record<string, unknown>; // serialized into the URL
  headers?: Record<string, string>; // per-request header overrides
}

interface TokenStorage {
  getItem(key: string): string | null | Promise<string | null>;
  setItem(key: string, value: string): void | Promise<void>;
  removeItem(key: string): void | Promise<void>;
}

interface BatchOptions {
  windowMs?: number; // coalescing window; 0 (default) = same microtask tick
  maxSize?: number; // max calls per POST /batch; longer queues split. Default 25
}

interface AuthCredentials {
  strategy: string;
  [key: string]: unknown;
}

interface AuthResult {
  accessToken: string;
  refreshToken?: string;
  user?: unknown;
}

interface Paginated<T> {
  total: number;
  limit: number;
  skip: number;
  data: T[];
}

class MantleClientError extends Error {
  code: number; // HTTP status
  name: string; // server error class: "BadRequest", "NotFound", …
  className?: string; // kebab-case class name from the wire format
  data?: unknown;
  errors?: unknown[];
  hint?: string; // server's actionable guidance, when present
}

Tokens are stored under the keys mantle-access-token and mantle-refresh-token.


Development

npx nx build client     # compile
npx nx test client      # run tests
npx nx lint client      # lint

Publishing

Build before publishing:

npx nx build client

First publish (scoped packages require --access public):

cd packages/client
npm publish --access public

Subsequent releases — bump version in packages/client/package.json, then:

cd packages/client
npm publish

Testing locally with Verdaccio

# Terminal 1 — start the local registry
npx nx run @mantle/source:local-registry

# Terminal 2 — publish to it
cd packages/client
npm publish --registry http://localhost:4873

# Install from it in another project
npm install @mantlejs/client --registry http://localhost:4873