Experiment to rebuild Diffuse using web applets.
1import { getTransferables } from "@okikio/transferables";
2
3import type { Track } from "@applets/core/types.js";
4import type { Item, State } from "./types";
5import { arrayShuffle, postMessages, provide, transfer } from "@scripts/common.ts";
6
7////////////////////////////////////////////
8// SETUP
9////////////////////////////////////////////
10
11const actions = {
12 add,
13 pool,
14 shift,
15 unshift,
16};
17
18const { ports, tasks } = provide({
19 actions,
20 tasks: { ...actions, data },
21});
22
23export type Actions = typeof actions;
24export type Tasks = typeof tasks;
25
26////////////////////////////////////////////
27// STATE
28////////////////////////////////////////////
29
30const QUEUE_SIZE = 25;
31
32const _internal: Record<string, { pool: Track[] }> = {};
33const _state: Record<string, State> = {};
34
35function data(groupId: string) {
36 return state(groupId);
37}
38
39function emptyState(groupId: string): State {
40 return {
41 future: [],
42 now: null,
43 past: [],
44 };
45}
46
47function notify(groupId: string) {
48 const d = data(groupId);
49
50 postMessages({
51 data: {
52 type: "data",
53 data: d,
54 groupId,
55 },
56 ports: ports.applets,
57 transfer: getTransferables(d),
58 });
59}
60
61function internal(groupId: string) {
62 _internal[groupId] ??= { pool: [] };
63 return _internal[groupId];
64}
65
66function state(groupId: string) {
67 _state[groupId] ??= emptyState(groupId);
68 return _state[groupId];
69}
70
71////////////////////////////////////////////
72// ACTIONS
73////////////////////////////////////////////
74
75function add({ groupId, items }: { groupId: string; items: Item[] }) {
76 state(groupId).future = [...state(groupId).future, ...items];
77 notify(groupId);
78}
79
80function pool({ groupId, tracks }: { groupId: string; tracks: Track[] }) {
81 internal(groupId).pool = tracks;
82 const queue = state(groupId);
83
84 // TODO: If the pool changes, only remove non-existing tracks
85 // instead of resetting the whole future queue.
86 //
87 // What about past queue items?
88
89 queue.future = [];
90 fill(groupId);
91
92 // Automatically insert track if there isn't any
93 if (!queue.now) return shift({ groupId });
94 else notify(groupId);
95}
96
97function shift({ groupId }: { groupId: string }) {
98 const queue = state(groupId);
99 const now = queue.future[0] ?? null;
100 queue.now = now;
101
102 queue.future = queue.future.slice(1);
103 queue.past = now ? [...queue.past, now] : queue.past;
104
105 fill(groupId);
106}
107
108function unshift({ groupId }: { groupId: string }) {
109 const queue = state(groupId);
110 if (queue.past.length === 0) return;
111
112 const [last] = queue.past.splice(queue.past.length - 1, 1);
113 const now = last ?? null;
114
115 queue.now = now;
116 queue.future = now ? [now, ...queue.future] : queue.future;
117
118 notify(groupId);
119}
120
121// 🛠️
122
123// TODO: Most likely there's a more performant solution
124function fill(groupId: string) {
125 const queue = state(groupId);
126 if (queue.future.length >= QUEUE_SIZE) return;
127
128 const pool: Track[] = [];
129
130 let past = new Set(queue.past.map((t) => t.id));
131 let reducedPool = pool;
132
133 internal(groupId).pool.forEach((track: Track) => {
134 if (past.has(track.id)) {
135 past = past.difference(new Set(track.id));
136 } else {
137 pool.push(track);
138 }
139 });
140
141 if (reducedPool.length === 0) {
142 reducedPool = internal(groupId).pool;
143 }
144
145 const poolSelection = arrayShuffle(reducedPool).slice(0, QUEUE_SIZE - queue.future.length);
146 add({ groupId, items: poolSelection });
147}