holo-evicted-publish: player +2 object(s)
Browse files
b/2402cb6992ea83c6853b7ed37f2bc17101ca1c6923d1a0b975a3c8fcabe0e8f4
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
b/9ce7bf36b3de9318775510922233e762acacd3c259c630bd0c6b9d9d9369477f
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// holo-library.mjs β THE PLANE: the ΞΊ-addressed universe catalog at runtime (HOLO-TV-LIBRARY-PROMPT.md L2).
|
| 2 |
+
//
|
| 3 |
+
// A lean resolver over the forge's shards: the tiny root (feed/library-index.json) names every shard by
|
| 4 |
+
// TWO hashes β blake3 (the ΞΊ-mirror path) and sha256 (the verify key). Reads are verify-or-refuse (Law
|
| 5 |
+
// L5): bytes are cached raw in CacheStorage and RE-HASHED on every read; a tampered shard is refused by
|
| 6 |
+
// name, never parsed. Once cached, the whole universe browses OFFLINE and every facet pivot is a plain
|
| 7 |
+
// in-memory scan over compact rows β ~19k titles filter in well under a frame, no bitset machinery needed.
|
| 8 |
+
//
|
| 9 |
+
// Dependency-injected (fetch + cache + sha256) so Node witnesses it with fake shards β no network.
|
| 10 |
+
// Browser binding: window.HoloLibrary.live() β { load, query, search, person, personsIndex, rows, root }.
|
| 11 |
+
|
| 12 |
+
// createLibrary({ root, fetch, cacheGet, cachePut, sha256hex })
|
| 13 |
+
// root : the parsed library-index.json (or null β loadRoot() fetches it)
|
| 14 |
+
// cacheGet : async (sha) β Uint8Array | null cachePut: async (sha, bytes) β void
|
| 15 |
+
// sha256hex : async (Uint8Array) β hex
|
| 16 |
+
export function createLibrary({ root = null, fetch: f, cacheGet, cachePut, sha256hex, rootUrl = "feed/library-index.json" } = {}) {
|
| 17 |
+
const doFetch = f || (typeof fetch !== "undefined" ? fetch.bind(globalThis) : null);
|
| 18 |
+
if (!doFetch) throw new Error("holo-library: fetch required");
|
| 19 |
+
let R = root, rows = [], byKey = new Map(), loaded = false, personsCache = null, searchIdx = null;
|
| 20 |
+
const stats = { shardsFromCache: 0, shardsFromNet: 0, refused: 0 };
|
| 21 |
+
|
| 22 |
+
async function loadRoot() {
|
| 23 |
+
if (R) return R;
|
| 24 |
+
try { R = await (await doFetch(rootUrl + "?v=" + Date.now(), { cache: "no-store" })).json(); }
|
| 25 |
+
catch { R = await (await doFetch(rootUrl)).json(); } // offline: SW/HTTP cache still answers
|
| 26 |
+
return R;
|
| 27 |
+
}
|
| 28 |
+
// verify-or-refuse read of ONE named object { sha256, blake3 }
|
| 29 |
+
async function readObject(ref, name) {
|
| 30 |
+
if (cacheGet) {
|
| 31 |
+
const c = await cacheGet(ref.sha256);
|
| 32 |
+
if (c) {
|
| 33 |
+
if ((await sha256hex(c)) === ref.sha256) { stats.shardsFromCache++; return c; }
|
| 34 |
+
stats.refused++; console.warn("holo-library: REFUSED tampered cache object", name, ref.sha256.slice(0, 12));
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
const res = await doFetch(R.mirror + ref.blake3);
|
| 38 |
+
if (!res.ok) throw new Error("holo-library: mirror " + res.status + " for " + name);
|
| 39 |
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
| 40 |
+
if ((await sha256hex(bytes)) !== ref.sha256) { stats.refused++; throw new Error("holo-library: REFUSED " + name + " β bytes do not re-derive"); }
|
| 41 |
+
stats.shardsFromNet++;
|
| 42 |
+
if (cachePut) await cachePut(ref.sha256, bytes);
|
| 43 |
+
return bytes;
|
| 44 |
+
}
|
| 45 |
+
const dec = (u8) => JSON.parse(new TextDecoder().decode(u8));
|
| 46 |
+
|
| 47 |
+
// load(onShard) β pull all shards (parallel), verify each, build the in-memory plane.
|
| 48 |
+
async function load(onShard = null) {
|
| 49 |
+
if (loaded) return rows;
|
| 50 |
+
await loadRoot();
|
| 51 |
+
const parts = await Promise.all(R.shards.map(async (ref, n) => {
|
| 52 |
+
const s = dec(await readObject(ref, "shard" + n));
|
| 53 |
+
if (onShard) try { onShard(n, s.rows.length); } catch {}
|
| 54 |
+
return s.rows;
|
| 55 |
+
}));
|
| 56 |
+
rows = parts.flat();
|
| 57 |
+
byKey = new Map(rows.map((r) => [r.k + r.i, r]));
|
| 58 |
+
loaded = true;
|
| 59 |
+
return rows;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// ββ facet algebra β one scan, every dimension composable ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 63 |
+
// q: { kind:"m"|"t", genres:[ids], moods:[keys], decades:[1980,β¦], runtimeMax, runtimeMin, ratingMin,
|
| 64 |
+
// votesMax (hidden gems), lang, person (id), exclude:Set(keys), limit, sort:"top"|"new"|"old"|"az" }
|
| 65 |
+
function query(q = {}) {
|
| 66 |
+
let list = rows;
|
| 67 |
+
if (q.person != null) { const p = personsCache && personsCache[q.person]; list = p ? p.t.map((k) => byKey.get(k)).filter(Boolean) : []; }
|
| 68 |
+
const out = [];
|
| 69 |
+
for (const r of list) {
|
| 70 |
+
if (q.kind && r.k !== q.kind) continue;
|
| 71 |
+
if (q.genres && q.genres.length && !q.genres.some((g) => r.g.includes(g))) continue;
|
| 72 |
+
if (q.moods && q.moods.length && !q.moods.some((m) => r.m.includes(m))) continue;
|
| 73 |
+
if (q.decades && q.decades.length && !q.decades.includes(r.dec)) continue;
|
| 74 |
+
if (q.runtimeMax && !(r.rt && r.rt <= q.runtimeMax)) continue;
|
| 75 |
+
if (q.runtimeMin && !(r.rt && r.rt >= q.runtimeMin)) continue;
|
| 76 |
+
if (q.ratingMin && !(r.r >= q.ratingMin)) continue;
|
| 77 |
+
if (q.votesMax && !(r.v <= q.votesMax)) continue;
|
| 78 |
+
if (q.lang && r.l !== q.lang) continue;
|
| 79 |
+
if (q.exclude && q.exclude.has(r.k + r.i)) continue;
|
| 80 |
+
out.push(r);
|
| 81 |
+
}
|
| 82 |
+
const score = (r) => r.r * Math.log10(1 + r.v); // quality Γ confidence
|
| 83 |
+
if (q.sort === "az") out.sort((a, b) => a.t.localeCompare(b.t));
|
| 84 |
+
else if (q.sort === "new") out.sort((a, b) => b.y - a.y || score(b) - score(a));
|
| 85 |
+
else if (q.sort === "old") out.sort((a, b) => a.y - b.y || score(b) - score(a));
|
| 86 |
+
else out.sort((a, b) => score(b) - score(a));
|
| 87 |
+
return q.limit ? out.slice(0, q.limit) : out;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
// search-as-you-type over the full index (lazy lowercase map; prefix beats substring)
|
| 91 |
+
function search(text, limit = 40) {
|
| 92 |
+
const t = String(text || "").toLowerCase().trim();
|
| 93 |
+
if (!t) return [];
|
| 94 |
+
if (!searchIdx) searchIdx = rows.map((r) => [r.t.toLowerCase(), r]);
|
| 95 |
+
const pre = [], sub = [];
|
| 96 |
+
for (const [lt, r] of searchIdx) {
|
| 97 |
+
if (lt.startsWith(t)) pre.push(r);
|
| 98 |
+
else if (lt.includes(t)) sub.push(r);
|
| 99 |
+
if (pre.length >= limit) break;
|
| 100 |
+
}
|
| 101 |
+
const sc = (r) => r.r * Math.log10(1 + r.v);
|
| 102 |
+
pre.sort((a, b) => sc(b) - sc(a)); sub.sort((a, b) => sc(b) - sc(a));
|
| 103 |
+
return [...pre, ...sub].slice(0, limit);
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
// persons (lazy) β person id β { n: name, t: [rowKeys] }; enables the "tap a face β filmography" pivot
|
| 107 |
+
async function personsIndex() {
|
| 108 |
+
if (personsCache) return personsCache;
|
| 109 |
+
await loadRoot();
|
| 110 |
+
personsCache = dec(await readObject(R.persons, "persons")).persons;
|
| 111 |
+
return personsCache;
|
| 112 |
+
}
|
| 113 |
+
async function person(id) { return (await personsIndex())[id] || null; }
|
| 114 |
+
|
| 115 |
+
return {
|
| 116 |
+
load, query, search, person, personsIndex, loadRoot,
|
| 117 |
+
get rows() { return rows; }, get root() { return R; }, get loaded() { return loaded; },
|
| 118 |
+
rowByKey: (k) => byKey.get(k) || null, stats: () => ({ ...stats }),
|
| 119 |
+
};
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
// ββ browser binding β CacheStorage raw-bytes tier + SubtleCrypto verify βββββββββββββββββββββββββββββββββ
|
| 123 |
+
if (typeof window !== "undefined") {
|
| 124 |
+
const CACHE = "holo-library-objects"; // content-addressed: entries never change, only appear
|
| 125 |
+
const sha256hex = async (u8) => [...new Uint8Array(await crypto.subtle.digest("SHA-256", u8))].map((b) => b.toString(16).padStart(2, "0")).join("");
|
| 126 |
+
const cacheGet = async (sha) => { try { const c = await caches.open(CACHE); const r = await c.match("https://holo.local/lib/" + sha); return r ? new Uint8Array(await r.arrayBuffer()) : null; } catch { return null; } };
|
| 127 |
+
const cachePut = async (sha, bytes) => { try { const c = await caches.open(CACHE); await c.put("https://holo.local/lib/" + sha, new Response(bytes)); } catch {} };
|
| 128 |
+
let one = null;
|
| 129 |
+
window.HoloLibrary = { createLibrary, live() { return one || (one = createLibrary({ sha256hex, cacheGet, cachePut })); } };
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
export default { createLibrary };
|