PageSourceSearch

https://astro-beattie.com/js/visitor-counter.bef334dead9e0893d8b91…feaf6c3a818dc9645b8c3cbdb94eb6ae0eb3b14.mjs

js astro-beattie.com collected 2026-10-09 22:07:18 UTC 3,075 bytes, 65 lines download raw bytes

1const badge = document.getElementById('visitor-counter');
2const number = document.getElementById('visit-count');
3const label = document.getElementById('counter-label');
4const endpoint = badge?.dataset.endpoint;
5const isProduction = ['astro-beattie.com', 'www.astro-beattie.com'].includes(location.hostname);
6const KEY = 'astro-beattie-visit-session-v1';
7const IDLE_MS = 30 * 60 * 1000;
8let memorySession;
9let busy = false;
10let lastTotal = -1;
11let needsRegistration = isProduction;
12let pendingSession;
13
14function sessionId() {
15  const now = Date.now();
16  let session = memorySession;
17  try { session = JSON.parse(localStorage.getItem(KEY)) || session; } catch { /* Storage may be blocked. */ }
18  if (!session || typeof session.id !== 'string' || !Number.isFinite(session.seen) || now - session.seen >= IDLE_MS || session.seen > now) {
19    session = { id: crypto.randomUUID(), seen: now };
20  }
21  session.seen = now;
22  memorySession = session;
23  try { localStorage.setItem(KEY, JSON.stringify(session)); } catch { /* Reuse the in-memory session for retries. */ }
24  return session.id;
25}
26
27async function update() {
28  if (busy || document.hidden || !endpoint) return;
29  busy = true;
30  try {
31    if (needsRegistration && !pendingSession) {
32      // Coordinate first loads in multiple tabs where Web Locks is supported.
33      pendingSession = navigator.locks ? await navigator.locks.request(KEY, sessionId) : sessionId();
34    }
35    const response = await fetch(endpoint, {
36      method: needsRegistration ? 'POST' : 'GET',
37      cache: 'no-store', credentials: 'omit', signal: AbortSignal.timeout(8000),
38      ...(needsRegistration ? { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session: pendingSession }) } : {}),
39    });
40    if (!response.ok) throw new Error('Counter unavailable');
41    const data = await response.json();
42    if (!Number.isSafeInteger(data.visits) || data.visits < 0) throw new Error('Invalid total');
43    needsRegistration = false;
44    // An older concurrent response must never move the visible total backwards.
45    lastTotal = Math.max(lastTotal, data.visits);
46    number.textContent = lastTotal.toLocaleString();
47    label.textContent = ' visits';
48    const since = document.getElementById('counter-since');
49    if (since) since.textContent = data.since ? `Since ${data.since}` : '';
50    badge.title = `Visits${data.since ? ' since ' + data.since : ''}. One browser session; a new visit after 30 minutes of inactivity. Updated every minute.`;
51    badge.removeAttribute('data-unavailable');
52  } catch {
53    // Never manufacture a number or present a browser-local total as global.
54    number.textContent = '—';
55    label.textContent = ' visits unavailable';
56    badge.title = 'The shared visit total is temporarily unavailable. Retrying shortly.';
57    badge.dataset.unavailable = 'true';
58  } finally { busy = false; }
59}
60
61if (badge && number && label) {
62  update();
63  setInterval(update, 60000); // Reading the total never adds a visit.
64  document.addEventListener('visibilitychange', () => { if (!document.hidden) update(); });
65}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.