实现账号绑定、持久登录与跨设备对局恢复
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
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
This commit is contained in:
1 parent
488db49f11
commit
7c25c89992
62 files changed
+6855
-1005
No files matched your search
@@ -0,0 +1,70 @@
|
||||
// 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(); },
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user