Mr. Doge

Reference

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

MethodSnapshot typePush events
matches.subscribeLiveMatch[]match.upd, match.del
matches.subscribeMatchDetailstats.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 populated

For 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:

ReasonWhen
user_cancelledYou called sub.cancel()
disconnectedWebSocket dropped and reconnect gave up
rate_limitedSubscription quota exceeded
unauthorizedToken expired and refresh failed
internal_errorServer 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:

TierSubs per connection
Free5
Starter25
Growth100
Business500

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:

  1. WS drops (network blip, server restart)
  2. SDK reconnects, re-authenticates
  3. SDK resubscribes to active subscriptions
  4. New snapshot delivered via the snapshot event
  5. Deltas resume on the same Subscription handle

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.

Next

On this page

Subscriptions