A music player that connects to your cloud/distributed storage.
at v4 5.3 kB view raw
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}