Subscriptions
Lifecycle, events, and cancellation for live WebSocket subscriptions.
Subscriptions are long-lived WebSocket channels — the server pushes events to you as data changes. The SDK abstracts framing, auth, and reconnection.
Subscription handle
Every subscribe* method returns a typed Subscription:
class Subscription<M> {
readonly id: string; // server-assigned (or "__pending_ws__" briefly during cold-start race)
readonly snapshot: SnapshotOf<M>;
on(event: PushEvent, fn: (data) => void): () => void;
on("snapshot", fn): () => void;
on("closed", fn): () => void;
cancel(): Promise<void>;
}Listen via .on() — never via constructor callbacks.
Methods that return subscriptions
| Method | Snapshot type | Push events |
|---|---|---|
matches.subscribeLive | Match[] | match.upd, match.del |
matches.subscribe | MatchDetail | stats.upd, odds.upd, status.upd |
Live methods require Starter tier; per-match subscribe requires
Growth tier.
Snapshot delivery
The first thing every subscription delivers is snapshot — the current
state matching your filters.
const sub = await mrdoge.matches.subscribeLive({ sports: ["soccer"] });
console.log(sub.snapshot); // already populatedFor subscribeLive, the snapshot arrives over HTTP (cache hit in ~100ms)
while the WebSocket connects in parallel. The WS picks up live deltas as
soon as it's ready. See the race details →.
When the WS lands after HTTP won the race, the SDK fires a snapshot
event with the canonical state:
sub.on("snapshot", (snapshot) => {
// optional — fires once when WS catches up with the HTTP snapshot,
// or after a reconnect
});Event listeners
matches.subscribeLive
const sub = await mrdoge.matches.subscribeLive({ sports: ["soccer"] });
sub.on("match.upd", (match) => {
// match: Match — full payload with the latest fields
});
sub.on("match.del", (matchId) => {
// matchId: string — match dropped out of the filter window
});matches.subscribe (single match)
const sub = await mrdoge.matches.subscribe({ matchId: "match_abc" });
sub.on("stats.upd", (stats) => {
// stats: MatchStats — full latest state, replace not merge
});
sub.on("odds.upd", (markets) => {
// markets: Market[] — refreshed odds
});
sub.on("status.upd", ({ status }) => {
// status: "upcoming" | "live" | "completed"
});Cancellation
Always cancel when you're done:
await sub.cancel();This sends subscription.cancel to the server, frees a subscription slot,
and releases server resources. Leaking subscriptions eats your tier's
concurrent quota.
In React, cancel on unmount:
useEffect(() => {
let sub: Awaited<ReturnType<typeof mrdoge.matches.subscribeLive>> | null = null;
mrdoge.matches
.subscribeLive({ sports: ["soccer"] })
.then((s) => {
sub = s;
s.on("match.upd", setMatch);
});
return () => {
sub?.cancel();
};
}, []);closed events
The subscription can close for several reasons. Listen and react:
sub.on("closed", ({ reason, message }) => {
console.log("Closed:", reason, message);
});Common reason values:
| Reason | When |
|---|---|
user_cancelled | You called sub.cancel() |
disconnected | WebSocket dropped and reconnect gave up |
rate_limited | Subscription quota exceeded |
unauthorized | Token expired and refresh failed |
internal_error | Server error in the subscription pipeline |
Concurrent limits
The cap is per WebSocket connection, not per key. Each connection maintains its own subscription map and rejects new subscriptions over the cap:
| Tier | Subs per connection |
|---|---|
| Free | 5 |
| Starter | 25 |
| Growth | 100 |
| Business | 500 |
A single key can hold multiple connections (browser tabs, server
instances) up to that tier's connection cap.
Effective max subs per key = subs/conn × max connections.
Exceeding a single connection's cap throws SubscriptionLimitError at
subscribe* time. Cancel an old subscription on the same connection, open
a new connection, or upgrade tier.
Reconnection
When the WebSocket drops, the SDK reconnects automatically with exponential backoff:
- WS drops (network blip, server restart)
- SDK reconnects, re-authenticates
- SDK resubscribes to active subscriptions
- New snapshot delivered via the
snapshotevent - Deltas resume on the same
Subscriptionhandle
You don't need to do anything — your match.upd / stats.upd etc.
listeners keep firing.
pingOrReconnect() — focus-aware wake-up
The SDK can't subscribe to OS-level focus signals on its own (no DOM or React Native runtime dependency). When the device comes back from a long background or a network drop, the WebSocket may be dead while the SDK's reconnect loop is still mid-backoff sleep — meaning the first user interaction pays that backoff delay.
Wire mrdoge.pingOrReconnect() to your platform's focus event. It's a
no-op when the socket is healthy, fires a reconnect when it's dead, and
wakes the backoff loop when one is sleeping.
// React Native
import { AppState } from "react-native";
AppState.addEventListener("change", (state) => {
if (state === "active") mrdoge.pingOrReconnect();
});
// Browser / Next.js (client component)
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") mrdoge.pingOrReconnect();
});
window.addEventListener("online", () => mrdoge.pingOrReconnect());Server-side consumers (@mrdoge/node) have no focus concept and don't
need this.
Never throws — failures fall through to the SDK's normal reconnect machinery.