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

Configure Feed

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

feat: reimplement rpc

* wip: rewrite rpc stuff

* fix: processing issue

* wip: broadcast

* fix: announcement issues

* fix: broadcasted audio

authored by

Steven Vandevelde and committed by
GitHub
d96d0af2 33bc348c

+816 -640
+1
deno.jsonc
··· 8 8 "@atcute/lexicons": "npm:@atcute/lexicons@^1.2.2", 9 9 "@bradenmacdonald/s3-lite-client": "jsr:@bradenmacdonald/s3-lite-client@^0.9.4", 10 10 "@fry69/deep-diff": "jsr:@fry69/deep-diff@^0.1.10", 11 + "@kunkun/kkrpc": "jsr:@kunkun/kkrpc@^0.6.0", 11 12 "@mary/ds-queue": "jsr:@mary/ds-queue@^0.1.3", 12 13 "@mys/m-rpc": "jsr:@mys/m-rpc@^0.12.2", 13 14 "@mys/worker-fn": "jsr:@mys/worker-fn@^3.2.1",
+86 -5
deno.lock
··· 4 4 "jsr:@bradenmacdonald/s3-lite-client@~0.9.4": "0.9.4", 5 5 "jsr:@deno/loader@0.3.6": "0.3.6", 6 6 "jsr:@fry69/deep-diff@~0.1.10": "0.1.10", 7 + "jsr:@kunkun/kkrpc@0.6": "0.6.0", 7 8 "jsr:@mary/ds-queue@~0.1.3": "0.1.3", 8 9 "jsr:@mys/m-rpc@~0.12.2": "0.12.2", 9 10 "jsr:@mys/worker-fn@^3.2.1": "3.2.1", ··· 41 42 "npm:@atcute/lex-cli@*": "2.3.1", 42 43 "npm:@atcute/lex-cli@^2.3.1": "2.3.1", 43 44 "npm:@atcute/lexicons@^1.2.2": "1.2.2", 45 + "npm:@tauri-apps/plugin-shell@^2.2.0": "2.3.3", 44 46 "npm:alien-signals@3": "3.0.3", 45 47 "npm:autoprefixer@10.4.21": "10.4.21_postcss@8.5.6", 46 48 "npm:esbuild-plugins-node-modules-polyfill@^1.7.1": "1.7.1_esbuild@0.25.12", ··· 54 56 "npm:postcss-import@16.1.1": "16.1.1_postcss@8.5.6", 55 57 "npm:postcss@8.5.6": "8.5.6", 56 58 "npm:query-string@^9.3.1": "9.3.1", 59 + "npm:socket.io-client@^4.8.1": "4.8.1", 57 60 "npm:subsonic-api@^3.2.0": "3.2.0", 61 + "npm:superjson@^2.2.2": "2.2.5", 58 62 "npm:throttle-debounce@^5.0.2": "5.0.2", 59 63 "npm:uint8arrays@^5.1.0": "5.1.0", 60 64 "npm:uri-js@^4.4.1": "4.4.1", ··· 70 74 }, 71 75 "@fry69/deep-diff@0.1.10": { 72 76 "integrity": "cdd88fefaef1ac896a038a5f3c0895038d8c725e61bac50489c455156e0275f5" 77 + }, 78 + "@kunkun/kkrpc@0.6.0": { 79 + "integrity": "44674738cac0712740ee9e2a57048bd53e1086829a2c4f21e15c03333793a19f", 80 + "dependencies": [ 81 + "npm:@tauri-apps/plugin-shell", 82 + "npm:socket.io-client", 83 + "npm:superjson" 84 + ] 73 85 }, 74 86 "@mary/ds-queue@0.1.3": { 75 87 "integrity": "a743caa397b924cb08b0bbdffc526eb1ea2d3fc9e675da6edc137c437fc93c76" ··· 414 426 "tslib" 415 427 ] 416 428 }, 429 + "@socket.io/component-emitter@3.1.2": { 430 + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" 431 + }, 417 432 "@standard-schema/spec@1.0.0": { 418 433 "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==" 419 434 }, 435 + "@tauri-apps/api@2.9.0": { 436 + "integrity": "sha512-qD5tMjh7utwBk9/5PrTA/aGr3i5QaJ/Mlt7p8NilQ45WgbifUNPyKWsA63iQ8YfQq6R8ajMapU+/Q8nMcPRLNw==" 437 + }, 438 + "@tauri-apps/plugin-shell@2.3.3": { 439 + "integrity": "sha512-Xod+pRcFxmOWFWEnqH5yZcA7qwAMuaaDkMR1Sply+F8VfBj++CGnj2xf5UoialmjZ2Cvd8qrvSCbU+7GgNVsKQ==", 440 + "dependencies": [ 441 + "@tauri-apps/api" 442 + ] 443 + }, 420 444 "@tokenizer/inflate@0.2.7": { 421 445 "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", 422 446 "dependencies": [ 423 - "debug", 447 + "debug@4.4.3", 424 448 "fflate", 425 449 "token-types@6.1.1" 426 450 ] ··· 582 606 "content-type@1.0.5": { 583 607 "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" 584 608 }, 609 + "copy-anything@4.0.5": { 610 + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", 611 + "dependencies": [ 612 + "is-what" 613 + ] 614 + }, 585 615 "core-js@2.6.12": { 586 616 "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", 587 617 "deprecated": true, ··· 592 622 }, 593 623 "csstype@3.1.3": { 594 624 "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" 625 + }, 626 + "debug@4.3.7": { 627 + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", 628 + "dependencies": [ 629 + "ms" 630 + ] 595 631 }, 596 632 "debug@4.4.3": { 597 633 "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", ··· 638 674 "endianness@8.0.2": { 639 675 "integrity": "sha512-IU+77+jJ7lpw2qZ3NUuqBZFy3GuioNgXUdsL1L9tooDNTaw0TgOnwNuc+8Ns+haDaTifK97QLzmOANJtI/rGvw==" 640 676 }, 677 + "engine.io-client@6.6.3": { 678 + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", 679 + "dependencies": [ 680 + "@socket.io/component-emitter", 681 + "debug@4.3.7", 682 + "engine.io-parser", 683 + "ws", 684 + "xmlhttprequest-ssl" 685 + ] 686 + }, 687 + "engine.io-parser@5.2.3": { 688 + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==" 689 + }, 641 690 "entities@4.5.0": { 642 691 "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" 643 692 }, ··· 819 868 "is-typedarray@1.0.0": { 820 869 "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" 821 870 }, 871 + "is-what@5.5.0": { 872 + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==" 873 + }, 822 874 "isarray@1.0.0": { 823 875 "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" 824 876 }, ··· 948 1000 "dependencies": [ 949 1001 "assert", 950 1002 "buffer", 951 - "debug", 1003 + "debug@4.4.3", 952 1004 "music-metadata@3.8.0", 953 1005 "readable-stream@3.6.2", 954 1006 "remove", ··· 962 1014 "@borewit/text-codec@0.2.0", 963 1015 "@tokenizer/token", 964 1016 "content-type", 965 - "debug", 1017 + "debug@4.4.3", 966 1018 "file-type@21.0.0", 967 1019 "media-typer@1.1.0", 968 1020 "strtok3@10.3.4", ··· 973 1025 "music-metadata@3.8.0": { 974 1026 "integrity": "sha512-aIADbp3uCS+ANr4nnFEHzTzMy81OT7PR7WBMW73SJ28Y7P94nnEugmTOj1ICP2JmxBBDlo+MeYVgiPnxVN69tg==", 975 1027 "dependencies": [ 976 - "debug", 1028 + "debug@4.4.3", 977 1029 "file-type@11.1.0", 978 1030 "media-typer@0.3.0", 979 1031 "strtok3@2.3.0", ··· 1208 1260 "setimmediate@1.0.5": { 1209 1261 "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" 1210 1262 }, 1263 + "socket.io-client@4.8.1": { 1264 + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", 1265 + "dependencies": [ 1266 + "@socket.io/component-emitter", 1267 + "debug@4.3.7", 1268 + "engine.io-client", 1269 + "socket.io-parser" 1270 + ] 1271 + }, 1272 + "socket.io-parser@4.2.4": { 1273 + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", 1274 + "dependencies": [ 1275 + "@socket.io/component-emitter", 1276 + "debug@4.3.7" 1277 + ] 1278 + }, 1211 1279 "source-map-js@1.2.1": { 1212 1280 "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" 1213 1281 }, ··· 1229 1297 "strtok3@2.3.0": { 1230 1298 "integrity": "sha512-AA67/1atBh7X0fUTDevjW89by2ZkY9RZAnkwusx5Yc1COYf0ruUbpYOOIs03SnRA1CF9K3+BtRXKOEtKhAXVaQ==", 1231 1299 "dependencies": [ 1232 - "debug", 1300 + "debug@4.4.3", 1233 1301 "then-read-stream", 1234 1302 "token-types@1.3.2" 1235 1303 ] 1236 1304 }, 1237 1305 "subsonic-api@3.2.0": { 1238 1306 "integrity": "sha512-BADBQ2hONdLb3agCiSDzNzTIFLWJAuxJTUJvC2zDFvXUVfnK3yy7r8xFu3NkrQl8p5UVI7q8Qfm62N1lFxWbww==" 1307 + }, 1308 + "superjson@2.2.5": { 1309 + "integrity": "sha512-zWPTX96LVsA/eVYnqOM2+ofcdPqdS1dAF1LN4TS2/MWuUpfitd9ctTa87wt4xrYnZnkLtS69xpBdSxVBP5Rm6w==", 1310 + "dependencies": [ 1311 + "copy-anything" 1312 + ] 1239 1313 }, 1240 1314 "supports-preserve-symlinks-flag@1.0.0": { 1241 1315 "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" ··· 1354 1428 }, 1355 1429 "winamp-eqf@1.0.0": { 1356 1430 "integrity": "sha512-yUIb4+lTYBKP4L6nPXdDj1CQBXlJ+/PrNAkT1VbTAgeFjX8lPxAthsUE5NxQP4s8SO4YMJemsrErZ49Bh+/Veg==" 1431 + }, 1432 + "ws@8.17.1": { 1433 + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==" 1434 + }, 1435 + "xmlhttprequest-ssl@2.1.2": { 1436 + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==" 1357 1437 }, 1358 1438 "xxh32@2.0.5": { 1359 1439 "integrity": "sha512-glQIaPvLHV4xG2Sn0E4mZWY25JT34+XcG4e2c8OMIH2SXxVrm6MmJ8miCsqGBLtf+rn2YcaeS11vq/66vkXGUQ==" ··· 1698 1778 "dependencies": [ 1699 1779 "jsr:@bradenmacdonald/s3-lite-client@~0.9.4", 1700 1780 "jsr:@fry69/deep-diff@~0.1.10", 1781 + "jsr:@kunkun/kkrpc@0.6", 1701 1782 "jsr:@mary/ds-queue@~0.1.3", 1702 1783 "jsr:@mys/m-rpc@~0.12.2", 1703 1784 "jsr:@mys/worker-fn@^3.2.1",
+117 -47
src/common/element.js
··· 1 + import QS from "query-string"; 1 2 import { html, render } from "lit-html"; 2 3 3 4 import { effect, signal } from "@common/signal.js"; 4 - import { define, use } from "@common/worker.js"; 5 + import { rpc, workerProxy } from "./worker.js"; 6 + import { BrowserPostMessageIo } from "./worker/rpc.js"; 7 + import { RPCChannel } from "@kunkun/kkrpc"; 5 8 6 9 /** 7 10 * @import {BroadcastingStatus, FnParams, FnReturn} from "./element.d.ts" 11 + * @import {ProxiedActions} from "./worker.d.ts"; 8 12 * @import {Signal} from "./signal.d.ts" 9 13 */ 10 14 ··· 21 25 22 26 constructor() { 23 27 super(); 24 - this.group = this.getAttribute("group") || crypto.randomUUID(); 28 + 29 + this.group = this.getAttribute("group") ?? "default"; 30 + 31 + this.worker = this.worker.bind(this); 32 + this.workerLink = this.workerLink.bind(this); 25 33 } 26 34 27 35 /** ··· 78 86 disconnectedCallback() { 79 87 this.#teardown(); 80 88 } 89 + 90 + // WORKER 91 + 92 + /** @type {undefined | Worker | SharedWorker} */ 93 + #worker; 94 + 95 + worker() { 96 + const NAME = this.constructor.prototype.constructor.NAME; 97 + const WORKER_URL = this.constructor.prototype.constructor.WORKER_URL; 98 + 99 + if (!NAME) throw new Error("Missing `NAME` static property"); 100 + if (!WORKER_URL) throw new Error("Missing `WORKER_URL` static property"); 101 + 102 + // Query 103 + const query = QS.stringify( 104 + "workerQuery" in this && typeof this.workerQuery === "function" 105 + ? this.workerQuery() 106 + : {}, 107 + ); 108 + 109 + // Setup worker 110 + const name = `${NAME}/${this.group}`; 111 + const url = import.meta.resolve("./" + WORKER_URL) + `?${query}`; 112 + 113 + let worker; 114 + 115 + if (this.hasAttribute("group")) { 116 + worker = new SharedWorker(url, { name, type: "module" }); 117 + } else { 118 + worker = new Worker(url, { name, type: "module" }); 119 + } 120 + 121 + return worker; 122 + } 123 + 124 + workerLink() { 125 + this.#worker ??= this.worker(); 126 + const worker = this.#worker; 127 + 128 + if (worker instanceof SharedWorker) { 129 + worker.port.start(); 130 + return worker.port; 131 + } else { 132 + return worker; 133 + } 134 + } 81 135 } 82 136 83 137 /** ··· 109 163 } 110 164 111 165 /** 166 + * @template {Record<string, { strategy: "leaderOnly" | "replicate", fn: (...args: any[]) => any }>} ActionsWithStrategy 167 + * @template {{ [K in keyof ActionsWithStrategy]: ActionsWithStrategy[K]["fn"] }} Actions 112 168 * @param {string} name 169 + * @param {ActionsWithStrategy} actionsWithStrategy 113 170 */ 114 - broadcast(name) { 171 + broadcast(name, actionsWithStrategy) { 172 + if (this.broadcasted) return; 173 + 115 174 const channel = new BroadcastChannel(name); 116 175 const msg = new MessageChannel(); 117 176 177 + /** 178 + * @typedef {{ [K in keyof ActionsWithStrategy]: ActionsWithStrategy[K]["fn"] }} A 179 + */ 180 + 118 181 this.broadcasted = true; 119 182 this.name = name; 120 183 184 + const _rpc = rpc( 185 + msg.port2, 186 + Object.fromEntries( 187 + Object.entries(actionsWithStrategy).map(([k, v]) => { 188 + return [k, v.fn.bind(this)]; 189 + }), 190 + ), 191 + ); 192 + 121 193 channel.addEventListener( 122 194 "message", 123 195 async (event) => { 124 - const name = event.data.name?.split(":"); 125 - 126 - if (name[0] === "leader") { 196 + if (event.data?.includes('"method":"leader:')) { 127 197 const status = await this.#status.promise; 128 198 if (status.leader) { 129 - msg.port1.postMessage({ 130 - ...event.data, 131 - name: name.splice(1).join(":"), 132 - }); 199 + const json = event.data.replace('"method":"leader:', '"method":"'); 200 + msg.port1.postMessage(json); 133 201 } 134 202 } else { 135 203 msg.port1.postMessage(event.data); ··· 150 218 return !!state.pending?.length; 151 219 } 152 220 153 - /** 154 - * @template I 155 - * @template O 156 - * @template {(...args: I[]) => O} Fn 157 - * @param {string} method 158 - * @param {Fn} fn 159 - */ 160 - return (method, fn) => { 161 - define(method, fn.bind(this), msg.port2); 221 + const io = new BrowserPostMessageIo(() => msg.port2); 162 222 163 - /** 164 - * @typedef {FnParams<typeof fn>} P 165 - * @typedef {FnReturn<typeof fn>} R 166 - */ 223 + /** @type {undefined | RPCChannel<{}, ProxiedActions<Actions>>} */ 224 + const proxyChannel = new RPCChannel(io, { enableTransfer: true }); 167 225 168 - /** @param {P} args */ 169 - const leaderOnly = async (...args) => { 170 - const status = await this.#status.promise; 171 - return status.leader 172 - ? /** @type {R} */ (fn.call(this, ...args)) 173 - : /** @type {Promise<R>} */ (use(`leader:${method}`, msg.port2)( 174 - ...args, 175 - )); 176 - }; 226 + /** @type {ProxiedActions<Actions>} */ 227 + const proxy = proxyChannel.getAPI(); 177 228 178 - /** 179 - * @param {P} args 180 - * @returns {R} 181 - */ 182 - const replicate = (...args) => { 183 - anyoneWaiting().then((bool) => { 184 - if (bool) use(method, msg.port2)(...args); 185 - }); 186 - return /** @type {R} */ (fn.call(this, ...args)); 187 - }; 229 + /** @type {any} */ 230 + const actions = {}; 231 + 232 + Object.entries(actionsWithStrategy).forEach( 233 + ([action, { fn, strategy }]) => { 234 + const ogFn = fn.bind(this); 235 + let wrapFn = ogFn; 236 + 237 + switch (strategy) { 238 + case "leaderOnly": 239 + /** @param {Parameters<Actions[action]>} args */ 240 + wrapFn = async (...args) => { 241 + const status = await this.#status.promise; 242 + return status.leader 243 + ? ogFn(...args) 244 + : proxyChannel.callMethod(`leader:${action}`, args); 245 + }; 246 + break; 247 + 248 + case "replicate": 249 + /** @param {Parameters<Actions[action]>} args */ 250 + wrapFn = async (...args) => { 251 + anyoneWaiting().then((bool) => { 252 + if (bool) proxy[action](...args); 253 + }); 254 + return ogFn(...args); 255 + }; 256 + break; 257 + } 188 258 189 - return { 190 - leaderOnly, 191 - replicate, 192 - }; 193 - }; 259 + actions[action] = wrapFn; 260 + }, 261 + ); 262 + 263 + return /** @type {ProxiedActions<Actions>} */ (actions); 194 264 } 195 265 196 266 // LIFECYCLE
+11 -2
src/common/worker.d.ts
··· 34 34 /** */ 35 35 export type ProxyProvider< 36 36 Actions extends Record<string, (...args: any[]) => any>, 37 - > = (workerOrPort: MessagePort | Worker) => ProxiedActions<Actions>; 37 + > = ( 38 + workerLinkCreator: () => MessagePort | Worker, 39 + ) => ProxiedActions<Actions>; 38 40 39 41 /** */ 40 42 export type ProxyProviderMethod< ··· 42 44 > = { proxy: ProxyProvider<Actions> }; 43 45 44 46 /** */ 45 - export type WorkerProvider = (group?: string) => Worker; 47 + export interface MessengerRealm { 48 + postMessage: MessagePort["postMessage"]; 49 + addEventListener: MessagePort["addEventListener"]; 50 + removeEventListener: MessagePort["removeEventListener"]; 51 + } 52 + 53 + /** */ 54 + export type WorkerProvider = (group?: string) => Worker | SharedWorker; 46 55 47 56 /** */ 48 57 export type WorkerProviderMethod = { worker: WorkerProvider };
+48 -148
src/common/worker.js
··· 1 - import Queue from "@mary/ds-queue"; 2 - 3 - import { MRpc } from "@mys/m-rpc"; 1 + import { RPCChannel } from "@kunkun/kkrpc"; 4 2 import { getTransferables } from "@okikio/transferables"; 5 3 import { debounceMicrotask } from "@vicary/debounce-microtask"; 6 4 import { xxh32 } from "xxh32"; 7 5 8 - import { batch } from "./signal.js"; 6 + import { BrowserPostMessageIo } from "./worker/rpc.js"; 9 7 10 - export { getTransferables } from "@okikio/transferables"; 8 + export { transfer } from "@kunkun/kkrpc"; 11 9 12 10 /** 13 - * @import {MRpcCallOptions, WorkerGlobalScope} from "@mys/m-rpc"; 14 - * @import {Announcement, IncompleteArray, ProxiedActions, ProxyProvider} from "./worker.d.ts" 11 + * @import {Announcement, MessengerRealm, ProxiedActions} from "./worker.d.ts" 15 12 */ 16 13 17 14 //////////////////////////////////////////// ··· 22 19 * Manage incoming connections for a shared worker. 23 20 * If a regular worker is used instead, it'll just execute the callback immediately. 24 21 * 25 - * @template {MessagePort | Worker | WorkerGlobalScope} T 26 - * @param {(context: MessagePort | T) => void} callback 22 + * @template {MessagePort | Worker | MessengerRealm} T 23 + * @param {(context: MessagePort | T, firstConnection: boolean, connectionId: string) => void} callback 27 24 * @param {T} [context] Uses `globalThis` by default. 28 25 */ 29 26 export function ostiary( ··· 31 28 context = /** @type {T} */ (/** @type {unknown} */ (globalThis)), 32 29 ) { 33 30 if (/** @type {any} */ (context).onmessage === null) { 34 - return callback(context); 31 + return callback(context, true, crypto.randomUUID()); 35 32 } 36 33 34 + const c = /** @type {any} */ (context); 35 + c.__id ??= crypto.randomUUID(); 36 + 37 37 context.addEventListener( 38 38 "connect", 39 39 /** ··· 45 45 port.start(); 46 46 47 47 // Initiate setup 48 - callback(port); 48 + callback(port, !(c.__initiated ?? false), c.__id); 49 + c.__initiated = true; 49 50 }, 50 51 ); 51 52 } 52 53 53 54 /** 54 - * @param {MessagePort | Worker} workerOrPort 55 + * @param {() => MessagePort | Worker} workerLinkCreator 55 56 */ 56 - export function portProvider(workerOrPort) { 57 + export function portProvider(workerLinkCreator) { 57 58 return () => { 58 59 const channel = new MessageChannel(); 60 + const workerOrPort = workerLinkCreator(); 59 61 60 62 channel.port1.addEventListener("message", (event) => { 61 63 workerOrPort.postMessage(event.data); ··· 85 87 }; 86 88 } 87 89 88 - /** 89 - * @template {Record<string, (...args: any[]) => any>} Actions 90 - * @template {keyof Actions} T 91 - * @template {T[]} U 92 - * @param {U & ([T] extends [U[number]] ? unknown : IncompleteArray<T>[])} actions 93 - */ 94 - export function proxyProvider(actions) { 95 - /** 96 - * @type {ProxyProvider<Actions>} 97 - */ 98 - return (workerOrPort) => { 99 - /** @type {Record<string | number | symbol, (...args: any[]) => any>} */ 100 - const proxy = {}; 101 - 102 - actions.forEach((action) => { 103 - proxy[action] = use(action.toString(), workerOrPort); 104 - }); 105 - 106 - return /** @type {ProxiedActions<Actions>} */ (proxy); 107 - }; 108 - } 109 - 110 90 //////////////////////////////////////////// 111 91 // RAW 112 92 //////////////////////////////////////////// ··· 115 95 * @template T 116 96 * @param {string} name 117 97 * @param {T} args 118 - * @param {MessagePort | Worker | WorkerGlobalScope} [context] Uses `globalThis` by default. 98 + * @param {MessagePort | Worker | MessengerRealm} [context] Uses `globalThis` by default. 119 99 */ 120 100 export function announce( 121 101 name, 122 102 args, 123 103 context, 124 104 ) { 125 - outgoing.enqueue(announcement(name, args)); 126 - flushOutgoingAnnouncements(context); 105 + const a = announcement(name, args); 106 + const transferables = getTransferables(a); 107 + (context ?? globalThis).postMessage(a, { transfer: transferables }); 127 108 } 128 109 129 110 /** 130 111 * @template T 131 112 * @param {string} name 132 113 * @param {(args: T) => void} fn 133 - * @param {MessagePort | Worker | WorkerGlobalScope} [context] 114 + * @param {MessagePort | Worker | MessengerRealm} [context] 134 115 */ 135 116 export function listen( 136 117 name, 137 118 fn, 138 - context = /** @type {WorkerGlobalScope} */ (globalThis), 119 + context = /** @type {MessengerRealm} */ (globalThis), 139 120 ) { 140 121 const c = /** @type {any} */ (context); 141 122 142 - if (!c.incoming) { 123 + if (!c.__incoming) { 143 124 context.addEventListener("message", incomingAnnouncementsHandler(context)); 144 - c.incoming = {}; 125 + c.__incoming = {}; 145 126 } 146 127 147 - c.incoming[name] = debounceMicrotask(fn, { updateArguments: true }); 128 + c.__incoming[name] = debounceMicrotask(fn, { updateArguments: true }); 148 129 } 149 130 150 131 //////////////////////////////////////////// ··· 152 133 //////////////////////////////////////////// 153 134 154 135 /** 155 - * @template {(...args: any[]) => any} Fn 156 - * @param {string} name 157 - * @param {Fn} fn 158 - * @param {MessagePort | Worker | WorkerGlobalScope} [context] Uses `globalThis` by default. 136 + * @template {Record<string, (...args: any[]) => any>} Actions 137 + * @param {MessagePort | Worker | MessengerRealm} context 138 + * @param {Actions} actions 159 139 */ 160 - export function define( 161 - name, 162 - fn, 163 - context = /** @type {WorkerGlobalScope} */ (globalThis), 164 - ) { 165 - const rpc = MRpc.ensureMRpc(context); 166 - return rpc.defineLocalFn(name, fn); 140 + export function rpc(context, actions) { 141 + const io = new BrowserPostMessageIo(() => context); 142 + 143 + /** @type {undefined | RPCChannel<Actions, {}>} */ 144 + return new RPCChannel(io, { enableTransfer: true, expose: actions }); 167 145 } 168 146 169 147 /** 170 - * @template {(...args: I[]) => O} Fn 171 - * @template I 172 - * @template O 173 - * @param {string} name 174 - * @param {MessagePort | Worker | WorkerGlobalScope} [context] Uses `globalThis` by default. 175 - * @param {MRpcCallOptions} [options] 148 + * @template {Record<string, (...args: any[]) => any>} Actions 149 + * @param {() => MessagePort | Worker} workerLinkCreator 150 + * @returns {ProxiedActions<Actions>} 176 151 */ 177 - export function use( 178 - name, 179 - context = /** @type {WorkerGlobalScope} */ (globalThis), 180 - options, 181 - ) { 182 - const rpc = MRpc.ensureMRpc(context); 183 - const _fn = rpc.useRemoteFn(name, { timeout: 60000, ...(options || {}) }); 152 + export function workerProxy(workerLinkCreator) { 153 + const io = new BrowserPostMessageIo(workerLinkCreator); 184 154 185 - const fn = /** @type {Fn} */ (async (...args) => { 186 - try { 187 - return await _fn(...args); 188 - } catch (err) { 189 - if ( 190 - err instanceof Error && 191 - err.message === 192 - `The remote threw an error when calling the function "${name}".` 193 - ) { 194 - err.message = `The worker function "${name}" throws an error.`; 195 - } 196 - throw err; 197 - } 198 - }); 155 + /** @type {undefined | RPCChannel<{}, ProxiedActions<Actions>>} */ 156 + const rpc = new RPCChannel(io, { enableTransfer: true }); 199 157 200 - return fn; 158 + /** @type {ProxiedActions<Actions>} */ 159 + const api = rpc.getAPI(); 160 + return api; 201 161 } 202 162 203 163 //////////////////////////////////////////// ··· 224 184 } 225 185 226 186 /** 227 - * Process incoming announcements. 228 - */ 229 - const flushIncomingAnnouncements = debounceMicrotask( 230 - /** 231 - * @param {MessagePort | Worker | WorkerGlobalScope} [context] Uses `globalThis` by default. 232 - */ 233 - (context = /** @type {WorkerGlobalScope} */ (globalThis)) => { 234 - /** @type {Announcement<any>[]} */ 235 - const arr = []; 236 - 237 - for (const a of incoming.drain()) { 238 - arr.push(a); 239 - } 240 - 241 - batch(() => { 242 - const c = /** @type {any} */ (context); 243 - 244 - arr.forEach((announcement) => { 245 - c.incoming[announcement.name]?.(announcement.args); 246 - }); 247 - }); 248 - }, 249 - ); 250 - 251 - /** 252 - * Process outgoing announcements. 253 - */ 254 - const flushOutgoingAnnouncements = debounceMicrotask( 255 - /** 256 - * @param {MessagePort | Worker | WorkerGlobalScope} [context] Uses `globalThis` by default. 257 - */ 258 - (context = /** @type {WorkerGlobalScope} */ (globalThis)) => { 259 - /** @type {Announcement<any>[]} */ 260 - const arr = []; 261 - 262 - for (const a of outgoing.drain()) { 263 - arr.push(a); 264 - } 265 - 266 - const transferables = getTransferables(arr); 267 - context.postMessage(arr, { transfer: transferables }); 268 - }, 269 - ); 270 - 271 - /** 272 - * @type {Queue<Announcement<any>>} 273 - */ 274 - const incoming = new Queue(); 275 - 276 - /** 277 - * @param {MessagePort | Worker | WorkerGlobalScope} context 187 + * @param {MessagePort | Worker | MessengerRealm} context 278 188 */ 279 189 function incomingAnnouncementsHandler(context) { 280 190 /** @param {any} event */ 281 191 return (event) => { 282 - const arr = /** @type {Announcement<any>[]} */ (event.data); 283 - 284 - if (Array.isArray(arr)) { 285 - arr.forEach((announcement) => { 286 - const { ns, type } = announcement; 287 - if (ns !== ANNOUNCEMENT || type !== ANNOUNCEMENT) return; 288 - incoming.enqueue(announcement); 289 - flushIncomingAnnouncements(context); 290 - }); 291 - } 192 + const { ns, type } = event.data; 193 + if (ns !== ANNOUNCEMENT || type !== ANNOUNCEMENT) return; 194 + const announcement = /** @type {Announcement<any>} */ (event.data); 195 + const c = /** @type {any} */ (context); 196 + c.__incoming[announcement.name]?.(announcement.args); 292 197 }; 293 198 } 294 - 295 - /** 296 - * @type {Queue<Announcement<any>>} 297 - */ 298 - const outgoing = new Queue();
+142
src/common/worker/rpc.js
··· 1 + /** 2 + * @import { IoCapabilities, IoInterface, IoMessage, WireEnvelope } from "@kunkun/kkrpc" 3 + * 4 + * @import { MessengerRealm } from "../worker.d.ts" 5 + */ 6 + 7 + const DESTROY_SIGNAL = "__DESTROY__"; 8 + 9 + /** 10 + * @implements {IoInterface} 11 + */ 12 + export 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 + }
+104 -116
src/components/configurator/input/worker.js
··· 2 2 3 3 import { groupTracksPerScheme } from "@common/index.js"; 4 4 import { connectionsFromQuery } from "../common.js"; 5 - import { use } from "@common/worker.js"; 6 5 7 6 /** 8 7 * @import {Track} from "@definitions/types.d.ts"; ··· 13 12 // ⚡️ 14 13 //////////////////////////////////////////// 15 14 16 - const connections = connectionsFromQuery(location); 17 - 18 - /** 19 - * @param {string} scheme 20 - * @param {string} actionName 21 - */ 22 - function proxy(scheme, actionName) { 23 - const worker = connections[scheme]; 24 - const proxyFn = use(actionName, worker); 25 - 26 - return proxyFn; 27 - } 15 + // const connections = connectionsFromQuery(location); 28 16 29 - /** 30 - * @param {string} scheme 31 - */ 32 - function isSupportedScheme(scheme) { 33 - return !!connections[scheme]; 34 - } 17 + // /** 18 + // * @param {string} scheme 19 + // */ 20 + // function isSupportedScheme(scheme) { 21 + // return !!connections[scheme]; 22 + // } 35 23 36 24 //////////////////////////////////////////// 37 25 // ACTIONS 38 26 //////////////////////////////////////////// 39 27 40 - /** 41 - * @type {Actions['consult']} 42 - */ 43 - export async function consult(fileUriOrScheme) { 44 - const scheme = fileUriOrScheme.includes(":") 45 - ? URI.parse(fileUriOrScheme).scheme || fileUriOrScheme 46 - : fileUriOrScheme; 28 + // /** 29 + // * @type {Actions['consult']} 30 + // */ 31 + // export async function consult(fileUriOrScheme) { 32 + // const scheme = fileUriOrScheme.includes(":") 33 + // ? URI.parse(fileUriOrScheme).scheme || fileUriOrScheme 34 + // : fileUriOrScheme; 47 35 48 - if (!isSupportedScheme(scheme)) { 49 - return { supported: false, reason: "Unsupported scheme" }; 50 - } 36 + // if (!isSupportedScheme(scheme)) { 37 + // return { supported: false, reason: "Unsupported scheme" }; 38 + // } 51 39 52 - return await proxy(scheme, "consult")(fileUriOrScheme); 53 - } 40 + // return await proxy(scheme, "consult")(fileUriOrScheme); 41 + // } 54 42 55 - /** 56 - * @type {Actions['contextualize']} 57 - */ 58 - export async function contextualize(tracks) { 59 - const groups = groupTracks(tracks); 60 - const promises = Object.entries(groups).map( 61 - async ([scheme, tracksGroup]) => { 62 - if (!isSupportedScheme(scheme) || tracksGroup.length === 0) return; 63 - return await proxy(scheme, "contextualize")(tracksGroup); 64 - }, 65 - ); 43 + // /** 44 + // * @type {Actions['contextualize']} 45 + // */ 46 + // export async function contextualize(tracks) { 47 + // const groups = groupTracks(tracks); 48 + // const promises = Object.entries(groups).map( 49 + // async ([scheme, tracksGroup]) => { 50 + // if (!isSupportedScheme(scheme) || tracksGroup.length === 0) return; 51 + // return await proxy(scheme, "contextualize")(tracksGroup); 52 + // }, 53 + // ); 66 54 67 - await Promise.all(promises); 68 - } 55 + // await Promise.all(promises); 56 + // } 69 57 70 - /** 71 - * @type {Actions['groupConsult']} 72 - */ 73 - export async function groupConsult(tracks) { 74 - const groups = groupTracksPerScheme(tracks); 58 + // /** 59 + // * @type {Actions['groupConsult']} 60 + // */ 61 + // export async function groupConsult(tracks) { 62 + // const groups = groupTracksPerScheme(tracks); 75 63 76 - /** @type {GroupConsult[]} */ 77 - const consultations = await Promise.all( 78 - Object.keys(groups).map(async (scheme) => { 79 - if (!isSupportedScheme(scheme)) { 80 - return { 81 - [scheme]: { 82 - available: false, 83 - reason: "Unsupported scheme", 84 - tracks: groups[scheme] || [], 85 - }, 86 - }; 87 - } 64 + // /** @type {GroupConsult[]} */ 65 + // const consultations = await Promise.all( 66 + // Object.keys(groups).map(async (scheme) => { 67 + // if (!isSupportedScheme(scheme)) { 68 + // return { 69 + // [scheme]: { 70 + // available: false, 71 + // reason: "Unsupported scheme", 72 + // tracks: groups[scheme] || [], 73 + // }, 74 + // }; 75 + // } 88 76 89 - return await proxy(scheme, "groupConsult")(groups[scheme] || {}); 90 - }), 91 - ); 77 + // return await proxy(scheme, "groupConsult")(groups[scheme] || {}); 78 + // }), 79 + // ); 92 80 93 - return consultations.reduce((acc, c) => { 94 - return { ...acc, ...c }; 95 - }, {}); 96 - } 81 + // return consultations.reduce((acc, c) => { 82 + // return { ...acc, ...c }; 83 + // }, {}); 84 + // } 97 85 98 - /** 99 - * @type {Actions['list']} 100 - */ 101 - export async function list(cachedTracks = []) { 102 - const groups = await groupConsult(cachedTracks); 86 + // /** 87 + // * @type {Actions['list']} 88 + // */ 89 + // export async function list(cachedTracks = []) { 90 + // const groups = await groupConsult(cachedTracks); 103 91 104 - Object.keys(connections).forEach((scheme) => { 105 - if (!groups[scheme]) groups[scheme] = { available: true, tracks: [] }; 106 - }); 92 + // Object.keys(connections).forEach((scheme) => { 93 + // if (!groups[scheme]) groups[scheme] = { available: true, tracks: [] }; 94 + // }); 107 95 108 - const promises = Object.entries(groups).map( 109 - async ([scheme, { available, tracks }]) => { 110 - if (!available) return tracks; 111 - if (!isSupportedScheme(scheme)) return tracks; 112 - return await proxy(scheme, "list")(tracks); 113 - }, 114 - ); 96 + // const promises = Object.entries(groups).map( 97 + // async ([scheme, { available, tracks }]) => { 98 + // if (!available) return tracks; 99 + // if (!isSupportedScheme(scheme)) return tracks; 100 + // return await proxy(scheme, "list")(tracks); 101 + // }, 102 + // ); 115 103 116 - const nested = await Promise.all(promises); 117 - const tracks = nested.flat(1); 104 + // const nested = await Promise.all(promises); 105 + // const tracks = nested.flat(1); 118 106 119 - return tracks; 120 - } 107 + // return tracks; 108 + // } 121 109 122 - /** 123 - * @type {Actions['resolve']} 124 - */ 125 - export async function resolve(args) { 126 - const scheme = args.uri.split(":", 1)[0]; 127 - if (!isSupportedScheme(scheme)) return undefined; 110 + // /** 111 + // * @type {Actions['resolve']} 112 + // */ 113 + // export async function resolve(args) { 114 + // const scheme = args.uri.split(":", 1)[0]; 115 + // if (!isSupportedScheme(scheme)) return undefined; 128 116 129 - try { 130 - return await proxy(scheme, "resolve")(args); 131 - } catch (err) { 132 - console.error( 133 - `[configurator/input] Resolve error for scheme '${scheme}'.`, 134 - err, 135 - ); 136 - } 137 - } 117 + // try { 118 + // return await proxy(scheme, "resolve")(args); 119 + // } catch (err) { 120 + // console.error( 121 + // `[configurator/input] Resolve error for scheme '${scheme}'.`, 122 + // err, 123 + // ); 124 + // } 125 + // } 138 126 139 127 //////////////////////////////////////////// 140 128 // 🛠️ 141 129 //////////////////////////////////////////// 142 130 143 - /** 144 - * @param {Track[]} tracks 145 - */ 146 - function groupTracks(tracks) { 147 - const grouped = groupTracksPerScheme( 148 - tracks, 149 - Object.fromEntries( 150 - Object.entries(connections).map(([k, _v]) => { 151 - return [k, []]; 152 - }), 153 - ), 154 - ); 131 + // /** 132 + // * @param {Track[]} tracks 133 + // */ 134 + // function groupTracks(tracks) { 135 + // const grouped = groupTracksPerScheme( 136 + // tracks, 137 + // Object.fromEntries( 138 + // Object.entries(connections).map(([k, _v]) => { 139 + // return [k, []]; 140 + // }), 141 + // ), 142 + // ); 155 143 156 - return grouped; 157 - } 144 + // return grouped; 145 + // }
+37 -31
src/components/engine/audio/element.js
··· 20 20 * @implements {Actions} 21 21 */ 22 22 class AudioEngine extends BroadcastableDiffuseElement { 23 - #VOLUME_KEY; 24 - 25 - constructor() { 26 - super(); 27 - 28 - // Setup leader election if shared 29 - if (this.hasAttribute("group")) { 30 - const fn = this.broadcast(`diffuse/engine/audio/${this.group}`); 31 - 32 - this.pause = fn("pause", this.pause).leaderOnly; 33 - this.play = fn("play", this.play).leaderOnly; 34 - this.reload = fn("reload", this.reload).leaderOnly; 35 - this.seek = fn("seek", this.seek).leaderOnly; 36 - this.supply = fn("supply", this.supply).replicate; 37 - 38 - this.$isPlaying.set = fn("isPlaying", this.$isPlaying.set).replicate; 39 - } 40 - 41 - // Get volume from previous session if possible 42 - this.#VOLUME_KEY = `@components/engine/audio/${this.group}/volume`; 43 - const volume = localStorage.getItem(this.#VOLUME_KEY); 44 - 45 - this.#volume = signal(volume ? parseInt(volume) : 0.5); 46 - this.volume = this.#volume.get; 47 - } 48 - 49 23 // SIGNALS 50 24 51 25 #items = signal(/** @type {Audio[]} */ ([])); 52 - #volume; 26 + #volume = signal(0.5); 53 27 54 28 $hasEnded = signal(false); 55 29 $isPlaying = signal(false); ··· 59 33 hasEnded = this.$hasEnded.get; 60 34 isPlaying = this.$isPlaying.get; 61 35 items = this.#items.get; 36 + volume = this.#volume.get; 62 37 63 38 // LIFECYCLE 64 39 ··· 66 41 * @override 67 42 */ 68 43 connectedCallback() { 44 + // Setup leader election if shared 45 + if (this.hasAttribute("group")) { 46 + const actions = this.broadcast(`diffuse/engine/audio/${this.group}`, { 47 + adjustVolume: { strategy: "leaderOnly", fn: this.adjustVolume }, 48 + pause: { strategy: "leaderOnly", fn: this.pause }, 49 + play: { strategy: "leaderOnly", fn: this.play }, 50 + seek: { strategy: "leaderOnly", fn: this.seek }, 51 + supply: { strategy: "replicate", fn: this.supply }, 52 + 53 + setIsPlaying: { strategy: "replicate", fn: this.$isPlaying.set }, 54 + }); 55 + 56 + if (actions) { 57 + this.adjustVolume = actions.adjustVolume; 58 + this.pause = actions.pause; 59 + this.play = actions.play; 60 + this.seek = actions.seek; 61 + this.supply = actions.supply; 62 + 63 + this.$isPlaying.set = actions.setIsPlaying; 64 + } 65 + } 66 + 67 + // Super 69 68 super.connectedCallback(); 70 69 70 + // Get volume from previous session if possible 71 + const VOLUME_KEY = `diffuse/engine/audio/${this.group}/volume`; 72 + const volume = localStorage.getItem(VOLUME_KEY); 73 + 74 + if (volume != undefined) { 75 + this.#volume.set(parseFloat(volume)); 76 + } 77 + 71 78 // Manage playback across tabs if needed 72 79 if (this.broadcasted) { 73 80 this.effect(async () => { 74 81 const status = await this.broadcastingStatus(); 75 82 if (status.leader && status.initialLeader === false) { 76 - // TODO: 77 - // console.log("🧙 Leadership acquired"); 83 + console.log("🧙 Leadership acquired (no actions performed)"); 78 84 } 79 85 }); 80 86 } ··· 89 95 }, 90 96 ); 91 97 92 - localStorage.setItem(this.#VOLUME_KEY, this.#volume.value.toString()); 98 + localStorage.setItem(VOLUME_KEY, this.#volume.value.toString()); 93 99 }); 94 100 } 95 101 ··· 217 223 218 224 return html` 219 225 <section id="audio-nodes"> 220 - ${nodes.join("")} 226 + ${nodes} 221 227 </section> 222 228 `; 223 229 }
+37 -46
src/components/engine/queue/element.js
··· 1 - import QS from "query-string"; 2 - 3 1 import { DiffuseElement } from "@common/element.js"; 4 2 import { signal } from "@common/signal.js"; 5 - import { listen, proxyProvider, use } from "@common/worker.js"; 3 + import { listen, workerProxy } from "@common/worker.js"; 6 4 import { hash } from "@common/index.js"; 7 5 8 6 /** 9 - * @import {ProxiedActions, ProxyProvider} from "@common/worker.d.ts"; 10 - * @import {Actions, Item} from "./types.d.ts" 7 + * @import {ProxiedActions} from "@common/worker.d.ts"; 8 + * @import {Actions, Item, State} from "./types.d.ts" 11 9 */ 12 10 13 11 //////////////////////////////////////////// ··· 18 16 * @implements {ProxiedActions<Actions>} 19 17 */ 20 18 class QueueEngine extends DiffuseElement { 19 + static NAME = "diffuse/engine/queue"; 20 + static WORKER_URL = "components/engine/queue/worker.js"; 21 + 21 22 constructor() { 22 23 super(); 23 24 24 - // Query 25 - const query = QS.stringify({ 26 - "fill": this.getAttribute("fill"), 27 - }); 28 - 29 - // Setup worker 30 - const name = `diffuse/engine/queue/${this.group}`; 31 - const url = `/components/engine/queue/worker.js?${query}`; 32 - 33 - let port; 25 + /** @type {ProxiedActions<Actions & State>} */ 26 + this.proxy = workerProxy(this.workerLink); 34 27 35 - if (this.hasAttribute("group")) { 36 - const worker = new SharedWorker(url, { name, type: "module" }); 37 - port = worker.port; 38 - port.start(); 39 - } else { 40 - const worker = new Worker(url, { name, type: "module" }); 41 - port = worker; 42 - } 43 - 44 - // Sync data with worker 45 - listen("future", this.#future.set, port); 46 - listen("now", this.#now.set, port); 47 - listen("past", this.#past.set, port); 48 - listen("poolHash", this.#poolHash.set, port); 49 - 50 - use("future", port)().then(this.#future.set); 51 - use("now", port)().then(this.#now.set); 52 - use("past", port)().then(this.#past.set); 53 - use("poolHash", port)().then(this.#poolHash.set); 54 - 55 - /** @type {ProxyProvider<Actions>} */ 56 - const proxy = proxyProvider(["add", "fill", "pool", "shift", "unshift"]); 57 - 58 - // Worker proxy 59 - const w = proxy(port); 60 - 61 - this.add = w.add; 62 - this.fill = w.fill; 63 - this.pool = w.pool; 64 - this.shift = w.shift; 65 - this.unshift = w.unshift; 28 + this.add = this.proxy.add; 29 + this.fill = this.proxy.fill; 30 + this.pool = this.proxy.pool; 31 + this.shift = this.proxy.shift; 32 + this.unshift = this.proxy.unshift; 66 33 } 67 34 68 35 // SIGNALS ··· 78 45 now = this.#now.get; 79 46 past = this.#past.get; 80 47 poolHash = this.#poolHash.get; 48 + 49 + // LIFECYCLE 50 + 51 + /** 52 + * @override 53 + */ 54 + connectedCallback() { 55 + super.connectedCallback(); 56 + 57 + // Sync data with worker 58 + const link = this.workerLink(); 59 + 60 + // Listen for remote data changes 61 + listen("future", this.#future.set, link); 62 + listen("now", this.#now.set, link); 63 + listen("past", this.#past.set, link); 64 + listen("poolHash", this.#poolHash.set, link); 65 + 66 + // Fetch current data state 67 + this.proxy.future().then(this.#future.set); 68 + this.proxy.now().then(this.#now.set); 69 + this.proxy.past().then(this.#past.set); 70 + this.proxy.poolHash().then(this.#poolHash.set); 71 + } 81 72 } 82 73 83 74 export default QueueEngine;
+1
src/components/engine/queue/types.d.ts
··· 24 24 future: SignalReader<Item[]>; 25 25 now: SignalReader<Item | null>; 26 26 past: SignalReader<Item[]>; 27 + poolHash: SignalReader<string>; 27 28 };
+20 -18
src/components/engine/queue/worker.js
··· 1 - import { announce, define, ostiary } from "@common/worker.js"; 1 + import { announce, ostiary, rpc } from "@common/worker.js"; 2 2 import { effect, signal } from "@common/signal.js"; 3 3 import { arrayShuffle, hash } from "@common/index.js"; 4 4 ··· 83 83 // ⚡️ 84 84 //////////////////////////////////////////// 85 85 86 - ostiary((port) => { 86 + ostiary((context, _firstConnection, _connectionId) => { 87 87 // Setup RPC 88 88 89 - define("future", $future.get, port); 90 - define("now", $now.get, port); 91 - define("past", $past.get, port); 92 - define("poolHash", $poolHash.get, port); 93 - 94 - define("add", add, port); 95 - define("fill", fill, port); 96 - define("pool", pool, port); 97 - define("shift", shift, port); 98 - define("unshift", unshift, port); 99 - 100 - // Communicate state 89 + rpc(context, { 90 + add, 91 + fill, 92 + pool, 93 + shift, 94 + unshift, 101 95 102 - effect(() => announce("future", $future.value, port)); 103 - effect(() => announce("now", $now.value, port)); 104 - effect(() => announce("past", $past.value, port)); 105 - effect(() => announce("poolHash", $poolHash.value, port)); 96 + // State 97 + future: $future.get, 98 + now: $now.get, 99 + past: $past.get, 100 + poolHash: $poolHash.get, 101 + }); 106 102 107 103 // Effects 104 + 105 + // Communicate state 106 + effect(() => announce("future", $future.value, context)); 107 + effect(() => announce("now", $now.value, context)); 108 + effect(() => announce("past", $past.value, context)); 109 + effect(() => announce("poolHash", $poolHash.value, context)); 108 110 109 111 // When the pool changes, 110 112 // make sure all future queue items still exist.
-12
src/components/input/constants.js
··· 1 - /** 2 - * @import {InputActions} from "./types.d.ts" 3 - */ 4 - 5 - /** @type {Array<keyof InputActions>} */ 6 - export const INPUT_ACTIONS = [ 7 - "consult", 8 - "contextualize", 9 - "groupConsult", 10 - "list", 11 - "resolve", 12 - ];
+13 -35
src/components/input/opensubsonic/element.js
··· 1 1 import { DiffuseElement } from "@common/element.js"; 2 - import { portProvider, proxyProvider } from "@common/worker.js"; 2 + import { portProvider, workerProxy } from "@common/worker.js"; 3 3 4 4 /** 5 5 * @import {InputActions} from "@components/input/types.d.ts" ··· 13 13 /** 14 14 * @implements {ProxiedActions<InputActions>} 15 15 * @implements {PortProviderMethod} 16 - * @implements {ProxyProviderMethod<InputActions>} 17 16 */ 18 17 class OpensubsonicInput extends DiffuseElement { 18 + static NAME = "diffuse/input/opensubsonic"; 19 + static WORKER_URL = "components/input/opensubsonic/worker.js"; 20 + 19 21 constructor() { 20 22 super(); 21 23 22 - // Setup worker 23 - const worker = this.worker(this.group); 24 - 25 - /** @type {ProxyProvider<InputActions>} */ 26 - this.proxy = proxyProvider([ 27 - "consult", 28 - "contextualize", 29 - "groupConsult", 30 - "list", 31 - "resolve", 32 - ]); 33 - 34 - // Worker proxy 35 - const w = this.proxy(worker); 36 - 37 - this.consult = w.consult; 38 - this.contextualize = w.contextualize; 39 - this.groupConsult = w.groupConsult; 40 - this.list = w.list; 41 - this.resolve = w.resolve; 42 - 43 - // Provide a channel to the worker 44 - this.port = portProvider(worker); 45 - } 24 + /** @type {ProxiedActions<InputActions>} */ 25 + const p = workerProxy(this.workerLink); 46 26 47 - /** 48 - * @param {string} [group] 49 - */ 50 - worker(group) { 51 - const name = `diffuse/input/opensubsonic/${group || crypto.randomUUID()}`; 52 - const url = import.meta.resolve( 53 - "./components/input/opensubsonic/worker.js", 54 - ); 27 + this.consult = p.consult; 28 + this.contextualize = p.contextualize; 29 + this.groupConsult = p.groupConsult; 30 + this.list = p.list; 31 + this.resolve = p.resolve; 55 32 56 - return new Worker(url, { name, type: "module" }); 33 + // Provide a channel to a worker 34 + this.port = portProvider(this.workerLink); 57 35 } 58 36 } 59 37
+12 -9
src/components/input/opensubsonic/worker.js
··· 1 1 import * as URI from "uri-js"; 2 2 3 3 import { effect, signal } from "@common/signal.js"; 4 - import { announce, define, ostiary } from "@common/worker.js"; 4 + import { announce, ostiary, rpc } from "@common/worker.js"; 5 5 6 6 import { SCHEME } from "./constants.js"; 7 7 import { ··· 275 275 // ⚡️ 276 276 //////////////////////////////////////////// 277 277 278 - ostiary((port) => { 278 + ostiary((context) => { 279 279 // Setup RPC 280 280 281 - define("servers", $servers.get, port); 281 + rpc(context, { 282 + consult, 283 + contextualize, 284 + groupConsult, 285 + list, 286 + resolve, 282 287 283 - define("consult", consult, port); 284 - define("contextualize", contextualize, port); 285 - define("groupConsult", groupConsult, port); 286 - define("list", list, port); 287 - define("resolve", resolve, port); 288 + // State 289 + servers: $servers.get, 290 + }); 288 291 289 292 // Communicate state 290 293 291 - effect(() => announce("servers", $servers.value, port)); 294 + effect(() => announce("servers", $servers.value, context)); 292 295 });
+13 -37
src/components/input/s3/element.js
··· 1 1 import { DiffuseElement } from "@common/element.js"; 2 - import { portProvider, proxyProvider } from "@common/worker.js"; 2 + import { portProvider, workerProxy } from "@common/worker.js"; 3 3 4 4 /** 5 5 * @import {InputActions} from "@components/input/types.d.ts" ··· 13 13 /** 14 14 * @implements {ProxiedActions<InputActions>} 15 15 * @implements {PortProviderMethod} 16 - * @implements {ProxyProviderMethod<InputActions>} 17 16 */ 18 17 class S3Input extends DiffuseElement { 18 + static NAME = "diffuse/input/s3"; 19 + static WORKER_URL = "components/input/s3/worker.js"; 20 + 19 21 constructor() { 20 22 super(); 21 23 22 - // Setup worker 23 - const worker = this.worker(this.group); 24 + /** @type {ProxiedActions<InputActions & { demo: () => Promise<void> }>} */ 25 + const p = workerProxy(this.workerLink); 24 26 25 - /** @type {ProxyProvider<InputActions & { demo: () => Promise<void> }>} */ 26 - this.proxy = proxyProvider([ 27 - "consult", 28 - "contextualize", 29 - "groupConsult", 30 - "list", 31 - "resolve", 32 - 33 - "demo", 34 - ]); 35 - 36 - // Worker proxy 37 - const w = this.proxy(worker); 38 - 39 - this.consult = w.consult; 40 - this.contextualize = w.contextualize; 41 - this.groupConsult = w.groupConsult; 42 - this.list = w.list; 43 - this.resolve = w.resolve; 27 + this.consult = p.consult; 28 + this.contextualize = p.contextualize; 29 + this.groupConsult = p.groupConsult; 30 + this.list = p.list; 31 + this.resolve = p.resolve; 44 32 45 - this.demo = w.demo; 33 + this.demo = p.demo; 46 34 47 35 // Provide a channel to the worker 48 - this.port = portProvider(worker); 49 - } 50 - 51 - /** 52 - * @param {string} [group] 53 - */ 54 - worker(group) { 55 - const name = `diffuse/input/s3/${group || crypto.randomUUID()}`; 56 - const url = import.meta.resolve( 57 - "./components/input/s3/worker.js", 58 - ); 59 - 60 - return new Worker(url, { name, type: "module" }); 36 + this.port = portProvider(this.workerLink); 61 37 } 62 38 } 63 39
+14 -10
src/components/input/s3/worker.js
··· 10 10 parseURI, 11 11 } from "./common.js"; 12 12 import { SCHEME } from "./constants.js"; 13 - import { announce, define, ostiary } from "@common/worker.js"; 13 + import { announce, ostiary, rpc } from "@common/worker.js"; 14 14 import { effect, signal } from "@common/signal.js"; 15 15 16 16 import { saveBuckets } from "./common.js"; ··· 213 213 // ⚡️ 214 214 //////////////////////////////////////////// 215 215 216 - ostiary((port) => { 216 + ostiary((context) => { 217 217 // Setup RPC 218 218 219 - define("buckets", $buckets.get, port); 219 + rpc(context, { 220 + consult, 221 + contextualize, 222 + groupConsult, 223 + list, 224 + resolve, 220 225 221 - define("consult", consult, port); 222 - define("contextualize", contextualize, port); 223 - define("groupConsult", groupConsult, port); 224 - define("list", list, port); 225 - define("resolve", resolve, port); 226 + // Additional actions 227 + demo, 226 228 227 - define("demo", demo, port); 229 + // State 230 + buckets: $buckets.get, 231 + }); 228 232 229 233 // Communicate state 230 234 231 - effect(() => announce("buckets", $buckets.value, port)); 235 + effect(() => announce("buckets", $buckets.value, context)); 232 236 });
+3 -4
src/components/input/types.d.ts
··· 5 5 } from "@common/worker.d.ts"; 6 6 7 7 import type { Track } from "@definitions/types.d.ts"; 8 + import { DiffuseElement } from "@common/element.js"; 8 9 9 10 /** 10 11 * Consultation. ··· 33 34 }; 34 35 35 36 export type InputElement = 36 - & HTMLElement 37 - & WorkerProviderMethod 38 - & ProxiedActions<InputActions> 39 - & ProxyProviderMethod<InputActions>; 37 + & DiffuseElement 38 + & ProxiedActions<InputActions>; 40 39 41 40 export type ResolvedUri = undefined | { 42 41 stream: ReadableStream;
+18 -18
src/components/orchestrator/process-tracks/element.js
··· 1 1 import { DiffuseElement, query } from "@common/element.js"; 2 2 import { signal, untracked } from "@common/signal.js"; 3 - import { getTransferables, portProvider, use } from "@common/worker.js"; 3 + import { portProvider, transfer, workerProxy } from "@common/worker.js"; 4 4 5 5 /** 6 6 * @import {Track} from "@definitions/types.d.ts" 7 + * @import {ProxiedActions} from "@common/worker.d.ts" 7 8 * @import {InputElement} from "@components/input/types.d.ts" 8 9 * @import {OutputElement} from "@components/output/types.d.ts" 10 + * 11 + * @import {Actions} from "./types.d.ts" 9 12 */ 10 13 11 14 //////////////////////////////////////////// ··· 21 24 #external; 22 25 #process; 23 26 27 + static NAME = "diffuse/orchestrator/process-tracks"; 28 + static WORKER_URL = "components/orchestrator/process-tracks/worker.js"; 29 + 24 30 constructor() { 25 31 super(); 26 32 27 - // Setup worker 28 - const name = `diffuse/orchestrator/process-tracks/${this.group}`; 29 - const url = import.meta.resolve( 30 - "./components/orchestrator/process-tracks/worker.js", 31 - ); 32 - 33 - const worker = new Worker(url, { name, type: "module" }); 34 - 35 33 /** @type {InputElement} */ 36 34 this.input = query(this, "input-selector"); 37 35 ··· 46 44 customElements.whenDefined(this.input.localName), 47 45 customElements.whenDefined(this.metadataProcessor.localName), 48 46 ]).then(() => ({ 49 - input: portProvider(this.input.worker()), 50 - metadataProcessor: portProvider(this.metadataProcessor.worker()), 47 + input: portProvider(this.input.workerLink), 48 + metadataProcessor: portProvider(this.metadataProcessor.workerLink), 51 49 })); 52 50 53 - // Worker proxy 54 - this.#process = use("process", worker, { 55 - timeout: 60000 * 60 * 2, // 2 hours 56 - transfer: getTransferables, 57 - }); 51 + /** @type {ProxiedActions<Actions>} */ 52 + const p = workerProxy(this.workerLink); 53 + 54 + this.#process = p.process; 58 55 } 59 56 60 57 // SIGNALS ··· 103 100 }; 104 101 105 102 // Send everything to worker 106 - const result = await this.#process({ 103 + const result = await this.#process(transfer({ 107 104 ports: { 108 105 input: ports.input.port, 109 106 metadataProcessor: ports.metadataProcessor.port, 110 107 }, 111 108 tracks: cachedTracks, 112 - }); 109 + }, [ 110 + ports.input.port, 111 + ports.metadataProcessor.port, 112 + ])); 113 113 114 114 // Save if collection changed 115 115 if (result) await this.output.tracks.save(result);
+8 -11
src/components/orchestrator/process-tracks/worker.js
··· 1 1 import deepDiff from "@fry69/deep-diff"; 2 2 3 - import { define, ostiary, proxyProvider } from "@common/worker.js"; 4 - import { INPUT_ACTIONS } from "@components/input/constants.js"; 3 + import { ostiary, rpc, workerProxy } from "@common/worker.js"; 5 4 6 5 /** 7 6 * @import {Track} from "@definitions/types.d.ts" 8 - * @import {ProxyProvider} from "@common/worker.d.ts" 7 + * @import {ProxiedActions} from "@common/worker.d.ts" 9 8 * @import {InputActions} from "@components/input/types.d.ts" 10 9 * @import {Actions as MetadataProcessorActions} from "@components/processor/metadata/types.d.ts" 11 10 * @import {Actions} from "./types.d.ts" ··· 22 21 const { ports } = args; 23 22 const cachedTracks = args.tracks; 24 23 25 - /** @type {ProxyProvider<InputActions>} */ 26 - const inputProvider = proxyProvider(INPUT_ACTIONS); 27 - const input = inputProvider(ports.input); 24 + /** @type {ProxiedActions<InputActions>} */ 25 + const input = workerProxy(() => ports.input); 28 26 29 - /** @type {ProxyProvider<MetadataProcessorActions>} */ 30 - const metadataProcessorProvider = proxyProvider(["supply"]); 31 - const metadataProcessor = metadataProcessorProvider(ports.metadataProcessor); 27 + /** @type {ProxiedActions<MetadataProcessorActions>} */ 28 + const metadataProcessor = workerProxy(() => ports.metadataProcessor); 32 29 33 30 ports.input.start(); 34 31 ports.metadataProcessor.start(); ··· 90 87 // ⚡️ 91 88 //////////////////////////////////////////// 92 89 93 - ostiary((port) => { 94 - define("process", process, port); 90 + ostiary((context) => { 91 + rpc(context, { process }); 95 92 });
+1
src/components/orchestrator/queue-tracks/element.js
··· 39 39 super.connectedCallback(); 40 40 41 41 // When defined 42 + await customElements.whenDefined(this.input.localName); 42 43 await customElements.whenDefined(this.output.localName); 43 44 44 45 // Watch tracks collection
+10 -11
src/components/output/polymorphic/indexed-db/element.js
··· 1 1 import { DiffuseElement } from "@common/element.js"; 2 - import { use } from "@common/worker.js"; 2 + import { workerProxy } from "@common/worker.js"; 3 3 import { outputManager } from "../../common.js"; 4 4 5 5 /** 6 - * @import {OutputManager} from "../../types.d.ts" 6 + * @import {ProxiedActions, ProxyProvider} from "@common/worker.d.ts" 7 + * @import {OutputManager, OutputWorkerActions} from "../../types.d.ts" 7 8 */ 8 9 9 10 //////////////////////////////////////////// ··· 14 15 * @implements {OutputManager<any>} 15 16 */ 16 17 class IndexedDBOutput extends DiffuseElement { 18 + static NAME = "diffuse/output/polymorphic/indexed-db"; 19 + static WORKER_URL = "components/output/polymorphic/indexed-db/worker.js"; 20 + 17 21 constructor() { 18 22 super(); 19 23 20 - // Setup worker 21 - const name = `diffuse/output/polymorphic/indexed-db/${this.group}`; 22 - const url = import.meta.resolve( 23 - "./components/output/polymorphic/indexed-db/worker.js", 24 - ); 25 - 26 - const worker = new Worker(url, { name, type: "module" }); 24 + /** @type {ProxiedActions<OutputWorkerActions>} */ 25 + const p = workerProxy(this.workerLink); 27 26 28 27 // Manager 29 28 const manager = outputManager({ 30 29 tracks: { 31 30 empty: () => [], 32 - get: use("getTracks", worker), 33 - put: use("putTracks", worker), 31 + get: p.getTracks, 32 + put: p.putTracks, 34 33 }, 35 34 }); 36 35
+6 -5
src/components/output/polymorphic/indexed-db/worker.js
··· 1 1 import * as IDB from "idb-keyval"; 2 2 3 3 import { IDB_PREFIX } from "./constants.js"; 4 - import { define, ostiary } from "@common/worker.js"; 4 + import { ostiary, rpc } from "@common/worker.js"; 5 5 6 6 /** 7 7 * @import {Track} from "@definitions/types.d.ts"; ··· 31 31 // ⚡️ 32 32 //////////////////////////////////////////// 33 33 34 - ostiary((port) => { 35 - // Setup RPC 36 - define("getTracks", getTracks, port); 37 - define("putTracks", putTracks, port); 34 + ostiary((context) => { 35 + rpc(context, { 36 + getTracks, 37 + putTracks, 38 + }); 38 39 }); 39 40 40 41 ////////////////////////////////////////////
+6
src/components/output/types.d.ts
··· 1 1 import type { SignalReader } from "@common/signal.d.ts"; 2 + import type { Track } from "@definitions/types.d.ts"; 2 3 3 4 export type OutputElement<Tracks> = HTMLElement & OutputManager<Tracks>; 4 5 ··· 19 20 put(tracks: Tracks): Promise<void>; 20 21 }; 21 22 }; 23 + 24 + export type OutputWorkerActions = { 25 + getTracks(): Promise<Track[]>; 26 + putTracks(tracks: Track[]): Promise<void>; 27 + };
+10 -9
src/components/processor/artwork/element.js
··· 1 1 import { DiffuseElement } from "@common/element.js"; 2 - import { use } from "@common/worker.js"; 2 + import { workerProxy } from "@common/worker.js"; 3 3 4 4 /** 5 + * @import {ProxiedActions, ProxyProvider} from "@common/worker.d.ts" 5 6 * @import {Actions} from "./types.d.ts" 6 7 */ 7 8 ··· 10 11 //////////////////////////////////////////// 11 12 12 13 /** 13 - * @implements {Actions} 14 + * @implements {ProxiedActions<Actions>} 14 15 */ 15 16 class ArtworkProcessor extends DiffuseElement { 17 + static NAME = "diffuse/processor/artwork"; 18 + static WORKER_URL = "components/processor/artwork/worker.js"; 19 + 16 20 constructor() { 17 21 super(); 18 22 19 - // Setup worker 20 - const name = `diffuse/processor/metadata/${this.group}`; 21 - const url = new URL("./worker.js", import.meta.url); 22 - const worker = new Worker(url, { name, type: "module" }); 23 + /** @type {ProxiedActions<Actions>} */ 24 + const p = workerProxy(this.workerLink); 23 25 24 - // Worker proxy 25 - this.artwork = use("artwork", worker); 26 - this.supply = use("supply", worker); 26 + this.artwork = p.artwork; 27 + this.supply = p.supply; 27 28 } 28 29 } 29 30
+7 -21
src/components/processor/metadata/element.js
··· 1 1 import { DiffuseElement } from "@common/element.js"; 2 - import { portProvider, proxyProvider } from "@common/worker.js"; 2 + import { workerProxy } from "@common/worker.js"; 3 3 4 4 /** 5 5 * @import {PortProviderMethod, ProxiedActions, ProxyProvider, ProxyProviderMethod, WorkerProviderMethod} from "@common/worker.d.ts" ··· 12 12 13 13 /** 14 14 * @implements {ProxiedActions<Actions>} 15 - * @implements {WorkerProviderMethod} 16 - * @implements {ProxyProviderMethod<Actions>} 17 15 */ 18 16 class MetadataProcessor extends DiffuseElement { 17 + static NAME = "diffuse/processor/metadata"; 18 + static WORKER_URL = "components/processor/metadata/worker.js"; 19 + 19 20 constructor() { 20 21 super(); 21 22 22 - // Setup worker 23 - const worker = this.worker(this.group); 24 - 25 - /** @type {ProxyProvider<Actions>} */ 26 - this.proxy = proxyProvider(["supply"]); 23 + /** @type {ProxiedActions<Actions>} */ 24 + const p = workerProxy(this.workerLink); 27 25 28 26 // Worker proxy 29 - this.supply = this.proxy(worker).supply; 30 - } 31 - 32 - /** 33 - * @param {string} [group] 34 - */ 35 - worker(group) { 36 - const name = `diffuse/processor/metadata/${group || crypto.randomUUID()}`; 37 - const url = import.meta.resolve( 38 - "./components/processor/metadata/worker.js", 39 - ); 40 - 41 - return new Worker(url, { name, type: "module" }); 27 + this.supply = p.supply; 42 28 } 43 29 } 44 30
+5 -4
src/components/processor/metadata/worker.js
··· 1 - import { define, ostiary } from "@common/worker.js"; 1 + import { ostiary, rpc } from "@common/worker.js"; 2 2 import { musicMetadataTags } from "./common.js"; 3 3 4 4 /** ··· 33 33 // ⚡️ 34 34 //////////////////////////////////////////// 35 35 36 - ostiary((port) => { 37 - // Setup RPC 38 - define("supply", supply, port); 36 + ostiary((context) => { 37 + rpc(context, { 38 + supply, 39 + }); 39 40 });
+10 -21
src/components/processor/search/element.js
··· 1 1 import { DiffuseElement } from "@common/element.js"; 2 - import { use } from "@common/worker.js"; 2 + import { workerProxy } from "@common/worker.js"; 3 3 4 4 /** 5 + * @import {ProxiedActions, ProxyProvider} from "@common/worker.d.ts"; 5 6 * @import {Actions} from "./types.d.ts" 6 7 */ 7 8 ··· 10 11 //////////////////////////////////////////// 11 12 12 13 /** 13 - * @implements {Actions} 14 + * @implements {ProxiedActions<Actions>} 14 15 */ 15 16 class SearchProcessor extends DiffuseElement { 17 + static NAME = "diffuse/processor/search"; 18 + static WORKER_URL = "components/processor/search/worker.js"; 19 + 16 20 constructor() { 17 21 super(); 18 22 19 - // Setup worker 20 - const name = `diffuse/processor/search/${this.group}`; 21 - const url = import.meta.resolve( 22 - "./components/processor/search/worker.js", 23 - ); 23 + /** @type {ProxiedActions<Actions>} */ 24 + const p = workerProxy(this.workerLink); 24 25 25 - let port; 26 - 27 - if (this.hasAttribute("group")) { 28 - const worker = new SharedWorker(url, { name, type: "module" }); 29 - port = worker.port; 30 - port.start(); 31 - } else { 32 - const worker = new Worker(url, { name, type: "module" }); 33 - port = worker; 34 - } 35 - 36 - // Worker proxy 37 - this.search = use("search", port); 38 - this.supply = use("supply", port); 26 + this.search = p.search; 27 + this.supply = p.supply; 39 28 } 40 29 } 41 30
+6 -5
src/components/processor/search/worker.js
··· 3 3 // import { pluginQPS } from "@orama/plugin-qps"; 4 4 5 5 import { SCHEMA } from "./constants.js"; 6 - import { define, ostiary } from "@common/worker.js"; 6 + import { ostiary, rpc } from "@common/worker.js"; 7 7 import { signal } from "@common/signal.js"; 8 8 9 9 /** ··· 89 89 // ⚡️ 90 90 //////////////////////////////////////////// 91 91 92 - ostiary((port) => { 93 - // Setup RPC 94 - define("search", search, port); 95 - define("supply", supply, port); 92 + ostiary((context) => { 93 + rpc(context, { 94 + search, 95 + supply, 96 + }); 96 97 }); 97 98 98 99 ////////////////////////////////////////////
+48 -4
src/themes/blur/index.js
··· 1 - import "@components/engine/audio/element.js"; 2 1 import "@components/input/opensubsonic/element.js"; 3 - import "@components/orchestrator/process-tracks/element.js"; 4 - import "@components/orchestrator/queue-audio/element.js"; 5 - import "@components/orchestrator/queue-tracks/element.js"; 6 2 import "@components/processor/metadata/element.js"; 3 + import "@components/transformer/output/string/json/element.js"; 4 + import "@components/transformer/output/refiner/default/element.js"; 7 5 6 + import * as Audio from "@components/engine/audio/element.js"; 8 7 import * as Output from "@components/output/polymorphic/indexed-db/element.js"; 9 8 import * as Queue from "@components/engine/queue/element.js"; 10 9 11 10 import { component } from "@common/element.js"; 12 11 import { effect } from "@common/signal.js"; 13 12 13 + const audio = component(Audio); 14 14 const output = component(Output); 15 15 const queue = component(Queue); 16 16 17 + globalThis.audio = audio; 17 18 globalThis.output = output; 18 19 globalThis.queue = queue; 19 20 21 + // 🚀 22 + 23 + isLeader().then((bool) => { 24 + if (!bool) return; 25 + 26 + // Only load orchestrators if leader 27 + import("@components/orchestrator/process-tracks/element.js"); 28 + import("@components/orchestrator/queue-audio/element.js"); 29 + import("@components/orchestrator/queue-tracks/element.js"); 30 + }); 31 + 32 + // EFFECTS 33 + 20 34 effect(() => { 21 35 console.log("Active queue item:", queue.now()); 22 36 }); 37 + 38 + effect(() => { 39 + console.log("Queue pool hash:", queue.poolHash()); 40 + }); 41 + 42 + /** 43 + * Make sure there's always some random tracks in the queue. 44 + */ 45 + effect(() => { 46 + const trigger = queue.now(); 47 + const _other_trigger = queue.poolHash(); 48 + 49 + isLeader().then((bool) => { 50 + if (bool) { 51 + queue.fill({ amount: 10, shuffled: true }); 52 + if (!trigger) queue.shift(); 53 + } 54 + }); 55 + }); 56 + 57 + // 🛠️ 58 + 59 + async function isLeader() { 60 + if (audio.broadcasted) { 61 + const status = await audio.broadcastingStatus(); 62 + return status.leader; 63 + } else { 64 + return true; 65 + } 66 + }
+13 -8
src/themes/blur/index.vto
··· 9 9 </main> 10 10 11 11 <!-- Engines --> 12 - <de-audio></de-audio> 13 - <de-queue></de-queue> 12 + <de-audio group="shared"></de-audio> 13 + <de-queue group="shared"></de-queue> 14 14 15 15 <!-- Inputs, Outputs & Processors --> 16 - <di-opensubsonic></di-opensubsonic> 17 16 <dop-indexed-db></dop-indexed-db> 18 17 <dp-metadata></dp-metadata> 18 + 19 + <di-opensubsonic id="input"></di-opensubsonic> 20 + 21 + <!-- Transformers --> 22 + <dtor-default id="output" output-selector="dtos-json"></dtor-default> 23 + <dtos-json output-selector="dop-indexed-db">now</dtos-json> 19 24 20 25 <!-- Orchestrators --> 21 26 <do-process-tracks 22 - input-selector="di-opensubsonic" 27 + input-selector="#input" 23 28 metadata-processor-selector="dp-metadata" 24 - output-selector="dop-indexed-db" 29 + output-selector="#output" 25 30 ></do-process-tracks> 26 31 27 32 <do-queue-audio 28 - input-selector="di-opensubsonic" 33 + input-selector="#input" 29 34 audio-engine-selector="de-audio" 30 35 queue-engine-selector="de-queue" 31 36 ></do-queue-audio> 32 37 33 38 <do-queue-tracks 34 - input-selector="di-opensubsonic" 35 - output-selector="dop-indexed-db" 39 + input-selector="#input" 40 + output-selector="#output" 36 41 queue-engine-selector="de-queue" 37 42 ></do-queue-tracks> 38 43
+2 -2
src/themes/webamp/index.js
··· 5 5 import "@components/transformer/output/string/json/element.js"; 6 6 import "@components/transformer/output/refiner/default/element.js"; 7 7 8 - import * as Input from "@components/input/s3/element.js"; 8 + import * as Input from "@components/input/opensubsonic/element.js"; 9 9 import * as Queue from "@components/engine/queue/element.js"; 10 10 11 11 import { component } from "@common/element.js"; ··· 141 141 queue.fill({ amount: 10, shuffled: true }); 142 142 143 143 // Automatically insert track if there isn't any 144 - if (!queue.now) queue.shift(); 144 + if (!queue.now()) queue.shift(); 145 145 }); 146 146 147 147 ////////////////////////////////////////////
+7 -1
src/themes/webamp/index.vto
··· 78 78 <de-queue></de-queue> 79 79 80 80 <!-- Inputs, Output & Processors --> 81 - <di-opensubsonic id="input"></di-opensubsonic> 82 81 <dop-indexed-db></dop-indexed-db> 83 82 <dp-metadata></dp-metadata> 83 + 84 + <di-opensubsonic id="input"></di-opensubsonic> 85 + 86 + <!--<dc-input id="input"> 87 + <di-opensubsonic></di-opensubsonic> 88 + <di-s3></di-s3> 89 + </dc-input>--> 84 90 85 91 <!-- Transformers --> 86 92 <dtor-default id="output" output-selector="dtos-json"></dtor-default>