Files
TurboTrades/frontend/src/stores/websocket.js
iDefineHD a23d774ca4
All checks were successful
Build Frontend / Build Frontend (push) Successful in 23s
Fix WebSocket fallback URL and add debug logging
- Fixed fallback to use api.turbotrades.dev instead of window.location.host
- Added console logging to debug WebSocket URL resolution
- Simplified fallback logic - always use API URL domain
- This fixes ws.turbotrades.dev connection errors
2026-01-11 02:10:55 +00:00

382 lines
9.5 KiB
JavaScript

import { defineStore } from "pinia";
import { ref, computed } from "vue";
import { useAuthStore } from "./auth";
import { useToast } from "vue-toastification";
const toast = useToast();
export const useWebSocketStore = defineStore("websocket", () => {
// State
const ws = ref(null);
const isConnected = ref(false);
const isConnecting = ref(false);
const reconnectAttempts = ref(0);
const maxReconnectAttempts = ref(5);
const reconnectDelay = ref(1000);
const heartbeatInterval = ref(null);
const reconnectTimeout = ref(null);
const messageQueue = ref([]);
const listeners = ref(new Map());
// Computed
const connectionStatus = computed(() => {
if (isConnected.value) return "connected";
if (isConnecting.value) return "connecting";
return "disconnected";
});
const canReconnect = computed(() => {
return reconnectAttempts.value < maxReconnectAttempts.value;
});
// Helper functions
const getWebSocketUrl = async () => {
console.log("[WebSocket] Getting WebSocket URL...");
// Use environment variable or fallback
if (import.meta.env.VITE_WS_URL) {
console.log(
"[WebSocket] Using VITE_WS_URL:",
import.meta.env.VITE_WS_URL
);
return import.meta.env.VITE_WS_URL;
}
// Try to fetch from backend config API
try {
const apiUrl = import.meta.env.VITE_API_URL || "http://localhost:3000";
console.log(
"[WebSocket] Fetching config from:",
`${apiUrl}/api/config/public`
);
const response = await fetch(`${apiUrl}/api/config/public`);
const data = await response.json();
if (data.success && data.config.websocket?.url) {
console.log(
"[WebSocket] Got URL from backend:",
data.config.websocket.url
);
return data.config.websocket.url;
}
console.warn("[WebSocket] Backend config missing websocket URL");
} catch (error) {
console.warn(
"[WebSocket] Failed to fetch WebSocket URL from config:",
error
);
}
// Fallback: use API domain with /ws path
const apiUrl =
import.meta.env.VITE_API_URL || "https://api.turbotrades.dev";
const fallbackUrl = apiUrl.replace(/^http/, "ws") + "/ws";
console.log("[WebSocket] Using fallback URL:", fallbackUrl);
return fallbackUrl;
};
const clearHeartbeat = () => {
if (heartbeatInterval.value) {
clearInterval(heartbeatInterval.value);
heartbeatInterval.value = null;
}
};
const clearReconnectTimeout = () => {
if (reconnectTimeout.value) {
clearTimeout(reconnectTimeout.value);
reconnectTimeout.value = null;
}
};
const startHeartbeat = () => {
clearHeartbeat();
// Send ping every 30 seconds
heartbeatInterval.value = setInterval(() => {
if (isConnected.value && ws.value?.readyState === WebSocket.OPEN) {
send({ type: "ping" });
}
}, 30000);
};
// Actions
const connect = async () => {
if (ws.value?.readyState === WebSocket.OPEN || isConnecting.value) {
console.log("WebSocket already connected or connecting");
return;
}
isConnecting.value = true;
clearReconnectTimeout();
try {
const wsUrl = await getWebSocketUrl();
console.log("Connecting to WebSocket:", wsUrl);
ws.value = new WebSocket(wsUrl);
ws.value.onopen = () => {
console.log("WebSocket connected");
isConnected.value = true;
isConnecting.value = false;
reconnectAttempts.value = 0;
startHeartbeat();
// Send queued messages
while (messageQueue.value.length > 0) {
const message = messageQueue.value.shift();
send(message);
}
// Emit connected event
emit("connected", { timestamp: Date.now() });
};
ws.value.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
console.log("WebSocket message received:", data);
handleMessage(data);
} catch (error) {
console.error("Failed to parse WebSocket message:", error);
}
};
ws.value.onerror = (error) => {
console.error("WebSocket error:", error);
isConnecting.value = false;
};
ws.value.onclose = (event) => {
console.log("WebSocket closed:", event.code, event.reason);
isConnected.value = false;
isConnecting.value = false;
clearHeartbeat();
// Emit disconnected event
emit("disconnected", {
code: event.code,
reason: event.reason,
timestamp: Date.now(),
});
// Attempt to reconnect
if (!event.wasClean && canReconnect.value) {
scheduleReconnect();
}
};
} catch (error) {
console.error("Failed to create WebSocket connection:", error);
isConnecting.value = false;
}
};
const disconnect = () => {
clearHeartbeat();
clearReconnectTimeout();
reconnectAttempts.value = maxReconnectAttempts.value; // Prevent auto-reconnect
if (ws.value) {
ws.value.close(1000, "Client disconnect");
ws.value = null;
}
isConnected.value = false;
isConnecting.value = false;
};
const scheduleReconnect = () => {
if (!canReconnect.value) {
console.log("Max reconnect attempts reached");
toast.error("Lost connection to server. Please refresh the page.");
return;
}
reconnectAttempts.value++;
const delay =
reconnectDelay.value * Math.pow(2, reconnectAttempts.value - 1);
console.log(
`Reconnecting in ${delay}ms (attempt ${reconnectAttempts.value}/${maxReconnectAttempts.value})`
);
clearReconnectTimeout();
reconnectTimeout.value = setTimeout(() => {
connect();
}, delay);
};
const send = (message) => {
if (!ws.value || ws.value.readyState !== WebSocket.OPEN) {
console.warn("WebSocket not connected, queueing message:", message);
messageQueue.value.push(message);
return false;
}
try {
const payload =
typeof message === "string" ? message : JSON.stringify(message);
ws.value.send(payload);
return true;
} catch (error) {
console.error("Failed to send WebSocket message:", error);
return false;
}
};
const handleMessage = (data) => {
const { type, data: payload, timestamp } = data;
switch (type) {
case "connected":
console.log("Server confirmed connection:", payload);
break;
case "pong":
// Heartbeat response
break;
case "notification":
if (payload?.message) {
toast.info(payload.message);
}
break;
case "balance_update":
// Update user balance
const authStore = useAuthStore();
if (payload?.balance !== undefined) {
authStore.updateBalance(payload.balance);
}
break;
case "item_sold":
toast.success(
`Your item "${payload?.itemName || "item"}" has been sold!`
);
break;
case "item_purchased":
toast.success(
`Successfully purchased "${payload?.itemName || "item"}"!`
);
break;
case "trade_status":
if (payload?.status === "completed") {
toast.success("Trade completed successfully!");
} else if (payload?.status === "failed") {
toast.error(`Trade failed: ${payload?.reason || "Unknown error"}`);
}
break;
case "price_update":
case "listing_update":
case "market_update":
// These will be handled by listeners
break;
case "announcement":
if (payload?.message) {
toast.warning(payload.message, { timeout: 10000 });
}
break;
case "error":
console.error("Server error:", payload);
if (payload?.message) {
toast.error(payload.message);
}
break;
default:
console.log("Unhandled message type:", type);
}
// Emit to listeners
emit(type, payload);
};
const on = (event, callback) => {
if (!listeners.value.has(event)) {
listeners.value.set(event, []);
}
listeners.value.get(event).push(callback);
// Return unsubscribe function
return () => off(event, callback);
};
const off = (event, callback) => {
if (!listeners.value.has(event)) return;
const callbacks = listeners.value.get(event);
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
if (callbacks.length === 0) {
listeners.value.delete(event);
}
};
const emit = (event, data) => {
if (!listeners.value.has(event)) return;
const callbacks = listeners.value.get(event);
callbacks.forEach((callback) => {
try {
callback(data);
} catch (error) {
console.error(`Error in event listener for "${event}":`, error);
}
});
};
const once = (event, callback) => {
const wrappedCallback = (data) => {
callback(data);
off(event, wrappedCallback);
};
return on(event, wrappedCallback);
};
const clearListeners = () => {
listeners.value.clear();
};
// Ping the server
const ping = () => {
send({ type: "ping" });
};
return {
// State
ws,
isConnected,
isConnecting,
reconnectAttempts,
maxReconnectAttempts,
messageQueue,
// Computed
connectionStatus,
canReconnect,
// Actions
connect,
disconnect,
send,
on,
off,
once,
emit,
clearListeners,
ping,
};
});