← Files ModRetro Chromatic PluginARCHIVED FILE
dist/web-preview-input.js
7.34 KB · Oct 2, 2026 · 00:37 UTC
import { randomUUID } from "node:crypto";
import { INPUT_LEASE_MS, inputButtons, inputDuration, inputGeneration, parseInputReceipt } from "./web-annotations/input-protocol.js";
export class PreviewInputError extends Error {
code;
commandId;
constructor(code, message, commandId) {
super(message);
this.code = code;
this.commandId = commandId;
this.name = "PreviewInputError";
}
}
/** A single non-replayable input operation for one live preview, across all its views. */
export class PreviewInputBroker {
operation;
accepting = true;
last;
get blocking() { return !!this.operation; }
status() {
const op = this.operation;
return { accepting: this.accepting, ...(op ? { active: { commandId: op.command.id, viewId: op.viewId, generation: op.command.generation,
status: op.uncertain ? "release_unknown" : op.cancelled ? "cancelling" : op.delivered ? "delivered" : "pending" } } : {}), ...(this.last ? { last: structuredClone(this.last) } : {}) };
}
request(viewId, readiness, generation, buttons, duration, signal) {
if (!this.accepting || this.operation)
throw new PreviewInputError("INPUT_BUSY", "The browser has an input command or unresolved release. Inspect its status first.");
inputGeneration(generation);
if (!readiness?.ready || readiness.generation !== generation)
throw new PreviewInputError("INPUT_NOT_READY", "The selected browser input state changed or is not ready. Read status again.");
if (signal?.aborted)
throw new PreviewInputError("INPUT_CANCELLED", "Input was cancelled before admission.");
const command = { id: randomUUID(), generation, buttons: inputButtons(buttons), durationMs: inputDuration(duration), deliverBy: Date.now() + 2_000, leaseUntil: 0 };
return new Promise((resolve, reject) => {
const abort = () => this.cancel("The caller cancelled the browser input command.");
const op = { viewId, command, delivered: false, cancelled: false, uncertain: false, resolve, reject,
detach: () => signal?.removeEventListener("abort", abort), drains: [] };
this.operation = op;
signal?.addEventListener("abort", abort, { once: true });
op.timer = setTimeout(() => this.expire(op), 2_000);
});
}
poll(viewId, readiness, activeId, rawReceipt) {
if (activeId !== undefined && (typeof activeId !== "string" || !/^[a-f0-9-]{36}$/i.test(activeId)))
throw new PreviewInputError("INPUT_INVALID", "Invalid active input ID.");
if (rawReceipt !== undefined) {
const receipt = parseInputReceipt(rawReceipt);
const op = this.operation;
if (this.last?.commandId === receipt.id && this.last.viewId === viewId && this.last.receipt && JSON.stringify(this.last.receipt) === JSON.stringify(receipt)) {
return {}; // Identical receipt retry never replays the command.
}
if (!op || !op.delivered || op.viewId !== viewId || op.command.id !== receipt.id || op.command.generation !== receipt.generation || op.command.durationMs !== receipt.requestedDurationMs) {
throw new PreviewInputError("INPUT_RESULT_MISMATCH", "The input result does not match a delivered command.");
}
this.last = { commandId: receipt.id, viewId, status: receipt.mcpButtonsReleased ? "complete" : "release_unknown", receipt };
if (op.timer)
clearTimeout(op.timer);
op.detach();
if (!receipt.mcpButtonsReleased) {
op.uncertain = op.cancelled = true;
const error = new PreviewInputError("INPUT_RELEASE_UNKNOWN", "The browser could not confirm input release. No command will be replayed.", receipt.id);
op.reject(error);
op.drains.splice(0).forEach(wait => wait.reject(error));
return { cancelInput: receipt.id };
}
this.operation = undefined;
op.resolve(receipt);
op.drains.splice(0).forEach(wait => wait.resolve());
return {};
}
const op = this.operation;
if (!op || op.viewId !== viewId) {
return activeId ? { cancelInput: activeId } : {};
}
if (op.cancelled || readiness && readiness.generation !== op.command.generation) {
if (!op.cancelled)
this.cancel("The browser lifecycle changed before input completed.");
return { cancelInput: op.command.id };
}
if (!op.delivered) {
if (Date.now() >= op.command.deliverBy || !readiness?.ready) {
this.expire(op);
return {};
}
op.delivered = true;
if (op.timer)
clearTimeout(op.timer);
op.timer = setTimeout(() => this.expire(op), op.command.durationMs + 1_000);
op.command.leaseUntil = Date.now() + INPUT_LEASE_MS;
return { inputCommand: structuredClone(op.command) };
}
if (activeId === op.command.id)
return { inputLease: { id: op.command.id, leaseUntil: Date.now() + INPUT_LEASE_MS } };
return {}; // A delivered command is never sent twice.
}
cancel(message) {
const op = this.operation;
if (!op)
return;
op.cancelled = true;
if (!op.delivered) {
this.last = { commandId: op.command.id, viewId: op.viewId, status: "not_delivered" };
this.operation = undefined;
if (op.timer)
clearTimeout(op.timer);
op.detach();
op.reject(new PreviewInputError("INPUT_NOT_DELIVERED", message, op.command.id));
op.drains.splice(0).forEach(wait => wait.resolve());
}
}
expire(op) {
if (this.operation !== op)
return;
if (op.timer)
clearTimeout(op.timer);
op.timer = undefined;
if (!op.delivered) {
this.cancel("The browser did not pick up input before its deadline. Nothing was delivered.");
return;
}
op.cancelled = op.uncertain = true;
this.last = { commandId: op.command.id, viewId: op.viewId, status: "release_unknown" };
op.detach();
const error = new PreviewInputError("INPUT_RELEASE_UNKNOWN", "Input was delivered but its release was not confirmed. Inspect status; do not replay it.", op.command.id);
op.reject(error);
op.drains.splice(0).forEach(wait => wait.reject(error));
}
connectionLost() {
this.accepting = false;
const op = this.operation;
if (op)
this.expire(op);
}
async drain() {
this.accepting = false;
this.cancel("The browser preview is closing or changing.");
const op = this.operation;
if (!op)
return;
if (op.uncertain)
throw new PreviewInputError("INPUT_RELEASE_UNKNOWN", "The original browser has unresolved input release; its owner is retained.", op.command.id);
await new Promise((resolve, reject) => {
const timer = setTimeout(() => this.expire(op), 1_500);
op.drains.push({ resolve: () => { clearTimeout(timer); resolve(); }, reject: error => { clearTimeout(timer); reject(error); } });
});
}
}
//# sourceMappingURL=web-preview-input.js.mapSHA-256: aff340b32809ec640300f80f5767630666c76218612dbace1bf8e66c4adfc2e7