A music player that connects to your cloud/distributed storage. diffuse.sh
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841import { announce, ostiary, rpc } from "~/common/worker.js";import { effect, signal } from "~/common/signal.js";import { arrayShuffle } from "~/common/utils.js";import { xxh32 } from "xxh32";
/** * @import {Actions, Item} from "@specs/components/engine/queue/types.d.ts" */
////////////////////////////////////////////// STATE////////////////////////////////////////////
let _key = 0;const nextKey = () => String(++_key);
/** Ordered list of available track IDs. */export const $lake = signal(/** @type {string[]} */ ([]));
// Communicated stateexport const $future = signal(/** @type {Item[]} */ ([]));export const $now = signal(/** @type {Item | null} */ (null));export const $past = signal(/** @type {Item[]} */ ([]));export const $supplyFingerprint = signal( /** @type {string | undefined} */ (undefined),);
////////////////////////////////////////////// ACTIONS////////////////////////////////////////////
/** * @type {Actions['add']} * * @example Adds tracks after the last manual entry, before any auto-filled items * ```js * import { add, $future } from "~/components/engine/queue/worker.js"; * * add({ trackIds: ["a", "b"] }); * * if ($future.value.length !== 2) throw new Error("expected 2 items"); * if ($future.value[0].id !== "a") throw new Error("wrong first item"); * if ($future.value[1].id !== "b") throw new Error("wrong second item"); * if (!$future.value[0].manualEntry) throw new Error("items should be manualEntry: true"); * ``` * * @example Inserts before auto-filled items when they are present * ```js * import { add, $future } from "~/components/engine/queue/worker.js"; * * $future.value = [ * { id: "manual", key: "1", manualEntry: true }, * { id: "auto", key: "2", manualEntry: false }, * ]; * * add({ trackIds: ["new"] }); * * if ($future.value[0].id !== "manual") throw new Error("expected 'manual' first"); * if ($future.value[1].id !== "new") throw new Error("expected 'new' second"); * if ($future.value[2].id !== "auto") throw new Error("expected 'auto' last"); * ``` * * @example Prepends tracks to the front with inFront: true * ```js * import { add, $future } from "~/components/engine/queue/worker.js"; * * add({ trackIds: ["c"] }); * add({ inFront: true, trackIds: ["a", "b"] }); * * if ($future.value[0].id !== "a") throw new Error("expected 'a' first"); * if ($future.value[1].id !== "b") throw new Error("expected 'b' second"); * if ($future.value[2].id !== "c") throw new Error("expected 'c' last"); * ``` */export function add({ inFront, trackIds }) { const items = trackIds.map((id) => { return { id, key: nextKey(), manualEntry: true }; });
if (inFront) { $future.value = [...items, ...$future.value]; } else { let lastManualIdx = -1; for (let i = 0; i < $future.value.length; i++) { if ($future.value[i].manualEntry) lastManualIdx = i; } $future.value = [ ...$future.value.slice(0, lastManualIdx + 1), ...items, ...$future.value.slice(lastManualIdx + 1), ]; }}
/** * @type {Actions['clear']} * * @example Keeps manual entries when keepManual is true * ```js * import { clear, $future } from "~/components/engine/queue/worker.js"; * * $future.value = [ * { id: "manual", key: "1", manualEntry: true }, * { id: "auto", key: "2", manualEntry: false }, * ]; * clear({ keepManual: true }); * * if ($future.value.length !== 1) throw new Error("expected 1 item remaining"); * if ($future.value[0].id !== "manual") throw new Error("manual entry should remain"); * ``` * * @example Clears all items when keepManual is false * ```js * import { clear, $future } from "~/components/engine/queue/worker.js"; * * $future.value = [ * { id: "manual", key: "1", manualEntry: true }, * { id: "auto", key: "2", manualEntry: false }, * ]; * clear({ keepManual: false }); * * if ($future.value.length !== 0) throw new Error("expected empty queue"); * ``` */export function clear({ keepManual }) { $future.value = keepManual ? $future.value.filter((i) => i.manualEntry === true) : [];}
/** * @type {Actions['expel']} * * @example Removes an item from the future by key * ```js * import { expel, $future } from "~/components/engine/queue/worker.js"; * * $future.value = [ * { id: "a", key: "1", manualEntry: true }, * { id: "b", key: "2", manualEntry: true }, * ]; * * expel({ key: "1" }); * * if ($future.value.length !== 1) throw new Error("expected 1 item remaining"); * if ($future.value[0].id !== "b") throw new Error("expected 'b' to remain"); * ``` * * @example Removes the now-playing item by key, setting now to null * ```js * import { expel, $now } from "~/components/engine/queue/worker.js"; * * $now.value = { id: "a", key: "1", manualEntry: false }; * * expel({ key: "1" }); * * if ($now.value !== null) throw new Error("expected now to be null"); * ``` * * @example Removes an item from the past by key * ```js * import { expel, $past } from "~/components/engine/queue/worker.js"; * * $past.value = [ * { id: "a", key: "1", manualEntry: false }, * { id: "b", key: "2", manualEntry: false }, * ]; * * expel({ key: "1" }); * * if ($past.value.length !== 1) throw new Error("expected 1 item remaining"); * if ($past.value[0].id !== "b") throw new Error("expected 'b' to remain"); * ``` * * @example Does nothing for an unknown key * ```js * import { expel, $past, $now, $future } from "~/components/engine/queue/worker.js"; * * $past.value = [{ id: "a", key: "1", manualEntry: false }]; * $now.value = { id: "b", key: "2", manualEntry: false }; * $future.value = [{ id: "c", key: "3", manualEntry: false }]; * * expel({ key: "z" }); * * if ($past.value.length !== 1) throw new Error("past should be unchanged"); * if ($now.value?.id !== "b") throw new Error("now should be unchanged"); * if ($future.value.length !== 1) throw new Error("future should be unchanged"); * ``` */export function expel({ key }) { const pastIdx = $past.value.findIndex((i) => i.key === key); if (pastIdx !== -1) { const p = [...$past.value]; p.splice(pastIdx, 1); $past.value = p; return; } if ($now.value?.key === key) { $now.value = null; return; } const futureIdx = $future.value.findIndex((i) => i.key === key); if (futureIdx !== -1) { const f = [...$future.value]; f.splice(futureIdx, 1); $future.value = f; }}
/** * @type {Actions['fill']} */export function fill({ augment, amount, shuffled }) { $future.value = fillQueue( shuffled, amount + (augment ? $future.value.filter((i) => i.manualEntry === false).length : 0), $future.value, );}
/** * @type {Actions['move']} * * @example Moves an item forward in the flat list * ```js * import { move, $future } from "~/components/engine/queue/worker.js"; * * $future.value = [ * { id: "a", key: "1", manualEntry: true }, * { id: "b", key: "2", manualEntry: true }, * { id: "c", key: "3", manualEntry: true }, * ]; * * move({ key: "1", to: 2 }); * * if ($future.value[0].id !== "b") throw new Error("expected 'b' first"); * if ($future.value[1].id !== "c") throw new Error("expected 'c' second"); * if ($future.value[2].id !== "a") throw new Error("expected 'a' last"); * ``` * * @example Moves an item backward in the flat list * ```js * import { move, $future } from "~/components/engine/queue/worker.js"; * * $future.value = [ * { id: "a", key: "1", manualEntry: true }, * { id: "b", key: "2", manualEntry: true }, * { id: "c", key: "3", manualEntry: true }, * ]; * * move({ key: "3", to: 0 }); * * if ($future.value[0].id !== "c") throw new Error("expected 'c' first"); * if ($future.value[1].id !== "a") throw new Error("expected 'a' second"); * if ($future.value[2].id !== "b") throw new Error("expected 'b' last"); * ``` * * @example Preserves now identity when reordering across past/future * ```js * import { move, $past, $now, $future } from "~/components/engine/queue/worker.js"; * * $past.value = [{ id: "a", key: "1", manualEntry: false }]; * $now.value = { id: "b", key: "2", manualEntry: false }; * $future.value = [{ id: "c", key: "3", manualEntry: false }]; * * // flat list is [a(0), b(1), c(2)]; moving c to front → [c, a, b] * move({ key: "3", to: 0 }); * * if ($now.value?.id !== "b") throw new Error("now should still be 'b'"); * if ($past.value[0]?.id !== "c") throw new Error("expected 'c' first in past"); * if ($past.value[1]?.id !== "a") throw new Error("expected 'a' second in past"); * if ($future.value.length !== 0) throw new Error("future should be empty"); * ``` * * @example Does nothing when the item is already at the target position * ```js * import { move, $future } from "~/components/engine/queue/worker.js"; * * $future.value = [{ id: "a", key: "1", manualEntry: true }, { id: "b", key: "2", manualEntry: true }]; * * move({ key: "2", to: 1 }); * * if ($future.value[0].id !== "a") throw new Error("order should be unchanged"); * if ($future.value[1].id !== "b") throw new Error("order should be unchanged"); * ``` * * @example Does nothing for out-of-bounds target or unknown key * ```js * import { move, $future } from "~/components/engine/queue/worker.js"; * * $future.value = [{ id: "a", key: "1", manualEntry: true }, { id: "b", key: "2", manualEntry: true }]; * * move({ key: "1", to: 99 }); * move({ key: "z", to: 0 }); * * if ($future.value[0].id !== "a") throw new Error("order should be unchanged"); * if ($future.value[1].id !== "b") throw new Error("order should be unchanged"); * ``` */export function move({ key, to }) { const past = $past.value; const now = $now.value; const future = $future.value; const pLen = past.length; const nLen = now ? 1 : 0; const futureStart = pLen + nLen; const total = futureStart + future.length;
let from = past.findIndex((i) => i.key === key); if (from === -1 && now?.key === key) from = pLen; if (from === -1) { const fi = future.findIndex((i) => i.key === key); if (fi !== -1) from = futureStart + fi; }
if (from === -1 || from === to || to < 0 || to >= total) return;
// Compute now's new flat index after the move let nowIdx = pLen; if (nLen) { if (from === pLen) nowIdx = to; else if (from < to && pLen > from && pLen <= to) nowIdx = pLen - 1; else if (from > to && pLen >= to && pLen < from) nowIdx = pLen + 1; }
// Map a post-move flat index back to the original flat index const origIdx = (/** @type {number} */ i) => { if (from < to) { if (i < from) return i; if (i < to) return i + 1; if (i === to) return from; } else { if (i < to) return i; if (i === to) return from; if (i <= from) return i - 1; } return i; };
const flatGet = (/** @type {number} */ i) => { const j = origIdx(i); return j < pLen ? past[j] : j < futureStart ? now : future[j - futureStart]; };
$past.value = /** @type {Item[]} */ (Array.from( { length: nowIdx }, (_, i) => flatGet(i), )); $future.value = /** @type {Item[]} */ (Array.from( { length: total - nowIdx - nLen }, (_, i) => flatGet(nowIdx + nLen + i), ));}
/** * @type {Actions['shift']} * * Advances the queue by one position (default) or by `by` positions. * * When `by` is greater than 1 the queue advances in a single signal * update, so only one `now` announcement reaches the main thread. This * prevents intermediate tracks — e.g. a preloaded track sitting at * `future[0]` — from briefly becoming `now` and starting playback when * the user jumps to a track further ahead in the queue. * * @example Advances by 2 positions, moving intermediate items to past * ```js * import { shift, $future, $now, $past } from "~/components/engine/queue/worker.js"; * * $past.value = []; * $now.value = { id: "a", key: "1", manualEntry: false }; * $future.value = [ * { id: "b", key: "2", manualEntry: false }, * { id: "c", key: "3", manualEntry: false }, * { id: "d", key: "4", manualEntry: false }, * ]; * * shift({ by: 2 }); * * if ($now.value?.id !== "c") throw new Error("expected 'c' as now"); * if ($past.value.map((i) => i.id).join(",") !== "a,b") throw new Error("expected 'a,b' in past"); * if ($future.value.map((i) => i.id).join(",") !== "d") throw new Error("expected 'd' in future"); * ``` * * @example Caps at available future items * ```js * import { shift, $now, $future } from "~/components/engine/queue/worker.js"; * * $now.value = { id: "a", key: "1", manualEntry: false }; * $future.value = [{ id: "b", key: "2", manualEntry: false }]; * * shift({ by: 5 }); * * if ($now.value?.id !== "b") throw new Error("expected 'b' as now"); * if ($future.value.length !== 0) throw new Error("expected empty future"); * ``` */export function shift(args) { const by = args?.by; if (!by || by <= 1) return _shift();
const n = $now.value; const f = $future.value; const items = f.slice(0, by); if (items.length === 0) return;
$now.value = items[items.length - 1]; if (n) $past.value = [...$past.value, n, ...items.slice(0, -1)]; $future.value = f.slice(items.length);}
/** * @type {Actions['supply']} * * @example Sets the track pool and computes a fingerprint * ```js * import { supply, $lake, $supplyFingerprint } from "~/components/engine/queue/worker.js"; * * supply({ trackIds: ["a", "b", "c"] }); * * if ($lake.value.join(",") !== "a,b,c") throw new Error("lake not set correctly"); * if (typeof $supplyFingerprint.value !== "string") throw new Error("fingerprint should be a string"); * ``` * * @example Returns undefined fingerprint for an empty supply * ```js * import { supply, $supplyFingerprint } from "~/components/engine/queue/worker.js"; * * supply({ trackIds: [] }); * * if ($supplyFingerprint.value !== undefined) throw new Error("fingerprint should be undefined for empty supply"); * ``` * * @example Same track IDs produce the same fingerprint * ```js * import { supply, $supplyFingerprint } from "~/components/engine/queue/worker.js"; * * supply({ trackIds: ["x", "y"] }); * const first = $supplyFingerprint.value; * * supply({ trackIds: ["x", "y"] }); * const second = $supplyFingerprint.value; * * if (first !== second) throw new Error("same tracks should produce the same fingerprint"); * ``` */export function supply({ trackIds }) { $lake.value = trackIds; $supplyFingerprint.value = trackIds.length ? xxh32(trackIds.join("\0")).toString() : undefined;}
/** * @type {Actions['unshift']} * * Rewinds the queue by one position (default) or by `by` positions. * * Like `shift({ by })`, this updates the signals exactly once so only * one `now` announcement reaches the main thread. * * @example Moves the last past item back to now, pushing now to the front of future * ```js * import { unshift, $future, $now, $past } from "~/components/engine/queue/worker.js"; * * $past.value = [{ id: "prev", key: "1", manualEntry: false }]; * $now.value = { id: "current", key: "2", manualEntry: false }; * $future.value = []; * * unshift(); * * if ($now.value?.id !== "prev") throw new Error("expected 'prev' as now"); * if ($past.value.length !== 0) throw new Error("expected empty past"); * if ($future.value[0]?.id !== "current") throw new Error("expected 'current' back at front of future"); * ``` * * @example Does nothing when past is empty * ```js * import { unshift, $now, $past } from "~/components/engine/queue/worker.js"; * * $past.value = []; * $now.value = { id: "current", key: "1", manualEntry: false }; * * unshift(); * * if ($now.value?.id !== "current") throw new Error("now should remain unchanged"); * ``` * * @example Rewinds by 2 positions, pushing intermediate items to future * ```js * import { unshift, $future, $now, $past } from "~/components/engine/queue/worker.js"; * * $past.value = [ * { id: "a", key: "1", manualEntry: false }, * { id: "b", key: "2", manualEntry: false }, * ]; * $now.value = { id: "c", key: "3", manualEntry: false }; * $future.value = [{ id: "d", key: "4", manualEntry: false }]; * * unshift({ by: 2 }); * * if ($now.value?.id !== "a") throw new Error("expected 'a' as now"); * if ($past.value.length !== 0) throw new Error("expected empty past"); * if ($future.value.map((i) => i.id).join(",") !== "b,c,d") throw new Error("expected 'b,c,d' in future"); * ``` * * @example Caps at available past items * ```js * import { unshift, $now, $past } from "~/components/engine/queue/worker.js"; * * $past.value = [{ id: "a", key: "1", manualEntry: false }]; * $now.value = { id: "b", key: "2", manualEntry: false }; * * unshift({ by: 5 }); * * if ($now.value?.id !== "a") throw new Error("expected 'a' as now"); * if ($past.value.length !== 0) throw new Error("expected empty past"); * ``` */export function unshift(args) { const by = args?.by ?? 1; const p = $past.value; if (p.length === 0) return;
const n = $now.value; const items = p.slice(p.length - by);
$past.value = p.slice(0, p.length - items.length); $now.value = items[0] ?? null; if (n) $future.value = [...items.slice(1), n, ...$future.value]; else $future.value = [...items.slice(1), ...$future.value];}
////////////////////////////////////////////// ⚡️////////////////////////////////////////////
ostiary((context, _firstConnection, _connectionId) => { // Setup RPC
rpc(context, { add, clear, expel, fill, move, shift, supply, unshift,
// State future: $future.get, now: $now.get, past: $past.get, supplyFingerprint: $supplyFingerprint.get, });
// Effects
// Communicate state effect(() => announce("future", $future.value, context)); effect(() => announce("now", $now.value, context)); effect(() => announce("past", $past.value, context)); effect(() => announce("supplyFingerprint", $supplyFingerprint.value, context) );});
////////////////////////////////////////////// ⛔️////////////////////////////////////////////
/** * Add non-manual items to the queue. * * @param {boolean} shuffled * @param {number | undefined | null} fillAmount * @param {Item[]} future * @returns {Item[]} */function fillQueue(shuffled, fillAmount, future) { if (!fillAmount) return future;
// Count let autoFutureCount = 0;
future.forEach((item) => { if (item.manualEntry) {} else autoFutureCount++; });
// Fill if (shuffled) { if (autoFutureCount >= fillAmount) return future; return fillShuffle(fillAmount, future, autoFutureCount); } else { return fillSequentially(fillAmount, future); }}
/** * @param {number} fillAmount * @param {Item[]} future * @returns {Item[]} * * @example Fills sequentially from the start of the lake * ```js * import { fillSequentially, $lake, $now } from "~/components/engine/queue/worker.js"; * * $lake.value = ["a", "b", "c", "d"]; * $now.value = null; * * const result = fillSequentially(3, []); * * if (result.length !== 3) throw new Error("expected 3 items"); * if (result[0].id !== "a") throw new Error("expected to start from 'a'"); * if (result[1].id !== "b") throw new Error("expected 'b' second"); * if (result[2].id !== "c") throw new Error("expected 'c' third"); * if (result[0].manualEntry !== false) throw new Error("auto items should have manualEntry: false"); * ``` * * @example Continues from after the current now item * ```js * import { fillSequentially, $lake, $now } from "~/components/engine/queue/worker.js"; * * $lake.value = ["a", "b", "c", "d"]; * $now.value = { id: "b", key: "1", manualEntry: false }; * * const result = fillSequentially(2, []); * * if (result[0].id !== "c") throw new Error("expected to start after now ('c')"); * if (result[1].id !== "d") throw new Error("expected 'd' second"); * ``` * * @example Wraps around to the beginning when reaching the end of the lake * ```js * import { fillSequentially, $lake, $now } from "~/components/engine/queue/worker.js"; * * $lake.value = ["a", "b", "c"]; * $now.value = { id: "b", key: "1", manualEntry: false }; * * const result = fillSequentially(3, []); * * if (result[0].id !== "c") throw new Error("expected 'c'"); * if (result[1].id !== "a") throw new Error("expected wrap around to 'a'"); * if (result[2].id !== "b") throw new Error("expected 'b'"); * ``` * * @example Preserves existing manual entries * ```js * import { fillSequentially, $lake, $now } from "~/components/engine/queue/worker.js"; * * $lake.value = ["a", "b", "c"]; * $now.value = null; * * const future = [{ id: "manual", key: "1", manualEntry: true }]; * const result = fillSequentially(2, future); * * if (result[0].id !== "manual") throw new Error("manual entry should be preserved"); * if (result.length !== 3) throw new Error("expected manual + 2 auto items"); * ``` */export function fillSequentially(fillAmount, future) { const onlyManual = future.filter((i) => i.manualEntry); const lastManual = onlyManual.slice(-1)[0]; const startIndex = lastManual ? $lake.value.indexOf(lastManual.id) + 1 : $now.value ? $lake.value.indexOf($now.value.id) + 1 : 0;
const maxIndex = $lake.value.length - 1; let currIndex = startIndex;
/** @type {Item[]} */ const autoItems = [];
for (let i = 0; i < fillAmount; i++) { if (currIndex > maxIndex) currIndex = 0; const id = $lake.value[currIndex]; if (id) { autoItems.push({ id, key: nextKey(), manualEntry: false }); } currIndex++; }
return [...onlyManual, ...autoItems];}
/** * @param {number} fillAmount * @param {Item[]} future * @param {number} autoFutureCount * @returns {Item[]} * * @example Adds shuffled items to reach the fill amount * ```js * import { fillShuffle, $lake, $past } from "~/components/engine/queue/worker.js"; * * $lake.value = ["a", "b", "c", "d", "e"]; * $past.value = []; * * const result = fillShuffle(3, [], 0); * * if (result.length !== 3) throw new Error("expected 3 items"); * if (!result.every((i) => i.manualEntry === false)) throw new Error("all items should be auto"); * ``` * * @example Only adds enough to reach the fill amount given existing auto items * ```js * import { fillShuffle, $lake, $past } from "~/components/engine/queue/worker.js"; * * $lake.value = ["a", "b", "c", "d"]; * $past.value = []; * * const existing = [ * { id: "x", key: "1", manualEntry: false }, * { id: "y", key: "2", manualEntry: false }, * ]; * * const result = fillShuffle(4, existing, 2); * * if (result.length !== 4) throw new Error("expected 4 total items (2 existing + 2 new)"); * ``` * * @example Does not add tracks that have already played or are now playing * ```js * import { fillShuffle, $lake, $past, $now } from "~/components/engine/queue/worker.js"; * * $lake.value = ["a", "b", "c", "d"]; * $past.value = [{ id: "a", key: "1", manualEntry: false }]; * $now.value = { id: "b", key: "2", manualEntry: false }; * * const result = fillShuffle(4, [], 0); * * if (result.some((i) => i.id === "a" || i.id === "b")) throw new Error("past and now tracks should be excluded"); * ``` * * @example Falls back to full lake when everything has been played * ```js * import { fillShuffle, $lake, $past } from "~/components/engine/queue/worker.js"; * * $lake.value = ["a", "b"]; * $past.value = [{ id: "a", key: "1", manualEntry: false }, { id: "b", key: "2", manualEntry: false }]; * * const result = fillShuffle(2, [], 0); * * if (result.length !== 2) throw new Error("expected 2 items from full lake fallback"); * ``` */export function fillShuffle(fillAmount, future, autoFutureCount) { const excludeIds = new Set($past.value.map((i) => i.id)); if ($now.value) excludeIds.add($now.value.id); future.forEach((i) => excludeIds.add(i.id));
let pool = $lake.value.filter((id) => !excludeIds.has(id));
// Fallback: if everything has been played/is playing/is queued, use tracks not in past or now if (pool.length === 0) { const pastAndNowIds = new Set($past.value.map((i) => i.id)); if ($now.value) pastAndNowIds.add($now.value.id); pool = $lake.value.filter((id) => !pastAndNowIds.has(id)); }
// Final fallback: everything has been played, use the full lake if (pool.length === 0) { pool = [...$lake.value]; }
const selected = arrayShuffle(pool).slice( 0, Math.max(0, fillAmount - autoFutureCount), );
return [ ...future, ...selected.map((id) => ({ id, key: nextKey(), manualEntry: false })), ];}
/** * @param {Item[]} [future] * * @example Moves the first future item to now * ```ts * import { _shift, $future, $now } from "~/components/engine/queue/worker.js"; * import type { Item } from "@specs/components/engine/queue/types.d.ts" * * $now.value = null as null | Item; * $future.value = [{ id: "a", key: "1", manualEntry: false }, { id: "b", key: "2", manualEntry: false }]; * * _shift(); * * if ($now.value?.id !== "a") throw new Error("expected 'a' as now"); * if ($future.value.length !== 1) throw new Error("expected 1 item remaining in future"); * if ($future.value[0].id !== "b") throw new Error("expected 'b' remaining in future"); * ``` * * @example Moves previous now to past * ```js * import { _shift, $future, $now, $past } from "~/components/engine/queue/worker.js"; * * $past.value = []; * $now.value = { id: "prev", key: "1", manualEntry: false }; * $future.value = [{ id: "next", key: "2", manualEntry: false }]; * * _shift(); * * if ($now.value?.id !== "next") throw new Error("expected 'next' as now"); * if ($past.value.length !== 1) throw new Error("expected 1 past item"); * if ($past.value[0].id !== "prev") throw new Error("expected 'prev' in past"); * ``` * * @example Does nothing when future is empty * ```js * import { _shift, $future, $now } from "~/components/engine/queue/worker.js"; * * $future.value = []; * $now.value = null; * * _shift(); * * if ($now.value !== null) throw new Error("now should remain null"); * ``` */export function _shift(future) { const n = $now.value; const f = future ?? $future.value; const v = f[0];
if (!v) return; $now.value = v; if (n) $past.value = [...$past.value, n]; $future.value = f.slice(1);}