import { ref } from 'vue'; import { getConfig } from '../utils/config'; export const REALTIME_KEY = Symbol('commRealtime'); function isRealtimeEnabled() { const app = window.APP || {}; return typeof window.Echo !== 'undefined' && (app.PUSHER_ENABLED === true || app.PUSHER_ENABLED === '1' || app.PUSHER_ENABLED === 1); } /** * Shared Laravel Echo service: multi-room subscriptions, whispers, user channel. */ export function useRealtime() { const connected = ref(false); const config = getConfig(); const businessId = config.businessId; const userId = config.userId; /** @type {Map} */ const subscribedRooms = new Map(); let userChannelName = null; let connectionBound = false; const handlers = new Set(); function register(handler) { handlers.add(handler); return () => handlers.delete(handler); } function notify(method, ...args) { handlers.forEach((handler) => handler[method]?.(...args)); } function roomChannel(roomId) { return `business.${businessId}.chat.${roomId}`; } function userChannel() { return `business.${businessId}.user.${userId}`; } function subscribeRoom(roomId) { if (!isRealtimeEnabled() || !roomId) return; const id = parseInt(roomId, 10); if (subscribedRooms.has(id)) return; const channelName = roomChannel(id); const channel = window.Echo.private(channelName); channel .listen('.message.sent', (payload) => notify('onMessage', payload, id)) .listen('.user.typing', (payload) => notify('onTyping', payload, id)) .listenForWhisper('typing', (payload) => { notify('onTyping', { ...payload, room_id: payload.room_id ?? id }, id); }); subscribedRooms.set(id, { name: channelName, channel }); } function unsubscribeRoom(roomId) { const id = parseInt(roomId, 10); const entry = subscribedRooms.get(id); if (!entry || typeof window.Echo === 'undefined') return; window.Echo.leave(entry.name); subscribedRooms.delete(id); } function syncRoomSubscriptions(roomIds) { if (!isRealtimeEnabled()) return; const next = new Set((roomIds || []).map((id) => parseInt(id, 10))); subscribedRooms.forEach((_, id) => { if (!next.has(id)) unsubscribeRoom(id); }); next.forEach((id) => subscribeRoom(id)); } function subscribeUserChannel() { if (!isRealtimeEnabled() || userChannelName) return; userChannelName = userChannel(); window.Echo.private(userChannelName) .listen('.meeting.invite', (payload) => notify('onMeetingInvite', payload)); } /** Send typing via WebSocket whisper (no HTTP). */ function whisperTyping(roomId, extra = {}) { if (!isRealtimeEnabled()) return false; const id = parseInt(roomId, 10); const entry = subscribedRooms.get(id); if (!entry?.channel?.whisper) return false; entry.channel.whisper('typing', { room_id: id, user_id: userId, user_name: config.userName || extra.user_name || 'User', ...extra, }); return true; } function bindConnection() { if (!isRealtimeEnabled() || connectionBound || !window.Echo?.connector?.pusher) return; connectionBound = true; const conn = window.Echo.connector.pusher.connection; const onConnected = () => { connected.value = true; notify('onConnected'); }; const onDisconnected = () => { connected.value = false; notify('onDisconnected'); }; conn.bind('connected', onConnected); conn.bind('disconnected', onDisconnected); conn.bind('unavailable', onDisconnected); conn.bind('failed', onDisconnected); if (conn.state === 'connected') { onConnected(); } } function cleanup() { if (typeof window.Echo === 'undefined') return; subscribedRooms.forEach((entry) => window.Echo.leave(entry.name)); subscribedRooms.clear(); if (userChannelName) { window.Echo.leave(userChannelName); userChannelName = null; } handlers.clear(); connectionBound = false; } return { connected, register, isRealtimeEnabled, bindConnection, syncRoomSubscriptions, subscribeUserChannel, whisperTyping, cleanup, }; }