Experiment to rebuild Diffuse using web applets.
1import * as Uint8 from "uint8arrays";
2import { createEndpoint, type MessageEndpoint } from "@remote-ui/rpc";
3import { xxh32 } from "xxh32";
4
5import type { Track } from "@applets/core/types";
6
7// export { SharedWorkerPolyfill as SharedWorker } from "@okikio/sharedworker";
8export const SharedWorker = globalThis.SharedWorker;
9
10export function arrayShuffle<T>(array: Array<T>): Array<T> {
11 if (array.length === 0) {
12 return [];
13 }
14
15 array = [...array];
16
17 for (let index = array.length - 1; index > 0; index--) {
18 const randArr = crypto.getRandomValues(new Uint32Array(1));
19 const randVal = randArr[0] / 2 ** 32;
20 const newIndex = Math.floor(randVal * (index + 1));
21 [array[index], array[newIndex]] = [array[newIndex], array[index]];
22 }
23
24 return array;
25}
26
27export function cleanUndefinedValuesForTracks(tracks: Track[]): Track[] {
28 return tracks.map((track) => {
29 const t = { ...track };
30
31 if (t.tags) {
32 if ("album" in t.tags && t.tags.album === undefined) delete t.tags.album;
33 if ("artist" in t.tags && t.tags.artist === undefined) delete t.tags.artist;
34 if ("genre" in t.tags && t.tags.genre === undefined) delete t.tags.genre;
35 if ("year" in t.tags && t.tags.year === undefined) delete t.tags.year;
36
37 if ("of" in t.tags.disc && t.tags.disc.of === undefined) delete t.tags.disc.of;
38 if ("of" in t.tags.track && t.tags.track.of === undefined) delete t.tags.track.of;
39 }
40
41 return t;
42 });
43}
44
45export function comparable(value: unknown) {
46 return xxh32(JSON.stringify(value));
47}
48
49export function endpoint<T extends Record<string, any>>(ini: MessageEndpoint) {
50 const e = createEndpoint<T>(ini);
51 if ("start" in ini && typeof ini.start === "function") ini.start();
52 return e;
53}
54
55export function expose<T extends Record<string, any>>(actions: T): T {
56 if (globalThis.SharedWorkerGlobalScope && self instanceof SharedWorkerGlobalScope) {
57 self.onconnect = (event: MessageEvent) => {
58 const port = event.ports[0];
59 createEndpoint<T>(port).expose(actions);
60 port.start();
61 };
62
63 (self as any).connected = true;
64 } else {
65 createEndpoint<T>(self).expose(actions);
66 }
67
68 return actions;
69}
70
71export function groupTracksPerScheme(
72 tracks: Track[],
73 initial: Record<string, Track[]> = {},
74): Record<string, Track[]> {
75 return tracks.reduce((acc: Record<string, Track[]>, track: Track) => {
76 const scheme = track.uri.split(":", 1)[0];
77 return { ...acc, [scheme]: [...(acc[scheme] || []), track] };
78 }, initial);
79}
80
81export function inIframe() {
82 return window.self !== window.top;
83}
84
85export function isPrimitive(test: unknown) {
86 return test !== Object(test);
87}
88
89export function jsonDecode<T>(a: any): T {
90 return JSON.parse(new TextDecoder().decode(a));
91}
92
93export function jsonEncode<T>(a: T): Uint8Array {
94 return new TextEncoder().encode(JSON.stringify(a));
95}
96
97export async function trackArtworkCacheId(track: Track): Promise<string> {
98 return await crypto.subtle
99 .digest("SHA-256", new TextEncoder().encode(track.uri))
100 .then((a) => Uint8.toString(new Uint8Array(a), "base64url"));
101}