memoscan
← all memos

Memo 0x1e189c27…d32601 on Ethereum

DOS_loader/* ============================================================================= * COS_loader.js — Decentralized Operation System boot splash + script loader * ----------------------------------------------------------------------------- * Single self-contained bootloader. Include this ONE file first; it: * 1. Paints a full-screen splash: title block ("DECENTRALIZED / OPERATING / * SYSTEM" + subtitle), a page-centred radar drawing, and a terminal-style * log with the load % pinned to the first line. * 2. Fetches every script from ScriptyStorageV2 via eth_call * getContent(name, 0x) through a parallel RPC pool (one worker per * endpoint, paced + cooldown, mirroring COS_ethCalls.js rpcPool), then * gunzips + injects them strictly in order. Each script prints a new line * at the top of the log (previous pushed down); the radar sweep + % track * overall progress (one full rotation == 100%). * 3. On resize the title scales to the space above the centred drawing, and * the drawing + log match the title block's width. * 4. Once everything is loaded (init() builds the world), fades the splash out * and reveals the world. * * Configure the script list via window.DOS_SCRIPTS BEFORE this file runs, * or via DEFAULT_SCRIPTS declared by DOS_LoadingArray.js (injected by the * renderer right before this file). Each entry: * { data: "<scripty-content-name>", gzip: true|false, label: "Human name" } * RPC endpoints come from the renderer-injected RPCString / WSRPCString. * The shared RPC layer (RPCs, WS_RPCs, rpcPool, rpcSleep, parallelRpcMap, * firstWorkingRpc, parseRpcString, parseClaimOrderString) is defined HERE and * exposed on window — every script injected afterwards reuses it. * The scripty address can be overridden via window.DOS_SCRIPTY_ADDRESS. * * The world's init() (called from COS_animate.js) can signal true readiness by * setting window.__WORLD_READY = true or calling DOSLoader.finish(). * * Styling: all colours are hardcoded. Only the viewport-driven sizes are left as * CSS variables — --w (block width), --gap, and --tu (title unit, JS-refined). * ========================================================================== */ (function () { "use strict"; /* ------------------------------------------------------------------ config */ var SETTLE_MS = 1200; // (legacy) unused now that we wait for __WORLD_READY var READY_TIMEOUT_MS = 30000; // hard cap: hide even if __WORLD_READY never set // ScriptyStorageV2 — every module entry's `data` is a content name // stored on this contract; blobs are fetched via getContent(name, 0x). var SCRIPTY_ADDRESS = (typeof window.DOS_SCRIPTY_ADDRESS === "string" && window.DOS_SCRIPTY_ADDRESS) ? window.DOS_SCRIPTY_ADDRESS : "0xbD11994aABB55Da86DC246EBB17C1Be0af5b7699"; var GETCONTENT_SELECTOR = "0x2647c23d"; // getContent(string,bytes) // Module list — order matters. Each `data` is the ScriptyStorageV2 content // name, `gzip` matches how each blob was stored. DEFAULT_SCRIPTS is declared // by DOS_LoadingArray.js, injected by the renderer right before this file. // Priority: window.DOS_SCRIPTS > DEFAULT_SCRIPTS. var SCRIPTS = (window.DOS_SCRIPTS && window.DOS_SCRIPTS.length) ? window.DOS_SCRIPTS : (typeof DEFAULT_SCRIPTS !== "undefined" ? DEFAULT_SCRIPTS : []); // Script manifest for the Provenance window (os.js): on-chain content name + // injected script size in bytes, filled in as each module is injected. window.DOS_SCRIPT_INFO = SCRIPTS.map(function (sc) { return { name: sc.data, label: sc.label, bytes: null }; }); function recordScriptSize(sc, jsCode) { if (!sc || typeof jsCode !== "string") return; for (var i = 0; i < window.DOS_SCRIPT_INFO.length; i++) { if (window.DOS_SCRIPT_INFO[i].name === sc.data && window.DOS_SCRIPT_INFO[i].bytes == null) { window.DOS_SCRIPT_INFO[i].bytes = new TextEncoder().encode(jsCode).length; return; } } } /* ------------------------------------------------------------------- styles */ // Colours hardcoded; only --w / --gap / --tu are viewport-driven variables. var css = ":root{" + "--w:min(320px, 80vw);" + "--gap:clamp(14px, 3.5vh, 40px);" + "--tu:min(var(--w), calc(((100vh - var(--w)) / 2 - var(--gap)) / 1.1));" + "}" + "#dos-loader{position:fixed;inset:0;z-index:100000;background:#0a0b0d;color:#cfd3cf;" + "font-family:'Andale Mono',monospace;display:grid;" + "grid-template-rows:minmax(0,1fr) auto minmax(0,1fr);justify-items:center;" + "opacity:1;transition:opacity .8s ease;pointer-events:auto;user-select:none;overflow:hidden;}" + "#dos-loader.dos-hide{opacity:0;pointer-events:none;}" + /* 1) title + subtitle : centred block, scales down to fit remaining height */ "#dos-head{grid-row:1;align-self:end;margin-bottom:var(--gap);font-weight:normal;" + "text-align:center;box-sizing:border-box;}" + "#dos-head .l{font-family:'MonoPixel','Andale Mono',monospace;line-height:1.25;" + "letter-spacing:.02em;margin:0;white-space:nowrap;}" + "#dos-head .l1{font-size:calc(var(--tu)*0.128*1.3);}" + "#dos-head .l2{font-size:calc(var(--tu)*0.185*1.31);}" + "#dos-head .l3{font-size:calc(var(--tu)*0.278*1.31);}" + "#dos-head .sub{font-size:calc(var(--tu)*0.038*1.02);opacity:.72;letter-spacing:.02em;" + "margin:calc(var(--tu)*0.05) 0 0;white-space:nowrap;}" + /* 2) drawing : page-centred, width matched to the title block by JS */ "#dos-art{grid-row:2;width:var(--w);height:var(--w);display:block;}" + /* 3) status : terminal log (newest first) + % pinned to the first line */ "#dos-status{grid-row:3;align-self:start;margin-top:var(--gap);width:var(--w);" + "max-height:100%;overflow:hidden;display:flex;align-items:flex-start;gap:1em;" + "font-size:calc(var(--w)*0.04);letter-spacing:.02em;line-height:1.5;}" + "#dos-log{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;}" + "#dos-log .line{display:flex;gap:.5em;white-space:nowrap;min-width:0;}" + "#dos-log .line .mk{flex:0 0 auto;width:1em;color:#FFD65C;}" + "#dos-log .line .tx{overflow:hidden;text-overflow:ellipsis;}" + "#dos-log .line.cur{color:#cfd3cf;opacity:1;}" + "#dos-log .line.done{opacity:.45;}" + "#dos-log .line.err .mk{color:#ff6b6b;}" + "#dos-log .line.err{color:#ff6b6b;opacity:1;}" + "#dos-status-pct{flex:0 0 auto;font-family:'MonoPixel','Andale Mono',monospace;" + "color:#FFD65C;font-variant-numeric:tabular-nums;}" + /* SVG classes (scoped to the drawing) */ "#dos-art .cls-1{fill:#000;}" + "#dos-art .cls-2{fill:none;}" + "#dos-art .cls-1,#dos-art .cls-2,#dos-art .cls-3{stroke:#cfd3cf;stroke-miterlimit:10;}" + "#dos-art .cls-2{stroke-width:2px;}" + "#dos-art .cls-4,#dos-art .cls-5{fill:#cfd3cf;font-family:'MonoPixel','Andale Mono',monospace;}" + "#dos-art .cls-4{font-size:36px;}" + "#dos-art .cls-5{font-size:21px;}" + "#dos-art .cls-6{fill:#FFD65C;}"; /* --------------------------------------------------------------- drawing SVG */ // Concentric-orbit "atom" scaled to nearly fill the 512 frame; the central // squares stay on the foreground and the radar (#dos-radar) is filled + driven // by JS. Colours hardcoded (amber accent #FFD65C, light strokes #cfd3cf). function drawingSVG() { return '' + '<svg id="dos-art" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">' + '<g id="Calque_2" transform="translate(256 256) scale(2) translate(-256 -256)">' + // animated dashed outer ring (rotates 40s) '<g>' + '<circle cx="256" cy="256" r="127" fill="none" stroke="#FFD65C" stroke-width="2" stroke-dasharray="6 10">' + '<animate attributeName="stroke-dashoffset" values="0;-32" dur="2s" repeatCount="indefinite"/>' + '</circle>' + '<animateTransform attributeName="transform" type="rotate" from="0 256 256" to="360 256 256" dur="40s" repeatCount="indefinite"/>' + '</g>' + // orbit r=101.23 (rotates -360 / 20s) '<g>' + '<path class="cls-2" d="M256,357.23c-55.82,0-101.23-45.41-101.23-101.23s45.41-101.23,101.23-101.23,101.23,45.41,101.23,101.23-45.41,101.23-101.23,101.23Z"/>' + '<circle class="cls-3" cx="256" cy="154.77" r="6" fill="#000"/>' + '<animateTransform attributeName="transform" type="rotate" from="0 256 256" to="-360 256 256" dur="20s" repeatCount="indefinite"/>' + '</g>' + // orbit r=61.23 (rotates -360 / 12s) '<g class="cls-3">' + '<path class="cls-2" d="M256,317.23c-33.76,0-61.23-27.47-61.23-61.23s27.47-61.23,61.23-61.23,61.23,27.47,61.23,61.23-27.47,61.23-61.23,61.23Z"/>' + '<circle cx="256" cy="194.77" r="6" fill="#000"/>' + '<circle cx="256" cy="317.23" r="6" fill="#000"/>' + '<circle cx="212.71" cy="212.72" r="6" fill="#000"/>' + '<circle cx="212.71" cy="299.29" r="6" fill="#000"/>' + '<circle cx="299.28" cy="212.72" r="6" fill="#000"/>' + '<circle cx="299.28" cy="299.28" r="6" fill="#000"/>' + '<circle cx="194.77" cy="256" r="6" fill="#000"/>' + '<circle cx="317.23" cy="256" r="6" fill="#000"/>' + '<animateTransform attributeName="transform" type="rotate" from="0 256 256" to="-360 256 256" dur="12s" repeatCount="indefinite"/>' + '</g>' + // orbit r=41.23 (rotates 360 / 5s) '<g class="cls-3">' + '<circle class="cls-2" cx="256" cy="256" r="41.23"/>' + '<circle cx="256" cy="214.77" r="6" fill="#000"/>' + '<circle cx="256" cy="297.23" r="6" fill="#000"/>' + '<animateTransform attributeName="transform" type="rotate" from="0 256 256" to="360 256 256" dur="5s" repeatCount="indefinite"/>' + '</g>' + // orbit r=81.23 (rotates 360 / 10s) '<g class="cls-3">' + '<path class="cls-2" d="M256,337.23c-44.79,0-81.23-36.44-81.23-81.23s36.44-81.23,81.23-81.23,81.23,36.44,81.23,81.23-36.44,81.23-81.23,81.23Z"/>' + '<circle cx="256" cy="174.77" r="6" fill="#000"/>' + '<circle cx="218.26" cy="184.09" r="6" fill="#000"/>' + '<circle cx="189.15" cy="209.85" r="6" fill="#000"/>' + '<circle cx="175.38" cy="246.2" r="6" fill="#000"/>' + '<circle cx="180.04" cy="284.79" r="6" fill="#000"/>' + '<circle cx="202.13" cy="316.78" r="6" fill="#000"/>' + '<circle cx="236.57" cy="334.81" r="6" fill="#000"/>' + '<circle cx="293.74" cy="184.09" r="6" fill="#000"/>' + '<circle cx="322.85" cy="209.85" r="6" fill="#000"/>' + '<circle cx="336.62" cy="246.2" r="6" fill="#000"/>' + '<circle cx="331.96" cy="284.79" r="6" fill="#000"/>' + '<circle cx="309.87" cy="316.78" r="6" fill="#000"/>' + '<circle cx="275.43" cy="334.81" r="6" fill="#000"/>' + '<animateTransform attributeName="transform" type="rotate" from="0 256 256" to="360 256 256" dur="10s" repeatCount="indefinite"/>' + '</g>' + // central squares (foreground) '<rect class="cls-1" x="236" y="236" width="15" height="15"/>' + '<rect class="cls-1" x="261" y="236" width="15" height="15"/>' + '<rect class="cls-1" x="261" y="261" width="15" height="15"/>' + '<rect class="cls-1" x="236" y="261" width="15" height="15"/>' + // rotating radar (populated + driven by JS; progress == its rotation) '<g id="dos-radar"></g>' + // centre marker on top of the radar '<rect id="dos-sndmark" fill="black" stroke="#FFD65C" x="253.5" y="253.5" width="5" height="5"/>' + '</g>' + '</svg>'; } /* ------------------------------------------------------------------- build UI */ var el = {}; function build() { var style = document.createElement("style"); style.id = "dos-loader-style"; style.textContent = css; document.head.appendChild(style); var root = document.createElement("div"); root.id = "dos-loader"; root.innerHTML = '<div id="dos-head">' + '<p class="l l1">DECENTRALIZED</p>' + '<p class="l l2">OPERATING</p>' + '<p class="l l3">SYSTEM</p>' + '<p class="sub">A fully onchain world for all the cypherdudes</p>' + '</div>' + drawingSVG() + '<div id="dos-status"><div id="dos-log"></div><span id="dos-status-pct">0%</span></div>'; document.body.appendChild(root); el.root = root; el.head = root.querySelector("#dos-head"); el.art = root.querySelector("#dos-art"); el.stat = root.querySelector("#dos-status"); el.radar = root.querySelector("#dos-radar"); el.pct = root.querySelector("#dos-status-pct"); el.log = root.querySelector("#dos-log"); buildRadar(); // Sound pre-arm: clicking the drawing toggles the ambient-audio intent // (window.DOS_SOUND_ON). The click is the user gesture the AudioContext // needs, so the shared context is created/resumed HERE and reused by the // world (COS_main.js initCtx); init() then starts the drone and the HUD // sound button shows ON. The centre marker fills amber while armed. el.art.style.cursor = "pointer"; el.art.setAttribute("title", "click to toggle sound"); el.art.addEventListener("click", function () { window.DOS_SOUND_ON = !window.DOS_SOUND_ON; try { var AC = window.AudioContext || window.webkitAudioContext; if (!window.__DOS_AUDIO_CTX && AC) window.__DOS_AUDIO_CTX = new AC({ sampleRate: 44100 }); if (window.__DOS_AUDIO_CTX && window.__DOS_AUDIO_CTX.state === "suspended") window.__DOS_AUDIO_CTX.resume(); } catch (e) { } // engine already injected -> apply immediately (starts/stops the drone) if (typeof window.setSoundState === "function") window.setSoundState(window.DOS_SOUND_ON); var mark = el.art.querySelector("#dos-sndmark"); if (mark) mark.setAttribute("fill", window.DOS_SOUND_ON ? "#FFD65C" : "black"); }); } /* --------------------------------------------------------------- radar sweep */ // A leading line from the centre + a fading trail of wedges (built once). function buildRadar() { if (!el.radar) return; var ACCENT = "#FFD65C", CX = 256, CY = 256, RIN = 0, ROUT = 128, SEG = 10, TRAIL = 58; function pt(r, deg) { var a = deg * Math.PI / 180; return [CX + r * Math.sin(a), CY - r * Math.cos(a)]; } function f(n) { return n.toFixed(2); } var buf = ""; for (var k = 0; k < SEG; k++) { var a0 = -(k + 1) * (TRAIL / SEG), a1 = -k * (TRAIL / SEG); var P0i = pt(RIN, a0), P0o = pt(ROUT, a0), P1o = pt(ROUT, a1), P1i = pt(RIN, a1); var op = (0.30 * (1 - k / SEG)).toFixed(3); buf += '<path d="M' + f(P0i[0]) + ',' + f(P0i[1]) + ' L' + f(P0o[0]) + ',' + f(P0o[1]) + ' L' + f(P1o[0]) + ',' + f(P1o[1]) + ' L' + f(P1i[0]) + ',' + f(P1i[1]) + ' Z" fill="' + ACCENT + '" fill-opacity="' + op + '"/>'; } var Lo = pt(ROUT, 0), Li = pt(RIN, 0); buf += '<line x1="' + f(Li[0]) + '" y1="' + f(Li[1]) + '" x2="' + f(Lo[0]) + '" y2="' + f(Lo[1]) + '" stroke="' + ACCENT + '" stroke-width="0.5" stroke-opacity="0.95"/>'; el.radar.innerHTML = buf; } /* ----------------------------------------------------------------- progress */ // Progress drives the radar rotation (one turn == 100%) and the %. The shown // value eases toward the target so discrete script loads sweep smoothly. var shown = 0, target = 0, raf = 0; function applyProgress(t) { if (el.radar) el.radar.setAttribute("transform", "rotate(" + (t * 360).toFixed(2) + " 256 256)"); if (el.pct) el.pct.textContent = Math.round(t * 100) + "%"; } function tick() { var d = target - shown; if (Math.abs(d) < 0.001) { shown = target; applyProgress(shown); raf = 0; return; } shown += d * 0.1; applyProgress(shown); raf = requestAnimationFrame(tick); } function setProgress(t) { target = Math.max(0, Math.min(1, t)); if (!raf) raf = requestAnimationFrame(tick); } /* ------------------------------------------------------------ terminal log */ var curLine = null; function markDone(line) { if (!line) return; if (!line.classList.contains("err")) line.className = "line done"; var mk = line.querySelector(".mk"); if (mk && !line.classList.contains("err")) mk.textContent = "✓"; // ✓ } function addLine(text) { if (!el.log) return; markDone(curLine); var d = document.createElement("div"); d.className = "line cur"; d.innerHTML = '<span class="mk">▸</span><span class="tx"></span>'; // ▸ d.querySelector(".tx").textContent = text; el.log.insertBefore(d, el.log.firstChild); // newest on the first line curLine = d; } function markError() { if (!curLine) return; curLine.className = "line err"; var mk = curLine.querySelector(".mk"); if (mk) mk.textContent = "✗"; // ✗ } /* --------------------------------------------------------------- responsive */ // Title scales to the band above the centred drawing; the drawing (kept // square) and the log are then matched to the title block's measured width. function layout() { if (!el.head || !el.root) return; var vh = window.innerHeight, vw = window.innerWidth; var gap = Math.max(14, Math.min(0.035 * vh, 40)); // mirrors --gap // measure title width/height per 1px of --tu at a reference size var REF = 200; el.root.style.setProperty("--tu", REF + "px"); var r = el.head.getBoundingClientRect(); var wPer = r.width / REF, hPer = r.height / REF; if (!wPer || !hPer) return; // drawing side = title width; keep centred => title fits the band above it: // hPer*tu <= (vh - wPer*tu)/2 - gap var tuFit = (vh - 2 * gap) / (2 * hPer + wPer); var tu = Math.max(24, Math.min(320, 0.92 * vw / wPer, tuFit)); el.root.style.setProperty("--tu", tu + "px"); var w = el.head.getBoundingClientRect().width; el.art.style.width = w + "px"; el.art.style.height = w + "px"; el.stat.style.width = w + "px"; } /* ---------------------------------------------------- scripty load + inject */ // Some historical on-chain blobs (notably the stored CypherDudesLayer1) still // carry a leftover `const fonts = `…`;` that collides with the canonical one in // CypherDudesFonts → "Identifier 'fonts' has already been declared". For the // modules listed here we strip that single declaration from the fetched text // before injecting, so CypherDudesFonts' `fonts` stays the only one. var STRIP_FONTS_FROM = { CypherDudesLayer1: 1 }; // Removes the first `const|let|var fonts = <string-literal>;` from `src`. // Handles backtick / single / double quoted literals (with escapes); if the // RHS isn't a plain string literal it leaves the source untouched. function stripFontsDecl(src) { var re = /\b(?:const|let|var)\s+fonts\s*=\s*/g; var m = re.exec(src); if (!m) return src; var start = m.index; var i = re.lastIndex; var q = src.charAt(i); if (q !== "`" && q !== '"' && q !== "'") return src; // not a simple literal — leave it i++; // past the opening delimiter for (; i < src.length; i++) { var ch = src.charAt(i); if (ch === "\\") { i++; continue; } // skip escaped char if (ch === q) { i++; break; } // closing delimiter } while (i < src.length && /\s/.test(src.charAt(i))) i++; // trailing whitespace if (src.charAt(i) === ";") i++; // optional semicolon return src.slice(0, start) + src.slice(i); } // (mirrors /Loader/loader.js) async function loadHexScript(hex, isGzipped, sc) { if (hex.startsWith("0x")) hex = hex.slice(2); var bytes = new Uint8Array(hex.length / 2); for (var i = 0; i < bytes.length; i++) bytes[i] = parseInt(hex.substr(i * 2, 2), 16); var jsCode; if (isGzipped) { // Two storage conventions supported: // • RAW gzip bytes (store-artifacts.mjs / deploySepolia.js) — detected // by the gzip magic number 0x1f 0x8b; // • legacy base64-of-gzip text — decoded via atob first. var gzipBytes; if (bytes.length > 2 && bytes[0] === 0x1f && bytes[1] === 0x8b) { gzipBytes = bytes; } else { var base64 = ""; for (var j = 0; j < bytes.length; j++) base64 += String.fromCharCode(bytes[j]); gzipBytes = Uint8Array.from(atob(base64), function (c) { return c.charCodeAt(0); }); } var stream = new Response(gzipBytes).body.pipeThrough(new DecompressionStream("gzip")); jsCode = await new Response(stream).text(); } else { jsCode = new TextDecoder("utf-8").decode(bytes); } if (sc && STRIP_FONTS_FROM[sc.data]) jsCode = stripFontsDecl(jsCode); recordScriptSize(sc, jsCode); var script = document.createElement("script"); script.textContent = jsCode; document.head.appendChild(script); // inline scripts run synchronously on append } async function injectOne(txt, sc) { txt = txt.trim(); // Heuristic: a hex blob is only [0-9a-fA-Fx]. If the fetched file is raw JS // (e.g. a plain .js during local testing), inject it verbatim. var isHex = /^(0x)?[0-9a-fA-F]+$/.test(txt); if (isHex) { await loadHexScript(txt, sc.gzip, sc); } else { if (sc && STRIP_FONTS_FROM[sc.data]) txt = stripFontsDecl(txt); recordScriptSize(sc, txt); var script = document.createElement("script"); script.textContent = txt; document.head.appendChild(script); } } /* ------------------------------------------- shared RPC layer (GLOBAL) */ // The loader is the FIRST script on the page, so the RPC plumbing lives here // and is exposed on window: every script injected afterwards // (COS_ethCalls.js, COS_main.js, recompose.min.js, ...) reuses this ONE // pool instead of rebuilding its own — shared pacing + cooldown state. // // The onchain HTML build (DOSRenderer) injects the endpoint lists as strings // BEFORE this file runs: // RPCString / WSRPCString — getRpcs()/getWsRpcs() output: "['url','url',...]" // ClaimOrderString — claimOrder storage string: "[0,1153,...]" window.parseRpcString = function (s) { if (!s || typeof s !== "string") return null; try { var a = JSON.parse(s.replace(/'/g, '"')); return Array.isArray(a) ? a : null; } catch (e) { console.warn("parseRpcString: invalid input", e); return null; } }; window.parseClaimOrderString = function (s) { if (!s || typeof s !== "string") return null; try { var a = JSON.parse(s); return Array.isArray(a) ? a.map(Number) : null; } catch (e) { console.warn("parseClaimOrderString: invalid input", e); return null; } }; // Runtime endpoint arrays (contract-fed; window.RPCs may be pre-set for dev). window.RPCs = (typeof RPCString !== "undefined" && window.parseRpcString(RPCString)) || window.RPCs || []; window.WS_RPCs = (typeof WSRPCString !== "undefined" && window.parseRpcString(WSRPCString)) || window.WS_RPCs || []; // --- Parallel RPC pool (same principle as recompose.min.js) -------------- // Every non-empty https RPC becomes an endpoint with its own request pacing // (100ms between calls ≈ 10 req/s) and a jittered exponential cooldown when // it fails / rate-limits. Work is consumed by one worker per endpoint, each // call going to whichever endpoint frees up first — so N configured RPCs // load the world roughly N times faster without hammering any provider. window.rpcPool = { eps: null, build: function () { this.eps = window.RPCs.filter(function (u) { return u; }) .map(function (url) { return { url: url, nextFree: 0, cooldownUntil: 0 }; }); return this.eps; }, endpoints: function () { return (this.eps && this.eps.length) ? this.eps : this.build(); }, pick: function () { var eps = this.endpoints(); var now = Date.now(); var best = eps[0], t = Math.max(now, best.cooldownUntil, best.nextFree); for (var i = 1; i < eps.length; i++) { var ti = Math.max(now, eps[i].cooldownUntil, eps[i].nextFree); if (ti < t) { t = ti; best = eps[i]; } } best.nextFree = t + 100; return { ep: best, waitMs: t - now }; }, cooldown: function (ep, attempt) {