e2ee-chat
Version:
A simple realtime chat SDK for web and mobile apps using socket.io with support for end-to-end encryption and multi-tenant backend integration.
221 lines (186 loc) • 5.99 kB
JavaScript
const { io } = require("socket.io-client");
const axios = require("axios");
let socket = null;
let presenceCallbacks = new Set();
let messageSeenCallbacks = new Set();
function connectSocket({
serverUrl,
roomId,
userId,
userType,
onJoin,
onMessage,
onHandlerChanged,
onPresenceChange,
onMessageSeen,
apiKey,
}) {
console.log("Initializing socket connection to:", serverUrl);
// Clean the server URL by removing any query parameters
const cleanUrl = serverUrl.split("?")[0];
socket = io(cleanUrl, {
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
query: { apiKey },
transports: ["polling"], // Use polling only for better HTTPS compatibility
timeout: 20000, // Increase timeout to 20 seconds
forceNew: true, // Force a new connection
path: "/socket.io/", // Explicitly set the Socket.IO path
});
socket.on("connect", () => {
console.log("Socket connected, joining room:", {
roomId,
userId,
userType,
});
socket.emit("joinRoom", { roomId, userId, userType, apiKey });
// Emit user online status
socket.emit("user:online", userId);
});
socket.on("connect_error", (error) => {
console.error("Socket connection error:", error.message);
});
socket.on("error", (error) => {
console.error("Socket error:", error);
});
socket.on("joinedRoom", (data) => {
console.log("Successfully joined room:", data);
onJoin && onJoin(data);
});
socket.on("receiveMessage", (msg) => {
console.log("Received message:", msg);
onMessage && onMessage(msg);
});
socket.on("handlerChanged", (payload) => {
console.log("Handler changed:", payload);
onHandlerChanged && onHandlerChanged(payload);
});
socket.on("unauthorized", (msg) => {
console.error("Socket unauthorized:", msg);
});
// Add presence change handler
socket.on("presence:change", (data) => {
console.log("Presence change:", data);
presenceCallbacks.forEach((callback) => callback(data));
onPresenceChange && onPresenceChange(data);
});
// Add message seen handler
socket.on("message:seen", (data) => {
console.log("Message seen:", data);
messageSeenCallbacks.forEach((callback) => callback(data));
onMessageSeen && onMessageSeen(data);
});
socket.on("disconnect", (reason) => {
console.log("Socket disconnected:", reason);
if (reason === "io server disconnect") {
// Server initiated disconnect, try to reconnect
socket.connect();
}
});
// Add ping/pong handlers to monitor connection health
socket.on("ping", () => {
console.log("Ping sent");
});
socket.on("pong", (latency) => {
console.log("Pong received, latency:", latency);
});
}
function sendMessage({ roomId, senderId, receiverId, encryptedText, apiKey }) {
if (!socket) return;
socket.emit("sendMessage", {
roomId,
senderId,
receiverId,
encryptedText,
apiKey,
});
}
function requestTakeover({ roomId, newHandlerId, apiKey }) {
if (!socket) return;
socket.emit("takeover", { roomId, newHandlerId, apiKey });
}
function disconnectSocket() {
if (socket) socket.disconnect();
}
function markMessageAsSeen({ messageId, userId, roomId, apiKey }) {
if (!socket) return;
socket.emit("message:seen", { messageId, userId, roomId, apiKey });
}
function subscribeToPresence(callback) {
presenceCallbacks.add(callback);
return () => presenceCallbacks.delete(callback);
}
function subscribeToMessageSeen(callback) {
messageSeenCallbacks.add(callback);
return () => messageSeenCallbacks.delete(callback);
}
function markMessagesAsRead({ roomId, userId, apiKey }) {
if (!socket) return;
socket.emit("markMessagesAsRead", { roomId, userId, apiKey });
}
// async function getUnreadCounts(serverUrl, userId, apiKey) {
// const url = `${serverUrl}/unread-counts/${userId}?apiKey=${apiKey}`;
// const response = await axios.get(url);
// return response.data;
// }
// // In socket.js, update the getUnreadCounts function:
// async function getUnreadCounts(serverUrl, userId, apiKey) {
// try {
// const cleanUrl = serverUrl.split("?")[0];
// const url = `${cleanUrl}/unread-counts/${userId}?apiKey=${apiKey}`;
// console.log("SDK: Fetching unread counts from:", url);
// const response = await axios.get(url);
// console.log("SDK: Raw unread counts response:", response.data);
// // The server should return data in this format:
// // { counts: { roomId1: 2, roomId2: 1, ... } }
// if (response.data && response.data.counts) {
// return response.data.counts;
// }
// // If the server returns a different format, handle it here
// if (response.data && typeof response.data === "object") {
// return response.data;
// }
// console.warn(
// "SDK: Unexpected unread counts response format:",
// response.data
// );
// return {};
// } catch (error) {
// console.error("SDK: Failed to fetch unread counts:", error);
// return {};
// }
// }
async function getUnreadCounts(serverUrl, userId, apiKey) {
try {
// Add validation for required parameters
if (!serverUrl || !userId || !apiKey) {
console.error("SDK: Missing required parameters:", {
serverUrl,
userId,
apiKey,
});
return {};
}
const cleanUrl = serverUrl.split("?")[0];
const url = `${cleanUrl}/unread-counts/${userId}?apiKey=${apiKey}`;
console.log("SDK: Fetching unread counts from:", url);
const response = await axios.get(url);
console.log("SDK: Raw unread counts response:", response.data);
return response.data || {};
} catch (error) {
console.error("SDK: Failed to fetch unread counts:", error);
return {};
}
}
module.exports = {
connectSocket,
sendMessage,
requestTakeover,
disconnectSocket,
markMessageAsSeen,
subscribeToPresence,
subscribeToMessageSeen,
markMessagesAsRead,
getUnreadCounts,
};