A music player that connects to your cloud/distributed storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at v4 172 lines 4.2 kB view raw
1/** 2 * @import {MessengerRealm, ProxiedActions} from "../worker.d.ts" 3 */ 4 5/** 6 * Lightweight RPC channel over postMessage using structured clone. 7 * 8 * Protocol: 9 * - Request: { __rpc: true, id, method, args, type: "request" } 10 * - Response: { __rpc: true, id, type: "response", result } or { ..., error } 11 * 12 * @template {Record<string, (...args: any[]) => any>} LocalAPI 13 * @template {Record<string, (...args: any[]) => any>} RemoteAPI 14 */ 15export class RpcChannel { 16 /** @type {LocalAPI | undefined} */ 17 #actions; 18 19 /** @type {Record<string, { resolve: (v: any) => void, reject: (e: any) => void }>} */ 20 #pending = {}; 21 22 #port; 23 24 /** @type {(event: MessageEvent) => void} */ 25 #listener; 26 27 /** 28 * @param {MessagePort | Worker | MessengerRealm} port 29 * @param {{ expose?: LocalAPI }} [options] 30 */ 31 constructor(port, options) { 32 this.#port = port; 33 this.#actions = options?.expose; 34 35 this.#listener = (/** @type {MessageEvent} */ event) => { 36 const msg = event.data; 37 if (!msg || msg.__rpc !== true) return; 38 39 if (msg.type === "request") { 40 if (this.#actions) this.#handleRequest(msg); 41 } else if (msg.type === "response") { 42 this.#handleResponse(msg); 43 } 44 }; 45 46 port.addEventListener( 47 "message", 48 /** @type {EventListener} */ (this.#listener), 49 ); 50 } 51 52 /** 53 * @param {LocalAPI} actions 54 */ 55 expose(actions) { 56 this.#actions = actions; 57 } 58 59 /** 60 * @template {keyof RemoteAPI & string} M 61 * @param {M} method 62 * @param {any[]} args 63 * @returns {Promise<any>} 64 */ 65 callMethod(method, args) { 66 return new Promise((resolve, reject) => { 67 const id = crypto.randomUUID(); 68 this.#pending[id] = { resolve, reject }; 69 this.#port.postMessage({ 70 __rpc: true, 71 id, 72 method, 73 args, 74 type: "request", 75 }); 76 }); 77 } 78 79 /** 80 * @returns {ProxiedActions<RemoteAPI>} 81 */ 82 getAPI() { 83 return /** @type {ProxiedActions<RemoteAPI>} */ ( 84 new Proxy(/** @type {any} */ ({}), { 85 get: (_target, /** @type {string} */ prop) => { 86 return (/** @type {any[]} */ ...args) => this.callMethod(prop, args); 87 }, 88 }) 89 ); 90 } 91 92 destroy() { 93 this.#port.removeEventListener( 94 "message", 95 /** @type {EventListener} */ (this.#listener), 96 ); 97 98 if ("close" in this.#port && typeof this.#port.close === "function") { 99 this.#port.close(); 100 } 101 102 if ( 103 "terminate" in this.#port && typeof this.#port.terminate === "function" 104 ) { 105 /** @type {any} */ (this.#port).terminate(); 106 } 107 108 // Reject all pending requests 109 for (const [id, { reject }] of Object.entries(this.#pending)) { 110 reject(new Error("RPC channel destroyed")); 111 delete this.#pending[id]; 112 } 113 } 114 115 /** 116 * @param {{ id: string, method: string, args: any[] }} msg 117 */ 118 #handleRequest(msg) { 119 const { id, method, args } = msg; 120 const fn = this.#actions?.[/** @type {keyof LocalAPI} */ (method)]; 121 122 if (typeof fn !== "function") { 123 this.#port.postMessage({ 124 __rpc: true, 125 id, 126 type: "response", 127 error: `Method "${method}" not found`, 128 }); 129 return; 130 } 131 132 try { 133 Promise.resolve(fn(...args)).then( 134 (result) => { 135 this.#port.postMessage({ __rpc: true, id, type: "response", result }); 136 }, 137 (err) => { 138 console.error(err); 139 this.#port.postMessage({ 140 __rpc: true, 141 id, 142 type: "response", 143 error: err?.message ?? String(err), 144 }); 145 }, 146 ); 147 } catch (err) { 148 console.error(err); 149 this.#port.postMessage({ 150 __rpc: true, 151 id, 152 type: "response", 153 error: /** @type {Error} */ (err)?.message ?? String(err), 154 }); 155 } 156 } 157 158 /** 159 * @param {{ id: string, result?: any, error?: string }} msg 160 */ 161 #handleResponse(msg) { 162 const pending = this.#pending[msg.id]; 163 if (!pending) return; 164 delete this.#pending[msg.id]; 165 166 if (msg.error !== undefined) { 167 pending.reject(new Error(msg.error)); 168 } else { 169 pending.resolve(msg.result); 170 } 171 } 172}