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 _listen: ReturnType<typeof _listen>;
17 _manage: ReturnType<typeof _manage>;
18};
19
20////////////////////////////////////////////
21// 🛠️
22////////////////////////////////////////////
23
24export function arrayShuffle<T>(array: Array<T>): Array<T> {
25 if (array.length === 0) {
26 return [];
27 }
28
29 array = [...array];
30
31 for (let index = array.length - 1; index > 0; index--) {
32 const randArr = crypto.getRandomValues(new Uint32Array(1));
33 const randVal = randArr[0] / 2 ** 32;
34 const newIndex = Math.floor(randVal * (index + 1));
35 [array[index], array[newIndex]] = [array[newIndex], array[index]];
36 }
37
38 return array;
39}
40
41export function cleanUndefinedValuesForTracks(tracks: Track[]): Track[] {
42 return tracks.map((track) => {
43 const t = { ...track };
44
45 if (t.tags) {
46 if ("album" in t.tags && t.tags.album === undefined) delete t.tags.album;
47 if ("artist" in t.tags && t.tags.artist === undefined) delete t.tags.artist;
48 if ("genre" in t.tags && t.tags.genre === undefined) delete t.tags.genre;
49 if ("year" in t.tags && t.tags.year === undefined) delete t.tags.year;
50
51 if ("of" in t.tags.disc && t.tags.disc.of === undefined) delete t.tags.disc.of;
52 if ("of" in t.tags.track && t.tags.track.of === undefined) delete t.tags.track.of;
53 }
54
55 return t;
56 });
57}
58
59export function comparable(value: unknown) {
60 return xxh32(JSON.stringify(value));
61}
62
63export function endpoint<T extends Record<string, any> = WorkerTasks>(ini: Comlink.Endpoint) {
64 const e = Comlink.wrap<T>(ini);
65 if ("start" in ini && typeof ini.start === "function") ini.start();
66 return e;
67}
68
69export function expose<A extends Record<string, any>>(tasks: A): A {
70 if (globalThis.SharedWorkerGlobalScope && self instanceof SharedWorkerGlobalScope) {
71 self.onconnect = (event: MessageEvent) => {
72 const port = event.ports[0];
73 Comlink.expose(tasks, port);
74 port.start();
75 };
76
77 (self as any).connected = true;
78 } else {
79 Comlink.expose(tasks, self);
80 }
81
82 return tasks;
83}
84
85export function groupTracksPerScheme(
86 tracks: Track[],
87 initial: Record<string, Track[]> = {},
88): Record<string, Track[]> {
89 return tracks.reduce((acc: Record<string, Track[]>, track: Track) => {
90 const scheme = track.uri.split(":", 1)[0];
91 return { ...acc, [scheme]: [...(acc[scheme] || []), track] };
92 }, initial);
93}
94
95export function inIframe() {
96 return window.self !== window.top;
97}
98
99export function initialConnections<C extends Record<string, any>>(ids: string[]) {
100 const connections: Record<string, PromiseWithResolvers<Comlink.Remote<C>>> = {};
101
102 ids.forEach((c) => {
103 connections[c] = Promise.withResolvers<Comlink.Remote<C>>();
104 });
105
106 return connections;
107}
108
109export function isPrimitive(test: unknown) {
110 return test !== Object(test);
111}
112
113export function jsonDecode<T>(a: any): T {
114 return JSON.parse(new TextDecoder().decode(a));
115}
116
117export function jsonEncode<T>(a: T): Uint8Array {
118 return new TextEncoder().encode(JSON.stringify(a));
119}
120
121export function provide<
122 C extends Record<string, any>,
123 A extends Record<string, any>,
124 T extends Record<string, any>,
125>({
126 actions,
127 connections,
128 tasks,
129}: {
130 actions?: A;
131 connections?: Record<string, PromiseWithResolvers<Comlink.Remote<C>>>;
132 tasks?: T;
133}) {
134 const allTasks = expose<WorkerTasks & T>({
135 _listen: _listen<A>(actions || ({} as A)),
136 _manage: _manage<C>(connections || {}),
137 ...(tasks || ({} as T)),
138 });
139
140 return {
141 connections: connections || ({} as Record<string, PromiseWithResolvers<Comlink.Remote<C>>>),
142 tasks: allTasks,
143 };
144}
145
146export async function trackArtworkCacheId(track: Track): Promise<string> {
147 return await crypto.subtle
148 .digest("SHA-256", new TextEncoder().encode(track.uri))
149 .then((a) => Uint8.toString(new Uint8Array(a), "base64url"));
150}
151
152export function transfer<T = unknown>(a: T) {
153 const b = getTransferables(a);
154 return Comlink.transfer(a, b);
155}
156
157// PRIVATE
158
159function _listen<A extends Record<string, any>>(actions: A) {
160 async function handleAction(
161 port: MessagePort,
162 action: {
163 type: "action";
164 id: string;
165 actionId: string;
166 arguments: any;
167 },
168 ) {
169 const result = await actions[action.actionId]?.(action.arguments);
170 return postMessage(port, action.id, result);
171 }
172
173 function postMessage<T>(port: MessagePort, id: string, result: T) {
174 port.postMessage(
175 {
176 type: "actioncomplete",
177 id,
178 result,
179 },
180 {
181 transfer: getTransferables(result),
182 },
183 );
184 }
185
186 return (port: MessagePort) => {
187 Comlink.expose(actions, port);
188
189 port.onmessage = async (message) => {
190 switch (message.data?.type) {
191 case "action":
192 return handleAction(port, message.data);
193 }
194 };
195 };
196}
197
198function _manage<C extends Record<string, any>>(
199 connections: Record<string, PromiseWithResolvers<Comlink.Remote<C>>>,
200) {
201 console.log(connections);
202
203 return (connectionId: string, workerPort: MessagePort) => {
204 let conn = connections[connectionId];
205 const remote = endpoint<C>(workerPort);
206
207 if (!conn) {
208 connections[connectionId] = Promise.withResolvers<Comlink.Remote<C>>();
209 conn = connections[connectionId];
210 }
211
212 conn.resolve(remote);
213 };
214}