// THE GAP BETWEEN TWO ONE-OFF IMPORTS ON ONE PLATFORM. A batch download, a // sync, a persist and the auto-download lane each wait their platform's floor // between two videos — but every import is its own job, so ten imports queued // on `platform:odysee` used to ask Odysee as fast as each job could finish // (Odysee answered 429 at 10–50 s apart, 2026-10-06). An import on a platform // with an import floor (platformImportMinGapSeconds, ytdlp/platformArgs.mjs) // waits here first, and sets the platform's next start when it settles. // // Process-wide (on globalThis, as the job registry is: a route bundle and the // job runner must share one view) and in memory: a restart is itself a gap. declare global { // eslint-disable-next-line no-var var __yttPlatformGap__: Map | undefined; } function nextStartAt(): Map { if (!globalThis.__yttPlatformGap__) globalThis.__yttPlatformGap__ = new Map(); return globalThis.__yttPlatformGap__; } // Milliseconds before `platform` may be asked again (0 = now). export function platformGapRemainingMs(platform: string, now: number = Date.now()): number { const at = nextStartAt().get(platform); return at !== undefined && at > now ? at - now : 0; } // A unit on `platform` settled at `now`: the next one starts no sooner than // `gapMs` later. A later start already set is kept (never shortened). export function notePlatformGap(platform: string, gapMs: number, now: number = Date.now()): void { if (gapMs <= 0) return; const at = now + gapMs; const map = nextStartAt(); if ((map.get(platform) ?? 0) < at) map.set(platform, at); } export function clearPlatformGaps(): void { nextStartAt().clear(); } // Wait out `remainingMs()` — re-read after each wait, since another import or // a 429 may have moved it — logging once per wait. Rejects with an AbortError // when `signal` aborts. export async function waitForPlatformGap(opts: { label: string; remainingMs: () => Promise | number; signal?: AbortSignal; onLog?: (line: string) => void; sleep?: (ms: number, signal?: AbortSignal) => Promise; }): Promise { const sleep = opts.sleep ?? abortableSleep; let waited = 0; for (;;) { if (opts.signal?.aborted) throw abortError(); const ms = await opts.remainingMs(); if (ms <= 0) return waited; opts.onLog?.(`${opts.label} asks for a gap between videos: waiting ${Math.ceil(ms / 1000)}s.`); await sleep(ms, opts.signal); waited += ms; } } function abortError(): Error { const err = new Error("aborted while waiting for the platform gap"); err.name = "AbortError"; return err; } function abortableSleep(ms: number, signal?: AbortSignal): Promise { return new Promise((resolve, reject) => { if (signal?.aborted) return reject(abortError()); const t = setTimeout(() => { signal?.removeEventListener("abort", onAbort); resolve(); }, ms); const onAbort = () => { clearTimeout(t); reject(abortError()); }; signal?.addEventListener("abort", onAbort, { once: true }); }); }