import { FinalizationRegistry, ObjectCreate, ReflectApply as apply, ReflectConstruct as construct, Set, SetPrototypeAdd as retain, SetPrototypeDelete as release, uncurryThis, WeakMap, WeakMapPrototypeSet as set, } from "ext:core/primordials"; import { op_broadcast_close, op_broadcast_open, op_broadcast_send, } from "ext:core/ops"; import { array, required, state, string } from "./idl.js"; import { addListener, createException, dataCloneError, dispatch, type Event, EventTarget, hasListener, observeListeners, preventDefault, removeListener, } from "./events.js"; import { adoptPort, dispatchMessage, messageData, MessageEvent, MessagePort, startPort, } from "./messaging.js"; import { classify, registerUncloneable, transferCallbacks, } from "./serialization.js"; interface BroadcastState { __proto__?: null; name: string; id: number; port: MessagePort; closed: boolean; onmessage?: ((event: Event) => unknown) | null; onmessageerror?: ((event: Event) => unknown) | null; onmessageListener?: ((event: Event) => void) | null; onmessageerrorListener?: ((event: Event) => void) | null; } const channels = new WeakMap(); registerUncloneable(channels); const closePort = MessagePort.prototype.close; const WeakRef = globalThis.WeakRef, deref = uncurryThis(WeakRef.prototype.deref); const register = uncurryThis(FinalizationRegistry.prototype.register), unregister = uncurryThis(FinalizationRegistry.prototype.unregister); const activeChannels = new Set(); const finalizer = new FinalizationRegistry<{ id: number; port: MessagePort }>( ({ id, port }) => { apply(closePort, port, []); op_broadcast_close(id); }, ); function retainChannel(target: object): void { if ( !state(channels, target).closed && (hasListener(target, "message") || hasListener(target, "messageerror")) ) { retain(activeChannels, target); } else release(activeChannels, target); } function handler( target: BroadcastChannel, current: BroadcastState, type: "message" | "messageerror", value: unknown, ): void { const name = "on" + type as "onmessage" | "onmessageerror", key = name + "Listener" as "onmessageListener" | "onmessageerrorListener"; value = typeof value === "function" ? value : null; current[name] = value as ((event: Event) => unknown) | null; if (!value && current[key]) { removeListener(target, type, current[key]); current[key] = null; } else if (value && !current[key]) { current[key] = (event) => { if (current[name] && apply(current[name]!, target, [event]) === false) { preventDefault(event); } }; addListener(target, type, current[key]); } } class BroadcastChannel extends EventTarget { // @ts-expect-error Reflect.construct preserves new.target without a guest-replaceable super(). constructor(name: unknown) { required(arguments.length, 1); name = string(name); const self = construct(EventTarget, [], new.target) as BroadcastChannel; const id = op_broadcast_open(name as string), port = adoptPort(id); const current: BroadcastState = { __proto__: null, name: name as string, id, port, closed: false, }; set(channels, self, current); observeListeners(self, retainChannel); const weak = new WeakRef(self); addListener(port, "message", (event: Event) => { const target = deref(weak); if (target && !state(channels, target).closed) { dispatchMessage(target, messageData(event)); } }); addListener(port, "messageerror", () => { const target = deref(weak); if (target && !state(channels, target).closed) { dispatch(target, new MessageEvent("messageerror", ObjectCreate(null))); } }); register(finalizer, self, { id, port }, self); startPort(port); return self as BroadcastChannel; } get name() { return state(channels, this).name; } postMessage(message: unknown): void { const current = state(channels, this); required(arguments.length, 1); if (current.closed) { throw createException({ name: "InvalidStateError", message: "BroadcastChannel is closed", }); } const callbacks = transferCallbacks(array(), dataCloneError, true); try { op_broadcast_send( current.id, { __proto__: null, data: message, ports: [] }, classify, callbacks.encodeHost, dataCloneError, callbacks.validate, callbacks.commit, ); } finally { callbacks.clear(); } } close() { const current = state(channels, this); if (current.closed) return; current.closed = true; retainChannel(this); unregister(finalizer, this); apply(closePort, current.port, []); op_broadcast_close(current.id); } get onmessage() { return state(channels, this).onmessage ?? null; } set onmessage(value) { handler(this, state(channels, this), "message", value); } get onmessageerror() { return state(channels, this).onmessageerror ?? null; } set onmessageerror(value) { handler(this, state(channels, this), "messageerror", value); } } export { BroadcastChannel };