const WebSocket = require("ws");
const apiKey = process.env.TICKDB_API_KEY;
const universe = process.env.TICKDB_UNIVERSE;
if (!apiKey) throw new Error("Set TICKDB_API_KEY first");
if (universe && !["CN_Stock", "HK_Stock", "US_Stock"].includes(universe)) {
throw new Error("Invalid TICKDB_UNIVERSE");
}
let attempts = 0;
let stopped = false;
const RECONNECT_STAGES = new Map([
[1_000, 3], // 等待 1 秒,最多 3 次
[5_000, 3], // 等待 5 秒,最多 3 次
[30_000, Infinity] // 此后每次等待 30 秒
]);
function reconnectDelay(attempt) {
for (const [delayMs, count] of RECONNECT_STAGES) {
if (attempt <= count) return delayMs;
attempt -= count;
}
}
function connect() {
const url = new URL("wss://api.tickdb.ai/v1/realtime");
url.searchParams.set("api_key", apiKey);
const ws = new WebSocket(url, { perMessageDeflate: true });
let heartbeat;
let pongTimeout;
ws.on("unexpected-response", (_request, response) => {
if ([401, 403].includes(response.statusCode)) stopped = true;
console.error("WebSocket handshake failed:", response.statusCode);
response.resume();
ws.terminate();
});
ws.on("open", () => {
attempts = 0;
if (universe && !ws.extensions.includes("permessage-deflate")) {
stopped = true;
console.error("Full-market subscriptions require permessage-deflate");
ws.close();
return;
}
const subscription = universe
? { cmd: "subscribe", data: { universes: [universe], channels: ["ticker"] } }
: { cmd: "subscribe", data: { channel: "ticker", symbols: ["BTCUSDT"] } };
ws.send(JSON.stringify(subscription)); // 每次重连后重新订阅
heartbeat = setInterval(() => {
if (ws.readyState !== WebSocket.OPEN || pongTimeout) return;
ws.send(JSON.stringify({ cmd: "ping" }));
pongTimeout = setTimeout(() => ws.terminate(), 20_000);
}, 60_000);
});
ws.on("message", (raw) => {
let message;
try { message = JSON.parse(raw.toString()); }
catch { return; }
for (const item of Array.isArray(message) ? message : [message]) {
if (item.cmd === "pong") {
clearTimeout(pongTimeout);
pongTimeout = undefined;
} else if (item.cmd === "ticker") {
console.log(item.data.symbol, item.data.last_price);
} else if (item.cmd === "error") {
console.error("Subscription error:", item.code, item.message);
if (["2008", "4005"].includes(String(item.code))) {
stopped = true;
ws.close();
}
}
}
});
ws.on("error", (error) => console.error("WebSocket error:", error.message));
ws.on("close", () => {
clearInterval(heartbeat);
clearTimeout(pongTimeout);
if (stopped) return;
setTimeout(connect, reconnectDelay(++attempts));
});
}
connect();