fix(core): disconnect ws when user logout (#8188)
This commit is contained in:
@@ -70,5 +70,7 @@ export class WorkspaceEngine extends Entity<{
|
|||||||
|
|
||||||
override dispose(): void {
|
override dispose(): void {
|
||||||
this.forceStop();
|
this.forceStop();
|
||||||
|
this.doc.dispose();
|
||||||
|
this.awareness.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,4 +16,10 @@ export class WorkspaceEngineService extends Service {
|
|||||||
constructor(private readonly workspaceScope: WorkspaceScope) {
|
constructor(private readonly workspaceScope: WorkspaceScope) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override dispose(): void {
|
||||||
|
this._engine?.dispose();
|
||||||
|
this._engine = null;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { Awareness } from 'y-protocols/awareness.js';
|
|||||||
export interface AwarenessConnection {
|
export interface AwarenessConnection {
|
||||||
connect(awareness: Awareness): void;
|
connect(awareness: Awareness): void;
|
||||||
disconnect(): void;
|
disconnect(): void;
|
||||||
|
dispose?(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AwarenessEngine {
|
export class AwarenessEngine {
|
||||||
@@ -15,4 +16,8 @@ export class AwarenessEngine {
|
|||||||
disconnect() {
|
disconnect() {
|
||||||
this.connections.forEach(connection => connection.disconnect());
|
this.connections.forEach(connection => connection.disconnect());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.connections.forEach(connection => connection.dispose?.());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,4 +219,9 @@ export class DocEngine {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.stop();
|
||||||
|
this.server?.dispose?.();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,4 +23,6 @@ export interface DocServer {
|
|||||||
waitForConnectingServer(signal: AbortSignal): Promise<void>;
|
waitForConnectingServer(signal: AbortSignal): Promise<void>;
|
||||||
disconnectServer(): void;
|
disconnectServer(): void;
|
||||||
onInterrupted(cb: (reason: string) => void): void;
|
onInterrupted(cb: (reason: string) => void): void;
|
||||||
|
|
||||||
|
dispose?(): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export function configureCloudModule(framework: Framework) {
|
|||||||
framework
|
framework
|
||||||
.service(FetchService)
|
.service(FetchService)
|
||||||
.service(GraphQLService, [FetchService])
|
.service(GraphQLService, [FetchService])
|
||||||
.service(WebSocketService)
|
.service(WebSocketService, [AuthService])
|
||||||
.service(ServerConfigService)
|
.service(ServerConfigService)
|
||||||
.entity(ServerConfig, [ServerConfigStore])
|
.entity(ServerConfig, [ServerConfigStore])
|
||||||
.store(ServerConfigStore, [GraphQLService])
|
.store(ServerConfigStore, [GraphQLService])
|
||||||
|
|||||||
@@ -1,37 +1,47 @@
|
|||||||
import { OnEvent, Service } from '@toeverything/infra';
|
import { ApplicationStarted, OnEvent, Service } from '@toeverything/infra';
|
||||||
import type { Socket } from 'socket.io-client';
|
|
||||||
import { Manager } from 'socket.io-client';
|
import { Manager } from 'socket.io-client';
|
||||||
|
|
||||||
import { getAffineCloudBaseUrl } from '../services/fetch';
|
import { getAffineCloudBaseUrl } from '../services/fetch';
|
||||||
|
import type { AuthService } from './auth';
|
||||||
import { AccountChanged } from './auth';
|
import { AccountChanged } from './auth';
|
||||||
|
|
||||||
@OnEvent(AccountChanged, e => e.reconnect)
|
@OnEvent(AccountChanged, e => e.update)
|
||||||
|
@OnEvent(ApplicationStarted, e => e.update)
|
||||||
export class WebSocketService extends Service {
|
export class WebSocketService extends Service {
|
||||||
ioManager: Manager = new Manager(`${getAffineCloudBaseUrl()}/`, {
|
ioManager: Manager = new Manager(`${getAffineCloudBaseUrl()}/`, {
|
||||||
autoConnect: false,
|
autoConnect: false,
|
||||||
transports: ['websocket'],
|
transports: ['websocket'],
|
||||||
secure: location.protocol === 'https:',
|
secure: location.protocol === 'https:',
|
||||||
});
|
});
|
||||||
sockets: Set<Socket> = new Set();
|
socket = this.ioManager.socket('/');
|
||||||
|
refCount = 0;
|
||||||
|
|
||||||
constructor() {
|
constructor(private readonly authService: AuthService) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
newSocket(): Socket {
|
/**
|
||||||
const socket = this.ioManager.socket('/');
|
* Connect socket, with automatic connect and reconnect logic.
|
||||||
this.sockets.add(socket);
|
* External code should not call `socket.connect()` or `socket.disconnect()` manually.
|
||||||
|
* When socket is no longer needed, call `dispose()` to clean up resources.
|
||||||
return socket;
|
*/
|
||||||
|
connect() {
|
||||||
|
this.refCount++;
|
||||||
|
this.update();
|
||||||
|
return {
|
||||||
|
socket: this.socket,
|
||||||
|
dispose: () => {
|
||||||
|
this.refCount--;
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
reconnect(): void {
|
update(): void {
|
||||||
for (const socket of this.sockets) {
|
if (this.authService.session.account$.value && this.refCount > 0) {
|
||||||
socket.disconnect();
|
this.socket.connect();
|
||||||
}
|
} else {
|
||||||
|
this.socket.disconnect();
|
||||||
for (const socket of this.sockets) {
|
|
||||||
socket.connect();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,9 @@ export class UserDBEngine extends Entity<{
|
|||||||
userId: string;
|
userId: string;
|
||||||
}> {
|
}> {
|
||||||
private readonly userId = this.props.userId;
|
private readonly userId = this.props.userId;
|
||||||
private readonly socket = this.websocketService.newSocket();
|
|
||||||
readonly docEngine = new DocEngine(
|
readonly docEngine = new DocEngine(
|
||||||
this.userspaceStorageProvider.getDocStorage('affine-cloud:' + this.userId),
|
this.userspaceStorageProvider.getDocStorage('affine-cloud:' + this.userId),
|
||||||
new UserDBDocServer(this.userId, this.socket)
|
new UserDBDocServer(this.userId, this.websocketService)
|
||||||
);
|
);
|
||||||
|
|
||||||
canGracefulStop() {
|
canGracefulStop() {
|
||||||
@@ -29,6 +28,5 @@ export class UserDBEngine extends Entity<{
|
|||||||
|
|
||||||
override dispose() {
|
override dispose() {
|
||||||
this.docEngine.stop();
|
this.docEngine.stop();
|
||||||
this.socket.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
import { type DocServer, throwIfAborted } from '@toeverything/infra';
|
import { type DocServer, throwIfAborted } from '@toeverything/infra';
|
||||||
import type { Socket } from 'socket.io-client';
|
import type { Socket } from 'socket.io-client';
|
||||||
|
|
||||||
|
import type { WebSocketService } from '../../cloud';
|
||||||
import {
|
import {
|
||||||
base64ToUint8Array,
|
base64ToUint8Array,
|
||||||
uint8ArrayToBase64,
|
uint8ArrayToBase64,
|
||||||
@@ -19,10 +20,17 @@ export class UserDBDocServer implements DocServer {
|
|||||||
interruptCb: ((reason: string) => void) | null = null;
|
interruptCb: ((reason: string) => void) | null = null;
|
||||||
SEND_TIMEOUT = 30000;
|
SEND_TIMEOUT = 30000;
|
||||||
|
|
||||||
|
socket: Socket;
|
||||||
|
disposeSocket: () => void;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly userId: string,
|
private readonly userId: string,
|
||||||
private readonly socket: Socket
|
webSocketService: WebSocketService
|
||||||
) {}
|
) {
|
||||||
|
const { socket, dispose } = webSocketService.connect();
|
||||||
|
this.socket = socket;
|
||||||
|
this.disposeSocket = dispose;
|
||||||
|
}
|
||||||
|
|
||||||
private async clientHandShake() {
|
private async clientHandShake() {
|
||||||
await this.socket.emitWithAck('space:join', {
|
await this.socket.emitWithAck('space:join', {
|
||||||
@@ -154,7 +162,6 @@ export class UserDBDocServer implements DocServer {
|
|||||||
if (this.socket.connected) {
|
if (this.socket.connected) {
|
||||||
await this.clientHandShake();
|
await this.clientHandShake();
|
||||||
} else {
|
} else {
|
||||||
this.socket.connect();
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
this.socket.on('connect', () => {
|
this.socket.on('connect', () => {
|
||||||
resolve();
|
resolve();
|
||||||
@@ -168,17 +175,12 @@ export class UserDBDocServer implements DocServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
disconnectServer(): void {
|
disconnectServer(): void {
|
||||||
if (!this.socket) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.socket.emit('space:leave', {
|
this.socket.emit('space:leave', {
|
||||||
spaceType: 'userspace',
|
spaceType: 'userspace',
|
||||||
spaceId: this.userId,
|
spaceId: this.userId,
|
||||||
});
|
});
|
||||||
this.socket.off('server-version-rejected', this.handleVersionRejected);
|
this.socket.off('server-version-rejected', this.handleVersionRejected);
|
||||||
this.socket.off('disconnect', this.handleDisconnect);
|
this.socket.off('disconnect', this.handleDisconnect);
|
||||||
this.socket.disconnect();
|
|
||||||
}
|
}
|
||||||
onInterrupted = (cb: (reason: string) => void) => {
|
onInterrupted = (cb: (reason: string) => void) => {
|
||||||
this.interruptCb = cb;
|
this.interruptCb = cb;
|
||||||
@@ -192,4 +194,9 @@ export class UserDBDocServer implements DocServer {
|
|||||||
handleVersionRejected = () => {
|
handleVersionRejected = () => {
|
||||||
this.interruptCb?.('Client version rejected');
|
this.interruptCb?.('Client version rejected');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
this.disconnectServer();
|
||||||
|
this.disposeSocket();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,17 +243,11 @@ export class CloudWorkspaceFlavourProviderService
|
|||||||
getAwarenessConnections: () => {
|
getAwarenessConnections: () => {
|
||||||
return [
|
return [
|
||||||
new BroadcastChannelAwarenessConnection(workspaceId),
|
new BroadcastChannelAwarenessConnection(workspaceId),
|
||||||
new CloudAwarenessConnection(
|
new CloudAwarenessConnection(workspaceId, this.webSocketService),
|
||||||
workspaceId,
|
|
||||||
this.webSocketService.newSocket()
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
getDocServer: () => {
|
getDocServer: () => {
|
||||||
return new CloudDocEngineServer(
|
return new CloudDocEngineServer(workspaceId, this.webSocketService);
|
||||||
workspaceId,
|
|
||||||
this.webSocketService.newSocket()
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
getDocStorage: () => {
|
getDocStorage: () => {
|
||||||
return this.storageProvider.getDocStorage(workspaceId);
|
return this.storageProvider.getDocStorage(workspaceId);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { WebSocketService } from '@affine/core/modules/cloud';
|
||||||
import { DebugLogger } from '@affine/debug';
|
import { DebugLogger } from '@affine/debug';
|
||||||
import type { AwarenessConnection } from '@toeverything/infra';
|
import type { AwarenessConnection } from '@toeverything/infra';
|
||||||
import type { Socket } from 'socket.io-client';
|
import type { Socket } from 'socket.io-client';
|
||||||
@@ -17,10 +18,17 @@ type AwarenessChanges = Record<'added' | 'updated' | 'removed', number[]>;
|
|||||||
export class CloudAwarenessConnection implements AwarenessConnection {
|
export class CloudAwarenessConnection implements AwarenessConnection {
|
||||||
awareness: Awareness | null = null;
|
awareness: Awareness | null = null;
|
||||||
|
|
||||||
|
socket: Socket;
|
||||||
|
disposeSocket: () => void;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly workspaceId: string,
|
private readonly workspaceId: string,
|
||||||
private readonly socket: Socket
|
webSocketService: WebSocketService
|
||||||
) {}
|
) {
|
||||||
|
const { socket, dispose } = webSocketService.connect();
|
||||||
|
this.socket = socket;
|
||||||
|
this.disposeSocket = dispose;
|
||||||
|
}
|
||||||
|
|
||||||
connect(awareness: Awareness): void {
|
connect(awareness: Awareness): void {
|
||||||
this.socket.on('space:broadcast-awareness-update', this.awarenessBroadcast);
|
this.socket.on('space:broadcast-awareness-update', this.awarenessBroadcast);
|
||||||
@@ -38,8 +46,6 @@ export class CloudAwarenessConnection implements AwarenessConnection {
|
|||||||
|
|
||||||
if (this.socket.connected) {
|
if (this.socket.connected) {
|
||||||
this.handleConnect();
|
this.handleConnect();
|
||||||
} else {
|
|
||||||
this.socket.connect();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +187,10 @@ export class CloudAwarenessConnection implements AwarenessConnection {
|
|||||||
|
|
||||||
handleReject = () => {
|
handleReject = () => {
|
||||||
this.socket.off('server-version-rejected', this.handleReject);
|
this.socket.off('server-version-rejected', this.handleReject);
|
||||||
this.disconnect();
|
|
||||||
this.socket.disconnect();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.disconnect();
|
||||||
|
this.disposeSocket();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { WebSocketService } from '@affine/core/modules/cloud';
|
||||||
import { DebugLogger } from '@affine/debug';
|
import { DebugLogger } from '@affine/debug';
|
||||||
import {
|
import {
|
||||||
ErrorNames,
|
ErrorNames,
|
||||||
@@ -20,10 +21,17 @@ export class CloudDocEngineServer implements DocServer {
|
|||||||
interruptCb: ((reason: string) => void) | null = null;
|
interruptCb: ((reason: string) => void) | null = null;
|
||||||
SEND_TIMEOUT = 30000;
|
SEND_TIMEOUT = 30000;
|
||||||
|
|
||||||
|
socket: Socket;
|
||||||
|
disposeSocket: () => void;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly workspaceId: string,
|
private readonly workspaceId: string,
|
||||||
private readonly socket: Socket
|
webSocketService: WebSocketService
|
||||||
) {}
|
) {
|
||||||
|
const { socket, dispose } = webSocketService.connect();
|
||||||
|
this.socket = socket;
|
||||||
|
this.disposeSocket = dispose;
|
||||||
|
}
|
||||||
|
|
||||||
private async clientHandShake() {
|
private async clientHandShake() {
|
||||||
await this.socket.emitWithAck('space:join', {
|
await this.socket.emitWithAck('space:join', {
|
||||||
@@ -169,17 +177,12 @@ export class CloudDocEngineServer implements DocServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
disconnectServer(): void {
|
disconnectServer(): void {
|
||||||
if (!this.socket) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.socket.emit('space:leave', {
|
this.socket.emit('space:leave', {
|
||||||
spaceType: 'workspace',
|
spaceType: 'workspace',
|
||||||
spaceId: this.workspaceId,
|
spaceId: this.workspaceId,
|
||||||
});
|
});
|
||||||
this.socket.off('server-version-rejected', this.handleVersionRejected);
|
this.socket.off('server-version-rejected', this.handleVersionRejected);
|
||||||
this.socket.off('disconnect', this.handleDisconnect);
|
this.socket.off('disconnect', this.handleDisconnect);
|
||||||
this.socket.disconnect();
|
|
||||||
}
|
}
|
||||||
onInterrupted = (cb: (reason: string) => void) => {
|
onInterrupted = (cb: (reason: string) => void) => {
|
||||||
this.interruptCb = cb;
|
this.interruptCb = cb;
|
||||||
@@ -193,4 +196,9 @@ export class CloudDocEngineServer implements DocServer {
|
|||||||
handleVersionRejected = () => {
|
handleVersionRejected = () => {
|
||||||
this.interruptCb?.('Client version rejected');
|
this.interruptCb?.('Client version rejected');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
this.disconnectServer();
|
||||||
|
this.disposeSocket();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user