← all packages

@playlive/tiltify-search

v0.4.7

npm install @playlive/tiltify-search

Requires @playlive:registry=https://packages.playlive.experience.stjude.org in your .npmrc. packument · tarball · API docs


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

Install

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.

Quick start

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 exports

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.

API reference

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.

Upstream spec

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.

Twitch Extension URL disclosure

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.

Migration from @playlive/tiltify-tools

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

Imports just move:

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

Examples

Enumerate every St. Jude campaign, with progress and retry logging

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.

Compose a filter and drive /multi-search without the SDK

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`);
}

Cause → fundraising events → rewards drill-down

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}`);
}

Merge unindexed REST campaigns into a search result set

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));
  }
}

License

MIT © St. Jude Children's Research Hospital