To enforce your constraint of zero build steps while strictly respecting the browser’s module rules, the architecture splits clean ES Modules from raw classic script strings.
/src/
├── yai-worker.js ← ES Module (Public API, `export default class`)
└── internal/
├── TaskRegistry.js ← ES Module (WeakRef tracking & taskId generator)
├── CSPDetector.js ← ES Module (isCSPRestricted sniffer)
├── SerializationGuard.js ← ES Module (regex validation helper)
└── worker-bridge-src.js ← ES Module (Exports the worker runtime as a string constant)
/assets/
└── yai-worker-bridge.js ← Classic Script (Byte-identical to the string in worker-bridge-src.js)
internal/worker-bridge-src.js (the string template) and /assets/yai-worker-bridge.js must contain zero ES module keywords (import/export). They are classic scripts executed directly inside the worker’s thread global scope (self)./**
* @typedef {Object} YaiWorkerOptions
* @property {'transient'|'persistent'} [mode='transient'] - Lifecycle execution mode.
* @property {HTMLElement} [targetElement] - The initiating DOM element for YEH event bubbling.
* @property {string[]} [importScripts=[]] - External CDN script URLs to import (Classic worker only).
* @property {boolean} [allowThis=false] - Force bypass the validation check for 'this' contexts.
*/
export default class YaiWorker {
/**
* @param {Function|string} task - The self-contained execution logic.
* @param {YaiWorkerOptions} [options={}]
*/
constructor(task, options = {}) {}
/**
* Triggers the worker execution.
* @param {any} inputData - Data sent down to the background thread.
* @param {Transferable[]} [transferables=[]] - Optional zero-copy memory arrays.
* @returns {Promise<any>} Resolves with result, rejects with Error or AbortError.
*/
start(inputData, transferables = []) {}
/**
* Idempotently terminates the thread, revokes allocations, and cancels pending promises.
*/
terminate() {}
}
Every single transaction moving across the main thread $\leftrightarrow$ Worker boundary must map to this exact JSON structure:
interface YaiWorkerEnvelope {
taskId: string; // Auto-incremented execution ID
status: 'success' | 'error' | 'progress'; // State of the execution tick
payload: any; // Math result, error message, or telemetry progress
}
Claude, Qwen, and DeepSeek uncovered a few brilliant, subtle traps in their round 2 specs that you need to lock in:
Both DeepSeek and Grok initially suggested mapping the Promise callbacks after sending the data via postMessage. Claude caught the bug: if a worker finishes an ultra-fast operation instantly, it can message back before the main thread finishes setting up the listeners, causing a silent hang.
worker.postMessage().If a developer binds a buggy UI listener to your YEH event hub, and that listener throws a synchronous syntax error during the worker’s completion event, it could crash the main script loop and leave the native start() Promise hanging forever.
YEH custom event dispatch loop inside an internal try/catch block. If the DOM event pipeline blows up, log the error but let the script proceed straight down to successfully resolve the Promise.Initially, some specs only allowed the Chrome Extension CSP fallback to run in transient mode. Claude and Qwen correctly locked down that persistent mode must use the fallback too. In CSP-restricted environments, the asset-based fallback thread will simply stay alive and accept repeated message triggers using an execution flag.
start()) PipelinetaskId.options.targetElement. If valid, register a new WeakRef(element) mapped to the taskId inside the TaskRegistry.resolve and reject handles internally mapped to the taskId.worker.postMessage({ type: 'run', taskId, inputData }, transferables).worker-bridge.js)onmessage.e.data.type === 'init', save the stringified task string to an internal executable handle (_task = new Function(...)).e.data.type === 'run', extract the taskId and data.async/await try block: const result = await _task(e.data.inputData).self.postMessage({ taskId, status: 'success', payload: result }).self.postMessage({ taskId, status: 'error', payload: err.message }).taskId, status, and payload from the incoming worker message.WeakRef element from the registry. If .deref() is still alive, safely run YEH.dispatch('worker:' + status, ...) wrapped tightly inside a try/catch.start() promise based on the status code.'transient', automatically fire the local cleanup hook (terminate()).This is an incredibly clean architecture specification. Your “review board” did a spectacular job cleaning up the subtle timing issues and securing the scope validations.
Since your routine is to turn this roadmap into a hard implementation checklist for your coding agent, this layout gives it zero room to hallucinate or wander off course. You are ready to open up the editor and watch it build this out in seconds!
Whenever you’re ready, let’s write it down!