import { Array, ArrayPrototypePush as push, Error, ErrorGetStack, Map, MapPrototypeDelete as mapDelete, MapPrototypeGet as mapGet, MapPrototypeSet as mapSet, NumberIsFinite as isFinite, ObjectCreate, ObjectDefineProperty as define, ObjectSetPrototypeOf, ReflectApply as apply, ReflectConstruct as construct, TypeError, uncurryThis, WeakMap, WeakMapPrototypeGet as get, WeakMapPrototypeSet as set, } from "ext:core/primordials"; import { array, dictionary, required, state, string, usv, } from "ext:web/idl.js"; import { op_timer_now as now } from "ext:core/ops"; import { enqueueMicrotask } from "ext:core/microtasks"; import { isPromise } from "ext:core/event_hooks"; import { scheduleTimeout } from "ext:timers/timers.js"; import { registerSerializable, registerUncloneable, registerWire, } from "./serialization.js"; interface EventState { type: string; bubbles: boolean; cancelable: boolean; composed: boolean; target: EventTarget | null; phase: number; dispatching: boolean; stopped: boolean; immediate: boolean; prevented: boolean; passive: boolean; trusted: boolean; time: number; } interface ExceptionState { message: string; name: string; } interface Listener { type: string; callback: EventListener; capture: boolean; once: boolean; passive: boolean; active: boolean; } export type EventListener = ((this: EventTarget, event: Event) => unknown) | { handleEvent(event: Event): unknown; }; interface SignalState { aborted: boolean; reason: unknown; algorithms: (() => void)[]; handler: ((event: Event) => unknown) | null; handlerListener?: ((event: Event) => void) | null; } const events = new WeakMap(), customEvents = new WeakMap(), errorEvents = new WeakMap< object, { message: string; filename: string; lineno: number; colno: number; error: unknown; } >(), rejectionEvents = new WeakMap< object, { promise: Promise; reason: unknown } >(), targets = new WeakMap(), listenerObservers = new WeakMap void>(), signals = new WeakMap(), controllers = new WeakMap(), exceptions = new WeakMap(); registerUncloneable(events); registerUncloneable(customEvents); registerUncloneable(errorEvents); registerUncloneable(rejectionEvents); registerUncloneable(targets); registerUncloneable(signals); registerUncloneable(controllers); const secret = ObjectCreate(null), trunc = Math.trunc, floor = Math.floor; function trustedGetter(this: object): boolean { return state(events, this).trusted; } const WeakRef = globalThis.WeakRef, deref = uncurryThis(WeakRef.prototype.deref); const slice: (array: readonly T[], start?: number, end?: number) => T[] = uncurryThis(Array.prototype.slice); let globalTarget: object | undefined, reporting = false; const pendingReports = new Map(); function report(error: unknown): void { reportErrorValue(error); } class DOMException extends Error { // @ts-expect-error Reflect.construct preserves the native Error prototype without a guest-replaceable super(). constructor(message: unknown = "", name: unknown = "Error") { const convertedMessage = string(message), convertedName = string(name); const self = construct(Error, [convertedMessage], new.target) as { message?: unknown; }; delete self.message; set(exceptions, self, { message: convertedMessage, name: convertedName }); return self as DOMException; } get name() { return state(exceptions, this).name; } get message() { return state(exceptions, this).message; } get code() { switch (state(exceptions, this).name) { case "IndexSizeError": return 1; case "HierarchyRequestError": return 3; case "WrongDocumentError": return 4; case "InvalidCharacterError": return 5; case "NoModificationAllowedError": return 7; case "NotFoundError": return 8; case "NotSupportedError": return 9; case "InUseAttributeError": return 10; case "InvalidStateError": return 11; case "SyntaxError": return 12; case "InvalidModificationError": return 13; case "NamespaceError": return 14; case "InvalidAccessError": return 15; case "TypeMismatchError": return 17; case "SecurityError": return 18; case "NetworkError": return 19; case "AbortError": return 20; case "URLMismatchError": return 21; case "QuotaExceededError": return 22; case "TimeoutError": return 23; case "InvalidNodeTypeError": return 24; case "DataCloneError": return 25; default: return 0; } } } for ( const [name, value] of [ ["INDEX_SIZE_ERR", 1], ["DOMSTRING_SIZE_ERR", 2], ["HIERARCHY_REQUEST_ERR", 3], ["WRONG_DOCUMENT_ERR", 4], ["INVALID_CHARACTER_ERR", 5], ["NO_DATA_ALLOWED_ERR", 6], ["NO_MODIFICATION_ALLOWED_ERR", 7], ["NOT_FOUND_ERR", 8], ["NOT_SUPPORTED_ERR", 9], ["INUSE_ATTRIBUTE_ERR", 10], ["INVALID_STATE_ERR", 11], ["SYNTAX_ERR", 12], ["INVALID_MODIFICATION_ERR", 13], ["NAMESPACE_ERR", 14], ["INVALID_ACCESS_ERR", 15], ["VALIDATION_ERR", 16], ["TYPE_MISMATCH_ERR", 17], ["SECURITY_ERR", 18], ["NETWORK_ERR", 19], ["ABORT_ERR", 20], ["URL_MISMATCH_ERR", 21], ["QUOTA_EXCEEDED_ERR", 22], ["TIMEOUT_ERR", 23], ["INVALID_NODE_TYPE_ERR", 24], ["DATA_CLONE_ERR", 25], ] ) { define(DOMException, name, { value, enumerable: true }); define(DOMException.prototype, name, { value, enumerable: true }); } function createException( data: ExceptionState, stack: unknown = undefined, ): DOMException { // Construct native Error data without a guest-replaceable dynamic super() call. const result = construct(Error, [data.message], DOMException) as { message?: unknown; }; delete result.message; if (typeof stack === "string") { define(result, "stack", { value: stack, writable: true, configurable: true, }); } set(exceptions, result, data); return result as DOMException; } function dataCloneError(message: string): DOMException { return createException({ message, name: "DataCloneError" }); } registerSerializable( exceptions, (data: ExceptionState, source: object) => createException(data, ErrorGetStack(source)), ); registerWire({ name: "DOMException", states: exceptions, encode: (data) => data, decode: (data: unknown) => createException(data as ExceptionState), }); class Event { constructor(type: unknown, init: unknown = {}) { if (arguments.length === 0) throw new TypeError("Event requires a type"); type = string(type); const options = dictionary(init); set(events, this, { type: type as string, bubbles: !!options.bubbles, cancelable: !!options.cancelable, composed: !!options.composed, target: null, phase: 0, dispatching: false, stopped: false, immediate: false, prevented: false, passive: false, trusted: false, time: floor(now(null) * 10) / 10, }); define(this, "isTrusted", { get: trustedGetter, enumerable: true, }); } get type() { return state(events, this).type; } get bubbles() { return state(events, this).bubbles; } get cancelable() { return state(events, this).cancelable; } get composed() { return state(events, this).composed; } get target() { return state(events, this).target; } get srcElement() { return state(events, this).target; } get currentTarget() { const s = state(events, this); return s.dispatching ? s.target : null; } get eventPhase() { return state(events, this).phase; } get timeStamp() { return state(events, this).time; } get defaultPrevented() { return state(events, this).prevented; } get cancelBubble() { return state(events, this).stopped; } set cancelBubble(value) { if (value) state(events, this).stopped = true; } get returnValue() { return !state(events, this).prevented; } set returnValue(value) { const s = state(events, this); if (!value && s.cancelable && !s.passive) s.prevented = true; } preventDefault() { const s = state(events, this); if (s.cancelable && !s.passive) s.prevented = true; } stopPropagation() { state(events, this).stopped = true; } stopImmediatePropagation() { const s = state(events, this); s.stopped = s.immediate = true; } composedPath() { const s = state(events, this); return s.dispatching ? [s.target] : []; } initEvent( type: unknown, bubbles: unknown = false, cancelable: unknown = false, ) { required(arguments.length, 1); type = string(type); bubbles = !!bubbles; cancelable = !!cancelable; const s = state(events, this); if (s.dispatching) return; s.type = type as string; s.bubbles = bubbles as boolean; s.cancelable = cancelable as boolean; s.composed = false; s.target = null; s.phase = 0; s.trusted = false; s.stopped = s.immediate = s.prevented = false; } } for ( const [name, value] of [ ["NONE", 0], ["CAPTURING_PHASE", 1], ["AT_TARGET", 2], ["BUBBLING_PHASE", 3], ] ) { define(Event, name, { value, enumerable: true }); define(Event.prototype, name, { value, enumerable: true }); } class CustomEvent extends Event { // @ts-expect-error Reflect.construct preserves new.target without a guest-replaceable super(). constructor(type: unknown, init: unknown = {}) { required(arguments.length, 1); type = string(type); const options = dictionary(init); const self = construct(Event, [type, options], new.target); const detail = options.detail; set(customEvents, self, { detail: detail === undefined ? null : detail, }); return self as CustomEvent; } get detail() { return state(customEvents, this).detail; } } class ErrorEvent extends Event { // @ts-expect-error Reflect.construct preserves new.target without a guest-replaceable super(). constructor(type: unknown, init: unknown = {}) { required(arguments.length, 1); type = string(type); const options = dictionary(init); const self = construct(Event, [type, options], new.target); const message = options.message, filename = options.filename, error = options.error, lineno = options.lineno, colno = options.colno; set(errorEvents, self, { message: message === undefined ? "" : string(message), filename: filename === undefined ? "" : string(filename), lineno: lineno === undefined ? 0 : +(lineno as number) >>> 0, colno: colno === undefined ? 0 : +(colno as number) >>> 0, error: error === undefined ? null : error, }); return self as ErrorEvent; } get message() { return state(errorEvents, this).message; } get filename() { return state(errorEvents, this).filename; } get lineno() { return state(errorEvents, this).lineno; } get colno() { return state(errorEvents, this).colno; } get error() { return state(errorEvents, this).error; } } class PromiseRejectionEvent extends Event { // @ts-expect-error Reflect.construct preserves new.target without a guest-replaceable super(). constructor(type: unknown, init: unknown = {}) { required(arguments.length, 1); type = string(type); const options = dictionary(init); const self = construct(Event, [type, options], new.target); const promise = options.promise; if (!isPromise(promise)) { throw new TypeError("Expected a Promise"); } set(rejectionEvents, self, { promise, reason: options.reason, }); return self as PromiseRejectionEvent; } get promise() { return state(rejectionEvents, this).promise; } get reason() { return state(rejectionEvents, this).reason; } } function dispatchEvent( target: object, event: Event, trusted: boolean, ): boolean { const list = state(targets, target), s = state(events, event); if (s.dispatching || !s.type) { throw new DOMException( "Event is already dispatching or uninitialized", "InvalidStateError", ); } s.trusted = trusted; s.dispatching = true; s.target = target as EventTarget; s.phase = 2; s.stopped = s.immediate = false; const snapshot = slice(list); for (let i = 0; i < snapshot.length; i++) { const l = snapshot[i]; if (!l.active || l.type !== s.type) continue; if (l.once) { l.active = false; listenersChanged(target); } s.passive = l.passive; try { if (typeof l.callback === "function") { apply(l.callback, target, [event]); } else l.callback.handleEvent(event); } catch (error) { report(error); } if (s.immediate) break; } s.passive = false; s.dispatching = false; s.phase = 0; return !s.prevented; } class EventTarget { constructor() { set(targets, this, array()); } addEventListener( type: unknown, callback: EventListener | null | undefined, options?: unknown, ): void; addEventListener( type: unknown, callback: unknown, options: unknown = {}, ): void { required(arguments.length, 2); const list = state(targets, this); type = string(type); if (callback === null || callback === undefined) return; if (typeof callback !== "function" && typeof callback !== "object") { throw new TypeError("Invalid event listener"); } const capture = typeof options === "boolean" ? options : !!(options as Record | null | undefined)?.capture; for (let i = 0; i < list.length; i++) { if ( list[i].active && list[i].type === type && list[i].callback === callback && list[i].capture === capture ) return; } const signal = typeof options === "object" ? (options as Record | null)?.signal as | AbortSignal | undefined : undefined; if (signal !== undefined && state(signals, signal).aborted) return; const listener = { type: type as string, callback: callback as EventListener, capture, once: !!(options as Record | null | undefined) ?.once, passive: !!(options as Record | null | undefined) ?.passive, active: true, }; push(list, listener); if (signal !== undefined) { const weakTarget = new WeakRef(this), weakListener = new WeakRef(listener); addAbort(signal, () => { const current = deref(weakListener), target = deref(weakTarget); if (current) current.active = false; if (target) listenersChanged(target); }); } listenersChanged(this); } removeEventListener( type: unknown, callback: EventListener | null | undefined, options?: unknown, ): void; removeEventListener( type: unknown, callback: unknown, options: unknown = {}, ): void { required(arguments.length, 2); const list = state(targets, this); type = string(type); const capture = typeof options === "boolean" ? options : !!(options as Record | null | undefined)?.capture; for (let i = 0; i < list.length; i++) { if ( list[i].type === type && list[i].callback === callback && list[i].capture === capture ) list[i].active = false; } listenersChanged(this); } dispatchEvent(event: Event): boolean { required(arguments.length, 1); return dispatchEvent(this, event, false); } } type CapturedListenerMethod = ( target: unknown, type: unknown, callback: EventListener | null | undefined, options?: unknown, ) => void; const addListener = uncurryThis( EventTarget.prototype.addEventListener, ) as CapturedListenerMethod, removeListener = uncurryThis( EventTarget.prototype.removeEventListener, ) as CapturedListenerMethod; const dispatch = (target: unknown, event: Event): boolean => dispatchEvent(target as object, event, true); function listenersChanged(target: object): void { const list = state(targets, target); let kept = 0; for (let i = 0; i < list.length; i++) { if (list[i].active) list[kept++] = list[i]; } list.length = kept; const observer = get(listenerObservers, target); if (observer) observer(target); } function observeListeners( target: object, observer: (target: object) => void, ): void { state(targets, target); set(listenerObservers, target, observer); observer(target); } function hasListener(target: object, type: string): boolean { const list = state(targets, target); for (let i = 0; i < list.length; i++) { if (list[i].active && list[i].type === type) return true; } return false; } const isDispatching = (event: Event): boolean => state(events, event).dispatching; const preventDefault = uncurryThis(Event.prototype.preventDefault); const globalHandlers = ObjectCreate(null) as Record, globalListeners = ObjectCreate(null) as Record; function installGlobalEventTarget(target: object): void { globalTarget = target; ObjectSetPrototypeOf(target, EventTarget.prototype); set(targets, target, array()); const methods: Record unknown> = { addEventListener( type: unknown, callback: unknown, options: unknown = undefined, ) { required(arguments.length, 2); return addListener( this ?? target, type, callback as EventListener | null, options, ); }, removeEventListener( type: unknown, callback: unknown, options: unknown = undefined, ) { required(arguments.length, 2); return removeListener( this ?? target, type, callback as EventListener | null, options, ); }, dispatchEvent(event: Event) { required(arguments.length, 1); return dispatchEvent(this ?? target, event, false); }, }; for ( const name of ["addEventListener", "removeEventListener", "dispatchEvent"] ) { define(target, name, { value: methods[name], writable: true, enumerable: true, configurable: true, }); } for ( const [name, type] of [ ["onerror", "error"], ["onunhandledrejection", "unhandledrejection"], ["onrejectionhandled", "rejectionhandled"], ] ) { define(target, name, { get: () => globalHandlers[name] ?? null, set: (handler) => setGlobalHandler(name, type, handler), enumerable: true, configurable: true, }); } } function setGlobalHandler(name: string, type: string, handler: unknown): void { handler = typeof handler === "function" ? handler : null; if (!handler) { const listener = globalListeners[name]; if (listener) removeListener(globalTarget!, type, listener); delete globalHandlers[name]; delete globalListeners[name]; return; } globalHandlers[name] = handler as Function; if (globalListeners[name]) return; const listener = (event: Event) => { const handler = globalHandlers[name]; if (!handler) return; const result = type === "error" ? apply(handler as (...args: never[]) => unknown, globalTarget!, [ (event as ErrorEvent).message, (event as ErrorEvent).filename, (event as ErrorEvent).lineno, (event as ErrorEvent).colno, (event as ErrorEvent).error, ]) : apply(handler as (...args: never[]) => unknown, globalTarget!, [event]); if (type === "error" ? result === true : result === false) { const s = state(events, event); if (s.cancelable && !s.passive) s.prevented = true; } }; globalListeners[name] = listener; addListener(globalTarget!, type, listener); } function exceptionMessage(error: unknown): string { if ( error !== null && (typeof error === "object" || typeof error === "function") ) { try { const message = (error as Record).message; if (message !== undefined) return usv(message); } catch (error) { void error; } } try { return usv(error); } catch { return ""; } } function dispatchError( error: unknown, filename = "", lineno = 0, colno = 0, ): boolean { if (reporting || !globalTarget) return false; reporting = true; try { const event = new ErrorEvent("error", { cancelable: true, message: exceptionMessage(error), filename, lineno, colno, error, }); state(errorEvents, event).error = error; dispatch(globalTarget, event); return state(events, event).prevented; } catch { return false; } finally { reporting = false; } } function reportException( error: unknown, filename = "", lineno = 0, colno = 0, ): boolean { const count = mapGet(pendingReports, error); if (count !== undefined) { if (count === 1) mapDelete(pendingReports, error); else mapSet(pendingReports, error, count - 1); return false; } return dispatchError(error, filename, lineno, colno); } function reportErrorValue(error: unknown): void { if (!reporting && dispatchError(error)) return; mapSet(pendingReports, error, (mapGet(pendingReports, error) ?? 0) + 1); enqueueMicrotask(() => { throw error; }); } function reportUnhandledRejection( promise: Promise, reason: unknown, ): boolean { const event = new PromiseRejectionEvent("unhandledrejection", { cancelable: true, promise, reason, }); dispatch(globalTarget!, event); return state(events, event).prevented; } function reportRejectionHandled( promise: Promise, reason: unknown, ): void { dispatch( globalTarget!, new PromiseRejectionEvent("rejectionhandled", { promise, reason }), ); } function addAbort(signal: unknown, callback: () => void): () => void { push(state(signals, signal).algorithms, callback); return () => { const current = state(signals, signal), algorithms = current.algorithms, remaining = array<() => void>(); for (let i = 0; i < algorithms.length; i++) { if (algorithms[i] !== callback) push(remaining, algorithms[i]); } current.algorithms = remaining; }; } function abort(signal: unknown, reason?: unknown): void { const s = state(signals, signal); if (s.aborted) return; s.aborted = true; s.reason = reason === undefined ? new DOMException("The operation was aborted", "AbortError") : reason; const algorithms = s.algorithms; s.algorithms = array<() => void>(); for (let i = 0; i < algorithms.length; i++) algorithms[i](); dispatch(signal, new Event("abort")); } class AbortSignal extends EventTarget { // @ts-expect-error Reflect.construct preserves new.target without a guest-replaceable super(). constructor(key: unknown) { if (key !== secret) throw new TypeError("Illegal constructor"); const self = construct(EventTarget, [], new.target) as AbortSignal; set(signals, self, { aborted: false, reason: undefined, handler: null, algorithms: array<() => void>(), }); return self as AbortSignal; } get aborted() { return state(signals, this).aborted; } get reason() { return state(signals, this).reason; } get onabort() { return state(signals, this).handler; } set onabort(value) { const s = state(signals, this); value = typeof value === "function" ? value : null; if (!s.handler && value) { s.handlerListener = (event) => { if (s.handler) apply(s.handler, this, [event]); }; addListener(this, "abort", s.handlerListener!); } else if (s.handler && !value) { removeListener(this, "abort", s.handlerListener!); } s.handler = value; } throwIfAborted() { const s = state(signals, this); if (s.aborted) throw s.reason; } static abort(reason?: unknown): AbortSignal { const signal = new AbortSignal(secret); abort(signal, reason); return signal; } static any(sequence: unknown): AbortSignal { if ( sequence === null || (typeof sequence !== "object" && typeof sequence !== "function") ) throw new TypeError("Expected a sequence of AbortSignals"); const list = array(); for (const signal of sequence as Iterable) { state(signals, signal); push(list, signal as AbortSignal); } const result = new AbortSignal(secret); for (let i = 0; i < list.length; i++) { if (state(signals, list[i]).aborted) { abort(result, state(signals, list[i]).reason); return result; } } const weak = new WeakRef(result); for (let i = 0; i < list.length; i++) { addAbort(list[i], () => { const follower = deref(weak); if (follower) abort(follower, state(signals, list[i]).reason); }); } return result; } static timeout(milliseconds: unknown): AbortSignal { required(arguments.length, 1); const number = +(milliseconds as number), delay = trunc(number); if (!isFinite(number) || delay < 0 || delay >= 2 ** 64) { throw new TypeError("milliseconds is outside the accepted range"); } const signal = new AbortSignal(secret); scheduleTimeout(() => { abort( signal, createException({ message: "The operation timed out", name: "TimeoutError", }), ); }, delay); return signal; } } class AbortController { constructor() { set(controllers, this, new AbortSignal(secret)); } get signal() { return state(controllers, this); } abort(reason?: unknown): void { abort(state(controllers, this), reason); } } function followSignal(signal: unknown): AbortSignal { const source = state(signals, signal), result = new AbortSignal(secret); if (source.aborted) abort(result, source.reason); else { const weak = new WeakRef(result); addAbort(signal, () => { const follower = deref(weak); if (follower) abort(follower, source.reason); }); } return result; } export { abort, AbortController, AbortSignal, addAbort, addListener, createException, CustomEvent, dataCloneError, dispatch, DOMException, ErrorEvent, Event, EventTarget, followSignal, hasListener, installGlobalEventTarget, isDispatching, observeListeners, preventDefault, PromiseRejectionEvent, removeListener, reportErrorValue, reportException, reportRejectionHandled, reportUnhandledRejection, secret, signals, };