Files
hockey_new/hockey_data/static/mapping-requests.js

116 lines
5.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(() => {
"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 };
})();