Skip to content

Commit 03cd9ac

Browse files
authored
Feat/#111-BK: 회의록 CRDT 추가 (#115)
회의록 동시 편집 기능에 사용될 자료구조
1 parent 84658fa commit 03cd9ac

File tree

4 files changed

+247
-1
lines changed

4 files changed

+247
-1
lines changed

client/src/utils/crdt/index.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import LinkedList from './linked-list';
2+
import { Identifier, Node } from './node';
3+
4+
interface RemoteInsertOperation {
5+
prevId: Identifier | null;
6+
node: Node;
7+
}
8+
9+
interface RemoteRemoveOperation {
10+
targetId: Identifier | null;
11+
clock: number;
12+
}
13+
14+
class CRDT {
15+
clock: number;
16+
client: number;
17+
private structure: LinkedList;
18+
19+
constructor(initialclock: number, client: number) {
20+
this.clock = initialclock;
21+
this.client = client;
22+
this.structure = new LinkedList();
23+
}
24+
25+
generateNode(letter: string) {
26+
const id = this.generateIdentifier();
27+
return new Node(letter, id);
28+
}
29+
30+
generateIdentifier() {
31+
return new Identifier(this.clock++, this.client);
32+
}
33+
34+
localInsert(index: number, letter: string): RemoteInsertOperation {
35+
const node = this.generateNode(letter);
36+
const prevId = this.structure.insertByIndex(index, node);
37+
38+
return { prevId, node };
39+
}
40+
41+
localDelete(index: number): RemoteRemoveOperation {
42+
const targetId = this.structure.deleteByIndex(index);
43+
44+
return { targetId, clock: this.clock };
45+
}
46+
47+
remoteInsert({ prevId, node }: RemoteInsertOperation) {
48+
const prevIndex = this.structure.insertById(prevId, node);
49+
50+
if (++this.clock < node.id.clock) {
51+
this.clock = node.id.clock + 1;
52+
}
53+
54+
return prevIndex;
55+
}
56+
57+
remoteDelete({ targetId, clock }: RemoteRemoveOperation) {
58+
const targetIndex = this.structure.deleteById(targetId);
59+
60+
if (++this.clock < clock) {
61+
this.clock = clock + 1;
62+
}
63+
64+
return targetIndex;
65+
}
66+
67+
read() {
68+
return this.structure.stringify();
69+
}
70+
}
71+
72+
export default CRDT;
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { Identifier, Node } from './node';
2+
3+
type RemoteIdentifier = Identifier | null;
4+
5+
type ModifiedIndex = number | null;
6+
7+
export default class LinkedList {
8+
head?: Node;
9+
10+
insertByIndex(index: number, node: Node): RemoteIdentifier {
11+
try {
12+
if (!this.head || index === -1) {
13+
node.next = this.head;
14+
this.head = node;
15+
16+
return null;
17+
}
18+
const prevNode = this.findByIndex(index);
19+
20+
node.next = prevNode.next;
21+
prevNode.next = node;
22+
23+
const { id: prevNodeId } = prevNode;
24+
return prevNodeId;
25+
} catch (e) {
26+
console.log(`insertByIndex 실패 ^^\n${e}`);
27+
28+
return null;
29+
}
30+
}
31+
32+
deleteByIndex(index: number): RemoteIdentifier {
33+
try {
34+
if (!index) {
35+
if (!this.head) throw new Error('head가 없는데 어떻게 삭제하셨나요 ^^');
36+
37+
this.head = this.head.next;
38+
39+
return null;
40+
}
41+
42+
const prevNode = this.findByIndex(index - 1);
43+
44+
const targetNode = prevNode.next;
45+
46+
prevNode.next = targetNode?.next;
47+
48+
if (!targetNode || !targetNode.id) return null;
49+
50+
return targetNode.id;
51+
} catch (e) {
52+
console.log(`deleteByIndex 실패 ^^\n${e}`);
53+
54+
return null;
55+
}
56+
}
57+
58+
insertById(id: RemoteIdentifier, node: Node): ModifiedIndex {
59+
try {
60+
if (id === null) {
61+
node.next = this.head;
62+
this.head = node;
63+
64+
return 0;
65+
}
66+
67+
const { node: prevNode, index: prevIndex } = this.findById(id);
68+
69+
node.next = prevNode.next;
70+
prevNode.next = node;
71+
72+
return prevIndex + 1;
73+
} catch (e) {
74+
console.log(`insertById 실패 ^^\n${e}`);
75+
76+
return null;
77+
}
78+
}
79+
80+
deleteById(id: RemoteIdentifier): ModifiedIndex {
81+
try {
82+
if (!id) {
83+
if (!this.head) throw new Error('일어날 수 없는 일이 발생했어요 ^^');
84+
85+
this.head = this.head.next;
86+
87+
return null;
88+
}
89+
90+
const { node: targetNode, index: targetIndex } = this.findById(id);
91+
const prevNode = this.findByIndex(targetIndex - 1);
92+
93+
prevNode.next = targetNode.next;
94+
95+
return targetIndex;
96+
} catch (e) {
97+
console.log(`deleteById 실패 ^^\n${e}`);
98+
99+
return null;
100+
}
101+
}
102+
103+
stringify(): string {
104+
if (!this.head) {
105+
return '';
106+
}
107+
108+
let node: Node | undefined = this.head;
109+
let result = '';
110+
111+
while (node) {
112+
result += node.value;
113+
node = node.next;
114+
}
115+
return result;
116+
}
117+
118+
private findByIndex(index: number): Node {
119+
let count = 0;
120+
let currentNode: Node | undefined = this.head;
121+
122+
while (count < index && currentNode) {
123+
currentNode = currentNode.next;
124+
count++;
125+
}
126+
127+
if (!currentNode) throw new Error('없는 인덱스인데요 ^^');
128+
129+
return currentNode;
130+
}
131+
132+
private findById(id: Identifier) {
133+
let count = 0;
134+
let currentNode: Node | undefined = this.head;
135+
136+
while (currentNode) {
137+
if (JSON.stringify(currentNode.id) === JSON.stringify(id)) {
138+
return { node: currentNode, index: count };
139+
}
140+
141+
currentNode = currentNode.next;
142+
count++;
143+
}
144+
145+
throw new Error('없는 노드인데요 ^^');
146+
}
147+
}

client/src/utils/crdt/node.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
export class Identifier {
2+
clock: number;
3+
client: number;
4+
5+
constructor(clock: number, client: number) {
6+
this.clock = clock;
7+
this.client = client;
8+
}
9+
}
10+
11+
export class Node {
12+
id: Identifier;
13+
value?: string;
14+
next?: Node;
15+
16+
constructor(value: string, id: Identifier) {
17+
this.id = id;
18+
this.value = value;
19+
}
20+
21+
precedes(id: Identifier) {
22+
if (this.id.clock < id.clock) return true;
23+
24+
if (this.id.clock === id.clock && this.id.client < id.client) return true;
25+
26+
return false;
27+
}
28+
}

client/src/utils/index.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)