npm install @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.
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-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:
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.
/multi-search without the SDKbuildFilterString 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