npm install @playlive/tiltify-phoenix
Tiltify Phoenix Channels realtime client. Drop-in replacement for the legacy
@playlive/tiltify-tools/tiltify-websocket module — same surface, same
wire-level behavior, same callback semantics.
bun add @playlive/tiltify-phoenix
# Peer dependency — the caller installs it
bun add phoenix
# TypeScript consumers also want the ambient types
bun add -d @types/phoenix
phoenix (^1.8.1) is the only entry in peerDependencies.
@playlive/tiltify-core is a regular dependency and installs
automatically — but you will usually want to import its tiltify singleton
yourself to configure auth/logging before constructing a TiltifyWebSocket
(see the caveat under Quick start).
import { tiltify } from "@playlive/tiltify-core";
import { EVENTS_TO_WATCH, TiltifyWebSocket } from "@playlive/tiltify-phoenix";
// Configure the REST singleton used for the slug → fact-id lookups.
// NOTE: `startListeningToCampaignBySlug` calls `tiltify.UseProxy()` itself,
// which resets routing to the **prod** St. Jude proxy. To target qa/dev,
// resolve the campaign yourself and call `startListeningToFact` instead.
tiltify.UseProxy("prod");
tiltify.SetPublicMode();
const ws = new TiltifyWebSocket(
(isInitial) => console.log("open", { isInitial }),
(err) => console.error("ws error", err),
(isDisc) => console.log("close", { isDisconnecting: isDisc }),
);
ws.addEventListener((factID, event, type, payload) => {
console.log("event", { factID, event, type, payload });
});
// High-level: resolve slug → start listening to campaign + parent facts.
await ws.startListeningToCampaignBySlug("@playliver", "campaign-slug", EVENTS_TO_WATCH);
// …on unmount:
ws.disconnect();
| Subpath | Description |
|---|---|
@playlive/tiltify-phoenix |
Default barrel — TiltifyWebSocket, DEFAULT_TILTIFY_SOCKET_URL, EVENTS_TO_WATCH, EventType, FactType, the callback types, PACKAGE_NAME, KNOWN_URLS. |
@playlive/tiltify-phoenix/client |
TiltifyWebSocket, DEFAULT_TILTIFY_SOCKET_URL, and the callback types only. Pulls the phoenix peer dep. |
@playlive/tiltify-phoenix/events |
EVENTS_TO_WATCH, EventType, FactType. Zero runtime deps — safe for type-only imports in route handlers / dispatch tables. |
The default barrel is a curated re-export (not export *), so these three
lists are exactly the public surface.
Full generated API documentation: https://packages.playlive.experience.stjude.org/p/@playlive/tiltify-phoenix/docs/
Top-level exports:
| Export | Source | Kind | Notes |
|---|---|---|---|
TiltifyWebSocket |
./client |
class | Phoenix Channel wrapper. Lifecycle, channel ack, per-event firehose, name cache. |
DEFAULT_TILTIFY_SOCKET_URL |
./client |
const | "wss://websockets.tiltify.com/socket". |
OnOpenCallback, OnErrorCallback, OnCloseCallback |
./client |
type | The three constructor callbacks. |
ChannelListener |
./client |
type | { onOk, onError, onTimeout } per-channel join acks. |
EVENTS_TO_WATCH |
./events |
const | EventType[] — every event suffix this client knows about. Order is stable. |
EventType |
./events |
type | "donation", "donation_updated", "match", "fact", "reward", "milestone", "challenge", "poll", "poll_option". |
FactType |
./events |
type | "campaign", "fundraising-event", "cause". |
PACKAGE_NAME, KNOWN_URLS |
./ |
const | Package metadata / Twitch Extension URL disclosure. |
TiltifyWebSocketThe constructor connects immediately:
new TiltifyWebSocket(onOpen, onError, onClose, address?) — address defaults
to DEFAULT_TILTIFY_SOCKET_URL.
| Method | Notes |
|---|---|
connect() |
Re-arm + re-open. The next onOpen reports isInitialConnection === true. |
addEventListener(listener) |
Firehose: (factID, event, type, payload) => void for every event on every joined fact. |
on(event, callback) |
Filtered form: (factID, type, payload) => void for one EventType. |
addChannelListener(listener) |
Per-channel ok / error / timeout acks. Register before you start listening. |
startListeningToCampaignBySlug(teamUserSlug, campaignSlug, events?) |
Resolves the slug pair via REST, then joins the campaign + its parent fundraising event. Throws if the campaign is not found. |
startListeningToFact(factID, events, type, lookup?) |
Low-level join. Idempotent per factID. lookup (default true) fetches the display name + parent facts. |
disconnect() |
Leaves every channel and closes the socket; next onClose reports isDisconnecting === true. |
getCampaignName(id) / getFundraisingEventName(id) / getCauseName(id) |
Cached display names, or undefined. |
getFactNameForType(factID, type) |
Dispatches to whichever of the three caches matches type. |
events defaults to EVENTS_TO_WATCH on both startListening* methods, but
on startListeningToFact it sits before the required type argument — pass
it explicitly there.
@playlive/tiltify-tools/tiltify-websocketThe surface is identical at the call site. Only the import specifier (and the underlying REST singleton) changed:
- import {
- EVENTS_TO_WATCH,
- type EventType,
- type FactType,
- TiltifyWebSocket,
- } from "@playlive/tiltify-tools/tiltify-websocket";
+ import {
+ EVENTS_TO_WATCH,
+ type EventType,
+ type FactType,
+ TiltifyWebSocket,
+ } from "@playlive/tiltify-phoenix";
Behavior deltas:
@playlive/tiltify-core tiltify
singleton instead of @playlive/tiltify-lib. Both expose the same Tiltify
class surface (FindCampaign, GetCampaign, GetFundraiser, GetCause,
GetFundraisingEventsSupportingCampaigns), so consumer code doesn't change.
If you were configuring @playlive/tiltify-lib's tiltify singleton at
boot, switch to configuring @playlive/tiltify-core's singleton.DEFAULT_TILTIFY_SOCKET_URL and overridable via the constructor's 4th
argument (same as before — the override is just documented now).Every Extension-eligible package declares the hosts / absolute URLs it can reach
over the network as KNOWN_URLS — the list a Twitch Extension submission must
disclose verbatim.
import { KNOWN_URLS } from "@playlive/tiltify-phoenix";
console.log(KNOWN_URLS);
// ["wss://websockets.tiltify.com"]
REST hosts (https://v5api.tiltify.com, the St. Jude proxy URLs) are disclosed
by @playlive/tiltify-core and inherited transitively — the
Extension form needs the union of both lists.
Follows the workspace's 0.x line. The actual realtime surface shipped in
0.2.0; 0.1.0 was an empty placeholder. See CHANGELOG.md
for per-version detail.
This package targets Tiltify's Phoenix Channels realtime gateway at
wss://websockets.tiltify.com/socket (DEFAULT_TILTIFY_SOCKET_URL). Tiltify
does not publish a specification for it: the channel topic format
(fact.<fact-id>.<event>) and the event suffix list (EVENTS_TO_WATCH) are
derived from observed wire traffic and frozen in this package, so pin the
version if you depend on an exact event set.
Fact lookups (slug → ID, parent fundraising-event resolution) delegate to the
documented Tiltify v5 REST API via @playlive/tiltify-core;
upstream reference documentation for that surface is at
https://developers.tiltify.com.
When Tiltify publishes new realtime event suffixes they are appended to
EVENTS_TO_WATCH (and to the derived EventType union) in a minor release.
The pattern a live-events provider in an overlay app wants. Register channel
and event listeners before starting to listen — addChannelListener only
fires for channels opened after it was registered.
import { tiltify } from "@playlive/tiltify-core";
import {
type EventType,
type FactType,
TiltifyWebSocket,
} from "@playlive/tiltify-phoenix";
interface DonationPayload {
id: string;
amount: { value: string; currency: string };
donor_name: string;
donor_comment: string | null;
}
export function startDonationFeed(teamUserSlug: string, campaignSlug: string) {
tiltify.UseProxy("prod");
tiltify.SetPublicMode();
const ws = new TiltifyWebSocket(
(isInitial) => {
// Phoenix auto-reconnects; only the first open is "initial".
if (!isInitial) console.info("tiltify socket reconnected");
},
(err) => console.error("tiltify socket error", err),
(isDisconnecting) => {
if (!isDisconnecting) console.warn("tiltify socket dropped unexpectedly");
},
);
// Per-channel join acks — useful for surfacing "subscribed" state in a UI.
ws.addChannelListener({
onOk: (factID, event, type) => console.debug("joined", { factID, event, type }),
onError: (factID, event, type, error) =>
console.error("join failed", { factID, event, type, error }),
onTimeout: (factID, event, type) => console.warn("join timed out", { factID, event, type }),
});
// Filtered subscription for the one event this feed cares about.
ws.on("donation", (factID, type: FactType, payload) => {
const donation = payload as DonationPayload;
const factName = ws.getFactNameForType(factID, type) ?? factID;
console.info(`💸 ${donation.donor_name} → ${factName}: ${donation.amount.value}`);
});
// Unfiltered firehose for logging / dev tooling.
ws.addEventListener((factID, event: EventType, type: FactType) => {
console.debug("tiltify event", { factID, event, type });
});
// Resolves the slug pair via REST, joins the campaign channel set, and —
// when the campaign belongs to one — the parent fundraising event's too.
const ready = ws
.startListeningToCampaignBySlug(teamUserSlug, campaignSlug)
.catch((err: unknown) => {
// "Could not find campaign with the provided slugs" on a 404;
// TiltifyLibError for transport failures.
console.error("could not subscribe", err);
ws.disconnect();
throw err;
});
return { ws, ready, stop: () => ws.disconnect() };
}
When you already know the fact id (from
@playlive/tiltify-core's FindCampaign, or
@playlive/tiltify-graphql's getFactByVanityAndSlug), call
startListeningToFact directly with lookup: false. The channel subscription
happens immediately; getCampaignName() stays undefined until you populate
it yourself.
import { TiltifyWebSocket } from "@playlive/tiltify-phoenix";
const ws = new TiltifyWebSocket(
() => {},
console.error,
() => {},
// Point at a local Phoenix simulator instead of Tiltify.
"ws://localhost:4000/socket",
);
// Only milestone + poll traffic, no name lookup.
await ws.startListeningToFact(
"9c1d0b7a-8f6e-4a2b-9c33-1f0e6a5b7c21",
["milestone", "poll", "poll_option"],
"fundraising-event",
false,
);
ws.on("milestone", (_factID, _type, payload) => {
console.log("milestone changed", payload);
});
startListeningToFact is idempotent per factID, so calling it again for a
fact you already joined is a no-op — even if the events list differs. Join
the widest event set you need the first time.
MIT © St. Jude Children's Research Hospital