Experiment to rebuild Diffuse using web applets.
1import * as URI from "uri-js";
2
3import type { Consult, ConsultGrouping, GroupConsult, Track } from "@applets/core/types.d.ts";
4import { SCHEME } from "./constants";
5import {
6 fetchHandles,
7 fetchHandlesList,
8 groupTracksByHandle,
9 recursiveList,
10 trackHandleId,
11} from "./common";
12import { expose, transfer } from "@scripts/common";
13
14////////////////////////////////////////////
15// ACTIONS
16////////////////////////////////////////////
17const actions = expose({
18 consult,
19 contextualize,
20 groupConsult,
21 list,
22 resolve,
23});
24
25export type Actions = typeof actions;
26
27// Actions
28
29export async function consult(fileUriOrScheme: string): Promise<Consult> {
30 if (!self.FileSystemDirectoryHandle) {
31 return { supported: false, reason: "File System Access API is not supported" };
32 }
33
34 if (!fileUriOrScheme.includes(":")) {
35 if (fileUriOrScheme !== SCHEME) return { supported: false, reason: "Scheme does not match" };
36 return { supported: true, consult: "undetermined" };
37 }
38
39 const handles = await fetchHandles();
40 const uri = URI.parse(fileUriOrScheme);
41 if (uri.scheme !== SCHEME) return { supported: false, reason: "Scheme does not match" };
42 return { supported: true, consult: uri.host && !!handles[uri.host] ? true : false };
43}
44
45export async function contextualize(cachedTracks: Track[]) {}
46
47async function groupConsult(tracks: Track[]): Promise<GroupConsult> {
48 const groups = groupTracksByHandle(tracks);
49 const handles = await fetchHandles();
50
51 const promises = Object.entries(groups).map(async ([handleId, { tracks }]) => {
52 const handle = handles[handleId];
53 const grouping: ConsultGrouping = handle
54 ? { available: true, tracks }
55 : { available: false, reason: "Handle not available" };
56
57 return {
58 key: URI.serialize({ scheme: SCHEME, host: handleId }),
59 grouping,
60 };
61 });
62
63 const entries = (await Promise.all(promises)).map((entry) => [entry.key, entry.grouping]);
64 const obj = Object.fromEntries(entries);
65
66 return transfer(obj);
67}
68
69export async function list(cachedTracks: Track[] = []) {
70 const handles = await fetchHandlesList();
71
72 // Recursive listing of all tracks of available handles
73 const processed: Track[][] = await Promise.all(
74 handles.map(({ id, handle }) => {
75 return recursiveList(handle, id, []);
76 }),
77 );
78
79 // Group tracks by handle id & index by track uri
80 const cache = cachedTracks.reduce((acc: Record<string, Record<string, Track>>, track: Track) => {
81 const handleId = trackHandleId(track);
82 if (!handleId) return acc;
83
84 return { ...acc, [handleId]: { ...(acc[handleId] || {}), [track.uri]: track } };
85 }, {});
86
87 // Replace indexes in groups of which we have the handle.
88 // Keeping around tracks with handles we don't have access to,
89 // and removing tracks that are no longer available (for handles we do have access to).
90 const groups = processed.flat(1).reduce(
91 (acc, track) => {
92 const handleId = trackHandleId(track);
93 if (!handleId) throw new Error("New tracks are missing a handle id!");
94
95 return { ...acc, [handleId]: { ...acc[handleId], [track.uri]: track } };
96 },
97 handles.reduce((acc: Record<string, Record<string, Track>>, handle) => {
98 return { ...acc, [handle.id]: {} };
99 }, cache),
100 );
101
102 // Transform in track list and sort by uri
103 const data = Object.values(groups)
104 .map((tracks) => Object.values(tracks))
105 .flat(1)
106 .sort((a: any, b: any) => {
107 if (a.uri < b.uri) return -1;
108 if (a.uri > b.uri) return 1;
109 return 0;
110 });
111
112 // Fin
113 return transfer(data);
114}
115
116export async function resolve(args: { uri: string }) {
117 const fileUri = args.uri;
118
119 const uri = URI.parse(fileUri);
120 if (uri.scheme !== SCHEME) return undefined;
121 if (!uri.host || !uri.path) return undefined;
122
123 const handles = await fetchHandles();
124 const handle = handles[uri.host];
125 if (!handle) return undefined;
126
127 const path = URI.unescapeComponent(uri.path);
128 const parts = (path.startsWith("/") ? path.slice(1) : path).split("/");
129 const filename = parts[parts.length - 1];
130
131 const dirHandle = await parts
132 .slice(0, -1)
133 .reduce(
134 async (
135 acc: Promise<FileSystemDirectoryHandle>,
136 part: string,
137 ): Promise<FileSystemDirectoryHandle> => {
138 const h = await acc;
139 return await h.getDirectoryHandle(part);
140 },
141 Promise.resolve(handle),
142 );
143
144 const fileHandle = await dirHandle.getFileHandle(filename);
145 const file = await fileHandle.getFile();
146 const url = URL.createObjectURL(file);
147
148 return { expiresAt: Infinity, url };
149}