diff --git a/nestjs-BE/crdt/lww-map.ts b/nestjs-BE/crdt/lww-map.ts new file mode 100644 index 00000000..52fb9bde --- /dev/null +++ b/nestjs-BE/crdt/lww-map.ts @@ -0,0 +1,66 @@ +import { LWWRegister, lwwRegisterState } from './lww-register'; + +export type lwwMapState = { + [key: string]: lwwRegisterState; +}; + +export class LWWMap { + readonly id: string; + private data = new Map>(); + + constructor(id: string, state: lwwMapState = {}) { + this.id = id; + this.initializeData(state); + } + + private initializeData(state: lwwMapState): void { + for (const [key, register] of Object.entries(state)) + this.data.set(key, new LWWRegister(this.id, register)); + } + + getState(): lwwMapState { + const state: lwwMapState = {}; + for (const [key, register] of this.data.entries()) + if (register) state[key] = register.state; + return state; + } + + get(key: string): T | null | undefined { + return this.data.get(key)?.getValue(); + } + + set(key: string, value: T): void { + const register = this.data.get(key); + if (register) register.setValue(value); + else + this.data.set( + key, + new LWWRegister(this.id, { + id: this.id, + timestamp: Date.now(), + value, + }), + ); + } + + delete(key: string): void { + this.data.get(key)?.setValue(null); + } + + has(key: string): boolean { + return !!this.data.get(key)?.getValue(); + } + + clear(): void { + for (const [key, register] of this.data.entries()) + if (register) this.delete(key); + } + + merge(state: lwwMapState): void { + for (const [key, remoteRegister] of Object.entries(state)) { + const local = this.data.get(key); + if (local) local.merge(remoteRegister); + else this.data.set(key, new LWWRegister(this.id, remoteRegister)); + } + } +} diff --git a/nestjs-BE/crdt/lww-register.ts b/nestjs-BE/crdt/lww-register.ts new file mode 100644 index 00000000..b7c020e2 --- /dev/null +++ b/nestjs-BE/crdt/lww-register.ts @@ -0,0 +1,31 @@ +export interface lwwRegisterState { + id: string; + timestamp: number; + value: T; +} + +export class LWWRegister { + readonly id: string; + state: lwwRegisterState; + + constructor(id: string, state: lwwRegisterState) { + this.id = id; + this.state = state; + } + + getValue(): T { + return this.state.value; + } + + setValue(value: T): void { + this.state = { id: this.id, timestamp: Date.now(), value }; + } + + merge(state: lwwRegisterState): void { + const { id: remoteId, timestamp: remoteTimestamp } = state; + const { id: localId, timestamp: localTimestamp } = this.state; + if (localTimestamp > remoteTimestamp) return; + if (localTimestamp === remoteTimestamp && localId > remoteId) return; + this.state = state; + } +} diff --git a/nestjs-BE/package-lock.json b/nestjs-BE/package-lock.json index a83ab54e..6b32191a 100644 --- a/nestjs-BE/package-lock.json +++ b/nestjs-BE/package-lock.json @@ -15,8 +15,7 @@ "@nestjs/platform-socket.io": "^10.2.8", "@nestjs/websockets": "^10.2.8", "reflect-metadata": "^0.1.13", - "rxjs": "^7.8.1", - "yjs": "^13.6.8" + "rxjs": "^7.8.1" }, "devDependencies": { "@nestjs/cli": "^10.0.0", @@ -5383,15 +5382,6 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "node_modules/isomorphic.js": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", - "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", - "funding": { - "type": "GitHub Sponsors ❤", - "url": "https://github.com/sponsors/dmonad" - } - }, "node_modules/istanbul-lib-coverage": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.1.tgz", @@ -6231,25 +6221,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lib0": { - "version": "0.2.87", - "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.87.tgz", - "integrity": "sha512-TbB63XJixvNToW2IHWAFsCJj9tVnajmwjE14p69i51Rx8byOQd2IP4ourE8v4d7vhyO++nVm1sQk3ePslfbucg==", - "dependencies": { - "isomorphic.js": "^0.2.4" - }, - "bin": { - "0gentesthtml": "bin/gentesthtml.js", - "0serve": "bin/0serve.js" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "type": "GitHub Sponsors ❤", - "url": "https://github.com/sponsors/dmonad" - } - }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -9006,22 +8977,6 @@ "node": ">=12" } }, - "node_modules/yjs": { - "version": "13.6.8", - "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.8.tgz", - "integrity": "sha512-ZPq0hpJQb6f59B++Ngg4cKexDJTvfOgeiv0sBc4sUm8CaBWH7OQC4kcCgrqbjJ/B2+6vO49exvTmYfdlPtcjbg==", - "dependencies": { - "lib0": "^0.2.74" - }, - "engines": { - "node": ">=16.0.0", - "npm": ">=8.0.0" - }, - "funding": { - "type": "GitHub Sponsors ❤", - "url": "https://github.com/sponsors/dmonad" - } - }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/nestjs-BE/package.json b/nestjs-BE/package.json index ff95ddcf..4352ced4 100644 --- a/nestjs-BE/package.json +++ b/nestjs-BE/package.json @@ -26,8 +26,7 @@ "@nestjs/platform-socket.io": "^10.2.8", "@nestjs/websockets": "^10.2.8", "reflect-metadata": "^0.1.13", - "rxjs": "^7.8.1", - "yjs": "^13.6.8" + "rxjs": "^7.8.1" }, "devDependencies": { "@nestjs/cli": "^10.0.0", diff --git a/nestjs-BE/src/mindmap/mindmap.service.ts b/nestjs-BE/src/mindmap/mindmap.service.ts index 473d65b5..14b24730 100644 --- a/nestjs-BE/src/mindmap/mindmap.service.ts +++ b/nestjs-BE/src/mindmap/mindmap.service.ts @@ -1,26 +1,26 @@ import { Injectable } from '@nestjs/common'; -import * as Y from 'yjs'; +import { LWWMap, lwwMapState } from 'crdt/lww-map'; @Injectable() export class MindmapService { - private ydocs = new Map(); + private boards = new Map>(); - updateMindmap(boardId: string, message: any) { - const ydoc = this.getMindmap(boardId); - Y.applyUpdate(ydoc, message); + updateMindmap(boardId: string, message: any): void { + const board = this.getMindmap(boardId); + board.merge(message); } - getEncodedState(boardId: string) { - const ydoc = this.getMindmap(boardId); - return Y.encodeStateAsUpdate(ydoc); + getEncodedState(boardId: string): lwwMapState { + const board = this.getMindmap(boardId); + return board.getState(); } private getMindmap(boardId: string) { - let ydoc = this.ydocs.get(boardId); - if (!ydoc) { - ydoc = new Y.Doc(); - this.ydocs.set(boardId, ydoc); + let board = this.boards.get(boardId); + if (!board) { + board = new LWWMap(boardId); // boardId 대신에 서버 아이디??? + this.boards.set(boardId, board); } - return ydoc; + return board; } }