387a1c26e3
清理已跟踪原始日志与个人本地配置 修复前后端有效门禁、测试和构建问题 补齐生产 Jenkins、SpacetimeDB 本地命令与文档约束 收紧图片编辑器状态、附件与媒体引用测试 排除已下线旧创作入口测试并清理 warning
134 lines
3.0 KiB
TypeScript
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();
|
|
}
|