forked from Keelrleek/SoulStream
src/lib/preview/ — framework-free ESM modules, per design §12–14: - contracts.js: versioned manifest/item/source-ref factories; signed URLs structurally excluded from manifests; Apple identity first-class. - lease.js: discriminated MediaLease union; computeSafeForMs derives lifetimes provider-clock-first with fallback/contradiction handling (margins are ADRs backed by Phase 0 soak evidence). - redact.js: URL redaction (host+hash+exp only) and the HTTPS + exact host allowlist gate applied to initial URLs and every redirect. - manifest.js: artist (rank order, exact-id dedupe), album (disc/track sort, context inheritance) and fixture builders — pure, no fetching. - session.js: reducer with session/generation guards on every async action; keyed active item with derived playable indices/cursor; auto-advance under fresh generations; close is terminal. - resolver.js: PreviewResolver contract + scripted fixture resolver with classified failures (stale_lease/not_found/network/…). - transport.js: transport seam + deterministic fake (manual ticks). - focus.js: revision gateway (all main-player entry points; focus- internal tag) and the coordinator — acquisition only via a generation-bound silence ack, restore only with revisions unchanged. tests/preview/*.test.mjs run via `npm test` (node --test): 61 passing, covering the full Phase 1 list in IMPLEMENTATION-PLAN §6 including the exit gate: stale lease/play events are inert, no restoration after any simulated main command, and no component renders anywhere.
117 lines
4.6 KiB
JavaScript
117 lines
4.6 KiB
JavaScript
// Ephemeral media leases — the discriminated union from design §12.
|
|
//
|
|
// A lease is the resolver's answer to "how do I play this item RIGHT NOW":
|
|
// either a transport-private URL (webview) or an opaque native handle, plus
|
|
// a conservative `safeForMs` after which the transport must re-resolve.
|
|
// Leases are never persisted, never used as cache identity, and never logged
|
|
// in full (see redact.js).
|
|
|
|
/**
|
|
* Safety margin subtracted from every derived lifetime. ADR (Phase 0): leases
|
|
* hard-403 after their signed exp with NO grace observed (206 at +14 min
|
|
* against a +14.97 exp, 403 by +17.5). 20 s covers request latency, decoder
|
|
* spin-up and one settle-window retry without meaningfully shortening a
|
|
* 13-minute lease.
|
|
*/
|
|
export const LEASE_SAFETY_MARGIN_MS = 20_000;
|
|
|
|
/**
|
|
* Fallback lifetime when provider time data is missing, malformed or
|
|
* contradictory. Deliberately short: Phase 0 never observed a lease younger
|
|
* than ~13 min, so 60 s is far inside any real lifetime while still letting
|
|
* a clip play out (clips are 30 s) before a forced refresh.
|
|
*/
|
|
export const LEASE_FALLBACK_SAFE_MS = 60_000;
|
|
|
|
/** Longest lifetime we will ever trust, however healthy the inputs look —
|
|
* a parse artifact claiming hours must not disable refresh. */
|
|
export const LEASE_MAX_SAFE_MS = 30 * 60_000;
|
|
|
|
/**
|
|
* Derive `safeForMs` from provider time data, per design §12:
|
|
* conservative, provider-clock based, never trusting any single input.
|
|
*
|
|
* @param {object} t
|
|
* @param {number|null} t.expEpochMs parsed signed expiry (provider clock)
|
|
* @param {number|null} t.dateHeaderMs provider `Date` header (provider clock)
|
|
* @param {number} t.localNowMs local wall clock at receipt
|
|
* @returns {number} milliseconds the lease may be used, >= 0
|
|
*/
|
|
export function computeSafeForMs({ expEpochMs, dateHeaderMs, localNowMs }) {
|
|
const bad =
|
|
!Number.isFinite(expEpochMs) ||
|
|
!Number.isFinite(localNowMs) ||
|
|
expEpochMs <= 0;
|
|
if (bad) return LEASE_FALLBACK_SAFE_MS;
|
|
|
|
// Remaining lifetime on the provider's own clock when both ends of it
|
|
// are known — immune to local clock skew entirely.
|
|
let remaining = null;
|
|
if (Number.isFinite(dateHeaderMs) && dateHeaderMs > 0) {
|
|
remaining = expEpochMs - dateHeaderMs;
|
|
// Contradictory provider data (exp at/before Date, or a lifetime far
|
|
// beyond anything the provider actually issues) → fallback.
|
|
if (remaining <= 0 || remaining > 2 * LEASE_MAX_SAFE_MS) {
|
|
return LEASE_FALLBACK_SAFE_MS;
|
|
}
|
|
}
|
|
|
|
// Local-clock view. If the local clock is skewed AHEAD of the provider,
|
|
// this is smaller than `remaining` and correctly wins (we refresh early).
|
|
// If skewed BEHIND, `remaining` wins and the local view can't extend it.
|
|
const localView = expEpochMs - localNowMs;
|
|
if (localView <= 0 && remaining === null) return LEASE_FALLBACK_SAFE_MS;
|
|
|
|
const usable =
|
|
remaining === null ? localView : Math.min(remaining, localView > 0 ? localView : remaining);
|
|
const safe = Math.min(usable, LEASE_MAX_SAFE_MS) - LEASE_SAFETY_MARGIN_MS;
|
|
return Math.max(0, safe);
|
|
}
|
|
|
|
/** Build a native-transport lease (the ratified transport). */
|
|
export function makeNativeLease({ source, opaqueLeaseId, safeForMs, mimeType, clipDurationMs, byteLength, supportsRanges }) {
|
|
if (typeof opaqueLeaseId !== "string" || !opaqueLeaseId) {
|
|
throw new Error("native lease requires opaqueLeaseId");
|
|
}
|
|
return Object.freeze({
|
|
transport: "native",
|
|
source,
|
|
opaqueLeaseId,
|
|
safeForMs,
|
|
mimeType,
|
|
clipDurationMs,
|
|
byteLength,
|
|
supportsRanges,
|
|
});
|
|
}
|
|
|
|
/** Build a webview-transport lease (documented fallback transport). */
|
|
export function makeWebviewLease({ source, url, safeForMs, mimeType, clipDurationMs, byteLength, supportsRanges }) {
|
|
if (typeof url !== "string" || !url) {
|
|
throw new Error("webview lease requires url");
|
|
}
|
|
return Object.freeze({
|
|
transport: "webview",
|
|
source,
|
|
url,
|
|
safeForMs,
|
|
mimeType,
|
|
clipDurationMs,
|
|
byteLength,
|
|
supportsRanges,
|
|
});
|
|
}
|
|
|
|
/** Union validity: exactly one locator, matching the transport tag. */
|
|
export function isValidLease(lease) {
|
|
if (!lease || typeof lease !== "object") return false;
|
|
if (!Number.isFinite(lease.safeForMs) || lease.safeForMs < 0) return false;
|
|
if (lease.transport === "native") {
|
|
return typeof lease.opaqueLeaseId === "string" && lease.opaqueLeaseId !== "" && lease.url === undefined;
|
|
}
|
|
if (lease.transport === "webview") {
|
|
return typeof lease.url === "string" && lease.url !== "" && lease.opaqueLeaseId === undefined;
|
|
}
|
|
return false;
|
|
}
|