mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
fix(app): guard CEF IPC fallback sync throw via safeInvoke wrapper (Sentry TAURI-REACT-7 + 6) (#2619)
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
@@ -6,7 +5,11 @@ import { useT } from '../../../lib/i18n/I18nContext';
|
||||
import { triggerSentryTestEvent } from '../../../services/analytics';
|
||||
import { useAppSelector } from '../../../store/hooks';
|
||||
import { APP_ENVIRONMENT } from '../../../utils/config';
|
||||
import { isTauri } from '../../../utils/tauriCommands/common';
|
||||
// `safeInvoke` (aliased to `invoke`) converts the CEF
|
||||
// `window.ipc.postMessage` synchronous throw — Sentry TAURI-REACT-7 /
|
||||
// TAURI-REACT-6 — into a rejected Promise so the existing `.catch(...)` /
|
||||
// try/catch handlers see it as a normal IPC failure.
|
||||
import { safeInvoke as invoke, isTauri } from '../../../utils/tauriCommands/common';
|
||||
import { resetWalkthrough } from '../../walkthrough/AppWalkthrough';
|
||||
import SettingsHeader from '../components/SettingsHeader';
|
||||
import SettingsMenuItem from '../components/SettingsMenuItem';
|
||||
|
||||
@@ -16,9 +16,16 @@ import { renderWithProviders } from '../../../test/test-utils';
|
||||
|
||||
const hoisted = vi.hoisted(() => ({ invoke: vi.fn(), isTauri: vi.fn(() => true) }));
|
||||
|
||||
vi.mock('@tauri-apps/api/core', () => ({ invoke: hoisted.invoke }));
|
||||
|
||||
vi.mock('../../../utils/tauriCommands/common', () => ({ isTauri: hoisted.isTauri }));
|
||||
// McpServerPanel imports `invoke` from `tauriCommands/common` (aliased from
|
||||
// `safeInvoke` — see OPENHUMAN-TAURI-REACT-7 / TAURI-REACT-6). Route
|
||||
// `hoisted.invoke` through `safeInvoke` so assertions on
|
||||
// `hoisted.invoke.toHaveBeenCalledWith(...)` work unchanged. The
|
||||
// `@tauri-apps/api/core` mock is omitted because `safeInvoke` shadows it
|
||||
// for all panel call sites.
|
||||
vi.mock('../../../utils/tauriCommands/common', () => ({
|
||||
isTauri: hoisted.isTauri,
|
||||
safeInvoke: (...args: unknown[]) => hoisted.invoke(...args),
|
||||
}));
|
||||
|
||||
vi.mock('../hooks/useSettingsNavigation', () => ({
|
||||
useSettingsNavigation: () => ({
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import debug from 'debug';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useT } from '../../../lib/i18n/I18nContext';
|
||||
import { isTauri } from '../../../utils/tauriCommands/common';
|
||||
// `safeInvoke` (aliased to `invoke`) converts the CEF
|
||||
// `window.ipc.postMessage` synchronous throw — Sentry TAURI-REACT-7 /
|
||||
// TAURI-REACT-6 — into a rejected Promise that the existing try/catch sees
|
||||
// as a regular IPC failure.
|
||||
import { safeInvoke as invoke, isTauri } from '../../../utils/tauriCommands/common';
|
||||
import SettingsHeader from '../components/SettingsHeader';
|
||||
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import debug from 'debug';
|
||||
|
||||
import { isTauri } from '../../utils/tauriCommands/common';
|
||||
// `safeInvoke` (aliased to `invoke`) replaces bare
|
||||
// `@tauri-apps/api/core::invoke` so the CEF `window.ipc.postMessage`
|
||||
// synchronous throw (Sentry TAURI-REACT-7 / TAURI-REACT-6) is converted into
|
||||
// a rejected Promise that the existing try/catch chains already handle.
|
||||
import { safeInvoke as invoke, isTauri } from '../../utils/tauriCommands/common';
|
||||
|
||||
const log = debug('native-notifications:bridge');
|
||||
const errLog = debug('native-notifications:bridge:error');
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
*
|
||||
* There is **no** demo loop — the overlay is entirely event-driven.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import {
|
||||
currentMonitor,
|
||||
getCurrentWindow,
|
||||
@@ -37,6 +36,13 @@ import RotatingTetrahedronCanvas from '../components/RotatingTetrahedronCanvas';
|
||||
import { useT } from '../lib/i18n/I18nContext';
|
||||
import { callCoreRpc, getCoreHttpBaseUrl } from '../services/coreRpcClient';
|
||||
import { connectCoreSocket } from '../services/coreSocket';
|
||||
// `safeInvoke` (aliased to `invoke`) converts the CEF
|
||||
// `window.ipc.postMessage` synchronous throw — Sentry TAURI-REACT-7 /
|
||||
// TAURI-REACT-6 — into a rejected Promise so the existing `.catch(...)`
|
||||
// handler sees it as a normal IPC failure. The overlay window is the most
|
||||
// at-risk surface here because it boots into its own WebView where the
|
||||
// CEF IPC bridge can briefly be unwired.
|
||||
import { safeInvoke as invoke } from '../utils/tauriCommands/common';
|
||||
|
||||
const OVERLAY_IDLE_WIDTH = 50;
|
||||
const OVERLAY_IDLE_HEIGHT = 50;
|
||||
|
||||
@@ -12,8 +12,16 @@ vi.mock('@tauri-apps/api/core', () => ({ invoke: invokeMock, isTauri: vi.fn(() =
|
||||
// coreIsTauri() from @tauri-apps/api/core. The default mock returns false
|
||||
// (non-Tauri env); tests that need the Tauri-path branch override it
|
||||
// inline.
|
||||
//
|
||||
// `safeInvoke` is the migration shim for the CEF IPC sync-throw
|
||||
// (OPENHUMAN-TAURI-REACT-7 / TAURI-REACT-6). In production it routes through
|
||||
// `coreInvoke`; for unit tests we keep `invokeMock` as the seam so the
|
||||
// existing assertions on call args / order keep working unchanged.
|
||||
const isTauriMock = vi.fn(() => false);
|
||||
vi.mock('../../utils/tauriCommands/common', () => ({ isTauri: isTauriMock }));
|
||||
vi.mock('../../utils/tauriCommands/common', () => ({
|
||||
isTauri: isTauriMock,
|
||||
safeInvoke: (...args: unknown[]) => invokeMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock('../coreRpcClient', () => ({ clearCoreRpcTokenCache: clearCoreRpcTokenCacheMock }));
|
||||
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
* is stuck. Outside Tauri (web preview / Vitest harness) this is a no-op
|
||||
* that returns a friendly error string.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
import { isTauri } from '../utils/tauriCommands/common';
|
||||
// `safeInvoke` (in place of `@tauri-apps/api/core::invoke`) converts the CEF
|
||||
// `window.ipc.postMessage` synchronous throw — Sentry TAURI-REACT-7 /
|
||||
// TAURI-REACT-6 — into a rejected Promise so the caller's `await` /
|
||||
// `.catch(...)` can handle it instead of bubbling as an unhandled rejection.
|
||||
import { safeInvoke as invoke, isTauri } from '../utils/tauriCommands/common';
|
||||
import { clearCoreRpcTokenCache } from './coreRpcClient';
|
||||
|
||||
export async function restartCoreProcess(): Promise<void> {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
|
||||
import debug from 'debug';
|
||||
import { z } from 'zod';
|
||||
@@ -15,7 +14,11 @@ import {
|
||||
import { addIntegrationNotification } from '../store/notificationSlice';
|
||||
import { fetchRespondQueue } from '../store/providerSurfaceSlice';
|
||||
import type { AccountProvider, IngestedMessage } from '../types/accounts';
|
||||
import { isTauri } from '../utils/tauriCommands/common';
|
||||
// `safeInvoke` replaces bare `@tauri-apps/api/core::invoke` so the CEF
|
||||
// `window.ipc.postMessage` synchronous throw (Sentry TAURI-REACT-7 /
|
||||
// TAURI-REACT-6) is converted into a rejected Promise rather than an
|
||||
// unhandled rejection. Existing `.catch(...)` chains keep working unchanged.
|
||||
import { safeInvoke as invoke, isTauri } from '../utils/tauriCommands/common';
|
||||
import { openhumanGetMeetSettings } from '../utils/tauriCommands/config';
|
||||
import { trackEvent } from './analytics';
|
||||
import { threadApi } from './api/threadApi';
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
/**
|
||||
* Authentication commands.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
import { callCoreRpc } from '../../services/coreRpcClient';
|
||||
import { CommandResponse, isTauri } from './common';
|
||||
// `safeInvoke` (aliased to `invoke`) replaces bare
|
||||
// `@tauri-apps/api/core::invoke` so the CEF `window.ipc.postMessage`
|
||||
// synchronous throw (Sentry TAURI-REACT-7 / TAURI-REACT-6) surfaces as a
|
||||
// rejected Promise. `exchangeToken` runs early in the auth flow where the
|
||||
// CEF bridge can still be unwired, so this matters most.
|
||||
import { type CommandResponse, safeInvoke as invoke, isTauri } from './common';
|
||||
|
||||
/**
|
||||
* Exchange a login token for a session token
|
||||
|
||||
@@ -15,11 +15,19 @@
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { isTauri, parseServiceCliOutput } from './common';
|
||||
import { IpcUnavailableError, isTauri, parseServiceCliOutput, safeInvoke } from './common';
|
||||
|
||||
const coreIsTauriMock = vi.fn();
|
||||
const coreInvokeMock = vi.fn();
|
||||
|
||||
vi.mock('@tauri-apps/api/core', () => ({ isTauri: () => coreIsTauriMock() }));
|
||||
vi.mock('@tauri-apps/api/core', () => ({
|
||||
isTauri: () => coreIsTauriMock(),
|
||||
// Forward only the args that `safeInvoke` actually passed so arity-strict
|
||||
// expectations (`toHaveBeenCalledWith(cmd)` / `toHaveBeenCalledWith(cmd,
|
||||
// args)`) match the wrapper's contract. Spreading `arguments` would invent
|
||||
// a trailing `undefined`; using rest preserves the original arity.
|
||||
invoke: (...args: unknown[]) => coreInvokeMock(...args),
|
||||
}));
|
||||
|
||||
describe('isTauri (tauriCommands/common)', () => {
|
||||
// We mutate `window` to simulate Tauri-runtime bootstrap state across cases.
|
||||
@@ -151,3 +159,117 @@ describe('parseServiceCliOutput (tauriCommands/common)', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// `safeInvoke` is the migration target for every IPC call site that today
|
||||
// hands a bare `invoke(...)` Promise to `.catch(noop)` or to a try/catch.
|
||||
// Under CEF the underlying `coreInvoke` can throw **synchronously** when the
|
||||
// vendored `ipc-protocol.js` fallback path runs into the unwired
|
||||
// `window.ipc.postMessage` (see OPENHUMAN-TAURI-REACT-7 / TAURI-REACT-6). The
|
||||
// sync throw escapes the surrounding Promise body and lands on
|
||||
// `onunhandledrejection`, where Sentry captures it as noisy `Non-Error
|
||||
// promise rejection` events. `safeInvoke` must convert that into a rejected
|
||||
// Promise tagged with `IpcUnavailableError`.
|
||||
describe('safeInvoke (tauriCommands/common)', () => {
|
||||
beforeEach(() => {
|
||||
coreInvokeMock.mockReset();
|
||||
});
|
||||
|
||||
it('returns the resolved value when the underlying invoke resolves', async () => {
|
||||
coreInvokeMock.mockResolvedValue('ok');
|
||||
|
||||
await expect(safeInvoke<string>('greet')).resolves.toBe('ok');
|
||||
// Wrapper forwards only the args the caller passed (preserves arity for
|
||||
// strict-match test mocks like `tauriBridge.test.ts`).
|
||||
expect(coreInvokeMock).toHaveBeenCalledWith('greet');
|
||||
});
|
||||
|
||||
it('forwards args (and only args) when called with two arguments', async () => {
|
||||
coreInvokeMock.mockResolvedValue(42);
|
||||
|
||||
await safeInvoke<number>('doStuff', { foo: 1 });
|
||||
|
||||
expect(coreInvokeMock).toHaveBeenCalledWith('doStuff', { foo: 1 });
|
||||
});
|
||||
|
||||
it('forwards args and options when called with three arguments', async () => {
|
||||
coreInvokeMock.mockResolvedValue(42);
|
||||
|
||||
await safeInvoke<number>('doStuff', { foo: 1 }, { headers: { 'X-Test': '1' } });
|
||||
|
||||
expect(coreInvokeMock).toHaveBeenCalledWith(
|
||||
'doStuff',
|
||||
{ foo: 1 },
|
||||
{ headers: { 'X-Test': '1' } }
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects with the original error when the underlying invoke returns a rejected promise (no sync throw)', async () => {
|
||||
coreInvokeMock.mockRejectedValue(new Error('command failed'));
|
||||
|
||||
await expect(safeInvoke<string>('whatever')).rejects.toThrow('command failed');
|
||||
});
|
||||
|
||||
// The OPENHUMAN-TAURI-REACT-7 / TAURI-REACT-6 regression: a sync `TypeError`
|
||||
// thrown inside the IPC fallback (vendored `ipc-protocol.js:84`) escapes the
|
||||
// Promise body of `coreInvoke` if the call site doesn't wrap it. `safeInvoke`
|
||||
// must catch that throw and surface it as a *rejected* Promise instead, so
|
||||
// existing `.catch(...)` chains keep working and Sentry stops capturing the
|
||||
// raw TypeError as an unhandled rejection.
|
||||
it('converts a synchronous coreInvoke throw into a rejected Promise', async () => {
|
||||
coreInvokeMock.mockImplementation(() => {
|
||||
throw new Error('something went wrong sync');
|
||||
});
|
||||
|
||||
const promise = safeInvoke<string>('willThrow');
|
||||
// The wrapper must return a Promise *object* even though the underlying
|
||||
// call threw synchronously. `expect(...).rejects` would fail with a
|
||||
// confusing message if the wrapper itself re-threw.
|
||||
expect(promise).toBeInstanceOf(Promise);
|
||||
await expect(promise).rejects.toThrow('something went wrong sync');
|
||||
});
|
||||
|
||||
it('wraps the CEF "postMessage of undefined" TypeError in IpcUnavailableError', async () => {
|
||||
const cefThrow = new TypeError("Cannot read properties of undefined (reading 'postMessage')");
|
||||
coreInvokeMock.mockImplementation(() => {
|
||||
throw cefThrow;
|
||||
});
|
||||
|
||||
const err = await safeInvoke<void>('webview_account_hide').catch((e: unknown) => e);
|
||||
|
||||
expect(err).toBeInstanceOf(IpcUnavailableError);
|
||||
const typed = err as IpcUnavailableError;
|
||||
expect(typed.name).toBe('IpcUnavailableError');
|
||||
expect(typed.cmd).toBe('webview_account_hide');
|
||||
expect(typed.cause).toBe(cefThrow);
|
||||
expect(typed.message).toContain('webview_account_hide');
|
||||
expect(typed.message).toContain('postMessage');
|
||||
});
|
||||
|
||||
it('does NOT wrap unrelated TypeErrors that do not mention postMessage', async () => {
|
||||
const otherTypeError = new TypeError('something else entirely');
|
||||
coreInvokeMock.mockImplementation(() => {
|
||||
throw otherTypeError;
|
||||
});
|
||||
|
||||
const err = await safeInvoke<void>('some_cmd').catch((e: unknown) => e);
|
||||
|
||||
// Pass through verbatim — existing message-based classifiers (e.g.
|
||||
// `classifyWebviewAccountError`) must keep seeing the original error.
|
||||
expect(err).toBe(otherTypeError);
|
||||
expect(err).not.toBeInstanceOf(IpcUnavailableError);
|
||||
});
|
||||
|
||||
it('also rejects with IpcUnavailableError when the failure mode arrives via the promise (mid-session fallback)', async () => {
|
||||
// The fallback path can also surface the same TypeError via the rejected
|
||||
// Promise pathway (when CEF eventually wires the bridge object, the call
|
||||
// proceeds far enough to construct the Promise but still fails on the
|
||||
// missing `postMessage`). Same classifier must handle both shapes.
|
||||
const cefThrow = new TypeError("Cannot read properties of undefined (reading 'postMessage')");
|
||||
coreInvokeMock.mockRejectedValue(cefThrow);
|
||||
|
||||
const err = await safeInvoke<void>('webview_account_reveal').catch((e: unknown) => e);
|
||||
|
||||
expect(err).toBeInstanceOf(IpcUnavailableError);
|
||||
expect((err as IpcUnavailableError).cmd).toBe('webview_account_reveal');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
/**
|
||||
* Common utilities and types for Tauri Commands.
|
||||
*/
|
||||
import { isTauri as coreIsTauri } from '@tauri-apps/api/core';
|
||||
import {
|
||||
invoke as coreInvoke,
|
||||
isTauri as coreIsTauri,
|
||||
type InvokeArgs,
|
||||
type InvokeOptions,
|
||||
} from '@tauri-apps/api/core';
|
||||
import debug from 'debug';
|
||||
|
||||
const log = debug('tauri:ipc-guard');
|
||||
const errLog = debug('tauri:ipc-guard:error');
|
||||
|
||||
/**
|
||||
* True when the Tauri runtime is present AND the underlying IPC transport is
|
||||
@@ -94,3 +100,128 @@ export function parseServiceCliOutput<T>(raw: string): CommandResponse<T> {
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Typed marker for the CEF "IPC bridge not wired" failure mode. The vendored
|
||||
* `app/src-tauri/vendor/tauri-cef/crates/tauri/scripts/ipc-protocol.js` falls
|
||||
* back to `window.ipc.postMessage(...)` whenever the custom-protocol fetch
|
||||
* rejects (network blip, navigation interrupt, mid-session re-entry). On CEF
|
||||
* `window.ipc` is never wired — `app/src-tauri/src/cef_impl.rs` drops the
|
||||
* `ipc_handler` registration — so the fallback throws
|
||||
* `TypeError: Cannot read properties of undefined (reading 'postMessage')`
|
||||
* **synchronously**, before the underlying `invoke()` constructs its Promise.
|
||||
* The throw escapes the Promise executor and lands on `onunhandledrejection`,
|
||||
* which Sentry then captures as `TAURI-REACT-7` / `TAURI-REACT-6` with no user
|
||||
* impact recorded because the call sites never caught it.
|
||||
*
|
||||
* `safeInvoke()` converts that synchronous throw into a rejected Promise
|
||||
* tagged with this error, so call sites can `.catch(...)` (or `await` inside
|
||||
* a try/catch) the same way they would for any other rejection. Callers that
|
||||
* want to branch on the failure shape (e.g. degrade to a non-Tauri path
|
||||
* rather than surface a generic error) can `instanceof IpcUnavailableError`.
|
||||
*/
|
||||
export class IpcUnavailableError extends Error {
|
||||
readonly cmd: string;
|
||||
/** The original `TypeError` (or other sync throw) the IPC bridge raised. */
|
||||
readonly cause: unknown;
|
||||
constructor(cmd: string, cause: unknown) {
|
||||
const message =
|
||||
cause instanceof Error && cause.message ? cause.message : 'IPC bridge not wired';
|
||||
super(`Tauri IPC unavailable for command "${cmd}": ${message}`);
|
||||
this.name = 'IpcUnavailableError';
|
||||
this.cmd = cmd;
|
||||
this.cause = cause;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pattern matching the CEF IPC-fallback `TypeError`. We match on the message
|
||||
* substring `postMessage` rather than the constructor because:
|
||||
*
|
||||
* - The throw originates inside Tauri's `sendIpcMessage` (vendored
|
||||
* `ipc-protocol.js:84`) which uses native `TypeError`. There is no
|
||||
* sentinel class we can `instanceof` against.
|
||||
* - V8 / Blink emit the exact message
|
||||
* `Cannot read properties of undefined (reading 'postMessage')`. CEF ships
|
||||
* the same engine, so the substring is stable across the supported channels
|
||||
* (see [feedback_cef_runtime_gaps]).
|
||||
*
|
||||
* Any future engine that changes the wording will still surface as a
|
||||
* generic `IpcUnavailableError` via the fallback branch in `classifyIpcThrow`.
|
||||
*/
|
||||
function looksLikeCefPostMessageThrow(err: unknown): boolean {
|
||||
if (!(err instanceof TypeError)) return false;
|
||||
const msg = err.message ?? '';
|
||||
return msg.includes('postMessage');
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify a value thrown synchronously by `coreInvoke()`. Returns the typed
|
||||
* `IpcUnavailableError` when the shape matches the CEF fallback failure, or
|
||||
* `null` to let the caller surface the original error verbatim.
|
||||
*/
|
||||
function classifyIpcThrow(cmd: string, err: unknown): IpcUnavailableError | null {
|
||||
if (looksLikeCefPostMessageThrow(err)) {
|
||||
return new IpcUnavailableError(cmd, err);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around `@tauri-apps/api/core::invoke()` that:
|
||||
*
|
||||
* 1. Calls through to `coreInvoke` inside a `try / catch` so a **synchronous**
|
||||
* throw (e.g. the CEF `window.ipc.postMessage` `TypeError`) is converted
|
||||
* into a rejected Promise. Without this, the throw escapes the Promise
|
||||
* executor where `coreInvoke` lives and lands on `onunhandledrejection`
|
||||
* → Sentry captures it as `Non-Error promise rejection`-shaped noise.
|
||||
* 2. Re-tags the specific CEF fallback throw as `IpcUnavailableError` so
|
||||
* callers can `.catch((e) => e instanceof IpcUnavailableError ? … : …)`
|
||||
* and degrade gracefully (skip / fallback) instead of surfacing a raw
|
||||
* `TypeError` message to the user.
|
||||
*
|
||||
* Use this in place of bare `invoke(...)` at every call site that is either:
|
||||
* - fire-and-forget (`void invoke(...)` / `invoke(...).catch(noop)`), or
|
||||
* - inside a try/catch that should also handle the bridge-unavailable case.
|
||||
*
|
||||
* Sites that already gate on `isTauri()` (which short-circuits the CEF
|
||||
* bootstrap gap) still benefit from `safeInvoke` because the gap is the
|
||||
* *common* failure window, but the same `TypeError` is also raised when the
|
||||
* custom-protocol path fails mid-session and the fallback path runs into the
|
||||
* missing `window.ipc` — `isTauri()` would return `true` at that point.
|
||||
*/
|
||||
export async function safeInvoke<T>(
|
||||
cmd: string,
|
||||
args?: InvokeArgs,
|
||||
options?: InvokeOptions
|
||||
): Promise<T> {
|
||||
try {
|
||||
// The throw we're guarding against happens BEFORE coreInvoke gets a chance
|
||||
// to return its Promise (the Promise constructor synchronously dispatches
|
||||
// through `__TAURI_INTERNALS__.postMessage` which in turn calls into the
|
||||
// vendored `sendIpcMessage`, which is where the bad `window.ipc` access
|
||||
// lives). Hence the wrapper itself needs to be a real `async` function so
|
||||
// the surrounding try/catch covers the call-time path.
|
||||
//
|
||||
// We forward only the args the caller actually provided so the call
|
||||
// shape (1-arg / 2-arg / 3-arg) matches what bare `invoke()` would have
|
||||
// produced. This preserves arity for downstream test mocks that use
|
||||
// `toHaveBeenCalledWith(cmd, args)` (strict arg-count matchers).
|
||||
if (options !== undefined) {
|
||||
return await coreInvoke<T>(cmd, args, options);
|
||||
}
|
||||
if (args !== undefined) {
|
||||
return await coreInvoke<T>(cmd, args);
|
||||
}
|
||||
return await coreInvoke<T>(cmd);
|
||||
} catch (err) {
|
||||
const typed = classifyIpcThrow(cmd, err);
|
||||
if (typed) {
|
||||
errLog('safeInvoke(%s) -> IpcUnavailableError: %s', cmd, typed.message);
|
||||
throw typed;
|
||||
}
|
||||
// Not the CEF bridge issue — surface as-is so existing message-based
|
||||
// classifiers (e.g. `classifyWebviewAccountError`) still match.
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
/**
|
||||
* Conscious loop commands.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
import { isTauri } from './common';
|
||||
// `safeInvoke` (aliased to `invoke`) replaces bare
|
||||
// `@tauri-apps/api/core::invoke` so the CEF `window.ipc.postMessage`
|
||||
// synchronous throw (Sentry TAURI-REACT-7 / TAURI-REACT-6) lands as a
|
||||
// rejected Promise instead of escaping to `onunhandledrejection`.
|
||||
import { safeInvoke as invoke, isTauri } from './common';
|
||||
|
||||
/**
|
||||
* Trigger a conscious loop run manually.
|
||||
|
||||
Reference in New Issue
Block a user