1// @ts-check 2/** 3 * The one third-party data source the app talks to directly: Scryfall, used only to turn 4 * MTGO catalog ids into card names during an MTGO `.dek` import (our own card table has no 5 * MTGO ids). Lives in js/data so the UI layer never holds a URL or a `fetch` (NFR-A-4/A-7), 6 * and so the call is bounded like every other network call (NFR-C-7). 7 */ 8import { withTimeout } from './api.js'; 9 10const COLLECTION_URL = 'https://api.scryfall.com/cards/collection'; 11/** Scryfall's documented per-request identifier cap. */ 12const CHUNK = 75; 13const TIMEOUT_MS = 15_000; 14 15/** 16 * Resolve MTGO catalog ids â card names via Scryfall's batch collection endpoint, matched on 17 * `mtgo_id`. Best-effort: a failed or timed-out chunk is skipped, the rest still resolve. 18 * @param {number[]} ids 19 * @param {{ timeoutMs?: number }} [opts] the deadline is overridable so tests need not wait 15s. 20 * @returns {Promise<Map<number, string>>} 21 */ 22export async function resolveMtgoIds(ids, { timeoutMs = TIMEOUT_MS } = {}) { 23 const map = new Map(); 24 for (let i = 0; i < ids.length; i += CHUNK) { 25 const chunk = ids.slice(i, i + CHUNK); 26 let data; 27 try { 28 data = await withTimeout(timeoutMs, async (signal) => { 29 const r = await fetch(COLLECTION_URL, { 30 method: 'POST', 31 headers: { 'Content-Type': 'application/json' }, 32 body: JSON.stringify({ identifiers: chunk.map((mtgo_i
32d) => ({ mtgo_id })) }), 33 signal, 34 }); 35 return r.ok ? r.json() : null; 36 }); 37 } catch { 38 data = null; 39 } 40 for (const card of data?.data || []) { 41 if (typeof card.mtgo_id === 'number') map.set(card.mtgo_id, card.name); 42 if (typeof card.mtgo_foil_id === 'number') map.set(card.mtgo_foil_id, card.name); 43 } 44 } 45 return map; 46}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.