forked from Keelrleek/SoulStream
Phase 1: preview contracts and pure session core, 61 tests
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.
This commit is contained in:
1 parent
6dcc62a2fd
commit
8ede6a8adf
14 files changed
+1722
-2
No files matched your search
@@ -0,0 +1,116 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in new issue
Block a user