50 lines
2.5 KiB
JavaScript
50 lines
2.5 KiB
JavaScript
// A local random guest key restores this browser's history, never customer order identity.
|
|
var VisitorSession = (function(){
|
|
const storageName = 'baiyue-visitor-key';
|
|
const nativeFetch = typeof window.fetch === 'function' ? window.fetch.bind(window) : fetch;
|
|
let ready = null, established = false;
|
|
function localKey(){
|
|
try{
|
|
const saved = localStorage.getItem(storageName);
|
|
if(saved && /^[0-9a-f]{64}$/.test(saved)) return saved;
|
|
const crypto = window.crypto;
|
|
if(!crypto || !crypto.getRandomValues) return null;
|
|
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
const key = Array.from(bytes, value => value.toString(16).padStart(2, '0')).join('');
|
|
localStorage.setItem(storageName, key);
|
|
return key;
|
|
}catch(error){ return null; } // HttpOnly cookie can still retain this guest when storage is disabled.
|
|
}
|
|
function ensure(){
|
|
if(ready) return ready;
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), 10000);
|
|
ready = nativeFetch('/api/visitor/session', {method:'POST',credentials:'same-origin',
|
|
headers:{'Content-Type':'application/json'},body:JSON.stringify({key:localKey()}),signal:controller.signal})
|
|
.then(async response => {
|
|
const data = await response.json();
|
|
if(!response.ok || !data.ok || typeof data.key !== 'string' || !/^[0-9a-f]{64}$/.test(data.key)){
|
|
throw new Error('访客会话暂时无法建立,请刷新后重试。');
|
|
}
|
|
try{ localStorage.setItem(storageName, data.key); }catch(error){}
|
|
established = true;
|
|
}).catch(error => { ready = null; established = false; throw error; }).finally(() => clearTimeout(timer));
|
|
return ready;
|
|
}
|
|
function request(path, options){
|
|
const init = Object.assign({}, options, {credentials:'same-origin'});
|
|
function send(){
|
|
if(init.signal && init.signal.aborted) return Promise.reject(Object.assign(new Error('Aborted'), {name:'AbortError'}));
|
|
const session = ready;
|
|
return nativeFetch(path, init).then(async response => {
|
|
if(response.status !== 401) return response;
|
|
if(ready === session){ ready = null; established = false; }
|
|
await ensure();
|
|
return nativeFetch(path, init);
|
|
});
|
|
}
|
|
return established ? send() : ensure().then(send);
|
|
}
|
|
return {fetch:request};
|
|
})();
|