Humuhumu33 commited on
Commit
f0a7818
Β·
verified Β·
1 Parent(s): b0ab9a7

holo-evicted-publish: player +2 object(s)

Browse files
b/b659f6ac2534f817fe33fbac5339bedc9d88e89063dba2ebc02f82370a2ee390 ADDED
The diff for this file is too large to render. See raw diff
 
b/eee0254f635b221b434f9d8177ef1b12b310c5ec3984ef44392397d17fc972f7 ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ // network-first (fresh universe), then HTTP cache, then the LAST GOOD root parked in the object cache β€”
25
+ // a returning user boots the whole library with the network gone (P4 offline-first).
26
+ try { R = await (await doFetch(rootUrl + "?v=" + Date.now(), { cache: "no-store" })).json(); }
27
+ catch { try { R = await (await doFetch(rootUrl)).json(); } catch { R = null; } }
28
+ if (R && cachePut) { try { await cachePut("root", new TextEncoder().encode(JSON.stringify(R))); } catch {} }
29
+ if (!R && cacheGet) { const c = await cacheGet("root"); if (c) R = JSON.parse(new TextDecoder().decode(c)); }
30
+ if (!R) throw new Error("holo-library: no root reachable (network + cache empty)");
31
+ return R;
32
+ }
33
+ // verify-or-refuse read of ONE named object { sha256, blake3 }
34
+ async function readObject(ref, name) {
35
+ if (cacheGet) {
36
+ const c = await cacheGet(ref.sha256);
37
+ if (c) {
38
+ if ((await sha256hex(c)) === ref.sha256) { stats.shardsFromCache++; return c; }
39
+ stats.refused++; console.warn("holo-library: REFUSED tampered cache object", name, ref.sha256.slice(0, 12));
40
+ }
41
+ }
42
+ const res = await doFetch(R.mirror + ref.blake3);
43
+ if (!res.ok) throw new Error("holo-library: mirror " + res.status + " for " + name);
44
+ const bytes = new Uint8Array(await res.arrayBuffer());
45
+ if ((await sha256hex(bytes)) !== ref.sha256) { stats.refused++; throw new Error("holo-library: REFUSED " + name + " β€” bytes do not re-derive"); }
46
+ stats.shardsFromNet++;
47
+ if (cachePut) await cachePut(ref.sha256, bytes);
48
+ return bytes;
49
+ }
50
+ const dec = (u8) => JSON.parse(new TextDecoder().decode(u8));
51
+
52
+ // load(onShard) β€” pull all shards (parallel), verify each, build the in-memory plane.
53
+ async function load(onShard = null) {
54
+ if (loaded) return rows;
55
+ await loadRoot();
56
+ const parts = await Promise.all(R.shards.map(async (ref, n) => {
57
+ const s = dec(await readObject(ref, "shard" + n));
58
+ if (onShard) try { onShard(n, s.rows.length); } catch {}
59
+ return s.rows;
60
+ }));
61
+ rows = parts.flat();
62
+ byKey = new Map(rows.map((r) => [r.k + r.i, r]));
63
+ loaded = true;
64
+ return rows;
65
+ }
66
+
67
+ // ── facet algebra β€” one scan, every dimension composable ────────────────────────────────────────────────
68
+ // q: { kind:"m"|"t", genres:[ids], moods:[keys], decades:[1980,…], runtimeMax, runtimeMin, ratingMin,
69
+ // votesMax (hidden gems), lang, person (id), exclude:Set(keys), limit, sort:"top"|"new"|"old"|"az" }
70
+ function query(q = {}) {
71
+ let list = rows;
72
+ if (q.person != null) { const p = personsCache && personsCache[q.person]; list = p ? p.t.map((k) => byKey.get(k)).filter(Boolean) : []; }
73
+ const out = [];
74
+ for (const r of list) {
75
+ if (q.kind && r.k !== q.kind) continue;
76
+ if (q.genres && q.genres.length && !q.genres.some((g) => r.g.includes(g))) continue;
77
+ if (q.moods && q.moods.length && !q.moods.some((m) => r.m.includes(m))) continue;
78
+ if (q.decades && q.decades.length && !q.decades.includes(r.dec)) continue;
79
+ if (q.runtimeMax && !(r.rt && r.rt <= q.runtimeMax)) continue;
80
+ if (q.runtimeMin && !(r.rt && r.rt >= q.runtimeMin)) continue;
81
+ if (q.ratingMin && !(r.r >= q.ratingMin)) continue;
82
+ if (q.votesMax && !(r.v <= q.votesMax)) continue;
83
+ if (q.lang && r.l !== q.lang) continue;
84
+ if (q.exclude && q.exclude.has(r.k + r.i)) continue;
85
+ out.push(r);
86
+ }
87
+ const score = (r) => r.r * Math.log10(1 + r.v); // quality Γ— confidence
88
+ if (q.sort === "az") out.sort((a, b) => a.t.localeCompare(b.t));
89
+ else if (q.sort === "new") out.sort((a, b) => b.y - a.y || score(b) - score(a));
90
+ else if (q.sort === "old") out.sort((a, b) => a.y - b.y || score(b) - score(a));
91
+ else out.sort((a, b) => score(b) - score(a));
92
+ return q.limit ? out.slice(0, q.limit) : out;
93
+ }
94
+
95
+ // search-as-you-type over the full index (lazy lowercase map; prefix beats substring)
96
+ function search(text, limit = 40) {
97
+ const t = String(text || "").toLowerCase().trim();
98
+ if (!t) return [];
99
+ if (!searchIdx) searchIdx = rows.map((r) => [r.t.toLowerCase(), r]);
100
+ const pre = [], sub = [];
101
+ for (const [lt, r] of searchIdx) {
102
+ if (lt.startsWith(t)) pre.push(r);
103
+ else if (lt.includes(t)) sub.push(r);
104
+ if (pre.length >= limit) break;
105
+ }
106
+ const sc = (r) => r.r * Math.log10(1 + r.v);
107
+ pre.sort((a, b) => sc(b) - sc(a)); sub.sort((a, b) => sc(b) - sc(a));
108
+ return [...pre, ...sub].slice(0, limit);
109
+ }
110
+
111
+ // persons (lazy) β€” person id β†’ { n: name, t: [rowKeys] }; enables the "tap a face β†’ filmography" pivot
112
+ async function personsIndex() {
113
+ if (personsCache) return personsCache;
114
+ await loadRoot();
115
+ personsCache = dec(await readObject(R.persons, "persons")).persons;
116
+ return personsCache;
117
+ }
118
+ async function person(id) { return (await personsIndex())[id] || null; }
119
+
120
+ // searchPeople(text) β€” cast/director name β†’ [{ id, name, count, titles:[rows] }] ranked by filmography size.
121
+ // Sync: returns [] if the persons posting isn't resolved yet (caller warms it via personsIndex()).
122
+ function searchPeople(text, limit = 4) {
123
+ if (!personsCache) return [];
124
+ const t = String(text || "").toLowerCase().trim();
125
+ if (t.length < 3) return [];
126
+ const wordRe = new RegExp("\\b" + t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), ""); // matches a name-word starting with the query
127
+ const hits = [];
128
+ for (const id in personsCache) {
129
+ const p = personsCache[id];
130
+ const nm = (p.n || "").toLowerCase();
131
+ if (!nm.includes(t)) continue;
132
+ // Relevance: an exact full-name match wins; then a whole-word match (surname/first name); ties break by
133
+ // filmography size β€” the best signal for "which Nolan did you mean" (Christopher, 12 films, not an extra).
134
+ hits.push({ id: +id, name: p.n, count: p.t.length, exact: nm === t, word: wordRe.test(nm) });
135
+ }
136
+ hits.sort((a, b) => (b.exact - a.exact) || (b.word - a.word) || (b.count - a.count));
137
+ return hits.slice(0, limit).map((h) => ({ ...h, titles: personsCache[h.id].t.map((k) => byKey.get(k)).filter(Boolean) }));
138
+ }
139
+
140
+ return {
141
+ load, query, search, searchPeople, person, personsIndex, loadRoot,
142
+ get rows() { return rows; }, get root() { return R; }, get loaded() { return loaded; },
143
+ personsReady: () => !!personsCache,
144
+ rowByKey: (k) => byKey.get(k) || null, stats: () => ({ ...stats }),
145
+ };
146
+ }
147
+
148
+ // ── browser binding β€” CacheStorage raw-bytes tier + SubtleCrypto verify ─────────────────────────────────
149
+ if (typeof window !== "undefined") {
150
+ const CACHE = "holo-library-objects"; // content-addressed: entries never change, only appear
151
+ const sha256hex = async (u8) => [...new Uint8Array(await crypto.subtle.digest("SHA-256", u8))].map((b) => b.toString(16).padStart(2, "0")).join("");
152
+ 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; } };
153
+ const cachePut = async (sha, bytes) => { try { const c = await caches.open(CACHE); await c.put("https://holo.local/lib/" + sha, new Response(bytes)); } catch {} };
154
+ let one = null;
155
+ window.HoloLibrary = { createLibrary, live() { return one || (one = createLibrary({ sha256hex, cacheGet, cachePut })); } };
156
+ }
157
+
158
+ export default { createLibrary };