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

    @playlive/tiltify-search

    @playlive/tiltify-search

    Meilisearch wrappers + a flags.tiltify.com client for the public Tiltify site-search surface. Ported from @playlive/tiltify-tools; class signatures, defaults, and the filter-string builder are preserved verbatim so consumers can migrate by changing the import specifier alone.

    Coverage

    bun add @playlive/tiltify-search meilisearch
    

    Two peer dependencies:

    Peer Range Optional Needed for
    meilisearch ^0.54.0 no Every *Search class, plus createMeilisearchClient.
    @playlive/tiltify-core workspace:* yes Only @playlive/tiltify-search/converters (REST campaign → FactHit).

    meilisearch is declared non-optional because the default barrel re-exports the index wrappers. The /filters, /flags, /multi, /http-client, and /types subpaths never touch it, so importing those directly keeps it out of your bundle even though the manifest asks for it.

    import { CampaignSearch, TiltifyFlags } from "@playlive/tiltify-search";

    const cs = new CampaignSearch();
    const page = await cs.getCampaigns({
    page: 1,
    hitsPerPage: 20,
    filter: { public: true, type: "campaign" },
    query: "marathon",
    });

    // Walk every campaign matching a filter — works around Meilisearch's
    // `maxTotalHits` cap by recursively partitioning along facetable axes.
    const all = await cs.getAllCampaigns(
    { public: true, type: "campaign" },
    ({ collected, total }) => console.log(`${collected}/${total}`),
    );

    // Hit the public flag endpoint
    const flags = await new TiltifyFlags().getFlags();
    Subpath Description
    @playlive/tiltify-search Default barrel — everything except /converters.
    @playlive/tiltify-search/types FactHit, FilterableFields, Avatar, FACT_DATA_TYPES, FactDataType.
    @playlive/tiltify-search/constants DEFAULT_SEARCH_API_KEY, DEFAULT_SEARCH_HOST, DEFAULT_FLAGS_URL.
    @playlive/tiltify-search/filters buildFilterString (no meilisearch dep).
    @playlive/tiltify-search/flags TiltifyFlags, TiltifyFlag (no meilisearch dep).
    @playlive/tiltify-search/multi TiltifyMultiSearch + its types (no meilisearch dep — raw HTTP).
    @playlive/tiltify-search/campaigns CampaignSearch + the getAllCampaigns partitioner and its option types.
    @playlive/tiltify-search/teams TeamSearch.
    @playlive/tiltify-search/users UserSearch.
    @playlive/tiltify-search/causes CauseSearch + cause/FE index types and CAUSE_INDEX_UIDS.
    @playlive/tiltify-search/rewards RewardSearch + RewardHit / RewardFilterableFields.
    @playlive/tiltify-search/auction-houses AuctionHouseSearch + CAUSE_IDS.ST_JUDE.
    @playlive/tiltify-search/http-client createResilientHttpClient, TiltifySearchHttpError, RetryInfo (no meilisearch dep).
    @playlive/tiltify-search/meilisearch-client createMeilisearchClient, createTiltifyFetchAdapter, the Tiltify page/hit caps.
    @playlive/tiltify-search/converters tiltifyCampaignToFactHit, restCampaignToFactHit. Opt-in only — pulls @playlive/tiltify-core.

    The /filters, /flags, /multi, /http-client, and /types subpaths are completely free of meilisearch — useful when you want filter-string composition, the retry client, or the raw multi-search endpoint without paying for the SDK. /converters is deliberately not re-exported from the default barrel so consumers who don't use it never pull @playlive/tiltify-core into their bundle.

    Full generated API documentation: https://packages.playlive.experience.stjude.org/p/@playlive/tiltify-search/docs/

    Top-level exports (default barrel):

    Export Source Kind Notes
    buildFilterString ./filters function Meilisearch filter literal composer; escapes " and \.
    CampaignSearch ./campaigns class facts index wrapper + getAllCampaigns partitioner.
    CAMPAIGN_FACT_TYPES, CampaignFactType ./campaigns const/type ["campaign", "team_event"] — the getAllCampaigns default.
    GetAllCampaignsFilter, GetAllCampaignsProgress, GetAllCampaignsOptions ./campaigns type Filter, progress event, and concurrency options.
    AuctionHouseSearch ./auction-houses class facts index wrapper scoped to type = "auction_house".
    CAUSE_IDS ./auction-houses const { ST_JUDE: "400f5687-…" }.
    CauseSearch ./causes class Wraps causes, fundraising_events, causes_and_fes.
    CAUSE_INDEX_UIDS, CauseIndexUid ./causes const/type The three cause/FE index uids.
    CauseOrEventHit, CauseOrEventFilterableFields ./causes type Cause/FE hit + filter shapes.
    RewardSearch ./rewards class rewards index wrapper.
    RewardHit, RewardFilterableFields ./rewards type Reward hit + filter shapes.
    TeamSearch ./teams class teams index wrapper (findTeamByTeamName).
    UserSearch ./users class user index wrapper (findUserByUsername only).
    TiltifyMultiSearch ./multi class Native-fetch wrapper for POST /multi-search.
    MultiSearchQuery, MultiSearchResult, MultiSearchResponse ./multi type Multi-search request/response shapes.
    TiltifyFlags, TiltifyFlag ./flags class/type flags.tiltify.com client.
    createResilientHttpClient ./http-client function Retry-After-aware httpClient for Meilisearch.
    TiltifySearchHttpError ./http-client class Carries status, url, bodySample (first 500 bytes).
    ResilientHttpClientOptions, RetryInfo ./http-client type Retry tunables + the onRetry diagnostic payload.
    createMeilisearchClient ./meilisearch-client function Meilisearch client pre-wired with the resilient httpClient.
    createTiltifyFetchAdapter ./meilisearch-client function Routes single-index searches through /multi-search + Origin.
    translateIndexSearchToMultiSearch ./meilisearch-client const Deprecated alias of createTiltifyFetchAdapter; removed in 0.5.0.
    TILTIFY_HITS_PER_PAGE_CAP, TILTIFY_MAX_PAGES, TILTIFY_MAX_TOTAL_HITS ./meilisearch-client const 100 / 10 / 1000 — the upstream pagination window.
    FactHit, FilterableFields, Avatar ./types type Shared facts-index shapes.
    FACT_DATA_TYPES, FactDataType ./types const/type Sub-resource enumeration for crawl orchestration.
    KNOWN_URLS ./ const Twitch Extension URL disclosure.
    PACKAGE_NAME ./ const Identifier for runtime version-pinning.

    Available only via the /converters subpath (not in the barrel): tiltifyCampaignToFactHit, restCampaignToFactHit, ConvertToFactHitOptions.

    This package targets the public Meilisearch indices behind tiltify.com's site search (https://api.tiltify.com/search, DEFAULT_SEARCH_HOST) plus the public flag endpoint at https://flags.tiltify.com (DEFAULT_FLAGS_URL). Neither has a published schema — the hit shapes here mirror what tiltify.com itself receives, so pin the version if you depend on exact field sets.

    The host and the public read-only search key are exported as constants (DEFAULT_SEARCH_HOST, DEFAULT_SEARCH_API_KEY) and refreshed in a release whenever Tiltify rotates the key or moves the host.

    For the documented, spec-backed Tiltify surface — the v5 REST API used by @playlive/tiltify-core and by this package's /converters subpath — see https://developers.tiltify.com.

    The KNOWN_URLS export enumerates every absolute URL or host this package can fetch — the list a Twitch Extension submission must disclose verbatim.

    import { KNOWN_URLS } from "@playlive/tiltify-search";
    console.log(KNOWN_URLS);
    // [
    // "https://api.tiltify.com",
    // "https://flags.tiltify.com",
    // ]

    The Meilisearch indices moved from site-search.tiltify.com to https://api.tiltify.com/search (DEFAULT_SEARCH_HOST), which is why only the two hosts above appear. Keep this list and the source export in sync — the Extension submission form requires the disclosure list verbatim.

    @playlive/tiltify-search is a near drop-in replacement for the search + flags surface that used to live inside tiltify-tools. Every class, method signature, and exported type is preserved unchanged with one exception:

    • UserSearch#getUserBySlug was removed. That method hit the Tiltify GraphQL endpoint and required a transitive GraphQL dependency; this package now stays free of any GraphQL dependency. Replacement: TiltifyGraphQL#getUserBySlug in @playlive/tiltify-graphql.

    Imports just move:

    - import { CampaignSearch } from "@playlive/tiltify-tools/campaign-search";
    + import { CampaignSearch } from "@playlive/tiltify-search";
    

    getAllCampaigns is the reason this package exists. Meilisearch caps any single (filter, query) tuple at TILTIFY_MAX_TOTAL_HITS (1000) reachable hits and Tiltify additionally caps hitsPerPage at 100 / page at 10 — so the method recursively partitions the filter along facetable axes until each leaf bucket fits under the cap, then paginates each leaf. Hits are deduped by id.

    import { CampaignSearch, type FactHit } from "@playlive/tiltify-search";
    import { CAUSE_IDS } from "@playlive/tiltify-search/auction-houses";
    import { TiltifySearchHttpError } from "@playlive/tiltify-search/http-client";

    // The third constructor arg tunes the resilient httpClient shared by every
    // wrapper class: Retry-After-aware backoff on 429/5xx, JSON content-type
    // guard, and structured errors with a body sample.
    const campaigns = new CampaignSearch(undefined, undefined, {
    maxRetries: 4,
    baseBackoffMs: 750,
    onRetry: ({ attempt, status, retryAfterMs, url }) => {
    console.warn(`tiltify search retry ${attempt} (HTTP ${status}, +${retryAfterMs}ms) ${url}`);
    },
    });

    export async function allStJudeCampaigns(): Promise<FactHit[]> {
    try {
    const hits = await campaigns.getAllCampaigns(
    { public: true, cause_public_id: CAUSE_IDS.ST_JUDE },
    ({ collected, total }) => {
    // `total` is fixed at probe time; `collected` grows monotonically.
    process.stdout.write(`\r${collected}/${total} campaigns…`);
    },
    // Default is 2 in-flight requests. Tiltify's Cloudflare layer
    // rate-limits shared NAT egress IPs, so stay gentle from Lambda.
    { concurrency: 2 },
    );

    return hits.sort((a, b) => b.total_amount_raised - a.total_amount_raised);
    } catch (err) {
    if (err instanceof TiltifySearchHttpError) {
    // `bodySample` is the first 500 bytes — a Cloudflare challenge page
    // reads very differently from a genuine Meilisearch error here.
    console.error(`search failed HTTP ${err.status} on ${err.url}: ${err.bodySample}`);
    return [];
    }
    throw err;
    }
    }

    Omitting type from the filter scopes results to CAMPAIGN_FACT_TYPES (campaign + team_event); the facts index also holds causes, fundraising events, users, teams, and auction houses, which would otherwise leak in.

    buildFilterString and TiltifyMultiSearch have no meilisearch dependency, so this whole block tree-shakes down to plain fetch.

    import { buildFilterString } from "@playlive/tiltify-search/filters";
    import { TiltifyMultiSearch } from "@playlive/tiltify-search/multi";
    import type { FactHit } from "@playlive/tiltify-search/types";

    const filter = buildFilterString({ public: true, type: "campaign", status: "" });
    // → 'public = true AND type = "campaign"' (undefined + "" clauses are dropped)

    const ms = new TiltifyMultiSearch();
    const { results } = await ms.multiSearch<FactHit>([
    { indexUid: "facts", q: "st jude", filter, hitsPerPage: 8, page: 1 },
    { indexUid: "causes_and_fes", q: "st jude", hitsPerPage: 8, page: 1 },
    ]);

    for (const result of results) {
    console.log(`${result.indexUid}: ${result.totalHits} hits in ${result.processingTimeMs}ms`);
    }

    Each wrapper returns the raw Meilisearch response, so hits, totalHits, and page are all available.

    import { CauseSearch, RewardSearch } from "@playlive/tiltify-search";

    const causes = new CauseSearch();
    const rewards = new RewardSearch();

    // 1. Find the cause record in the unioned `causes_and_fes` index.
    const { hits: causeHits } = await causes.getCauses({ query: "st jude", hitsPerPage: 1 });
    const causeFactPublicId = causeHits[0]?.cause_fact_public_id;
    if (!causeFactPublicId) throw new Error("cause not found");

    // 2. Every published fundraising event under that cause.
    const { hits: events } = await causes.getFundraisingEventsByCauseFactPublicId({
    causeFactPublicId,
    hitsPerPage: 50,
    });

    // 3. Active rewards for the first event.
    const first = events[0];
    if (first?.fundraising_event_public_id) {
    const { hits: incentives } = await rewards.getRewards({
    filter: {
    fundraising_event_public_id: first.fundraising_event_public_id,
    active: true,
    public: true,
    },
    hitsPerPage: 50,
    });
    console.log(`${incentives.length} active rewards on ${first.name ?? first.id}`);
    }

    The facts index lags for brand-new campaigns. /converters turns a Tiltify REST campaign into a FactHit so both sources render through the same UI code — prefer the real index hit whenever both exist for an id.

    import { tiltify } from "@playlive/tiltify-core";
    import { CampaignSearch, type FactHit } from "@playlive/tiltify-search";
    import { tiltifyCampaignToFactHit } from "@playlive/tiltify-search/converters";

    tiltify.UseProxy("prod");

    const indexed = await new CampaignSearch().getAllCampaigns({ public: true });
    const byId = new Map<string, FactHit>(indexed.map((hit) => [hit.id, hit]));

    const fresh = await tiltify.GetUserCampaigns("user-id");
    for (const campaign of fresh) {
    // Index hits win; REST-derived hits only fill gaps.
    if (!byId.has(campaign.id)) {
    byId.set(campaign.id, tiltifyCampaignToFactHit(campaign));
    }
    }

    MIT © St. Jude Children's Research Hospital