perf(ui): reduce startup connect error overhead (#123910)

This commit is contained in:
Peter Steinberger
2026-08-14 18:07:20 -07:00
committed by GitHub
parent b8a8df7008
commit 0982ee5729
5 changed files with 20 additions and 62 deletions
@@ -11,7 +11,7 @@ import {
formatConnectPairingRequiredMessage,
normalizePairingConnectRequestId,
readConnectErrorDetailCode,
readControlUiBuildMismatchDetails,
readControlUiBuildMismatchId,
readConnectErrorRecoveryAdvice,
readConnectPairingRequiredMessage,
readPairingConnectErrorDetails,
@@ -44,19 +44,15 @@ describe("readConnectErrorDetailCode", () => {
});
});
describe("readControlUiBuildMismatchDetails", () => {
describe("readControlUiBuildMismatchId", () => {
it("returns a bounded reload target", () => {
expect(
readControlUiBuildMismatchDetails({
readControlUiBuildMismatchId({
code: ConnectErrorDetailCodes.CONTROL_UI_BUILD_MISMATCH,
gatewayBuildId: "gateway-build",
reloadRequired: true,
}),
).toEqual({
code: ConnectErrorDetailCodes.CONTROL_UI_BUILD_MISMATCH,
gatewayBuildId: "gateway-build",
reloadRequired: true,
});
).toBe("gateway-build");
});
it.each([
@@ -73,7 +69,7 @@ describe("readControlUiBuildMismatchDetails", () => {
reloadRequired: false,
},
])("rejects malformed details", (details) => {
expect(readControlUiBuildMismatchDetails(details)).toBeNull();
expect(readControlUiBuildMismatchId(details)).toBeNull();
});
});
@@ -55,12 +55,6 @@ export const ConnectErrorDetailCodes = {
type ConnectErrorDetailCode =
(typeof ConnectErrorDetailCodes)[keyof typeof ConnectErrorDetailCodes];
export type ControlUiBuildMismatchDetails = {
code: typeof ConnectErrorDetailCodes.CONTROL_UI_BUILD_MISMATCH;
gatewayBuildId: string;
reloadRequired: true;
};
/** Pairing-specific reasons clients can display and use for reconnect policy. */
const ConnectPairingRequiredReasons = {
NOT_PAIRED: "not-paired",
@@ -237,15 +231,8 @@ export function readConnectErrorDetailCode(details: unknown): string | null {
}
/** Read the exact target artifact from an untrusted reload-required rejection. */
export function readControlUiBuildMismatchDetails(
details: unknown,
): ControlUiBuildMismatchDetails | null {
if (
readConnectErrorDetailCode(details) !== ConnectErrorDetailCodes.CONTROL_UI_BUILD_MISMATCH ||
!details ||
typeof details !== "object" ||
Array.isArray(details)
) {
export function readControlUiBuildMismatchId(details: unknown): string | null {
if (readConnectErrorDetailCode(details) !== ConnectErrorDetailCodes.CONTROL_UI_BUILD_MISMATCH) {
return null;
}
const raw = details as { gatewayBuildId?: unknown; reloadRequired?: unknown };
@@ -253,11 +240,7 @@ export function readControlUiBuildMismatchDetails(
if (!gatewayBuildId || gatewayBuildId.length > 96 || raw.reloadRequired !== true) {
return null;
}
return {
code: ConnectErrorDetailCodes.CONTROL_UI_BUILD_MISMATCH,
gatewayBuildId,
reloadRequired: true,
};
return gatewayBuildId;
}
/** Extracts normalized retry advice from untrusted connect-error details. */
-21
View File
@@ -1,10 +1,8 @@
import {
ConnectErrorDetailCodes,
GatewayProtocolRequestError,
MIN_CLIENT_PROTOCOL_VERSION,
PROTOCOL_VERSION,
readConnectErrorDetailCode,
shouldPauseGatewayReconnect,
} from "@openclaw/gateway-client/browser";
export function enrichProtocolMismatchDetails(
@@ -30,22 +28,3 @@ export function resolveGatewayErrorDetailCode(
): string | null {
return readConnectErrorDetailCode(error?.details);
}
export function isLegacyGatewayBuildIdSchemaError(
error: GatewayProtocolRequestError,
clientBuildId: string | undefined,
): boolean {
return Boolean(
clientBuildId && /invalid connect params.*unexpected property.*buildid/iu.test(error.message),
);
}
/** Token mismatch stays with its bounded retry owner; static failures pause. */
export function isNonRecoverableConnectError(error: { details?: unknown } | undefined): boolean {
return error
? shouldPauseGatewayReconnect({
details: error.details,
protocolMismatchIsTerminal: true,
})
: false;
}
+8 -8
View File
@@ -27,6 +27,7 @@ import {
PROTOCOL_VERSION,
resolveGatewayStartupRetryAfterMs,
resolveSafeTimeoutDelayMs,
shouldPauseGatewayReconnect,
} from "@openclaw/gateway-client/browser";
export type { EventFrame as GatewayEventFrame } from "@openclaw/gateway-client/browser";
import type {
@@ -54,8 +55,6 @@ import { generateUUID } from "../lib/uuid.ts";
import { createBrowserGatewaySocket } from "./gateway-browser-socket.ts";
import {
enrichProtocolMismatchDetails,
isLegacyGatewayBuildIdSchemaError,
isNonRecoverableConnectError,
resolveGatewayErrorDetailCode,
} from "./gateway-connect-errors.ts";
@@ -288,7 +287,6 @@ export class GatewayBrowserClient {
// Older shipped Gateways used a closed client schema. Downgrade once per
// browser client; a document reload creates a fresh exact-identity attempt.
private clientBuildIdCompatibilityDisabled = false;
private clientBuildIdRetryBudgetUsed = false;
// Close/stop advances this generation before another socket can make stale hello work look active.
private recovery = { value: "", resolved: false, generation: 0 };
private scopeUpgradeBinding: ScopeUpgradeBinding | null = null;
@@ -380,7 +378,6 @@ export class GatewayBrowserClient {
this.pendingDeviceTokenRetry = false;
this.deviceTokenRetryBudgetUsed = false;
this.clientBuildIdCompatibilityDisabled = false;
this.clientBuildIdRetryBudgetUsed = false;
}
get connected() {
@@ -587,11 +584,11 @@ export class GatewayBrowserClient {
const connectErrorCode =
err instanceof GatewayRequestError ? resolveGatewayErrorDetailCode(err) : null;
if (
!this.clientBuildIdRetryBudgetUsed &&
isLegacyGatewayBuildIdSchemaError(err, plan.params.client.buildId)
!this.clientBuildIdCompatibilityDisabled &&
plan.params.client.buildId &&
/invalid connect params.*unexpected property.*buildid/iu.test(err.message)
) {
this.clientBuildIdCompatibilityDisabled = true;
this.clientBuildIdRetryBudgetUsed = true;
this.client.resetReconnectBackoff(250);
return { closeCode: CONNECT_FAILED_CLOSE_CODE, closeReason: "connect retry" };
}
@@ -725,7 +722,10 @@ export class GatewayBrowserClient {
const retry =
connectErrorCode === ConnectErrorDetailCodes.AUTH_TOKEN_MISMATCH
? this.pendingDeviceTokenRetry
: !isNonRecoverableConnectError(connectError);
: !shouldPauseGatewayReconnect({
details: connectError?.details,
protocolMismatchIsTerminal: true,
});
return { retry, notify: true, pendingError: error };
}
+4 -4
View File
@@ -1,4 +1,4 @@
import { readControlUiBuildMismatchDetails } from "@openclaw/gateway-client/browser";
import { readControlUiBuildMismatchId } from "@openclaw/gateway-client/browser";
import type { ControlUiBootstrapProfileHint } from "../../../src/gateway/control-ui-contract.js";
// Control UI module owns the application gateway store: the reactive
// snapshot around GatewayBrowserClient consumed by the app shell.
@@ -431,9 +431,9 @@ export function createApplicationGateway(
return;
}
stopCanvasSurfaceLease();
const buildMismatch = readControlUiBuildMismatchDetails(error?.details);
if (buildMismatch) {
void scheduleStaleChunkReload({ buildId: buildMismatch.gatewayBuildId });
const mismatchedBuildId = readControlUiBuildMismatchId(error?.details);
if (mismatchedBuildId) {
void scheduleStaleChunkReload({ buildId: mismatchedBuildId });
}
setSnapshot({
...snapshot,