Experiment to rebuild Diffuse using web applets.
0

Configure Feed

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

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 return (connectionId: string, workerPort: MessagePort) => { 202 let conn = connections[connectionId]; 203 const remote = endpoint<C>(workerPort); 204 205 if (!conn) { 206 connections[connectionId] = Promise.withResolvers<Comlink.Remote<C>>(); 207 conn = connections[connectionId]; 208 } 209 210 conn.resolve(remote); 211 }; 212}