live-browser-session.js

  1/**
  2 * Browser-side durable session helpers for Impeccable live mode.
  3 *
  4 * Kept separate from live-browser.js so recovery state can be tested without
  5 * booting the full overlay UI. Served before live-browser.js and attached to
  6 * window.__IMPECCABLE_LIVE_SESSION__.
  7 */
  8(function (root) {
  9  'use strict';
 10
 11  function createLiveBrowserSessionState({ prefix, storage, idFactory }) {
 12    if (!prefix) throw new Error('prefix required');
 13    const store = storage || root.localStorage;
 14    const makeId = idFactory || function () { return Math.random().toString(16).slice(2, 10); };
 15    const sessionKey = prefix + '-session';
 16    const handledKey = sessionKey + '-handled';
 17    const scrollKey = sessionKey + '-scroll';
 18    let checkpointRevision = 0;
 19    const owner = makeId();
 20
 21    function safeRead(key) {
 22      try { return store.getItem(key); } catch { return null; }
 23    }
 24
 25    function safeWrite(key, value) {
 26      try { store.setItem(key, value); } catch { /* quota exceeded or private mode */ }
 27    }
 28
 29    function safeRemove(key) {
 30      try { store.removeItem(key); } catch { /* unavailable storage */ }
 31    }
 32
 33    function loadSession() {
 34      try {
 35        const raw = safeRead(sessionKey);
 36        if (!raw) return null;
 37        const parsed = JSON.parse(raw);
 38        if (Number.isInteger(parsed.checkpointRevision)) {
 39          checkpointRevision = Math.max(checkpointRevision, parsed.checkpointRevision);
 40        }
 41        return parsed;
 42      } catch { return null; }
 43    }
 44
 45    function saveSession(session) {
 46      if (!session || !session.id) return;
 47      const payload = {
 48        ...session,
 49        checkpointRevision,
 50      };
 51      safeWrite(sessionKey, JSON.stringify(payload));
 52    }
 53
 54    function clearSession() {
 55      safeRemove(sessionKey);
 56    }
 57
 58    function nextCheckpointRevision() {
 59      checkpointRevision += 1;
 60      const existing = loadSession();
 61      if (existing?.id) saveSession(existing);
 62      return checkpointRevision;
 63    }
 64
 65    function seedCheckpointRevision(value) {
 66      if (Number.isInteger(value)) checkpointRevision = Math.max(checkpointRevision, value);
 67      return checkpointRevision;
 68    }
 69
 70    function currentCheckpointRevision() {
 71      return checkpointRevision;
 72    }
 73
 74    function markHandled(id) {
 75      if (!id) return;
 76      safeWrite(handledKey, id);
 77    }
 78
 79    function isHandled(id) {
 80      return !!id && safeRead(handledKey) === id;
 81    }
 82
 83    function clearHandled() {
 84      safeRemove(handledKey);
 85    }
 86
 87    function writeScrollY(y) {
 88      safeWrite(scrollKey, String(y));
 89    }
 90
 91    function readScrollY() {
 92      const raw = safeRead(scrollKey);
 93      if (raw == null) return null;
 94      const n = parseFloat(raw);
 95      return isFinite(n) ? n : null;
 96    }
 97
 98    function clearScrollY() {
 99      safeRemove(scrollKey);
100    }
101
102    return {
103      owner,
104      sessionKey,
105      handledKey,
106      scrollKey,
107      saveSession,
108      loadSession,
109      clearSession,
110      nextCheckpointRevision,
111      seedCheckpointRevision,
112      currentCheckpointRevision,
113      markHandled,
114      isHandled,
115      clearHandled,
116      writeScrollY,
117      readScrollY,
118      clearScrollY,
119    };
120  }
121
122  root.__IMPECCABLE_LIVE_SESSION__ = { createLiveBrowserSessionState };
123})(typeof window !== 'undefined' ? window : globalThis);