OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
 
master @ 308 LINES
 
[ HISTORY ]  [ UP ]
 

// index cache system for Discord bot
// loads and caches API indexes for autocomplete and validation

import { API_BASE_URL, CACHE } from "../constants.js";

// simplified types for cached data
export interface CachedDungeon {
  id: number;
  slug: string;
  name: string;
  short_name: string;
}

export interface CachedClass {
  id: number;
  key: string;
  name: string;
  specs: string[];
}

export interface CachedPlayer {
  id: number;
  name: string;
  region: string;
  realm_slug: string;
  realm_name: string;
  class_name: string;
  global_ranking: number;
  global_ranking_bracket: string;
}

export interface CachedRealm {
  slug: string;
  name: string;
  connected_realm_id: number;
  parent_realm?: string | null;
  player_count: number;
}

export interface CachedSeason {
  id: string;
  name: string;
  start_timestamp: string;
  end_timestamp?: string;
  is_current: boolean;
}

interface Cache {
  dungeons: CachedDungeon[];
  classes: CachedClass[];
  players: CachedPlayer[];
  realms: {
    us: CachedRealm[];
    eu: CachedRealm[];
    kr: CachedRealm[];
    tw: CachedRealm[];
  };
  seasons: CachedSeason[];
  lastUpdated: {
    dungeons: number;
    classes: number;
    players: number;
    realms: Record<string, number>;
    seasons: number;
  };
}

// module-level cache
const cache: Cache = {
  dungeons: [],
  classes: [],
  players: [],
  realms: {
    us: [],
    eu: [],
    kr: [],
    tw: [],
  },
  seasons: [],
  lastUpdated: {
    dungeons: 0,
    classes: 0,
    players: 0,
    realms: {},
    seasons: 0,
  },
};

// loads dungeon index from API
async function loadDungeons(): Promise<CachedDungeon[]> {
  const now = Date.now();
  if (
    cache.dungeons.length > 0 &&
    now - cache.lastUpdated.dungeons < CACHE.INDEX_TTL_MS
  ) {
    return cache.dungeons;
  }

  try {
    const url = `${API_BASE_URL}/api/leaderboard/season/2/global/index.json`;
    const response = await fetch(url);
    if (!response.ok)
      throw new Error(`Failed to fetch dungeons: ${response.statusText}`);

    const data = await response.json();
    cache.dungeons = data.data || [];
    cache.lastUpdated.dungeons = now;
    console.log(`Loaded ${cache.dungeons.length} dungeons`);
    return cache.dungeons;
  } catch (error) {
    console.error("Failed to load dungeons:", error);
    return cache.dungeons; // return stale cache if available
  }
}

// loads class index from API
async function loadClasses(): Promise<CachedClass[]> {
  const now = Date.now();
  if (
    cache.classes.length > 0 &&
    now - cache.lastUpdated.classes < CACHE.INDEX_TTL_MS
  ) {
    return cache.classes;
  }

  try {
    const url = `${API_BASE_URL}/api/leaderboard/season/2/players/class/index.json
`;
    const response = await fetch(url);
    if (!response.ok)
      throw new Error(`Failed to fetch classes: ${response.statusText}`);

    const data = await response.json();
    cache.classes = data.data || [];
    cache.lastUpdated.classes = now;
    console.log(`Loaded ${cache.classes.length} classes`);
    return cache.classes;
  } catch (error) {
    console.error("Failed to load classes:", error);
    return cache.classes;
  }
}

// loads player search index from API (all shards)
async function loadPlayers(): Promise<CachedPlayer[]> {
  const now = Date.now();
  if (
    cache.players.length > 0 &&
    now - cache.lastUpdated.players < CACHE.PLAYER_SEARCH_TTL_MS
  ) {
    return cache.players;
  }

  try {
    // load all shards (000, 001, 002, ...)
    const shards = ["000", "001", "002"];
    const allPlayers: CachedPlayer[] = [];

    for (const shard of shards) {
      const url = `${API_BASE_URL}/api/search/players-${shard}.json`;
      const response = await fetch(url);
      if (!response.ok) {
        console.warn(
          `Failed to fetch player shard ${shard}: ${response.statusText}`,
        );
        continue;
      }

      const data = await response.json();
      if (data.players) {
        allPlayers.push(...data.players);
      }
    }

    cache.players = allPlayers;
    cache.lastUpdated.players = now;
    console.log(`Loaded ${cache.players.length} players`);
    return cache.players;
  } catch (error) {
    console.error("Failed to load players:", error);
    return cache.players;
  }
}

// loads realm index for a specific region
async function loadRealms(
  region: "us" | "eu" | "kr" | "tw",
): Promise<CachedRealm[]> {
  const now = Date.now();
  const lastUpdate = cache.lastUpdated.realms[region] || 0;

  if (
    cache.realms[region].length > 0 &&
    now - lastUpdate < CACHE.REGION_INDEX_TTL_MS
  ) {
    return cache.realms[region];
  }

  try {
    const url = `${API_BASE_URL}/api/leaderboard/season/2/${region}/index.json`;
    const response = await fetch(url);
    if (!response.ok)
      throw new Error(
        `Failed to fetch ${region} realms: ${response.statusText}`,
      );

    const data = await response.json();
    cache.realms[region] = data.data || [];
    cache.lastUpdated.realms[region] = now;
    console.log(
      `Loaded ${cache.realms[region].length} ${region.toUpperCase()} realms`,
    );
    return cache.realms[region];
  } catch (error) {
    console.error(`Failed to load ${region} realms:`, error);
    return cache.realms[region];
  }
}

// loads season index from API
async function loadSeasons(): Promise<CachedSeason[]> {
  const now = Date.now();
  if (
    cache.seasons.length > 0 &&
    now - cache.lastUpdated.seasons < CACHE.INDEX_TTL_MS
  ) {
    return cache.seasons;
  }

  try {
    const url = `${API_BASE_URL}/api/leaderboard/season/index.json`;
    const response = await fetch(url);
    if (!response.ok)
      throw new Error(`Failed to fetch seasons: ${response.statusText}`);

    const data = await response.json();
    cache.seasons = data.data || [];
    cache.lastUpdated.seasons = now;
    console.log(`Loaded ${cache.seasons.length} seasons`);
    return cache.seasons;
  } catch (error) {
    console.error("Failed to load seasons:", error);
    return cache.seasons;
  }
}

// initializes all caches (call on serverless function cold start)
// only loads lightweight indexes - player search loads on-demand
export async function initializeCaches(): Promise<void> {
  console.log("Initializing Discord bot caches...");
  try {
    await Promise.all([
      loadDungeons(),
      loadClasses(),
      // loadPlayers() removed - loads on-demand for /player autocomplete only
      loadSeasons(),
    ]);
    console.log("Discord bot caches initialized successfully");
  } catch (error) {
    console.error("Failed to initialize caches:", error);
  }
}

// gets cached dungeons (loads if not cached)
export async function getDungeons(): Promise<CachedDungeon[]> {
  return loadDungeons();
}

/**
 * gets cached classes (loads if not cached)
 */
export async function getClasses(): Promise<CachedClass[]> {
  return loadClasses();
}

// gets cached players (loads if not cached)
export async function getPlayers(): Promise<CachedPlayer[]> {
  return loadPlayers();
}

// gets cached realms for a region (loads if not cached)
export async function getRealms(
  region: "us" | "eu" | "kr" | "tw",
): Promise<CachedRealm[]> {
  return loadRealms(region);
}

// gets cached seasons (loads if not cached)
export async function getSeasons(): Promise<CachedSeason[]> {
  return loadSeasons();
}

// clears all caches (useful for testing or manual refresh)
export function clearCaches(): void {
  cache.dungeons = [];
  cache.classes = [];
  cache.players = [];
  cache.realms = { us: [], eu: [], kr: [], tw: [] };
  cache.seasons = [];
  cache.lastUpdated = {
    dungeons: 0,
    classes: 0,
    players: 0,
    realms: {},
    seasons: 0,
  };
  console.log("Caches cleared");
}

// index cache system for Discord bot
// loads and caches API indexes for autocomp
lete and validation

import { API_BASE_URL, CACHE } from "../cons
tants.js";

// simplified types for cached data
export interface CachedDungeon {
  id: number;
  slug: string;
  name: string;
  short_name: string;
}

export interface CachedClass {
  id: number;
  key: string;
  name: string;
  specs: string[];
}

export interface CachedPlayer {
  id: number;
  name: string;
  region: string;
  realm_slug: string;
  realm_name: string;
  class_name: string;
  global_ranking: number;
  global_ranking_bracket: string;
}

export interface CachedRealm {
  slug: string;
  name: string;
  connected_realm_id: number;
  parent_realm?: string | null;
  player_count: number;
}

export interface CachedSeason {
  id: string;
  name: string;
  start_timestamp: string;
  end_timestamp?: string;
  is_current: boolean;
}

interface Cache {
  dungeons: CachedDungeon[];
  classes: CachedClass[];
  players: CachedPlayer[];
  realms: {
    us: CachedRealm[];
    eu: CachedRealm[];
    kr: CachedRealm[];
    tw: CachedRealm[];
  };
  seasons: CachedSeason[];
  lastUpdated: {
    dungeons: number;
    classes: number;
    players: number;
    realms: Record<string, number>;
    seasons: number;
  };
}

// module-level cache
const cache: Cache = {
  dungeons: [],
  classes: [],
  players: [],
  realms: {
    us: [],
    eu: [],
    kr: [],
    tw: [],
  },
  seasons: [],
  lastUpdated: {
    dungeons: 0,
    classes: 0,
    players: 0,
    realms: {},
    seasons: 0,
  },
};

// loads dungeon index from API
async function loadDungeons(): Promise<Cache
dDungeon[]> {
  const now = Date.now();
  if (
    cache.dungeons.length > 0 &&
    now - cache.lastUpdated.dungeons < CACHE
.INDEX_TTL_MS
  ) {
    return cache.dungeons;
  }

  try {
    const url = `${API_BASE_URL}/api/leaderb
oard/season/2/global/index.json`;
    const response = await fetch(url);
    if (!response.ok)
      throw new Error(`Failed to fetch dunge
ons: ${response.statusText}`);

    const data = await response.json();
    cache.dungeons = data.data || [];
    cache.lastUpdated.dungeons = now;
    console.log(`Loaded ${cache.dungeons.len
gth} dungeons`);
    return cache.dungeons;
  } catch (error) {
    console.error("Failed to load dungeons:"
, error);
    return cache.dungeons; // return stale c
ache if available
  }
}

// loads class index from API
async function loadClasses(): Promise<Cached
Class[]> {
  const now = Date.now();
  if (
    cache.classes.length > 0 &&
    now - cache.lastUpdated.classes < CACHE.
INDEX_TTL_MS
  ) {
    return cache.classes;
  }

  try {
    const url = `${API_BASE_URL}/api/leaderb
oard/season/2/players/class/index.json`;
    const response = await fetch(url);
    if (!response.ok)
      throw new Error(`Failed to fetch class
es: ${response.statusText}`);

    const data = await response.json();
    cache.classes = data.data || [];
    cache.lastUpdated.classes = now;
    console.log(`Loaded ${cache.classes.leng
th} classes`);
    return cache.classes;
  } catch (error) {
    console.error("Failed to load classes:",
 error);
    return cache.classes;
  }
}

// loads player search index from API (all s
hards)
async function loadPlayers(): Promise<Cached
Player[]> {
  const now = Date.now();
  if (
    cache.players.length > 0 &&
    now - cache.lastUpdated.players < CACHE.
PLAYER_SEARCH_TTL_MS
  ) {
    return cache.players;
  }

  try {
    // load all shards (000, 001, 002, ...)
    const shards = ["000", "001", "002"];
    const allPlayers: CachedPlayer[] = [];

    for (const shard of shards) {
      const url = `${API_BASE_URL}/api/searc
h/players-${shard}.json`;
      const response = await fetch(url);
      if (!response.ok) {
        console.warn(
          `Failed to fetch player shard ${sh
ard}: ${response.statusText}`,
        );
        continue;
      }

      const data = await response.json();
      if (data.players) {
        allPlayers.push(...data.players);
      }
    }

    cache.players = allPlayers;
    cache.lastUpdated.players = now;
    console.log(`Loaded ${cache.players.leng
th} players`);
    return cache.players;
  } catch (error) {
    console.error("Failed to load players:",
 error);
    return cache.players;
  }
}

// loads realm index for a specific region
async function loadRealms(
  region: "us" | "eu" | "kr" | "tw",
): Promise<CachedRealm[]> {
  const now = Date.now();
  const lastUpdate = cache.lastUpdated.realm
s[region] || 0;

  if (
    cache.realms[region].length > 0 &&
    now - lastUpdate < CACHE.REGION_INDEX_TT
L_MS
  ) {
    return cache.realms[region];
  }

  try {
    const url = `${API_BASE_URL}/api/leaderb
oard/season/2/${region}/index.json`;
    const response = await fetch(url);
    if (!response.ok)
      throw new Error(
        `Failed to fetch ${region} realms: $
{response.statusText}`,
      );

    const data = await response.json();
    cache.realms[region] = data.data || [];
    cache.lastUpdated.realms[region] = now;
    console.log(
      `Loaded ${cache.realms[region].length}
 ${region.toUpperCase()} realms`,
    );
    return cache.realms[region];
  } catch (error) {
    console.error(`Failed to load ${region} 
realms:`, error);
    return cache.realms[region];
  }
}

// loads season index from API
async function loadSeasons(): Promise<Cached
Season[]> {
  const now = Date.now();
  if (
    cache.seasons.length > 0 &&
    now - cache.lastUpdated.seasons < CACHE.
INDEX_TTL_MS
  ) {
    return cache.seasons;
  }

  try {
    const url = `${API_BASE_URL}/api/leaderb
oard/season/index.json`;
    const response = await fetch(url);
    if (!response.ok)
      throw new Error(`Failed to fetch seaso
ns: ${response.statusText}`);

    const data = await response.json();
    cache.seasons = data.data || [];
    cache.lastUpdated.seasons = now;
    console.log(`Loaded ${cache.seasons.leng
th} seasons`);
    return cache.seasons;
  } catch (error) {
    console.error("Failed to load seasons:",
 error);
    return cache.seasons;
  }
}

// initializes all caches (call on serverles
s function cold start)
// only loads lightweight indexes - player s
earch loads on-demand
export async function initializeCaches(): Pr
omise<void> {
  console.log("Initializing Discord bot cach
es...");
  try {
    await Promise.all([
      loadDungeons(),
      loadClasses(),
      // loadPlayers() removed - loads on-de
mand for /player autocomplete only
      loadSeasons(),
    ]);
    console.log("Discord bot caches initiali
zed successfully");
  } catch (error) {
    console.error("Failed to initialize cach
es:", error);
  }
}

// gets cached dungeons (loads if not cached
)
export async function getDungeons(): Promise
<CachedDungeon[]> {
  return loadDungeons();
}

/**
 * gets cached classes (loads if not cached)
 */
export async function getClasses(): Promise<
CachedClass[]> {
  return loadClasses();
}

// gets cached players (loads if not cached)
export async function getPlayers(): Promise<
CachedPlayer[]> {
  return loadPlayers();
}

// gets cached realms for a region (loads if
 not cached)
export async function getRealms(
  region: "us" | "eu" | "kr" | "tw",
): Promise<CachedRealm[]> {
  return loadRealms(region);
}

// gets cached seasons (loads if not cached)
export async function getSeasons(): Promise<
CachedSeason[]> {
  return loadSeasons();
}

// clears all caches (useful for testing or 
manual refresh)
export function clearCaches(): void {
  cache.dungeons = [];
  cache.classes = [];
  cache.players = [];
  cache.realms = { us: [], eu: [], kr: [], t
w: [] };
  cache.seasons = [];
  cache.lastUpdated = {
    dungeons: 0,
    classes: 0,
    players: 0,
    realms: {},
    seasons: 0,
  };
  console.log("Caches cleared");
}
 
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET