Skip to content

Commit

Permalink
refactor: CRDT 라이브러리 개선 - 1
Browse files Browse the repository at this point in the history
- block, char 별로 interface 분리
- localInsert에서 block, char 별로 다르게 return 하게 처리

#109
  • Loading branch information
hyonun321 committed Nov 18, 2024
1 parent 51ee8cc commit 51c81ba
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 68 deletions.
39 changes: 24 additions & 15 deletions @noctaCrdt/Crdt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { LinkedList } from "./LinkedList";
import { CharId, BlockId, NodeId } from "./NodeId";
import { Node, Char, Block } from "./Node";
import {
RemoteDeleteOperation,
RemoteInsertOperation,
RemoteBlockDeleteOperation,
RemoteCharDeleteOperation,
RemoteBlockInsertOperation,
RemoteCharInsertOperation,
SerializedProps,
RemoteReorderOperation,
} from "./Interfaces";
Expand All @@ -19,18 +21,24 @@ export class CRDT<T extends Node<NodeId>> {
this.LinkedList = new LinkedList<T>();
}

localInsert(index: number, value: string): RemoteInsertOperation {
const id =
this instanceof BlockCRDT
? new CharId(this.clock + 1, this.client)
: new BlockId(this.clock + 1, this.client);

const remoteInsertion = this.LinkedList.insertAtIndex(index, value, id);
this.clock += 1;
return { node: remoteInsertion.node };
localInsert(
index: number,
value: string,
): RemoteBlockInsertOperation | RemoteCharInsertOperation {
if (this instanceof BlockCRDT) {
const id = new CharId(this.clock + 1, this.client);
const remoteInsertion = this.LinkedList.insertAtIndex(index, value, id);
this.clock += 1;
return { node: remoteInsertion.node, blockId: BlockId } as RemoteBlockInsertOperation;
} else {
const id = new BlockId(this.clock + 1, this.client);
const remoteInsertion = this.LinkedList.insertAtIndex(index, value, id);
this.clock += 1;
return { node: remoteInsertion.node } as RemoteCharInsertOperation;
}
}

localDelete(index: number): RemoteDeleteOperation {
localDelete(index: number): RemoteBlockDeleteOperation | RemoteCharDeleteOperation {
if (index < 0 || index >= this.LinkedList.spread().length) {
throw new Error(`Invalid index: ${index}`);
}
Expand All @@ -40,7 +48,7 @@ export class CRDT<T extends Node<NodeId>> {
throw new Error(`Node not found at index: ${index}`);
}

const operation: RemoteDeleteOperation = {
const operation: RemoteBlockDeleteOperation | RemoteCharDeleteOperation = {
targetId: nodeToDelete.id,
clock: this.clock + 1,
};
Expand All @@ -51,11 +59,12 @@ export class CRDT<T extends Node<NodeId>> {
return operation;
}

remoteInsert(operation: RemoteInsertOperation): void {
remoteInsert(operation: RemoteBlockInsertOperation | RemoteCharInsertOperation): void {
const NodeIdClass = this instanceof BlockCRDT ? CharId : BlockId;
const NodeClass = this instanceof BlockCRDT ? Char : Block;

const newNodeId = new NodeIdClass(operation.node.id.clock, operation.node.id.client);

const newNode = new NodeClass(operation.node.value, newNodeId) as T;
newNode.next = operation.node.next;
newNode.prev = operation.node.prev;
Expand All @@ -67,7 +76,7 @@ export class CRDT<T extends Node<NodeId>> {
}
}

remoteDelete(operation: RemoteDeleteOperation): void {
remoteDelete(operation: RemoteBlockDeleteOperation | RemoteCharDeleteOperation): void {
const { targetId, clock } = operation;
if (targetId) {
this.LinkedList.deleteNode(targetId);
Expand Down
16 changes: 13 additions & 3 deletions @noctaCrdt/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,21 @@ export interface DeleteOperation {
clock: number;
}

export interface RemoteInsertOperation {
node: Block | Char;
export interface RemoteBlockInsertOperation {
node: Block;
}

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

export interface RemoteBlockDeleteOperation {
targetId: NodeId;
clock: number;
}

export interface RemoteDeleteOperation {
export interface RemoteCharDeleteOperation {
targetId: NodeId;
clock: number;
}
Expand Down
48 changes: 33 additions & 15 deletions client/src/apis/useSocket.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {
RemoteInsertOperation,
RemoteDeleteOperation,
RemoteBlockInsertOperation,
RemoteBlockDeleteOperation,
RemoteCharInsertOperation,
RemoteCharDeleteOperation,
CursorPosition,
SerializedProps,
} from "@noctaCrdt/Interfaces";
Expand All @@ -9,16 +11,18 @@ import { useEffect, useRef } from "react";
import { io, Socket } from "socket.io-client";
// 구독 핸들러들의 타입 정의
interface RemoteOperationHandlers {
onRemoteInsert: (operation: RemoteInsertOperation) => void;
onRemoteDelete: (operation: RemoteDeleteOperation) => void;
onRemoteBlockInsert: (operation: RemoteBlockInsertOperation) => void;
onRemoteBlockDelete: (operation: RemoteBlockDeleteOperation) => void;
onRemoteCharInsert: (operation: RemoteCharInsertOperation) => void;
onRemoteCharDelete: (operation: RemoteCharDeleteOperation) => void;
onRemoteCursor: (position: CursorPosition) => void;
}

// 훅의 반환 타입을 명시적으로 정의
interface UseSocketReturn {
socket: Socket | null;
sendInsertOperation: (operation: RemoteInsertOperation) => void;
sendDeleteOperation: (operation: RemoteDeleteOperation) => void;
sendInsertOperation: (operation: RemoteBlockInsertOperation | RemoteCharInsertOperation) => void;
sendDeleteOperation: (operation: RemoteBlockDeleteOperation | RemoteCharDeleteOperation) => void;
sendCursorPosition: (position: CursorPosition) => void;
subscribeToRemoteOperations: (handlers: RemoteOperationHandlers) => (() => void) | undefined;
}
Expand Down Expand Up @@ -67,12 +71,20 @@ export const useSocket = (): UseSocketReturn => {
};
}, []);

const sendInsertOperation = (operation: RemoteInsertOperation) => {
socketRef.current?.emit("insert", operation);
const sendInsertOperation = (
operation: RemoteBlockInsertOperation | RemoteCharInsertOperation,
) => {
if (operation.node instanceof Block) {
socketRef.current?.emit("insert/block", operation);
} else {
socketRef.current?.emit("insert/char", operation);
}
console.log(operation);
};

const sendDeleteOperation = (operation: RemoteDeleteOperation) => {
const sendDeleteOperation = (
operation: RemoteBlockDeleteOperation | RemoteCharDeleteOperation,
) => {
socketRef.current?.emit("delete", operation);
};

Expand All @@ -81,19 +93,25 @@ export const useSocket = (): UseSocketReturn => {
};

const subscribeToRemoteOperations = ({
onRemoteInsert,
onRemoteDelete,
onRemoteBlockInsert,
onRemoteBlockDelete,
onRemoteCharInsert,
onRemoteCharDelete,
onRemoteCursor,
}: RemoteOperationHandlers) => {
if (!socketRef.current) return;

socketRef.current.on("insert", onRemoteInsert);
socketRef.current.on("delete", onRemoteDelete);
socketRef.current.on("insert/block", onRemoteBlockInsert);
socketRef.current.on("delete/block", onRemoteBlockDelete);
socketRef.current.on("insert/char", onRemoteCharInsert);
socketRef.current.on("delete/char", onRemoteCharDelete);
socketRef.current.on("cursor", onRemoteCursor);

return () => {
socketRef.current?.off("insert", onRemoteInsert);
socketRef.current?.off("delete", onRemoteDelete);
socketRef.current?.off("insert/block", onRemoteBlockInsert);
socketRef.current?.off("delete/block", onRemoteBlockDelete);
socketRef.current?.off("insert/char", onRemoteCharInsert);
socketRef.current?.off("delete/char", onRemoteCharDelete);
socketRef.current?.off("cursor", onRemoteCursor);
};
};
Expand Down
103 changes: 84 additions & 19 deletions client/src/features/editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { EditorCRDT } from "@noctaCrdt/Crdt";
import { BlockLinkedList } from "@noctaCrdt/LinkedList";
import { Block as CRDTBlock } from "@noctaCrdt/Node";
import { BlockId } from "@noctaCrdt/NodeId";
import { RemoteCharInsertOperation } from "node_modules/@noctaCrdt/Interfaces.ts";
import { useRef, useState, useCallback, useEffect } from "react";
import { useSocket } from "@src/apis/useSocket.ts";
import { editorContainer, editorTitleContainer, editorTitle } from "./Editor.style";
Expand All @@ -23,7 +24,7 @@ export interface EditorStateProps {

export const Editor = ({ onTitleChange }: EditorProps) => {
const editorCRDT = useRef<EditorCRDT>(new EditorCRDT(0));
const { sendInsertOperation, sendDeleteOperation } = useSocket();
const { sendInsertOperation, sendDeleteOperation, subscribeToRemoteOperations } = useSocket();
const [editorState, setEditorState] = useState<EditorStateProps>({
clock: editorCRDT.current.clock,
linkedList: editorCRDT.current.LinkedList,
Expand Down Expand Up @@ -71,41 +72,37 @@ export const Editor = ({ onTitleChange }: EditorProps) => {
};

const handleBlockInput = useCallback(
(e: React.FormEvent<HTMLDivElement>, blockId: BlockId) => {
const block = editorState.linkedList.getNode(blockId);
(e: React.FormEvent<HTMLDivElement>, block: CRDTBlock) => {
if (!block) return;
let testNode;

let operationNode;
const element = e.currentTarget;
const newContent = element.textContent || "";
const currentContent = block.crdt.read();
const selection = window.getSelection();
const caretPosition = selection?.focusOffset || 0;

if (newContent.length > currentContent.length) {
// 문자가 추가된 경우
if (caretPosition === 0) {
const [addedChar] = newContent;
testNode = block.crdt.localInsert(0, addedChar);
operationNode = block.crdt.localInsert(0, addedChar);
block.crdt.currentCaret = 1;
} else if (caretPosition > currentContent.length) {
const addedChar = newContent[newContent.length - 1];
testNode = block.crdt.localInsert(currentContent.length, addedChar);
operationNode = block.crdt.localInsert(currentContent.length, addedChar);
block.crdt.currentCaret = caretPosition;
} else {
const addedChar = newContent[caretPosition - 1];
testNode = block.crdt.localInsert(caretPosition - 1, addedChar);
operationNode = block.crdt.localInsert(caretPosition - 1, addedChar);
block.crdt.currentCaret = caretPosition;
}
sendInsertOperation(testNode);
console.log("여기여", operationNode);
sendInsertOperation(operationNode);
} else if (newContent.length < currentContent.length) {
if (caretPosition === 0) {
testNode = block.crdt.localDelete(0);
block.crdt.currentCaret = 0;
} else {
testNode = block.crdt.localDelete(caretPosition);
block.crdt.currentCaret = caretPosition;
}
console.log(testNode);
sendDeleteOperation(testNode);
// 문자가 삭제된 경우
operationNode = block.crdt.localDelete(caretPosition);
block.crdt.currentCaret = caretPosition;
sendDeleteOperation(operationNode);
}

setEditorState((prev) => ({
Expand All @@ -114,7 +111,7 @@ export const Editor = ({ onTitleChange }: EditorProps) => {
currentBlock: prev.currentBlock,
}));
},
[editorState.linkedList],
[editorState.linkedList, sendInsertOperation, sendDeleteOperation],
);

useEffect(() => {
Expand All @@ -129,6 +126,74 @@ export const Editor = ({ onTitleChange }: EditorProps) => {
});
}, []);

const subscriptionRef = useRef(false);

useEffect(() => {
if (subscriptionRef.current) return;
subscriptionRef.current = true;

const unsubscribe = subscribeToRemoteOperations({
onRemoteBlockInsert: (operation) => {
console.log(operation, "block : 입력 확인합니다이");
if (!editorCRDT.current) return;
editorCRDT.current.remoteInsert(operation);
setEditorState((prev) => ({
clock: editorCRDT.current.clock,
linkedList: editorCRDT.current.LinkedList,
currentBlock: prev.currentBlock,
}));
},

onRemoteBlockDelete: (operation) => {
console.log(operation, "block : 삭제 확인합니다이");
if (!editorCRDT.current) return;
editorCRDT.current.remoteDelete(operation);
setEditorState((prev) => ({
clock: editorCRDT.current.clock,
linkedList: editorCRDT.current.LinkedList,
currentBlock: prev.currentBlock,
}));
},

onRemoteCharInsert: (operation) => {
// 변경되는건 char
console.log(operation, "char : 입력 확인합니다이");
if (!editorCRDT.current) return;
const insertOperation: RemoteCharInsertOperation = {
node: operation.node,
blockId: operation.blockId,
};
// 여기 ㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇ

editorCRDT.current.remoteInsert(insertOperation);
setEditorState((prev) => ({
clock: editorCRDT.current.clock,
linkedList: editorCRDT.current.LinkedList,
currentBlock: prev.currentBlock,
}));
},

onRemoteCharDelete: (operation) => {
console.log(operation, "char : 삭제 확인합니다이");
if (!editorCRDT.current) return;
editorCRDT.current.remoteDelete(operation);
setEditorState((prev) => ({
clock: editorCRDT.current.clock,
linkedList: editorCRDT.current.LinkedList,
currentBlock: prev.currentBlock,
}));
},
onRemoteCursor: (position) => {
console.log(position);
},
});

return () => {
subscriptionRef.current = false;
unsubscribe?.();
};
}, []);

console.log("block list", editorState.linkedList.spread());

return (
Expand Down
Loading

0 comments on commit 51c81ba

Please sign in to comment.