alpha
Login
or
Join now
tokono.ma
/
diffuse-applets
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Experiment to rebuild Diffuse using web applets.
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Overview
Issues
Pulls
Pipelines
feat: redesign output system
author
Steven Vandevelde
date
1 year ago
(Jun 1, 2025, 2:43 PM +0200)
commit
1573c9b0
1573c9b0c43c219fac853543ccc783bee82b25ce
parent
6dc1dd8e
6dc1dd8ed92207dd2614a214c3d19ddb6db9716b
+437
-441
28 changed files
Expand all
Collapse all
Unified
Split
deno.lock
package-lock.json
package.json
src
components
List.astro
pages
configurator
output
_applet.astro
_manifest.json
core
types.d.ts
index.astro
input
native-fs
_applet.astro
s3
_applet.astro
orchestrator
input-cache
_applet.astro
output-management
_applet.astro
_manifest.json
types.d.ts
single-queue
_applet.astro
output
indexed-db
_applet.astro
_manifest.json
native-fs
_applet.astro
_manifest.json
storacha-automerge
_applet.astro
_manifest.json
index.astro
processor
metadata-fetcher
_applet.astro
scripts
applets
common.ts
input
common.ts
output
common.ts
themes
pilot
index.ts
webamp
index.ts
+1
deno.lock
Reviewed
···
29
29
"npm:@tokenizer/http@~0.9.2",
30
30
"npm:@tokenizer/range@0.13",
31
31
"npm:@types/throttle-debounce@^5.0.2",
32
32
+
"npm:@types/wicg-file-system-access@^2023.10.6",
32
33
"npm:astro-purgecss@^5.2.2",
33
34
"npm:astro-scope@^3.0.1",
34
35
"npm:astro@^5.7.4",
+8
package-lock.json
Reviewed
···
27
27
},
28
28
"devDependencies": {
29
29
"@types/throttle-debounce": "^5.0.2",
30
30
+
"@types/wicg-file-system-access": "^2023.10.6",
30
31
"astro": "^5.7.4",
31
32
"astro-purgecss": "^5.2.2",
32
33
"astro-scope": "^3.0.1",
···
1911
1912
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
1912
1913
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
1913
1914
"dev": true
1915
1915
+
},
1916
1916
+
"node_modules/@types/wicg-file-system-access": {
1917
1917
+
"version": "2023.10.6",
1918
1918
+
"resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2023.10.6.tgz",
1919
1919
+
"integrity": "sha512-YO/183gNRzZFSdKu+ikkD7ambAj4PhgjFAF2A/Mw/7wroSF6ne8r804RkpZzqrJ/F6DO2/IYlQF/ULOZ/bhKyA==",
1920
1920
+
"dev": true,
1921
1921
+
"license": "MIT"
1914
1922
},
1915
1923
"node_modules/@ungap/structured-clone": {
1916
1924
"version": "1.3.0",
+1
package.json
Reviewed
···
22
22
},
23
23
"devDependencies": {
24
24
"@types/throttle-debounce": "^5.0.2",
25
25
+
"@types/wicg-file-system-access": "^2023.10.6",
25
26
"astro": "^5.7.4",
26
27
"astro-purgecss": "^5.2.2",
27
28
"astro-scope": "^3.0.1",
+1
-1
src/components/List.astro
Reviewed
···
6
6
{
7
7
items.map((item: { title: string; url: string }) => (
8
8
<li>
9
9
-
{item.title.startsWith("(TODO) ") ? (
9
9
+
{item.title.startsWith("(TODO) ") || item.title.startsWith("(WIP) ") ? (
10
10
<span>{item.title}</span>
11
11
) : (
12
12
<a href={item.url}>{item.title}</a>
+56
-93
src/pages/configurator/output/_applet.astro
Reviewed
···
28
28
</p>
29
29
</main>
30
30
31
31
-
<style>
32
32
-
#iframes {
31
31
+
<style is:global>
32
32
+
iframe {
33
33
display: none;
34
34
}
35
35
</style>
···
40
40
import { type Signal, computed, effect, signal } from "spellcaster/spellcaster.js";
41
41
import { type ElementConfigurator, repeat, text } from "spellcaster/hyperscript.js";
42
42
43
43
+
import type { ManagedOutput } from "@applets/core/types";
43
44
import { applet, hs, register } from "@scripts/applets/common";
44
44
-
import type { OutputGetter, OutputSetter } from "@applets/core/types.d.ts";
45
45
+
import { INITIAL_MANAGED_OUTPUT } from "@scripts/output/common";
46
46
+
import { Applet, AppletEvent } from "@web-applets/sdk";
45
47
46
48
const METHODS = ["browser", "custom", "device"] as const;
47
49
50
50
+
const CONNECTIONS = {
51
51
+
browser: "../../../output/indexed-db/",
52
52
+
custom: undefined,
53
53
+
device: "../../../output/native-fs/",
54
54
+
};
55
55
+
48
56
type Method = (typeof METHODS)[number];
49
57
type List<M extends Method = Method> = Map<string, ListItem<M>>;
50
58
type ListItem<M> = { activated: boolean; icon: string; method: M; title: string };
···
62
70
////////////////////////////////////////////
63
71
// SETUP
64
72
////////////////////////////////////////////
65
65
-
const context = register<{ ready: boolean }>();
73
73
+
const context = register<ManagedOutput>();
66
74
67
67
-
// Applets container
68
68
-
const container = document.createElement("div");
69
69
-
container.id = "iframes";
70
70
-
document.body.appendChild(container);
75
75
+
// Applet connections
76
76
+
const connections: Record<string, Applet<ManagedOutput>> = {};
71
77
72
72
-
// TODO: Use https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API
73
73
-
// so that the other instances of this applet can be notified
74
74
-
// that something changed?
75
75
-
//
76
76
-
// TODO: Should migrate + merge data when switching storages
77
77
-
// Or button to do that?
78
78
-
//
79
79
-
// TODO: Pressing ESC button should hide dialog/modal?
78
78
+
async function connection(method: Method) {
79
79
+
if (connections[method]) return connections[method];
80
80
+
81
81
+
let href;
82
82
+
83
83
+
if (method === "custom") {
84
84
+
href = localStorage.getItem(CUSTOM_KEY);
85
85
+
if (!href) throw new Error("Missing custom applet URL");
86
86
+
} else {
87
87
+
href = CONNECTIONS[method];
88
88
+
if (!href) throw new Error("No href defined for this connection method.");
89
89
+
}
80
90
81
81
-
// Applet connections
82
82
-
const storage = {
83
83
-
output: {
84
84
-
indexedDB: await applet("../../../output/indexed-db/", { container }),
85
85
-
nativeFs: await applet("../../../output/native-fs/", { container }),
86
86
-
},
87
87
-
};
91
91
+
connections[method] = await applet(href);
92
92
+
return connections[method];
93
93
+
}
88
94
89
95
// Initial state
90
90
-
context.data = { ready: false };
96
96
+
context.data = INITIAL_MANAGED_OUTPUT;
91
97
92
98
// Signals
93
99
const stored = localStorage.getItem(LOCALSTORAGE_KEY);
···
96
102
);
97
103
98
104
effect(() => {
99
99
-
localStorage.setItem(LOCALSTORAGE_KEY, active());
105
105
+
const method = active();
106
106
+
localStorage.setItem(LOCALSTORAGE_KEY, method);
107
107
+
108
108
+
// Monitor data
109
109
+
// TODO: encode/decode data?
110
110
+
(async () => {
111
111
+
const conn = await connection(method);
112
112
+
context.data = conn.data;
113
113
+
conn.addEventListener("data", dataEventHandler);
114
114
+
})();
100
115
});
101
116
117
117
+
function dataEventHandler(event: AppletEvent) {
118
118
+
context.data = event.data as ManagedOutput;
119
119
+
}
120
120
+
102
121
// Mount + Unmount
103
122
async function mountStorageMethod(method: Method) {
104
123
switch (method) {
105
105
-
case "browser":
106
106
-
await storage.output.indexedDB.sendAction("mount");
107
107
-
setActive(method);
108
108
-
break;
109
124
case "custom":
110
125
setModalIsOpen(true);
111
126
break;
112
112
-
case "device":
113
113
-
await storage.output.nativeFs.sendAction("mount");
127
127
+
default:
128
128
+
const conn = await connection(method);
129
129
+
await conn.sendAction("mount");
114
130
setActive(method);
115
115
-
break;
116
131
}
117
132
}
118
133
119
134
async function unmountStorageMethod(method: Method) {
120
120
-
switch (method) {
121
121
-
case "browser":
122
122
-
await storage.output.indexedDB.sendAction("unmount");
123
123
-
break;
124
124
-
case "custom":
125
125
-
const applet = await connectToCustomApplet();
126
126
-
await applet.sendAction("unmount");
127
127
-
localStorage.removeItem(CUSTOM_KEY);
128
128
-
break;
129
129
-
case "device":
130
130
-
await storage.output.nativeFs.sendAction("unmount");
131
131
-
break;
132
132
-
}
135
135
+
const conn = await connection(method);
136
136
+
conn.removeEventListener("data", dataEventHandler);
137
137
+
await conn.sendAction("unmount");
133
138
}
134
139
135
140
////////////////////////////////////////////
···
298
303
const url = input.value;
299
304
setCustomState("connecting");
300
305
301
301
-
const apl = await applet(url, { container }).catch((err) => {
306
306
+
const apl = await applet(url).catch((err) => {
302
307
setCustomState({ error: "Failed to connect" });
303
308
throw err;
304
309
});
305
310
306
311
let missingAction;
307
312
308
308
-
["get", "put", "mount", "unmount"].forEach((method) => {
313
313
+
["tracks", "mount", "unmount"].forEach((method) => {
309
314
if (!apl.manifest.actions?.[method]) missingAction = method;
310
315
});
311
316
···
322
327
setCustomState("waiting");
323
328
}
324
329
325
325
-
// 🛠️
326
326
-
async function connectToCustomApplet() {
327
327
-
const url = localStorage.getItem(CUSTOM_KEY);
328
328
-
if (!url) throw new Error("Missing custom applet URL");
329
329
-
return await applet(url, { container });
330
330
-
}
331
331
-
332
330
// Add to DOM
333
331
document.querySelector("main")?.appendChild(Modal());
334
332
335
333
////////////////////////////////////////////
336
334
// ACTIONS
337
335
////////////////////////////////////////////
338
338
-
const get: OutputGetter = async (args) => {
339
339
-
let data: Uint8Array | undefined;
340
340
-
341
341
-
switch (active()) {
342
342
-
case "browser": {
343
343
-
return await storage.output.indexedDB.sendAction<Uint8Array | undefined>("get", args);
344
344
-
}
345
345
-
case "custom":
346
346
-
const a = await connectToCustomApplet();
347
347
-
return await a.sendAction<Uint8Array | undefined>("get", args);
348
348
-
case "device": {
349
349
-
return await storage.output.nativeFs.sendAction<Uint8Array | undefined>("get", args);
350
350
-
}
351
351
-
default: {
352
352
-
return undefined;
353
353
-
}
354
354
-
}
336
336
+
const tracks = async (...args: unknown[]) => {
337
337
+
const conn = await connection(active());
338
338
+
await conn.sendAction("tracks", ...args);
355
339
};
356
340
357
357
-
const put: OutputSetter = async (args) => {
358
358
-
switch (active()) {
359
359
-
case "browser":
360
360
-
await storage.output.indexedDB.sendAction("put", args);
361
361
-
break;
362
362
-
case "custom":
363
363
-
const a = await connectToCustomApplet();
364
364
-
await a.sendAction("put", args);
365
365
-
break;
366
366
-
case "device":
367
367
-
await storage.output.nativeFs.sendAction("put", args);
368
368
-
break;
369
369
-
}
370
370
-
};
371
371
-
372
372
-
context.setActionHandler("get", get);
373
373
-
context.setActionHandler("put", put);
374
374
-
375
375
-
////////////////////////////////////////////
376
376
-
// 🚦
377
377
-
////////////////////////////////////////////
378
378
-
context.data = { ready: true };
341
341
+
context.setActionHandler("tracks", tracks);
379
342
</script>
+8
-20
src/pages/configurator/output/_manifest.json
Reviewed
···
3
3
"title": "Diffuse Configurator | Output",
4
4
"entrypoint": "index.html",
5
5
"actions": {
6
6
-
"get": {
7
7
-
"title": "Get",
8
8
-
"description": "Get data from the configured storage",
9
9
-
"params_schema": {
10
10
-
"type": "object",
11
11
-
"properties": {
12
12
-
"name": { "type": "string" }
13
13
-
},
14
14
-
"required": ["name"]
15
15
-
}
16
16
-
},
17
17
-
"put": {
18
18
-
"title": "Put",
19
19
-
"description": "Put data on the configured storage",
6
6
+
"tracks": {
7
7
+
"title": "Tracks",
8
8
+
"description": "Store or retrieve tracks. Passing in an array of tracks, stores them; passing no parameter, retrieves them.",
20
9
"params_schema": {
21
21
-
"type": "object",
22
22
-
"properties": {
23
23
-
"data": { "type": "object" },
24
24
-
"name": { "type": "string" }
25
25
-
},
26
26
-
"required": ["data", "name"]
10
10
+
"type": "array",
11
11
+
"description": "List of tracks",
12
12
+
"items": {
13
13
+
"type": "object"
14
14
+
}
27
15
}
28
16
}
29
17
}
+10
-5
src/pages/core/types.d.ts
Reviewed
···
1
1
/* OUTPUT */
2
2
3
3
-
export interface Output<T = TrackTags> {
4
4
-
tracks: Track<T>[];
3
3
+
export interface Output<S = TrackStats, T = TrackTags> {
4
4
+
tracks: Track<S, T>[];
5
5
}
6
6
7
7
-
export type OutputGetter = ({ name }: { name: string }) => Promise<Uint8Array | undefined>;
8
8
-
export type OutputSetter = ({ data, name }: { data: Uint8Array; name: string }) => Promise<void>;
7
7
+
export interface ManagedOutput<S = TrackStats, T = TrackTags> {
8
8
+
tracks: {
9
9
+
cacheId: string;
10
10
+
state: "loading" | "loaded";
11
11
+
collection: Track<S, T>[];
12
12
+
};
13
13
+
}
9
14
10
15
/* TRACKS */
11
16
12
17
export type ResolvedUri = undefined | { url: string; expiresAt: number }; // TODO: Streams?
13
18
14
14
-
export interface Track<Tags = TrackTags, Stats = TrackStats> {
19
19
+
export interface Track<Stats = TrackStats, Tags = TrackTags> {
15
20
id: string;
16
21
17
22
stats?: Stats;
+1
-1
src/pages/index.astro
Reviewed
···
41
41
42
42
const orchestrators = [
43
43
{ url: "orchestrator/input-cache/", title: "Input caching" },
44
44
-
{ url: "orchestrator/output-management/", title: "Output management" },
45
44
{ url: "orchestrator/single-queue/", title: "Single queue" },
46
45
];
47
46
48
47
const output = [
49
48
{ url: "output/indexed-db/", title: "IndexedDB" },
50
49
{ url: "output/native-fs/", title: "Native File System" },
50
50
+
{ url: "output/storacha-automerge", title: "Storacha Storage + Automerge CRDT" },
51
51
{ url: "output/todo/", title: "(TODO) Keyhive/Beelay" },
52
52
{ url: "output/todo/", title: "(TODO) Dialog DB" },
53
53
];
+1
-1
src/pages/input/native-fs/_applet.astro
Reviewed
···
24
24
import QS from "query-string";
25
25
26
26
import type { Track } from "@applets/core/types.d.ts";
27
27
-
import { isAudioFile } from "@scripts/inputs/common";
27
27
+
import { isAudioFile } from "@scripts/input/common";
28
28
import { register } from "@scripts/applets/common";
29
29
30
30
import manifest from "./_manifest.json";
+1
-1
src/pages/input/s3/_applet.astro
Reviewed
···
45
45
import QS from "query-string";
46
46
47
47
import type { Track } from "@applets/core/types.d.ts";
48
48
-
import { isAudioFile } from "@scripts/inputs/common";
48
48
+
import { isAudioFile } from "@scripts/input/common";
49
49
import { register } from "@scripts/applets/common";
50
50
import manifest from "./_manifest.json";
51
51
+7
-10
src/pages/orchestrator/input-cache/_applet.astro
Reviewed
···
1
1
<script>
2
2
-
import type { ResolvedUri, Track } from "@applets/core/types.d.ts";
2
2
+
import type { ManagedOutput, ResolvedUri, Track } from "@applets/core/types.d.ts";
3
3
4
4
import {
5
5
applet,
···
11
11
////////////////////////////////////////////
12
12
// SETUP
13
13
////////////////////////////////////////////
14
14
-
import type * as OutputOrchestrator from "@applets/orchestrator/output-management/types.d.ts";
15
15
-
16
14
const context = register<{ isProcessing: boolean; ready: boolean }>();
17
15
18
16
// Initial data
···
24
22
// Applet connections
25
23
const configurator = {
26
24
input: await applet("../../configurator/input"),
27
27
-
};
28
28
-
29
29
-
const orchestrator = {
30
30
-
output: await applet<OutputOrchestrator.State>("../../orchestrator/output-management"),
25
25
+
output: await applet<ManagedOutput>("../../configurator/output"),
31
26
};
32
27
33
28
const processor = {
···
35
30
};
36
31
37
32
// 🚀
38
38
-
waitUntilAppletData(orchestrator.output, (d) => !!d?.hasSyncedTracks).then(() => process());
33
33
+
waitUntilAppletData(configurator.output, (d) => d?.tracks.state === "loaded").then(() =>
34
34
+
process(),
35
35
+
);
39
36
40
37
////////////////////////////////////////////
41
38
// ACTIONS
···
49
46
50
47
await waitUntilAppletIsReady(configurator.input);
51
48
52
52
-
const cachedTracks = orchestrator.output.data.tracks.collection;
49
49
+
const cachedTracks = configurator.output.data.tracks.collection;
53
50
await configurator.input.sendAction("contextualize", cachedTracks);
54
51
55
52
const tracks = await configurator.input.sendAction<Track[]>("list", cachedTracks, {
···
97
94
);
98
95
99
96
// Save
100
100
-
await orchestrator.output.sendAction("tracks", tracksWithMetadata, {
97
97
+
await configurator.output.sendAction("tracks", tracksWithMetadata, {
101
98
timeoutDuration: 60000 * 5,
102
99
});
103
100
-172
src/pages/orchestrator/output-management/_applet.astro
Reviewed
···
1
1
-
<!-- TODO: Button to export all user/output data. --><!-- TODO: Button to import data? -->
2
2
-
<script>
3
3
-
import { debounce } from "throttle-debounce";
4
4
-
import * as Automerge from "@automerge/automerge";
5
5
-
import * as Uint8 from "uint8arrays";
6
6
-
7
7
-
import type { Track } from "@applets/core/types.d.ts";
8
8
-
import type { State } from "./types.d.ts";
9
9
-
import { applet, register, waitUntilAppletIsReady } from "@scripts/applets/common";
10
10
-
11
11
-
type TracksDoc = { collection: Track[] };
12
12
-
13
13
-
const TRACKS_INITIAL_DOC = Automerge.load<TracksDoc>(
14
14
-
Uint8.fromString(
15
15
-
"hW9Kg5qsIsEAeAEQkb+c0IkXTSWyGqZ6jXtFxgETwM42fL3CMN78UZ4Qa3a9RfOrJu5qKzlM7IxwAUXelQYBAgMCEwIjBkACVgIHFQwhAiMCNAFCAlYCgAECfwB/AX8Bf+ub7MEGfwB/B38KY29sbGVjdGlvbn8AfwEBfwJ/AH8AAA",
16
16
-
"base64",
17
17
-
),
18
18
-
);
19
19
-
20
20
-
////////////////////////////////////////////
21
21
-
// SETUP
22
22
-
////////////////////////////////////////////
23
23
-
const context = register<State>();
24
24
-
25
25
-
// Data codec
26
26
-
const codec = {
27
27
-
decode(data: any) {
28
28
-
return {
29
29
-
hasSyncedTracks: data.hasSyncedTracks,
30
30
-
ready: context.data.ready,
31
31
-
tracks: Automerge.load<TracksDoc>(data.tracks),
32
32
-
};
33
33
-
},
34
34
-
35
35
-
encode(data: State) {
36
36
-
return {
37
37
-
hasSyncedTracks: true,
38
38
-
ready: context.data.ready,
39
39
-
tracks: Automerge.save(data.tracks),
40
40
-
};
41
41
-
},
42
42
-
};
43
43
-
44
44
-
context.codec = codec;
45
45
-
46
46
-
// Initial data
47
47
-
context.data = {
48
48
-
// Empty tracks collection, DO NOT CHANGE.
49
49
-
// (avoids the initial sync problem with Automerge)
50
50
-
tracks: TRACKS_INITIAL_DOC,
51
51
-
52
52
-
hasSyncedTracks: false,
53
53
-
54
54
-
ready: false,
55
55
-
};
56
56
-
57
57
-
// Applet connections
58
58
-
const configurator = {
59
59
-
output: await applet("../../configurator/output"),
60
60
-
};
61
61
-
62
62
-
// Load tracks if needed
63
63
-
if (context.isMainInstance())
64
64
-
loadTracks().then((doc) => {
65
65
-
console.log("LOADED DOC", doc);
66
66
-
67
67
-
if (doc) {
68
68
-
const mergedDoc = Automerge.merge(context.data.tracks, doc);
69
69
-
console.log("MERGED DOC", doc);
70
70
-
update({ tracks: mergedDoc });
71
71
-
}
72
72
-
73
73
-
update({ hasSyncedTracks: true });
74
74
-
});
75
75
-
76
76
-
// State helpers
77
77
-
function update(partial: Partial<State>): void {
78
78
-
context.data = { ...context.data, ...partial };
79
79
-
}
80
80
-
81
81
-
function updateTracks(tracks: Track[]): Automerge.Doc<TracksDoc> {
82
82
-
console.log(context.data.tracks);
83
83
-
console.log(context.isMainInstance());
84
84
-
85
85
-
const doc = Automerge.change(context.data.tracks, (d) => {
86
86
-
d.collection = cleanUndefinedValuesForTracks(tracks);
87
87
-
});
88
88
-
89
89
-
update({ tracks: doc });
90
90
-
91
91
-
return doc;
92
92
-
}
93
93
-
94
94
-
////////////////////////////////////////////
95
95
-
// LOADERS
96
96
-
////////////////////////////////////////////
97
97
-
async function loadTracks() {
98
98
-
await waitUntilAppletIsReady(configurator.output);
99
99
-
100
100
-
const data = await configurator.output.sendAction(
101
101
-
"get",
102
102
-
{
103
103
-
name: "tracks.json",
104
104
-
},
105
105
-
{
106
106
-
timeoutDuration: 120000,
107
107
-
},
108
108
-
);
109
109
-
110
110
-
console.log("🔮 Loading tracks, got:", data);
111
111
-
112
112
-
if (!data) {
113
113
-
return undefined;
114
114
-
}
115
115
-
116
116
-
return Automerge.load<TracksDoc>(data as Uint8Array);
117
117
-
}
118
118
-
119
119
-
////////////////////////////////////////////
120
120
-
// ACTIONS
121
121
-
////////////////////////////////////////////
122
122
-
const tracksHandler = (tracks: Track[]) => {
123
123
-
const doc = updateTracks(tracks);
124
124
-
125
125
-
console.log("🔮 Tracks collection updated in memory");
126
126
-
127
127
-
// Save tracks to output, but only the ones that need to be saved.
128
128
-
// TODO: For each track.uri scheme ask the input configurator if it needs to be cached?
129
129
-
saveTracksToOutput(doc);
130
130
-
};
131
131
-
132
132
-
const saveTracksToOutput = debounce(5000, async function (doc: Automerge.Doc<TracksDoc>) {
133
133
-
const data = Automerge.save(doc);
134
134
-
135
135
-
console.log("🔮 Saving tracks");
136
136
-
137
137
-
await configurator.output.sendAction("put", {
138
138
-
name: "tracks.json",
139
139
-
data,
140
140
-
});
141
141
-
142
142
-
console.log("🔮 Tracks saved to output");
143
143
-
});
144
144
-
145
145
-
context.setActionHandler("tracks", tracksHandler);
146
146
-
147
147
-
////////////////////////////////////////////
148
148
-
// 🛠️
149
149
-
////////////////////////////////////////////
150
150
-
function cleanUndefinedValuesForTracks(tracks: Track[]): Track[] {
151
151
-
return tracks.map((track) => {
152
152
-
const t = { ...track };
153
153
-
154
154
-
if (t.tags) {
155
155
-
if ("album" in t.tags && t.tags.album === undefined) delete t.tags.album;
156
156
-
if ("artist" in t.tags && t.tags.artist === undefined) delete t.tags.artist;
157
157
-
if ("genre" in t.tags && t.tags.genre === undefined) delete t.tags.genre;
158
158
-
if ("year" in t.tags && t.tags.year === undefined) delete t.tags.year;
159
159
-
160
160
-
if ("of" in t.tags.disc && t.tags.disc.of === undefined) delete t.tags.disc.of;
161
161
-
if ("of" in t.tags.track && t.tags.track.of === undefined) delete t.tags.track.of;
162
162
-
}
163
163
-
164
164
-
return t;
165
165
-
});
166
166
-
}
167
167
-
168
168
-
////////////////////////////////////////////
169
169
-
// 🚦
170
170
-
////////////////////////////////////////////
171
171
-
update({ ready: true });
172
172
-
</script>
-18
src/pages/orchestrator/output-management/_manifest.json
Reviewed
···
1
1
-
{
2
2
-
"name": "diffuse/orchestrator/output-management",
3
3
-
"title": "Diffuse Orchestrator | Output management",
4
4
-
"entrypoint": "index.html",
5
5
-
"actions": {
6
6
-
"tracks": {
7
7
-
"title": "Tracks",
8
8
-
"description": "Manage tracks.",
9
9
-
"params_schema": {
10
10
-
"type": "array",
11
11
-
"description": "A list of tracks",
12
12
-
"items": {
13
13
-
"type": "object"
14
14
-
}
15
15
-
}
16
16
-
}
17
17
-
}
18
18
-
}
src/pages/orchestrator/output-management/index.astro
src/pages/output/storacha-automerge/index.astro
Reviewed
-10
src/pages/orchestrator/output-management/types.d.ts
Reviewed
···
1
1
-
import type { Doc } from "@automerge/automerge";
2
2
-
import type { Output } from "@applets/core/types";
3
3
-
4
4
-
export type State = {
5
5
-
tracks: Doc<{ collection: Output["tracks"] }>;
6
6
-
7
7
-
hasSyncedTracks: boolean;
8
8
-
9
9
-
ready: boolean;
10
10
-
};
+10
-14
src/pages/orchestrator/single-queue/_applet.astro
Reviewed
···
1
1
<script>
2
2
-
import type { ResolvedUri, Track } from "@applets/core/types.d.ts";
3
3
-
import { applet, comparable, reactive, register } from "@scripts/applets/common";
2
2
+
import type { ManagedOutput, ResolvedUri, Track } from "@applets/core/types.d.ts";
3
3
+
import { applet, reactive, register } from "@scripts/applets/common";
4
4
5
5
////////////////////////////////////////////
6
6
// SETUP
7
7
////////////////////////////////////////////
8
8
import type * as AudioEngine from "@applets/engine/audio/types.d.ts";
9
9
import type * as QueueEngine from "@applets/engine/queue/types.d.ts";
10
10
-
import type * as OutputOrchestrator from "@applets/orchestrator/output-management/types.d.ts";
11
10
12
11
// Register applet
13
12
const context = register<unknown>();
···
15
14
// Applet connections
16
15
const configurator = {
17
16
input: await applet("../../configurator/input"),
17
17
+
output: await applet<ManagedOutput>("../../configurator/output"),
18
18
};
19
19
20
20
const engine = {
21
21
audio: await applet<AudioEngine.State>("../../engine/audio"),
22
22
queue: await applet<QueueEngine.State>("../../engine/queue"),
23
23
-
};
24
24
-
25
25
-
const orchestrator = {
26
26
-
output: await applet<OutputOrchestrator.State>("../../orchestrator/output-management"),
27
23
};
28
24
29
25
////////////////////////////////////////////
···
106
102
},
107
103
});
108
104
109
109
-
fill(orchestrator.output.data.tracks.collection);
105
105
+
fill(configurator.output.data.tracks.collection);
110
106
},
111
107
);
112
108
113
109
////////////////////////////////////////////
114
114
-
// 🎻 [Connections → Orchestrators]
115
115
-
// 📦 STORAGE
110
110
+
// 🎻 [Connections → Configurators]
111
111
+
// 📦 OUTPUT
116
112
////////////////////////////////////////////
117
113
reactive(
118
118
-
orchestrator.output,
119
119
-
(data) => (data ? comparable(data.tracks.collection) : undefined),
120
120
-
(hash) => {
121
121
-
if (hash) fill(orchestrator.output.data.tracks.collection);
114
114
+
configurator.output,
115
115
+
(data) => data.tracks.cacheId,
116
116
+
() => {
117
117
+
fill(configurator.output.data.tracks.collection);
122
118
},
123
119
);
124
120
</script>
+47
-13
src/pages/output/indexed-db/_applet.astro
Reviewed
···
1
1
<script>
2
2
import * as IDB from "idb-keyval";
3
3
4
4
-
import type { OutputGetter, OutputSetter } from "@applets/core/types.d.ts";
5
5
-
import { register } from "@scripts/applets/common";
4
4
+
import type { ManagedOutput, Track } from "@applets/core/types.d.ts";
5
5
+
import { jsonDecode, jsonEncode, register } from "@scripts/applets/common";
6
6
+
import { INITIAL_MANAGED_OUTPUT } from "@scripts/output/common";
6
7
7
8
////////////////////////////////////////////
8
9
// SETUP
9
10
////////////////////////////////////////////
10
11
const IDB_PREFIX = "@applets/output/indexed-db";
11
11
-
const context = register();
12
12
+
13
13
+
const context = register<ManagedOutput>();
14
14
+
context.data = INITIAL_MANAGED_OUTPUT;
15
15
+
16
16
+
// Load initial data
17
17
+
if (context.isMainInstance())
18
18
+
tracks().then((collection) => {
19
19
+
context.data = {
20
20
+
...context.data,
21
21
+
tracks: {
22
22
+
...context.data.tracks,
23
23
+
cacheId: crypto.randomUUID(),
24
24
+
state: "loaded",
25
25
+
collection,
26
26
+
},
27
27
+
};
28
28
+
});
12
29
13
30
////////////////////////////////////////////
14
31
// ACTIONS
15
32
////////////////////////////////////////////
16
16
-
const get: OutputGetter = async ({ name }) => {
17
17
-
return await IDB.get(`${IDB_PREFIX}/${name}`);
18
18
-
};
33
33
+
async function tracks(): Promise<Track[]>;
34
34
+
async function tracks(tracks: Track[]): Promise<void>;
35
35
+
async function tracks(tracks?: Track[]): Promise<Track[] | void> {
36
36
+
if (tracks) {
37
37
+
const data = jsonEncode(tracks);
38
38
+
await put({ name: "tracks.json", data });
39
39
+
return;
40
40
+
} else {
41
41
+
const encoded = await get({ name: "tracks.json" });
42
42
+
if (!encoded) return [];
43
43
+
return jsonDecode<Track[]>(encoded);
44
44
+
}
45
45
+
}
19
46
20
20
-
const put: OutputSetter = async ({ data, name }) => {
21
21
-
return await IDB.set(`${IDB_PREFIX}/${name}`, data);
22
22
-
};
47
47
+
async function mount() {}
48
48
+
async function unmount() {}
23
49
24
24
-
const mount = async () => {};
25
25
-
const unmount = async () => {};
50
50
+
context.setActionHandler("tracks", tracks);
26
51
27
27
-
context.setActionHandler("get", get);
28
28
-
context.setActionHandler("put", put);
29
52
context.setActionHandler("mount", mount);
30
53
context.setActionHandler("unmount", unmount);
54
54
+
55
55
+
////////////////////////////////////////////
56
56
+
// 🛠️
57
57
+
////////////////////////////////////////////
58
58
+
async function get({ name }: { name: string }) {
59
59
+
return await IDB.get(`${IDB_PREFIX}/${name}`);
60
60
+
}
61
61
+
62
62
+
async function put({ data, name }: { data: Uint8Array; name: string }) {
63
63
+
return await IDB.set(`${IDB_PREFIX}/${name}`, data);
64
64
+
}
31
65
</script>
+8
-20
src/pages/output/indexed-db/_manifest.json
Reviewed
···
3
3
"title": "Diffuse Output | IndexedDB",
4
4
"entrypoint": "index.html",
5
5
"actions": {
6
6
-
"get": {
7
7
-
"title": "Get",
8
8
-
"description": "Retrieve data.",
9
9
-
"params_schema": {
10
10
-
"type": "object",
11
11
-
"properties": {
12
12
-
"name": { "type": "string" }
13
13
-
},
14
14
-
"required": ["name"]
15
15
-
}
16
16
-
},
17
17
-
"put": {
18
18
-
"title": "Put",
19
19
-
"description": "Store data.",
6
6
+
"tracks": {
7
7
+
"title": "Tracks",
8
8
+
"description": "Store or retrieve tracks. Passing in an array of tracks, stores them; passing no parameter, retrieves them.",
20
9
"params_schema": {
21
21
-
"type": "object",
22
22
-
"properties": {
23
23
-
"data": { "type": "object" },
24
24
-
"name": { "type": "string" }
25
25
-
},
26
26
-
"required": ["data", "name"]
10
10
+
"type": "array",
11
11
+
"description": "List of tracks",
12
12
+
"items": {
13
13
+
"type": "object"
14
14
+
}
27
15
}
28
16
},
29
17
"mount": {
+66
-28
src/pages/output/native-fs/_applet.astro
Reviewed
···
1
1
<script>
2
2
import * as IDB from "idb-keyval";
3
3
-
import { type FileSystemDirectoryHandle, showDirectoryPicker } from "native-file-system-adapter";
3
3
+
import "wicg-file-system-access";
4
4
5
5
-
import type { OutputGetter, OutputSetter } from "@applets/core/types.d.ts";
6
6
-
import { register } from "@scripts/applets/common";
5
5
+
import type { ManagedOutput, Track } from "@applets/core/types";
6
6
+
import { jsonDecode, jsonEncode, register } from "@scripts/applets/common";
7
7
+
import { INITIAL_MANAGED_OUTPUT } from "@scripts/output/common";
7
8
8
9
////////////////////////////////////////////
9
10
// SETUP
···
11
12
const IDB_PREFIX = "@applets/output/native-fs";
12
13
const IDB_DEVICE_KEY = `${IDB_PREFIX}/device`;
13
14
14
14
-
const context = register();
15
15
+
const context = register<ManagedOutput>();
16
16
+
context.data = INITIAL_MANAGED_OUTPUT;
17
17
+
18
18
+
// Load initial data
19
19
+
if (context.isMainInstance())
20
20
+
tracks().then((collection) => {
21
21
+
context.data = {
22
22
+
...context.data,
23
23
+
tracks: {
24
24
+
...context.data.tracks,
25
25
+
cacheId: crypto.randomUUID(),
26
26
+
state: "loaded",
27
27
+
collection,
28
28
+
},
29
29
+
};
30
30
+
});
15
31
16
32
////////////////////////////////////////////
17
33
// ACTIONS
18
34
////////////////////////////////////////////
19
19
-
const get: OutputGetter = async ({ name }) => {
35
35
+
async function tracks(): Promise<Track[]>;
36
36
+
async function tracks(tracks: Track[]): Promise<void>;
37
37
+
async function tracks(tracks?: Track[]): Promise<Track[] | void> {
38
38
+
if (tracks) {
39
39
+
const data = jsonEncode(tracks);
40
40
+
await put({ name: "tracks.json", data });
41
41
+
return;
42
42
+
} else {
43
43
+
const encoded = await get({ name: "tracks.json" });
44
44
+
if (!encoded) return [];
45
45
+
return jsonDecode<Track[]>(encoded);
46
46
+
}
47
47
+
}
48
48
+
49
49
+
async function mount() {
50
50
+
if (!("showDirectoryPicker" in self)) {
51
51
+
alert("File System Access API is not supported on this platform.");
52
52
+
return;
53
53
+
}
54
54
+
55
55
+
const existingHandle = await IDB.get(IDB_DEVICE_KEY);
56
56
+
if (!existingHandle) {
57
57
+
const directoryHandle = await self.showDirectoryPicker();
58
58
+
await IDB.set(IDB_DEVICE_KEY, directoryHandle);
59
59
+
await directoryHandle.requestPermission({ mode: "readwrite" });
60
60
+
}
61
61
+
}
62
62
+
63
63
+
async function unmount() {
64
64
+
try {
65
65
+
await IDB.del(IDB_DEVICE_KEY);
66
66
+
} catch (err) {}
67
67
+
}
68
68
+
69
69
+
context.setActionHandler("tracks", tracks);
70
70
+
71
71
+
context.setActionHandler("mount", mount);
72
72
+
context.setActionHandler("unmount", unmount);
73
73
+
74
74
+
////////////////////////////////////////////
75
75
+
// 🛠️
76
76
+
////////////////////////////////////////////
77
77
+
async function get({ name }: { name: string }) {
20
78
const handle: FileSystemDirectoryHandle | null = (await IDB.get(IDB_DEVICE_KEY)) ?? null;
21
79
if (!handle) throw new Error("Storage not configured properly, handle not found.");
22
80
···
28
86
} catch (err) {
29
87
return undefined;
30
88
}
31
31
-
};
89
89
+
}
32
90
33
33
-
const put: OutputSetter = async ({ data, name }) => {
91
91
+
async function put({ data, name }: { data: Uint8Array; name: string }) {
34
92
const handle: FileSystemDirectoryHandle | null = (await IDB.get(IDB_DEVICE_KEY)) ?? null;
35
93
if (!handle) throw new Error("Storage not configured properly, handle not found.");
36
94
const fileHandle = await handle.getFileHandle(name, { create: true });
37
95
const stream = await fileHandle.createWritable();
38
96
await stream.write(data);
39
97
await stream.close();
40
40
-
};
41
41
-
42
42
-
const mount = async () => {
43
43
-
const existingHandle = await IDB.get(IDB_DEVICE_KEY);
44
44
-
if (!existingHandle) {
45
45
-
const directoryHandle = await showDirectoryPicker();
46
46
-
await IDB.set(IDB_DEVICE_KEY, directoryHandle);
47
47
-
await directoryHandle.requestPermission({ mode: "readwrite" });
48
48
-
}
49
49
-
};
50
50
-
51
51
-
const unmount = async () => {
52
52
-
try {
53
53
-
await IDB.del(IDB_DEVICE_KEY);
54
54
-
} catch (err) {}
55
55
-
};
56
56
-
57
57
-
context.setActionHandler("get", get);
58
58
-
context.setActionHandler("put", put);
59
59
-
context.setActionHandler("mount", mount);
60
60
-
context.setActionHandler("unmount", unmount);
98
98
+
}
61
99
</script>
+8
-20
src/pages/output/native-fs/_manifest.json
Reviewed
···
3
3
"title": "Diffuse Output | Native File System",
4
4
"entrypoint": "index.html",
5
5
"actions": {
6
6
-
"get": {
7
7
-
"title": "Get",
8
8
-
"description": "Retrieve data.",
9
9
-
"params_schema": {
10
10
-
"type": "object",
11
11
-
"properties": {
12
12
-
"name": { "type": "string" }
13
13
-
},
14
14
-
"required": ["name"]
15
15
-
}
16
16
-
},
17
17
-
"put": {
18
18
-
"title": "Put",
19
19
-
"description": "Store data.",
6
6
+
"tracks": {
7
7
+
"title": "Tracks",
8
8
+
"description": "Store or retrieve tracks. Passing in an array of tracks, stores them; passing no parameter, retrieves them.",
20
9
"params_schema": {
21
21
-
"type": "object",
22
22
-
"properties": {
23
23
-
"data": { "type": "object" },
24
24
-
"name": { "type": "string" }
25
25
-
},
26
26
-
"required": ["data", "name"]
10
10
+
"type": "array",
11
11
+
"description": "List of tracks",
12
12
+
"items": {
13
13
+
"type": "object"
14
14
+
}
27
15
}
28
16
},
29
17
"mount": {
+123
src/pages/output/storacha-automerge/_applet.astro
Reviewed
···
1
1
+
<script>
2
2
+
import * as IDB from "idb-keyval";
3
3
+
import * as Automerge from "@automerge/automerge";
4
4
+
import * as Uint8 from "uint8arrays";
5
5
+
6
6
+
import type { ManagedOutput, Track } from "@applets/core/types.d.ts";
7
7
+
import { cleanUndefinedValuesForTracks, register } from "@scripts/applets/common";
8
8
+
import { INITIAL_MANAGED_OUTPUT } from "@scripts/output/common";
9
9
+
10
10
+
////////////////////////////////////////////
11
11
+
// 🏔️
12
12
+
////////////////////////////////////////////
13
13
+
type TracksDocument = { collection: Track[] };
14
14
+
15
15
+
const INITIAL_TRACKS_DOCUMENT = Automerge.load<TracksDocument>(
16
16
+
Uint8.fromString(
17
17
+
"hW9Kg5qsIsEAeAEQkb+c0IkXTSWyGqZ6jXtFxgETwM42fL3CMN78UZ4Qa3a9RfOrJu5qKzlM7IxwAUXelQYBAgMCEwIjBkACVgIHFQwhAiMCNAFCAlYCgAECfwB/AX8Bf+ub7MEGfwB/B38KY29sbGVjdGlvbn8AfwEBfwJ/AH8AAA",
18
18
+
"base64",
19
19
+
),
20
20
+
);
21
21
+
22
22
+
////////////////////////////////////////////
23
23
+
// SETUP
24
24
+
////////////////////////////////////////////
25
25
+
const IDB_PREFIX = "@applets/output/storacha-automerge";
26
26
+
27
27
+
const context = register<ManagedOutput>();
28
28
+
context.data = {
29
29
+
...INITIAL_MANAGED_OUTPUT,
30
30
+
tracks: {
31
31
+
...INITIAL_MANAGED_OUTPUT.tracks,
32
32
+
...INITIAL_TRACKS_DOCUMENT,
33
33
+
},
34
34
+
};
35
35
+
36
36
+
// Data codec
37
37
+
const codec = {
38
38
+
decode(data: any) {
39
39
+
return {
40
40
+
...data,
41
41
+
tracks: Automerge.load<TracksDocument>(data.tracks),
42
42
+
};
43
43
+
},
44
44
+
45
45
+
encode(data: ManagedOutput) {
46
46
+
return {
47
47
+
...data,
48
48
+
tracks: Automerge.save(data.tracks),
49
49
+
};
50
50
+
},
51
51
+
};
52
52
+
53
53
+
context.codec = codec;
54
54
+
55
55
+
// Load initial data
56
56
+
if (context.isMainInstance())
57
57
+
tracks().then((trackDocument) => {
58
58
+
context.data = {
59
59
+
...context.data,
60
60
+
tracks: {
61
61
+
...context.data.tracks,
62
62
+
...trackDocument,
63
63
+
cacheId: crypto.randomUUID(),
64
64
+
state: "loaded",
65
65
+
},
66
66
+
};
67
67
+
});
68
68
+
69
69
+
////////////////////////////////////////////
70
70
+
// ACTIONS
71
71
+
////////////////////////////////////////////
72
72
+
async function tracks(): Promise<Track[]>;
73
73
+
async function tracks(tracks: Track[]): Promise<void>;
74
74
+
async function tracks(tracks?: Track[]): Promise<Track[] | void> {
75
75
+
if (tracks) {
76
76
+
const doc = Automerge.change(context.data.tracks, (d) => {
77
77
+
d.collection = cleanUndefinedValuesForTracks(tracks);
78
78
+
});
79
79
+
80
80
+
context.data = {
81
81
+
...context.data,
82
82
+
tracks: {
83
83
+
...context.data.tracks,
84
84
+
...doc,
85
85
+
},
86
86
+
};
87
87
+
88
88
+
const data = Automerge.save(doc);
89
89
+
// TODO: Save to Storacha
90
90
+
91
91
+
return;
92
92
+
} else {
93
93
+
const doc = await tracksDocument();
94
94
+
return doc.collection;
95
95
+
}
96
96
+
}
97
97
+
98
98
+
async function tracksDocument(): Promise<TracksDocument> {
99
99
+
// TODO: Load from Storacha
100
100
+
// const data = new Uint8Array()
101
101
+
// const doc = Automerge.load<TracksDocument>(data as Uint8Array);
102
102
+
return INITIAL_TRACKS_DOCUMENT;
103
103
+
}
104
104
+
105
105
+
async function mount() {}
106
106
+
async function unmount() {}
107
107
+
108
108
+
context.setActionHandler("tracks", tracks);
109
109
+
110
110
+
context.setActionHandler("mount", mount);
111
111
+
context.setActionHandler("unmount", unmount);
112
112
+
113
113
+
////////////////////////////////////////////
114
114
+
// 🛠️
115
115
+
////////////////////////////////////////////
116
116
+
async function get({ name }: { name: string }) {
117
117
+
return await IDB.get(`${IDB_PREFIX}/${name}`);
118
118
+
}
119
119
+
120
120
+
async function put({ data, name }: { data: Uint8Array; name: string }) {
121
121
+
return await IDB.set(`${IDB_PREFIX}/${name}`, data);
122
122
+
}
123
123
+
</script>
+26
src/pages/output/storacha-automerge/_manifest.json
Reviewed
···
1
1
+
{
2
2
+
"name": "diffuse/output/storacha-automerge",
3
3
+
"title": "Diffuse Output | Storacha Storage + Automerge CRDT",
4
4
+
"entrypoint": "index.html",
5
5
+
"actions": {
6
6
+
"tracks": {
7
7
+
"title": "Tracks",
8
8
+
"description": "Store or retrieve tracks. Passing in an array of tracks, stores them; passing no parameter, retrieves them.",
9
9
+
"params_schema": {
10
10
+
"type": "array",
11
11
+
"description": "List of tracks",
12
12
+
"items": {
13
13
+
"type": "object"
14
14
+
}
15
15
+
}
16
16
+
},
17
17
+
"mount": {
18
18
+
"title": "Mount",
19
19
+
"description": "Prepare for usage."
20
20
+
},
21
21
+
"unmount": {
22
22
+
"title": "Unmount",
23
23
+
"description": "Callback after usage."
24
24
+
}
25
25
+
}
26
26
+
}
+3
-3
src/pages/processor/metadata-fetcher/_applet.astro
Reviewed
···
1
1
<script>
2
2
-
import { applets } from "@web-applets/sdk";
3
2
import { parseFromTokenizer, parseWebStream } from "music-metadata";
4
3
import { contentType } from "@std/media-types";
5
4
import * as URI from "uri-js";
6
5
import * as HTTP_TOKENIZER from "@tokenizer/http";
7
6
import * as RANGE_TOKENIZER from "@tokenizer/range";
8
7
9
9
-
import { TrackStats, TrackTags } from "@applets/core/types";
8
8
+
import type { TrackStats, TrackTags } from "@applets/core/types";
9
9
+
import { register } from "@scripts/applets/common";
10
10
11
11
////////////////////////////////////////////
12
12
// SETUP
13
13
////////////////////////////////////////////
14
14
-
const context = applets.register();
14
14
+
const context = register();
15
15
16
16
type Extraction = { stats: TrackStats; tags: TrackTags };
17
17
type Urls = { get: string; head: string };
+27
src/scripts/applets/common.ts
Reviewed
···
5
5
import { type ElementConfigurator, h } from "spellcaster/hyperscript.js";
6
6
import { effect, isSignal, Signal, signal } from "spellcaster/spellcaster.js";
7
7
import { xxh32 } from "xxh32";
8
8
+
import { Track } from "@applets/core/types";
8
9
9
10
////////////////////////////////////////////
10
11
// 🪟 Applet connector
···
231
232
return () => port;
232
233
}
233
234
235
235
+
export function cleanUndefinedValuesForTracks(tracks: Track[]): Track[] {
236
236
+
return tracks.map((track) => {
237
237
+
const t = { ...track };
238
238
+
239
239
+
if (t.tags) {
240
240
+
if ("album" in t.tags && t.tags.album === undefined) delete t.tags.album;
241
241
+
if ("artist" in t.tags && t.tags.artist === undefined) delete t.tags.artist;
242
242
+
if ("genre" in t.tags && t.tags.genre === undefined) delete t.tags.genre;
243
243
+
if ("year" in t.tags && t.tags.year === undefined) delete t.tags.year;
244
244
+
245
245
+
if ("of" in t.tags.disc && t.tags.disc.of === undefined) delete t.tags.disc.of;
246
246
+
if ("of" in t.tags.track && t.tags.track.of === undefined) delete t.tags.track.of;
247
247
+
}
248
248
+
249
249
+
return t;
250
250
+
});
251
251
+
}
252
252
+
234
253
export function comparable(value: unknown) {
235
254
return xxh32(JSON.stringify(value));
236
255
}
···
251
270
252
271
export function isPrimitive(test: unknown) {
253
272
return test !== Object(test);
273
273
+
}
274
274
+
275
275
+
export function jsonDecode<T>(a: any): T {
276
276
+
return JSON.parse(new TextDecoder().decode(a));
277
277
+
}
278
278
+
279
279
+
export function jsonEncode<T>(a: T): Uint8Array {
280
280
+
return new TextEncoder().encode(JSON.stringify(a));
254
281
}
255
282
256
283
export function waitUntilAppletData<A>(
src/scripts/inputs/common.ts
src/scripts/input/common.ts
Reviewed
+9
src/scripts/output/common.ts
Reviewed
···
1
1
+
import type { ManagedOutput } from "@applets/core/types";
2
2
+
3
3
+
export const INITIAL_MANAGED_OUTPUT: ManagedOutput = {
4
4
+
tracks: {
5
5
+
cacheId: crypto.randomUUID(),
6
6
+
state: "loading",
7
7
+
collection: [],
8
8
+
},
9
9
+
};
-1
src/scripts/themes/pilot/index.ts
Reviewed
···
24
24
input: await applet("../../orchestrator/input-cache", {
25
25
applets: { input: "todo" },
26
26
}),
27
27
-
output: await applet("../../orchestrator/output-management"),
28
27
queue: await applet("../../orchestrator/single-queue"),
29
28
};
30
29
+15
-10
src/scripts/themes/webamp/index.ts
Reviewed
···
1
1
import Webamp from "webamp";
2
2
import { URLTrack } from "webamp";
3
3
4
4
-
import type { ResolvedUri, Track } from "@applets/core/types.d.ts";
5
5
-
import { applet } from "@scripts/applets/common";
4
4
+
import type { ManagedOutput, ResolvedUri, Track } from "@applets/core/types.d.ts";
5
5
+
import { applet, waitUntilAppletData } from "@scripts/applets/common";
6
6
7
7
////////////////////////////////////////////
8
8
// 🎨 Styles
···
12
12
////////////////////////////////////////////
13
13
// 🗂️ Applets
14
14
////////////////////////////////////////////
15
15
-
import type * as OutputOrchestrator from "@applets/orchestrator/output-management/types.d.ts";
16
16
-
17
15
const configurator = {
18
16
input: await applet("../../configurator/input"),
17
17
+
output: await applet<ManagedOutput>("../../configurator/output"),
19
18
};
20
19
21
20
const orchestrator = {
22
22
-
output: await applet<OutputOrchestrator.State>("../../orchestrator/output-management"),
23
23
-
24
24
-
// TODO: Should this be explicitely be ran after the output orchestrator is loaded?
25
21
input: await applet("../../orchestrator/input-cache"),
26
22
};
27
23
···
37
33
document.body.appendChild(ampNode);
38
34
amp.renderWhenReady(ampNode);
39
35
40
40
-
orchestrator.output.ondata = async () => {
36
36
+
waitUntilAppletData(configurator.output, (d) => d?.tracks.state === "loaded").then(loadAndInsert);
37
37
+
configurator.output.ondata = loadAndInsert;
38
38
+
39
39
+
let inserting = false;
40
40
+
41
41
+
async function loadAndInsert() {
42
42
+
if (configurator.output.data.tracks.state !== "loaded") return;
43
43
+
if (inserting) return;
44
44
+
inserting = true;
41
45
const tracks = await loadTracks();
42
46
amp.setTracksToPlay([]);
43
47
amp.appendTracks(tracks);
44
48
amp.nextTrack();
45
45
-
};
49
49
+
inserting = false;
50
50
+
}
46
51
47
52
////////////////////////////////////////////
48
53
// 🛠️
49
54
////////////////////////////////////////////
50
55
async function loadTracks(): Promise<URLTrack[]> {
51
51
-
return await orchestrator.output.data.tracks.collection.reduce(
56
56
+
return await configurator.output.data.tracks.collection.reduce(
52
57
async (promise: Promise<URLTrack[]>, track: Track) => {
53
58
const acc = await promise;
54
59