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/#019 crdt 동시 편집 구현 #100

Merged
merged 19 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/node_modules
**/node_modules

/dist
*/dist
/build
.DS_Store
.env
129 changes: 129 additions & 0 deletions @noctaCrdt/Crdt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { LinkedList } from "./LinkedList";
import { NodeId, Node } from "./Node";
import { RemoteInsertOperation, RemoteDeleteOperation, SerializedProps } 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(): SerializedProps {
return {
clock: this.clock,
client: this.client,
textLinkedList: {
head: this.textLinkedList.head,
nodeMap: this.textLinkedList.nodeMap,
},
};
}
}
32 changes: 32 additions & 0 deletions @noctaCrdt/Interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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;
}

export interface SerializedProps {
clock: number;
client: number;
textLinkedList: {
head: NodeId | null;
nodeMap: { [key: string]: Node };
};
}
Loading
Loading