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

Configure Feed

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

at v4 137 lines 4.3 kB view raw
1import { effect } from "~/common/signal.js"; 2 3/** 4 * @import {SignalReader} from "~/common/signal.d.ts"; 5 */ 6 7/** 8 * Merges two track arrays by `id`. Tracks from `incoming` replace any 9 * matching tracks in `existing`; unmatched existing tracks are preserved. 10 * 11 * @template {{ id: string }} T 12 * @param {T[]} existing 13 * @param {T[]} incoming 14 * @returns {T[]} 15 * 16 * @example Returns incoming tracks when existing is empty 17 * ```js 18 * import { mergeTracks } from "~/common/output.js"; 19 * 20 * const result = mergeTracks([], [{ id: "a" }, { id: "b" }]); 21 * if (result.map(t => t.id).join(",") !== "a,b") throw new Error("unexpected result"); 22 * ``` 23 * 24 * @example Returns existing tracks when incoming is empty 25 * ```js 26 * import { mergeTracks } from "~/common/output.js"; 27 * 28 * const result = mergeTracks([{ id: "a" }, { id: "b" }], []); 29 * if (result.map(t => t.id).join(",") !== "a,b") throw new Error("unexpected result"); 30 * ``` 31 * 32 * @example Preserves tracks not present in incoming 33 * ```js 34 * import { mergeTracks } from "~/common/output.js"; 35 * 36 * const result = mergeTracks([{ id: "a" }, { id: "b" }], [{ id: "c" }]); 37 * if (result.map(t => t.id).join(",") !== "a,b,c") throw new Error("unexpected result"); 38 * ``` 39 * 40 * @example Replaces existing track with incoming version when ids match 41 * ```js 42 * import { mergeTracks } from "~/common/output.js"; 43 * 44 * const result = mergeTracks([{ id: "a", uri: "old://a" }], [{ id: "a", uri: "new://a" }]); 45 * if (result.length !== 1) throw new Error("expected length 1"); 46 * if (result[0].uri !== "new://a") throw new Error("expected new uri"); 47 * ``` 48 * 49 * @example Preserves other-source tracks when incoming covers one source 50 * ```js 51 * import { mergeTracks } from "~/common/output.js"; 52 * 53 * const existing = [ 54 * { id: "s3-1", uri: "s3://bucket/a.flac" }, 55 * { id: "s3-2", uri: "s3://bucket/b.flac" }, 56 * { id: "wd-1", uri: "webdav://server/c.flac" }, 57 * ]; 58 * const incoming = [ 59 * { id: "s3-1", uri: "s3://bucket/a.flac" }, 60 * { id: "s3-3", uri: "s3://bucket/d.flac" }, 61 * ]; 62 * const result = mergeTracks(existing, incoming); 63 * const sorted = result.map(t => t.id).sort().join(","); 64 * if (sorted !== "s3-1,s3-2,s3-3,wd-1") throw new Error("unexpected result: " + sorted); 65 * ``` 66 * 67 * @example Incoming tracks appear after preserved existing tracks 68 * ```js 69 * import { mergeTracks } from "~/common/output.js"; 70 * 71 * const result = mergeTracks([{ id: "x" }], [{ id: "y" }, { id: "z" }]); 72 * if (result.map(t => t.id).join(",") !== "x,y,z") throw new Error("unexpected result"); 73 * ``` 74 * 75 * @example Handles both arrays empty 76 * ```js 77 * import { mergeTracks } from "~/common/output.js"; 78 * 79 * const result = mergeTracks([], []); 80 * if (result.length !== 0) throw new Error("expected empty result"); 81 * ``` 82 */ 83export function mergeTracks(existing, incoming) { 84 const ids = new Set(incoming.map((t) => t.id)); 85 const preserved = existing.filter((t) => !ids.has(t.id)); 86 return [...preserved, ...incoming]; 87} 88 89/** 90 * @template T 91 * @param {{ collection: SignalReader<{ state: "loading" } | { state: "loaded"; data: T }> }} output 92 * @returns {Promise<T>} 93 * 94 * @example Resolves immediately when collection is already loaded 95 * ```js 96 * import { data } from "~/common/output.js"; 97 * import { signal } from "~/common/signal.js"; 98 * 99 * const col = signal(JSON.parse('{"state":"loaded","data":["a","b"]}')); 100 * const result = await data({ collection: col.get }); 101 * if (result.join(",") !== "a,b") throw new Error("expected ['a', 'b']"); 102 * ``` 103 * 104 * @example Waits for collection to transition to loaded 105 * ```js 106 * import { data } from "~/common/output.js"; 107 * import { signal } from "~/common/signal.js"; 108 * 109 * const col = signal(JSON.parse('{"state":"loading"}')); 110 * const promise = data({ collection: col.get }); 111 * 112 * await Promise.resolve(); 113 * col.set({ state: "loaded", data: [1, 2, 3] }); 114 * 115 * const result = await promise; 116 * if (result.join(",") !== "1,2,3") throw new Error("expected [1, 2, 3]"); 117 * ``` 118 */ 119export async function data(output) { 120 return await new Promise((resolve) => { 121 let resolved = false; 122 123 const stop = effect(() => { 124 if (resolved) { 125 stop(); 126 return; 127 } 128 129 const col = output.collection(); 130 131 if (col.state === "loaded") { 132 resolved = true; 133 resolve(col.data); 134 } 135 }); 136 }); 137}