1import { announce, ostiary, rpc } from "@common/worker.js";
2import { effect, signal } from "@common/signal.js";
3import { arrayShuffle, hash } from "@common/index.js";
4
5/**
6 * @import {Actions, Item} from "./types.d.ts"
7 * @import {Track} from "@definitions/types.d.ts"
8 */
9
10////////////////////////////////////////////
11// STATE
12////////////////////////////////////////////
13
14export const $lake = signal(/** @type {Track[]} */ ([]));
15
16// Communicated state
17export const $future = signal(/** @type {Item[]} */ ([]));
18export const $now = signal(/** @type {Item | null} */ (null));
19export const $past = signal(/** @type {Item[]} */ ([]));
20export const $poolHash = signal(hash([]));
21
22////////////////////////////////////////////
23// ACTIONS
24////////////////////////////////////////////
25
26/**
27 * @type {Actions['add']}
28 */
29export function add({ inFront, tracks }) {
30 const items = tracks.map((track) => {
31 return { ...track, manualEntry: true };
32 });
33
34 $future.value = inFront
35 ? [...items, ...$future.value]
36 : [...$future.value, ...items];
37}
38
39/**
40 * @type {Actions['fill']}
41 */
42export function fill({ augment, amount, shuffled }) {
43 $future.value = fillQueue(
44 shuffled,
45 amount +
46 (augment
47 ? $future.value.filter((i) => i.manualEntry === false).length
48 : 0),
49 $future.value,
50 );
51}
52
53/**
54 * @type {Actions['pool']}
55 */
56export function pool(tracks) {
57 $lake.value = tracks;
58 $poolHash.value = hash(tracks);
59}
60
61/**
62 * @type {Actions['shift']}
63 */
64export function shift() {
65 return _shift();
66}
67
68/**
69 * @type {Actions['unshift']}
70 */
71export function unshift() {
72 const p = $past.value;
73 if (p.length === 0) return;
74
75 const n = $now.value;
76 const [last] = p.splice(p.length - 1, 1);
77
78 $now.value = last ?? null;
79 if (n) $future.value = [n, ...$future.value];
80}
81
82////////////////////////////////////////////
83// ⚡️
84////////////////////////////////////////////
85
86ostiary((context, _firstConnection, _connectionId) => {
87 // Setup RPC
88
89 rpc(context, {
90 add,
91 fill,
92 pool,
93 shift,
94 unshift,
95
96 // State
97 future: $future.get,
98 now: $now.get,
99 past: $past.get,
100 poolHash: $poolHash.get,
101 });
102
103 // Effects
104
105 // Communicate state
106 effect(() => announce("future", $future.value, context));
107 effect(() => announce("now", $now.value, context));
108 effect(() => announce("past", $past.value, context));
109 effect(() => announce("poolHash", $poolHash.value, context));
110
111 // When the pool changes,
112 // make sure all future queue items still exist.
113 effect(() => {
114 const existing = new Set($lake.value.map((t) => t.id));
115
116 $future.value = $future.value.filter((i) => {
117 return existing.has(i.id);
118 });
119 });
120});
121
122////////////////////////////////////////////
123// ⛔️
124////////////////////////////////////////////
125
126/**
127 * Add non-manual items to the queue.
128 *
129 * @param {boolean} shuffled
130 * @param {number | undefined | null} fillAmount
131 * @param {Item[]} future
132 * @returns {Item[]}
133 */
134function fillQueue(shuffled, fillAmount, future) {
135 if (!fillAmount) return future;
136
137 // Count
138 let autoFutureCount = 0;
139 let manualFutureCount = 0;
140
141 future.forEach((item) => {
142 if (item.manualEntry) manualFutureCount++;
143 else autoFutureCount++;
144 });
145
146 // Fill
147 if (shuffled) {
148 if (autoFutureCount >= fillAmount) return future;
149 return fillShuffle(fillAmount, future, autoFutureCount);
150 } else {
151 return fillSequentially(fillAmount, future);
152 }
153}
154
155/**
156 * @param {number} fillAmount
157 * @param {Item[]} future
158 * @returns {Item[]}
159 */
160export function fillSequentially(fillAmount, future) {
161 const onlyManual = future.filter((i) => i.manualEntry);
162 const lastManual = onlyManual.slice(-1)[0];
163 const startIndex = lastManual
164 ? $lake.value.findIndex((t) => t.id === lastManual.id) + 1
165 : $now.value
166 ? $lake.value.findIndex((t) => t.id === $now.value?.id) + 1
167 : 0;
168
169 const maxIndex = $lake.value.length - 1;
170 let currIndex = startIndex;
171
172 /** @type {Item[]} */
173 const autoItems = [];
174
175 for (let i = 0; i < fillAmount; i++) {
176 if (currIndex > maxIndex) currIndex = 0;
177 const item = $lake.value[currIndex];
178 if (item) {
179 autoItems.push({
180 ...item,
181 manualEntry: false,
182 });
183 }
184 currIndex++;
185 }
186
187 return [...onlyManual, ...autoItems];
188}
189
190/**
191 * @param {number} fillAmount
192 * @param {Item[]} future
193 * @param {number} autoFutureCount
194 * @returns {Item[]}
195 */
196export function fillShuffle(fillAmount, future, autoFutureCount) {
197 // Determine pool of available queue items
198 /** @type {Item[]} */
199 const pool = [];
200
201 const pastSet = new Set($past.value.map((i) => i.id));
202 let reducedPool = pool;
203
204 $lake.value.forEach((track) => {
205 if (pastSet.delete(track.id) === false) {
206 pool.push({
207 ...track,
208 manualEntry: false,
209 });
210 }
211 });
212
213 if (reducedPool.length === 0) {
214 reducedPool = $lake.value;
215 }
216
217 const poolSelection = arrayShuffle(reducedPool).slice(
218 0,
219 Math.max(0, fillAmount - autoFutureCount),
220 );
221
222 return [...future, ...poolSelection];
223}
224
225/**
226 * @param {Item[]} [future]
227 */
228export function _shift(future) {
229 const n = $now.value;
230 const f = future ?? $future.value;
231
232 $now.value = f[0] ?? null;
233 if (n) $past.value = [...$past.value, n];
234 $future.value = f.slice(1);
235}