mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-17 01:56:27 +08:00
223 lines
5.1 KiB
TypeScript
223 lines
5.1 KiB
TypeScript
import EventEmitter2 from 'eventemitter2';
|
|
|
|
import { MANUALLY_STOP } from '../utils/throw-if-aborted';
|
|
|
|
export type ConnectionStatus =
|
|
| 'idle'
|
|
| 'connecting'
|
|
| 'connected'
|
|
| 'error'
|
|
| 'closed';
|
|
|
|
export interface Connection<T = any> {
|
|
readonly status: ConnectionStatus;
|
|
readonly error?: Error;
|
|
readonly inner: T;
|
|
connect(): void;
|
|
disconnect(): void;
|
|
waitForConnected(signal?: AbortSignal): Promise<void>;
|
|
onStatusChanged(
|
|
cb: (status: ConnectionStatus, error?: Error) => void
|
|
): () => void;
|
|
}
|
|
|
|
export abstract class AutoReconnectConnection<T = any>
|
|
implements Connection<T>
|
|
{
|
|
private readonly event = new EventEmitter2({
|
|
maxListeners: 100,
|
|
});
|
|
private _inner: T | undefined = undefined;
|
|
private _status: ConnectionStatus = 'idle';
|
|
private _error: Error | undefined = undefined;
|
|
retryDelay = 3000;
|
|
private refCount = 0;
|
|
private connectingAbort?: AbortController;
|
|
private reconnectingAbort?: AbortController;
|
|
|
|
constructor() {}
|
|
|
|
get shareId(): string | undefined {
|
|
return undefined;
|
|
}
|
|
|
|
get maybeConnection() {
|
|
return this._inner;
|
|
}
|
|
|
|
get inner(): T {
|
|
if (this._inner === undefined) {
|
|
throw new Error(
|
|
`Connection ${this.constructor.name} has not been established.`
|
|
);
|
|
}
|
|
|
|
return this._inner;
|
|
}
|
|
|
|
private set inner(inner: T | undefined) {
|
|
this._inner = inner;
|
|
}
|
|
|
|
get status() {
|
|
return this._status;
|
|
}
|
|
|
|
get error() {
|
|
return this._error;
|
|
}
|
|
|
|
protected set error(error: Error | undefined) {
|
|
this.handleError(error);
|
|
}
|
|
|
|
private setStatus(status: ConnectionStatus, error?: Error) {
|
|
const shouldEmit = status !== this._status || error !== this._error;
|
|
this._status = status;
|
|
// we only clear-up error when status is connected
|
|
if (error || status === 'connected') {
|
|
this._error = error;
|
|
}
|
|
if (shouldEmit) {
|
|
this.emitStatusChanged(status, this._error);
|
|
}
|
|
}
|
|
|
|
protected abstract doConnect(signal?: AbortSignal): Promise<T>;
|
|
protected abstract doDisconnect(conn: T): void;
|
|
|
|
private innerConnect() {
|
|
if (this.status !== 'connecting') {
|
|
this.setStatus('connecting');
|
|
this.connectingAbort = new AbortController();
|
|
const signal = this.connectingAbort.signal;
|
|
this.doConnect(signal)
|
|
.then(value => {
|
|
if (!signal.aborted) {
|
|
this._inner = value;
|
|
this.setStatus('connected');
|
|
} else {
|
|
try {
|
|
this.doDisconnect(value);
|
|
} catch (error) {
|
|
console.error('failed to disconnect', error);
|
|
}
|
|
}
|
|
})
|
|
.catch(error => {
|
|
if (!signal.aborted) {
|
|
console.error('failed to connect', error);
|
|
this.handleError(error as any);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
private innerDisconnect() {
|
|
this.connectingAbort?.abort(MANUALLY_STOP);
|
|
this.reconnectingAbort?.abort(MANUALLY_STOP);
|
|
try {
|
|
if (this._inner) {
|
|
this.doDisconnect(this._inner);
|
|
}
|
|
} catch (error) {
|
|
console.error('failed to disconnect', error);
|
|
}
|
|
this.reconnectingAbort = undefined;
|
|
this.connectingAbort = undefined;
|
|
this._inner = undefined;
|
|
}
|
|
|
|
private handleError(reason?: Error) {
|
|
// on error
|
|
console.error('connection error, will reconnect', reason);
|
|
this.innerDisconnect();
|
|
this.setStatus('error', reason);
|
|
// reconnect
|
|
|
|
this.reconnectingAbort = new AbortController();
|
|
const signal = this.reconnectingAbort.signal;
|
|
setTimeout(() => {
|
|
if (!signal.aborted) {
|
|
this.innerConnect();
|
|
}
|
|
}, this.retryDelay);
|
|
}
|
|
|
|
connect() {
|
|
this.refCount++;
|
|
if (this.refCount === 1) {
|
|
this.innerConnect();
|
|
}
|
|
}
|
|
|
|
disconnect(force?: boolean) {
|
|
if (force) {
|
|
this.refCount = 0;
|
|
} else {
|
|
this.refCount = Math.max(this.refCount - 1, 0);
|
|
}
|
|
if (this.refCount === 0) {
|
|
this.innerDisconnect();
|
|
this.setStatus('closed');
|
|
}
|
|
}
|
|
|
|
waitForConnected(signal?: AbortSignal) {
|
|
return new Promise<void>((resolve, reject) => {
|
|
if (this.status === 'connected') {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
const off = this.onStatusChanged(status => {
|
|
if (status === 'connected') {
|
|
resolve();
|
|
off();
|
|
}
|
|
});
|
|
|
|
signal?.addEventListener('abort', reason => {
|
|
reject(reason);
|
|
off();
|
|
});
|
|
});
|
|
}
|
|
|
|
onStatusChanged(
|
|
cb: (status: ConnectionStatus, error?: Error) => void
|
|
): () => void {
|
|
this.event.on('statusChanged', cb);
|
|
return () => {
|
|
this.event.off('statusChanged', cb);
|
|
};
|
|
}
|
|
|
|
private readonly emitStatusChanged = (
|
|
status: ConnectionStatus,
|
|
error?: Error
|
|
) => {
|
|
this.event.emit('statusChanged', status, error);
|
|
};
|
|
}
|
|
|
|
export class DummyConnection implements Connection<undefined> {
|
|
readonly status: ConnectionStatus = 'connected';
|
|
readonly inner: undefined;
|
|
|
|
connect(): void {
|
|
return;
|
|
}
|
|
disconnect(): void {
|
|
return;
|
|
}
|
|
waitForConnected(_signal?: AbortSignal): Promise<void> {
|
|
return Promise.resolve();
|
|
}
|
|
onStatusChanged(
|
|
_cb: (status: ConnectionStatus, error?: Error) => void
|
|
): () => void {
|
|
return () => {};
|
|
}
|
|
}
|