Something went wrong. Try again.
this post sux
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351/* * sux background page — settings mirror, classification queue, Jev HTTP client, * verdict cache, and messaging hub (Ports to bsky.app content scripts, messages * from the sidebar). Runs as a Firefox MV3 non-persistent background page. *//* global SuxShared, browser */(function () { "use strict";
var SH = globalThis.SuxShared; var API_URL = "https://api.typesafe.ai/v1/systemone"; var CONCURRENCY = 2; var MAX_ATTEMPTS = 5; var CACHE_CAP = 2000; var VERDICT_CAP = 10000; var REQUEST_TIMEOUT_MS = 30000;
var settings = SH.defaults(); var cache = new Map(); // cacheKey -> verdict (persisted) var verdictsById = new Map(); // post id -> verdict (session only) var ports = new Set(); var queue = []; // pending jobs: {key, text, alt, ids: []} var running = 0; var portStats = new Map(); // port -> last reported content-script diagnostics var pumpScheduled = false; var cacheWriteTimer = null; var settingsWriteTimer = null;
var stats = { judged: 0, // verdicts served (API-computed + cache hits) hidden: 0, // of those, "hide" failed: 0, // API attempts that exhausted retries (failed open) lastError: "", lastErrorAt: 0, };
function recordError(context, err) { stats.failed++; stats.lastError = (context ? context + ": " : "") + String((err && err.message) || err).slice(0, 500); stats.lastErrorAt = Date.now(); }
function sleep(ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); }
/* ---------------- Jev API ---------------- */
async function jevRequest(apiKey, payload) { var delay = 2000; for (var attempt = 1; ; attempt++) { var res = await fetch(API_URL, { method: "POST", headers: { Authorization: "Bearer " + apiKey, "Content-Type": "application/json", }, body: JSON.stringify(payload), signal: typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(REQUEST_TIMEOUT_MS) : undefined, }); if (res.status === 429 || res.status === 529) { if (attempt >= MAX_ATTEMPTS) { throw new Error("Jev API overloaded (HTTP " + res.status + "), gave up after " + MAX_ATTEMPTS + " attempts"); } var retryAfter = res.headers.get("Retry-After"); var wait = retryAfter ? Math.min((parseInt(retryAfter, 10) || 0) * 1000, 60000) : delay; if (!wait || wait < 0) wait = delay; await sleep(wait); delay = Math.min(delay * 2, 30000); continue; } if (!res.ok) { var detail = ""; try { detail = (await res.text()).slice(0, 200); } catch (e) { /* ignore */ } throw new Error("Jev API HTTP " + res.status + (detail ? ": " + detail : "")); } return res.json(); } }
async function classify(job) { var verdict = "show"; var details = null; try { // Parse with the settings the request was built with — the live settings // may have changed (rule toggled/words edited) while the job was queued. var st = job.settings || settings; var payload = SH.buildRequest(st, { text: job.text, alt: job.alt }); var res = await jevRequest(settings.apiKey, payload); var v = SH.decideFromAnswers(st, (res && res.answers) || null); if (v === null) { throw new Error( "Unexpected Jev response shape: " + JSON.stringify(res && res.answers).slice(0, 400) ); } verdict = v; details = SH.answerDetails(st, (res && res.answers) || null); } catch (err) { recordError("classify", err); verdict = "show"; // fail open: a broken filter must not brick the feed } stats.judged++; cache.set( job.key, details && Object.keys(details).length ? { v: verdict, d: details } : verdict ); trimCache(); scheduleCacheWrite(); for (var i = 0; i < job.ids.length; i++) { var id = job.ids[i]; verdictsById.set(id, { key: job.key, verdict: verdict }); if (verdictsById.size > VERDICT_CAP) { verdictsById.delete(verdictsById.keys().next().value); } } broadcast({ type: "verdicts", items: job.ids.map(function (id) { var item = { id: id, verdict: verdict }; if (details && Object.keys(details).length) item.d = details; return item; }), }); }
/* ---------------- queue ---------------- */
function schedulePump() { if (pumpScheduled) return; pumpScheduled = true; setTimeout(function () { pumpScheduled = false; pump(); }, 0); }
function pump() { if (!SH.isActive(settings)) { queue.length = 0; // filtering off → drop pending work return; } if (!settings.apiKey) return; // resolvePost fails open before enqueueing while (running < CONCURRENCY && queue.length) { var job = queue.shift(); running++; classify(job) .catch(function (err) { recordError("classify", err); }) .then(function () { running--; schedulePump(); }); } }
// Returns an immediate verdict object, or null when the post is (now) pending. // In-memory verdicts are tagged with the rule-config key they belong to, so // flipping a rule or a direction never serves a stale verdict. function resolvePost(p) { var key = SH.isActive(settings) && SH.isJudgeable(p) && settings.apiKey ? SH.cacheKey(settings, p.text, p.alt) : null; var prev = verdictsById.get(p.id); if (prev && prev.key === key) { return { id: p.id, verdict: prev.verdict }; } if (!SH.isJudgeable(p)) { var open = { id: p.id, verdict: "show" }; // nothing to judge → fail open verdictsById.set(p.id, { key: key, verdict: open.verdict }); return open; } if (!SH.isActive(settings)) { return { id: p.id, verdict: "show" }; // deliberately not cached } if (!settings.apiKey) { stats.lastError = "Missing TypeSafe API key — add it in the sux sidebar."; stats.lastErrorAt = Date.now(); var v = { id: p.id, verdict: "show" }; verdictsById.set(p.id, { key: key, verdict: v.verdict }); return v; } if (cache.has(key)) { var entry = cache.get(key); var verdict = typeof entry === "string" ? entry : entry.v; // tolerate legacy string entries var d = typeof entry === "string" ? null : entry.d; verdictsById.set(p.id, { key: key, verdict: verdict }); stats.judged++; var r = { id: p.id, verdict: verdict }; if (d) r.d = d; return r; } var existing = null; for (var i = 0; i < queue.length; i++) { if (queue[i].key === key) { existing = queue[i]; break; } } if (existing) { if (existing.ids.indexOf(p.id) === -1) existing.ids.push(p.id); } else { queue.push({ key: key, text: p.text || "", alt: p.alt || "", ids: [p.id], settings: settings, // snapshot for request build + response parse }); } schedulePump(); return null; // pending }
/* ---------------- persistence ---------------- */
function trimCache() { while (cache.size > CACHE_CAP) { cache.delete(cache.keys().next().value); } }
function scheduleCacheWrite() { if (cacheWriteTimer) return; cacheWriteTimer = setTimeout(function () { cacheWriteTimer = null; var obj = {}; cache.forEach(function (v, k) { obj[k] = v; }); browser.storage.local.set({ cache: obj }).catch(function () { /* storage may be unavailable */ }); }, 2000); }
function scheduleSettingsWrite() { if (settingsWriteTimer) return; settingsWriteTimer = setTimeout(function () { settingsWriteTimer = null; browser.storage.local.set({ settings: settings }).catch(function () { /* ignore */ }); }, 300); }
async function loadState() { try { var stored = await browser.storage.local.get(["settings", "cache"]); if (stored.settings) settings = SH.normalizeSettings(stored.settings); if (stored.cache) { Object.keys(stored.cache).forEach(function (k) { cache.set(k, stored.cache[k]); }); } } catch (err) { recordError("load", err); } }
/* ---------------- messaging ---------------- */
function broadcast(msg) { ports.forEach(function (port) { try { port.postMessage(msg); } catch (e) { ports.delete(port); } }); }
browser.runtime.onConnect.addListener(function (port) { if (port.name !== "sux") return; ports.add(port); port.postMessage({ type: "settings", settings: settings, active: SH.isActive(settings) }); port.onMessage.addListener(function (msg) { if (!msg || typeof msg !== "object") return; if (msg.type === "contentStats") { portStats.set(port, msg.stats || {}); return; } if (msg.type !== "getVerdicts" || !Array.isArray(msg.posts)) return; var items = []; for (var i = 0; i < msg.posts.length; i++) { var r = resolvePost(msg.posts[i]); if (r) items.push(r); } if (items.length) { try { port.postMessage({ type: "verdicts", items: items }); } catch (e) { /* ignore */ } } }); port.onDisconnect.addListener(function () { ports.delete(port); portStats.delete(port); }); });
browser.runtime.onMessage.addListener(function (msg) { if (!msg || typeof msg !== "object") return undefined; switch (msg.type) { case "getSettings": return Promise.resolve({ settings: settings }); case "saveSettings": settings = SH.normalizeSettings(msg.settings); scheduleSettingsWrite(); broadcast({ type: "settings", settings: settings, active: SH.isActive(settings) }); if (!SH.isActive(settings)) queue.length = 0; return Promise.resolve({ ok: true, active: SH.isActive(settings) }); case "getStats": { var liveHidden = 0; var agg = { seen: 0, domExtracted: 0, items: 0, hidden: 0, unmatchedAnchors: 0, scans: 0 }; var activeSeen = false; portStats.forEach(function (s) { liveHidden += s.hidden || 0; Object.keys(agg).forEach(function (k) { agg[k] += s[k] || 0; }); if (s.settingsActive) activeSeen = true; }); agg.settingsActive = activeSeen; return Promise.resolve({ stats: { judged: stats.judged, hidden: liveHidden, // currently hidden, across tabs — 0 when filtering is off failed: stats.failed, lastError: stats.lastError, lastErrorAt: stats.lastErrorAt, waiting: queue.length + running, }, active: SH.isActive(settings), content: agg, }); } case "testKey": { var apiKey = typeof msg.apiKey === "string" ? msg.apiKey : ""; if (!apiKey) return Promise.resolve({ ok: false, error: "Enter an API key first." }); var probe = { state: { post: { text: "The kittens are playing in the garden again.", alt: "" } }, model: "jev-latest", questions: { ping: { type: "noul", instructions: "Does the `post` mention an animal?", }, }, }; return jevRequest(apiKey, probe).then( function (res) { var ok = !!(res && res.answers && res.answers.ping && typeof res.answers.ping.noul === "number"); return ok ? { ok: true, model: res.model || "jev-latest" } : { ok: false, error: "Response shape was not what sux expects." }; }, function (err) { return { ok: false, error: String((err && err.message) || err).slice(0, 300) }; } ); } default: return undefined; } });
loadState().then(function () { broadcast({ type: "settings", settings: settings, active: SH.isActive(settings) }); });})();