IodidDOCUMENTATION

concepts.mdx

/doc/concepts

PUBLIC DOCUMENTATION

Concepts

Iodid domain glossary — puuid, riotId, shard, filter, MMR, staleness, ApiResult states.

Operational glossary for reading APIs, models and caches without ambiguity.

Identity

puuid

Riot global identifier, stable and unique per account. Primary key of Summoner (src/main/java/com/safjnest/lol/model/summoner/Summoner.java:21 @JsonProperty("puuid")). Used in all profile routes: GET /api/lol/{shard}/profile/{puuid} (src/main/java/com/safjnest/spring/controller/LolController.java:63). It contains no region information; region is always an explicit shard parameter.

riotId — String, not a record

riotId is a String in the format gameName#tagLine (src/main/java/com/safjnest/lol/model/summoner/Summoner.java:23 private String riotId). No dedicated RiotId type exists by architectural choice (AGENTS.md: "riotId remains a String"). Split via name()/tag() which do split("#",2) (src/main/java/com/safjnest/lol/model/summoner/Summoner.java:171).

Routes that accept Riot ID: GET /profile-by-name/{gameName}/{tagLine} (src/main/java/com/safjnest/spring/controller/LolController.java:226), GET /livegame-by-name/{gameName}/{tagLine} (:253).

Shard vs RegionShard

  • LeagueShard (no.stelar7.api.r4j.basic.constants.api.regions.LeagueShard) — LoL platform shard: EUW1, NA1, KR, etc. It is the {shard} parameter of every controller (LolController.java:36). Active list in LeagueShardUtils.getActives (src/main/java/com/safjnest/lol/utils/LeagueShardUtils.java:64).

  • RegionShard (RegionShard) — geographic grouping for Account/Match V5 APIs: EUROPE, AMERICAS, ASIA, SEA. Derived via LeagueShardUtils.getAccountRegion (src/main/java/com/safjnest/lol/utils/LeagueShardUtils.java:45) which maps VN2, OC1, SG2, PH2, TH2, TW2 -> ASIA, otherwise shard.toRegionShard().

Cache region is LeagueShardUtils.cacheRegion (src/main/java/com/safjnest/lol/utils/LeagueShardUtils.java:52) which returns shard.toRegionShard().name() — used as the first segment of Redis keys (ProfileService.java:341).

Filter — base64 key

Filter (src/main/java/com/safjnest/lol/model/Filter.java:13) encapsulates 10 dimensions:

text
champion, lane, queue, rank, rankBehavior, patch, region, opponent, duo, timeStart, timeEnd
  • canonical() (src/main/java/com/safjnest/lol/model/Filter.java:44) — current-season filter, zeroes everything except the period SeasonRange (SeasonUtils.getCurrentSeasonRange src/main/java/com/safjnest/lol/utils/SeasonUtils.java:41).
  • summoner(tStart, tEnd) (src/main/java/com/safjnest/lol/model/Filter.java:31) — filter for profile activity, explicit period.

Serialization:

  • toSummonerKey() (src/main/java/com/safjnest/lol/model/Filter.java:283) — | join + FilterCodec.encodeUrl (URL-safe Base64 without padding, src/main/java/com/safjnest/lol/utils/FilterCodec.java:15). 11-part format: champion|lane|queue|rank|rankBehavior|patch|region|opponent|duo|timeStart|timeEnd. Reverse decode fromSummonerKey (Filter.java:105) with validation parts.length == 11.
  • toKey() / pageKey() / toStateKey() / genericKey() — variants with standard Base64 (FilterCodec.encode :10) or URL, used for champion stats and pagination.

Filter keys are Redis keys for SUMMONER_STATISTICS, SUMMONER_ACTIVITY, SUMMONER_MATCHUPS (ProfileService.java:341).

Queue canonicalization

GameQueueTypeUtils.canonicalQueue (src/main/java/com/safjnest/lol/utils/GameQueueTypeUtils.java:38):

java
queue == TEAM_BUILDER_RANKED_SOLO ? RANKED_SOLO_5X5 : queue

TEAM_BUILDER_RANKED_SOLO is Riot's historic ID for Solo/Duo; it is always normalized to RANKED_SOLO_5X5 before persistence, indexing and queries. isRankedSolo (:48), defaultQueue (:44) and rankHistoryQueue (LolApiParameters.java:61) apply the same rule. Queues with leaderboards are only RANKED_SOLO_5X5 and RANKED_FLEX_SR (:25).

Patch — patchMajor

PatchUtils.getPatch() (src/main/java/com/safjnest/lol/utils/PatchUtils.java:20) returns the most recent patch from https://ddragon.leagueoflegends.com/api/versions.json (:84). patchMajor(patch) (:53) extracts major.minor (truncates the third bugfix segment). Patch is persisted both as the full string and as patchMajor for indexed queries (src/main/java/com/safjnest/lol/model/match/Match.java:74).

Filter.patch is validated as \d+\.\d+ in LolApiParameters.patch (src/main/java/com/safjnest/spring/controller/LolApiParameters.java:103). Champion statistical matrices cover the last 3 patches (ComputeScheduler.startChampionStatistics src/main/java/com/safjnest/lol/queue/scheduler/ComputeScheduler.java:132).

Season

SeasonUtils (src/main/java/com/safjnest/lol/utils/SeasonUtils.java:13) reads rsc/testing/lol_testing/split.json (:15). Structure: seasons[].splits[] with start_date/end_date (yyyy-MM-dd HH:mm:ss).

  • getCurrentSplitRange() (:22) — current split (now between start/end).
  • getCurrentSeasonRange() (:41) — aggregated current season (min start, max end of splits).
  • getSeasonRange(year) (:66) — lookup by year (year = 2010 + seasonNumber).
  • getSeasonRanges() (:143) — complete list.

Filter.canonical() uses getCurrentSeasonRange for timeStart/timeEnd. RankHistoryQuery validates season and patch against SeasonUtils (LolApiParameters.java:93).

MMR vs LP

  • LP — League Points of a single tier/division, value 0..100 (typically) provided by Riot.
  • MMR — continuous score computed by TierDivisionUtils.getMmr (src/main/java/com/safjnest/lol/utils/TierDivisionUtils.java:78): fixed base for TierDivisionType (IRON_IV=0, +100 per division, MASTER_I=10000, GRANDMASTER=20000, CHALLENGER=30000) + lp. UNRANKED returns -1.

getMmrRange(TierType) (src/main/java/com/safjnest/lol/utils/TierDivisionUtils.java:119) defines half-open intervals: IRON 0..400, BRONZE 400..800, ..., CHALLENGER 30000..∞. getTierFromMmr (:136) inverts the mapping. getAverageRank (:49) averages TierDivisionType ordinals.

MMR is used for leaderboard ordering and distribution; LP remains visible only as a Rank detail.

Staleness — 30 days + jitter

ProfileService.isStale (src/main/java/com/safjnest/lol/service/ProfileService.java:356):

java
STALE_BASE_MILLIS = 30 * DAY
STALE_JITTER_DAYS = 14
STALE_LAST_SEEN_MILLIS = 60 * DAY

jitter = floorMod(puuid.hashCode(), 15) * DAY   // 0..14 days
stale = now - lastUpdate >= 30d + jitter

Additional conditions:

  • If lastSeenAt missing or now - lastSeenAt > 60d → not stale (dormant profile, avoids useless refreshes).
  • If lastUpdate <=0 → stale.
  • If lastSeenAt null/blank → stale.

getStatistics(summoner, shard, filter) (ProfileService.java:101) starts ComputeScheduler.startProfileStatistics if missing, startStaleProfileStatistics (priority BACKGROUND) if stale. Same logic for getActivity (:146) and getMatchups (:168). Jitter spreads refresh load over time and avoids thundering herd at exactly 30 days.

Pending vs Ready — ApiResult

ApiResult<T> (src/main/java/com/safjnest/lol/model/ApiResult.java:3) — uniform wrapper for async responses:

java
enum Status { READY, PARTIAL, PENDING, NOT_FOUND }
StatusMeaningHTTPTypical use
READYcomplete and fresh payload200profile with current statistics, leaderboard
PARTIALpayload present but stale/incomplete200ProfileService.get returns ApiResult.partial when isStale (ProfileService.java:58) — client shows data but knows a refresh is in progress
PENDINGno payload, work queued202 + LolApiErrorprofile_pending, match_pending, profile_activity_pending — scheduled via ComputeScheduler/SyncScheduler
NOT_FOUNDentity does not exist404unknown puuid, invalid match id

LolApiResponses.from (src/main/java/com/safjnest/spring/controller/LolApiResponses.java:14) does the switch. PARTIAL and READY share the same 200 body but the client can distinguish via ResponseMetadata.refresh (ProfileService.java:58): ApiResult.partial(page, metadata) with refresh=true vs ApiResult.ready(page, metadata).

ResponseMetadata (src/main/java/com/safjnest/lol/model/ResponseMetadata.java:3) carries lastUpdate, refresh, pagination, filter — always present when data comes from cached/Mongo computation, null on NOT_FOUND/PENDING without context.