A music player that connects to your cloud/distributed storage.
at v4 142 lines 3.2 kB view raw
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}