Files
Genarrative/apps/mobile-shell/src/host-bridge/scanner.ts
T
kdletters 387a1c26e3 完成全量检查整改
清理已跟踪原始日志与个人本地配置
修复前后端有效门禁、测试和构建问题
补齐生产 Jenkins、SpacetimeDB 本地命令与文档约束
收紧图片编辑器状态、附件与媒体引用测试
排除已下线旧创作入口测试并清理 warning
2026-07-17 16:56:46 +08:00

134 lines
3.0 KiB
TypeScript

import {
HOST_BRIDGE_SCANNER_TIMEOUT_MS,
type HostBridgeError,
type HostBridgeRequest,
normalizeHostBridgeQrCodeValue,
type ScannerScanQrCodeResult,
} from '../../../../packages/shared/src/contracts/hostBridge';
import { ok } from './protocol';
type QrScannerState = {
active: boolean;
requestKey: number;
};
type PendingQrScan = {
requestKey: number;
resolve: (result: ScannerScanQrCodeResult) => void;
reject: (error: HostBridgeError) => void;
timeout: ReturnType<typeof setTimeout>;
};
const scannerListeners = new Set<(state: QrScannerState) => void>();
let pendingQrScan: PendingQrScan | null = null;
let nextQrScanRequestKey = 0;
function hostBridgeError(
code: HostBridgeError['code'],
message: string,
): HostBridgeError {
return { code, message };
}
function currentQrScannerState(): QrScannerState {
return {
active: Boolean(pendingQrScan),
requestKey: pendingQrScan?.requestKey ?? 0,
};
}
function emitQrScannerState() {
const state = currentQrScannerState();
for (const listener of scannerListeners) {
listener(state);
}
}
function clearPendingQrScan() {
if (pendingQrScan) {
clearTimeout(pendingQrScan.timeout);
}
pendingQrScan = null;
emitQrScannerState();
}
export function subscribeQrScannerState(
listener: (state: QrScannerState) => void,
) {
scannerListeners.add(listener);
listener(currentQrScannerState());
return () => {
scannerListeners.delete(listener);
};
}
export function scanQrCode(): Promise<ScannerScanQrCodeResult> {
if (pendingQrScan) {
return Promise.reject(
hostBridgeError('host_error', 'qr scanner already active'),
);
}
nextQrScanRequestKey += 1;
return new Promise((resolve, reject) => {
const requestKey = nextQrScanRequestKey;
pendingQrScan = {
requestKey,
resolve,
reject,
timeout: setTimeout(() => {
if (pendingQrScan?.requestKey !== requestKey) {
return;
}
pendingQrScan.reject(hostBridgeError('timeout', 'qr scan timed out'));
clearPendingQrScan();
}, HOST_BRIDGE_SCANNER_TIMEOUT_MS),
};
emitQrScannerState();
});
}
export async function scanMobileHostBridgeQrCode(request: HostBridgeRequest) {
return ok(request, await scanQrCode());
}
export function completeQrCodeScan(rawValue: unknown) {
const result = normalizeHostBridgeQrCodeValue(rawValue);
if (!result || !pendingQrScan) {
return false;
}
pendingQrScan.resolve(result);
clearPendingQrScan();
return true;
}
export function cancelQrCodeScan() {
if (!pendingQrScan) {
return;
}
pendingQrScan.reject(hostBridgeError('cancelled', 'qr scan cancelled'));
clearPendingQrScan();
}
export function failQrCodeScan() {
if (!pendingQrScan) {
return;
}
pendingQrScan.reject(hostBridgeError('host_error', 'qr scanner unavailable'));
clearPendingQrScan();
}
export function resetQrScannerForTest() {
if (pendingQrScan) {
clearTimeout(pendingQrScan.timeout);
}
pendingQrScan = null;
nextQrScanRequestKey = 0;
scannerListeners.clear();
}