forked from
tokono.ma/diffuse
A music player that connects to your cloud/distributed storage.
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}