All checks were successful
Build Frontend / Build Frontend (push) Successful in 23s
- 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
382 lines
9.5 KiB
JavaScript
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,
|
|
};
|
|
});
|