import type { PreviewViewportSetting } from "@t3tools/contracts"; type BrowserViewportHandler = (setting: PreviewViewportSetting) => Promise; export const BROWSER_VIEWPORT_COMMIT_TIMEOUT_MS = 15_000; export class BrowserViewportCommitTimeoutError extends Error { override readonly name = "BrowserViewportCommitTimeoutError"; constructor(readonly tabId: string) { super(`Timed out committing the browser viewport for tab ${tabId}`); } } const handlers = new Map(); const commitTails = new Map>(); const runHandlerWithTimeout = (tabId: string, operation: Promise): Promise => { let timeoutId: ReturnType | undefined; const timeout = new Promise((_resolve, reject) => { timeoutId = setTimeout( () => reject(new BrowserViewportCommitTimeoutError(tabId)), BROWSER_VIEWPORT_COMMIT_TIMEOUT_MS, ); }); return Promise.race([operation, timeout]).finally(() => { if (timeoutId !== undefined) clearTimeout(timeoutId); }); }; export function subscribeBrowserViewportChange( tabId: string, handler: BrowserViewportHandler, ): () => void { handlers.set(tabId, handler); return () => { if (handlers.get(tabId) === handler) handlers.delete(tabId); }; } export function commitBrowserViewportChange( tabId: string, setting: PreviewViewportSetting, ): Promise { const previous = commitTails.get(tabId) ?? Promise.resolve(); const started = previous .catch(() => undefined) .then(() => { const handler = handlers.get(tabId); const operation = handler ? Promise.resolve().then(() => handler(setting)) : Promise.reject(new Error(`No visible browser viewport handler for tab ${tabId}`)); return { operation }; }); // The queue follows the real handler lifetime, not the caller-facing timeout. // A slow commit therefore cannot time out, release the queue, and overwrite a // newer viewport after that newer request has already completed. const execution = started.then(({ operation }) => operation); const result = started.then(({ operation }) => runHandlerWithTimeout(tabId, operation)); commitTails.set(tabId, execution); const clear = () => { if (commitTails.get(tabId) === execution) commitTails.delete(tabId); }; void execution.then(clear, clear); return result; }