mirror of
https://github.com/block/buzz.git
synced 2026-11-09 02:41:57 +00:00
fix(desktop): retry failed initial relay dials (#2564)
Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -534,8 +534,6 @@ export class RelayClient {
|
||||
}
|
||||
|
||||
private async connect() {
|
||||
// Clear any pending stability timer from a previous connection — a new
|
||||
// connect attempt resets the clock and must re-arm the timer on success.
|
||||
if (this.stabilityTimer !== null) {
|
||||
window.clearTimeout(this.stabilityTimer);
|
||||
this.stabilityTimer = null;
|
||||
@@ -545,51 +543,66 @@ export class RelayClient {
|
||||
this.hasConnectedOnce ? "reconnecting" : "connecting",
|
||||
);
|
||||
|
||||
if (!this.relayUrl) {
|
||||
this.relayUrl = await getRelayWsUrl();
|
||||
}
|
||||
|
||||
const generation = ++this.connectionGeneration;
|
||||
this.onMessageChannel = new Channel<unknown>((message) => {
|
||||
void this.handleWsMessage(message, generation);
|
||||
});
|
||||
|
||||
this.wsId = await invoke<number>("plugin:websocket|connect", {
|
||||
url: this.relayUrl,
|
||||
onMessage: this.onMessageChannel,
|
||||
config: {},
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timeout = window.setTimeout(() => {
|
||||
this.authRequest = null;
|
||||
void this.handleWsMessage(message, generation).catch((error) => {
|
||||
if (generation !== this.connectionGeneration) return;
|
||||
this.resetConnection(
|
||||
new Error("Timed out while waiting for relay authentication."),
|
||||
this.normalizeRelayError(error, "Relay connection errored."),
|
||||
);
|
||||
reject(new Error("Timed out while waiting for relay authentication."));
|
||||
}, AUTH_TIMEOUT_MS);
|
||||
|
||||
this.authRequest = {
|
||||
pendingEventId: "",
|
||||
resolve,
|
||||
reject,
|
||||
timeout,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// Start a stability timer instead of resetting backoff immediately.
|
||||
// The backoff resets to its base value only after BACKOFF_RESET_STABLE_MS
|
||||
// of uninterrupted uptime, preventing fast reconnect loops from erasing
|
||||
// the exponential backoff that throttles them.
|
||||
this.stabilityTimer = window.setTimeout(() => {
|
||||
this.stabilityTimer = null;
|
||||
this.reconnectDelayMs = RECONNECT_BASE_DELAY_MS;
|
||||
}, BACKOFF_RESET_STABLE_MS);
|
||||
try {
|
||||
if (!this.relayUrl) {
|
||||
this.relayUrl = await getRelayWsUrl();
|
||||
}
|
||||
const wsId = await invoke<number>("plugin:websocket|connect", {
|
||||
url: this.relayUrl,
|
||||
onMessage: this.onMessageChannel,
|
||||
config: {},
|
||||
});
|
||||
if (generation !== this.connectionGeneration) {
|
||||
void closeWebSocket(wsId, "stale connection attempt");
|
||||
throw new Error("Relay connection attempt was superseded.");
|
||||
}
|
||||
this.wsId = wsId;
|
||||
|
||||
await this.replayLiveSubscriptions();
|
||||
this.connectionStateEmitter.set("connected");
|
||||
this.stallWatchdog.start();
|
||||
this.emitReconnectIfNeeded();
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timeout = window.setTimeout(() => {
|
||||
const error = new Error("Relay authentication timed out.");
|
||||
this.authRequest = null;
|
||||
this.resetConnection(error);
|
||||
reject(error);
|
||||
}, AUTH_TIMEOUT_MS);
|
||||
|
||||
this.authRequest = {
|
||||
pendingEventId: "",
|
||||
resolve,
|
||||
reject,
|
||||
timeout,
|
||||
};
|
||||
});
|
||||
|
||||
this.stabilityTimer = window.setTimeout(() => {
|
||||
this.stabilityTimer = null;
|
||||
this.reconnectDelayMs = RECONNECT_BASE_DELAY_MS;
|
||||
}, BACKOFF_RESET_STABLE_MS);
|
||||
|
||||
await this.replayLiveSubscriptions();
|
||||
this.connectionStateEmitter.set("connected");
|
||||
this.stallWatchdog.start();
|
||||
this.emitReconnectIfNeeded();
|
||||
} catch (error) {
|
||||
const connectionError = this.normalizeRelayError(
|
||||
error,
|
||||
"Failed to connect to relay.",
|
||||
);
|
||||
if (generation === this.connectionGeneration) {
|
||||
this.resetConnection(connectionError);
|
||||
}
|
||||
throw connectionError;
|
||||
}
|
||||
}
|
||||
|
||||
private async subscribe(
|
||||
|
||||
@@ -250,6 +250,8 @@ type E2eConfig = {
|
||||
openerError?: string;
|
||||
/** Delay binding signatures so specs can exercise request supersession. */
|
||||
nostrBindSignDelayMs?: number;
|
||||
/** Reject successive mock WebSocket connect attempts, then resume. */
|
||||
websocketConnectErrors?: string[];
|
||||
stallWebsocketSends?: boolean;
|
||||
userSearchDelayMs?: number;
|
||||
// NIP-IA gate inputs — see tests/helpers/bridge.ts:MockBridgeOptions for
|
||||
@@ -8498,6 +8500,11 @@ async function connectRealSocket(args: { url?: string; onMessage: unknown }) {
|
||||
}
|
||||
|
||||
async function connectMockSocket(args: { onMessage: unknown }) {
|
||||
const connectError = getConfig()?.mock?.websocketConnectErrors?.shift();
|
||||
if (connectError) {
|
||||
throw new Error(connectError);
|
||||
}
|
||||
|
||||
if (mockWebsocketSendMutexWedged) {
|
||||
return new Promise<number>(() => {});
|
||||
}
|
||||
|
||||
@@ -80,6 +80,33 @@ test.beforeEach(async ({ page }) => {
|
||||
await installMockBridge(page);
|
||||
});
|
||||
|
||||
test("failed initial relay dial retries automatically", async ({ page }) => {
|
||||
await installMockBridge(page, {
|
||||
websocketConnectErrors: ["mock relay pod unavailable"],
|
||||
});
|
||||
await page.goto("/");
|
||||
|
||||
// App-shell preconnect owns a keep-alive request. The first native dial is
|
||||
// rejected before a socket ID exists; the session must still enter its
|
||||
// backoff loop and recover without a click, query, or reload.
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
page.evaluate(() => {
|
||||
const getState = (
|
||||
window as Window & {
|
||||
__BUZZ_E2E_GET_RELAY_CONNECTION_STATE__?: () => string;
|
||||
}
|
||||
).__BUZZ_E2E_GET_RELAY_CONNECTION_STATE__;
|
||||
if (!getState) throw new Error("Relay state seam is not installed.");
|
||||
return getState();
|
||||
}),
|
||||
{ timeout: 10_000 },
|
||||
)
|
||||
.toBe("connected");
|
||||
await expect(page.getByTestId("channel-general")).toBeVisible();
|
||||
});
|
||||
|
||||
test("passive relay watchdog does not write while the websocket is half-open", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
@@ -261,6 +261,8 @@ type MockBridgeOptions = {
|
||||
openerError?: string;
|
||||
/** Delay binding signatures so specs can exercise request supersession. */
|
||||
nostrBindSignDelayMs?: number;
|
||||
/** Reject successive mock WebSocket connect attempts, then resume. */
|
||||
websocketConnectErrors?: string[];
|
||||
stallWebsocketSends?: boolean;
|
||||
userSearchDelayMs?: number;
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user