Skip to content

Commit

Permalink
Merge pull request #124 from boostcampwm-2024/Feature/#122_mongoDB_Bl…
Browse files Browse the repository at this point in the history
…ockCRDT연동

Feature/#122 mongo db block crdt연동
  • Loading branch information
github-actions[bot] authored Nov 14, 2024
2 parents be122c0 + 4c33ca2 commit 3ab5d52
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 94 deletions.
29 changes: 2 additions & 27 deletions server/src/crdt/crdt.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,20 @@ import {

@WebSocketGateway({
cors: {
origin: "*", // 실제 배포 시에는 보안을 위해 적절히 설정하세요
origin: "*",
},
})
export class CrdtGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
private server: Server;
private clientIdCounter: number = 1;
private clientMap: Map<string, number> = new Map(); // socket.id -> clientId
private clientMap: Map<string, number> = new Map();

constructor(private readonly crdtService: CrdtService) {}

afterInit(server: Server) {
this.server = server;
}

/**
* 초기에 연결될때, 클라이언트에 숫자id및 문서정보를 송신한다.
* @param client 클라이언트 socket 정보
*/
async handleConnection(client: Socket) {
console.log(`클라이언트 연결: ${client.id}`);
const assignedId = (this.clientIdCounter += 1);
Expand All @@ -44,37 +40,21 @@ export class CrdtGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
client.emit("document", currentCRDT);
}

/**
* 연결이 끊어지면 클라이언트 맵에서 클라이언트 삭제
* @param client 클라이언트 socket 정보
*/
handleDisconnect(client: Socket) {
console.log(`클라이언트 연결 해제: ${client.id}`);
this.clientMap.delete(client.id);
}

/**
* 클라이언트로부터 받은 원격 삽입 연산
* @param data 클라이언트가 송신한 Node 정보
* @param client 클라이언트 번호
*/
@SubscribeMessage("insert")
async handleInsert(
@MessageBody() data: RemoteInsertOperation,
@ConnectedSocket() client: Socket,
): Promise<void> {
console.log(`Insert 연산 수신 from ${client.id}:`, data);

await this.crdtService.handleInsert(data);

client.broadcast.emit("insert", data);
}

/**
* 클라이언트로부터 받은 원격 삭제 연산
* @param data 클라이언트가 송신한 Node 정보
* @param client 클라이언트 번호
*/
@SubscribeMessage("delete")
async handleDelete(
@MessageBody() data: RemoteDeleteOperation,
Expand All @@ -85,11 +65,6 @@ export class CrdtGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
client.broadcast.emit("delete", data);
}

/**
* 추후 caret 표시 기능을 위해 받아놓음 + 추후 개선때 인덱스 계산할때 캐럿으로 계산하면 용이할듯 하여 데이터로 만듦
* @param data 클라이언트가 송신한 caret 정보
* @param client 클라이언트 번호
*/
@SubscribeMessage("cursor")
handleCursor(@MessageBody() data: CursorPosition, @ConnectedSocket() client: Socket): void {
console.log(`Cursor 위치 수신 from ${client.id}:`, data);
Expand Down
2 changes: 1 addition & 1 deletion server/src/crdt/crdt.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Module } from "@nestjs/common";
import { CrdtService } from "./crdt.service";
import { MongooseModule } from "@nestjs/mongoose";
import { Doc, DocumentSchema } from "../schemas/document.schema";
import { Doc, DocumentSchema } from "./schemas/document.schema";
import { CrdtGateway } from "./crdt.gateway";

@Module({
Expand Down
92 changes: 42 additions & 50 deletions server/src/crdt/crdt.service.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,61 @@
// src/crdt/crdt.service.ts
import { Injectable, OnModuleInit } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { Doc, DocumentDocument } from "../schemas/document.schema";
import { Doc, DocumentDocument } from "./schemas/document.schema";
import { Model } from "mongoose";
import { CRDT } from "@noctaCrdt/Crdt";
import { NodeId, Node } from "@noctaCrdt/Node";

interface RemoteInsertOperation {
node: Node;
}

interface RemoteDeleteOperation {
targetId: NodeId | null;
clock: number;
}
import { BlockCRDT } from "@noctaCrdt/Crdt";
import { RemoteInsertOperation, RemoteDeleteOperation } from "@noctaCrdt/Interfaces";
import { CharId } from "@noctaCrdt/NodeId";
import { Char } from "@noctaCrdt/Node";

@Injectable()
export class CrdtService implements OnModuleInit {
private crdt: CRDT;
private crdt: BlockCRDT;

constructor(@InjectModel(Doc.name) private documentModel: Model<DocumentDocument>) {
this.crdt = new CRDT(0); // 초기 클라이언트 ID는 0으로 설정 (서버 자체)
this.crdt = new BlockCRDT(0);
}
async onModuleInit() {
try {
const doc = await this.getDocument();
if (doc && doc.crdt) {
this.crdt = new BlockCRDT(0);
try {
// 저장된 CRDT 상태를 복원
this.crdt.clock = doc.crdt.clock;
this.crdt.client = doc.crdt.client;

if (doc && doc.content) {
this.crdt = new CRDT(0);
let contentArray: string[];
// LinkedList 복원
if (doc.crdt.LinkedList.head) {
this.crdt.LinkedList.head = new CharId(
doc.crdt.LinkedList.head.clock,
doc.crdt.LinkedList.head.client,
);
}

try {
contentArray = JSON.parse(doc.content) as string[];
this.crdt.LinkedList.nodeMap = {};
for (const [key, node] of Object.entries(doc.crdt.LinkedList.nodeMap)) {
const reconstructedNode = new Char(
node.value,
new CharId(node.id.clock, node.id.client),
);

if (node.next) {
reconstructedNode.next = new CharId(node.next.clock, node.next.client);
}
if (node.prev) {
reconstructedNode.prev = new CharId(node.prev.clock, node.prev.client);
}

this.crdt.LinkedList.nodeMap[key] = reconstructedNode;
}
} catch (e) {
console.error("Invalid JSON in document content:", doc.content);
console.error("Error reconstructing CRDT:", e);
}
contentArray.forEach((char, index) => {
this.crdt.localInsert(index, char);
});
}
} catch (error) {
console.error("Error during CrdtService initialization:", error);
}
}

/**
* MongoDB에서 문서를 가져옵니다.
*/
async getDocument(): Promise<Doc> {
let doc = await this.documentModel.findOne();
if (!doc) {
Expand All @@ -56,49 +65,32 @@ export class CrdtService implements OnModuleInit {
return doc;
}

/**
* MongoDB에 문서를 업데이트합니다.
*/
async updateDocument(): Promise<Doc> {
const content = JSON.stringify(this.crdt.spread());
const serializedCRDT = this.crdt.serialize();
const doc = await this.documentModel.findOneAndUpdate(
{},
{ content, updatedAt: new Date() },
{ crdt: serializedCRDT, updatedAt: new Date() },
{ new: true, upsert: true },
);
("d");
if (!doc) {
throw new Error("문서가 저장되지 않았습니다.");
}
if (!doc) throw new Error("문서 저장 실패");
return doc;
}

/**
* 삽입 연산을 처리하고 문서를 업데이트합니다.
*/
async handleInsert(operation: RemoteInsertOperation): Promise<void> {
this.crdt.remoteInsert(operation);
await this.updateDocument();
}

/**
* 삭제 연산을 처리하고 문서를 업데이트합니다.
*/
async handleDelete(operation: RemoteDeleteOperation): Promise<void> {
this.crdt.remoteDelete(operation);
await this.updateDocument();
}

/**
* 현재 CRDT의 텍스트를 반환합니다.
*/
getText(): string {
return this.crdt.read();
}
/**
* CRDT 인스턴스를 반환하는 Getter 메서드
*/
getCRDT(): CRDT {

getCRDT(): BlockCRDT {
return this.crdt;
}
}
31 changes: 31 additions & 0 deletions server/src/crdt/schemas/document.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { Document } from "mongoose";

export type DocumentDocument = Document & Doc;

@Schema()
export class Doc {
@Prop({ type: Object, required: true })
crdt: {
clock: number;
client: number;
LinkedList: {
head: {
clock: number;
client: number;
} | null;
nodeMap: {
[key: string]: {
id: { clock: number; client: number };
value: string;
next: { clock: number; client: number } | null;
prev: { clock: number; client: number } | null;
};
};
};
};

@Prop({ default: Date.now })
updatedAt: Date;
}
export const DocumentSchema = SchemaFactory.createForClass(Doc);
15 changes: 0 additions & 15 deletions server/src/schemas/document.schema.ts

This file was deleted.

2 changes: 1 addition & 1 deletion server/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@
"esModuleInterop": true,
"resolveJsonModule": true
},
"include": ["src/**/*", "test/**/*"],
"include": ["src/**/*", "test/**/*", "schemas"],
"exclude": ["node_modules", "dist"]
}

0 comments on commit 3ab5d52

Please sign in to comment.