mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-01 06:10:16 +08:00
feat!: affine cloud support (#3813)
Co-authored-by: Hongtao Lye <codert.sn@gmail.com> Co-authored-by: liuyi <forehalo@gmail.com> Co-authored-by: LongYinan <lynweklm@gmail.com> Co-authored-by: X1a0t <405028157@qq.com> Co-authored-by: JimmFly <yangjinfei001@gmail.com> Co-authored-by: Peng Xiao <pengxiao@outlook.com> Co-authored-by: xiaodong zuo <53252747+zuoxiaodong0815@users.noreply.github.com> Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com> Co-authored-by: Qi <474021214@qq.com> Co-authored-by: danielchim <kahungchim@gmail.com>
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
import {
|
||||
ConnectedSocket,
|
||||
MessageBody,
|
||||
OnGatewayConnection,
|
||||
OnGatewayDisconnect,
|
||||
SubscribeMessage,
|
||||
WebSocketGateway,
|
||||
WebSocketServer,
|
||||
} from '@nestjs/websockets';
|
||||
import { Server, Socket } from 'socket.io';
|
||||
import { encodeStateAsUpdate, encodeStateVector } from 'yjs';
|
||||
|
||||
import { Metrics } from '../../../metrics/metrics';
|
||||
import { trimGuid } from '../../../utils/doc';
|
||||
import { DocManager } from '../../doc';
|
||||
|
||||
@WebSocketGateway({
|
||||
cors: process.env.NODE_ENV !== 'production',
|
||||
})
|
||||
export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
||||
private connectionCount = 0;
|
||||
|
||||
constructor(
|
||||
private readonly docManager: DocManager,
|
||||
private readonly metric: Metrics
|
||||
) {}
|
||||
|
||||
@WebSocketServer()
|
||||
server!: Server;
|
||||
|
||||
handleConnection() {
|
||||
this.connectionCount++;
|
||||
this.metric.socketIOConnectionGauge(this.connectionCount, {});
|
||||
}
|
||||
|
||||
handleDisconnect() {
|
||||
this.connectionCount--;
|
||||
this.metric.socketIOConnectionGauge(this.connectionCount, {});
|
||||
}
|
||||
|
||||
@SubscribeMessage('client-handshake')
|
||||
async handleClientHandShake(
|
||||
@MessageBody() workspaceId: string,
|
||||
@ConnectedSocket() client: Socket
|
||||
) {
|
||||
this.metric.socketIOEventCounter(1, { event: 'client-handshake' });
|
||||
const endTimer = this.metric.socketIOEventTimer({
|
||||
event: 'client-handshake',
|
||||
});
|
||||
await client.join(workspaceId);
|
||||
endTimer();
|
||||
}
|
||||
|
||||
@SubscribeMessage('client-leave')
|
||||
async handleClientLeave(
|
||||
@MessageBody() workspaceId: string,
|
||||
@ConnectedSocket() client: Socket
|
||||
) {
|
||||
this.metric.socketIOEventCounter(1, { event: 'client-leave' });
|
||||
const endTimer = this.metric.socketIOEventTimer({
|
||||
event: 'client-leave',
|
||||
});
|
||||
await client.leave(workspaceId);
|
||||
endTimer();
|
||||
}
|
||||
|
||||
@SubscribeMessage('client-update')
|
||||
async handleClientUpdate(
|
||||
@MessageBody()
|
||||
message: {
|
||||
workspaceId: string;
|
||||
guid: string;
|
||||
update: string;
|
||||
},
|
||||
@ConnectedSocket() client: Socket
|
||||
) {
|
||||
this.metric.socketIOEventCounter(1, { event: 'client-update' });
|
||||
const endTimer = this.metric.socketIOEventTimer({ event: 'client-update' });
|
||||
const update = Buffer.from(message.update, 'base64');
|
||||
client.to(message.workspaceId).emit('server-update', message);
|
||||
const guid = trimGuid(message.workspaceId, message.guid);
|
||||
|
||||
await this.docManager.push(message.workspaceId, guid, update);
|
||||
endTimer();
|
||||
}
|
||||
|
||||
@SubscribeMessage('doc-load')
|
||||
async loadDoc(
|
||||
@MessageBody()
|
||||
message: {
|
||||
workspaceId: string;
|
||||
guid: string;
|
||||
stateVector?: string;
|
||||
targetClientId?: number;
|
||||
}
|
||||
): Promise<{ missing: string; state?: string } | false> {
|
||||
this.metric.socketIOEventCounter(1, { event: 'doc-load' });
|
||||
const endTimer = this.metric.socketIOEventTimer({ event: 'doc-load' });
|
||||
const guid = trimGuid(message.workspaceId, message.guid);
|
||||
const doc = await this.docManager.getLatest(message.workspaceId, guid);
|
||||
|
||||
if (!doc) {
|
||||
endTimer();
|
||||
return false;
|
||||
}
|
||||
|
||||
const missing = Buffer.from(
|
||||
encodeStateAsUpdate(
|
||||
doc,
|
||||
message.stateVector
|
||||
? Buffer.from(message.stateVector, 'base64')
|
||||
: undefined
|
||||
)
|
||||
).toString('base64');
|
||||
const state = Buffer.from(encodeStateVector(doc)).toString('base64');
|
||||
|
||||
endTimer();
|
||||
return {
|
||||
missing,
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
@SubscribeMessage('awareness-init')
|
||||
async handleInitAwareness(
|
||||
@MessageBody() workspaceId: string,
|
||||
@ConnectedSocket() client: Socket
|
||||
) {
|
||||
this.metric.socketIOEventCounter(1, { event: 'awareness-init' });
|
||||
const endTimer = this.metric.socketIOEventTimer({
|
||||
event: 'init-awareness',
|
||||
});
|
||||
client.to(workspaceId).emit('new-client-awareness-init');
|
||||
endTimer();
|
||||
}
|
||||
|
||||
@SubscribeMessage('awareness-update')
|
||||
async handleHelpGatheringAwareness(
|
||||
@MessageBody() message: { workspaceId: string; awarenessUpdate: string },
|
||||
@ConnectedSocket() client: Socket
|
||||
) {
|
||||
this.metric.socketIOEventCounter(1, { event: 'awareness-update' });
|
||||
const endTimer = this.metric.socketIOEventTimer({
|
||||
event: 'awareness-update',
|
||||
});
|
||||
client.to(message.workspaceId).emit('server-awareness-broadcast', {
|
||||
...message,
|
||||
});
|
||||
|
||||
endTimer();
|
||||
return 'ack';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { DocModule } from '../../doc';
|
||||
import { EventsGateway } from './events.gateway';
|
||||
import { WorkspaceService } from './workspace';
|
||||
|
||||
@Module({
|
||||
imports: [DocModule.forFeature()],
|
||||
providers: [EventsGateway, WorkspaceService],
|
||||
})
|
||||
export class EventsModule {}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Doc, encodeStateAsUpdate } from 'yjs';
|
||||
|
||||
import { DocManager } from '../../doc';
|
||||
import { assertExists } from '../utils';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceService {
|
||||
constructor(private readonly docManager: DocManager) {}
|
||||
|
||||
async getDocsFromWorkspaceId(workspaceId: string): Promise<
|
||||
Array<{
|
||||
guid: string;
|
||||
update: Buffer;
|
||||
}>
|
||||
> {
|
||||
const docs: Array<{
|
||||
guid: string;
|
||||
update: Buffer;
|
||||
}> = [];
|
||||
const queue: Array<[string, Doc]> = [];
|
||||
// Workspace Doc's guid is the same as workspaceId. This is achieved by when creating a new workspace, the doc guid
|
||||
// is manually set to workspaceId.
|
||||
const doc = await this.docManager.getLatest(workspaceId, workspaceId);
|
||||
if (doc) {
|
||||
queue.push([workspaceId, doc]);
|
||||
}
|
||||
|
||||
while (queue.length > 0) {
|
||||
const head = queue.pop();
|
||||
assertExists(head);
|
||||
const [guid, doc] = head;
|
||||
docs.push({
|
||||
guid: guid,
|
||||
update: Buffer.from(encodeStateAsUpdate(doc)),
|
||||
});
|
||||
|
||||
for (const { guid } of doc.subdocs) {
|
||||
const subDoc = await this.docManager.getLatest(workspaceId, guid);
|
||||
if (subDoc) {
|
||||
queue.push([guid, subDoc]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return docs;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { EventsModule } from './events/events.module';
|
||||
|
||||
@Module({
|
||||
imports: [EventsModule],
|
||||
})
|
||||
export class SyncModule {}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { IoAdapter } from '@nestjs/platform-socket.io';
|
||||
import { createAdapter } from '@socket.io/redis-adapter';
|
||||
import { Redis } from 'ioredis';
|
||||
import { ServerOptions } from 'socket.io';
|
||||
|
||||
export class RedisIoAdapter extends IoAdapter {
|
||||
private adapterConstructor: ReturnType<typeof createAdapter> | undefined;
|
||||
|
||||
async connectToRedis(
|
||||
host: string,
|
||||
port: number,
|
||||
username: string,
|
||||
password: string,
|
||||
db: number
|
||||
): Promise<void> {
|
||||
const pubClient = new Redis(port, host, {
|
||||
username,
|
||||
password,
|
||||
db,
|
||||
});
|
||||
pubClient.on('error', err => {
|
||||
console.error(err);
|
||||
});
|
||||
const subClient = pubClient.duplicate();
|
||||
subClient.on('error', err => {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
this.adapterConstructor = createAdapter(pubClient, subClient);
|
||||
}
|
||||
|
||||
override createIOServer(port: number, options?: ServerOptions): any {
|
||||
const server = super.createIOServer(port, options);
|
||||
server.adapter(this.adapterConstructor);
|
||||
return server;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export function assertExists<T>(
|
||||
val: T | null | undefined,
|
||||
message: string | Error = 'val does not exist'
|
||||
): asserts val is T {
|
||||
if (val === null || val === undefined) {
|
||||
if (message instanceof Error) {
|
||||
throw message;
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user