@yappr/core
v0.3.0
Published
Headless reliability layer for Yappr realtime chat — optimistic send, reconnect, backfill, unread. Framework-agnostic.
Readme
@yappr/core
The headless reliability layer for Yappr realtime chat. Framework-agnostic, zero-dependency, useSyncExternalStore-shaped (subscribe + getSnapshot).
It handles the hard parts of a chat client for you: optimistic send with an outbox + retry/dedupe, reconnect with backoff, ?since backfill + loadOlder() pagination, and server-synced unread counts — over a single WebSocket to your Yappr server.
Using React? Install
@yappr/reactinstead — it's a thin binding over this package with auseChannelhook. Use@yappr/coredirectly for other frameworks (Vue, Svelte, vanilla) or custom integrations.
Install
npm install @yappr/coreQuickstart
import { createClient } from "@yappr/core";
const client = createClient({
// url defaults to wss://api.yappr.sh — override for self-host/local
tenantId: "t_xxx", // your tenant id
key: "pk_live_xxx", // your publishable key (browser-safe)
userId: "alice",
getToken: () => fetchFreshToken(), // called on every connect attempt (see Auth)
});
const channel = client.channel("room1");
// It's a store: subscribe to changes, read the current snapshot.
const unsubscribe = channel.subscribe(() => {
const { messages, status, unreadCount, hasOlder } = channel.getSnapshot();
render(messages, status, unreadCount);
});
channel.send("hello"); // optimistic — appears immediately, retries on failure
await channel.loadOlder(); // paginate backwards
channel.markRead(seq); // advance the read cursor
// later
unsubscribe();
client.close();Auth
The publishable key (pk_live_…) identifies your tenant and is safe to ship in the browser. To prove who the end-user is, your backend mints a short-lived JWT signed with your tenant secret — the secret never reaches the client. Use @yappr/server-node's mintToken on your server, then pass a getToken function that fetches a fresh token from your backend.
getToken is invoked on every connect attempt (not just once at startup), so a long-lived session survives past the token's TTL — each reconnect mints a fresh credential instead of retrying forever with a stale one.
For local prototyping you can use a pk_test_… key (self-asserted identity, no backend, not for production).
API
createClient(config)→YapprClientconfig:{ url?, tenantId, key, userId, displayName?, getToken?, storage? }(urldefaults towss://api.yappr.sh)
client.channel(channelId)→ChannelHandleclient.reconnect()·client.close()ChannelHandle:subscribe(listener) => unsubscribegetSnapshot()→{ messages, status, unreadCount, hasOlder, error? }send(content)·loadOlder()·markRead(seq)·reconnect()·close()
statusis one of"connecting" | "connected" | "disconnected" | "faulted"."faulted"is terminal — the server rejected credentials or entitlement — and carrieserror: { code, message }. Callreconnect()to retry (e.g. after refreshing whatevergetTokendepends on).
Cached data is scoped per identity
Anything cached locally through storage — recent messages, last-read position, and the
unsent outbox — is keyed by the server URL, tenantId, and userId together. Change any
of them and you get a separate cache, so pointing one build at a local server and then at
production will not replay the local server's history, and signing a second user in on a
shared device does not show them the first user's messages or flush their queued sends.
Upgrading from an earlier version changes the key format, so the first launch after the
upgrade starts from an empty cache and re-syncs from the server. Nothing is lost that the
server does not already hold, but the previous format's entries are not reclaimed:
YapprStorage is get/set/remove with no way to enumerate keys, so they cannot be
found to delete.
0.2.0 — breaking
token is replaced by getToken. A static token cannot be refreshed, so any
session outliving the JWT's TTL (1 hour by default) died silently and retried
forever. getToken is called on every connect attempt, so an expired credential
self-heals.
createClient({
tenantId, key, userId,
- token: await mintToken(claims, secret),
+ getToken: () => fetchFreshToken(),
})ConnStatus gains "faulted" — a terminal state reached when the server rejects
credentials (4401 twice) or entitlement (4403). Clear it with reconnect().
