Skip to content

Commit 5ab184f

Browse files
feat: implement WebSocket client cache eviction on error
- Evict the cached WebSocket client when a WebSocket error occurs, ensuring that subsequent subscriptions create a fresh client. - Add tests to verify the behavior of client cache eviction upon WebSocket errors, confirming that the client is recreated as expected. This change enhances the reliability of WebSocket subscriptions by preventing stale connections.
1 parent 06cb62a commit 5ab184f

2 files changed

Lines changed: 50 additions & 0 deletions

File tree

src/subscriptions/actions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ function createEventStream<T>(
114114
resolveNext = null;
115115
rejectNext = null;
116116
}
117+
// Evict the dead WebSocket client so the next subscription creates a fresh one
118+
wsClientCache.delete(client);
117119
// Auto-terminate stream on WebSocket failure so subsequent next() calls
118120
// return {done: true} instead of hanging indefinitely
119121
isUnsubscribed = true;

tests/subscriptions.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,29 @@ import {
77
} from "@/subscriptions/actions";
88
import {GenLayerClient, GenLayerChain} from "@/types";
99

10+
// Track createPublicClient calls and captured onError handlers
11+
const {createPublicClientSpy, getCapturedOnError, setCapturedOnError} = vi.hoisted(() => {
12+
let capturedOnError: ((error: Error) => void) | null = null;
13+
return {
14+
createPublicClientSpy: vi.fn(() => ({
15+
watchContractEvent: vi.fn((opts: {onError?: (error: Error) => void}) => {
16+
capturedOnError = opts.onError ?? null;
17+
return vi.fn(); // unwatch function
18+
}),
19+
})),
20+
getCapturedOnError: () => capturedOnError,
21+
setCapturedOnError: (v: ((error: Error) => void) | null) => {
22+
capturedOnError = v;
23+
},
24+
};
25+
});
26+
1027
// Mock viem module for WebSocket-related tests
1128
vi.mock("viem", async importOriginal => {
1229
const actual = (await importOriginal()) as object;
1330
return {
1431
...actual,
32+
createPublicClient: createPublicClientSpy,
1533
webSocket: vi.fn(() => ({
1634
request: vi.fn(),
1735
type: "webSocket" as const,
@@ -189,3 +207,33 @@ describe("ConsensusEventStream interface", () => {
189207
expect(typeof actions.subscribeToTransactionLeaderTimeout).toBe("function");
190208
});
191209
});
210+
211+
describe("WebSocket client cache eviction on error", () => {
212+
afterEach(() => {
213+
setCapturedOnError(null);
214+
vi.restoreAllMocks();
215+
});
216+
217+
it("should evict cached wsClient on WebSocket error so next subscription creates a fresh client", () => {
218+
createPublicClientSpy.mockClear();
219+
const client = createMockClient({webSocketUrl: "wss://example.com/ws"});
220+
const actions = subscriptionActions(client);
221+
222+
// First subscription creates the wsClient
223+
actions.subscribeToNewTransaction();
224+
expect(createPublicClientSpy).toHaveBeenCalledTimes(1);
225+
226+
// Second subscription reuses the cached wsClient
227+
actions.subscribeToNewTransaction();
228+
expect(createPublicClientSpy).toHaveBeenCalledTimes(1);
229+
230+
// Simulate a WebSocket error
231+
const onError = getCapturedOnError();
232+
expect(onError).not.toBeNull();
233+
onError!(new Error("WebSocket connection closed"));
234+
235+
// Third subscription should create a new wsClient because the cache was evicted
236+
actions.subscribeToNewTransaction();
237+
expect(createPublicClientSpy).toHaveBeenCalledTimes(2);
238+
});
239+
});

0 commit comments

Comments
 (0)