feat(realtime): long polling for app-wide events behind proxies

When a proxy lets neither WebSocket nor SSE through, the app opens a
poll connection: the server buffers its events (an SSEClient without a
stream), each GET waits up to 25 s, and room joins work like for SSE.
Tasks and lists now update live there too, and note sync takes its
notifications from it instead of a second long poll. Idle connections
close after two minutes. The profile shows such a connection as limited.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-11-02 11:50:27 +01:00
1 parent c19d8fd03e
commit 2b7356d043
16 files changed
+369 -20

No files matched your search

+7 -5
View File
@@ -6,9 +6,9 @@ export const SERVER_OFFLINE_GRACE_MS = 4000;
// How often the server is asked while the realtime connection is missing
export const SERVER_PROBE_INTERVAL_MS = 15000;
// online: realtime connection (WebSocket/SSE) is up; limited: the server answers plain requests, but no
// realtime connection gets through (e.g. a proxy that buffers event streams; notes fall back to long
// polling); offline: no server at all
// online: realtime connection (WebSocket/SSE) is up; limited: changes only come by long polling (a proxy
// that buffers event streams), or not live at all while the server still answers plain requests;
// offline: no server at all
export type ConnectionState = 'online' | 'limited' | 'offline';
async function serverReachable(): Promise<boolean> {
@@ -29,6 +29,7 @@ export function useConnectionState(): ConnectionState {
useEffect(() => {
let connected = false;
let viaPoll = false;
let disposed = false;
let timer: number | undefined;
const stopProbing = () => {
@@ -45,7 +46,7 @@ export function useConnectionState(): ConnectionState {
const update = () => {
if (connected) {
stopProbing();
setState('online');
setState(viaPoll ? 'limited' : 'online');
} else if (navigator.onLine === false) {
stopProbing();
setState('offline');
@@ -53,8 +54,9 @@ export function useConnectionState(): ConnectionState {
timer = window.setTimeout(probe, SERVER_OFFLINE_GRACE_MS);
}
};
const unsubscribe = onRealtimeStatus((isConnected) => {
const unsubscribe = onRealtimeStatus((isConnected, transport) => {
connected = isConnected;
viaPoll = transport === 'poll';
update();
});
window.addEventListener('online', update);