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