Experiment to rebuild Diffuse using web applets.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: primary orchestrator

+398 -398
+5 -5
src/pages/configurator/input/_applet.astro
··· 42 42 // SETUP 43 43 //////////////////////////////////////////// 44 44 const worker = endpoint<Tasks>( 45 - new SharedWorker(new URL("../../../scripts/configurator/input/worker", import.meta.url), { 45 + new Worker(new URL("../../../scripts/configurator/input/worker", import.meta.url), { 46 46 type: "module", 47 47 name: manifest.name, 48 - }).port, 48 + }), 49 49 ); 50 50 51 51 // Register applet + worker ··· 53 53 54 54 // Applet connections 55 55 const input = { 56 - "file+local": applet("/input/native-fs"), 57 - opensubsonic: applet("/input/opensubsonic"), 58 - s3: applet("/input/s3"), 56 + "file+local": applet("/input/native-fs", { context: self }), 57 + opensubsonic: applet("/input/opensubsonic", { context: self }), 58 + s3: applet("/input/s3", { context: self }), 59 59 }; 60 60 61 61 // Provide tunnel to worker
+41 -15
src/pages/constituent/blur/artwork-controller/_applet.astro
··· 87 87 transition: 88 88 background-color var(--transition-durition), 89 89 color var(--transition-durition); 90 - z-index: 1; 90 + z-index: 10; 91 91 } 92 92 93 93 /* Progress bars */ ··· 339 339 import { computed, effect, type Signal, signal } from "spellcaster"; 340 340 import { tags, text, type ElementConfigurator } from "spellcaster/hyperscript.js"; 341 341 342 - import type { ManagedOutput, Track } from "@applets/core/types"; 342 + import type { Track } from "@applets/core/types"; 343 343 import { applet, hs, inputUrl, reactive, register } from "@scripts/applet/common"; 344 - import { comparable, trackArtworkCacheId } from "@scripts/common"; 344 + import { trackArtworkCacheId } from "@scripts/common"; 345 345 346 346 //////////////////////////////////////////// 347 347 // SETUP ··· 382 382 queue: await applet<QueueEngine.State>("/engine/queue", { groupId: context.groupId }), 383 383 }; 384 384 385 - const _orchestrator = { 386 - queueAudio: await applet("/orchestrator/queue-audio", { 385 + const orchestrator = { 386 + primary: await applet("/orchestrator/primary", { 387 387 groupId: context.groupId, 388 388 }), 389 - 390 - // When using the `main` group, load additional orchestrators: 391 - inputCache: 392 - context.groupId === undefined || context.groupId === "main" 393 - ? await applet("/orchestrator/input-cache") 394 - : undefined, 395 - queueTracks: 396 - context.groupId === undefined || context.groupId === "main" 397 - ? applet("/orchestrator/queue-tracks") 398 - : undefined, 399 389 }; 400 390 401 391 const processor = { ··· 472 462 (volume) => setVolume(volume), 473 463 ); 474 464 465 + // ORCHESTRATED 466 + 467 + context.settled().then(() => { 468 + if (context.isMainInstance()) 469 + orchestrator.primary.sendAction("monitor_audio_end", undefined, { timeoutDuration: 60000 }); 470 + }); 471 + 475 472 //////////////////////////////////////////// 476 473 // 🎢 QUEUE 477 474 //////////////////////////////////////////// ··· 518 515 const currCacheId = currTrack ? await trackArtworkCacheId(currTrack) : undefined; 519 516 if (cacheId === currCacheId) setArtwork(art); 520 517 } 518 + 519 + // ORCHESTRATED 520 + 521 + context.settled().then(() => { 522 + if (context.isMainInstance()) 523 + orchestrator.primary.sendAction("monitor_active_queue_item", undefined, { 524 + timeoutDuration: 60000, 525 + }); 526 + }); 527 + 528 + //////////////////////////////////////////// 529 + // TRACKS 530 + //////////////////////////////////////////// 531 + 532 + // ORCHESTRATED 533 + 534 + context.settled().then(() => { 535 + if (isMainGroup() && context.isMainInstance()) { 536 + orchestrator.primary 537 + .sendAction("insert_tracks_into_queue", undefined, { 538 + timeoutDuration: 60000 * 5, 539 + }) 540 + .then(() => { 541 + orchestrator.primary.sendAction("process_inputs", undefined, { 542 + timeoutDuration: 60000 * 60, 543 + }); 544 + }); 545 + } 546 + }); 521 547 522 548 //////////////////////////////////////////// 523 549 // UI
+8 -7
src/pages/engine/audio/_applet.astro
··· 25 25 const vol = localStorage.getItem(VOLUME_KEY); 26 26 27 27 // Initial state 28 - context.data = { 29 - isPlaying: false, 30 - items: {}, 31 - volume: { 32 - default: vol ? parseFloat(vol) : 0.5, 33 - }, 34 - }; 28 + if (context.isMainInstance()) 29 + context.data = { 30 + isPlaying: false, 31 + items: {}, 32 + volume: { 33 + default: vol ? parseFloat(vol) : 0.5, 34 + }, 35 + }; 35 36 36 37 // State helpers 37 38 function update(partial: Partial<State>): void {
+3 -7
src/pages/engine/queue/_applet.astro
··· 20 20 // Register applet 21 21 const context = register<State>({ mode: "shared-worker", worker }); 22 22 23 + // Initial state 24 + context.data = await worker.data(); 25 + 23 26 // Keep applet data with worker data in sync 24 27 sync(context, port); 25 - 26 - // Initial state 27 - context.data = { 28 - past: [], 29 - now: null, 30 - future: [], 31 - }; 32 28 33 29 //////////////////////////////////////////// 34 30 // ACTIONS
+1 -5
src/pages/index.astro
··· 46 46 { url: "input/s3/", title: "S3-Compatible API" }, 47 47 ]; 48 48 49 - const orchestrators = [ 50 - { url: "orchestrator/input-cache/", title: "Input caching" }, 51 - { url: "orchestrator/queue-audio/", title: "Queue ⭤ Audio" }, 52 - { url: "orchestrator/queue-tracks/", title: "Queue ⭤ Tracks" }, 53 - ]; 49 + const orchestrators = [{ url: "orchestrator/primary/", title: "Primary (Queue, audio, tracks)" }]; 54 50 55 51 const output = [ 56 52 { url: "output/indexed-db/", title: "IndexedDB" },
+2 -2
src/pages/input/native-fs/_applet.astro
··· 27 27 // SETUP 28 28 //////////////////////////////////////////// 29 29 const worker = endpoint<Tasks>( 30 - new SharedWorker(new URL("../../../scripts/input/native-fs/worker", import.meta.url), { 30 + new Worker(new URL("../../../scripts/input/native-fs/worker", import.meta.url), { 31 31 type: "module", 32 32 name: manifest.name, 33 - }).port, 33 + }), 34 34 ); 35 35 36 36 // Register applet
+2 -2
src/pages/input/opensubsonic/_applet.astro
··· 28 28 // SETUP 29 29 //////////////////////////////////////////// 30 30 const worker = endpoint<Tasks>( 31 - new SharedWorker(new URL("../../../scripts/input/opensubsonic/worker", import.meta.url), { 31 + new Worker(new URL("../../../scripts/input/opensubsonic/worker", import.meta.url), { 32 32 type: "module", 33 33 name: manifest.name, 34 - }).port, 34 + }), 35 35 ); 36 36 37 37 // Register applet
+2 -2
src/pages/input/s3/_applet.astro
··· 47 47 // SETUP 48 48 //////////////////////////////////////////// 49 49 const worker = endpoint<Tasks>( 50 - new SharedWorker(new URL("../../../scripts/input/s3/worker", import.meta.url), { 50 + new Worker(new URL("../../../scripts/input/s3/worker", import.meta.url), { 51 51 type: "module", 52 52 name: manifest.name, 53 - }).port, 53 + }), 54 54 ); 55 55 56 56 // Register applet
-112
src/pages/orchestrator/input-cache/_applet.astro
··· 1 - <script> 2 - import type { ManagedOutput, ResolvedUri, Track } from "@applets/core/types.d.ts"; 3 - import { applet, register, wait } from "@scripts/applet/common"; 4 - import { tracksCacheId } from "@scripts/output/common"; 5 - 6 - //////////////////////////////////////////// 7 - // SETUP 8 - //////////////////////////////////////////// 9 - const context = register<{ isProcessing: boolean }>(); 10 - 11 - // Initial data 12 - context.data = { 13 - isProcessing: false, 14 - }; 15 - 16 - // Applet connections 17 - const configurator = { 18 - input: applet("/configurator/input"), 19 - output: applet<ManagedOutput>("/configurator/output"), 20 - }; 21 - 22 - const processor = { 23 - metadata: applet("/processor/metadata"), 24 - }; 25 - 26 - // Start processing once settled and tracks are loaded 27 - context 28 - .settled() 29 - .then(() => configurator.output) 30 - .then((output) => wait(output, (d) => d?.tracks.state === "loaded")) 31 - .then(() => (context.isMainInstance() ? process() : undefined)); 32 - 33 - //////////////////////////////////////////// 34 - // ACTIONS 35 - //////////////////////////////////////////// 36 - context.setActionHandler("process", process); 37 - 38 - async function process() { 39 - if (context.data.isProcessing) return; 40 - context.data = { ...context.data, isProcessing: true }; 41 - console.log("🪵 Processing initiated"); 42 - 43 - const input = await configurator.input; 44 - const output = await configurator.output; 45 - 46 - const cachedTracks = output.data.tracks.collection; 47 - 48 - await input.sendAction("contextualize", cachedTracks, { 49 - timeoutDuration: 60000 * 5, 50 - worker: true, 51 - }); 52 - 53 - const tracks = await input.sendAction<Track[]>("list", cachedTracks, { 54 - timeoutDuration: 60000 * 60 * 24, 55 - worker: true, 56 - }); 57 - 58 - // Process 59 - const tracksWithMetadata = await tracks.reduce( 60 - async (promise: Promise<Track[]>, track: Track) => { 61 - const acc = await promise; 62 - 63 - if (track.tags && track.stats) return [...acc, track]; 64 - 65 - const resGet = await input.sendAction<ResolvedUri>( 66 - "resolve", 67 - { method: "GET", uri: track.uri }, 68 - { 69 - timeoutDuration: 60000 * 5, 70 - worker: true, 71 - }, 72 - ); 73 - 74 - const resHead = await input.sendAction<ResolvedUri>( 75 - "resolve", 76 - { method: "HEAD", uri: track.uri }, 77 - { 78 - timeoutDuration: 60000 * 5, 79 - worker: true, 80 - }, 81 - ); 82 - 83 - if (!resGet) return [...acc, track]; 84 - 85 - const metadataProcessor = await processor.metadata; 86 - const { stats, tags } = await metadataProcessor.sendAction( 87 - "supply", 88 - { urls: { get: resGet.url, head: resHead?.url || resGet.url } }, 89 - { 90 - timeoutDuration: 60000 * 15, 91 - worker: true, 92 - }, 93 - ); 94 - 95 - return [...acc, { ...track, stats, tags }]; 96 - }, 97 - Promise.resolve([]), 98 - ); 99 - 100 - // Save 101 - const changed = tracksCacheId(tracksWithMetadata) !== output.data.tracks.cacheId; 102 - 103 - if (changed) 104 - await output.sendAction("tracks", tracksWithMetadata, { 105 - timeoutDuration: 60000 * 5, 106 - }); 107 - 108 - // Fin 109 - console.log("🪵 Processing completed"); 110 - context.data = { ...context.data, isProcessing: false }; 111 - } 112 - </script>
-11
src/pages/orchestrator/input-cache/_manifest.json
··· 1 - { 2 - "name": "diffuse/orchestrator/input-cache", 3 - "title": "Diffuse Orchestrator | Input cache", 4 - "entrypoint": "index.html", 5 - "actions": { 6 - "process": { 7 - "title": "Process", 8 - "description": "Process inputs; listing all tracks, fetching metadata where needed and passing the result to the output manager." 9 - } 10 - } 11 - }
src/pages/orchestrator/input-cache/index.astro src/pages/orchestrator/primary/index.astro
+231
src/pages/orchestrator/primary/_applet.astro
··· 1 + <script> 2 + import type { Applet } from "@web-applets/sdk"; 3 + import type { GroupConsult, ManagedOutput, ResolvedUri, Track } from "@applets/core/types.d.ts"; 4 + import { applet, inputUrl, reactive, register, wait } from "@scripts/applet/common"; 5 + import { tracksCacheId } from "@scripts/output/common"; 6 + 7 + //////////////////////////////////////////// 8 + // SETUP 9 + //////////////////////////////////////////// 10 + import type * as AudioEngine from "@applets/engine/audio/types.d.ts"; 11 + import type * as QueueEngine from "@applets/engine/queue/types.d.ts"; 12 + 13 + const context = register<{ isProcessing: boolean }>(); 14 + 15 + // Initial data 16 + context.data = { 17 + isProcessing: false, 18 + }; 19 + 20 + // Applet connections 21 + const configurator = { 22 + input: applet("/configurator/input", { context: self }), 23 + input_2: undefined as Applet | undefined, 24 + output: applet<ManagedOutput>("/configurator/output"), 25 + }; 26 + 27 + const engine = { 28 + audio: applet<AudioEngine.State>("/engine/audio", { groupId: context.groupId }), 29 + queue: applet<QueueEngine.State>("/engine/queue", { groupId: context.groupId }), 30 + }; 31 + 32 + const processor = { 33 + metadata: applet("/processor/metadata"), 34 + }; 35 + 36 + //////////////////////////////////////////// 37 + // [ACTIONS] AUDIO ⭤ QUEUE 38 + //////////////////////////////////////////// 39 + context.setActionHandler("monitor_active_queue_item", monitorActiveQueueItem); 40 + context.setActionHandler("monitor_audio_end", monitorAudioEnd); 41 + 42 + async function monitorActiveQueueItem() { 43 + await context.settled(); 44 + 45 + const audio = await engine.audio; 46 + const queue = await engine.queue; 47 + 48 + // When the active queue item has changed, 49 + // coordinate the audio engine accordingly. 50 + reactive( 51 + queue, 52 + (data) => data.now?.id, 53 + async () => { 54 + const activeTrack = queue.data.now; 55 + const isPlaying = audio.data.isPlaying; 56 + 57 + // Resolve URIs 58 + const url = activeTrack 59 + ? await inputUrl(await configurator.input, activeTrack.uri).then((a) => a?.url) 60 + : undefined; 61 + 62 + // Check if we still need to render 63 + if (queue.data.now?.id !== activeTrack?.id) return; 64 + 65 + // Play new active queue item 66 + // TODO: Take URL expiration timestamp into account 67 + // TODO: Preload next queue item 68 + audio.sendAction( 69 + "render", 70 + { 71 + audio: activeTrack 72 + ? [ 73 + { 74 + id: activeTrack.id, 75 + isPreload: false, 76 + url, 77 + }, 78 + ] 79 + : // TODO: Keep preloads 80 + [], 81 + play: activeTrack && isPlaying ? { audioId: activeTrack.id } : undefined, 82 + }, 83 + { 84 + timeoutDuration: 60000, 85 + }, 86 + ); 87 + }, 88 + ); 89 + } 90 + 91 + async function monitorAudioEnd() { 92 + const audio = await engine.audio; 93 + const queue = await engine.queue; 94 + 95 + // When the active audio has ended, 96 + // shift the queue. 97 + reactive( 98 + audio, 99 + (data) => data.items[queue.data.now?.id ?? Infinity]?.hasEnded ?? false, 100 + (hasEnded) => { 101 + if (hasEnded) queue.sendAction("shift", undefined, { worker: true }); 102 + }, 103 + ); 104 + } 105 + 106 + //////////////////////////////////////////// 107 + // [ACTIONS] PROCESS 108 + //////////////////////////////////////////// 109 + context.setActionHandler("process_inputs", processInputs); 110 + 111 + async function processInputs() { 112 + if (context.data.isProcessing) return; 113 + context.data = { ...context.data, isProcessing: true }; 114 + console.log("🪵 Processing initiated"); 115 + 116 + const input = configurator.input_2 117 + ? configurator.input_2 118 + : await applet("/configurator/input", { context: self, newInstance: true }); 119 + 120 + if (!configurator.input_2) configurator.input_2 = input; 121 + 122 + const output = await configurator.output; 123 + const cachedTracks = output.data.tracks.collection; 124 + 125 + await input.sendAction("contextualize", cachedTracks, { 126 + timeoutDuration: 60000 * 5, 127 + worker: true, 128 + }); 129 + 130 + const tracks = await input.sendAction<Track[]>("list", cachedTracks, { 131 + timeoutDuration: 60000 * 60 * 24, 132 + worker: true, 133 + }); 134 + 135 + // Process 136 + const tracksWithMetadata = await tracks.reduce( 137 + async (promise: Promise<Track[]>, track: Track) => { 138 + const acc = await promise; 139 + 140 + if (track.tags && track.stats) return [...acc, track]; 141 + 142 + const resGet = await input.sendAction<ResolvedUri>( 143 + "resolve", 144 + { method: "GET", uri: track.uri }, 145 + { 146 + timeoutDuration: 60000 * 5, 147 + worker: true, 148 + }, 149 + ); 150 + 151 + const resHead = await input.sendAction<ResolvedUri>( 152 + "resolve", 153 + { method: "HEAD", uri: track.uri }, 154 + { 155 + timeoutDuration: 60000 * 5, 156 + worker: true, 157 + }, 158 + ); 159 + 160 + if (!resGet) return [...acc, track]; 161 + 162 + const metadataProcessor = await processor.metadata; 163 + const { stats, tags } = await metadataProcessor.sendAction( 164 + "supply", 165 + { urls: { get: resGet.url, head: resHead?.url || resGet.url } }, 166 + { 167 + timeoutDuration: 60000 * 15, 168 + worker: true, 169 + }, 170 + ); 171 + 172 + return [...acc, { ...track, stats, tags }]; 173 + }, 174 + Promise.resolve([]), 175 + ); 176 + 177 + // Save 178 + const changed = tracksCacheId(tracksWithMetadata) !== output.data.tracks.cacheId; 179 + 180 + if (changed) 181 + await output.sendAction("tracks", tracksWithMetadata, { 182 + timeoutDuration: 60000 * 5, 183 + }); 184 + 185 + // Fin 186 + console.log("🪵 Processing completed"); 187 + context.data = { ...context.data, isProcessing: false }; 188 + } 189 + 190 + //////////////////////////////////////////// 191 + // [ACTIONS] QUEUE ⭤ TRACKS 192 + //////////////////////////////////////////// 193 + context.setActionHandler("insert_tracks_into_queue", insertTracksIntoQueue); 194 + 195 + async function insertTracksIntoQueue() { 196 + await context.settled(); 197 + 198 + // Add tracks to the queue once the tracks have been loaded; 199 + // and every time the collection changes. 200 + 201 + const input = await configurator.input; 202 + const output = await configurator.output; 203 + const queue = await engine.queue; 204 + 205 + await wait(output, (d) => d?.tracks.state === "loaded"); 206 + 207 + reactive( 208 + output, 209 + (data) => data.tracks.cacheId, 210 + async () => { 211 + const groups = await input.sendAction<GroupConsult>( 212 + "groupConsult", 213 + output.data.tracks.collection, 214 + { timeoutDuration: 60000 * 5, worker: true }, 215 + ); 216 + 217 + // Available tracks 218 + const tracks = Object.values(groups).reduce((acc: Track[], value) => { 219 + if (value.available === false) return acc; 220 + return [...acc, ...value.tracks]; 221 + }, []); 222 + 223 + // Set pool 224 + await queue.sendAction("pool", tracks, { 225 + timeoutDuration: 60000, 226 + worker: true, 227 + }); 228 + }, 229 + ); 230 + } 231 + </script>
+6
src/pages/orchestrator/primary/_manifest.json
··· 1 + { 2 + "name": "diffuse/orchestrator/primary", 3 + "title": "Diffuse Orchestrator | Primary", 4 + "entrypoint": "index.html", 5 + "actions": {} 6 + }
-95
src/pages/orchestrator/queue-audio/_applet.astro
··· 1 - <script> 2 - import { applet, inputUrl, makeConnect, register } from "@scripts/applet/common"; 3 - 4 - //////////////////////////////////////////// 5 - // SETUP 6 - //////////////////////////////////////////// 7 - import type * as AudioEngine from "@applets/engine/audio/types.d.ts"; 8 - import type * as QueueEngine from "@applets/engine/queue/types.d.ts"; 9 - 10 - // Register applet 11 - const context = register(); 12 - const connect = makeConnect(context); 13 - 14 - // Applet connections 15 - const configurator = { 16 - input: await applet("/configurator/input"), 17 - }; 18 - 19 - const engine = { 20 - audio: await applet<AudioEngine.State>("/engine/audio", { groupId: context.groupId }), 21 - queue: await applet<QueueEngine.State>("/engine/queue", { groupId: context.groupId }), 22 - }; 23 - 24 - //////////////////////////////////////////// 25 - // Connections 26 - //////////////////////////////////////////// 27 - await context.settled(); 28 - 29 - //////////////////////////////////////////// 30 - // ⚙️ [Connections → Engines] 31 - // 🔉 AUDIO 32 - //////////////////////////////////////////// 33 - 34 - // When the active audio has ended, 35 - // shift the queue. 36 - 37 - // NOTE: 38 - // This could probably be optimised, but it works. 39 - 40 - connect( 41 - engine.audio, 42 - (data) => data.items[engine.queue.data.now?.id ?? Infinity]?.hasEnded ?? false, 43 - (hasEnded) => { 44 - if (hasEnded) engine.queue.sendAction("shift", undefined, { worker: true }); 45 - }, 46 - ); 47 - 48 - //////////////////////////////////////////// 49 - // ⚙️ [Connections → Engines] 50 - // 🚏 QUEUE 51 - //////////////////////////////////////////// 52 - 53 - // When the active queue item has changed, 54 - // coordinate the audio engine accordingly. 55 - 56 - connect( 57 - engine.queue, 58 - (data) => data.now?.id, 59 - async () => { 60 - const activeTrack = engine.queue.data.now; 61 - const isPlaying = engine.audio.data.isPlaying; 62 - 63 - // Resolve URIs 64 - const url = activeTrack 65 - ? await inputUrl(configurator.input, activeTrack.uri).then((a) => a?.url) 66 - : undefined; 67 - 68 - // Check if we still need to render 69 - if (engine.queue.data.now?.id !== activeTrack?.id) return; 70 - 71 - // Play new active queue item 72 - // TODO: Take URL expiration timestamp into account 73 - // TODO: Preload next queue item 74 - engine.audio.sendAction( 75 - "render", 76 - { 77 - audio: activeTrack 78 - ? [ 79 - { 80 - id: activeTrack.id, 81 - isPreload: false, 82 - url, 83 - }, 84 - ] 85 - : // TODO: Keep preloads 86 - [], 87 - play: activeTrack && isPlaying ? { audioId: activeTrack.id } : undefined, 88 - }, 89 - { 90 - timeoutDuration: 60000, 91 - }, 92 - ); 93 - }, 94 - ); 95 - </script>
-6
src/pages/orchestrator/queue-audio/_manifest.json
··· 1 - { 2 - "name": "diffuse/orchestrator/queue-audio", 3 - "title": "Diffuse Orchestrator | Queue Audio", 4 - "entrypoint": "index.html", 5 - "actions": {} 6 - }
-9
src/pages/orchestrator/queue-audio/index.astro
··· 1 - --- 2 - import Layout from "@layouts/applet.astro"; 3 - import Applet from "./_applet.astro"; 4 - import { title } from "./_manifest.json"; 5 - --- 6 - 7 - <Layout title={title}> 8 - <Applet /> 9 - </Layout>
-57
src/pages/orchestrator/queue-tracks/_applet.astro
··· 1 - <script> 2 - import type { GroupConsult, ManagedOutput, Track } from "@applets/core/types.d.ts"; 3 - import { applet, makeConnect, register, wait } from "@scripts/applet/common"; 4 - 5 - //////////////////////////////////////////// 6 - // SETUP 7 - //////////////////////////////////////////// 8 - import type * as QueueEngine from "@applets/engine/queue/types.d.ts"; 9 - 10 - // Register applet 11 - const context = register(); 12 - const connect = makeConnect(context); 13 - 14 - // Applet connections 15 - const configurator = { 16 - input: await applet("/configurator/input"), 17 - output: await applet<ManagedOutput>("/configurator/output"), 18 - }; 19 - 20 - const engine = { 21 - queue: await applet<QueueEngine.State>("/engine/queue", { groupId: context.groupId }), 22 - }; 23 - 24 - //////////////////////////////////////////// 25 - // Connections 26 - //////////////////////////////////////////// 27 - await context.settled(); 28 - 29 - // Add tracks to the queue once the tracks have been loaded; 30 - // and every time the collection changes. 31 - 32 - wait(configurator.output, (d) => d?.tracks.state === "loaded").then(async () => { 33 - connect( 34 - configurator.output, 35 - (data) => data.tracks.cacheId, 36 - async () => { 37 - const groups = await configurator.input.sendAction<GroupConsult>( 38 - "groupConsult", 39 - configurator.output.data.tracks.collection, 40 - { timeoutDuration: 60000 * 5, worker: true }, 41 - ); 42 - 43 - // Available tracks 44 - const tracks = Object.values(groups).reduce((acc: Track[], value) => { 45 - if (value.available === false) return acc; 46 - return [...acc, ...value.tracks]; 47 - }, []); 48 - 49 - // Clear 50 - engine.queue.sendAction("pool", tracks, { 51 - timeoutDuration: 60000, 52 - worker: true, 53 - }); 54 - }, 55 - ); 56 - }); 57 - </script>
-6
src/pages/orchestrator/queue-tracks/_manifest.json
··· 1 - { 2 - "name": "diffuse/orchestrator/queue-tracks", 3 - "title": "Diffuse Orchestrator | Queue Tracks", 4 - "entrypoint": "index.html", 5 - "actions": {} 6 - }
-9
src/pages/orchestrator/queue-tracks/index.astro
··· 1 - --- 2 - import Layout from "@layouts/applet.astro"; 3 - import Applet from "./_applet.astro"; 4 - import { title } from "./_manifest.json"; 5 - --- 6 - 7 - <Layout title={title}> 8 - <Applet /> 9 - </Layout>
+17 -20
src/scripts/applet/common.ts
··· 20 20 context?: Window; 21 21 frameId?: string; 22 22 groupId?: string; 23 + newInstance?: boolean; 23 24 setHeight?: boolean; 24 25 } = {}, 25 26 ): Promise<Applet<D>> { ··· 41 42 src = QS.stringifyUrl({ url: src, query }); 42 43 } 43 44 44 - const context = opts.context || self.top || self.parent; 45 - const existingFrame: HTMLIFrameElement | null = context.document.querySelector(`[src="${src}"]`); 45 + const context = opts.newInstance ? self : opts.context || self.top || self.parent; 46 + const existingFrame: HTMLIFrameElement | null = opts.newInstance 47 + ? null 48 + : context.document.querySelector(`[src="${src}"]`); 46 49 47 50 let frame; 48 51 ··· 122 125 encode(data: T): any; 123 126 }; 124 127 128 + export function lookupGroupId() { 129 + const url = new URL(location.href); 130 + return url.searchParams.get("groupId") || "main"; 131 + } 132 + 125 133 export function register<DataType = any>( 126 134 options: { mode?: "broadcast" | "shared-worker"; worker?: Comlink.Remote<WorkerTasks> } = {}, 127 135 ): DiffuseApplet<DataType> { 128 136 const mode = options.mode ?? "broadcast"; 129 - const url = new URL(location.href); 130 137 const scope = applets.register<DataType>(); 131 138 132 - const groupId = url.searchParams.get("groupId") || "main"; 139 + const groupId = lookupGroupId(); 133 140 const channelId = `${location.host}${location.pathname}/${groupId}`; 134 141 const instanceId = crypto.randomUUID(); 135 142 ··· 270 277 const timeoutId = setTimeout(() => { 271 278 channel.removeEventListener("message", handler); 272 279 resolve({ isMain: true }); 273 - }, 1000); 280 + }, 5000); 274 281 275 282 const handler = (event: MessageEvent) => { 276 - if (event.data === "pong" || event.data === "ping") { 283 + if (event.data?.type === "PONG" || event.data?.type === "PING") { 277 284 clearTimeout(timeoutId); 278 285 channel.removeEventListener("message", handler); 279 286 resolve({ isMain: false }); ··· 281 288 }; 282 289 283 290 channel.addEventListener("message", handler); 291 + channel.postMessage({ 292 + type: "PING", 293 + instanceId, 294 + }); 284 295 }); 285 296 } 286 297 ··· 297 308 data: codec.encode(event.data), 298 309 }); 299 310 } 300 - }); 301 - 302 - // Send out ping 303 - channel.postMessage({ 304 - type: "PING", 305 - instanceId, 306 311 }); 307 312 308 313 // Action handler ··· 374 379 effectFn(value); 375 380 } 376 381 }); 377 - } 378 - 379 - export function makeConnect<X>(context: DiffuseApplet<X>) { 380 - return <D, T>(applet: Applet<D>, dataFn: (data: D) => T, effectFn: (t: T) => void) => { 381 - return reactive(applet, dataFn, (t: T) => { 382 - if (context.isMainInstance()) effectFn(t); 383 - }); 384 - }; 385 382 } 386 383 387 384 ////////////////////////////////////////////
+4 -1
src/scripts/common.ts
··· 178 178 }; 179 179 } 180 180 181 - export function sync<DataType = unknown>(context: DiffuseApplet<DataType>, port: MessagePort) { 181 + export function sync<DataType = unknown>( 182 + context: DiffuseApplet<DataType>, 183 + port: MessagePort | Worker, 184 + ) { 182 185 port.onmessage = (event) => { 183 186 if (event.data?.type === "data") { 184 187 context.data = event.data.data;
+14 -8
src/scripts/engine/queue/worker.ts
··· 18 18 19 19 const { ports, tasks } = provide({ 20 20 actions, 21 - tasks: actions, 21 + tasks: { ...actions, data }, 22 22 }); 23 23 24 24 export type Actions = typeof actions; ··· 39 39 const [now, setNow] = signal<Item | null>(null); 40 40 41 41 effect(() => { 42 - const state: State = { 43 - future: future(), 44 - past: past(), 45 - now: now(), 46 - }; 47 - 48 42 postMessages({ 49 43 data: { 50 44 type: "data", 51 - data: state, 45 + data: state(), 52 46 }, 53 47 ports: ports.applets, 54 48 transfer: getTransferables(state), 55 49 }); 56 50 }); 51 + 52 + function data() { 53 + return state(); 54 + } 55 + 56 + function state(): State { 57 + return { 58 + future: future(), 59 + past: past(), 60 + now: now(), 61 + }; 62 + } 57 63 58 64 //////////////////////////////////////////// 59 65 // ACTIONS
+1 -1
src/scripts/input/native-fs/worker.ts
··· 22 22 resolve, 23 23 }; 24 24 25 - const tasks = provide({ actions, tasks: actions }); 25 + const { tasks } = provide({ actions, tasks: actions }); 26 26 27 27 export type Actions = typeof actions; 28 28 export type Tasks = typeof tasks;
+1 -1
src/scripts/input/opensubsonic/worker.ts
··· 27 27 resolve, 28 28 }; 29 29 30 - const tasks = provide({ actions, tasks: actions }); 30 + const { tasks } = provide({ actions, tasks: actions }); 31 31 32 32 export type Actions = typeof actions; 33 33 export type Tasks = typeof tasks;
+1 -1
src/scripts/input/s3/worker.ts
··· 24 24 resolve, 25 25 }; 26 26 27 - const tasks = provide({ actions, tasks: actions }); 27 + const { tasks } = provide({ actions, tasks: actions }); 28 28 29 29 export type Actions = typeof actions; 30 30 export type Tasks = typeof tasks;
+16
src/scripts/orchestrator/primary/worker.ts
··· 1 + import { provide } from "@scripts/common"; 2 + 3 + //////////////////////////////////////////// 4 + // SETUP 5 + //////////////////////////////////////////// 6 + 7 + const actions = {}; 8 + 9 + const { tasks } = provide({ actions, tasks: actions }); 10 + 11 + export type Actions = typeof actions; 12 + export type Tasks = typeof tasks; 13 + 14 + //////////////////////////////////////////// 15 + // ACTIONS 16 + ////////////////////////////////////////////
+3 -3
src/scripts/theme/blur/index.ts
··· 21 21 b: applet("/constituent/blur/artwork-controller", { container, groupId: labelB }), 22 22 }; 23 23 24 - const _orchestrator = { 25 - queueTracks: applet("/orchestrator/queue-tracks", { groupId: labelA }), 26 - }; 24 + // const _orchestrator = { 25 + // primary: applet("/orchestrator/primary", { groupId: labelA }), 26 + // }; 27 27 28 28 // const engine = { 29 29 // queue: {
+2 -4
src/scripts/theme/pilot/index.ts
··· 13 13 queue: await applet<QueueEngine.State>("/engine/queue"), 14 14 }; 15 15 16 - const _orchestrator = { 17 - input: await applet("/orchestrator/input-cache"), 18 - queueAudio: await applet("/orchestrator/queue-audio"), 19 - queueTracks: await applet("/orchestrator/queue-tracks"), 16 + const orchestrator = { 17 + primary: await applet("/orchestrator/primary"), 20 18 }; 21 19 22 20 const ui = {
+38 -9
src/scripts/theme/webamp/index.ts
··· 1 1 import type { URLTrack } from "webamp"; 2 2 import Webamp from "webamp"; 3 3 4 - import type { ManagedOutput, ResolvedUri, Track } from "@applets/core/types.d.ts"; 4 + import type { GroupConsult, ManagedOutput, ResolvedUri, Track } from "@applets/core/types.d.ts"; 5 5 import { applet, inputUrl, wait } from "@scripts/applet/common"; 6 6 7 7 //////////////////////////////////////////// 8 8 // 🗂️ Applets 9 9 //////////////////////////////////////////// 10 10 const configurator = { 11 - input: await applet("/configurator/input"), 12 - output: await applet<ManagedOutput>("/configurator/output"), 11 + input: applet("/configurator/input"), 12 + output: applet<ManagedOutput>("/configurator/output"), 13 13 }; 14 14 15 15 const orchestrator = { 16 - input: await applet("/orchestrator/input-cache"), 16 + primary: applet("/orchestrator/primary"), 17 17 }; 18 18 19 19 //////////////////////////////////////////// ··· 27 27 const loadFromUrl = amp.media.loadFromUrl.bind(amp.media); 28 28 29 29 async function loadOverride(uri: string, autoPlay: boolean) { 30 - const resp = await inputUrl(configurator.input, uri); 30 + const resp = await inputUrl(await configurator.input, uri); 31 31 if (!resp) throw new Error("Failed to resolve URI"); 32 32 return await loadFromUrl(resp.url, autoPlay); 33 33 } ··· 41 41 amp.renderWhenReady(ampNode); 42 42 43 43 // Wait for tracks to load 44 - wait(configurator.output, (d) => d?.tracks.state === "loaded").then(loadAndInsert); 45 - configurator.output.ondata = loadAndInsert; 44 + configurator.output 45 + .then((output) => { 46 + output.ondata = loadAndInsert; 47 + return wait(output, (d) => d?.tracks.state === "loaded"); 48 + }) 49 + .then(async () => { 50 + await loadAndInsert(); 51 + (await orchestrator.primary).sendAction("process_inputs", undefined, { 52 + timeoutDuration: 60000 * 60, 53 + }); 54 + }); 46 55 47 56 // Load & insert 48 57 let inserting = false; 58 + let tracksCacheId: string | undefined = undefined; 49 59 50 60 async function loadAndInsert() { 51 - if (configurator.output.data.tracks.state !== "loaded") return; 61 + const output = await configurator.output; 62 + 63 + if (output.data.tracks.state !== "loaded") return; 64 + if (output.data.tracks.cacheId === tracksCacheId) return; 52 65 if (inserting) return; 66 + 53 67 inserting = true; 68 + tracksCacheId = output.data.tracks.cacheId; 54 69 const tracks = await loadTracks(); 55 70 56 71 // TODO: This kinda messes up the UI, ··· 70 85 // 🛠️ 71 86 //////////////////////////////////////////// 72 87 async function loadTracks(): Promise<URLTrack[]> { 73 - const tracks = configurator.output.data.tracks.collection; 88 + const input = await configurator.input; 89 + const output = await configurator.output; 90 + 91 + const groups = await input.sendAction<GroupConsult>( 92 + "groupConsult", 93 + output.data.tracks.collection, 94 + { timeoutDuration: 60000 * 5, worker: true }, 95 + ); 96 + 97 + // Available tracks 98 + const tracks = Object.values(groups).reduce((acc: Track[], value) => { 99 + if (value.available === false) return acc; 100 + return [...acc, ...value.tracks]; 101 + }, []); 102 + 74 103 return tracks.map((track) => { 75 104 const urlTrack: URLTrack = { 76 105 url: track.uri,