Skip to content

Commit daeb250

Browse files
authored
Feat/#132-K: CRDT 패키지 개선 (#135)
1 parent 4d97cf1 commit daeb250

File tree

10 files changed

+225
-58
lines changed

10 files changed

+225
-58
lines changed

@wabinar-crdt/convergence.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import CRDT from './index';
2+
import LinkedList, { RemoteInsertOperation } from './linked-list';
3+
import { Node } from './node';
4+
5+
/**
6+
* utilities
7+
* 바로 전달하면 같은 인스턴스를 가리키게 되어 remote operation 의미가 사라짐
8+
*/
9+
const deepCopyRemoteInsertion = (op: RemoteInsertOperation) => {
10+
const { prevId, node } = op;
11+
12+
const copy = { ...node };
13+
Object.setPrototypeOf(copy, Node.prototype);
14+
15+
return { prevId, node: copy as Node };
16+
};
17+
18+
const remoteInsertThroughSocket = (crdt: CRDT, op: RemoteInsertOperation) => {
19+
const copy = deepCopyRemoteInsertion(op);
20+
crdt.remoteInsert(copy);
21+
};
22+
23+
/**
24+
* 모든 remote site 문자열이 일치하는지 확인
25+
*/
26+
const convergenceCheck = () => {
27+
expect(원희.read()).toEqual(주영.read());
28+
expect(주영.read()).toEqual(도훈.read());
29+
expect(도훈.read()).toEqual(세영.read());
30+
};
31+
32+
/**
33+
* 테스트용 site들
34+
*/
35+
const 원희 = new CRDT(1, 1, new LinkedList());
36+
const 주영 = new CRDT(1, 2, new LinkedList());
37+
const 도훈 = new CRDT(1, 3, new LinkedList());
38+
const 세영 = new CRDT(1, 4, new LinkedList());
39+
40+
describe('Convergence', () => {
41+
it('하나의 site에서 삽입', () => {
42+
const 원희remotes = [
43+
원희.localInsert(-1, '녕'),
44+
원희.localInsert(-1, '안'),
45+
];
46+
47+
[주영, 도훈, 세영].forEach(() => {
48+
원희remotes.forEach((op) => remoteInsertThroughSocket(, op));
49+
});
50+
51+
convergenceCheck();
52+
});
53+
54+
it('여러 site에서 같은 위치에 삽입', () => {
55+
const 도훈remote = 도훈.localInsert(0, '왭');
56+
const 세영remote = 세영.localInsert(0, '?');
57+
58+
remoteInsertThroughSocket(도훈, 세영remote);
59+
remoteInsertThroughSocket(세영, 도훈remote);
60+
61+
[원희, 주영].forEach(() => {
62+
remoteInsertThroughSocket(, 도훈remote);
63+
remoteInsertThroughSocket(, 세영remote);
64+
});
65+
66+
convergenceCheck();
67+
});
68+
69+
it('여러 site에서 다른 위치에 삽입', () => {
70+
const 원희remote = 원희.localInsert(0, '네');
71+
const 주영remote = 주영.localInsert(1, '!');
72+
73+
remoteInsertThroughSocket(원희, 주영remote);
74+
remoteInsertThroughSocket(주영, 원희remote);
75+
76+
[도훈, 세영].forEach(() => {
77+
remoteInsertThroughSocket(, 원희remote);
78+
remoteInsertThroughSocket(, 주영remote);
79+
});
80+
81+
convergenceCheck();
82+
});
83+
});

@wabinar-crdt/crdt.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import CRDT from './index';
2+
import LinkedList from './linked-list';
3+
4+
describe('Local operation', () => {
5+
const initialStructure = new LinkedList();
6+
7+
const = new CRDT(1, 1, initialStructure);
8+
9+
it('head에 삽입', () => {
10+
let innerText = .read();
11+
12+
.localInsert(-1, '녕');
13+
expect(.read()).toEqual('녕' + innerText);
14+
15+
innerText = .read();
16+
17+
.localInsert(-1, '안');
18+
expect(.read()).toEqual('안' + innerText);
19+
});
20+
21+
it('tail에 삽입', () => {
22+
let innerText = .read();
23+
24+
.localInsert(1, '하');
25+
expect(.read()).toEqual(innerText + '하');
26+
});
27+
28+
it('head 삭제', () => {
29+
let innerText = .read();
30+
31+
.localDelete(0);
32+
expect(.read()).toEqual(innerText.replace('안', ''));
33+
});
34+
});

@wabinar-crdt/index.ts

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
1-
import LinkedList from './linked-list';
2-
import { Identifier, Node } from './node';
3-
4-
export interface RemoteInsertOperation {
5-
prevId: Identifier | null;
6-
node: Node;
7-
}
8-
9-
export interface RemoteDeleteOperation {
10-
targetId: Identifier | null;
11-
clock: number;
12-
}
1+
import LinkedList, {
2+
RemoteDeleteOperation,
3+
RemoteInsertOperation,
4+
} from './linked-list';
5+
import { Identifier } from './node';
136

147
class CRDT {
158
private clock: number;
@@ -32,20 +25,12 @@ class CRDT {
3225
return this.structure;
3326
}
3427

35-
generateNode(letter: string) {
36-
const id = this.generateIdentifier();
37-
return new Node(letter, id);
38-
}
39-
40-
generateIdentifier() {
41-
return new Identifier(this.clock++, this.client);
42-
}
43-
4428
localInsert(index: number, letter: string): RemoteInsertOperation {
45-
const node = this.generateNode(letter);
46-
const prevId = this.structure.insertByIndex(index, node);
29+
const id = new Identifier(this.clock++, this.client);
30+
31+
const remoteInsertion = this.structure.insertByIndex(index, letter, id);
4732

48-
return { prevId, node };
33+
return remoteInsertion;
4934
}
5035

5136
localDelete(index: number): RemoteDeleteOperation {

@wabinar-crdt/jest.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = {
2+
preset: 'ts-jest', // to use typescript
3+
verbose: true,
4+
modulePathIgnorePatterns: ['<rootDir>/dist/'],
5+
};

@wabinar-crdt/linked-list.ts

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,64 @@ type RemoteIdentifier = Identifier | null;
44

55
type ModifiedIndex = number | null;
66

7+
export interface RemoteInsertOperation {
8+
prevId: Identifier | null;
9+
node: Node;
10+
}
11+
12+
export interface RemoteDeleteOperation {
13+
targetId: Identifier | null;
14+
clock: number;
15+
}
16+
717
export default class LinkedList {
818
head?: Node;
919

10-
insertByIndex(index: number, node: Node): RemoteIdentifier {
20+
insertByIndex(
21+
index: number,
22+
letter: string,
23+
id: Identifier,
24+
): RemoteInsertOperation {
25+
const node = new Node(letter, id);
26+
1127
try {
28+
// insertion to head
1229
if (!this.head || index === -1) {
1330
node.next = this.head;
31+
node.prev = null;
32+
1433
this.head = node;
1534

16-
return null;
35+
return { prevId: null, node };
1736
}
37+
1838
const prevNode = this.findByIndex(index);
1939

2040
node.next = prevNode.next;
2141
prevNode.next = node;
2242

23-
const { id: prevNodeId } = prevNode;
24-
return prevNodeId;
25-
} catch (e) {
26-
console.log(`insertByIndex 실패 ^^\n${e}`);
43+
const { id: prevId } = prevNode;
2744

28-
return null;
45+
node.prev = prevId;
46+
47+
return { prevId, node };
48+
} catch (e) {
49+
throw new Error(`insertByIndex 실패 ^^\n${e}`);
2950
}
3051
}
3152

3253
deleteByIndex(index: number): RemoteIdentifier {
3354
try {
34-
if (!index) {
55+
// head deleted
56+
if (index === 0) {
3557
if (!this.head) throw new Error('head가 없는데 어떻게 삭제하셨나요 ^^');
3658

59+
if (!this.head.next) {
60+
this.head = undefined;
61+
return null;
62+
}
63+
64+
this.head.next.prev = null;
3765
this.head = this.head.next;
3866

3967
return null;
@@ -49,24 +77,41 @@ export default class LinkedList {
4977

5078
return targetNode.id;
5179
} catch (e) {
52-
console.log(`deleteByIndex 실패 ^^\n${e}`);
53-
54-
return null;
80+
throw new Error(`deleteByIndex 실패 ^^\n${e}`);
5581
}
5682
}
5783

5884
insertById(id: RemoteIdentifier, node: Node): ModifiedIndex {
5985
try {
86+
let prevNode, prevIndex;
87+
88+
// insertion to head
6089
if (id === null) {
61-
node.next = this.head;
62-
this.head = node;
90+
// 기존 head가 없거나 현재 node가 선행하는 경우
91+
if (!this.head || node.precedes(this.head)) {
92+
node.next = this.head;
93+
this.head = node;
6394

64-
return 0;
95+
return null;
96+
}
97+
98+
prevNode = this.head;
99+
prevIndex = 0;
100+
} else {
101+
let { node: targetNode, index: targetIndex } = this.findById(id);
102+
103+
prevNode = targetNode;
104+
prevIndex = targetIndex;
65105
}
66106

67-
const { node: prevNode, index: prevIndex } = this.findById(id);
107+
// prevNode에 연결된 노드가 현재 node에 선행하는 경우
108+
while (prevNode.next && prevNode.next.precedes(node)) {
109+
prevNode = prevNode.next;
110+
prevIndex++;
111+
}
68112

69113
node.next = prevNode.next;
114+
node.prev = prevNode.id;
70115
prevNode.next = node;
71116

72117
return prevIndex + 1;
@@ -101,17 +146,14 @@ export default class LinkedList {
101146
}
102147

103148
stringify(): string {
104-
if (!this.head) {
105-
return '';
106-
}
107-
108149
let node: Node | undefined = this.head;
109150
let result = '';
110151

111152
while (node) {
112153
result += node.value;
113154
node = node.next;
114155
}
156+
115157
return result;
116158
}
117159

@wabinar-crdt/node.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,21 @@ export class Node {
1212
id: Identifier;
1313
value?: string;
1414
next?: Node;
15+
prev?: Identifier | null;
1516

1617
constructor(value: string, id: Identifier) {
1718
this.id = id;
1819
this.value = value;
1920
}
2021

21-
precedes(id: Identifier) {
22-
if (this.id.clock < id.clock) return true;
22+
precedes(node: Node) {
23+
// prev가 다른 경우는 비교 대상에서 제외
24+
if (JSON.stringify(this.prev) !== JSON.stringify(node.prev)) return false;
2325

24-
if (this.id.clock === id.clock && this.id.client < id.client) return true;
26+
if (node.id.clock < this.id.clock) return true;
27+
28+
if (this.id.clock === node.id.clock && this.id.client < node.id.client)
29+
return true;
2530

2631
return false;
2732
}

@wabinar-crdt/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,11 @@
22
"name": "@wabinar/crdt",
33
"version": "1.0.0",
44
"description": "CRDT for wabinar",
5-
"license": "MIT"
5+
"license": "MIT",
6+
"scripts": {
7+
"test": "jest"
8+
},
9+
"devDependencies": {
10+
"jest": "^29.3.1"
11+
}
612
}

@wabinar-crdt/tsconfig.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES6",
4+
"esModuleInterop": true
5+
}
6+
}

client/src/hooks/useCRDT.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import CRDT, {
1+
import CRDT from '@wabinar/crdt';
2+
import LinkedList, {
23
RemoteInsertOperation,
34
RemoteDeleteOperation,
4-
} from '@wabinar/crdt';
5-
import LinkedList from '@wabinar/crdt/linked-list';
5+
} from '@wabinar/crdt/linked-list';
66
import { useRef } from 'react';
77
import { useUserContext } from 'src/hooks/useUserContext';
88

0 commit comments

Comments
 (0)