const BASE = (document.querySelector('meta[name="zeyn-base"]')?.content ?? '').replace(/\/$/, '');
const PREFIX = 'zeyn.v1.';
const VERSION = 1;
const MAX_TEXT = 200;
const SLACK_MS = 60000;
const memory = new Map();
const listeners = new Set();
let consent = null;
function area(name) {
try {
const s = window[name];
s.setItem('zeyn.probe', '1');
s.removeItem('zeyn.probe');
return s;
} catch {
return null;
}
}
const local = area('localStorage');
const session = area('sessionStorage');
export function keyOf(el) {
const name = el?.dataset?.zeynRemember;
const ms = Number(el?.dataset?.zeynRememberFor);
if (!name || !/^[A-Za-z0-9_-]{1,40}$/.test(name) || !(ms > 0)) return null;
return { name, ms, purpose: el.dataset.zeynRememberPurpose === 'preference' ? 'preference' : 'necessary' };
}
function check(raw, ms) {
let r;
try {
r = JSON.parse(raw);
} catch {
return undefined;
}
if (!r || typeof r !== 'object' || r.v !== VERSION || !Number.isInteger(r.exp)) return undefined;
const now = Date.now();
if (r.exp <= now || r.exp > now + ms + SLACK_MS) return undefined;
if (r.val === true) return true;
if (typeof r.val === 'string' && r.val.length > 0 && r.val.length <= MAX_TEXT) return r.val;
return undefined;
}
export function get(key) {
const id = PREFIX + key.name;
if (memory.has(id)) {
const v = check(memory.get(id), key.ms);
if (v !== undefined) return v;
memory.delete(id);
}
for (const s of [session, local]) {
let raw = null;
try {
raw = s?.getItem(id) ?? null;
} catch {
continue;
}
if (raw === null) continue;
const v = check(raw, key.ms);
if (v !== undefined) return v;
try {
s.removeItem(id);
} catch {}
}
return undefined;
}
function allowed() {
consent ??= fetch(`${BASE}/_zeyn/prefs`, { credentials: 'same-origin', cache: 'no-store' })
.then((r) => (r.ok ? r.json() : null))
.then((b) => b?.preferences === true)
.catch(() => false);
return consent;
}
export async function set(key, val) {
if (val !== true && !(typeof val === 'string' && val.length > 0 && val.length <= MAX_TEXT)) return;
const id = PREFIX + key.name;
const raw = JSON.stringify({ v: VERSION, exp: Date.now() + key.ms, val });
const lasting = key.purpose === 'necessary' || (await allowed());
const target = lasting ? local : session;
const other = lasting ? session : local;
try {
if (!target) throw new Error('blocked');
target.setItem(id, raw);
other?.removeItem(id);
memory.delete(id);
} catch {
memory.set(id, raw);
}
}
export function remove(key) {
const id = PREFIX + key.name;
memory.delete(id);
for (const s of [session, local]) {
try {
s?.removeItem(id);
} catch {}
}
}
export function clear() {
memory.clear();
for (const s of [session, local]) {
try {
for (let i = (s?.length ?? 0) - 1; i >= 0; i--) {
const k = s.key(i);
if (k?.startsWith(PREFIX)) s.removeItem(k);
}
} catch {}
}
for (const fn of listeners) fn(null);
}
export function on(fn) {
listeners.add(fn);
}
window.addEventListener('storage', (e) => {
if (e.key !== null && !e.key.startsWith(PREFIX)) return;
const name = e.key === null ? null : e.key.slice(PREFIX.length);
for (const fn of listeners) fn(name);
});
document.addEventListener('click', (e) => {
const button = e.target instanceof Element ? e.target.closest('[data-zeyn-prefs-clear]') : null;
if (button) clear();
});
globalThis.zeyn = Object.assign(globalThis.zeyn ?? {}, { state: { get, set, remove, keyOf, on }, prefs: { clear } });