Skip to content

Commit

Permalink
Merge pull request #163 from boostcampwm-2024/Feature/#156_server에_Pa…
Browse files Browse the repository at this point in the history
…ge별_EditorCRDT_적용

Feature/#156 server에 Page별 editor crdt 적용
  • Loading branch information
github-actions[bot] authored Nov 21, 2024
2 parents 0dc0239 + 5024188 commit 95b0f0d
Show file tree
Hide file tree
Showing 28 changed files with 1,485 additions and 339 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@
.DS_Store
.env

tsconfig.tsbuildinfo

# Jest globalConfig file
../globalConfig.json
121 changes: 96 additions & 25 deletions @noctaCrdt/Crdt.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,46 @@
import { LinkedList } from "./LinkedList";
import { LinkedList, BlockLinkedList, TextLinkedList } from "./LinkedList";
import { CharId, BlockId, NodeId } from "./NodeId";
import { Node, Char, Block } from "./Node";
import {
RemoteBlockDeleteOperation,
RemoteCharDeleteOperation,
RemoteBlockInsertOperation,
RemoteCharInsertOperation,
SerializedProps,
CRDTSerializedProps,
RemoteReorderOperation,
RemoteBlockUpdateOperation,
} from "./Interfaces";

export class CRDT<T extends Node<NodeId>> {
clock: number;
client: number;
LinkedList: LinkedList<T>;

constructor(client: number) {
constructor(client: number, LinkedListClass: new () => LinkedList<T>) {
this.clock = 0;
this.client = client;
this.LinkedList = new LinkedList<T>();
this.LinkedList = new LinkedListClass();
}

localInsert(index: number, value: string, blockId?: BlockId) {}
localInsert(index: number, value: string, blockId?: BlockId, pageId?: string): any {
// 기본 CRDT에서는 구현하지 않고, 하위 클래스에서 구현
throw new Error("Method not implemented.");
}

localDelete(index: number, blockId?: BlockId) {}
localDelete(index: number, blockId?: BlockId, pageId?: string): any {
// 기본 CRDT에서는 구현하지 않고, 하위 클래스에서 구현
throw new Error("Method not implemented.");
}

remoteInsert(operation: RemoteBlockInsertOperation | RemoteCharInsertOperation) {}
remoteInsert(operation: any): void {
// 기본 CRDT에서는 구현하지 않고, 하위 클래스에서 구현
throw new Error("Method not implemented.");
}

remoteDelete(operation: RemoteBlockDeleteOperation | RemoteCharDeleteOperation) {}
remoteDelete(operation: any): void {
// 기본 CRDT에서는 구현하지 않고, 하위 클래스에서 구현
throw new Error("Method not implemented.");
}

read(): string {
return this.LinkedList.stringify();
Expand All @@ -37,23 +50,27 @@ export class CRDT<T extends Node<NodeId>> {
return this.LinkedList.spread();
}

serialize(): SerializedProps<T> {
serialize(): CRDTSerializedProps<T> {
return {
clock: this.clock,
client: this.client,
LinkedList: {
head: this.LinkedList.head,
nodeMap: this.LinkedList.nodeMap || {},
},
LinkedList: this.LinkedList.serialize(),
};
}

deserialize(data: any): void {
this.clock = data.clock;
this.client = data.client;
this.LinkedList.deserialize(data.LinkedList);
}
}

// EditorCRDT 클래스: 블록을 관리
export class EditorCRDT extends CRDT<Block> {
currentBlock: Block | null;

constructor(client: number) {
super(client);
super(client, BlockLinkedList);
this.currentBlock = null;
}

Expand All @@ -64,7 +81,7 @@ export class EditorCRDT extends CRDT<Block> {
return { node: remoteInsertion.node } as RemoteBlockInsertOperation;
}

localDelete(index: number): RemoteBlockDeleteOperation {
localDelete(index: number, blockId: undefined, pageId: string): RemoteBlockDeleteOperation {
if (index < 0 || index >= this.LinkedList.spread().length) {
throw new Error(`Invalid index: ${index}`);
}
Expand All @@ -76,7 +93,8 @@ export class EditorCRDT extends CRDT<Block> {

const operation: RemoteBlockDeleteOperation = {
targetId: nodeToDelete.id,
clock: this.clock + 1,
clock: this.clock,
pageId,
};

this.LinkedList.deleteNode(nodeToDelete.id);
Expand All @@ -85,9 +103,15 @@ export class EditorCRDT extends CRDT<Block> {
return operation;
}

remoteUpdate(block: Block) {
this.LinkedList.nodeMap[JSON.stringify(block.id)] = block;
return { remoteUpdateOperation: block };
remoteUpdate(block: Block, pageId: string): RemoteBlockUpdateOperation {
const updatedBlock = this.LinkedList.nodeMap[JSON.stringify(block.id)];
updatedBlock.animation = block.animation;
updatedBlock.icon = block.icon;
updatedBlock.indent = block.indent;
updatedBlock.style = block.style;
updatedBlock.type = block.type;
// this.LinkedList.nodeMap[JSON.stringify(block.id)] = block;
return { node: updatedBlock, pageId };
}

remoteInsert(operation: RemoteBlockInsertOperation): void {
Expand All @@ -99,19 +123,26 @@ export class EditorCRDT extends CRDT<Block> {

this.LinkedList.insertById(newNode);

this.clock = Math.max(this.clock, operation.node.id.clock) + 1;
/*
if (this.clock <= newNode.id.clock) {
this.clock = newNode.id.clock + 1;
}
*/
}

remoteDelete(operation: RemoteBlockDeleteOperation): void {
const { targetId, clock } = operation;
if (targetId) {
this.LinkedList.deleteNode(targetId);
const targetNodeId = new BlockId(operation.targetId.clock, operation.targetId.client);
this.LinkedList.deleteNode(targetNodeId);
}
this.clock = Math.max(this.clock, clock) + 1;
/*
if (this.clock <= clock) {
this.clock = clock + 1;
}
*/
}

localReorder(params: {
Expand All @@ -121,7 +152,7 @@ export class EditorCRDT extends CRDT<Block> {
}): RemoteReorderOperation {
const operation: RemoteReorderOperation = {
...params,
clock: this.clock + 1,
clock: this.clock,
client: this.client,
};

Expand All @@ -144,29 +175,48 @@ export class EditorCRDT extends CRDT<Block> {
this.clock = clock + 1;
}
}

serialize(): CRDTSerializedProps<Block> {
return {
...super.serialize(),
currentBlock: this.currentBlock ? this.currentBlock.serialize() : null,
};
}

deserialize(data: any): void {
super.deserialize(data);
this.currentBlock = data.currentBlock ? Block.deserialize(data.currentBlock) : null;
}
}

// BlockCRDT 클래스: 문자(Char)를 관리
export class BlockCRDT extends CRDT<Char> {
currentCaret: number;

constructor(client: number) {
super(client);
super(client, TextLinkedList);
this.currentCaret = 0;
}

localInsert(index: number, value: string, blockId: BlockId): RemoteCharInsertOperation {
localInsert(
index: number,
value: string,
blockId: BlockId,
pageId: string,
): RemoteCharInsertOperation {
const id = new CharId(this.clock + 1, this.client);
const { node } = this.LinkedList.insertAtIndex(index, value, id);
this.clock += 1;
const operation: RemoteCharInsertOperation = {
node,
blockId,
pageId,
};

return operation;
}

localDelete(index: number, blockId: BlockId): RemoteCharDeleteOperation {
localDelete(index: number, blockId: BlockId, pageId: string): RemoteCharDeleteOperation {
if (index < 0 || index >= this.LinkedList.spread().length) {
throw new Error(`Invalid index: ${index}`);
}
Expand All @@ -178,8 +228,9 @@ export class BlockCRDT extends CRDT<Char> {

const operation: RemoteCharDeleteOperation = {
targetId: nodeToDelete.id,
clock: this.clock + 1,
clock: this.clock,
blockId,
pageId,
};

this.LinkedList.deleteNode(nodeToDelete.id);
Expand Down Expand Up @@ -212,4 +263,24 @@ export class BlockCRDT extends CRDT<Char> {
this.clock = clock + 1;
}
}

serialize(): CRDTSerializedProps<Char> {
return {
...super.serialize(),
currentCaret: this.currentCaret,
};
}

static deserialize(data: any): BlockCRDT {
const crdt = new BlockCRDT(data.client);
crdt.clock = data.clock;
crdt.LinkedList.deserialize(data.LinkedList);
crdt.currentCaret = data.currentCaret;
return crdt;
}

deserialize(data: any): void {
super.deserialize(data);
this.currentCaret = data.currentCaret;
}
}
41 changes: 39 additions & 2 deletions @noctaCrdt/Interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { NodeId, BlockId, CharId } from "./NodeId";
import { Block, Char } from "./Node";
import { Page } from "./Page";
import { EditorCRDT } from "./Crdt";

export type ElementType = "p" | "h1" | "h2" | "h3" | "ul" | "ol" | "li" | "checkbox" | "blockquote";

Expand All @@ -12,42 +14,72 @@ export interface DeleteOperation {
clock: number;
}

export interface RemotePageCreateOperation {
clientId: number;
workspaceId: string;
page?: Page;
}

export interface RemoteBlockUpdateOperation {
node: Block;
pageId: string;
}

export interface RemoteBlockInsertOperation {
node: Block;
pageId: string;
}

export interface RemoteCharInsertOperation {
node: Char;
blockId: BlockId;
pageId: string;
}

export interface RemoteBlockDeleteOperation {
targetId: BlockId;
clock: number;
pageId: string;
}

export interface RemoteCharDeleteOperation {
targetId: CharId;
clock: number;
blockId?: BlockId;
pageId: string;
}

export interface CursorPosition {
clientId: number;
position: number;
}

export interface SerializedProps<T> {
// CRDT 직렬화라서 이름바꿔야함.
export interface CRDTSerializedProps<T> {
clock: number;
client: number;
LinkedList: {
head: NodeId | null;
nodeMap: { [key: string]: T };
};
currentBlock?: Block | null;
currentCaret?: number | null;
}

export interface serializedEditorDataProps {
clock: number;
client: number;
LinkedList: {
head: NodeId | null;
nodeMap: { [key: string]: Block };
};
currentBlock: Block | null;
}

export interface serializedPageProps {
id: string;
title: string;
icon: string;
crdt: EditorCRDT;
}

export interface ReorderNodesProps {
Expand All @@ -56,6 +88,11 @@ export interface ReorderNodesProps {
afterId: BlockId | null;
}

export interface WorkSpaceSerializedProps {
id: string;
pageList: Page[];
authUser: Map<string, string>;
}
export interface RemoteReorderOperation {
targetId: BlockId;
beforeId: BlockId | null;
Expand Down
Loading

0 comments on commit 95b0f0d

Please sign in to comment.