(() => { "use strict"; if (window.HockeyMappingRequests) return; const lanes = new Map(); const superseded = () => ({ ok: false, reason: "mapping_superseded", superseded: true }); const cancelled = () => new DOMException("Mapping request cancelled", "AbortError"); function current(consumer) { return !consumer.done && !consumer.signal?.aborted && consumer.isCurrent(); } function finish(consumer, error, result) { if (consumer.done) return; const valid = current(consumer); consumer.done = true; consumer.signal?.removeEventListener("abort", consumer.onAbort); if (!valid) consumer.resolve(superseded()); else if (error) consumer.reject(error); else consumer.resolve(result); } async function send(job) { let response; let payload; try { // A full Mapping may contain hundreds of fields. A browser deadline does // not stop the server/Agent and can turn the next click into a second push. response = await fetch(job.path, { method: "POST", cache: "no-store", credentials: "same-origin" }); payload = await response.json(); } catch (cause) { const error = new Error("Результат применения Mapping не подтверждён: связь с сервером прервалась. Проверьте Agent перед повтором."); error.cause = cause; error.code = "mapping_outcome_unknown"; throw error; } if (!response.ok) { if (response.status === 401) window.location?.assign?.("/"); throw new Error(typeof payload?.detail === "string" ? payload.detail : payload?.detail?.message || `HTTP ${response.status}`); } if (!payload || typeof payload.ok !== "boolean") { const error = new Error("Результат применения Mapping не подтверждён: некорректный ответ сервера. Проверьте Agent перед повтором."); error.code = "mapping_outcome_unknown"; throw error; } return payload; } async function run(key, lane, job) { lane.running = job; try { // Queued work can become obsolete while another match/language is sent. if (!job.consumers.some(current)) { job.consumers.forEach((consumer) => finish(consumer, null, superseded())); return; } const result = await send(job); job.consumers.forEach((consumer) => finish(consumer, null, result)); } catch (error) { job.consumers.forEach((consumer) => finish(consumer, error)); } finally { lane.running = null; const next = lane.queued; lane.queued = null; if (next) void run(key, lane, next); else lanes.delete(key); } } function apply({ deviceId = "", scope = "device", language = "ru", context = "", isCurrent = () => true, signal, forceFresh = false } = {}) { const device = String(deviceId || "").trim(); const key = scope === "all" ? "all" : device ? `device:${device}` : "active"; const path = scope === "all" ? "/api/hockey/admin/vmix-mapping/apply-all-active" : device ? `/api/hockey/agents/devices/${encodeURIComponent(device)}/apply-mapping` : "/api/hockey/agents/apply-active-mapping"; const signature = JSON.stringify([path, language === "en" ? "en" : "ru", String(context)]); if (signal?.aborted) return Promise.reject(cancelled()); if (!isCurrent()) return Promise.resolve(superseded()); const lane = lanes.get(key) || { running: null, queued: null }; lanes.set(key, lane); // A queued intent may already have changed server-side preferences/context. // Returning to the running signature therefore needs a fresh resolver read; // only uninterrupted duplicate intents can share the running HTTP request. let job = lane.queued?.signature === signature ? lane.queued : !forceFresh && !lane.queued && lane.running?.signature === signature ? lane.running : null; if (forceFresh && lane.running) { // A completed preference write invalidates the old resolver snapshot, // even if intermediate intents never reached this request coordinator. lane.running.consumers.forEach((consumer) => finish(consumer, null, superseded())); } if (!job) { job = { path, signature, consumers: [] }; if (lane.queued) lane.queued.consumers.forEach((consumer) => finish(consumer, null, superseded())); lane.queued = job; } const promise = new Promise((resolve, reject) => { const consumer = { resolve, reject, isCurrent, signal, done: false, onAbort: null }; consumer.onAbort = () => { if (consumer.done) return; consumer.done = true; signal.removeEventListener("abort", consumer.onAbort); reject(cancelled()); }; signal?.addEventListener("abort", consumer.onAbort, { once: true }); job.consumers.push(consumer); }); if (!lane.running) { lane.queued = null; void run(key, lane, job); } return promise; } window.HockeyMappingRequests = { apply }; })();