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 } 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 return Object.fromEntries(entries);
65}
66
67export async function list(cachedTracks: Track[] = []) {
68 const handles = await fetchHandlesList();
69
70 // Recursive listing of all tracks of available handles
71 const processed: Track[][] = await Promise.all(
72 handles.map(({ id, handle }) => {
73 return recursiveList(handle, id, []);
74 }),
75 );
76
77 // Group tracks by handle id & index by track uri
78 const cache = cachedTracks.reduce((acc: Record<string, Record<string, Track>>, track: Track) => {
79 const handleId = trackHandleId(track);
80 if (!handleId) return acc;
81
82 return { ...acc, [handleId]: { ...(acc[handleId] || {}), [track.uri]: track } };
83 }, {});
84
85 // Replace indexes in groups of which we have the handle.
86 // Keeping around tracks with handles we don't have access to,
87 // and removing tracks that are no longer available (for handles we do have access to).
88 const groups = processed.flat(1).reduce(
89 (acc, track) => {
90 const handleId = trackHandleId(track);
91 if (!handleId) throw new Error("New tracks are missing a handle id!");
92
93 return { ...acc, [handleId]: { ...acc[handleId], [track.uri]: track } };
94 },
95 handles.reduce((acc: Record<string, Record<string, Track>>, handle) => {
96 return { ...acc, [handle.id]: {} };
97 }, cache),
98 );
99
100 // Transform in track list and sort by uri
101 const data = Object.values(groups)
102 .map((tracks) => Object.values(tracks))
103 .flat(1)
104 .sort((a: any, b: any) => {
105 if (a.uri < b.uri) return -1;
106 if (a.uri > b.uri) return 1;
107 return 0;
108 });
109
110 // Fin
111 return data;
112}
113
114export async function resolve(args: { uri: string }) {
115 const fileUri = args.uri;
116
117 const uri = URI.parse(fileUri);
118 if (uri.scheme !== SCHEME) return undefined;
119 if (!uri.host || !uri.path) return undefined;
120
121 const handles = await fetchHandles();
122 const handle = handles[uri.host];
123 if (!handle) return undefined;
124
125 const path = URI.unescapeComponent(uri.path);
126 const parts = (path.startsWith("/") ? path.slice(1) : path).split("/");
127 const filename = parts[parts.length - 1];
128
129 const dirHandle = await parts
130 .slice(0, -1)
131 .reduce(
132 async (
133 acc: Promise<FileSystemDirectoryHandle>,
134 part: string,
135 ): Promise<FileSystemDirectoryHandle> => {
136 const h = await acc;
137 return await h.getDirectoryHandle(part);
138 },
139 Promise.resolve(handle),
140 );
141
142 const fileHandle = await dirHandle.getFileHandle(filename);
143 const file = await fileHandle.getFile();
144 const url = URL.createObjectURL(file);
145
146 return { expiresAt: Infinity, url };
147}