feat(load): phase-1 preload gate + eligible-pick pool growth

Gate the universe on one clip per altitude + connecting morphs (~126 MB, ~10-20s),
then grow the pool in the background. The random pick (pickPoolClip/pickRandomMember)
is restricted to fully-loaded clips — base + connecting morphs both ways cached
(HEFPreload.eligibleDestinations) — so a transition never stalls on un-downloaded
media and new clips become reachable only once fully loaded. Pure planner in
preload.js (+ node tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BenStullsBets
2026-06-30 09:55:46 -07:00
parent 380c2d13e7
commit 1a6ffc74a1
5 changed files with 163 additions and 18 deletions
+53
View File
@@ -0,0 +1,53 @@
"use strict";
const test = require("node:test");
const assert = require("node:assert/strict");
const P = require("../static/preload.js");
// Tiny fixture: 3-altitude cyclic ring, 2 pool members each. base_file = "<id>.b";
// morph file = "<from>>-<to>" when it exists.
const scales = [
{ id: "a", pool: [{ clip_id: "a1" }, { clip_id: "a2" }] },
{ id: "b", pool: [{ clip_id: "b1" }, { clip_id: "b2" }] },
{ id: "c", pool: [{ clip_id: "c1" }, { clip_id: "c2" }] },
];
const baseOf = (id) => id + ".b";
const morphFile = (a, b) => `${a}>-${b}`; // pretend every pair has a morph
const cached = new Set();
const deps = () => ({ baseOf, morphFile, isCached: (f) => cached.has(f) });
test("phase1Files = first member of each altitude + connecting morphs (both dirs, cyclic)", () => {
const files = P.phase1Files(scales, deps());
// bases: a1.b b1.b c1.b
for (const b of ["a1.b", "b1.b", "c1.b"]) assert.ok(files.includes(b), `missing base ${b}`);
assert.ok(!files.includes("a2.b"), "second members must NOT be in phase 1");
// connecting morphs between adjacent first-members, both directions, cyclic (a-b, b-c, c-a)
for (const m of ["a1>-b1", "b1>-a1", "b1>-c1", "c1>-b1", "c1>-a1", "a1>-c1"])
assert.ok(files.includes(m), `missing morph ${m}`);
assert.equal(files.length, 3 + 6);
});
test("eligibleDestinations requires base + BOTH morphs cached", () => {
cached.clear();
// from a1, candidate b1: nothing cached → not eligible
assert.deepEqual(P.eligibleDestinations(["b1", "b2"], "a1", deps()), []);
cached.add("b1.b"); cached.add("a1>-b1"); // base + forward only
assert.deepEqual(P.eligibleDestinations(["b1", "b2"], "a1", deps()), [], "reverse morph still missing");
cached.add("b1>-a1"); // now reverse too
assert.deepEqual(P.eligibleDestinations(["b1", "b2"], "a1", deps()), ["b1"]);
// b2 fully cached too → both eligible
cached.add("b2.b"); cached.add("a1>-b2"); cached.add("b2>-a1");
assert.deepEqual(P.eligibleDestinations(["b1", "b2"], "a1", deps()).sort(), ["b1", "b2"]);
});
test("eligibleMembers requires only the base cached", () => {
cached.clear();
assert.deepEqual(P.eligibleMembers(["a1", "a2"], deps()), []);
cached.add("a1.b");
assert.deepEqual(P.eligibleMembers(["a1", "a2"], deps()), ["a1"]);
});
test("pick returns null on empty, a member otherwise", () => {
assert.equal(P.pick([], Math.random), null);
assert.equal(P.pick(["x"], () => 0), "x");
assert.equal(P.pick(["x", "y", "z"], () => 0.99), "z");
});