Experiment to rebuild Diffuse using web applets.
1import * as Uint8 from "uint8arrays";
2import * as Comlink from "comlink";
3import { xxh32 } from "xxh32";
4import { getTransferables } from "@okikio/transferables";
5
6import type { Track } from "@applets/core/types";
7
8// export { SharedWorkerPolyfill as SharedWorker } from "@okikio/sharedworker";
9export const SharedWorker = globalThis.SharedWorker;
10
11////////////////////////////////////////////
12// 🌳
13////////////////////////////////////////////
14
15export type WorkerTasks = {
16 listenForActions: ReturnType<typeof handleWorkerActions>;
17};
18
19////////////////////////////////////////////
20// 🛠️
21////////////////////////////////////////////
22
23export function arrayShuffle<T>(array: Array<T>): Array<T> {
24 if (array.length === 0) {
25 return [];
26 }
27
28 array = [...array];
29
30 for (let index = array.length - 1; index > 0; index--) {
31 const randArr = crypto.getRandomValues(new Uint32Array(1));
32 const randVal = randArr[0] / 2 ** 32;
33 const newIndex = Math.floor(randVal * (index + 1));
34 [array[index], array[newIndex]] = [array[newIndex], array[index]];
35 }
36
37 return array;
38}
39
40export function cleanUndefinedValuesForTracks(tracks: Track[]): Track[] {
41 return tracks.map((track) => {
42 const t = { ...track };
43
44 if (t.tags) {
45 if ("album" in t.tags && t.tags.album === undefined) delete t.tags.album;
46 if ("artist" in t.tags && t.tags.artist === undefined) delete t.tags.artist;
47 if ("genre" in t.tags && t.tags.genre === undefined) delete t.tags.genre;
48 if ("year" in t.tags && t.tags.year === undefined) delete t.tags.year;
49
50 if ("of" in t.tags.disc && t.tags.disc.of === undefined) delete t.tags.disc.of;
51 if ("of" in t.tags.track && t.tags.track.of === undefined) delete t.tags.track.of;
52 }
53
54 return t;
55 });
56}
57
58export function comparable(value: unknown) {
59 return xxh32(JSON.stringify(value));
60}
61
62export function endpoint<T extends Record<string, any> = WorkerTasks>(ini: Comlink.Endpoint) {
63 const e = Comlink.wrap<T>(ini);
64 if ("start" in ini && typeof ini.start === "function") ini.start();
65 return e;
66}
67
68export function expose<A extends Record<string, any>>(tasks: A): A {
69 if (globalThis.SharedWorkerGlobalScope && self instanceof SharedWorkerGlobalScope) {
70 self.onconnect = (event: MessageEvent) => {
71 const port = event.ports[0];
72 Comlink.expose(tasks, port);
73 port.start();
74 };
75
76 (self as any).connected = true;
77 } else {
78 Comlink.expose(tasks, self);
79 }
80
81 return tasks;
82}
83
84export function groupTracksPerScheme(
85 tracks: Track[],
86 initial: Record<string, Track[]> = {},
87): Record<string, Track[]> {
88 return tracks.reduce((acc: Record<string, Track[]>, track: Track) => {
89 const scheme = track.uri.split(":", 1)[0];
90 return { ...acc, [scheme]: [...(acc[scheme] || []), track] };
91 }, initial);
92}
93
94export function inIframe() {
95 return window.self !== window.top;
96}
97
98export function isPrimitive(test: unknown) {
99 return test !== Object(test);
100}
101
102export function jsonDecode<T>(a: any): T {
103 return JSON.parse(new TextDecoder().decode(a));
104}
105
106export function jsonEncode<T>(a: T): Uint8Array {
107 return new TextEncoder().encode(JSON.stringify(a));
108}
109
110export function provide<A extends Record<string, any>, B extends Record<string, any>>(
111 actions: A,
112 tasks: B = {} as B,
113) {
114 return expose<WorkerTasks & B>({
115 listenForActions: handleWorkerActions(actions),
116 ...tasks,
117 });
118}
119
120export async function trackArtworkCacheId(track: Track): Promise<string> {
121 return await crypto.subtle
122 .digest("SHA-256", new TextEncoder().encode(track.uri))
123 .then((a) => Uint8.toString(new Uint8Array(a), "base64url"));
124}
125
126export function transfer<T = unknown>(a: T) {
127 const b = getTransferables(a);
128 return Comlink.transfer(a, b);
129}
130
131// PRIVATE
132
133function handleWorkerActions<A extends Record<string, any>>(actions: A) {
134 async function handleAction(
135 port: MessagePort,
136 action: {
137 type: "action";
138 id: string;
139 actionId: string;
140 arguments: any;
141 },
142 ) {
143 const result = await actions[action.actionId]?.(action.arguments);
144 return postMessage(port, action.id, result);
145 }
146
147 function postMessage<T>(port: MessagePort, id: string, result: T) {
148 port.postMessage(
149 {
150 type: "actioncomplete",
151 id,
152 result,
153 },
154 {
155 transfer: getTransferables(result),
156 },
157 );
158 }
159
160 return (port: MessagePort) => {
161 Comlink.expose(actions, port);
162
163 port.onmessage = async (message) => {
164 switch (message.data?.type) {
165 case "action":
166 return handleAction(port, message.data);
167 }
168 };
169 };
170}