fix: fix reconnection logic

This commit is contained in:
2026-03-11 18:52:37 +09:00
parent d9f340fd69
commit ceebbf42bb
4 changed files with 64 additions and 19 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "tap-sdk-js", "name": "tap-sdk-js",
"version": "1.1.3", "version": "1.2.3",
"description": "", "description": "",
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",

View File

@@ -12,6 +12,7 @@ export type WSEvent = {
export type WSState = { export type WSState = {
socket: WebSocket | null; socket: WebSocket | null;
connected: boolean; connected: boolean;
connecting: boolean;
retries: number; retries: number;
emitter: EventEmitter<WSEvent>; emitter: EventEmitter<WSEvent>;
}; };
@@ -28,49 +29,87 @@ export function createWebSocketManager(config: WSConfig) {
const state: WSState = { const state: WSState = {
socket: null, socket: null,
connected: false, connected: false,
connecting: false,
retries: 0, retries: 0,
emitter: new EventEmitter(), emitter: new EventEmitter(),
}; };
function connect() { function connect() {
return new Promise<void>((resolve) => { state.connecting = true;
return new Promise<void>((resolve, reject) => {
const socket = new WebSocket(config.url); 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.socket = socket;
state.connected = true; state.connected = true;
state.connecting = false;
state.retries = 0; state.retries = 0;
state.emitter.emit("open"); state.emitter.emit("open");
resolve(); resolve();
});
// Re-attach persistent listeners
socket.on("message", (msg) => { socket.on("message", (msg) => {
state.emitter.emit("message", msg.toString()); state.emitter.emit("message", msg.toString());
}); });
socket.on("error", (err) => { socket.on("error", (err) => {
if (state.connected) state.emitter.emit("error", err); state.emitter.emit("error", err);
else state.emitter.emit("connectionError", err);
}); });
socket.on("close", () => { socket.on("close", () => {
state.emitter.emit("close");
state.connected = false; 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() { function retry() {
if (state.connected || state.connecting) return;
state.retries++;
setTimeout( setTimeout(
() => { () => {
connect().catch(() => retry()); start();
state.retries++;
}, },
backoffDelay(state.retries, config.backoffBaseMs), backoffDelay(state.retries - 1, config.backoffBaseMs),
); );
} }
async function start() { async function start() {
await connect().catch(() => retry()); if (state.connected || state.connecting) return;
try {
await connect();
} catch (e) {
retry();
}
} }
function send(message: string) { function send(message: string) {

View File

@@ -141,6 +141,7 @@ export function createClient(userConfig: ZakoTapOptions): ZakoTapClientHandle {
socket.events.on("connectionError", (err) => socket.events.on("connectionError", (err) =>
emitter.emit("connectionError", err), emitter.emit("connectionError", err),
); );
socket.events.on("close", () => emitter.emit("close"));
socket.events.on("message", (content) => { socket.events.on("message", (content) => {
const data = JSON.parse(content); const data = JSON.parse(content);

View File

@@ -26,6 +26,11 @@ export type TapEvents = {
* Event called on initial connection error. * Event called on initial connection error.
*/ */
connectionError: (e: Error) => any; connectionError: (e: Error) => any;
/**
* Event called on connection close.
*/
close: () => any;
}; };
export const createEmitter = () => new EventEmitter<TapEvents>(); export const createEmitter = () => new EventEmitter<TapEvents>();