Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/BE/#115 server socket.io로 mongo db연동 #116

Merged
merged 18 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/lint_and_unit_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ jobs:
node-version: "20"
cache: "pnpm"

# Run build
- name: Install dependencies and build packages
run: |
pnpm install --frozen-lockfile
pnpm --filter @noctaCrdt build
pnpm --filter server build

# Run lint
- name: Run Lint
run: pnpm eslint .
Expand Down
25 changes: 15 additions & 10 deletions @noctaCrdt/package.json
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
{
"name": "@noctaCrdt",
"version": "1.0.0",
"main": "dist/Crdt.js",
"main": "./dist/Crdt.js",
"module": "./dist/Crdt.js",
"types": "dist/Crdt.d.ts",
"scripts": {
"build": "tsc -b"
},
"exports": {
".": {
"types": "./dist/Crdt.d.ts",
"default": "./dist/Crdt.js"
"./Crdt": {
"require": "./dist/Crdt.js",
"import": "./dist/Crdt.js",
"types": "./dist/Crdt.d.js"
},
"./Node": {
"types": "./dist/Node.d.ts",
"default": "./dist/Node.js"
"require": "./dist/Node.js",
"import": "./dist/Node.js",
"types": "./dist/Node.d.ts"
},
"./LinkedList": {
"types": "./dist/LinkedList.d.ts",
"default": "./dist/LinkedList.js"
"require": "./dist/LinkedList.js",
"import": "./dist/LinkedList.js",
"types": "./dist/LinkedList.d.ts"
},
"./Interfaces": {
"types": "./dist/Interfaces.d.ts",
"default": "./dist/Interfaces.js"
"require": "./dist/Interfaces.js",
"import": "./dist/Interfaces.js",
"types": "./dist/Interfaces.d.ts"
}
}
}
7 changes: 6 additions & 1 deletion @noctaCrdt/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"isolatedModules": true
"isolatedModules": true,
"module": "CommonJS",
"esModuleInterop": true,
"allowJs": true, // 추가
"moduleResolution": "node", // 추가
"target": "ES2021" // 추가
},
"include": ["**/*.ts"],
"exclude": ["node_modules", "dist"]
Expand Down
2 changes: 1 addition & 1 deletion pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
packages:
- "@noctaCrdt"
- "client"
- "server"
- "./@noctaCrdt"
12 changes: 11 additions & 1 deletion server/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,23 @@ const config: Config = {
rootDir: ".",
testRegex: ".*\\.spec\\.ts$",
transform: {
"^.+\\.(t|j)s$": "ts-jest",
"^.+\\.(t|j)s$": [
"ts-jest",
{
tsconfig: "tsconfig.json",
},
],
},
collectCoverageFrom: ["**/*.(t|j)s"],
coverageDirectory: "./coverage",
testEnvironment: "node",
preset: "@shelf/jest-mongodb",
watchPathIgnorePatterns: ["globalConfig"],
moduleNameMapper: {
"^@noctaCrdt$": "<rootDir>/../@noctaCrdt/dist/Crdt.js",
"^@noctaCrdt/(.*)$": "<rootDir>/../@noctaCrdt/dist/$1.js",
},
transformIgnorePatterns: ["node_modules/(?!@noctaCrdt)"],
};

export default config;
5 changes: 3 additions & 2 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@
"license": "UNLICENSED",
"type": "commonjs",
"scripts": {
"build": "nest build --webpack --webpackPath webpack.config.js",
"build": " pnpm build:noctaCrdt && nest build --webpack --webpackPath webpack.config.js",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"src/**/*.{ts,tsx}\" --fix",
"test": "jest",
"build:noctaCrdt": "pnpm --filter @noctaCrdt build",
"test": "pnpm build:noctaCrdt && jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
Expand Down
2 changes: 2 additions & 0 deletions server/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { MongooseModule } from "@nestjs/mongoose";
import { CrdtModule } from "./crdt/crdt.module";

@Module({
imports: [
Expand All @@ -19,6 +20,7 @@ import { MongooseModule } from "@nestjs/mongoose";
uri: configService.get<string>("MONGO_URI"), // 환경 변수에서 MongoDB URI 가져오기
}),
}),
CrdtModule,
],
controllers: [AppController],
providers: [AppService],
Expand Down
98 changes: 98 additions & 0 deletions server/src/crdt/crdt.gateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {
WebSocketGateway,
SubscribeMessage,
OnGatewayInit,
OnGatewayConnection,
OnGatewayDisconnect,
MessageBody,
ConnectedSocket,
} from "@nestjs/websockets";
import { Socket, Server } from "socket.io";
import { CrdtService } from "./crdt.service";
import {
RemoteInsertOperation,
RemoteDeleteOperation,
CursorPosition,
} from "@noctaCrdt/Interfaces";

@WebSocketGateway({
cors: {
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

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);
this.clientMap.set(client.id, assignedId);
client.emit("assignId", assignedId);
const currentCRDT = this.crdtService.getCRDT().serialize();
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,
@ConnectedSocket() client: Socket,
): Promise<void> {
console.log(`Delete 연산 수신 from ${client.id}:`, data);
await this.crdtService.handleDelete(data);
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);
client.broadcast.emit("cursor", data);
}
}
12 changes: 12 additions & 0 deletions server/src/crdt/crdt.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Module } from "@nestjs/common";
import { CrdtService } from "./crdt.service";
import { MongooseModule } from "@nestjs/mongoose";
import { Doc, DocumentSchema } from "../schemas/document.schema";
import { CrdtGateway } from "./crdt.gateway";

@Module({
imports: [MongooseModule.forFeature([{ name: Doc.name, schema: DocumentSchema }])],
providers: [CrdtService, CrdtGateway],
exports: [CrdtService],
})
export class CrdtModule {}
104 changes: 104 additions & 0 deletions server/src/crdt/crdt.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// src/crdt/crdt.service.ts
import { Injectable, OnModuleInit } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
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;
}

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

constructor(@InjectModel(Doc.name) private documentModel: Model<DocumentDocument>) {
this.crdt = new CRDT(0); // 초기 클라이언트 ID는 0으로 설정 (서버 자체)
}
async onModuleInit() {
try {
const doc = await this.getDocument();

if (doc && doc.content) {
this.crdt = new CRDT(0);
let contentArray: string[];

try {
contentArray = JSON.parse(doc.content) as string[];
} catch (e) {
console.error("Invalid JSON in document content:", doc.content);
}
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) {
doc = new this.documentModel({ content: JSON.stringify(this.crdt.spread()) });
await doc.save();
}
return doc;
}

/**
* MongoDB에 문서를 업데이트합니다.
*/
async updateDocument(): Promise<Doc> {
const content = JSON.stringify(this.crdt.spread());
const doc = await this.documentModel.findOneAndUpdate(
{},
{ content, updatedAt: new Date() },
{ new: true, upsert: true },
);
("d");
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 {
return this.crdt;
}
}
15 changes: 15 additions & 0 deletions server/src/schemas/document.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { Document } from "mongoose";

export type DocumentDocument = Document & Doc;

@Schema()
export class Doc {
@Prop({ required: true })
content: string;

@Prop({ default: Date.now })
updatedAt: Date;
}

export const DocumentSchema = SchemaFactory.createForClass(Doc);
Loading
Loading