A music player that connects to your cloud/distributed storage. diffuse.sh
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580import QS from "query-string";import { html, render } from "lit-html";
import { effect, signal } from "~/common/signal.js";import { rpc, workerLink, workerProxy, workerTunnel } from "./worker.js";import { RpcChannel } from "./worker/rpc-channel.js";
/** * @import {BroadcastingStatus, WorkerOpts} from "./element.d.ts" * @import {ProxiedActions, Tunnel} from "./worker.d.ts"; * @import {Signal} from "./signal.d.ts" */
export { html, nothing } from "lit-html";export const DEFAULT_GROUP = "default";
/** * Base for custom elements, provides some utility functionality * around rendering and managing signals. */export class DiffuseElement extends HTMLElement { $connected = signal(false);
#connected = Promise.withResolvers(); #disposables = /** @type {Array<() => void>} */ ([]);
/** */ constructor() { super();
this.worker = this.worker.bind(this); this.workerLink = this.workerLink.bind(this); }
/** * @param {string} _name * @param {string} oldValue * @param {string} newValue */ attributeChangedCallback(_name, oldValue, newValue) { if (oldValue !== newValue) this.#render(); }
/** * Effect helper that automatically is disposes * when this element is removed from the DOM. * * @param {() => void} fn */ effect(fn) { const unregister = effect(fn); this.#disposables.push(unregister); return unregister; }
/** */ forceRender() { return this.#render(); }
/** */ get group() { return this.getAttribute("group") ?? DEFAULT_GROUP; }
/** */ get identifier() { const ns = this.namespace; return `${this.constructor.prototype.constructor.NAME}/${this.group}${ ns?.length ? "/" + ns : "" }`; }
/** */ get label() { return this.getAttribute("label") ?? this.id ?? this.localName; }
/** */ get namespace() { return this.getAttribute("namespace") ? this.getAttribute("namespace") : undefined; }
/** */ root() { return (this.shadowRoot ?? this); }
/** */ get selector() { return this.id.length ? `#${this.id}` : this.localName; }
/** */ whenConnected() { return this.#connected.promise; }
/** */ whenDefined() { return customElements.whenDefined(this.localName); }
/** * Avoid replacing the whole subtree, * morph the existing DOM into the new given tree. */ #render() { if (!("render" in this && typeof this.render === "function")) return;
const tmp = this.render({ html: html, state: "state" in this ? this.state : undefined, });
render(tmp, this.root()); }
// LIFECYCLE
connectedCallback() { this.$connected.value = true; this.#connected.resolve(null);
if (!("render" in this && typeof this.render === "function")) return;
this.effect(() => { if (!("render" in this && typeof this.render === "function")) return; this.#render(); }); }
disconnectedCallback() { this.$connected.value = false; this.#teardown(); }
#teardown() { this.#disposables.forEach((fn) => fn()); }
// WORKERS
/** @type {undefined | Worker | SharedWorker} */ #worker;
createWorker() { const NAME = this.constructor.prototype.constructor.NAME; const WORKER_URL = this.constructor.prototype.constructor.WORKER_URL;
if (!NAME) throw new Error("Missing `NAME` static property"); if (!WORKER_URL) throw new Error("Missing `WORKER_URL` static property");
// Query const query = QS.stringify( "workerQuery" in this && typeof this.workerQuery === "function" ? this.workerQuery() : {}, );
// Setup worker const name = this.identifier; const url = import.meta.resolve("./" + WORKER_URL) + `?${query}`;
let worker;
if (this.hasAttribute("group") && typeof SharedWorker !== "undefined") { worker = new SharedWorker(url, { name, type: "module" }); } else { worker = new Worker(url, { name, type: "module" }); }
return worker; }
/** * @returns {Record<string, DiffuseElement> | null} */ dependencies() { return null; }
worker() { this.#worker ??= this.createWorker(); return this.#worker; }
workerLink() { const worker = this.worker(); return workerLink(worker); }
/** * @template {Record<string, (...args: any[]) => any>} Actions * @param {WorkerOpts} [opts] * @returns {ProxiedActions<Actions>} */ workerProxy(opts) { return workerProxy( () => this.workerTunnel(opts).port, ); }
/** * Creates a MessagePort that is connected to the worker. * All the dependencies are added automatically. * * @param {WorkerOpts} [opts] */ workerTunnel({ forceNew } = {}) { const worker = forceNew === true || (typeof forceNew === "object" && forceNew.self === true) ? () => this.createWorker() : () => this.worker(); const deps = this.dependencies();
let toWorker;
if (deps) { toWorker = /** * @param {any} msg */ async (msg) => { /** @type {Array<[string, Tunnel]>} */ const ports = Object.entries(deps).map( /** @param {[string, DiffuseElement]} _ */ ([k, v]) => { const n = typeof forceNew === "object" ? forceNew.dependencies?.[k] ?? false : false; return [k, v.workerTunnel({ forceNew: n })]; }, );
const data = { data: Array.isArray(msg.args) ? msg.args[0] : msg.args, ports: Object.fromEntries(ports.map(([k, v]) => { return [k, v.port]; })), };
this.#disposables.push(() => { ports.forEach(([_k, v]) => v.disconnect()); });
return { data: { ...msg, args: Array.isArray(msg.args) ? [data, ...msg.args.slice(1)] : msg.args, }, transfer: ports.map(([_k, v]) => v.port), }; }; }
const tunnel = workerTunnel(worker, { toWorker }); return tunnel; }}
/** * Broadcastable version of the base class. * * Share the state of an element across multiple tabs * of the same origin and have one instance be the leader. */export class BroadcastableDiffuseElement extends DiffuseElement { broadcasted = false;
/** @type {{ assumeLeadership?: boolean }} */ #broadcastingOptions = {};
#broadcastingStatus; broadcastingStatus;
/** @type {PromiseWithResolvers<void>} */ #lock = Promise.withResolvers();
/** @type {PromiseWithResolvers<BroadcastingStatus>} */ #status = Promise.withResolvers();
constructor() { super();
this.broadcast = this.broadcast.bind(this);
/** @type {Signal<Promise<BroadcastingStatus>>} */ this.#broadcastingStatus = signal(this.#status.promise); this.broadcastingStatus = this.#broadcastingStatus.get; }
/** * @template {Record<string, { strategy: "leaderOnly" | "replicate", fn: (...args: any[]) => any }>} ActionsWithStrategy * @template {{ [K in keyof ActionsWithStrategy]: ActionsWithStrategy[K]["fn"] }} Actions * @param {string} channelName * @param {ActionsWithStrategy} actionsWithStrategy * @param {{ assumeLeadership?: boolean }} [options] */ broadcast(channelName, actionsWithStrategy, options) { if (this.broadcasted) return; if (options) this.#broadcastingOptions = options;
const channel = new BroadcastChannel(channelName); const msg = new MessageChannel();
/** * @typedef {{ [K in keyof ActionsWithStrategy]: ActionsWithStrategy[K]["fn"] }} A */
this.broadcasted = true; this.channelName = channelName;
/** @type {RpcChannel<{}, Actions>} */ const _rpc = rpc( msg.port2, /** @type {Actions} */ ( Object.fromEntries( Object.entries(actionsWithStrategy).map(([k, v]) => { return [k, v.fn.bind(this)]; }), ) ), );
channel.addEventListener( "message", async (event) => { if (event.data?.method?.startsWith("leader:")) { const status = await this.#status.promise; if (status.leader) { msg.port1.postMessage({ ...event.data, method: event.data.method.slice("leader:".length), }); } } else { msg.port1.postMessage(event.data); } }, );
msg.port1.addEventListener( "message", (event) => channel.postMessage(event.data), );
msg.port1.start(); msg.port2.start();
async function anyoneWaiting() { if (typeof navigator.locks?.query !== "function") return false; const state = await navigator.locks.query(); return !!state.pending?.length; }
/** @type {RpcChannel<{}, Actions>} */ const proxyChannel = new RpcChannel(msg.port2);
/** @type {ProxiedActions<Actions>} */ const proxy = proxyChannel.getAPI();
/** @type {any} */ const actions = {};
Object.entries(actionsWithStrategy).forEach( ([action, { fn, strategy }]) => { const ogFn = fn.bind(this); let wrapFn = ogFn;
switch (strategy) { case "leaderOnly": /** @param {Parameters<Actions[action]>} args */ wrapFn = async (...args) => { const status = await this.#status.promise; return status.leader ? ogFn(...args) : proxyChannel.callMethod(`leader:${action}`, args); }; break;
case "replicate": /** @param {Parameters<Actions[action]>} args */ wrapFn = async (...args) => { anyoneWaiting().then((bool) => { if (bool) proxy[action](...args); }); return ogFn(...args); }; break; }
actions[action] = wrapFn; }, );
return /** @type {ProxiedActions<Actions>} */ (actions); }
async isLeader() { if (this.broadcasted) { const status = await this.broadcastingStatus(); return status.leader; } else { return true; } }
// LIFECYCLE
/** * @override */ connectedCallback() { super.connectedCallback();
if (!this.broadcasted) return;
// Grab a lock if it isn't acquired yet and if needed, // and hold it until `this.lock.promise` resolves. const assumeLeadership = this.#broadcastingOptions?.assumeLeadership;
if (assumeLeadership === undefined || assumeLeadership === true) { if (typeof navigator.locks?.request === "function") { navigator.locks.request( `${this.channelName}/lock`, assumeLeadership === true ? { steal: true } : { ifAvailable: true }, (lock) => { this.#status.resolve( lock ? { leader: true, initialLeader: true } : { leader: false }, ); if (lock) return this.#lock.promise; }, ); } else { this.#status.resolve( { leader: true, initialLeader: true }, ); } } else { this.#status.resolve( { leader: false }, ); }
// When the lock status is initially determined, log its status. // Additionally, wait for lock if needed. this.#status.promise.then((status) => { if (status.leader) { console.log(`🧙 Elected leader for: ${this.channelName}`); } else { console.log(`🔮 Watching leader: ${this.channelName}`); }
// Wait for leadership if (status.leader === false) { navigator.locks.request( `${this.channelName}/lock`, () => { this.#status = Promise.withResolvers(); this.#status.resolve({ leader: true, initialLeader: false });
this.#broadcastingStatus.value = this.#status.promise;
return this.#lock.promise; }, ); } }); }
/** * @override */ disconnectedCallback() { super.disconnectedCallback(); this.#lock.resolve(); }}
/** * Component DOM selector. * * Basically `document.querySelector` but returns the element * with the correct type based on the element module given. * * ``` * import * as QueryEngine from "~/components/engine/query/element.js" * * const instance = component(QueryEngine) * ``` * * @template {abstract new (...args: any[]) => any} C * @param {{ CLASS: C; NAME: string }} elementModule * @param {string} [id] Optional id to select */export function component(elementModule, id) { const el = document.querySelector( id ? `${elementModule.NAME}#${id}` : elementModule.NAME, ); if (!el) { throw new Error(`Element for selector '${elementModule.NAME}' not found.`); } return /** @type {InstanceType<C>} */ (el);}
/** * Defines a custom element, guarding against double-registration. * * @param {string} name * @param {CustomElementConstructor} constructor */export function defineElement(name, constructor) { if (!customElements.get(name)) customElements.define(name, constructor); else {console.warn( `The '${name}' element was asked to be defined again, this should be avoided. The code may have been executed multiple times.`, );}}
/** * @template {HTMLElement} T * @param {DiffuseElement} parent * @param {string} attribute * @returns {T} */export function query(parent, attribute) { const selector = parent.getAttribute(attribute);
if (!selector) { throw new Error(`Missing required '${attribute}' attribute`); }
/** @type {T | null} */ const element = document.querySelector(selector); if (!element) throw new Error(`Missing required '${selector}' element`); return element;}
/** * @template {HTMLElement} T * @param {DiffuseElement} parent * @param {string} attribute */export function queryOptional(parent, attribute) { const selector = parent.getAttribute(attribute);
if (!selector) { return null; }
/** @type {T | null} */ const elementOrNull = document.querySelector(selector); return elementOrNull;}
/** * @param {Record<string, Worker | SharedWorker>} workers */export function terminateWorkers(workers) { Object.values(workers).forEach((worker) => { if (worker instanceof Worker) worker.terminate(); });}
/** * @template {Record<string, DiffuseElement>} T * @param {T} elements */export async function whenElementsDefined(elements) { await Promise.all( Object.values(elements).map((element) => customElements.whenDefined(element.localName) ), );}