forked from Keelrleek/SoulStream
Phase 2A+2B: native preview backend and renderer glue
Rust (2A):
- metadata: deezer_track_uncached (fresh mint, cache-updating, paced) and
fetch_preview_media — dedicated client whose custom redirect policy
re-validates every hop against the media allowlist; bounded body read
(cap enforced on received bytes, not Content-Length's promise); Date
header captured for provider-clock lease math. httpdate dep added.
- src-tauri/preview.rs: second Player instance + soulstream:preview event
channel; preview_resolve (fresh mint → host gate → MIME gate → ID3v2
shell strip → MPEG sync check → clip file + opaque lease id → safe_for_ms
mirroring lease.js); preview_load with start_paused via the
desired-paused latch; preview_control/dispose; player_pause_acked
exposing the Phase 0.4 ack on the MAIN player. 9 unit tests.
JS (2B):
- api.js: preview command surface (opaque lease ids only).
- nativeTransport.js: stamps {sessionId, generation, sessionKey} onto
backend events; cleared context makes stragglers dispatch nothing.
- productionResolver.js: DTO → MediaLease, failure classes preserved.
- focusBridge.js: production coordinator binding with the documented
renderer-side generation invariant (buffering counter + gateway).
- service.js: the orchestrator — settle window, resolve→load→play under
focus, single capped retry, one forward lease prefetch with safe-window
expiry, stale re-checks before every transport command, close/handoff.
The service constructs its transport around its own dispatch — the
factory signature exists because a pre-built transport silently loses
the event wiring (caught by test).
70 JS tests passing (npm test); metadata 46 + app preview 9 in cargo.
This commit is contained in:
1 parent
8ede6a8adf
commit
8cb3cd593b
11 files changed
+1536
No files matched your search
@@ -0,0 +1,302 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { AVAILABILITY } from "../../src/lib/preview/contracts.js";
|
||||
import { buildFixtureManifest } from "../../src/lib/preview/manifest.js";
|
||||
import { createFixtureResolver } from "../../src/lib/preview/resolver.js";
|
||||
import { createPreviewService } from "../../src/lib/preview/service.js";
|
||||
import { PHASE } from "../../src/lib/preview/session.js";
|
||||
|
||||
const T0 = 1_700_000_000_000;
|
||||
const SID = "svc-1";
|
||||
|
||||
function manifest(n = 3) {
|
||||
return buildFixtureManifest({
|
||||
rows: Array.from({ length: n }, (_, i) => ({
|
||||
providerTrackId: String(i + 1),
|
||||
title: `T${i + 1}`,
|
||||
initialAvailability: AVAILABILITY.AVAILABLE,
|
||||
})),
|
||||
createdAtMs: T0,
|
||||
});
|
||||
}
|
||||
|
||||
/** Manual scheduler: settle windows fire only when the test says so. */
|
||||
function makeScheduler() {
|
||||
let queue = new Map();
|
||||
let nextId = 1;
|
||||
return {
|
||||
set(fn, ms) {
|
||||
const id = nextId++;
|
||||
queue.set(id, fn);
|
||||
return id;
|
||||
},
|
||||
clear(id) {
|
||||
queue.delete(id);
|
||||
},
|
||||
async flush() {
|
||||
const fns = [...queue.values()];
|
||||
queue.clear();
|
||||
for (const fn of fns) await fn();
|
||||
await drainMicrotasks();
|
||||
},
|
||||
pending: () => queue.size,
|
||||
};
|
||||
}
|
||||
const drainMicrotasks = () => new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
/** Transport double: records every command AND behaves like the real one —
|
||||
* play/pause dispatch their media events stamped with the load's context,
|
||||
* which is exactly the wiring the service's transportFactory provides. */
|
||||
function makeTransport() {
|
||||
const log = [];
|
||||
let dispatch = null;
|
||||
let ctx = null;
|
||||
const t = {
|
||||
log,
|
||||
async load(lease, c, opts) {
|
||||
ctx = c;
|
||||
log.push(["load", lease.opaqueLeaseId, c.generation, !!opts?.startPaused]);
|
||||
},
|
||||
async play() {
|
||||
log.push(["play"]);
|
||||
if (ctx) dispatch({ type: "MEDIA_PLAYING", ...ctx });
|
||||
},
|
||||
async pause() {
|
||||
log.push(["pause"]);
|
||||
if (ctx) dispatch({ type: "MEDIA_PAUSED", ...ctx });
|
||||
},
|
||||
async seek(ms) {
|
||||
log.push(["seek", ms]);
|
||||
},
|
||||
async stop() {
|
||||
ctx = null;
|
||||
log.push(["stop"]);
|
||||
},
|
||||
async dispose() {
|
||||
log.push(["dispose"]);
|
||||
},
|
||||
end() {
|
||||
if (ctx) dispatch({ type: "MEDIA_ENDED", ...ctx });
|
||||
},
|
||||
};
|
||||
return {
|
||||
transport: t,
|
||||
factory: (d) => {
|
||||
dispatch = d;
|
||||
return t;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeFocus({ grant = true, pausedByPreview = true } = {}) {
|
||||
const calls = { acquire: 0, release: 0, handoff: 0 };
|
||||
let live = null;
|
||||
return {
|
||||
calls,
|
||||
released: [],
|
||||
coordinator: {
|
||||
async acquirePreview() {
|
||||
calls.acquire += 1;
|
||||
if (!grant) return { granted: false, token: null };
|
||||
live = { id: `t${calls.acquire}`, pausedByPreview };
|
||||
return { granted: true, token: live };
|
||||
},
|
||||
release(token) {
|
||||
calls.release += 1;
|
||||
const ok = token === live;
|
||||
live = null;
|
||||
return { restored: ok && pausedByPreview };
|
||||
},
|
||||
handoffToMain() {
|
||||
calls.handoff += 1;
|
||||
live = null;
|
||||
return { restored: false, reason: "handoff" };
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeService({ plan, grant = true, n = 3 } = {}) {
|
||||
const scheduler = makeScheduler();
|
||||
const { transport, factory } = makeTransport();
|
||||
const focus = makeFocus({ grant });
|
||||
let clock = 0;
|
||||
const resolver = createFixtureResolver(
|
||||
plan ?? { 1: { opaqueLeaseId: "op-1" }, 2: { opaqueLeaseId: "op-2" }, 3: { opaqueLeaseId: "op-3" } }
|
||||
);
|
||||
const service = createPreviewService({
|
||||
resolver,
|
||||
transportFactory: factory,
|
||||
focus,
|
||||
scheduler,
|
||||
monotonicNow: () => clock,
|
||||
});
|
||||
return { service, scheduler, transport, focus, resolver, advance: (ms) => (clock += ms), manifest: manifest(n) };
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ open --
|
||||
|
||||
test("open: focus, settle, resolve, load audible, play", async () => {
|
||||
const { service, scheduler, transport, focus, manifest } = makeService();
|
||||
await service.open({ sessionId: SID, manifest });
|
||||
assert.equal(focus.calls.acquire, 1);
|
||||
assert.equal(service.getState().phase, PHASE.RESOLVING_MEDIA);
|
||||
assert.equal(scheduler.pending(), 1); // settle armed
|
||||
|
||||
await scheduler.flush();
|
||||
const st = service.getState();
|
||||
assert.equal(st.phase, PHASE.PLAYING); // lease landed, played, confirmed
|
||||
const load = transport.log.find((e) => e[0] === "load");
|
||||
assert.deepEqual(load, ["load", "op-1", st.requestGeneration, false]); // audible → not parked
|
||||
assert.ok(transport.log.some((e) => e[0] === "play"));
|
||||
});
|
||||
|
||||
test("open with focus denied: session opens PARKED, wait state surfaced", async () => {
|
||||
const { service, scheduler, transport, manifest } = makeService({ grant: false });
|
||||
await service.open({ sessionId: SID, manifest });
|
||||
assert.equal(service.getState().desiredPlaying, false);
|
||||
await scheduler.flush();
|
||||
const load = transport.log.find((e) => e[0] === "load");
|
||||
assert.equal(load[3], true, "must load parked without focus");
|
||||
assert.ok(!transport.log.some((e) => e[0] === "play"), "no audio without focus");
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------- stepping ---
|
||||
|
||||
test("rapid steps: only the LAST generation ever reaches the transport", async () => {
|
||||
const { service, scheduler, transport, manifest } = makeService();
|
||||
await service.open({ sessionId: SID, manifest });
|
||||
service.step(1); // supersedes the open's pipeline before its settle fires
|
||||
service.step(1);
|
||||
const finalGen = service.getState().requestGeneration;
|
||||
await scheduler.flush();
|
||||
await scheduler.flush();
|
||||
const loads = transport.log.filter((e) => e[0] === "load");
|
||||
assert.equal(loads.length, 1, "superseded generations never loaded");
|
||||
assert.equal(loads[0][2], finalGen);
|
||||
assert.equal(loads[0][1], "op-3"); // stepped 1 → 2 → 3
|
||||
});
|
||||
|
||||
test("failed resolve retries exactly once, then fails the item in place", async () => {
|
||||
const { service, scheduler, resolver, manifest } = makeService({
|
||||
plan: {
|
||||
1: (n) => (n === 1 ? { failure: "stale_lease", retryable: true } : { opaqueLeaseId: "op-fresh" }),
|
||||
2: { failure: "network", retryable: true }, // fails on retry too
|
||||
3: { opaqueLeaseId: "op-3" },
|
||||
},
|
||||
});
|
||||
await service.open({ sessionId: SID, manifest });
|
||||
await scheduler.flush();
|
||||
await drainMicrotasks(); // background neighbor prefetch (item 2) settles
|
||||
assert.equal(service.getState().phase, PHASE.PLAYING); // retry rescued item 1
|
||||
assert.equal(resolver.callCount("1"), 2);
|
||||
assert.equal(resolver.callCount("2"), 1, "prefetch tried item 2 once — prefetch never retries");
|
||||
|
||||
service.step(1);
|
||||
await scheduler.flush();
|
||||
const st = service.getState();
|
||||
assert.equal(st.phase, PHASE.ITEM_FAILED);
|
||||
assert.equal(st.errorClass, "network");
|
||||
// 1 prefetch + 1 pipeline resolve + exactly 1 capped retry = 3, then stop
|
||||
assert.equal(resolver.callCount("2"), 3, "one capped retry in the pipeline, never a loop");
|
||||
// the failed card stays; stepping onward still works
|
||||
service.step(1);
|
||||
await scheduler.flush();
|
||||
assert.equal(service.getState().phase, PHASE.PLAYING);
|
||||
assert.equal(resolver.callCount("2"), 3, "failure is settled — no more calls");
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------- prefetch ---
|
||||
|
||||
test("neighbor lease prefetch: stepping forward skips the resolve", async () => {
|
||||
const { service, scheduler, resolver, manifest } = makeService();
|
||||
await service.open({ sessionId: SID, manifest });
|
||||
await scheduler.flush(); // pipeline + prefetch of item 2 kicked
|
||||
await drainMicrotasks();
|
||||
assert.equal(resolver.callCount("2"), 1, "prefetched in the background");
|
||||
service.step(1);
|
||||
await scheduler.flush();
|
||||
assert.equal(resolver.callCount("2"), 1, "no second resolve on activation");
|
||||
assert.equal(service.getState().phase, PHASE.PLAYING);
|
||||
});
|
||||
|
||||
test("a prefetched lease past 80% of its safe window is not trusted", async () => {
|
||||
const { service, scheduler, resolver, advance, manifest } = makeService({
|
||||
plan: {
|
||||
1: { opaqueLeaseId: "op-1", safeForMs: 10_000 },
|
||||
2: { opaqueLeaseId: "op-2", safeForMs: 10_000 },
|
||||
3: { opaqueLeaseId: "op-3" },
|
||||
},
|
||||
});
|
||||
await service.open({ sessionId: SID, manifest });
|
||||
await scheduler.flush();
|
||||
await drainMicrotasks();
|
||||
advance(9_000); // beyond 80% of the 10s safe window
|
||||
service.step(1);
|
||||
await scheduler.flush();
|
||||
assert.equal(resolver.callCount("2"), 2, "stale prefetch re-resolved");
|
||||
});
|
||||
|
||||
// ------------------------------------------------------ close/handoff -----
|
||||
|
||||
test("close during resolve: terminal, transport torn down, focus released once", async () => {
|
||||
const { service, scheduler, transport, focus, manifest } = makeService();
|
||||
await service.open({ sessionId: SID, manifest });
|
||||
// close BEFORE the settle window fires — mid-"resolving" phase
|
||||
const out = await service.close();
|
||||
assert.equal(service.getState().phase, PHASE.CLOSING);
|
||||
assert.equal(scheduler.pending(), 0, "settle timer cancelled");
|
||||
assert.ok(transport.log.some((e) => e[0] === "dispose"));
|
||||
assert.equal(focus.calls.release, 1);
|
||||
assert.ok(out.restored);
|
||||
// straggler flush does nothing
|
||||
await scheduler.flush();
|
||||
assert.ok(!transport.log.some((e) => e[0] === "load"));
|
||||
});
|
||||
|
||||
test("play-full handoff: transport stopped, no restoration, main starts with the active item", async () => {
|
||||
const { service, scheduler, transport, focus, manifest } = makeService();
|
||||
await service.open({ sessionId: SID, manifest });
|
||||
await scheduler.flush();
|
||||
let started = null;
|
||||
await service.playFullTrack(async (item) => {
|
||||
started = item;
|
||||
});
|
||||
assert.equal(focus.calls.handoff, 1);
|
||||
assert.equal(started.title, "T1");
|
||||
assert.ok(transport.log.some((e) => e[0] === "stop"));
|
||||
// closing afterwards must NOT restore the pre-preview main track
|
||||
const out = await service.close();
|
||||
assert.equal(out.restored, false);
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------- controls --
|
||||
|
||||
test("togglePlay: pause from playing, resume from paused, replay from ended", async () => {
|
||||
const { service, scheduler, transport, manifest } = makeService();
|
||||
await service.open({ sessionId: SID, manifest });
|
||||
await scheduler.flush();
|
||||
assert.equal(service.getState().phase, PHASE.PLAYING);
|
||||
|
||||
await service.togglePlay();
|
||||
assert.equal(service.getState().phase, PHASE.PAUSED);
|
||||
assert.equal(service.getState().desiredPlaying, false);
|
||||
assert.ok(transport.log.some((e) => e[0] === "pause"));
|
||||
|
||||
await service.togglePlay();
|
||||
assert.equal(service.getState().phase, PHASE.PLAYING);
|
||||
assert.equal(service.getState().desiredPlaying, true);
|
||||
|
||||
// ride to the LAST playable item, end its clip, and replay from ENDED
|
||||
service.step(1);
|
||||
await scheduler.flush();
|
||||
service.step(1);
|
||||
await scheduler.flush();
|
||||
assert.equal(service.getState().phase, PHASE.PLAYING);
|
||||
transport.end();
|
||||
assert.equal(service.getState().phase, PHASE.ENDED);
|
||||
await service.togglePlay(); // replay
|
||||
assert.equal(service.getState().phase, PHASE.RESOLVING_MEDIA);
|
||||
assert.equal(service.getState().desiredPlaying, true);
|
||||
});
|
||||
Reference in new issue
Block a user