fix(collab): fix corporate SSE fallback, realtime lock propagation and prevent stale overwrite
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m1s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m1s
This commit is contained in:
1 parent
996f27133c
commit
a1c7a65d84
5 files changed
+238
-63
No files matched your search
+49
-27
@@ -650,17 +650,22 @@ export const api = {
|
||||
},
|
||||
|
||||
async lockBlock(docId: string, blockId: string, userColor?: string, ttlSeconds: number = 30): Promise<BlockLockInfo> {
|
||||
console.log(`[MindSift Lock] Lock anfordern für Block "${blockId}" in Notiz "${docId}"...`);
|
||||
const res = await request<any>(`/api/v1/documents/${docId}/blocks/${encodeURIComponent(blockId)}/lock`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ userColor, ttlSeconds }),
|
||||
body: JSON.stringify({ userColor, ttlSeconds, ttl: ttlSeconds }),
|
||||
});
|
||||
console.log(`[MindSift Lock] Lock erfolgreich erhalten für "${blockId}":`, res?.lock || res);
|
||||
return res?.lock || res;
|
||||
},
|
||||
|
||||
async unlockBlock(docId: string, blockId: string): Promise<{ status: string }> {
|
||||
return request<{ status: string }>(`/api/v1/documents/${docId}/blocks/${encodeURIComponent(blockId)}/unlock`, {
|
||||
console.log(`[MindSift Lock] Lock freigeben für Block "${blockId}" in Notiz "${docId}"...`);
|
||||
const res = await request<{ status: string }>(`/api/v1/documents/${docId}/blocks/${encodeURIComponent(blockId)}/unlock`, {
|
||||
method: 'POST',
|
||||
});
|
||||
console.log(`[MindSift Lock] Lock freigegeben für "${blockId}"`);
|
||||
return res;
|
||||
},
|
||||
|
||||
async startLiveSession(docId: string): Promise<LiveSessionInfo> {
|
||||
@@ -1140,7 +1145,7 @@ export function onRealtimeStatus(callback: (connected: boolean, transport: Realt
|
||||
};
|
||||
}
|
||||
|
||||
let isWsBlockedByProxy = false;
|
||||
let isWsBlockedByProxy = typeof window !== 'undefined' && sessionStorage.getItem('mindsift_ws_blocked') === 'true';
|
||||
let sseReconnectTimer: any = null;
|
||||
let sseReconnectDelay = 2000;
|
||||
const globalSubscribedRooms = new Set<string>();
|
||||
@@ -1148,6 +1153,7 @@ const globalSubscribedRooms = new Set<string>();
|
||||
function notifyRealtimeStatusChange(connected: boolean, transport: RealtimeTransport) {
|
||||
isRealtimeConnectedState = connected;
|
||||
activeTransport = transport;
|
||||
console.log(`[MindSift Realtime] Verbindungsstatus geändert: connected=${connected}, transport=${transport}`);
|
||||
realtimeStatusListeners.forEach((fn) => {
|
||||
try {
|
||||
fn(connected, transport);
|
||||
@@ -1158,6 +1164,25 @@ function notifyRealtimeStatusChange(connected: boolean, transport: RealtimeTrans
|
||||
window.dispatchEvent(new CustomEvent('mindsift:ws:status', { detail: { connected, transport } }));
|
||||
}
|
||||
|
||||
function switchToSSE(reason: string) {
|
||||
console.warn(`[MindSift Realtime] ${reason} -> Schalte sofort und dauerhaft auf Server-Sent Events (SSE) um.`);
|
||||
isWsBlockedByProxy = true;
|
||||
try {
|
||||
sessionStorage.setItem('mindsift_ws_blocked', 'true');
|
||||
} catch {}
|
||||
clearTimeout(wsConnectionWatchdog);
|
||||
clearTimeout(wsReconnectTimer);
|
||||
if (activeGlobalWS) {
|
||||
try {
|
||||
activeGlobalWS.onclose = null;
|
||||
activeGlobalWS.onerror = null;
|
||||
activeGlobalWS.close();
|
||||
} catch {}
|
||||
activeGlobalWS = null;
|
||||
}
|
||||
connectSSE();
|
||||
}
|
||||
|
||||
export function connectSSE(rooms?: string[]): void {
|
||||
if (rooms) {
|
||||
rooms.forEach((r) => globalSubscribedRooms.add(r));
|
||||
@@ -1173,15 +1198,17 @@ export function connectSSE(rooms?: string[]): void {
|
||||
|
||||
try {
|
||||
const url = buildSSEURL(undefined, undefined, Array.from(globalSubscribedRooms));
|
||||
console.log(`[MindSift Realtime] Verbinde SSE zu ${url}...`);
|
||||
const es = new EventSource(url);
|
||||
activeGlobalSSE = es;
|
||||
|
||||
es.onopen = () => {
|
||||
console.info('[MindSift Realtime] Server-Sent Events (SSE) verbunden (Proxy-Fallback aktiv).');
|
||||
console.info('[MindSift Realtime] Server-Sent Events (SSE) erfolgreich verbunden (Corporate Fallback aktiv).');
|
||||
sseReconnectDelay = 2000;
|
||||
notifyRealtimeStatusChange(true, 'sse');
|
||||
if (activeSSEId && globalSubscribedRooms.size > 0) {
|
||||
globalSubscribedRooms.forEach((room) => {
|
||||
console.log(`[MindSift Realtime] SSE Rejoin Room "${room}"`);
|
||||
api.joinSSERoom(activeSSEId!, room, 'join').catch(() => {});
|
||||
});
|
||||
}
|
||||
@@ -1190,10 +1217,13 @@ export function connectSSE(rooms?: string[]): void {
|
||||
es.onmessage = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data) as WSMessage;
|
||||
console.log('[MindSift Realtime Event] [SSE]', msg.event, msg.payload);
|
||||
if (msg.event === 'SSE_CONNECTED') {
|
||||
activeSSEId = msg.payload?.sseId || null;
|
||||
console.log('[MindSift Realtime] SSE-Client registriert mit ID:', activeSSEId);
|
||||
if (activeSSEId && globalSubscribedRooms.size > 0) {
|
||||
globalSubscribedRooms.forEach((room) => {
|
||||
console.log(`[MindSift Realtime] Trete Raum per SSE bei: "${room}"`);
|
||||
api.joinSSERoom(activeSSEId!, room, 'join').catch(() => {});
|
||||
});
|
||||
}
|
||||
@@ -1212,6 +1242,7 @@ export function connectSSE(rooms?: string[]): void {
|
||||
};
|
||||
|
||||
es.onerror = (e) => {
|
||||
console.warn('[MindSift Realtime] SSE Verbindungsfehler:', e);
|
||||
if (activeTransport === 'sse') {
|
||||
notifyRealtimeStatusChange(false, 'none');
|
||||
}
|
||||
@@ -1223,11 +1254,7 @@ export function connectSSE(rooms?: string[]): void {
|
||||
sseReconnectDelay = Math.min(30000, Math.round(sseReconnectDelay * 1.5));
|
||||
sseReconnectTimer = setTimeout(() => {
|
||||
if (!manualDisconnect && getToken()) {
|
||||
if (isWsBlockedByProxy) {
|
||||
connectSSE();
|
||||
} else {
|
||||
connectWebSocket();
|
||||
}
|
||||
connectSSE();
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
@@ -1245,6 +1272,7 @@ export function connectWebSocket(): void {
|
||||
|
||||
// If proxy blocked WebSocket previously, do not attempt failing WebSocket; stay on SSE
|
||||
if (isWsBlockedByProxy) {
|
||||
console.info('[MindSift Realtime] WebSocket im Firmennetzwerk blockiert (Cache aktiv). Verwende direkt SSE.');
|
||||
connectSSE();
|
||||
return;
|
||||
}
|
||||
@@ -1259,22 +1287,20 @@ export function connectWebSocket(): void {
|
||||
|
||||
try {
|
||||
const url = buildWebSocketURL();
|
||||
console.log(`[MindSift Realtime] Versuche WebSocket-Verbindung zu ${url}...`);
|
||||
const ws = new WebSocket(url);
|
||||
activeGlobalWS = ws;
|
||||
|
||||
// 3.5s watchdog for corporate proxies silently stripping Upgrade header
|
||||
// 2.5s watchdog for corporate proxies silently stripping Upgrade header or hanging
|
||||
wsConnectionWatchdog = setTimeout(() => {
|
||||
if (ws && ws.readyState !== WebSocket.OPEN) {
|
||||
console.warn('[MindSift Realtime] WebSocket connection timeout (Proxy Upgrade Block). Wechsle automatisch zu SSE...');
|
||||
isWsBlockedByProxy = true;
|
||||
try { ws.close(); } catch {}
|
||||
activeGlobalWS = null;
|
||||
connectSSE();
|
||||
switchToSSE('WebSocket Timeout nach 2.5s (Proxy Upgrade Header blockiert)');
|
||||
}
|
||||
}, 3500);
|
||||
}, 2500);
|
||||
|
||||
ws.onopen = () => {
|
||||
clearTimeout(wsConnectionWatchdog);
|
||||
console.info('[MindSift Realtime] WebSocket erfolgreich verbunden!');
|
||||
if (activeGlobalSSE) {
|
||||
try { activeGlobalSSE.close(); } catch {}
|
||||
activeGlobalSSE = null;
|
||||
@@ -1284,6 +1310,7 @@ export function connectWebSocket(): void {
|
||||
if (globalSubscribedRooms.size > 0) {
|
||||
globalSubscribedRooms.forEach((room) => {
|
||||
try {
|
||||
console.log(`[MindSift Realtime] Trete Raum per WS bei: "${room}"`);
|
||||
ws.send(JSON.stringify({ event: 'JOIN_ROOM', room }));
|
||||
} catch {}
|
||||
});
|
||||
@@ -1293,6 +1320,7 @@ export function connectWebSocket(): void {
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data) as WSMessage;
|
||||
console.log('[MindSift Realtime Event] [WS]', msg.event, msg.payload);
|
||||
wsListeners.forEach((fn) => {
|
||||
try {
|
||||
fn(msg);
|
||||
@@ -1310,11 +1338,9 @@ export function connectWebSocket(): void {
|
||||
clearTimeout(wsConnectionWatchdog);
|
||||
activeGlobalWS = null;
|
||||
|
||||
const isProxyIssue = evt?.code === 1006 || evt?.code === 1002 || evt?.code === 1008;
|
||||
if (isProxyIssue && !manualDisconnect && getToken()) {
|
||||
console.warn('[MindSift Realtime] Proxy/Firewall Inkompatibilität erkannt (Code ' + evt.code + '). Wechsle dauerhaft zu SSE...');
|
||||
isWsBlockedByProxy = true;
|
||||
connectSSE();
|
||||
// In corporate environments, blocked WebSockets close abruptly without clean handshake
|
||||
if (!evt.wasClean || evt.code === 1006 || evt.code === 1002 || evt.code === 1008 || evt.code === 1005) {
|
||||
switchToSSE(`WebSocket unsauber beendet (Code ${evt.code}, wasClean=${evt.wasClean})`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1336,14 +1362,10 @@ export function connectWebSocket(): void {
|
||||
|
||||
ws.onerror = (e) => {
|
||||
clearTimeout(wsConnectionWatchdog);
|
||||
try {
|
||||
ws.close();
|
||||
} catch {}
|
||||
switchToSSE('WebSocket Fehler (Handshake/Verbindung verweigert)');
|
||||
};
|
||||
} catch (e) {
|
||||
clearTimeout(wsConnectionWatchdog);
|
||||
isWsBlockedByProxy = true;
|
||||
connectSSE();
|
||||
switchToSSE('WebSocket Exception beim Initialisieren');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user