Skip to content

Commit

Permalink
feat: Implement CRDT register and map with associated types (#64)
Browse files Browse the repository at this point in the history
* feat: Implement CRDT register and map with associated types

* chore: Reorganize file path from crdt/core to core/crdt
  • Loading branch information
kwaksj329 authored Nov 14, 2024
1 parent fb15601 commit 3f5a958
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 0 deletions.
25 changes: 25 additions & 0 deletions client/src/core/crdt.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export type Point = {
x: number;
y: number;
};

export type StrokeStyle = {
color: string;
width: number;
};

export type Stroke = {
points: Point[];
style: StrokeStyle;
};

export type RegisterState<T> = [peerId: string, timestamp: number, value: T];

export type MapState = {
[key: string]: RegisterState<Stroke | null>;
};

export type CRDTMessage = {
type: 'sync' | 'update';
state: MapState | { key: string; register: RegisterState<Stroke | null> };
};
90 changes: 90 additions & 0 deletions client/src/core/crdt/LWWMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { MapState, RegisterState, Stroke } from '../crdt.types.ts';
import { LWWRegister } from './LWWRegister.ts';

export class LWWMap {
readonly id: string;
#data: Map<string, LWWRegister<Stroke | null>>;

constructor(id: string, initialState: MapState = {}) {
this.id = id;
this.#data = new Map();

for (const [key, registerState] of Object.entries(initialState)) {
this.#data.set(key, new LWWRegister(this.id, registerState));
}
}

get state(): MapState {
const state: MapState = {};
for (const [key, register] of this.#data.entries()) {
state[key] = register.state;
}
return state;
}

get strokes(): { id: string; stroke: Stroke }[] {
const result = [];
for (const [key, register] of this.#data.entries()) {
const value = register.value;
if (value !== null) {
result.push({ id: key, stroke: value });
}
}
return result;
}

// 선 생성
addStroke(stroke: Stroke): string {
const timestamp = Date.now();
const id = `${this.id}-${timestamp}-${Math.random().toString(36).substring(2, 9)}`;
const register = new LWWRegister<Stroke | null>(this.id, [this.id, timestamp, stroke]);
this.#data.set(id, register);
return id;
}

// 선 삭제
deleteStroke(id: string): boolean {
const register = this.#data.get(id);
if (register) {
register.set(null);
return true;
}
return false;
}

// 원격 상태 병합
merge(remoteState: MapState): string[] {
const updatedKeys: string[] = [];

for (const [key, remoteRegisterState] of Object.entries(remoteState)) {
const localRegister = this.#data.get(key);

if (localRegister) {
// 기존 레지스터가 있으면 병합
if (localRegister.merge(remoteRegisterState)) {
updatedKeys.push(key);
}
} else {
// 새로운 레지스터면 추가
this.#data.set(key, new LWWRegister(this.id, remoteRegisterState));
updatedKeys.push(key);
}
}

return updatedKeys;
}

// 단일 레지스터 업데이트
mergeRegister(key: string, remoteRegisterState: RegisterState<Stroke | null>): boolean {
const localRegister = this.#data.get(key);

if (localRegister) {
// 기존 stroke에 대한 원격 업데이트 병합
return localRegister.merge(remoteRegisterState);
} else {
// 새로운 stroke 추가
this.#data.set(key, new LWWRegister(this.id, remoteRegisterState));
return true;
}
}
}
35 changes: 35 additions & 0 deletions client/src/core/crdt/LWWRegister.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { RegisterState } from '../crdt.types';

export class LWWRegister<T> {
readonly id: string;
#state: RegisterState<T>; // [peerId, timestamp, value]

constructor(id: string, initialState: RegisterState<T>) {
this.id = id;
this.#state = initialState;
}

get value(): T {
return this.#state[2];
}

get state(): RegisterState<T> {
return this.#state;
}

set(value: T): void {
this.#state = [this.id, Date.now(), value];
}

// 원격 상태와 병합 (더 새로운 타임스탬프 또는 더 큰 피어 ID 우선)
merge(remoteState: RegisterState<T>): boolean {
const [remotePeer, remoteTimestamp] = remoteState;
const [localPeer, localTimestamp] = this.#state;

if (remoteTimestamp > localTimestamp || (remoteTimestamp === localTimestamp && remotePeer > localPeer)) {
this.#state = remoteState;
return true;
}
return false;
}
}

0 comments on commit 3f5a958

Please sign in to comment.