Skip to content

Commit d36152f

Browse files
authored
Merge pull request #136 from boostcampwm-2022/dev
Deploy: 3주차 결과
2 parents 44c3951 + 66c6239 commit d36152f

36 files changed

+1249
-86
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: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import LinkedList, {
2+
RemoteDeleteOperation,
3+
RemoteInsertOperation,
4+
} from './linked-list';
5+
import { Identifier } from './node';
6+
7+
class CRDT {
8+
private clock: number;
9+
private client: number;
10+
private structure: LinkedList;
11+
12+
constructor(
13+
initialclock: number = 1,
14+
client: number = 0,
15+
initialStructure: LinkedList,
16+
) {
17+
this.clock = initialclock;
18+
this.client = client;
19+
20+
Object.setPrototypeOf(initialStructure, LinkedList.prototype);
21+
this.structure = initialStructure as LinkedList;
22+
}
23+
24+
get data() {
25+
return this.structure;
26+
}
27+
28+
localInsert(index: number, letter: string): RemoteInsertOperation {
29+
const id = new Identifier(this.clock++, this.client);
30+
31+
const remoteInsertion = this.structure.insertByIndex(index, letter, id);
32+
33+
return remoteInsertion;
34+
}
35+
36+
localDelete(index: number): RemoteDeleteOperation {
37+
const targetId = this.structure.deleteByIndex(index);
38+
39+
return { targetId, clock: this.clock };
40+
}
41+
42+
remoteInsert({ prevId, node }: RemoteInsertOperation) {
43+
const prevIndex = this.structure.insertById(prevId, node);
44+
45+
if (++this.clock < node.id.clock) {
46+
this.clock = node.id.clock + 1;
47+
}
48+
49+
return prevIndex;
50+
}
51+
52+
remoteDelete({ targetId, clock }: RemoteDeleteOperation) {
53+
const targetIndex = this.structure.deleteById(targetId);
54+
55+
if (++this.clock < clock) {
56+
this.clock = clock + 1;
57+
}
58+
59+
return targetIndex;
60+
}
61+
62+
read() {
63+
return this.structure.stringify();
64+
}
65+
}
66+
67+
export default CRDT;

@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: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import { Identifier, Node } from './node';
2+
3+
type RemoteIdentifier = Identifier | null;
4+
5+
type ModifiedIndex = number | null;
6+
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+
17+
export default class LinkedList {
18+
head?: Node;
19+
20+
insertByIndex(
21+
index: number,
22+
letter: string,
23+
id: Identifier,
24+
): RemoteInsertOperation {
25+
const node = new Node(letter, id);
26+
27+
try {
28+
// insertion to head
29+
if (!this.head || index === -1) {
30+
node.next = this.head;
31+
node.prev = null;
32+
33+
this.head = node;
34+
35+
return { prevId: null, node };
36+
}
37+
38+
const prevNode = this.findByIndex(index);
39+
40+
node.next = prevNode.next;
41+
prevNode.next = node;
42+
43+
const { id: prevId } = prevNode;
44+
45+
node.prev = prevId;
46+
47+
return { prevId, node };
48+
} catch (e) {
49+
throw new Error(`insertByIndex 실패 ^^\n${e}`);
50+
}
51+
}
52+
53+
deleteByIndex(index: number): RemoteIdentifier {
54+
try {
55+
// head deleted
56+
if (index === 0) {
57+
if (!this.head) throw new Error('head가 없는데 어떻게 삭제하셨나요 ^^');
58+
59+
if (!this.head.next) {
60+
this.head = undefined;
61+
return null;
62+
}
63+
64+
this.head.next.prev = null;
65+
this.head = this.head.next;
66+
67+
return null;
68+
}
69+
70+
const prevNode = this.findByIndex(index - 1);
71+
72+
const targetNode = prevNode.next;
73+
74+
prevNode.next = targetNode?.next;
75+
76+
if (!targetNode || !targetNode.id) return null;
77+
78+
return targetNode.id;
79+
} catch (e) {
80+
throw new Error(`deleteByIndex 실패 ^^\n${e}`);
81+
}
82+
}
83+
84+
insertById(id: RemoteIdentifier, node: Node): ModifiedIndex {
85+
try {
86+
let prevNode, prevIndex;
87+
88+
// insertion to head
89+
if (id === null) {
90+
// 기존 head가 없거나 현재 node가 선행하는 경우
91+
if (!this.head || node.precedes(this.head)) {
92+
node.next = this.head;
93+
this.head = node;
94+
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;
105+
}
106+
107+
// prevNode에 연결된 노드가 현재 node에 선행하는 경우
108+
while (prevNode.next && prevNode.next.precedes(node)) {
109+
prevNode = prevNode.next;
110+
prevIndex++;
111+
}
112+
113+
node.next = prevNode.next;
114+
node.prev = prevNode.id;
115+
prevNode.next = node;
116+
117+
return prevIndex + 1;
118+
} catch (e) {
119+
console.log(`insertById 실패 ^^\n${e}`);
120+
121+
return null;
122+
}
123+
}
124+
125+
deleteById(id: RemoteIdentifier): ModifiedIndex {
126+
try {
127+
if (!id) {
128+
if (!this.head) throw new Error('일어날 수 없는 일이 발생했어요 ^^');
129+
130+
this.head = this.head.next;
131+
132+
return null;
133+
}
134+
135+
const { node: targetNode, index: targetIndex } = this.findById(id);
136+
const prevNode = this.findByIndex(targetIndex - 1);
137+
138+
prevNode.next = targetNode.next;
139+
140+
return targetIndex;
141+
} catch (e) {
142+
console.log(`deleteById 실패 ^^\n${e}`);
143+
144+
return null;
145+
}
146+
}
147+
148+
stringify(): string {
149+
let node: Node | undefined = this.head;
150+
let result = '';
151+
152+
while (node) {
153+
result += node.value;
154+
node = node.next;
155+
}
156+
157+
return result;
158+
}
159+
160+
private findByIndex(index: number): Node {
161+
let count = 0;
162+
let currentNode: Node | undefined = this.head;
163+
164+
while (count < index && currentNode) {
165+
currentNode = currentNode.next;
166+
count++;
167+
}
168+
169+
if (!currentNode) throw new Error('없는 인덱스인데요 ^^');
170+
171+
return currentNode;
172+
}
173+
174+
private findById(id: Identifier) {
175+
let count = 0;
176+
let currentNode: Node | undefined = this.head;
177+
178+
while (currentNode) {
179+
if (JSON.stringify(currentNode.id) === JSON.stringify(id)) {
180+
return { node: currentNode, index: count };
181+
}
182+
183+
currentNode = currentNode.next;
184+
count++;
185+
}
186+
187+
throw new Error('없는 노드인데요 ^^');
188+
}
189+
}

0 commit comments

Comments
 (0)