-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Implement CRDT register and map with associated types (#64)
* feat: Implement CRDT register and map with associated types * chore: Reorganize file path from crdt/core to core/crdt
- Loading branch information
Showing
3 changed files
with
150 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |