/** * Brain Compaction Runner * * Triggers the unified Brain pipeline Lambda (consolidation + compaction) * in the background when a user's page loads. Fire-and-forget: never blocks * the main thread and never throws to the caller. * * Usage: * window.BrainCompaction.runInBackground(siteKey); * * Guarantees: * - Never blocks the main thread (scheduled via requestIdleCallback). * - Never throws to the caller; all errors are logged and swallowed. * - Throttled per-siteKey via localStorage to avoid hammering the Lambda. */ (function () { 'use strict'; var LOG_PREFIX = '[BrainCompaction]'; var CONFIG_URL = 'js/compaction/compaction-config.json'; var STORAGE_KEY_PREFIX = 'brainCompaction:lastRun:'; var configPromise = null; function log() { if (!window.__brainCompactionVerbose) return; var args = Array.prototype.slice.call(arguments); args.unshift(LOG_PREFIX); console.log.apply(console, args); } function warn() { var args = Array.prototype.slice.call(arguments); args.unshift(LOG_PREFIX); console.warn.apply(console, args); } function loadConfig() { if (configPromise) return configPromise; configPromise = fetch(CONFIG_URL, { cache: 'no-cache' }) .then(function (res) { if (!res.ok) throw new Error('Config HTTP ' + res.status); return res.json(); }) .then(function (cfg) { window.__brainCompactionVerbose = !!(cfg.logging && cfg.logging.verbose); return cfg; }) .catch(function (err) { warn('Failed to load config; skipping pipeline.', err); return null; }); return configPromise; } function throttleKey(siteKey) { return STORAGE_KEY_PREFIX + siteKey; } function isThrottled(siteKey, minIntervalMinutes) { try { var raw = localStorage.getItem(throttleKey(siteKey)); if (!raw) return false; var last = parseInt(raw, 10); if (isNaN(last)) return false; var elapsedMin = (Date.now() - last) / 60000; return elapsedMin < minIntervalMinutes; } catch (e) { return false; } } function stampRun(siteKey) { try { localStorage.setItem(throttleKey(siteKey), String(Date.now())); } catch (e) { /* ignore */ } } function fetchWithTimeout(url, options, timeoutMs) { var controller = new AbortController(); var timer = setTimeout(function () { controller.abort(); }, timeoutMs); var opts = Object.assign({}, options, { signal: controller.signal }); return fetch(url, opts).finally(function () { clearTimeout(timer); }); } /** * Last-resort fire-and-forget using no-cors mode. Cannot read the response * but the request still reaches the Lambda. `keepalive: true` lets the * request survive tab close for up to 64 KB of body. */ function fireAndForget(url, payload) { try { fetch(url, { method: 'POST', mode: 'no-cors', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), keepalive: true }).catch(function () { /* ignore */ }); log('Fire-and-forget sent to', url); } catch (e) { /* ignore */ } } function callPipeline(cfg, siteKey) { var url = cfg.function_urls.pipeline; var timeoutMs = (cfg.timing && cfg.timing.request_timeout_ms) || 900000; log('Calling pipeline for siteKey:', siteKey); return fetchWithTimeout(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ customer_id: siteKey }) }, timeoutMs) .then(function (res) { if (!res.ok) { warn('Pipeline HTTP', res.status); return null; } return res.json(); }) .then(function (data) { if (data) log('Pipeline response:', data); return data; }) .catch(function (err) { warn('Pipeline call failed (network/CORS):', err && err.message); fireAndForget(url, { customer_id: siteKey }); return null; }); } function scheduleIdle(fn) { if (typeof window.requestIdleCallback === 'function') { window.requestIdleCallback(fn, { timeout: 5000 }); } else { setTimeout(fn, 0); } } function runInBackground(siteKey) { if (!siteKey) { warn('No siteKey provided; skipping.'); return; } scheduleIdle(function () { loadConfig().then(function (cfg) { if (!cfg) return; if (!cfg.enabled) { log('Disabled via config; skipping.'); return; } if (!cfg.function_urls || !cfg.function_urls.pipeline) { warn('Missing function_urls.pipeline in config; skipping.'); return; } // Throttle disabled — every page load triggers the pipeline. // var minInterval = (cfg.throttle && cfg.throttle.min_interval_minutes) || 60; // if (isThrottled(siteKey, minInterval)) { // log('Throttled (last run within ' + minInterval + ' min); skipping.'); // return; // } stampRun(siteKey); callPipeline(cfg, siteKey) .then(function () { log('Pipeline completed for', siteKey); }) .catch(function (err) { warn('Pipeline error:', err && err.message); }); }); }); } window.BrainCompaction = { runInBackground: runInBackground }; })();