From 7a5fea397f0e35e8e63a4768276ef134a6e3aaf1 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Tue, 12 Nov 2024 21:01:12 +0900 Subject: [PATCH 01/17] =?UTF-8?q?build:=20CRDT=EC=9A=A9=20pnpm=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EB=B0=8F=20workspace=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - package.json 등록 - tsconfig.json 빌드 설정 #098 --- @noctaCrdt/Crdt.ts | 0 @noctaCrdt/Interfaces.ts | 0 @noctaCrdt/LinkedList.ts | 0 @noctaCrdt/Node.ts | 0 @noctaCrdt/package.json | 27 +++++++++++++++++++++++++++ @noctaCrdt/tsconfig.json | 14 ++++++++++++++ pnpm-workspace.yaml | 5 +++-- 7 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 @noctaCrdt/Crdt.ts create mode 100644 @noctaCrdt/Interfaces.ts create mode 100644 @noctaCrdt/LinkedList.ts create mode 100644 @noctaCrdt/Node.ts create mode 100644 @noctaCrdt/package.json create mode 100644 @noctaCrdt/tsconfig.json diff --git a/@noctaCrdt/Crdt.ts b/@noctaCrdt/Crdt.ts new file mode 100644 index 00000000..e69de29b diff --git a/@noctaCrdt/Interfaces.ts b/@noctaCrdt/Interfaces.ts new file mode 100644 index 00000000..e69de29b diff --git a/@noctaCrdt/LinkedList.ts b/@noctaCrdt/LinkedList.ts new file mode 100644 index 00000000..e69de29b diff --git a/@noctaCrdt/Node.ts b/@noctaCrdt/Node.ts new file mode 100644 index 00000000..e69de29b diff --git a/@noctaCrdt/package.json b/@noctaCrdt/package.json new file mode 100644 index 00000000..8abfc264 --- /dev/null +++ b/@noctaCrdt/package.json @@ -0,0 +1,27 @@ +{ + "name": "@noctaCrdt", + "version": "1.0.0", + "main": "dist/Crdt.js", + "types": "dist/Crdt.d.ts", + "scripts": { + "build": "tsc -b" + }, + "exports": { + ".": { + "types": "./dist/Crdt.d.ts", + "default": "./dist/Crdt.js" + }, + "./Node": { + "types": "./dist/Node.d.ts", + "default": "./dist/Node.js" + }, + "./LinkedList": { + "types": "./dist/LinkedList.d.ts", + "default": "./dist/LinkedList.js" + }, + "./Interfaces": { + "types": "./dist/Interfaces.d.ts", + "default": "./dist/Interfaces.js" + } + } +} diff --git a/@noctaCrdt/tsconfig.json b/@noctaCrdt/tsconfig.json new file mode 100644 index 00000000..f1f0224a --- /dev/null +++ b/@noctaCrdt/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": ".", + "composite": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "isolatedModules": true + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c43375c8..340a2a67 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,4 @@ packages: - - 'client' - - 'server' + - "client" + - "server" + - "@noctaCrdt" From 14a27f05034c43bca0c17aa89103af4409082b3b Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Tue, 12 Nov 2024 21:04:41 +0900 Subject: [PATCH 02/17] =?UTF-8?q?chore:=20dist=20=ED=8F=B4=EB=8D=94=20git?= =?UTF-8?q?=EC=97=90=20=EB=B0=98=EC=98=81=ED=95=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8140d698..07237331 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ /node_modules **/node_modules -/dist +*/dist /build .DS_Store .env \ No newline at end of file From 4395c6117c22b4522664789a76e0243d473a4495 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Tue, 12 Nov 2024 21:10:45 +0900 Subject: [PATCH 03/17] =?UTF-8?q?build:=20CRDT=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B8=8C=EB=9F=AC=EB=A6=AC=20pnpm=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=A3=BC=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/package.json b/package.json index 9914e855..64822fa6 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "author": "", "license": "ISC", "devDependencies": { + "@noctaCrdt": "workspace:*", "@eslint/js": "^9.14.0", "@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/parser": "^7.18.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 789a39b5..014832a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@eslint/js': specifier: ^9.14.0 version: 9.14.0 + '@noctaCrdt': + specifier: workspace:* + version: link:@noctaCrdt '@typescript-eslint/eslint-plugin': specifier: ^7.18.0 version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.3.3))(eslint@8.57.1)(typescript@5.3.3) @@ -54,6 +57,8 @@ importers: specifier: ~5.3.3 version: 5.3.3 + '@noctaCrdt': {} + client: dependencies: '@pandabox/panda-plugins': From ab75cea66649d6b1573ef1c11859c61f2dd17e4f Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Tue, 12 Nov 2024 21:11:37 +0900 Subject: [PATCH 04/17] =?UTF-8?q?build:=20client=20CRDT=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=A3=BC=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tsconfig 설정 - vite 설정 추가 --- client/package.json | 1 + client/tsconfig.json | 5 ++++- client/vite.config.ts | 6 ++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/client/package.json b/client/package.json index 29a4a505..7381caeb 100644 --- a/client/package.json +++ b/client/package.json @@ -11,6 +11,7 @@ "prepare": "panda codegen" }, "dependencies": { + "@noctaCrdt": "workspace:*", "@pandabox/panda-plugins": "^0.0.8", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/client/tsconfig.json b/client/tsconfig.json index d0a90532..daf6a7b6 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -23,9 +23,12 @@ "@components/*": ["src/components/*"], "@assets/*": ["src/assets/*"], "@features/*": ["src/features/*"], - "@styles/*": ["src/styles/*"] + "@styles/*": ["src/styles/*"], + "@noctaCrdt": ["../@noctaCrdt/dist"], + "@noctaCrdt/*": ["../@noctaCrdt/dist/*"] } }, + "references": [{ "path": "../@noctaCrdt" }], "include": ["src", "*.ts", "*.tsx", "vite.config.ts", "styled-system"], "exclude": ["node_modules"] } diff --git a/client/vite.config.ts b/client/vite.config.ts index 338943d5..5f5414bc 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -1,7 +1,13 @@ import { defineConfig } from "vite"; +import path from "path"; import react from "@vitejs/plugin-react"; import tsconfigPaths from "vite-tsconfig-paths"; export default defineConfig({ plugins: [react(), tsconfigPaths()], + resolve: { + alias: { + "@noctaCrdt": path.resolve(__dirname, "../@noctaCrdt"), + }, + }, }); From 281b5c9c2f4d55482f8795610690bfaa88a7940d Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Tue, 12 Nov 2024 21:15:27 +0900 Subject: [PATCH 05/17] =?UTF-8?q?build:=20server=20CRDT=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=A3=BC=EC=9E=85=20=EB=B0=8F=20webpack=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - nest-cli webpack으로 빌드되도록 설정 추가 - tsconfig 경로를 읽을 수 있도록 설정 --- server/nest-cli.json | 15 ++++++++++++++- server/package.json | 1 + server/tsconfig.json | 7 ++++++- server/webpack.config.js | 25 +++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 server/webpack.config.js diff --git a/server/nest-cli.json b/server/nest-cli.json index f9aa683b..5e07d508 100644 --- a/server/nest-cli.json +++ b/server/nest-cli.json @@ -3,6 +3,19 @@ "collection": "@nestjs/schematics", "sourceRoot": "src", "compilerOptions": { - "deleteOutDir": true + "deleteOutDir": true, + "webpack": true, + "tsConfigPath": "tsconfig.json" + }, + "projects": { + "crdt": { + "type": "library", + "root": "../@noctaCrdt", + "entryFile": "Crdt", + "sourceRoot": "../@noctaCrdt", + "compilerOptions": { + "tsConfigPath": "../@noctaCrdt/tsconfig.json" + } + } } } diff --git a/server/package.json b/server/package.json index 62ffcad0..decccfd3 100644 --- a/server/package.json +++ b/server/package.json @@ -22,6 +22,7 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { + "@noctaCrdt": "workspace:*", "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/mongoose": "^10.1.0", diff --git a/server/tsconfig.json b/server/tsconfig.json index 9d70f891..5b93a0ad 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -1,4 +1,6 @@ { + "extend": "../tsconfig.base.json", + "references": [{ "path": "../@noctaCrdt" }], "compilerOptions": { // 기본 설정 "module": "commonjs", @@ -18,7 +20,10 @@ "strictNullChecks": false, "noImplicitAny": false, "strictBindCallApply": false, - + "paths": { + "@Nocta/crdt": ["../@noctaCrdt/dist"], + "@Nocta/crdt/*": ["../@noctaCrdt/dist/*"] + }, "forceConsistentCasingInFileNames": false, "noFallthroughCasesInSwitch": false, diff --git a/server/webpack.config.js b/server/webpack.config.js new file mode 100644 index 00000000..f45982f9 --- /dev/null +++ b/server/webpack.config.js @@ -0,0 +1,25 @@ +const path = require("path"); + +module.exports = { + mode: "development", + resolve: { + extensions: [".ts", ".js"], + alias: { + "@noctaCrdt": path.resolve(__dirname, "../@noctaCrdt"), + }, + }, + module: { + rules: [ + { + test: /\.ts$/, + use: { + loader: "ts-loader", + options: { + configFile: "tsconfig.json", + }, + }, + exclude: /node_modules/, + }, + ], + }, +}; From 251e6d294c78804d069ea972cf615520ed9afd58 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Tue, 12 Nov 2024 21:20:25 +0900 Subject: [PATCH 06/17] =?UTF-8?q?build:=20server=EC=99=80=20root=EC=97=90?= =?UTF-8?q?=20build=20=EA=B4=80=EB=A0=A8=20scripts=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - server build - webpack으로 빌드되도록 추가 - CRDT라이브러리가 먼저 빌드 되어야 client와 server가 안정적으로 빌드되어 root package의 build 멘트에 cd명령어 추가 --- package.json | 4 ++++ server/package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 64822fa6..01ce7c5f 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,10 @@ "lint": "eslint . --fix", "lint:client": "eslint \"client/src/**/*.{ts,tsx}\" --fix", "lint:server": "eslint \"server/src/**/*.{ts,tsx}\" --fix", + "build": "cd @noctaCrdt && pnpm build && cd .. && pnpm -r build", + "build:lib": "cd @noctaCrdt && pnpm build", + "build:client": "cd client && pnpm build", + "build:server": "cd server && pnpm build", "dev": "pnpm -r --parallel dev" }, "keywords": [], diff --git a/server/package.json b/server/package.json index decccfd3..f13fb011 100644 --- a/server/package.json +++ b/server/package.json @@ -7,7 +7,7 @@ "license": "UNLICENSED", "type": "commonjs", "scripts": { - "build": "nest build", + "build": "nest build --webpack --webpackPath webpack.config.js", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", "start:dev": "nest start --watch", From 196771a14d3bb65348f92ae9132031f00e01b6ea Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Tue, 12 Nov 2024 21:28:27 +0900 Subject: [PATCH 07/17] =?UTF-8?q?chore:=20pnpm-lock=EC=97=90=20CRDT?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pnpm-lock.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 014832a3..21ab87e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -61,6 +61,9 @@ importers: client: dependencies: + '@noctaCrdt': + specifier: workspace:* + version: link:../@noctaCrdt '@pandabox/panda-plugins': specifier: ^0.0.8 version: 0.0.8 @@ -137,6 +140,9 @@ importers: '@nestjs/websockets': specifier: ^10.4.7 version: 10.4.7(@nestjs/common@10.4.6(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.6)(@nestjs/platform-socket.io@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@noctaCrdt': + specifier: workspace:* + version: link:../@noctaCrdt mongoose: specifier: ^8.8.0 version: 8.8.0 From f69719119feeac29699d19bda62ade78e615cc81 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Tue, 12 Nov 2024 21:43:26 +0900 Subject: [PATCH 08/17] =?UTF-8?q?feat:=20CRDT=20=EA=B4=80=EB=A0=A8=20inter?= =?UTF-8?q?faces=20=EC=84=A0=EC=96=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 추후 RemoteInsertOperation과 InsertOperation 합칠 가능성 있음 - 현재는 동작별로 분류하여 명시성을 중점으로 둠 --- @noctaCrdt/Interfaces.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/@noctaCrdt/Interfaces.ts b/@noctaCrdt/Interfaces.ts index e69de29b..0a91de4b 100644 --- a/@noctaCrdt/Interfaces.ts +++ b/@noctaCrdt/Interfaces.ts @@ -0,0 +1,23 @@ +import { NodeId, Node } from "./Node"; + +export interface InsertOperation { + node: Node; +} + +export interface DeleteOperation { + targetId: NodeId | null; + clock: number; +} +export interface RemoteInsertOperation { + node: Node; +} + +export interface RemoteDeleteOperation { + targetId: NodeId | null; + clock: number; +} + +export interface CursorPosition { + clientId: number; + position: number; +} From 780af4b972483192c02ddfac95f6489e59777aa5 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Tue, 12 Nov 2024 21:53:10 +0900 Subject: [PATCH 09/17] =?UTF-8?q?feat:=20Node=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NodeId는 논리적 시계 clock과 숫자형의 client정보로 판단 - Node는 양방향 링크드 리스트 형태로 선언하여 순서비교 메소드 추가 --- @noctaCrdt/Node.ts | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/@noctaCrdt/Node.ts b/@noctaCrdt/Node.ts index e69de29b..2b874457 100644 --- a/@noctaCrdt/Node.ts +++ b/@noctaCrdt/Node.ts @@ -0,0 +1,43 @@ +export class NodeId { + clock: number; + client: number; + + constructor(clock: number, client: number) { + this.clock = clock; + this.client = client; + } + + equals(other: NodeId): boolean { + return this.clock === other.clock && this.client === other.client; + } +} + +export class Node { + id: NodeId; + value: string; + next: NodeId | null; + prev: NodeId | null; + + constructor(value: string, id: NodeId) { + this.id = id; + this.value = value; + this.next = null; + this.prev = null; + } + + /** + * 두 노드의 순서를 비교하여, 이 노드가 다른 노드보다 먼저 와야 하는지 여부를 반환합니다. + * @param node 비교할 노드 + * @returns 순서 결정 결과 + */ + precedes(node: Node): boolean { + // prev가 다르면 비교 불가 + if (!this.prev || !node.prev) return false; + if (!this.prev.equals(node.prev)) return false; + + if (this.id.clock < node.id.clock) return true; + if (this.id.clock === node.id.clock && this.id.client < node.id.client) return true; + + return false; + } +} From a793bc7e9280105d9d82e85915e1c46d9f71d357 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Tue, 12 Nov 2024 21:58:02 +0900 Subject: [PATCH 10/17] =?UTF-8?q?feat:=20LinkedList=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - head와 node정보를 담는 nodeMap 선언 - setNode : 노드맵에 노드 추가 - getNode : 노드 조회 - deleteNode : 특정 노드 제거 후 nodeMap에서 제거 - findByIndex : 특정 인덱스에 해당하는 노드 찾기 - insertAtIndex : 인덱스가 주어지면 추가 - insertById : 추가할 Id가 주어지면 추가 - stringify : 링크드리스트를 문자열로 변환 - spread : 링크드리스트를 배열로 변환 - 추후 react에 사용가능성 있음 --- @noctaCrdt/LinkedList.ts | 239 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 239 insertions(+) diff --git a/@noctaCrdt/LinkedList.ts b/@noctaCrdt/LinkedList.ts index e69de29b..84b589c8 100644 --- a/@noctaCrdt/LinkedList.ts +++ b/@noctaCrdt/LinkedList.ts @@ -0,0 +1,239 @@ +import { NodeId, Node } from "./Node"; +import { InsertOperation } from "./Interfaces"; + +export class LinkedList { + head: NodeId | null; + nodeMap: { [key: string]: Node }; + + constructor(initialStructure?: LinkedList) { + if (initialStructure) { + this.head = initialStructure.head; + this.nodeMap = { ...initialStructure.nodeMap }; + } else { + this.head = null; + this.nodeMap = {}; + } + } + + // 노드맵에 노드 추가 메소드 + setNode(id: NodeId, node: Node): void { + this.nodeMap[JSON.stringify(id)] = node; + } + + // 노드맵에서 노드 조회 메서드 + getNode(id: NodeId | null): Node | null { + if (!id) return null; + return this.nodeMap[JSON.stringify(id)] || null; + } + + // 링크드 리스트에서 노드를 제거하고 nodeMap에서 삭제 + deleteNode(id: NodeId): void { + const nodeToDelete = this.getNode(id); + if (!nodeToDelete) return; + + // 삭제할 노드가 헤드인 경우 + if (this.head && this.head.equals(id)) { + this.head = nodeToDelete.next; + if (nodeToDelete.next) { + const nextNode = this.getNode(nodeToDelete.next); + if (nextNode) { + nextNode.prev = null; + } + } + } else { + // 삭제할 노드의 이전 노드를 찾아 연결을 끊는다. + if (nodeToDelete.prev) { + const prevNode = this.getNode(nodeToDelete.prev); + if (prevNode) { + prevNode.next = nodeToDelete.next; + if (nodeToDelete.next) { + const nextNode = this.getNode(nodeToDelete.next); + if (nextNode) { + nextNode.prev = nodeToDelete.prev; + } + } + } + } + } + + // nodeMap에서 노드 삭제 + delete this.nodeMap[JSON.stringify(id)]; + } + + /** + * 링크드 리스트 안에 특정 인덱스에 해당하는 노드를 찾습니다. + * @param index 찾을 인덱스 (0-부터 출발한다.) + * @returns 해당 인덱스의 노드 + */ + findByIndex(index: number): Node { + if (index < 0) { + throw new Error(`링크드 리스트에서 특정 인덱스${index}가 음수가 입력되었습니다.`); + } + + let currentNodeId = this.head; + let currentIndex = 0; + + while (currentNodeId !== null && currentIndex < index) { + const currentNode = this.getNode(currentNodeId); + if (!currentNode) { + throw new Error( + `링크드 리스트에서 특정 인덱스에 해당하는 노드를 찾다가 에러가 발생했습니다. ${currentIndex}`, + ); + } + currentNodeId = currentNode.next; + currentIndex += 1; + } + + // 유효성 검사 + if (currentNodeId === null) { + throw new Error(`링크드 리스트에서 ${index}를 조회했지만 링크드 리스트가 비어있습니다. `); + } + const node = this.getNode(currentNodeId); + if (!node) { + throw new Error(`링크드 리스트에서 인덱스 ${index}에서 노드를 가져오지 못했습니다. `); + } + + return node; + } + + /** + * 인덱스를 기반으로 노드를 삽입합니다. + * 글자를 작성할때 특정 인덱스에 삽입해야 하기 때문. + * @param index 삽입할 인덱스 (0-based) + * @param value 삽입할 값 + * @param id 삽입할 노드의 식별자 + * @returns 삽입된 노드 + */ + insertAtIndex(index: number, value: string, id: NodeId): InsertOperation { + try { + const node = new Node(value, id); + this.setNode(id, node); + + // 헤드에 삽입하는 경우 + if (!this.head || index === -1) { + node.next = this.head; + node.prev = null; + if (this.head) { + const oldHead = this.getNode(this.head); + if (oldHead) { + oldHead.prev = id; + } + } + + this.head = id; + return { node }; + } + + // 삽입할 위치의 이전 노드 찾기 + const prevNode = this.findByIndex(index - 1); + + node.next = prevNode.next; + prevNode.next = id; + node.prev = prevNode.id; + + // 노드의 다음께 있으면 node를 얻고 다음 노드의 prev가 새로 추가된 노드로 업데이트 + if (node.next) { + const nextNode = this.getNode(node.next); + if (nextNode) { + nextNode.prev = id; + } + } + + return { node }; + } catch (e) { + throw new Error(`링크드 리스트 내에서 insertAtIndex 실패\n${e}`); + } + } + + /** + * 원격 삽입 연산을 처리합니다. + * 원격 연산이 왔을때는 이미 node정보가 완성된 상태로 수신하여 큰 연산이 필요 없다. + * @param node 삽입할 노드 객체 + * @returns 수정된 인덱스 (선택사항) + */ + insertById(node: Node): void { + // 이미 존재하는 노드라면 무시 + if (this.getNode(node.id)) { + return; + } + + // 노드의 prev가 null이면 헤드에 삽입 + if (!node.prev) { + node.next = this.head; + node.prev = null; + + if (this.head) { + const oldHead = this.getNode(this.head); + if (oldHead) { + oldHead.prev = node.id; + } + } + + this.head = node.id; + this.setNode(node.id, node); + return; + } + + // 삽입할 위치의 이전 노드 찾기 + const prevNode = this.getNode(node.prev); + if (!prevNode) { + throw new Error( + `원격 삽입 시, 이전 노드를 찾을 수 없습니다. prevId: ${JSON.stringify(node.prev)}`, + ); + } + + // 새 노드의 다음을 이전 노드의 다음으로 설정 + node.next = prevNode.next; + node.prev = prevNode.id; + + // 이전 노드의 다음을 새 노드로 설정 + prevNode.next = node.id; + + // 새 노드의 다음 노드가 있다면, 그 노드의 prev를 새 노드로 업데이트 + if (node.next) { + const nextNode = this.getNode(node.next); + if (nextNode) { + nextNode.prev = node.id; + } + } + + // 새 노드를 nodeMap에 추가 + this.setNode(node.id, node); + } + + /** + * 현재 리스트를 문자열로 변환합니다. + * @returns 링크드 리스트를 순회하여 얻은 문자열 + */ + stringify(): string { + let currentNodeId = this.head; + let result = ""; + + while (currentNodeId !== null) { + const currentNode = this.getNode(currentNodeId); + if (!currentNode) break; + result += currentNode.value; + currentNodeId = currentNode.next; + } + + return result; + } + + /** + * 현재 리스트를 배열로 변환합니다. + * @returns 배열로 변환된 리스트 + */ + spread(): string[] { + let currentNodeId = this.head; + const result: string[] = []; + + while (currentNodeId !== null) { + const currentNode = this.getNode(currentNodeId); + if (!currentNode) break; + result.push(currentNode.value); + currentNodeId = currentNode.next; + } + + return result; + } +} From 91c251f1adfc6ef201623b034ac44cb64ea8eb3f Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Tue, 12 Nov 2024 22:01:01 +0900 Subject: [PATCH 11/17] =?UTF-8?q?feat:=20CRDT=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 로컬,원격 삽입 알고리즘 구현 - 로컬,원격 삭제 알고리즘 구현 - localInsert : 로컬 삽입 연산 후 원격 전파 - localDelete : 로컬 삭제 연산 후 원격 전파 - remoteInsert : 원격 삽입 연산 수신 반영 - remoteDelete : 원격 삭제 연산 수신 반영 - read: CRDT 내부의 링크드리스트에서 문자열 반환 - spread: CRDT 내부의 링크드리스트에서 텍스트 배열 반환 - getTextLinkedList : 링크드리스트 인스턴스 반환 - 추후 CRDT 확장 가능성 - serialize : CRDT 직렬화 함수 - 추후 역직렬화 함수 추가 가능성 있음 #025 #031 --- @noctaCrdt/Crdt.ts | 129 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/@noctaCrdt/Crdt.ts b/@noctaCrdt/Crdt.ts index e69de29b..74d0b738 100644 --- a/@noctaCrdt/Crdt.ts +++ b/@noctaCrdt/Crdt.ts @@ -0,0 +1,129 @@ +import { LinkedList } from "./LinkedList"; +import { NodeId, Node } from "./Node"; +import { RemoteInsertOperation, RemoteDeleteOperation } from "./Interfaces"; + +export class CRDT { + clock: number; + client: number; + textLinkedList: LinkedList; + + constructor(client: number) { + this.clock = 0; // 이 CRDT의 논리적 시간 설정 + this.client = client; + this.textLinkedList = new LinkedList(); + } + + /** + * 로컬에서 삽입 연산을 수행하고, 원격에 전파할 연산 객체를 반환합니다. + * @param index 삽입할 인덱스 + * @param value 삽입할 값 + * @returns 원격에 전파할 삽입 연산 객체 + */ + localInsert(index: number, value: string): RemoteInsertOperation { + const id = new NodeId((this.clock += 1), this.client); + const remoteInsertion = this.textLinkedList.insertAtIndex(index, value, id); + return { node: remoteInsertion.node }; + } + + /** + * 로컬에서 삭제 연산을 수행하고, 원격에 전파할 연산 객체를 반환합니다. + * @param index 삭제할 인덱스 + * @returns 원격에 전파할 삭제 연산 객체 + */ + localDelete(index: number): RemoteDeleteOperation { + // 유효한 인덱스인지 확인 + if (index < 0 || index >= this.textLinkedList.spread().length) { + throw new Error(`유효하지 않은 인덱스입니다: ${index}`); + } + + // 삭제할 노드 찾기 + const nodeToDelete = this.textLinkedList.findByIndex(index); + if (!nodeToDelete) { + throw new Error(`삭제할 노드를 찾을 수 없습니다. 인덱스: ${index}`); + } + + // 삭제 연산 객체 생성 + const operation: RemoteDeleteOperation = { + targetId: nodeToDelete.id, + clock: this.clock + 1, + }; + + // 로컬 삭제 수행 + this.textLinkedList.deleteNode(nodeToDelete.id); + + // 클록 업데이트 + this.clock += 1; + + return operation; + } + + /** + * 원격에서 삽입 연산을 수신했을 때 처리합니다. + * @param operation 원격 삽입 연산 객체 + */ + remoteInsert(operation: RemoteInsertOperation): void { + const newNodeId = new NodeId(operation.node.id.clock, operation.node.id.client); + const newNode = new Node(operation.node.value, newNodeId); + newNode.next = operation.node.next; + newNode.prev = operation.node.prev; + this.textLinkedList.insertById(newNode); + // 동기화 논리적 시간 + if (this.clock <= newNode.id.clock) { + this.clock = newNode.id.clock + 1; + } + } + + /** + * 원격에서 삭제 연산을 수신했을때 처리합니다. + * @param operation 원격 삭제 연산 객체 + */ + remoteDelete(operation: RemoteDeleteOperation): void { + const { targetId, clock } = operation; + if (targetId) { + this.textLinkedList.deleteNode(targetId); + } + // 동기화 논리적 시간 + if (this.clock <= clock) { + this.clock = clock + 1; + } + } + + /** + * 현재 텍스트를 문자열로 반환합니다. + * @returns 현재 텍스트 + */ + read(): string { + return this.textLinkedList.stringify(); + } + + /** + * 현재 텍스트를 배열로 반환합니다. + * @returns 현재 텍스트 배열 + */ + spread(): string[] { + return this.textLinkedList.spread(); + } + + /** + * textLinkedList를 반환하는 getter 메서드 + * @returns LinkedList 인스턴스 + */ + public getTextLinkedList(): LinkedList { + return this.textLinkedList; + } + + /** + * CRDT의 상태를 직렬화 가능한 객체로 반환합니다. + * @returns 직렬화 가능한 CRDT 상태 + */ + serialize(): any { + return { + clock: this.clock, + client: this.client, + textLinkedList: { + head: this.textLinkedList.head, + nodeMap: this.textLinkedList.nodeMap, + }, + }; + } +} From 0335395a6bd1433e4a10bfe28abafc5fb4079052 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Tue, 12 Nov 2024 22:09:50 +0900 Subject: [PATCH 12/17] =?UTF-8?q?refactor:=20SerializedProps=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 추후 직렬화를 위한 interface 선언 --- @noctaCrdt/Crdt.ts | 4 ++-- @noctaCrdt/Interfaces.ts | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/@noctaCrdt/Crdt.ts b/@noctaCrdt/Crdt.ts index 74d0b738..153bb8fc 100644 --- a/@noctaCrdt/Crdt.ts +++ b/@noctaCrdt/Crdt.ts @@ -1,6 +1,6 @@ import { LinkedList } from "./LinkedList"; import { NodeId, Node } from "./Node"; -import { RemoteInsertOperation, RemoteDeleteOperation } from "./Interfaces"; +import { RemoteInsertOperation, RemoteDeleteOperation, SerializedProps } from "./Interfaces"; export class CRDT { clock: number; @@ -116,7 +116,7 @@ export class CRDT { * CRDT의 상태를 직렬화 가능한 객체로 반환합니다. * @returns 직렬화 가능한 CRDT 상태 */ - serialize(): any { + serialize(): SerializedProps { return { clock: this.clock, client: this.client, diff --git a/@noctaCrdt/Interfaces.ts b/@noctaCrdt/Interfaces.ts index 0a91de4b..f3359df6 100644 --- a/@noctaCrdt/Interfaces.ts +++ b/@noctaCrdt/Interfaces.ts @@ -21,3 +21,12 @@ export interface CursorPosition { clientId: number; position: number; } + +export interface SerializedProps { + clock: number; + client: number; + textLinkedList: { + head: NodeId | null; + nodeMap: { [key: string]: Node }; + }; +} From 512035ee437cbd63c537113f68f07d2d0f7e187e Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Tue, 12 Nov 2024 21:43:26 +0900 Subject: [PATCH 13/17] =?UTF-8?q?feat:=20CRDT=20=EA=B4=80=EB=A0=A8=20inter?= =?UTF-8?q?faces=20=EC=84=A0=EC=96=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 추후 RemoteInsertOperation과 InsertOperation 합칠 가능성 있음 - 현재는 동작별로 분류하여 명시성을 중점으로 둠 --- @noctaCrdt/Interfaces.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/@noctaCrdt/Interfaces.ts b/@noctaCrdt/Interfaces.ts index e69de29b..0a91de4b 100644 --- a/@noctaCrdt/Interfaces.ts +++ b/@noctaCrdt/Interfaces.ts @@ -0,0 +1,23 @@ +import { NodeId, Node } from "./Node"; + +export interface InsertOperation { + node: Node; +} + +export interface DeleteOperation { + targetId: NodeId | null; + clock: number; +} +export interface RemoteInsertOperation { + node: Node; +} + +export interface RemoteDeleteOperation { + targetId: NodeId | null; + clock: number; +} + +export interface CursorPosition { + clientId: number; + position: number; +} From 4c8e3ac87a1f47579e988717b7e0fddec0b08319 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Tue, 12 Nov 2024 21:53:10 +0900 Subject: [PATCH 14/17] =?UTF-8?q?feat:=20Node=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NodeId는 논리적 시계 clock과 숫자형의 client정보로 판단 - Node는 양방향 링크드 리스트 형태로 선언하여 순서비교 메소드 추가 --- @noctaCrdt/Node.ts | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/@noctaCrdt/Node.ts b/@noctaCrdt/Node.ts index e69de29b..2b874457 100644 --- a/@noctaCrdt/Node.ts +++ b/@noctaCrdt/Node.ts @@ -0,0 +1,43 @@ +export class NodeId { + clock: number; + client: number; + + constructor(clock: number, client: number) { + this.clock = clock; + this.client = client; + } + + equals(other: NodeId): boolean { + return this.clock === other.clock && this.client === other.client; + } +} + +export class Node { + id: NodeId; + value: string; + next: NodeId | null; + prev: NodeId | null; + + constructor(value: string, id: NodeId) { + this.id = id; + this.value = value; + this.next = null; + this.prev = null; + } + + /** + * 두 노드의 순서를 비교하여, 이 노드가 다른 노드보다 먼저 와야 하는지 여부를 반환합니다. + * @param node 비교할 노드 + * @returns 순서 결정 결과 + */ + precedes(node: Node): boolean { + // prev가 다르면 비교 불가 + if (!this.prev || !node.prev) return false; + if (!this.prev.equals(node.prev)) return false; + + if (this.id.clock < node.id.clock) return true; + if (this.id.clock === node.id.clock && this.id.client < node.id.client) return true; + + return false; + } +} From b17964a19e33181023aeafa44b5ba4e9965d51c7 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Tue, 12 Nov 2024 21:58:02 +0900 Subject: [PATCH 15/17] =?UTF-8?q?feat:=20LinkedList=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - head와 node정보를 담는 nodeMap 선언 - setNode : 노드맵에 노드 추가 - getNode : 노드 조회 - deleteNode : 특정 노드 제거 후 nodeMap에서 제거 - findByIndex : 특정 인덱스에 해당하는 노드 찾기 - insertAtIndex : 인덱스가 주어지면 추가 - insertById : 추가할 Id가 주어지면 추가 - stringify : 링크드리스트를 문자열로 변환 - spread : 링크드리스트를 배열로 변환 - 추후 react에 사용가능성 있음 --- @noctaCrdt/LinkedList.ts | 239 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 239 insertions(+) diff --git a/@noctaCrdt/LinkedList.ts b/@noctaCrdt/LinkedList.ts index e69de29b..84b589c8 100644 --- a/@noctaCrdt/LinkedList.ts +++ b/@noctaCrdt/LinkedList.ts @@ -0,0 +1,239 @@ +import { NodeId, Node } from "./Node"; +import { InsertOperation } from "./Interfaces"; + +export class LinkedList { + head: NodeId | null; + nodeMap: { [key: string]: Node }; + + constructor(initialStructure?: LinkedList) { + if (initialStructure) { + this.head = initialStructure.head; + this.nodeMap = { ...initialStructure.nodeMap }; + } else { + this.head = null; + this.nodeMap = {}; + } + } + + // 노드맵에 노드 추가 메소드 + setNode(id: NodeId, node: Node): void { + this.nodeMap[JSON.stringify(id)] = node; + } + + // 노드맵에서 노드 조회 메서드 + getNode(id: NodeId | null): Node | null { + if (!id) return null; + return this.nodeMap[JSON.stringify(id)] || null; + } + + // 링크드 리스트에서 노드를 제거하고 nodeMap에서 삭제 + deleteNode(id: NodeId): void { + const nodeToDelete = this.getNode(id); + if (!nodeToDelete) return; + + // 삭제할 노드가 헤드인 경우 + if (this.head && this.head.equals(id)) { + this.head = nodeToDelete.next; + if (nodeToDelete.next) { + const nextNode = this.getNode(nodeToDelete.next); + if (nextNode) { + nextNode.prev = null; + } + } + } else { + // 삭제할 노드의 이전 노드를 찾아 연결을 끊는다. + if (nodeToDelete.prev) { + const prevNode = this.getNode(nodeToDelete.prev); + if (prevNode) { + prevNode.next = nodeToDelete.next; + if (nodeToDelete.next) { + const nextNode = this.getNode(nodeToDelete.next); + if (nextNode) { + nextNode.prev = nodeToDelete.prev; + } + } + } + } + } + + // nodeMap에서 노드 삭제 + delete this.nodeMap[JSON.stringify(id)]; + } + + /** + * 링크드 리스트 안에 특정 인덱스에 해당하는 노드를 찾습니다. + * @param index 찾을 인덱스 (0-부터 출발한다.) + * @returns 해당 인덱스의 노드 + */ + findByIndex(index: number): Node { + if (index < 0) { + throw new Error(`링크드 리스트에서 특정 인덱스${index}가 음수가 입력되었습니다.`); + } + + let currentNodeId = this.head; + let currentIndex = 0; + + while (currentNodeId !== null && currentIndex < index) { + const currentNode = this.getNode(currentNodeId); + if (!currentNode) { + throw new Error( + `링크드 리스트에서 특정 인덱스에 해당하는 노드를 찾다가 에러가 발생했습니다. ${currentIndex}`, + ); + } + currentNodeId = currentNode.next; + currentIndex += 1; + } + + // 유효성 검사 + if (currentNodeId === null) { + throw new Error(`링크드 리스트에서 ${index}를 조회했지만 링크드 리스트가 비어있습니다. `); + } + const node = this.getNode(currentNodeId); + if (!node) { + throw new Error(`링크드 리스트에서 인덱스 ${index}에서 노드를 가져오지 못했습니다. `); + } + + return node; + } + + /** + * 인덱스를 기반으로 노드를 삽입합니다. + * 글자를 작성할때 특정 인덱스에 삽입해야 하기 때문. + * @param index 삽입할 인덱스 (0-based) + * @param value 삽입할 값 + * @param id 삽입할 노드의 식별자 + * @returns 삽입된 노드 + */ + insertAtIndex(index: number, value: string, id: NodeId): InsertOperation { + try { + const node = new Node(value, id); + this.setNode(id, node); + + // 헤드에 삽입하는 경우 + if (!this.head || index === -1) { + node.next = this.head; + node.prev = null; + if (this.head) { + const oldHead = this.getNode(this.head); + if (oldHead) { + oldHead.prev = id; + } + } + + this.head = id; + return { node }; + } + + // 삽입할 위치의 이전 노드 찾기 + const prevNode = this.findByIndex(index - 1); + + node.next = prevNode.next; + prevNode.next = id; + node.prev = prevNode.id; + + // 노드의 다음께 있으면 node를 얻고 다음 노드의 prev가 새로 추가된 노드로 업데이트 + if (node.next) { + const nextNode = this.getNode(node.next); + if (nextNode) { + nextNode.prev = id; + } + } + + return { node }; + } catch (e) { + throw new Error(`링크드 리스트 내에서 insertAtIndex 실패\n${e}`); + } + } + + /** + * 원격 삽입 연산을 처리합니다. + * 원격 연산이 왔을때는 이미 node정보가 완성된 상태로 수신하여 큰 연산이 필요 없다. + * @param node 삽입할 노드 객체 + * @returns 수정된 인덱스 (선택사항) + */ + insertById(node: Node): void { + // 이미 존재하는 노드라면 무시 + if (this.getNode(node.id)) { + return; + } + + // 노드의 prev가 null이면 헤드에 삽입 + if (!node.prev) { + node.next = this.head; + node.prev = null; + + if (this.head) { + const oldHead = this.getNode(this.head); + if (oldHead) { + oldHead.prev = node.id; + } + } + + this.head = node.id; + this.setNode(node.id, node); + return; + } + + // 삽입할 위치의 이전 노드 찾기 + const prevNode = this.getNode(node.prev); + if (!prevNode) { + throw new Error( + `원격 삽입 시, 이전 노드를 찾을 수 없습니다. prevId: ${JSON.stringify(node.prev)}`, + ); + } + + // 새 노드의 다음을 이전 노드의 다음으로 설정 + node.next = prevNode.next; + node.prev = prevNode.id; + + // 이전 노드의 다음을 새 노드로 설정 + prevNode.next = node.id; + + // 새 노드의 다음 노드가 있다면, 그 노드의 prev를 새 노드로 업데이트 + if (node.next) { + const nextNode = this.getNode(node.next); + if (nextNode) { + nextNode.prev = node.id; + } + } + + // 새 노드를 nodeMap에 추가 + this.setNode(node.id, node); + } + + /** + * 현재 리스트를 문자열로 변환합니다. + * @returns 링크드 리스트를 순회하여 얻은 문자열 + */ + stringify(): string { + let currentNodeId = this.head; + let result = ""; + + while (currentNodeId !== null) { + const currentNode = this.getNode(currentNodeId); + if (!currentNode) break; + result += currentNode.value; + currentNodeId = currentNode.next; + } + + return result; + } + + /** + * 현재 리스트를 배열로 변환합니다. + * @returns 배열로 변환된 리스트 + */ + spread(): string[] { + let currentNodeId = this.head; + const result: string[] = []; + + while (currentNodeId !== null) { + const currentNode = this.getNode(currentNodeId); + if (!currentNode) break; + result.push(currentNode.value); + currentNodeId = currentNode.next; + } + + return result; + } +} From 30cb29933a78bfd2e6f35ab679c1e87d9449385c Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Tue, 12 Nov 2024 22:01:01 +0900 Subject: [PATCH 16/17] =?UTF-8?q?feat:=20CRDT=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 로컬,원격 삽입 알고리즘 구현 - 로컬,원격 삭제 알고리즘 구현 - localInsert : 로컬 삽입 연산 후 원격 전파 - localDelete : 로컬 삭제 연산 후 원격 전파 - remoteInsert : 원격 삽입 연산 수신 반영 - remoteDelete : 원격 삭제 연산 수신 반영 - read: CRDT 내부의 링크드리스트에서 문자열 반환 - spread: CRDT 내부의 링크드리스트에서 텍스트 배열 반환 - getTextLinkedList : 링크드리스트 인스턴스 반환 - 추후 CRDT 확장 가능성 - serialize : CRDT 직렬화 함수 - 추후 역직렬화 함수 추가 가능성 있음 #025 #031 --- @noctaCrdt/Crdt.ts | 129 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/@noctaCrdt/Crdt.ts b/@noctaCrdt/Crdt.ts index e69de29b..74d0b738 100644 --- a/@noctaCrdt/Crdt.ts +++ b/@noctaCrdt/Crdt.ts @@ -0,0 +1,129 @@ +import { LinkedList } from "./LinkedList"; +import { NodeId, Node } from "./Node"; +import { RemoteInsertOperation, RemoteDeleteOperation } from "./Interfaces"; + +export class CRDT { + clock: number; + client: number; + textLinkedList: LinkedList; + + constructor(client: number) { + this.clock = 0; // 이 CRDT의 논리적 시간 설정 + this.client = client; + this.textLinkedList = new LinkedList(); + } + + /** + * 로컬에서 삽입 연산을 수행하고, 원격에 전파할 연산 객체를 반환합니다. + * @param index 삽입할 인덱스 + * @param value 삽입할 값 + * @returns 원격에 전파할 삽입 연산 객체 + */ + localInsert(index: number, value: string): RemoteInsertOperation { + const id = new NodeId((this.clock += 1), this.client); + const remoteInsertion = this.textLinkedList.insertAtIndex(index, value, id); + return { node: remoteInsertion.node }; + } + + /** + * 로컬에서 삭제 연산을 수행하고, 원격에 전파할 연산 객체를 반환합니다. + * @param index 삭제할 인덱스 + * @returns 원격에 전파할 삭제 연산 객체 + */ + localDelete(index: number): RemoteDeleteOperation { + // 유효한 인덱스인지 확인 + if (index < 0 || index >= this.textLinkedList.spread().length) { + throw new Error(`유효하지 않은 인덱스입니다: ${index}`); + } + + // 삭제할 노드 찾기 + const nodeToDelete = this.textLinkedList.findByIndex(index); + if (!nodeToDelete) { + throw new Error(`삭제할 노드를 찾을 수 없습니다. 인덱스: ${index}`); + } + + // 삭제 연산 객체 생성 + const operation: RemoteDeleteOperation = { + targetId: nodeToDelete.id, + clock: this.clock + 1, + }; + + // 로컬 삭제 수행 + this.textLinkedList.deleteNode(nodeToDelete.id); + + // 클록 업데이트 + this.clock += 1; + + return operation; + } + + /** + * 원격에서 삽입 연산을 수신했을 때 처리합니다. + * @param operation 원격 삽입 연산 객체 + */ + remoteInsert(operation: RemoteInsertOperation): void { + const newNodeId = new NodeId(operation.node.id.clock, operation.node.id.client); + const newNode = new Node(operation.node.value, newNodeId); + newNode.next = operation.node.next; + newNode.prev = operation.node.prev; + this.textLinkedList.insertById(newNode); + // 동기화 논리적 시간 + if (this.clock <= newNode.id.clock) { + this.clock = newNode.id.clock + 1; + } + } + + /** + * 원격에서 삭제 연산을 수신했을때 처리합니다. + * @param operation 원격 삭제 연산 객체 + */ + remoteDelete(operation: RemoteDeleteOperation): void { + const { targetId, clock } = operation; + if (targetId) { + this.textLinkedList.deleteNode(targetId); + } + // 동기화 논리적 시간 + if (this.clock <= clock) { + this.clock = clock + 1; + } + } + + /** + * 현재 텍스트를 문자열로 반환합니다. + * @returns 현재 텍스트 + */ + read(): string { + return this.textLinkedList.stringify(); + } + + /** + * 현재 텍스트를 배열로 반환합니다. + * @returns 현재 텍스트 배열 + */ + spread(): string[] { + return this.textLinkedList.spread(); + } + + /** + * textLinkedList를 반환하는 getter 메서드 + * @returns LinkedList 인스턴스 + */ + public getTextLinkedList(): LinkedList { + return this.textLinkedList; + } + + /** + * CRDT의 상태를 직렬화 가능한 객체로 반환합니다. + * @returns 직렬화 가능한 CRDT 상태 + */ + serialize(): any { + return { + clock: this.clock, + client: this.client, + textLinkedList: { + head: this.textLinkedList.head, + nodeMap: this.textLinkedList.nodeMap, + }, + }; + } +} From 84e425169cd4b8ae2a1b4728db366bcd1f46d8e4 Mon Sep 17 00:00:00 2001 From: hyonun321 Date: Tue, 12 Nov 2024 22:09:50 +0900 Subject: [PATCH 17/17] =?UTF-8?q?refactor:=20SerializedProps=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 추후 직렬화를 위한 interface 선언 --- @noctaCrdt/Crdt.ts | 4 ++-- @noctaCrdt/Interfaces.ts | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/@noctaCrdt/Crdt.ts b/@noctaCrdt/Crdt.ts index 74d0b738..153bb8fc 100644 --- a/@noctaCrdt/Crdt.ts +++ b/@noctaCrdt/Crdt.ts @@ -1,6 +1,6 @@ import { LinkedList } from "./LinkedList"; import { NodeId, Node } from "./Node"; -import { RemoteInsertOperation, RemoteDeleteOperation } from "./Interfaces"; +import { RemoteInsertOperation, RemoteDeleteOperation, SerializedProps } from "./Interfaces"; export class CRDT { clock: number; @@ -116,7 +116,7 @@ export class CRDT { * CRDT의 상태를 직렬화 가능한 객체로 반환합니다. * @returns 직렬화 가능한 CRDT 상태 */ - serialize(): any { + serialize(): SerializedProps { return { clock: this.clock, client: this.client, diff --git a/@noctaCrdt/Interfaces.ts b/@noctaCrdt/Interfaces.ts index 0a91de4b..f3359df6 100644 --- a/@noctaCrdt/Interfaces.ts +++ b/@noctaCrdt/Interfaces.ts @@ -21,3 +21,12 @@ export interface CursorPosition { clientId: number; position: number; } + +export interface SerializedProps { + clock: number; + client: number; + textLinkedList: { + head: NodeId | null; + nodeMap: { [key: string]: Node }; + }; +}