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.