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.

feat: queue

+229 -30
+1
_config.ts
··· 12 12 site.use(esbuild({ 13 13 options: { 14 14 bundle: true, 15 + minify: false, 15 16 splitting: true, 16 17 }, 17 18 }));
+1
deno.jsonc
··· 4 4 "@mys/worker-fn": "jsr:@mys/worker-fn@^3.2.1", 5 5 "@okikio/transferables": "jsr:@okikio/transferables@^1.0.2", 6 6 "@std/fs": "jsr:@std/fs@^1.0.15", 7 + "alien-deepsignals": "npm:alien-deepsignals@^0.2.7", 7 8 "alien-signals": "npm:alien-signals@^3.0.0", 8 9 "lume/": "https://deno.land/x/lume@v3.0.11/", 9 10 "lume/jsx-runtime": "https://deno.land/x/ssx@v0.1.12/jsx-runtime.ts",
+8
deno.lock
··· 36 36 "jsr:@std/yaml@1.0.9": "1.0.9", 37 37 "jsr:@std/yaml@^1.0.5": "1.0.9", 38 38 "npm:@types/node@*": "24.2.0", 39 + "npm:alien-deepsignals@~0.2.7": "0.2.7_alien-signals@3.0.0", 39 40 "npm:alien-signals@3": "3.0.0", 40 41 "npm:autoprefixer@10.4.21": "10.4.21_postcss@8.5.6", 41 42 "npm:lightningcss-wasm@1.30.1": "1.30.1", ··· 257 258 "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", 258 259 "dependencies": [ 259 260 "undici-types" 261 + ] 262 + }, 263 + "alien-deepsignals@0.2.7_alien-signals@3.0.0": { 264 + "integrity": "sha512-fERozTuMRWzO5fv9xBNk1Zm0ZxUllRPiMckoyQcEcCBZ4mpEiZhVnmmSqn26i5KwYnDcZxjS2LAo/o6QjzoEOQ==", 265 + "dependencies": [ 266 + "alien-signals" 260 267 ] 261 268 }, 262 269 "alien-signals@3.0.0": { ··· 774 781 "jsr:@mys/worker-fn@^3.2.1", 775 782 "jsr:@okikio/transferables@^1.0.2", 776 783 "jsr:@std/fs@^1.0.15", 784 + "npm:alien-deepsignals@~0.2.7", 777 785 "npm:alien-signals@3", 778 786 "npm:morphdom@^2.7.7", 779 787 "npm:xxh32@^2.0.5"
+6
src/common/worker.d.ts
··· 1 1 export type Announcement<T> = MRpcBaseMsg & { type: "announcement"; args: T }; 2 + 3 + /** 4 + * Comes from the `@mys/m-rpc` library, 5 + * but it is not exported. Used to identify 6 + * messages sent via `postMessage`. 7 + */ 2 8 export type MRpcBaseMsg = { ns: string; name: string; key: number };
+20 -9
src/common/worker.js
··· 1 1 import { defineWorkerFn, useWorkerFn } from "@mys/worker-fn"; 2 2 import { getTransferables } from "@okikio/transferables"; 3 + 3 4 import { xxh32 } from "xxh32"; 4 5 5 6 /** 7 + * @import {NodeWorkerOrNodeMessagePort} from "@mys/m-rpc"; 6 8 * @import {Announcement} from "./worker.d.ts" 7 9 */ 8 10 ··· 23 25 * @template T 24 26 * @param {string} name 25 27 * @param {(args: T) => void} fn 28 + * @param {Worker | NodeWorkerOrNodeMessagePort} context 26 29 */ 27 - export function listen(name, fn) { 28 - globalThis.addEventListener("message", (event) => { 29 - const announcement = 30 - /** @type {Announcement<T>} */ (/** @type {unknown} */ (event)); 31 - const { ns, type } = announcement; 30 + export function listen( 31 + name, 32 + fn, 33 + context = globalThis, 34 + ) { 35 + context.addEventListener( 36 + "message", 37 + /** @param {MessageEvent} event */ (event) => { 38 + const announcement = /** @type {Announcement<T>} */ (event.data); 39 + const { ns, type } = announcement; 32 40 33 - if (announcement.name !== name) return; 34 - if (ns !== ANNOUNCEMENT || type !== ANNOUNCEMENT) return; 41 + if (announcement.name !== name) return; 42 + if (ns !== ANNOUNCEMENT || type !== ANNOUNCEMENT) return; 35 43 36 - fn(announcement.args); 37 - }); 44 + fn(announcement.args); 45 + }, 46 + ); 38 47 } 39 48 49 + //////////////////////////////////////////// 40 50 // PRIVATE 51 + //////////////////////////////////////////// 41 52 42 53 const ANNOUNCEMENT = "announcement"; 43 54
+5 -2
src/component/engine/audio/element.js
··· 422 422 // REGISTER 423 423 //////////////////////////////////////////// 424 424 425 - customElements.define("de-audio", AudioEngine); 426 - customElements.define("de-audio-item", AudioEngineItem); 425 + export const NAME = "de-audio"; 426 + export const NAME_ITEM = "de-audio-item"; 427 + 428 + customElements.define(NAME, AudioEngine); 429 + customElements.define(NAME_ITEM, AudioEngineItem);
+1 -1
src/component/engine/audio/types.d.ts
··· 1 - import { Signal } from "@common/signal.d.ts"; 1 + import type { Signal } from "@common/signal.d.ts"; 2 2 3 3 export interface Actions { 4 4 pause: (_: { audioId: string }) => void;
+70
src/component/engine/queue/element.js
··· 1 + import DiffuseElement from "@common/element.js"; 2 + import { signal } from "@common/signal.js"; 3 + import { listen, use } from "@common/worker.js"; 4 + 5 + /** 6 + * @import {Actions, Item, Signals} from "./types.d.ts" 7 + */ 8 + 9 + //////////////////////////////////////////// 10 + // ELEMENT 11 + //////////////////////////////////////////// 12 + 13 + /** 14 + * @implements {Actions} 15 + * @implements {Signals} 16 + */ 17 + class QueueEngine extends DiffuseElement { 18 + constructor() { 19 + super(); 20 + 21 + // TODO: 22 + // const worker = new SharedWorker(new URL("./worker.js", import.meta.url), { 23 + // type: "module", 24 + // }); 25 + // 26 + // const port = worker.port; 27 + 28 + const worker = new Worker(new URL("./worker.js", import.meta.url), { 29 + type: "module", 30 + }); 31 + 32 + const port = worker; 33 + 34 + listen("future", this.future, port); 35 + listen("now", this.now, port); 36 + listen("past", this.past, port); 37 + 38 + this.add = use("add", port); 39 + 40 + this.load(port); 41 + } 42 + 43 + /** 44 + * @param {Worker} port 45 + */ 46 + async load(port) { 47 + const f = await use("future", port)(); 48 + const n = await use("now", port)(); 49 + const p = await use("past", port)(); 50 + 51 + this.future(f); 52 + this.now(n); 53 + this.past(p); 54 + } 55 + 56 + // SIGNALS 57 + 58 + future = signal(/** @type {Array<Item>} */ ([])); 59 + now = signal(/** @type {Item | null} */ (null)); 60 + past = signal(/** @type {Array<Item>} */ ([])); 61 + } 62 + 63 + export default QueueEngine; 64 + 65 + //////////////////////////////////////////// 66 + // REGISTER 67 + //////////////////////////////////////////// 68 + 69 + export const NAME = "de-queue"; 70 + customElements.define(NAME, QueueEngine);
+17
src/component/engine/queue/types.d.ts
··· 1 + import type { Track, TrackStats, TrackTags } from "@component/core/types.d.ts"; 2 + import type { Signal } from "@common/signal.d.ts"; 3 + 4 + export interface Actions { 5 + add: (items: Item[]) => void; 6 + // TODO 7 + } 8 + 9 + export type Item<Stats = TrackStats, Tags = TrackTags> = 10 + & Track<Stats, Tags> 11 + & { manualEntry?: boolean }; 12 + 13 + export interface Signals { 14 + future: Signal<Item[]>; 15 + now: Signal<Item | null>; 16 + past: Signal<Item[]>; 17 + }
+89 -14
src/component/engine/queue/worker.js
··· 1 1 import { announce, define } from "@common/worker.js"; 2 2 import { effect, signal } from "@common/signal.js"; 3 + import { arrayShuffle } from "@common/index.js"; 3 4 4 5 /** 6 + * @import {Item} from "./types.d.ts" 5 7 * @import {Track} from "@component/core/types.d.ts" 6 8 */ 9 + 10 + const QUEUE_SIZE = 25; 7 11 8 12 //////////////////////////////////////////// 9 13 // STATE 10 14 //////////////////////////////////////////// 11 15 12 - const pools = signal(/** @type {Record<string, { pool: Track[] }>} */ ({})); 16 + const future = signal(/** @type {Item[]} */ ([])); 17 + const lake = signal(/** @type {Track[]} */ ([])); 18 + const now = signal(/** @type {Item | null} */ (null)); 19 + const past = signal(/** @type {Item[]} */ ([])); 20 + 21 + effect(() => announce("future", future())); 22 + effect(() => announce("now", now())); 23 + effect(() => announce("past", past())); 24 + 25 + define("future", () => future()); 26 + define("now", () => now()); 27 + define("past", () => past()); 13 28 14 29 //////////////////////////////////////////// 15 30 // ACTIONS 16 31 //////////////////////////////////////////// 17 32 33 + define("add", add); 18 34 define("pool", pool); 35 + define("shift", shift); 36 + define("unshift", unshift); 19 37 20 38 /** 21 - * @param {{ groupId: string; tracks: Track[] }} _ 39 + * @param {Item[]} items 22 40 */ 23 - function pool({ groupId, tracks }) { 24 - fill(); 41 + function add(items) { 42 + future([...future(), ...items]); 43 + } 44 + 45 + /** 46 + * @param {Track[]} tracks 47 + */ 48 + function pool(tracks) { 49 + lake(tracks); 50 + 51 + // TODO: If the pool changes, only remove non-existing tracks 52 + // instead of resetting the whole future queue. 53 + // 54 + // What about past queue items? 55 + 56 + future(fill([])); 57 + 58 + // Automatically insert track if there isn't any 59 + if (!now()) return shift(); 60 + } 61 + 62 + function shift() { 63 + const n = now(); 64 + const f = future(); 65 + 66 + now(f[0] ?? null); 67 + 68 + if (n) past([...past(), n]); 69 + future(fill(f.slice(1))); 70 + } 71 + 72 + function unshift() { 73 + const p = past(); 74 + if (p.length === 0) return; 75 + 76 + const n = now(); 77 + const [last] = p.splice(p.length - 1, 1); 25 78 26 - // TODO: 27 - // Create a signal for each group: future, past, now 28 - // Whenever that state changes it should create an annoucement. 29 - // Custom elements on the main thread can then listen for those. 79 + now(last ?? null); 80 + if (n) future([n, ...future()]); 30 81 } 31 82 83 + //////////////////////////////////////////// 32 84 // PRIVATE 85 + //////////////////////////////////////////// 33 86 34 - function fill() { 35 - } 87 + /** 88 + * @param {Item[]} future 89 + * @returns {Item[]} 90 + */ 91 + function fill(future) { 92 + if (future.length >= QUEUE_SIZE) return future; 93 + 94 + /** @type {Track[]} */ 95 + const pool = []; 96 + 97 + let p = new Set(past().map((t) => t.id)); 98 + let reducedPool = pool; 36 99 37 - function todo() { 38 - effect(() => { 39 - const data = groupSignal(); 40 - announce("some-name", data); 100 + lake().forEach((track) => { 101 + if (p.has(track.id)) { 102 + p = p.difference(new Set(track.id)); 103 + } else { 104 + pool.push(track); 105 + } 41 106 }); 107 + 108 + if (reducedPool.length === 0) { 109 + reducedPool = lake(); 110 + } 111 + 112 + const poolSelection = arrayShuffle(reducedPool).slice( 113 + 0, 114 + QUEUE_SIZE - future.length, 115 + ); 116 + return [...future, ...poolSelection]; 42 117 }
+11 -4
src/theme/blur/index.vto
··· 2 2 <html> 3 3 <head> 4 4 <link rel="stylesheet" href="../../styles/theme/blur/index.css" /> 5 + </head> 6 + <body> 7 + <de-queue></de-queue> 5 8 6 9 <script type="module"> 7 - import "../../component/engine/audio/element.js"; 10 + import * as Queue from "../../component/engine/queue/element.js"; 11 + import { effect } from "../../common/signal.js" 12 + 13 + const queue = document.querySelector(Queue.NAME) 14 + 15 + effect(() => { 16 + console.log("Future:", queue.future()) 17 + }) 8 18 </script> 9 - </head> 10 - <body> 11 - <de-audio></de-audio> 12 19 </body> 13 20 </html>