@playlive/tiltify-phoenix
    Preparing search index...

    @playlive/tiltify-phoenix

    @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.

    Coverage

    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.

    The 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.

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

    • REST lookups now use the @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.
    • The socket URL was hard-coded inside the legacy class. It's now exposed as 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