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.

chore: rework signals

+144 -101
+7 -1
src/common/element.d.ts
··· 2 2 | { leader: true; initialLeader: boolean } 3 3 | { leader: false }; 4 4 5 + export type FnParams<Fn> = Fn extends (...args: infer P) => any ? P : never; 6 + export type FnReturn<Fn> = Fn extends (...args: any[]) => infer P ? P : never; 7 + 5 8 export type HtmlTagFunction = ( 6 9 strings: string[] | ArrayLike<string>, 7 10 ...values: unknown[] ··· 24 27 childrenOnly?: boolean; 25 28 }; 26 29 27 - export type RenderArg<State> = { html: HtmlTagFunction; state: State }; 30 + export type RenderArg<State = undefined> = { 31 + html: HtmlTagFunction; 32 + state: State; 33 + };
+28 -11
src/common/element.js
··· 1 1 import morphdom from "morphdom"; 2 2 3 - import { effect, unbiasedSignal } from "@common/signal.js"; 3 + import { effect, signal } from "@common/signal.js"; 4 4 import { define, use } from "@common/worker.js"; 5 5 6 6 /** 7 - * @import {BroadcastingStatus, HtmlTagFunction, MorphOptions} from "./element.d.ts" 8 - * @import {Signal} from "./signal.d.ts" 7 + * @import {BroadcastingStatus, FnParams, FnReturn, HtmlTagFunction, MorphOptions} from "./element.d.ts" 8 + * @import {Signal, SignalReader} from "./signal.d.ts" 9 9 */ 10 10 11 11 /** ··· 109 109 export class BroadcastableDiffuseElement extends DiffuseElement { 110 110 broadcasted = false; 111 111 112 + #broadcastingStatus; 113 + broadcastingStatus; 114 + 112 115 /** @type {PromiseWithResolvers<void>} */ 113 116 #lock = Promise.withResolvers(); 114 117 ··· 121 124 this.broadcast = this.broadcast.bind(this); 122 125 123 126 /** @type {Signal<Promise<BroadcastingStatus>>} */ 124 - this.broadcastingStatus = unbiasedSignal(this.#status.promise); 127 + this.#broadcastingStatus = signal(this.#status.promise, { unbiased: true }); 128 + this.broadcastingStatus = this.#broadcastingStatus.get; 125 129 } 126 130 127 131 /** ··· 167 171 } 168 172 169 173 /** 174 + * @template I 175 + * @template O 176 + * @template {(...args: I[]) => O} Fn 170 177 * @param {string} method 171 - * @param {Function} fn 178 + * @param {Fn} fn 172 179 */ 173 180 return (method, fn) => { 174 181 define(method, fn.bind(this), msg.port2); 175 182 176 - /** @param {any[]} args */ 183 + /** 184 + * @typedef {FnParams<typeof fn>} P 185 + * @typedef {FnReturn<typeof fn>} R 186 + */ 187 + 188 + /** @param {P} args */ 177 189 const leaderOnly = async (...args) => { 178 190 const status = await this.#status.promise; 179 191 return status.leader 180 - ? fn.call(this, ...args) 181 - : use(`leader:${method}`, msg.port2)(...args); 192 + ? /** @type {R} */ (fn.call(this, ...args)) 193 + : /** @type {Promise<R>} */ (use(`leader:${method}`, msg.port2)( 194 + ...args, 195 + )); 182 196 }; 183 197 184 - /** @param {any[]} args */ 198 + /** 199 + * @param {P} args 200 + * @returns {R} 201 + */ 185 202 const replicate = (...args) => { 186 203 anyoneWaiting().then((bool) => { 187 204 if (bool) use(method, msg.port2)(...args); 188 205 }); 189 - return fn.call(this, ...args); 206 + return /** @type {R} */ (fn.call(this, ...args)); 190 207 }; 191 208 192 209 return { ··· 236 253 this.#status = Promise.withResolvers(); 237 254 this.#status.resolve({ leader: true, initialLeader: false }); 238 255 239 - this.broadcastingStatus(this.#status.promise); 256 + this.#broadcastingStatus.value = this.#status.promise; 240 257 241 258 return this.#lock.promise; 242 259 },
+9 -2
src/common/signal.d.ts
··· 1 - import type { signal } from "alien-signals"; 1 + export type SignalReader<T> = () => T; 2 + export type SignalWriter<T> = (t: T) => void; 3 + 4 + export type Signal<T> = { 5 + get: SignalReader<T>; 6 + set: SignalWriter<T>; 2 7 3 - export type Signal<T> = ReturnType<typeof signal<T>>; 8 + get value(): T; 9 + set value(t: T); 10 + };
+35 -10
src/common/signal.js
··· 4 4 export * from "alien-signals"; 5 5 6 6 /** 7 - * @import {Signal} from "./signal.d.ts" 7 + * @import {Signal, SignalReader, SignalWriter} from "./signal.d.ts" 8 8 */ 9 9 10 10 /** 11 11 * @template T 12 12 * @param {T} initialValue 13 + * @param {{ unbiased?: boolean }} [options] 13 14 * @returns {Signal<T>} 14 15 */ 15 - export function signal(initialValue) { 16 + export function signal(initialValue, options) { 16 17 const s = alienSignal(initialValue); 17 18 const isPrimitive = Object(initialValue) !== initialValue; 18 - if (isPrimitive) return s; 19 + if (isPrimitive || options?.unbiased === true) { 20 + return _signal({ 21 + get: () => s(), 22 + set: (v) => s(v), 23 + }); 24 + } 19 25 20 - return /** @type {Signal<T>} */ ((b) => { 21 - const a = s(); 22 - if (b === undefined) return a; 23 - 24 - const diff = deepDiff(a, b); 25 - if (diff) s(b); 26 + return _signal({ 27 + get: () => s(), 28 + set: (b) => { 29 + const a = s(); 30 + const diff = deepDiff(a, b); 31 + if (diff) s(b); 32 + }, 26 33 }); 27 34 } 28 35 29 - export const unbiasedSignal = alienSignal; 36 + /** 37 + * @template T 38 + * @param {{ get: SignalReader<T>; set: SignalWriter<T> }} _ 39 + * @returns {Signal<T>} 40 + */ 41 + function _signal({ get, set }) { 42 + return { 43 + get, 44 + set, 45 + 46 + get value() { 47 + return get(); 48 + }, 49 + 50 + set value(v) { 51 + set(v); 52 + }, 53 + }; 54 + }
+1 -1
src/component/constituent/blur/browser-list/element.js
··· 18 18 19 19 get state() { 20 20 return { 21 - tracks: this.tracks, 21 + tracks: this.tracks.get, 22 22 }; 23 23 } 24 24
+3 -3
src/component/constituent/blur/browser-list/types.d.ts
··· 1 - import type { Signal } from "../../../../common/signal.d.ts"; 2 - import type { Track } from "../../../core/types.d.ts"; 1 + import type { SignalReader } from "@common/signal.d.ts"; 2 + import type { Track } from "@component/core/types.d.ts"; 3 3 4 - export type State = { tracks: Signal<Track[]> }; 4 + export type State = { tracks: SignalReader<Track[]> };
+16 -30
src/component/engine/audio/element.js
··· 1 - import { 2 - BroadcastableDiffuseElement, 3 - DiffuseElement, 4 - } from "@common/element.js"; 1 + import { BroadcastableDiffuseElement } from "@common/element.js"; 5 2 import { signal } from "@common/signal.js"; 6 3 7 4 /** ··· 21 18 22 19 /** 23 20 * @implements {Actions} 24 - * @implements {Signals} 25 21 */ 26 22 class AudioEngine extends BroadcastableDiffuseElement { 27 - // TODO: 28 - // static observedAttributes = ["volume"]; 29 - 30 23 constructor() { 31 24 super(); 32 25 ··· 44 37 this.seek = fn("seek", this.seek).leaderOnly; 45 38 this.supply = fn("supply", this.supply).replicate; 46 39 47 - this.isPlaying = fn("isPlaying", this.isPlaying).replicate; 48 - this.volume = fn("volume", this.volume).replicate; 40 + this.__isPlaying.set = fn("isPlaying", this.__isPlaying.set).replicate; 49 41 } 50 42 51 43 // TODO: Get volume from previous session if possible ··· 55 47 56 48 // SIGNALS 57 49 58 - volume = signal(0.5); 59 - isPlaying = signal(false); 60 50 #items = signal(/** @type {Audio[]} */ ([])); 51 + #volume = signal(0.5); 52 + 53 + __isPlaying = signal(false); 61 54 62 55 // STATE 63 56 64 - /** 65 - * @type {State} 66 - */ 67 - get state() { 68 - return { 69 - isPlaying: this.isPlaying, 70 - items: this.#items, 71 - volume: this.volume, 72 - }; 73 - } 57 + isPlaying = this.__isPlaying.get; 58 + items = this.#items.get; 59 + volume = this.#volume.get; 74 60 75 61 // LIFECYCLE 76 62 ··· 98 84 (node) => { 99 85 const audio = /** @type {HTMLAudioElement} */ (node); 100 86 if (audio.hasAttribute("preload")) return; 101 - audio.volume = this.volume(); 87 + audio.volume = this.#volume.value; 102 88 }, 103 89 ); 104 90 }); ··· 118 104 */ 119 105 play({ audioId, volume }) { 120 106 this.withAudioNode(audioId, (audio, item) => { 121 - audio.volume = volume ?? this.state.volume(); 107 + audio.volume = volume ?? this.volume(); 122 108 audio.muted = false; 123 109 124 110 if (audio.readyState === 0) audio.load(); ··· 176 162 * @type {Actions["supply"]} 177 163 */ 178 164 supply(args) { 179 - this.#items(args.audio); 165 + this.#items.value = args.audio; 180 166 if (args.play) this.play(args.play); 181 167 } 182 168 183 169 // RENDER 184 170 185 171 /** 186 - * @param {RenderArg<State>} _ 172 + * @param {RenderArg} _ 187 173 */ 188 - render({ html, state }) { 189 - const nodes = state.items().map((audio) => { 174 + render({ html }) { 175 + const nodes = this.items().map((audio) => { 190 176 const ip = audio.progress === undefined 191 177 ? "0" 192 178 : JSON.stringify(audio.progress); ··· 372 358 : false; 373 359 374 360 item?.state({ isPlaying: false }); 375 - item?.engine?.isPlaying(ended); 361 + item?.engine?.__isPlaying.set(ended); 376 362 } 377 363 378 364 /** ··· 382 368 const audio = /** @type {HTMLAudioElement} */ (event.target); 383 369 384 370 engineItem(audio)?.state({ isPlaying: true }); 385 - engineItem(audio)?.engine?.isPlaying(true); 371 + engineItem(audio)?.engine?.__isPlaying.set(true); 386 372 387 373 // In case audio was preloaded: 388 374 if (audio.readyState === 4) finishedLoading(event);
+5 -8
src/component/engine/audio/types.d.ts
··· 1 - import type { Signal } from "@common/signal.d.ts"; 1 + import type { SignalReader } from "@common/signal.d.ts"; 2 2 3 3 export type Actions = { 4 4 pause: (_: { audioId: string }) => void; ··· 36 36 url: string; 37 37 }; 38 38 39 - export type Signals = { 40 - isPlaying: Signal<boolean>; 41 - volume: Signal<number>; 42 - }; 43 - 44 - export type State = Signals & { 45 - items: Signal<Audio[]>; 39 + export type State = { 40 + isPlaying: SignalReader<boolean>; 41 + items: SignalReader<Audio[]>; 42 + volume: SignalReader<number>; 46 43 };
+13 -8
src/component/engine/queue/element.js
··· 3 3 import { listen, use } from "@common/worker.js"; 4 4 5 5 /** 6 - * @import {Actions, Item, Signals} from "./types.d.ts" 6 + * @import {Actions, Item} from "./types.d.ts" 7 7 */ 8 8 9 9 //////////////////////////////////////////// ··· 12 12 13 13 /** 14 14 * @implements {Actions} 15 - * @implements {Signals} 16 15 */ 17 16 class QueueEngine extends DiffuseElement { 18 17 constructor() { ··· 38 37 } 39 38 40 39 // Sync data with worker 41 - listen("future", this.future, port); 42 - listen("now", this.now, port); 43 - listen("past", this.past, port); 40 + listen("future", this.#future.set, port); 41 + listen("now", this.#now.set, port); 42 + listen("past", this.#past.set, port); 44 43 45 44 // Worker proxy 46 45 this.add = use("add", port); ··· 51 50 52 51 // SIGNALS 53 52 54 - future = signal(/** @type {Array<Item>} */ ([])); 55 - now = signal(/** @type {Item | null} */ (null)); 56 - past = signal(/** @type {Array<Item>} */ ([])); 53 + #future = signal(/** @type {Array<Item>} */ ([])); 54 + #now = signal(/** @type {Item | null} */ (null)); 55 + #past = signal(/** @type {Array<Item>} */ ([])); 56 + 57 + // STATE 58 + 59 + future = this.#future.get; 60 + now = this.#now.get; 61 + past = this.#past.get; 57 62 } 58 63 59 64 export default QueueEngine;
+5 -5
src/component/engine/queue/types.d.ts
··· 1 1 import type { Track, TrackStats, TrackTags } from "@component/core/types.d.ts"; 2 - import type { Signal } from "@common/signal.d.ts"; 2 + import type { SignalReader } from "@common/signal.d.ts"; 3 3 4 4 export type Actions = { 5 5 add: (items: Item[]) => void; ··· 12 12 & Track<Stats, Tags> 13 13 & { manualEntry?: boolean }; 14 14 15 - export type Signals = { 16 - future: Signal<Item[]>; 17 - now: Signal<Item | null>; 18 - past: Signal<Item[]>; 15 + export type State = { 16 + future: SignalReader<Item[]>; 17 + now: SignalReader<Item | null>; 18 + past: SignalReader<Item[]>; 19 19 };
+22 -22
src/component/engine/queue/worker.js
··· 26 26 * @type {Actions['add']} 27 27 */ 28 28 export function add(items) { 29 - future([...future(), ...items]); 29 + future.value = [...future.value, ...items]; 30 30 } 31 31 32 32 /** 33 33 * @type {Actions['pool']} 34 34 */ 35 35 export function pool(tracks) { 36 - lake(tracks); 36 + lake.value = tracks; 37 37 38 38 // TODO: If the pool changes, only remove non-existing tracks 39 39 // instead of resetting the whole future queue. 40 40 // 41 41 // What about past queue items? 42 42 43 - future(fill([])); 43 + future.value = fill([]); 44 44 45 45 // Automatically insert track if there isn't any 46 - if (!now()) return shift(); 46 + if (!now.value) return shift(); 47 47 } 48 48 49 49 /** 50 50 * @type {Actions['shift']} 51 51 */ 52 52 export function shift() { 53 - const n = now(); 54 - const f = future(); 53 + const n = now.value; 54 + const f = future.value; 55 55 56 - now(f[0] ?? null); 56 + now.value = f[0] ?? null; 57 57 58 - if (n) past([...past(), n]); 59 - future(fill(f.slice(1))); 58 + if (n) past.value = [...past.value, n]; 59 + future.value = fill(f.slice(1)); 60 60 } 61 61 62 62 /** 63 63 * @type {Actions['unshift']} 64 64 */ 65 65 export function unshift() { 66 - const p = past(); 66 + const p = past.value; 67 67 if (p.length === 0) return; 68 68 69 - const n = now(); 69 + const n = now.value; 70 70 const [last] = p.splice(p.length - 1, 1); 71 71 72 - now(last ?? null); 73 - if (n) future([n, ...future()]); 72 + now.value = last ?? null; 73 + if (n) future.value = [n, ...future.value]; 74 74 } 75 75 76 76 //////////////////////////////////////////// ··· 80 80 ostiary((port) => { 81 81 // Setup RPC 82 82 83 - define("future", () => future(), port); 84 - define("now", () => now(), port); 85 - define("past", () => past(), port); 83 + define("future", future.get, port); 84 + define("now", now.get, port); 85 + define("past", past.get, port); 86 86 87 87 define("add", add, port); 88 88 define("pool", pool, port); ··· 91 91 92 92 // Communicate state 93 93 94 - effect(() => announce("future", future(), port)); 95 - effect(() => announce("now", now(), port)); 96 - effect(() => announce("past", past(), port)); 94 + effect(() => announce("future", future.value, port)); 95 + effect(() => announce("now", now.value, port)); 96 + effect(() => announce("past", past.value, port)); 97 97 }); 98 98 99 99 //////////////////////////////////////////// ··· 110 110 /** @type {Track[]} */ 111 111 const pool = []; 112 112 113 - let p = new Set(past().map((t) => t.id)); 113 + let p = new Set(past.value.map((t) => t.id)); 114 114 let reducedPool = pool; 115 115 116 - lake().forEach((track) => { 116 + lake.value.forEach((track) => { 117 117 if (p.has(track.id)) { 118 118 p = p.difference(new Set(track.id)); 119 119 } else { ··· 122 122 }); 123 123 124 124 if (reducedPool.length === 0) { 125 - reducedPool = lake(); 125 + reducedPool = lake.value; 126 126 } 127 127 128 128 const poolSelection = arrayShuffle(reducedPool).slice(