// Bridges a real, host-rendered to a plugin's hostCall // without ever pushing raw file bytes across the sandbox/postMessage // boundary. A plugin-rendered still becomes a real DOM // node in the host's own document (see pluginRendering.js's normal // VirtualEl -> DOM path) — so the *host* already has direct access to the // picked File the moment its change event fires. This store lets the host // hand the plugin only an opaque, single-use token standing in for that // File; a subsequent hostCall (e.g. registerCustomImage) redeems the token // for the actual bytes, read host-side. const MAX_STAGED_PER_PLUGIN = 5; const STAGE_TTL_MS = 5 * 60 * 1000; export class PluginFileStaging { constructor(now = () => Date.now()) { this._now = now; this._byPlugin = new Map(); // pluginId -> Map this._tokenCounter = 0; } stage(pluginId, file) { this._evictExpired(pluginId); let staged = this._byPlugin.get(pluginId); if (!staged) { staged = new Map(); this._byPlugin.set(pluginId, staged); } if (staged.size >= MAX_STAGED_PER_PLUGIN) { // Evict the oldest entry (Maps iterate in insertion order) to make // room, rather than growing unbounded for a plugin that stages files // faster than it redeems them. const oldestToken = staged.keys().next().value; staged.delete(oldestToken); } const token = `${pluginId}:${this._tokenCounter++}`; staged.set(token, { file, expiresAt: this._now() + STAGE_TTL_MS }); return token; } // Consumes the token on read — a token is only ever redeemable once. take(pluginId, token) { const staged = this._byPlugin.get(pluginId); if (!staged) return null; const entry = staged.get(token); if (!entry) return null; staged.delete(token); if (entry.expiresAt < this._now()) return null; return entry.file; } _evictExpired(pluginId) { const staged = this._byPlugin.get(pluginId); if (!staged) return; const now = this._now(); for (const [token, entry] of staged) { if (entry.expiresAt < now) staged.delete(token); } } }