A music player that connects to your cloud/distributed storage.
5
fork

Configure Feed

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

at v4 752 lines 23 kB view raw
1import { announce, ostiary, rpc } from "~/common/worker.js"; 2import { effect, signal } from "~/common/signal.js"; 3import { arrayShuffle } from "~/common/utils.js"; 4import { xxh32 } from "xxh32"; 5 6/** 7 * @import {Actions, Item} from "./types.d.ts" 8 */ 9 10//////////////////////////////////////////// 11// STATE 12//////////////////////////////////////////// 13 14let _key = 0; 15const nextKey = () => String(++_key); 16 17/** Ordered list of available track IDs. */ 18export const $lake = signal(/** @type {string[]} */ ([])); 19 20// Communicated state 21export const $future = signal(/** @type {Item[]} */ ([])); 22export const $now = signal(/** @type {Item | null} */ (null)); 23export const $past = signal(/** @type {Item[]} */ ([])); 24export const $supplyFingerprint = signal( 25 /** @type {string | undefined} */ (undefined), 26); 27 28//////////////////////////////////////////// 29// ACTIONS 30//////////////////////////////////////////// 31 32/** 33 * @type {Actions['add']} 34 * 35 * @example Adds tracks after the last manual entry, before any auto-filled items 36 * ```js 37 * import { add, $future } from "~/components/engine/queue/worker.js"; 38 * 39 * add({ trackIds: ["a", "b"] }); 40 * 41 * if ($future.value.length !== 2) throw new Error("expected 2 items"); 42 * if ($future.value[0].id !== "a") throw new Error("wrong first item"); 43 * if ($future.value[1].id !== "b") throw new Error("wrong second item"); 44 * if (!$future.value[0].manualEntry) throw new Error("items should be manualEntry: true"); 45 * ``` 46 * 47 * @example Inserts before auto-filled items when they are present 48 * ```js 49 * import { add, $future } from "~/components/engine/queue/worker.js"; 50 * 51 * $future.value = [ 52 * { id: "manual", manualEntry: true }, 53 * { id: "auto", manualEntry: false }, 54 * ]; 55 * 56 * add({ trackIds: ["new"] }); 57 * 58 * if ($future.value[0].id !== "manual") throw new Error("expected 'manual' first"); 59 * if ($future.value[1].id !== "new") throw new Error("expected 'new' second"); 60 * if ($future.value[2].id !== "auto") throw new Error("expected 'auto' last"); 61 * ``` 62 * 63 * @example Prepends tracks to the front with inFront: true 64 * ```js 65 * import { add, $future } from "~/components/engine/queue/worker.js"; 66 * 67 * add({ trackIds: ["c"] }); 68 * add({ inFront: true, trackIds: ["a", "b"] }); 69 * 70 * if ($future.value[0].id !== "a") throw new Error("expected 'a' first"); 71 * if ($future.value[1].id !== "b") throw new Error("expected 'b' second"); 72 * if ($future.value[2].id !== "c") throw new Error("expected 'c' last"); 73 * ``` 74 */ 75export function add({ inFront, trackIds }) { 76 const items = trackIds.map((id) => { 77 return { id, key: nextKey(), manualEntry: true }; 78 }); 79 80 if (inFront) { 81 $future.value = [...items, ...$future.value]; 82 } else { 83 let lastManualIdx = -1; 84 for (let i = 0; i < $future.value.length; i++) { 85 if ($future.value[i].manualEntry) lastManualIdx = i; 86 } 87 $future.value = [ 88 ...$future.value.slice(0, lastManualIdx + 1), 89 ...items, 90 ...$future.value.slice(lastManualIdx + 1), 91 ]; 92 } 93} 94 95/** 96 * @type {Actions['clear']} 97 * 98 * @example Keeps manual entries when keepManual is true 99 * ```js 100 * import { clear, $future } from "~/components/engine/queue/worker.js"; 101 * 102 * $future.value = [ 103 * { id: "manual", manualEntry: true }, 104 * { id: "auto", manualEntry: false }, 105 * ]; 106 * clear({ keepManual: true }); 107 * 108 * if ($future.value.length !== 1) throw new Error("expected 1 item remaining"); 109 * if ($future.value[0].id !== "manual") throw new Error("manual entry should remain"); 110 * ``` 111 * 112 * @example Clears all items when keepManual is false 113 * ```js 114 * import { clear, $future } from "~/components/engine/queue/worker.js"; 115 * 116 * $future.value = [ 117 * { id: "manual", manualEntry: true }, 118 * { id: "auto", manualEntry: false }, 119 * ]; 120 * clear({ keepManual: false }); 121 * 122 * if ($future.value.length !== 0) throw new Error("expected empty queue"); 123 * ``` 124 */ 125export function clear({ keepManual }) { 126 $future.value = keepManual 127 ? $future.value.filter((i) => i.manualEntry === true) 128 : []; 129} 130 131/** 132 * @type {Actions['expel']} 133 * 134 * @example Removes an item from the future by key 135 * ```js 136 * import { expel, $future } from "~/components/engine/queue/worker.js"; 137 * 138 * $future.value = [ 139 * { id: "a", key: "1", manualEntry: true }, 140 * { id: "b", key: "2", manualEntry: true }, 141 * ]; 142 * 143 * expel({ key: "1" }); 144 * 145 * if ($future.value.length !== 1) throw new Error("expected 1 item remaining"); 146 * if ($future.value[0].id !== "b") throw new Error("expected 'b' to remain"); 147 * ``` 148 * 149 * @example Removes the now-playing item by key, setting now to null 150 * ```js 151 * import { expel, $now } from "~/components/engine/queue/worker.js"; 152 * 153 * $now.value = { id: "a", key: "1", manualEntry: false }; 154 * 155 * expel({ key: "1" }); 156 * 157 * if ($now.value !== null) throw new Error("expected now to be null"); 158 * ``` 159 * 160 * @example Removes an item from the past by key 161 * ```js 162 * import { expel, $past } from "~/components/engine/queue/worker.js"; 163 * 164 * $past.value = [ 165 * { id: "a", key: "1", manualEntry: false }, 166 * { id: "b", key: "2", manualEntry: false }, 167 * ]; 168 * 169 * expel({ key: "1" }); 170 * 171 * if ($past.value.length !== 1) throw new Error("expected 1 item remaining"); 172 * if ($past.value[0].id !== "b") throw new Error("expected 'b' to remain"); 173 * ``` 174 * 175 * @example Does nothing for an unknown key 176 * ```js 177 * import { expel, $past, $now, $future } from "~/components/engine/queue/worker.js"; 178 * 179 * $past.value = [{ id: "a", key: "1", manualEntry: false }]; 180 * $now.value = { id: "b", key: "2", manualEntry: false }; 181 * $future.value = [{ id: "c", key: "3", manualEntry: false }]; 182 * 183 * expel({ key: "z" }); 184 * 185 * if ($past.value.length !== 1) throw new Error("past should be unchanged"); 186 * if ($now.value?.id !== "b") throw new Error("now should be unchanged"); 187 * if ($future.value.length !== 1) throw new Error("future should be unchanged"); 188 * ``` 189 */ 190export function expel({ key }) { 191 const pastIdx = $past.value.findIndex((i) => i.key === key); 192 if (pastIdx !== -1) { 193 const p = [...$past.value]; 194 p.splice(pastIdx, 1); 195 $past.value = p; 196 return; 197 } 198 if ($now.value?.key === key) { 199 $now.value = null; 200 return; 201 } 202 const futureIdx = $future.value.findIndex((i) => i.key === key); 203 if (futureIdx !== -1) { 204 const f = [...$future.value]; 205 f.splice(futureIdx, 1); 206 $future.value = f; 207 } 208} 209 210/** 211 * @type {Actions['fill']} 212 */ 213export function fill({ augment, amount, shuffled }) { 214 $future.value = fillQueue( 215 shuffled, 216 amount + 217 (augment 218 ? $future.value.filter((i) => i.manualEntry === false).length 219 : 0), 220 $future.value, 221 ); 222} 223 224/** 225 * @type {Actions['move']} 226 * 227 * @example Moves an item forward in the flat list 228 * ```js 229 * import { move, $future } from "~/components/engine/queue/worker.js"; 230 * 231 * $future.value = [ 232 * { id: "a", key: "1", manualEntry: true }, 233 * { id: "b", key: "2", manualEntry: true }, 234 * { id: "c", key: "3", manualEntry: true }, 235 * ]; 236 * 237 * move({ key: "1", to: 2 }); 238 * 239 * if ($future.value[0].id !== "b") throw new Error("expected 'b' first"); 240 * if ($future.value[1].id !== "c") throw new Error("expected 'c' second"); 241 * if ($future.value[2].id !== "a") throw new Error("expected 'a' last"); 242 * ``` 243 * 244 * @example Moves an item backward in the flat list 245 * ```js 246 * import { move, $future } from "~/components/engine/queue/worker.js"; 247 * 248 * $future.value = [ 249 * { id: "a", key: "1", manualEntry: true }, 250 * { id: "b", key: "2", manualEntry: true }, 251 * { id: "c", key: "3", manualEntry: true }, 252 * ]; 253 * 254 * move({ key: "3", to: 0 }); 255 * 256 * if ($future.value[0].id !== "c") throw new Error("expected 'c' first"); 257 * if ($future.value[1].id !== "a") throw new Error("expected 'a' second"); 258 * if ($future.value[2].id !== "b") throw new Error("expected 'b' last"); 259 * ``` 260 * 261 * @example Preserves now identity when reordering across past/future 262 * ```js 263 * import { move, $past, $now, $future } from "~/components/engine/queue/worker.js"; 264 * 265 * $past.value = [{ id: "a", key: "1", manualEntry: false }]; 266 * $now.value = { id: "b", key: "2", manualEntry: false }; 267 * $future.value = [{ id: "c", key: "3", manualEntry: false }]; 268 * 269 * // flat list is [a(0), b(1), c(2)]; moving c to front → [c, a, b] 270 * move({ key: "3", to: 0 }); 271 * 272 * if ($now.value?.id !== "b") throw new Error("now should still be 'b'"); 273 * if ($past.value[0]?.id !== "c") throw new Error("expected 'c' first in past"); 274 * if ($past.value[1]?.id !== "a") throw new Error("expected 'a' second in past"); 275 * if ($future.value.length !== 0) throw new Error("future should be empty"); 276 * ``` 277 * 278 * @example Does nothing when the item is already at the target position 279 * ```js 280 * import { move, $future } from "~/components/engine/queue/worker.js"; 281 * 282 * $future.value = [{ id: "a", key: "1", manualEntry: true }, { id: "b", key: "2", manualEntry: true }]; 283 * 284 * move({ key: "2", to: 1 }); 285 * 286 * if ($future.value[0].id !== "a") throw new Error("order should be unchanged"); 287 * if ($future.value[1].id !== "b") throw new Error("order should be unchanged"); 288 * ``` 289 * 290 * @example Does nothing for out-of-bounds target or unknown key 291 * ```js 292 * import { move, $future } from "~/components/engine/queue/worker.js"; 293 * 294 * $future.value = [{ id: "a", key: "1", manualEntry: true }, { id: "b", key: "2", manualEntry: true }]; 295 * 296 * move({ key: "1", to: 99 }); 297 * move({ key: "z", to: 0 }); 298 * 299 * if ($future.value[0].id !== "a") throw new Error("order should be unchanged"); 300 * if ($future.value[1].id !== "b") throw new Error("order should be unchanged"); 301 * ``` 302 */ 303export function move({ key, to }) { 304 const past = $past.value; 305 const now = $now.value; 306 const future = $future.value; 307 const pLen = past.length; 308 const nLen = now ? 1 : 0; 309 const futureStart = pLen + nLen; 310 const total = futureStart + future.length; 311 312 let from = past.findIndex((i) => i.key === key); 313 if (from === -1 && now?.key === key) from = pLen; 314 if (from === -1) { 315 const fi = future.findIndex((i) => i.key === key); 316 if (fi !== -1) from = futureStart + fi; 317 } 318 319 if (from === -1 || from === to || to < 0 || to >= total) return; 320 321 // Compute now's new flat index after the move 322 let nowIdx = pLen; 323 if (nLen) { 324 if (from === pLen) nowIdx = to; 325 else if (from < to && pLen > from && pLen <= to) nowIdx = pLen - 1; 326 else if (from > to && pLen >= to && pLen < from) nowIdx = pLen + 1; 327 } 328 329 // Map a post-move flat index back to the original flat index 330 const origIdx = (/** @type {number} */ i) => { 331 if (from < to) { 332 if (i < from) return i; 333 if (i < to) return i + 1; 334 if (i === to) return from; 335 } else { 336 if (i < to) return i; 337 if (i === to) return from; 338 if (i <= from) return i - 1; 339 } 340 return i; 341 }; 342 343 const flatGet = (/** @type {number} */ i) => { 344 const j = origIdx(i); 345 return j < pLen ? past[j] : j < futureStart ? now : future[j - futureStart]; 346 }; 347 348 $past.value = 349 /** @type {Item[]} */ (Array.from( 350 { length: nowIdx }, 351 (_, i) => flatGet(i), 352 )); 353 $future.value = 354 /** @type {Item[]} */ (Array.from( 355 { length: total - nowIdx - nLen }, 356 (_, i) => flatGet(nowIdx + nLen + i), 357 )); 358} 359 360/** 361 * @type {Actions['shift']} 362 */ 363export function shift() { 364 return _shift(); 365} 366 367/** 368 * @type {Actions['supply']} 369 * 370 * @example Sets the track pool and computes a fingerprint 371 * ```js 372 * import { supply, $lake, $supplyFingerprint } from "~/components/engine/queue/worker.js"; 373 * 374 * supply({ trackIds: ["a", "b", "c"] }); 375 * 376 * if ($lake.value.join(",") !== "a,b,c") throw new Error("lake not set correctly"); 377 * if (typeof $supplyFingerprint.value !== "string") throw new Error("fingerprint should be a string"); 378 * ``` 379 * 380 * @example Returns undefined fingerprint for an empty supply 381 * ```js 382 * import { supply, $supplyFingerprint } from "~/components/engine/queue/worker.js"; 383 * 384 * supply({ trackIds: [] }); 385 * 386 * if ($supplyFingerprint.value !== undefined) throw new Error("fingerprint should be undefined for empty supply"); 387 * ``` 388 * 389 * @example Same track IDs produce the same fingerprint 390 * ```js 391 * import { supply, $supplyFingerprint } from "~/components/engine/queue/worker.js"; 392 * 393 * supply({ trackIds: ["x", "y"] }); 394 * const first = $supplyFingerprint.value; 395 * 396 * supply({ trackIds: ["x", "y"] }); 397 * const second = $supplyFingerprint.value; 398 * 399 * if (first !== second) throw new Error("same tracks should produce the same fingerprint"); 400 * ``` 401 */ 402export function supply({ trackIds }) { 403 $lake.value = trackIds; 404 $supplyFingerprint.value = trackIds.length 405 ? xxh32(trackIds.join("\0")).toString() 406 : undefined; 407} 408 409/** 410 * @type {Actions['unshift']} 411 * 412 * @example Moves the last past item back to now, pushing now to the front of future 413 * ```js 414 * import { unshift, $future, $now, $past } from "~/components/engine/queue/worker.js"; 415 * 416 * $past.value = [{ id: "prev", manualEntry: false }]; 417 * $now.value = { id: "current", manualEntry: false }; 418 * $future.value = []; 419 * 420 * unshift(); 421 * 422 * if ($now.value?.id !== "prev") throw new Error("expected 'prev' as now"); 423 * if ($past.value.length !== 0) throw new Error("expected empty past"); 424 * if ($future.value[0]?.id !== "current") throw new Error("expected 'current' back at front of future"); 425 * ``` 426 * 427 * @example Does nothing when past is empty 428 * ```js 429 * import { unshift, $now, $past } from "~/components/engine/queue/worker.js"; 430 * 431 * $past.value = []; 432 * $now.value = { id: "current", manualEntry: false }; 433 * 434 * unshift(); 435 * 436 * if ($now.value?.id !== "current") throw new Error("now should remain unchanged"); 437 * ``` 438 */ 439export function unshift() { 440 const p = $past.value; 441 if (p.length === 0) return; 442 443 const n = $now.value; 444 const last = p[p.length - 1]; 445 446 $past.value = p.slice(0, p.length - 1); 447 $now.value = last ?? null; 448 if (n) $future.value = [n, ...$future.value]; 449} 450 451//////////////////////////////////////////// 452// ⚡️ 453//////////////////////////////////////////// 454 455ostiary((context, _firstConnection, _connectionId) => { 456 // Setup RPC 457 458 rpc(context, { 459 add, 460 clear, 461 expel, 462 fill, 463 move, 464 shift, 465 supply, 466 unshift, 467 468 // State 469 future: $future.get, 470 now: $now.get, 471 past: $past.get, 472 supplyFingerprint: $supplyFingerprint.get, 473 }); 474 475 // Effects 476 477 // Communicate state 478 effect(() => announce("future", $future.value, context)); 479 effect(() => announce("now", $now.value, context)); 480 effect(() => announce("past", $past.value, context)); 481 effect(() => 482 announce("supplyFingerprint", $supplyFingerprint.value, context) 483 ); 484}); 485 486//////////////////////////////////////////// 487// ⛔️ 488//////////////////////////////////////////// 489 490/** 491 * Add non-manual items to the queue. 492 * 493 * @param {boolean} shuffled 494 * @param {number | undefined | null} fillAmount 495 * @param {Item[]} future 496 * @returns {Item[]} 497 */ 498function fillQueue(shuffled, fillAmount, future) { 499 if (!fillAmount) return future; 500 501 // Count 502 let autoFutureCount = 0; 503 504 future.forEach((item) => { 505 if (item.manualEntry) {} 506 else autoFutureCount++; 507 }); 508 509 // Fill 510 if (shuffled) { 511 if (autoFutureCount >= fillAmount) return future; 512 return fillShuffle(fillAmount, future, autoFutureCount); 513 } else { 514 return fillSequentially(fillAmount, future); 515 } 516} 517 518/** 519 * @param {number} fillAmount 520 * @param {Item[]} future 521 * @returns {Item[]} 522 * 523 * @example Fills sequentially from the start of the lake 524 * ```js 525 * import { fillSequentially, $lake, $now } from "~/components/engine/queue/worker.js"; 526 * 527 * $lake.value = ["a", "b", "c", "d"]; 528 * $now.value = null; 529 * 530 * const result = fillSequentially(3, []); 531 * 532 * if (result.length !== 3) throw new Error("expected 3 items"); 533 * if (result[0].id !== "a") throw new Error("expected to start from 'a'"); 534 * if (result[1].id !== "b") throw new Error("expected 'b' second"); 535 * if (result[2].id !== "c") throw new Error("expected 'c' third"); 536 * if (result[0].manualEntry !== false) throw new Error("auto items should have manualEntry: false"); 537 * ``` 538 * 539 * @example Continues from after the current now item 540 * ```js 541 * import { fillSequentially, $lake, $now } from "~/components/engine/queue/worker.js"; 542 * 543 * $lake.value = ["a", "b", "c", "d"]; 544 * $now.value = { id: "b", manualEntry: false }; 545 * 546 * const result = fillSequentially(2, []); 547 * 548 * if (result[0].id !== "c") throw new Error("expected to start after now ('c')"); 549 * if (result[1].id !== "d") throw new Error("expected 'd' second"); 550 * ``` 551 * 552 * @example Wraps around to the beginning when reaching the end of the lake 553 * ```js 554 * import { fillSequentially, $lake, $now } from "~/components/engine/queue/worker.js"; 555 * 556 * $lake.value = ["a", "b", "c"]; 557 * $now.value = { id: "b", manualEntry: false }; 558 * 559 * const result = fillSequentially(3, []); 560 * 561 * if (result[0].id !== "c") throw new Error("expected 'c'"); 562 * if (result[1].id !== "a") throw new Error("expected wrap around to 'a'"); 563 * if (result[2].id !== "b") throw new Error("expected 'b'"); 564 * ``` 565 * 566 * @example Preserves existing manual entries 567 * ```js 568 * import { fillSequentially, $lake, $now } from "~/components/engine/queue/worker.js"; 569 * 570 * $lake.value = ["a", "b", "c"]; 571 * $now.value = null; 572 * 573 * const future = [{ id: "manual", manualEntry: true }]; 574 * const result = fillSequentially(2, future); 575 * 576 * if (result[0].id !== "manual") throw new Error("manual entry should be preserved"); 577 * if (result.length !== 3) throw new Error("expected manual + 2 auto items"); 578 * ``` 579 */ 580export function fillSequentially(fillAmount, future) { 581 const onlyManual = future.filter((i) => i.manualEntry); 582 const lastManual = onlyManual.slice(-1)[0]; 583 const startIndex = lastManual 584 ? $lake.value.indexOf(lastManual.id) + 1 585 : $now.value 586 ? $lake.value.indexOf($now.value.id) + 1 587 : 0; 588 589 const maxIndex = $lake.value.length - 1; 590 let currIndex = startIndex; 591 592 /** @type {Item[]} */ 593 const autoItems = []; 594 595 for (let i = 0; i < fillAmount; i++) { 596 if (currIndex > maxIndex) currIndex = 0; 597 const id = $lake.value[currIndex]; 598 if (id) { 599 autoItems.push({ id, key: nextKey(), manualEntry: false }); 600 } 601 currIndex++; 602 } 603 604 return [...onlyManual, ...autoItems]; 605} 606 607/** 608 * @param {number} fillAmount 609 * @param {Item[]} future 610 * @param {number} autoFutureCount 611 * @returns {Item[]} 612 * 613 * @example Adds shuffled items to reach the fill amount 614 * ```js 615 * import { fillShuffle, $lake, $past } from "~/components/engine/queue/worker.js"; 616 * 617 * $lake.value = ["a", "b", "c", "d", "e"]; 618 * $past.value = []; 619 * 620 * const result = fillShuffle(3, [], 0); 621 * 622 * if (result.length !== 3) throw new Error("expected 3 items"); 623 * if (!result.every((i) => i.manualEntry === false)) throw new Error("all items should be auto"); 624 * ``` 625 * 626 * @example Only adds enough to reach the fill amount given existing auto items 627 * ```js 628 * import { fillShuffle, $lake, $past } from "~/components/engine/queue/worker.js"; 629 * 630 * $lake.value = ["a", "b", "c", "d"]; 631 * $past.value = []; 632 * 633 * const existing = [ 634 * { id: "x", manualEntry: false }, 635 * { id: "y", manualEntry: false }, 636 * ]; 637 * 638 * const result = fillShuffle(4, existing, 2); 639 * 640 * if (result.length !== 4) throw new Error("expected 4 total items (2 existing + 2 new)"); 641 * ``` 642 * 643 * @example Does not add tracks that have already played or are now playing 644 * ```js 645 * import { fillShuffle, $lake, $past, $now } from "~/components/engine/queue/worker.js"; 646 * 647 * $lake.value = ["a", "b", "c", "d"]; 648 * $past.value = [{ id: "a", manualEntry: false }]; 649 * $now.value = { id: "b", manualEntry: false }; 650 * 651 * const result = fillShuffle(4, [], 0); 652 * 653 * if (result.some((i) => i.id === "a" || i.id === "b")) throw new Error("past and now tracks should be excluded"); 654 * ``` 655 * 656 * @example Falls back to full lake when everything has been played 657 * ```js 658 * import { fillShuffle, $lake, $past } from "~/components/engine/queue/worker.js"; 659 * 660 * $lake.value = ["a", "b"]; 661 * $past.value = [{ id: "a", manualEntry: false }, { id: "b", manualEntry: false }]; 662 * 663 * const result = fillShuffle(2, [], 0); 664 * 665 * if (result.length !== 2) throw new Error("expected 2 items from full lake fallback"); 666 * ``` 667 */ 668export function fillShuffle(fillAmount, future, autoFutureCount) { 669 const excludeIds = new Set($past.value.map((i) => i.id)); 670 if ($now.value) excludeIds.add($now.value.id); 671 future.forEach((i) => excludeIds.add(i.id)); 672 673 let pool = $lake.value.filter((id) => !excludeIds.has(id)); 674 675 // Fallback: if everything has been played/is playing/is queued, use tracks not in past or now 676 if (pool.length === 0) { 677 const pastAndNowIds = new Set($past.value.map((i) => i.id)); 678 if ($now.value) pastAndNowIds.add($now.value.id); 679 pool = $lake.value.filter((id) => !pastAndNowIds.has(id)); 680 } 681 682 // Final fallback: everything has been played, use the full lake 683 if (pool.length === 0) { 684 pool = [...$lake.value]; 685 } 686 687 const selected = arrayShuffle(pool).slice( 688 0, 689 Math.max(0, fillAmount - autoFutureCount), 690 ); 691 692 return [ 693 ...future, 694 ...selected.map((id) => ({ id, key: nextKey(), manualEntry: false })), 695 ]; 696} 697 698/** 699 * @param {Item[]} [future] 700 * 701 * @example Moves the first future item to now 702 * ```ts 703 * import { _shift, $future, $now } from "~/components/engine/queue/worker.js"; 704 * import type { Item } from "./types.d.ts" 705 * 706 * $now.value = null as null | Item; 707 * $future.value = [{ id: "a", manualEntry: false }, { id: "b", manualEntry: false }]; 708 * 709 * _shift(); 710 * 711 * if ($now.value?.id !== "a") throw new Error("expected 'a' as now"); 712 * if ($future.value.length !== 1) throw new Error("expected 1 item remaining in future"); 713 * if ($future.value[0].id !== "b") throw new Error("expected 'b' remaining in future"); 714 * ``` 715 * 716 * @example Moves previous now to past 717 * ```js 718 * import { _shift, $future, $now, $past } from "~/components/engine/queue/worker.js"; 719 * 720 * $past.value = []; 721 * $now.value = { id: "prev", manualEntry: false }; 722 * $future.value = [{ id: "next", manualEntry: false }]; 723 * 724 * _shift(); 725 * 726 * if ($now.value?.id !== "next") throw new Error("expected 'next' as now"); 727 * if ($past.value.length !== 1) throw new Error("expected 1 past item"); 728 * if ($past.value[0].id !== "prev") throw new Error("expected 'prev' in past"); 729 * ``` 730 * 731 * @example Does nothing when future is empty 732 * ```js 733 * import { _shift, $future, $now } from "~/components/engine/queue/worker.js"; 734 * 735 * $future.value = []; 736 * $now.value = null; 737 * 738 * _shift(); 739 * 740 * if ($now.value !== null) throw new Error("now should remain null"); 741 * ``` 742 */ 743export function _shift(future) { 744 const n = $now.value; 745 const f = future ?? $future.value; 746 const v = f[0]; 747 748 if (!v) return; 749 $now.value = v; 750 if (n) $past.value = [...$past.value, n]; 751 $future.value = f.slice(1); 752}