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