Files
ark-stronghold-protocol/public/js/skland/scan.js
T
herryyun 7c25c89992
CI / test (ubuntu-latest, node 22) (push) Failing after 33m10s
CI / test (ubuntu-latest, node 24) (push) Failing after 34m41s
CI / tooling (lint, imports, typecheck) (push) Failing after 36m11s
CI / test (windows-latest, node 22) (push) Canceled after 0s
CI / test (windows-latest, node 24) (push) Canceled after 0s
实现账号绑定、持久登录与跨设备对局恢复
2026-10-14 23:22:50 +08:00

71 lines
3.0 KiB
JavaScript

// Serial QR requests: a refresh cannot replace a scan while its confirmation poll is still in flight.
export const SCAN_POLL_MS = 2_000;
export const SCAN_REFRESH_MS = 10_000;
export const SCAN_CONFIRM_REFRESH_MS = 60_000;
export function createQrLogin({ request, encode, onQr, onPending, onError, onConfirmed, onBusy,
now = Date.now, timers = globalThis, isHidden = () => false }) {
let stopped = false, completed = false, confirmed = false, active = false, busy = false, timer;
let ready = false, awaiting = false, manual = true;
let refreshAt = Infinity, pollAt = Infinity;
const clear = () => { timers.clearTimeout(timer); timer = undefined; };
const schedule = () => {
clear();
if (stopped || completed || !active || busy) return;
const due = manual ? now() : ready ? Math.min(refreshAt, pollAt) : refreshAt;
timer = timers.setTimeout(tick, isHidden() ? SCAN_POLL_MS : Math.max(0, due - now()));
};
const generate = async () => {
manual = false; ready = false; awaiting = false;
// Keep the displayed image until its replacement is encoded; refreshing only changes the existing img's src.
onBusy(true); onPending(false); onError(null);
try {
const result = await request('scan/start', {});
if (stopped) return;
if (!result.ok || !result.scanUrl) throw new Error(result.error || 'scan_response');
const image = await encode(result.scanUrl);
if (stopped) return;
ready = true; refreshAt = now() + SCAN_REFRESH_MS; pollAt = now() + SCAN_POLL_MS;
onQr(image);
} finally { if (!stopped) onBusy(false); }
};
async function tick() {
clear();
if (stopped || completed || !active || busy) return;
if (isHidden()) { schedule(); return; }
busy = true;
try {
if (confirmed) { manual = false; await onConfirmed(); completed = true; }
else if (manual || !ready) await generate();
else {
// Poll once before an automatic refresh too: a just-scanned QR gets its full confirmation window.
const result = await request('scan/poll', {});
if (stopped) return;
if (!result.ok || result.error) throw new Error(result.error || 'scan_response');
if (result.state === 'confirmed') {
confirmed = true; onQr(null);
await onConfirmed();
completed = true;
return;
}
if (result.state !== 'waiting') throw new Error('scan_expired');
if (result.awaitingConfirmation && !awaiting) {
awaiting = true; refreshAt = now() + SCAN_CONFIRM_REFRESH_MS; onPending(true);
}
pollAt = now() + SCAN_POLL_MS;
if (active && !isHidden() && (manual || now() >= refreshAt)) await generate();
}
} catch (error) {
if (!stopped) {
ready = false; refreshAt = now() + SCAN_REFRESH_MS;
awaiting = false; onPending(false); onQr(null); onError(error);
}
} finally { busy = false; schedule(); }
}
return {
setActive(value) { active = value; schedule(); },
refresh() { manual = true; schedule(); },
stop() { stopped = true; clear(); },
};
}