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:
Alex Yang
2023-08-29 05:07:05 -05:00
committed by GitHub
parent d0145c6f38
commit 2f6c4e3696
414 changed files with 19469 additions and 7591 deletions
@@ -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;
}
}
+8
View File
@@ -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;
}
}
+11
View File
@@ -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);
}
}