1/**
2 * @import { IoCapabilities, IoInterface, IoMessage, WireEnvelope } from "@kunkun/kkrpc"
3 *
4 * @import { MessengerRealm } from "../worker.d.ts"
5 */
6
7const DESTROY_SIGNAL = "__DESTROY__";
8
9/**
10 * @implements {IoInterface}
11 */
12export class BrowserPostMessageIo {
13 name = "browser-postmessage-io";
14
15 /** @type {Array<string | IoMessage>} */
16 #messageQueue = [];
17
18 /** @type {((value: string | IoMessage | null) => void) | null} */
19 #resolveRead = null;
20
21 /** */
22 #realm;
23
24 /** @type {IoCapabilities} */
25 capabilities = {
26 structuredClone: true,
27 transfer: true,
28 };
29
30 /**
31 * @param {() => MessengerRealm} realmCreator
32 */
33 constructor(realmCreator) {
34 /** @type {undefined | MessengerRealm} */
35 const realm = realmCreator();
36 realm.addEventListener("message", this.#handleMessage.bind(this));
37
38 this.#realm = () => {
39 return realm;
40 };
41 }
42
43 /**
44 * @param {MessageEvent} event
45 */
46 #handleMessage(event) {
47 const raw = event.data;
48 const message = this.#normalizeIncoming(raw);
49
50 // Handle destroy signal
51 if (message === DESTROY_SIGNAL) {
52 this.destroy();
53 return;
54 }
55
56 if (this.#resolveRead) {
57 this.#resolveRead(message);
58 this.#resolveRead = null;
59 } else {
60 this.#messageQueue.push(message);
61 }
62 }
63
64 /**
65 * @param {any} message
66 * @returns {string | IoMessage}
67 */
68 #normalizeIncoming(message) {
69 if (typeof message === "string") {
70 return message;
71 }
72
73 if (message && typeof message === "object" && message.version === 2) {
74 const envelope = /** @type {WireEnvelope} */ (message);
75 return {
76 data: envelope,
77 transfers: (/** @type {unknown[] | undefined} */ (envelope
78 .__transferredValues)) ?? [],
79 };
80 }
81
82 return /** @type {string} */ (message);
83 }
84
85 /** @returns {Promise<string | IoMessage | null>} */
86 read() {
87 // If there are queued messages, return the first one
88 if (this.#messageQueue.length > 0) {
89 return Promise.resolve(this.#messageQueue.shift() ?? null);
90 }
91
92 // Otherwise, wait for the next message
93 return new Promise((resolve) => {
94 this.#resolveRead = resolve;
95 });
96 }
97
98 /**
99 * @param {string | IoMessage} message
100 */
101 write(message) {
102 if (typeof message === "string") {
103 this.#realm().postMessage(message);
104 return Promise.resolve();
105 }
106
107 if (message.transfers && message.transfers.length > 0) {
108 const msg = { ...message };
109
110 if (typeof msg.data === "object" && msg.data.payload.args) {
111 if (msg.data.payload.args[0] instanceof HTMLElement) {
112 msg.data.payload.args[0] = undefined;
113 }
114 }
115
116 this.#realm().postMessage(
117 message.data,
118 /** @type {Transferable[]} */ (message.transfers),
119 );
120 } else {
121 this.#realm().postMessage(message.data);
122 }
123
124 return Promise.resolve();
125 }
126
127 destroy() {
128 const realm = this.#realm();
129
130 realm.postMessage(DESTROY_SIGNAL);
131
132 if (
133 "terminate" in realm && typeof realm.terminate === "function"
134 ) {
135 realm.terminate();
136 }
137 }
138
139 signalDestroy() {
140 this.#realm().postMessage(DESTROY_SIGNAL);
141 }
142}