From ceebbf42bb1708c0995e063f7a7e073d603af33e Mon Sep 17 00:00:00 2001 From: minco Date: Wed, 11 Mar 2026 18:52:37 +0900 Subject: [PATCH] fix: fix reconnection logic --- package.json | 2 +- src/api/websocket.ts | 75 +++++++++++++++++++++++++++++++++----------- src/core/client.ts | 1 + src/core/events.ts | 5 +++ 4 files changed, 64 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 06ae050..71ee39c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tap-sdk-js", - "version": "1.1.3", + "version": "1.2.3", "description": "", "scripts": { "build": "tsc", diff --git a/src/api/websocket.ts b/src/api/websocket.ts index 9ad67ee..0c33e99 100644 --- a/src/api/websocket.ts +++ b/src/api/websocket.ts @@ -12,6 +12,7 @@ export type WSEvent = { export type WSState = { socket: WebSocket | null; connected: boolean; + connecting: boolean; retries: number; emitter: EventEmitter; }; @@ -28,49 +29,87 @@ export function createWebSocketManager(config: WSConfig) { const state: WSState = { socket: null, connected: false, + connecting: false, retries: 0, emitter: new EventEmitter(), }; function connect() { - return new Promise((resolve) => { + state.connecting = true; + return new Promise((resolve, reject) => { const socket = new WebSocket(config.url); - socket.on("open", () => { + + const onOpen = () => { + socket.off("open", onOpen); + socket.off("error", onError); + socket.off("close", onClose); + state.socket = socket; state.connected = true; + state.connecting = false; state.retries = 0; state.emitter.emit("open"); resolve(); - }); - socket.on("message", (msg) => { - state.emitter.emit("message", msg.toString()); - }); + // Re-attach persistent listeners + socket.on("message", (msg) => { + state.emitter.emit("message", msg.toString()); + }); - socket.on("error", (err) => { - if (state.connected) state.emitter.emit("error", err); - else state.emitter.emit("connectionError", err); - }); + socket.on("error", (err) => { + state.emitter.emit("error", err); + }); - socket.on("close", () => { - state.emitter.emit("close"); - state.connected = false; - }); + socket.on("close", () => { + state.connected = false; + state.emitter.emit("close"); + retry(); + }); + }; + + const onError = (err: Error) => { + socket.off("open", onOpen); + socket.off("error", onError); + socket.off("close", onClose); + state.connecting = false; + state.emitter.emit("connectionError", err); + reject(err); + }; + + const onClose = () => { + socket.off("open", onOpen); + socket.off("error", onError); + socket.off("close", onClose); + state.connecting = false; + const err = new Error("Connection closed before open"); + state.emitter.emit("connectionError", err); + reject(err); + }; + + socket.on("open", onOpen); + socket.on("error", onError); + socket.on("close", onClose); }); } function retry() { + if (state.connected || state.connecting) return; + state.retries++; setTimeout( () => { - connect().catch(() => retry()); - state.retries++; + start(); }, - backoffDelay(state.retries, config.backoffBaseMs), + backoffDelay(state.retries - 1, config.backoffBaseMs), ); } async function start() { - await connect().catch(() => retry()); + if (state.connected || state.connecting) return; + try { + await connect(); + } catch (e) { + retry(); + } } function send(message: string) { diff --git a/src/core/client.ts b/src/core/client.ts index b65949d..6dec9f9 100644 --- a/src/core/client.ts +++ b/src/core/client.ts @@ -141,6 +141,7 @@ export function createClient(userConfig: ZakoTapOptions): ZakoTapClientHandle { socket.events.on("connectionError", (err) => emitter.emit("connectionError", err), ); + socket.events.on("close", () => emitter.emit("close")); socket.events.on("message", (content) => { const data = JSON.parse(content); diff --git a/src/core/events.ts b/src/core/events.ts index b54f7da..23f8a11 100644 --- a/src/core/events.ts +++ b/src/core/events.ts @@ -26,6 +26,11 @@ export type TapEvents = { * Event called on initial connection error. */ connectionError: (e: Error) => any; + + /** + * Event called on connection close. + */ + close: () => any; }; export const createEmitter = () => new EventEmitter();