refactor: decompose CrmWorkspaceApp.vue into 15 composables
Split the 6000+ line monolithic component into modular composables: - crm-types.ts: shared types and utility functions - useAuth, useContacts, useContactInboxes, useCalendar, useDeals, useDocuments, useFeed, useTimeline, usePilotChat, useCallAudio, usePins, useChangeReview, useCrmRealtime, useWorkspaceRouting CrmWorkspaceApp.vue is now a thin orchestrator (~2500 lines) that wires composables together with glue code, keeping template and styles intact. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
130
frontend/app/composables/useCrmRealtime.ts
Normal file
130
frontend/app/composables/useCrmRealtime.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { ref } from "vue";
|
||||
|
||||
export function useCrmRealtime(opts: {
|
||||
isAuthenticated: () => boolean;
|
||||
onDashboardChanged: () => Promise<void>;
|
||||
}) {
|
||||
const crmRealtimeState = ref<"idle" | "connecting" | "open" | "error">("idle");
|
||||
let crmRealtimeSocket: WebSocket | null = null;
|
||||
let crmRealtimeReconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let crmRealtimeRefreshTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let crmRealtimeRefreshInFlight = false;
|
||||
let crmRealtimeReconnectAttempt = 0;
|
||||
|
||||
function clearCrmRealtimeReconnectTimer() {
|
||||
if (!crmRealtimeReconnectTimer) return;
|
||||
clearTimeout(crmRealtimeReconnectTimer);
|
||||
crmRealtimeReconnectTimer = null;
|
||||
}
|
||||
|
||||
function clearCrmRealtimeRefreshTimer() {
|
||||
if (!crmRealtimeRefreshTimer) return;
|
||||
clearTimeout(crmRealtimeRefreshTimer);
|
||||
crmRealtimeRefreshTimer = null;
|
||||
}
|
||||
|
||||
async function runCrmRealtimeRefresh() {
|
||||
if (!opts.isAuthenticated() || crmRealtimeRefreshInFlight) return;
|
||||
crmRealtimeRefreshInFlight = true;
|
||||
try {
|
||||
await opts.onDashboardChanged();
|
||||
} catch {
|
||||
// ignore transient realtime refresh errors
|
||||
} finally {
|
||||
crmRealtimeRefreshInFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleCrmRealtimeRefresh(delayMs = 250) {
|
||||
clearCrmRealtimeRefreshTimer();
|
||||
crmRealtimeRefreshTimer = setTimeout(() => {
|
||||
crmRealtimeRefreshTimer = null;
|
||||
void runCrmRealtimeRefresh();
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
function scheduleCrmRealtimeReconnect() {
|
||||
clearCrmRealtimeReconnectTimer();
|
||||
const attempt = Math.min(crmRealtimeReconnectAttempt + 1, 8);
|
||||
crmRealtimeReconnectAttempt = attempt;
|
||||
const delayMs = Math.min(1000 * 2 ** (attempt - 1), 15000);
|
||||
crmRealtimeReconnectTimer = setTimeout(() => {
|
||||
crmRealtimeReconnectTimer = null;
|
||||
startCrmRealtime();
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
function stopCrmRealtime() {
|
||||
clearCrmRealtimeReconnectTimer();
|
||||
clearCrmRealtimeRefreshTimer();
|
||||
|
||||
if (crmRealtimeSocket) {
|
||||
const socket = crmRealtimeSocket;
|
||||
crmRealtimeSocket = null;
|
||||
socket.onopen = null;
|
||||
socket.onmessage = null;
|
||||
socket.onerror = null;
|
||||
socket.onclose = null;
|
||||
try {
|
||||
socket.close(1000, "client stop");
|
||||
} catch {
|
||||
// ignore socket close errors
|
||||
}
|
||||
}
|
||||
|
||||
crmRealtimeState.value = "idle";
|
||||
}
|
||||
|
||||
function startCrmRealtime() {
|
||||
if (process.server || !opts.isAuthenticated()) return;
|
||||
if (crmRealtimeSocket) {
|
||||
const state = crmRealtimeSocket.readyState;
|
||||
if (state === WebSocket.OPEN || state === WebSocket.CONNECTING) return;
|
||||
}
|
||||
|
||||
clearCrmRealtimeReconnectTimer();
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
const url = `${protocol}//${window.location.host}/ws/crm-updates`;
|
||||
|
||||
const socket = new WebSocket(url);
|
||||
crmRealtimeSocket = socket;
|
||||
crmRealtimeState.value = "connecting";
|
||||
|
||||
socket.onopen = () => {
|
||||
crmRealtimeState.value = "open";
|
||||
crmRealtimeReconnectAttempt = 0;
|
||||
};
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
const raw = typeof event.data === "string" ? event.data : "";
|
||||
if (!raw) return;
|
||||
try {
|
||||
const payload = JSON.parse(raw) as { type?: string };
|
||||
if (payload.type === "dashboard.changed") {
|
||||
scheduleCrmRealtimeRefresh();
|
||||
}
|
||||
} catch {
|
||||
// ignore malformed realtime payloads
|
||||
}
|
||||
};
|
||||
|
||||
socket.onerror = () => {
|
||||
crmRealtimeState.value = "error";
|
||||
};
|
||||
|
||||
socket.onclose = () => {
|
||||
const wasActive = crmRealtimeSocket === socket;
|
||||
if (wasActive) {
|
||||
crmRealtimeSocket = null;
|
||||
}
|
||||
if (!opts.isAuthenticated()) {
|
||||
crmRealtimeState.value = "idle";
|
||||
return;
|
||||
}
|
||||
crmRealtimeState.value = "error";
|
||||
scheduleCrmRealtimeReconnect();
|
||||
};
|
||||
}
|
||||
|
||||
return { crmRealtimeState, startCrmRealtime, stopCrmRealtime };
|
||||
}
|
||||
Reference in New Issue
Block a user