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