mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-12 14:40:22 +08:00
feat(core): improve reconnect handling (#15458)
#### PR Dependency Tree * **PR #15458** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved realtime connection recovery with progressively longer retry delays, capped to prevent excessive waiting. * Connection retries now reset after successful reconnection and stop cleanly when no longer needed. * Reduced false error alerts by confirming repeated failures before notifying users. * Added cancellation handling when connection context changes or the service is closed. * **User Experience** * Realtime connection alerts now identify authentication, network, server, and timeout issues with localized messages. * Active alerts are automatically dismissed when the connection recovers. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -150,13 +150,23 @@ export const WorkspaceSideEffects = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!realtimeConnectionError) return;
|
||||
notify.warning(
|
||||
const message = {
|
||||
authentication:
|
||||
t['com.affine.realtime.connection-error.authentication'](),
|
||||
network: t['com.affine.realtime.connection-error.network'](),
|
||||
server: t['com.affine.realtime.connection-error.server'](),
|
||||
timeout: t['com.affine.realtime.connection-error.timeout'](),
|
||||
}[realtimeConnectionError.type];
|
||||
const id = notify.warning(
|
||||
{
|
||||
title: t['com.affine.realtime.connection-error.title'](),
|
||||
message: t['com.affine.realtime.connection-error.message'](),
|
||||
message,
|
||||
},
|
||||
{ id: `realtime-connection-error:${realtimeConnectionError.endpoint}` }
|
||||
);
|
||||
return () => {
|
||||
notify.dismiss(id);
|
||||
};
|
||||
}, [realtimeConnectionError, t]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -8,13 +8,62 @@ import type { NbstoreService } from '../../storage';
|
||||
import type { Server } from '../entities/server';
|
||||
import type { ServersService } from './servers';
|
||||
|
||||
const CONNECTION_FAILURE_THRESHOLD = 3;
|
||||
const INITIAL_RETRY_DELAY = 3000;
|
||||
const MAX_RETRY_DELAY = 60000;
|
||||
|
||||
function getConnectionErrorType(error: unknown) {
|
||||
if (!(error instanceof Error)) {
|
||||
return 'server';
|
||||
}
|
||||
|
||||
const detail = `${error.name} ${error.message}`.toLowerCase();
|
||||
if (
|
||||
detail.includes('auth') ||
|
||||
detail.includes('forbidden') ||
|
||||
detail.includes('unauthorized') ||
|
||||
detail.includes('jwt')
|
||||
) {
|
||||
return 'authentication';
|
||||
}
|
||||
if (detail.includes('timeout') || detail.includes('timed out')) {
|
||||
return 'timeout';
|
||||
}
|
||||
if (
|
||||
detail.includes('network') ||
|
||||
detail.includes('transport') ||
|
||||
detail.includes('websocket') ||
|
||||
detail.includes('xhr') ||
|
||||
detail.includes('connect')
|
||||
) {
|
||||
return 'network';
|
||||
}
|
||||
return 'server';
|
||||
}
|
||||
|
||||
function waitForRetry(delay: number, signal: AbortSignal) {
|
||||
return new Promise<void>(resolve => {
|
||||
const onAbort = () => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
};
|
||||
const timeout = setTimeout(() => {
|
||||
signal.removeEventListener('abort', onAbort);
|
||||
resolve();
|
||||
}, delay);
|
||||
signal.addEventListener('abort', onAbort, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
@OnEvent(ApplicationStarted, service => service.onApplicationStarted)
|
||||
export class RealtimeService extends Service {
|
||||
readonly connectionError$ = new LiveData<{
|
||||
endpoint: string;
|
||||
error: unknown;
|
||||
type: ReturnType<typeof getConnectionErrorType>;
|
||||
} | null>(null);
|
||||
private contextGeneration = 0;
|
||||
private probeAbort?: AbortController;
|
||||
|
||||
private readonly currentServer$ =
|
||||
this.globalContextService.globalContext.serverId.$.selector(id =>
|
||||
@@ -50,11 +99,17 @@ export class RealtimeService extends Service {
|
||||
|
||||
const subscription = this.currentServer$.subscribe(context => {
|
||||
const generation = ++this.contextGeneration;
|
||||
this.configure(context, generation).catch(error => {
|
||||
this.probeAbort?.abort();
|
||||
const probeAbort = new AbortController();
|
||||
this.probeAbort = probeAbort;
|
||||
this.configure(context, generation, probeAbort.signal).catch(error => {
|
||||
console.error('Failed to configure realtime context', error);
|
||||
});
|
||||
});
|
||||
this.disposables.push(() => subscription.unsubscribe());
|
||||
this.disposables.push(() => {
|
||||
subscription.unsubscribe();
|
||||
this.probeAbort?.abort();
|
||||
});
|
||||
}
|
||||
|
||||
onApplicationStarted() {}
|
||||
@@ -65,7 +120,8 @@ export class RealtimeService extends Service {
|
||||
authenticated: boolean;
|
||||
isSelfHosted: boolean;
|
||||
},
|
||||
generation: number
|
||||
generation: number,
|
||||
signal: AbortSignal
|
||||
) {
|
||||
await this.nbstoreService.realtime.configure(context);
|
||||
if (generation !== this.contextGeneration) return;
|
||||
@@ -74,19 +130,34 @@ export class RealtimeService extends Service {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.nbstoreService.realtime.request(
|
||||
'user.profile.get',
|
||||
{},
|
||||
{ timeoutMs: 10_000 }
|
||||
);
|
||||
if (generation === this.contextGeneration) {
|
||||
this.connectionError$.next(null);
|
||||
}
|
||||
} catch (error) {
|
||||
if (generation === this.contextGeneration) {
|
||||
this.connectionError$.next({ endpoint: context.endpoint, error });
|
||||
let failures = 0;
|
||||
let retryDelay = INITIAL_RETRY_DELAY;
|
||||
while (generation === this.contextGeneration && !signal.aborted) {
|
||||
try {
|
||||
await this.nbstoreService.realtime.request(
|
||||
'user.profile.get',
|
||||
{},
|
||||
{ timeoutMs: 10_000, signal }
|
||||
);
|
||||
if (generation === this.contextGeneration && !signal.aborted) {
|
||||
this.connectionError$.next(null);
|
||||
}
|
||||
return;
|
||||
} catch (error) {
|
||||
if (signal.aborted || generation !== this.contextGeneration) {
|
||||
return;
|
||||
}
|
||||
failures++;
|
||||
if (failures >= CONNECTION_FAILURE_THRESHOLD) {
|
||||
this.connectionError$.next({
|
||||
endpoint: context.endpoint,
|
||||
error,
|
||||
type: getConnectionErrorType(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
await waitForRetry(retryDelay, signal);
|
||||
retryDelay = Math.min(retryDelay * 2, MAX_RETRY_DELAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user