原生壳补齐Tauri请求超时
H5 Tauri HostBridge transport 按 timeoutMs 释放卡住的 invoke 请求 补充 Tauri 超时、计时器清理和迟到回包不改写结果的单测 桌面壳配置门禁锁定 Tauri transport 超时封装 同步宿主壳方案文档和共享决策日志
This commit is contained in:
@@ -119,6 +119,110 @@ describe('nativeAppHostBridge', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Tauri invoke 超时时按 HostBridge 超时错误拒绝请求', async () => {
|
||||
vi.useFakeTimers();
|
||||
const invoke = vi.fn(
|
||||
() =>
|
||||
new Promise<HostBridgeResponse>(() => {
|
||||
// 保持宿主命令挂起,让 H5 超时先返回。
|
||||
}),
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
core: {
|
||||
invoke: asTauriInvoke(invoke),
|
||||
},
|
||||
};
|
||||
|
||||
const pending = requestNativeAppHostBridge('share.open', undefined, {
|
||||
timeoutMs: 10,
|
||||
});
|
||||
const assertion = expect(pending).rejects.toMatchObject({
|
||||
name: 'timeout',
|
||||
message: 'host_bridge_timeout',
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(11);
|
||||
|
||||
await assertion;
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
});
|
||||
|
||||
test('Tauri invoke 成功回包后清理超时计时器', async () => {
|
||||
vi.useFakeTimers();
|
||||
const invoke = vi.fn(async (_command: string, args?: Record<string, unknown>) => {
|
||||
const request = (args as { request: HostBridgeRequest }).request;
|
||||
return {
|
||||
bridge: HOST_BRIDGE_PROTOCOL,
|
||||
version: HOST_BRIDGE_VERSION,
|
||||
id: request.id,
|
||||
ok: true,
|
||||
result: {
|
||||
handled: true,
|
||||
},
|
||||
} satisfies HostBridgeResponse;
|
||||
});
|
||||
window.__TAURI__ = {
|
||||
core: {
|
||||
invoke: asTauriInvoke(invoke),
|
||||
},
|
||||
};
|
||||
|
||||
await expect(
|
||||
requestNativeAppHostBridge('share.open', undefined, {
|
||||
timeoutMs: 1000,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
handled: true,
|
||||
});
|
||||
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
});
|
||||
|
||||
test('Tauri invoke 超时后的迟到回包不改变已拒绝结果', async () => {
|
||||
vi.useFakeTimers();
|
||||
let resolveInvoke:
|
||||
| ((response: HostBridgeResponse<{ handled: boolean }>) => void)
|
||||
| null = null;
|
||||
const invoke = vi.fn(
|
||||
() =>
|
||||
new Promise<HostBridgeResponse<{ handled: boolean }>>((resolve) => {
|
||||
resolveInvoke = resolve;
|
||||
}),
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
core: {
|
||||
invoke: asTauriInvoke(invoke),
|
||||
},
|
||||
};
|
||||
|
||||
const pending = requestNativeAppHostBridge('share.open', undefined, {
|
||||
timeoutMs: 10,
|
||||
});
|
||||
const assertion = expect(pending).rejects.toMatchObject({
|
||||
name: 'timeout',
|
||||
message: 'host_bridge_timeout',
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(11);
|
||||
await assertion;
|
||||
|
||||
const request = (invoke.mock.calls[0]?.[1] as { request: HostBridgeRequest })
|
||||
.request;
|
||||
resolveInvoke?.({
|
||||
bridge: HOST_BRIDGE_PROTOCOL,
|
||||
version: HOST_BRIDGE_VERSION,
|
||||
id: request.id,
|
||||
ok: true,
|
||||
result: {
|
||||
handled: true,
|
||||
},
|
||||
});
|
||||
await Promise.resolve();
|
||||
|
||||
await expect(pending).rejects.toMatchObject({
|
||||
name: 'timeout',
|
||||
message: 'host_bridge_timeout',
|
||||
});
|
||||
});
|
||||
|
||||
test('没有原生宿主 bridge 时返回 null', async () => {
|
||||
await expect(
|
||||
requestNativeAppHostBridge('host.getRuntime'),
|
||||
|
||||
@@ -32,6 +32,11 @@ type PendingNativeRequest = {
|
||||
timeoutId: ReturnType<typeof setTimeout>;
|
||||
};
|
||||
|
||||
type NativeTauriInvoke = <Result = unknown>(
|
||||
command: string,
|
||||
args?: Record<string, unknown>,
|
||||
) => Promise<Result>;
|
||||
|
||||
const pendingNativeRequests = new Map<string, PendingNativeRequest>();
|
||||
const nativeEventListeners = new Map<string, Set<(payload: unknown) => void>>();
|
||||
|
||||
@@ -66,6 +71,13 @@ function createHostBridgeError(error: HostBridgeError) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function createNativeHostBridgeTimeoutError() {
|
||||
return createHostBridgeError({
|
||||
code: 'timeout',
|
||||
message: 'host_bridge_timeout',
|
||||
});
|
||||
}
|
||||
|
||||
function isHostBridgeResponse(value: unknown): value is HostBridgeResponse {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return false;
|
||||
@@ -174,6 +186,32 @@ function ensureNativeBridgeListener() {
|
||||
nativeBridgeListenerInstalled = true;
|
||||
}
|
||||
|
||||
async function invokeTauriHostBridgeWithTimeout<Result>(
|
||||
tauriInvoke: NativeTauriInvoke,
|
||||
request: HostBridgeRequest,
|
||||
timeoutMs: number,
|
||||
) {
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
reject(createNativeHostBridgeTimeoutError());
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
return await Promise.race([
|
||||
tauriInvoke<HostBridgeResponse<Result>>(HOST_BRIDGE_TAURI_COMMAND, {
|
||||
request,
|
||||
}),
|
||||
timeoutPromise,
|
||||
]);
|
||||
} finally {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function canUseReactNativeHostBridge() {
|
||||
return (
|
||||
typeof resolveNativeWindow()?.ReactNativeWebView?.postMessage ===
|
||||
@@ -233,9 +271,10 @@ export async function requestNativeAppHostBridge<Result = unknown>(
|
||||
|
||||
const tauriInvoke = nativeWindow.__TAURI__?.core?.invoke;
|
||||
if (typeof tauriInvoke === 'function') {
|
||||
const response = await tauriInvoke<HostBridgeResponse<Result>>(
|
||||
HOST_BRIDGE_TAURI_COMMAND,
|
||||
{ request },
|
||||
const response = await invokeTauriHostBridgeWithTimeout<Result>(
|
||||
tauriInvoke,
|
||||
request,
|
||||
timeoutMs,
|
||||
);
|
||||
if (!isHostBridgeResponse(response)) {
|
||||
throw new Error('host_bridge_invalid_response');
|
||||
@@ -256,10 +295,7 @@ export async function requestNativeAppHostBridge<Result = unknown>(
|
||||
return await new Promise<Result>((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
pendingNativeRequests.delete(request.id);
|
||||
reject(createHostBridgeError({
|
||||
code: 'timeout',
|
||||
message: 'host_bridge_timeout',
|
||||
}));
|
||||
reject(createNativeHostBridgeTimeoutError());
|
||||
}, timeoutMs);
|
||||
|
||||
pendingNativeRequests.set(request.id, {
|
||||
|
||||
Reference in New Issue
Block a user