Pagination & async iterators¶
Some Riot endpoints are unbounded. A player's match history and a ranked ladder
are both far too large to return in one response, so Riot paginates them — the
match list by an item offset (start + count), the ranked ladder by a
page number. Walking either one by hand means a while loop, a cursor you
increment yourself, and manual pacing so you don't trip the rate limiter.
yasuo hides all of that behind a single type: Paginator<T>. It is a lazy
AsyncIterable —
you for await over it and it fetches pages on demand, one at a time, each
fetch flowing through the rate limiter. Nothing is requested
until you start iterating, and when you stop, it stops. Because the starting
cursor is configurable, you can begin from any offset or page and resume
later from exactly where you left off.
import { Yasuo, RegionGroup } from 'yasuo.js'
const yasuo = new Yasuo({ key: process.env.RIOT_API_KEY })
// Every match in a player's history, paced automatically:
for await (const id of yasuo.lol.match.streamIds(puuid, RegionGroup.ASIA)) {
console.log(id) // 'KR_1234…'
}
The four ways to consume a Paginator¶
Every stream* method returns a Paginator<T> directly — a paginator is not
a query builder, so there is no .execute() to call and no single result to
await; you iterate the paginator itself. You can drain it four ways.
Iteration throws on failure. Where a single/collection query's
.execute()resolves the entity/collection directly and never throws for an API error (it sets.errorinstead), aPaginatorfollows async-iterator convention: every page is fetched with{ throw: true }internally, so a failed request (404,429,5xx, a transport error) throws mid-iteration. Wrap afor await,.toArray(),.first()or.pages()loop intry/catchwhen you need to handle those — the thrown value is the same richApiErrorthe query builders attach as the result's.error.
Item by item — for await¶
The paginator is an async iterable, so iterate it directly. Pages are fetched transparently as you cross their boundaries:
for await (const entry of yasuo.lol.league.streamEntries(
RankedQueue.SOLO_5x5, Tier.DIAMOND, Division.I, Region.EUW,
)) {
console.log(entry.puuid, entry.leaguePoints)
}
Collect eagerly — toArray(limit?)¶
Pull everything (or up to limit items) into a plain array. The optional
limit stops the fetching early — pages past the limit are never requested:
Peek at the first — first()¶
Fetch just the first item, or null if the sequence is empty. Only one page is
ever requested:
const mostRecent = await yasuo.lol.match
.streamIds(puuid, RegionGroup.ASIA)
.first() // string | null
Page by page — pages()¶
pages() yields whole Page<T> objects instead of individual items. Each page
exposes its items, the response meta for the request that produced it (rate
limits, status, url — see entities), and the
cursor it was fetched with:
for await (const page of yasuo.lol.match.streamMatches(puuid, RegionGroup.ASIA).pages()) {
console.log(`cursor ${page.cursor}: ${page.items.length} matches`)
console.log(page.meta.rateLimits.app) // budget for this page's request
await persist(page.items) // process a whole batch at once
}
interface Page<T> {
readonly items: readonly T[] // the items on this page
readonly meta: ResponseMeta // rate limits / status / url for this fetch
readonly cursor: number // the offset or page number it was fetched with
}
Streaming a match history¶
yasuo.lol.match exposes two streams, both regionally routed
(RegionGroup):
streamIds(puuid, regionGroup, options?)→Paginator<string>— match ids.streamMatches(puuid, regionGroup, options?)→Paginator<MatchEntity>— the full match entities. Each page fetches its ids, then hydrates every match (one request per match), so this is heavier — lean onmaxItems.
Both take the same MatchStreamOptions:
| Option | Type | Default | Meaning |
|---|---|---|---|
start |
number |
0 |
Item offset to begin at — start from anywhere. |
pageSize |
number |
100 |
Match ids fetched per request (1–100). |
maxItems |
number |
— | Hard cap on the total items yielded. |
startTime |
number |
— | Epoch seconds; only matches after this time. |
endTime |
number |
— | Epoch seconds; only matches before this time. |
queue |
number |
— | Filter by queue id. Cannot be combined with type. |
type |
MatchType |
— | Match category. Cannot be combined with queue. |
import { RegionGroup, MatchType } from 'yasuo.js'
// Ranked matches only, 100 per request, but stop after 500:
for await (const match of yasuo.lol.match.streamMatches(puuid, RegionGroup.ASIA, {
pageSize: 100,
maxItems: 500,
type: MatchType.RANKED,
})) {
console.log(match.info.gameCreation, match.metadata.matchId)
}
// Start from the 300th match — every page after that offset:
const older = yasuo.lol.match.streamIds(puuid, RegionGroup.ASIA, { start: 300 })
Streaming a ranked ladder¶
yasuo.lol.league.streamEntries(queue, tier, division, region, options?) walks
a whole tier/division, page by page (Riot pages the ladder, not offsets), and
returns a Paginator<LeagueEntryEntity>:
import { RankedQueue, Tier, Division, Region } from 'yasuo.js'
for await (const entry of yasuo.lol.league.streamEntries(
RankedQueue.SOLO_5x5, Tier.DIAMOND, Division.I, Region.EUW,
)) {
console.log(entry.puuid, entry.wins, entry.losses)
}
LeagueStreamOptions is page-based:
| Option | Type | Default | Meaning |
|---|---|---|---|
startPage |
number |
1 |
Page to begin at (1-indexed) — lets you resume. |
maxItems |
number |
— | Hard cap on the total entries yielded. |
// Resume from page 7, and only take 200 more entries:
const rest = yasuo.lol.league.streamEntries(
RankedQueue.SOLO_5x5, Tier.DIAMOND, Division.I, Region.EUW,
{ startPage: 7, maxItems: 200 },
)
Via lazy relations¶
If you already hold a summoner reference, the same streams hang off it — the
region group is derived for you, so a summoner on Region.KR streams from
RegionGroup.ASIA without you re-specifying it:
const summoner = yasuo.lol.summoner.byPuuid(puuid, Region.KR)
for await (const id of summoner.streamMatchIds({ maxItems: 1000 })) {
console.log(id)
}
for await (const match of summoner.streamMatches({ type: MatchType.RANKED })) {
console.log(match.metadata.matchId)
}
streamMatchIds / streamMatches take the exact same MatchStreamOptions as
their yasuo.lol.match counterparts.
Practical patterns¶
Cap the total with maxItems. The surest way to bound work — the paginator
stops fetching the moment the cap is reached, mid-page if need be:
const recent = await yasuo.lol.match
.streamMatches(puuid, RegionGroup.ASIA, { maxItems: 20 })
.toArray()
Resume from a saved offset or page. Persist the last cursor you saw, then
hand it back as start (matches) or startPage (ladder) next run:
let last = await loadCursor() // e.g. 400
for await (const page of yasuo.lol.match.streamIds(puuid, RegionGroup.ASIA, {
start: last,
}).pages()) {
await handle(page.items)
last = page.cursor + page.items.length
await saveCursor(last)
}
Process in batches with pages(). When your sink is batch-oriented (a bulk
insert, a queue publish), iterate pages() and hand off page.items whole
rather than one item at a time.
Early-exit by breaking the loop. Because fetching is lazy, break costs
nothing — no further pages are requested: