-
Notifications
You must be signed in to change notification settings - Fork 1
Refactor/#047 nocta라이브러리 리팩토링 #48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
hyonun321
wants to merge
9
commits into
refactor/noctaDoc
Choose a base branch
from
refactor/#047_Nocta라이브러리_리팩토링
base: refactor/noctaDoc
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
The head ref may contain hidden characters: "refactor/#047_Nocta\uB77C\uC774\uBE0C\uB7EC\uB9AC_\uB9AC\uD329\uD1A0\uB9C1"
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
2cc1c2f
build: nocta-doc 라이브러리 추가 및 client tsconfig 수정
hyonun321 649e5e2
chore: 테스트 컴포넌트 생성하여 main에 연동
hyonun321 69ca5b8
feat: Nocta라이브러리 생성
hyonun321 7c377de
feat: CRDT 클래스 선언
hyonun321 5907781
feat: type 관리용 model 파일 추가
hyonun321 63110cc
feat: operation 연산 인터페이스 정의
hyonun321 df8f2b6
feat: 추후 block,char의 상태를 저장하는 파일 추가
hyonun321 f7ca775
feat: 추후 decode과 encode 을 위해 생성
hyonun321 3d21f40
feat: util관련 파일 생성
hyonun321 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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,52 @@ | ||
import { Nocta } from "@noctaDoc"; | ||
import { useRef, useEffect } from "react"; | ||
|
||
const socket = { | ||
on: () => {}, | ||
emit: () => {}, | ||
}; | ||
|
||
const client = Nocta.createClient({ socket, clientId: "client-123" }); | ||
|
||
export const TestEditor = () => { | ||
const editorRef = useRef<HTMLDivElement>(null); | ||
const prevText = useRef(""); | ||
useEffect(() => { | ||
// 처음 렌더링 시 block-1 생성 | ||
client.insertBlock(null, "block-1", "paragraph"); | ||
}, []); | ||
|
||
const handleInput = () => { | ||
const blockId = "block-1"; | ||
const newText = editorRef.current?.innerText || ""; | ||
const oldText = prevText.current; | ||
|
||
if (newText.length > oldText.length) { | ||
// 입력 발생 | ||
const addedChar = newText.slice(oldText.length); // 단일 문자만 가정 | ||
client.insertChar(blockId, oldText.length, addedChar); | ||
} else if (newText.length < oldText.length) { | ||
// 삭제 발생 | ||
client.deleteChar(blockId, oldText.length - 1); | ||
} | ||
console.log(client.getText("block-1")); | ||
prevText.current = newText; | ||
}; | ||
|
||
return ( | ||
<div> | ||
<h2>TestEditor</h2> | ||
<div | ||
ref={editorRef} | ||
contentEditable | ||
onInput={handleInput} | ||
style={{ | ||
border: "1px solid black", | ||
padding: "8px", | ||
minHeight: "100px", | ||
fontSize: "16px", | ||
}} | ||
/> | ||
</div> | ||
); | ||
}; |
This file contains hidden or 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 |
---|---|---|
@@ -1,21 +1,31 @@ | ||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; | ||
// import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; | ||
// import { StrictMode } from "react"; | ||
// import { createRoot } from "react-dom/client"; | ||
// import "./index.css"; | ||
// import App from "./App.tsx"; | ||
|
||
// const queryClient = new QueryClient({ | ||
// defaultOptions: { | ||
// queries: { | ||
// refetchOnWindowFocus: false, | ||
// }, | ||
// }, | ||
// }); | ||
|
||
// createRoot(document.getElementById("root")!).render( | ||
// <StrictMode> | ||
// <QueryClientProvider client={queryClient}> | ||
// <App /> | ||
// </QueryClientProvider> | ||
// </StrictMode>, | ||
// ); | ||
|
||
import { StrictMode } from "react"; | ||
import { createRoot } from "react-dom/client"; | ||
import "./index.css"; | ||
import App from "./App.tsx"; | ||
|
||
const queryClient = new QueryClient({ | ||
defaultOptions: { | ||
queries: { | ||
refetchOnWindowFocus: false, | ||
}, | ||
}, | ||
}); | ||
import { TestEditor } from "./TestEditor"; | ||
|
||
createRoot(document.getElementById("root")!).render( | ||
<StrictMode> | ||
<QueryClientProvider client={queryClient}> | ||
<App /> | ||
</QueryClientProvider> | ||
<TestEditor /> | ||
</StrictMode>, | ||
); |
This file contains hidden or 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
This file contains hidden or 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,52 @@ | ||
import { NoctaDoc } from "./NoctaDoc"; | ||
import { ClientNoctaRealm, ServerNoctaRealm } from "./NoctaRealm"; | ||
|
||
export class Nocta { | ||
private doc: NoctaDoc; | ||
private realm: ClientNoctaRealm | ServerNoctaRealm; | ||
|
||
private constructor(doc: NoctaDoc, realm: ClientNoctaRealm | ServerNoctaRealm) { | ||
this.doc = doc; | ||
this.realm = realm; | ||
} | ||
|
||
static createClient({ socket, clientId }): Nocta { | ||
const doc = new NoctaDoc(clientId); | ||
const realm = new ClientNoctaRealm(socket, doc); | ||
return new Nocta(doc, realm); | ||
} | ||
|
||
static createServer({ socket, clientId }): Nocta { | ||
const doc = new NoctaDoc(clientId); | ||
const realm = new ServerNoctaRealm(socket, doc); | ||
return new Nocta(doc, realm); | ||
} | ||
|
||
setCaret(blockId: string, charId: string) { | ||
this.realm.setCaret(blockId, charId); | ||
} | ||
|
||
insertChar(blockId: string, index: number, value: string) { | ||
this.doc.insertChar(blockId, index, value); | ||
} | ||
|
||
deleteChar(blockId: string, index: number) { | ||
this.doc.deleteChar(blockId, index); | ||
} | ||
|
||
applyUpdate(update: Uint8Array) { | ||
this.doc.applyUpdate(update); | ||
} | ||
|
||
encodeUpdate(): Uint8Array { | ||
return this.doc.encodeUpdate(); | ||
} | ||
|
||
getText(blockId: string): string { | ||
return this.doc.getText(blockId); | ||
} | ||
|
||
insertBlock(prevId: string | null, blockId: string, type: string) { | ||
this.doc.insertBlock(prevId, blockId, type); | ||
} | ||
} |
This file contains hidden or 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,59 @@ | ||
import { BlockCRDT } from "../crdt/BlockCRDT"; | ||
import { IdGenerator, NodeId } from "../crdt/Id"; | ||
import type { Operation } from "../operations"; | ||
|
||
export class NoctaDoc { | ||
public blockCRDT: BlockCRDT; | ||
public clientId: string; | ||
private idGenerator: IdGenerator; | ||
|
||
constructor(clientId: string) { | ||
this.clientId = clientId; | ||
this.idGenerator = new IdGenerator(clientId); | ||
this.blockCRDT = new BlockCRDT(clientId); | ||
} | ||
|
||
insertChar(blockId: string, index: number, value: string) { | ||
const nextId: NodeId = this.idGenerator.next(); | ||
const id: string = this.idGenerator.toString(nextId); | ||
const block = this.blockCRDT.getBlock(blockId); | ||
block.charCRDT.insert(index, id, value); | ||
} | ||
|
||
deleteChar(blockId: string, index: number) { | ||
const block = this.blockCRDT.getBlock(blockId); | ||
block.charCRDT.delete(index); | ||
} | ||
|
||
insertBlock(prevId: string | null, blockId: string, type: string) { | ||
this.blockCRDT.insertBlockAfter(prevId, blockId, type); | ||
} | ||
|
||
applyUpdate(update: Uint8Array) { | ||
const json = new TextDecoder().decode(update); | ||
const operations: Operation[] = JSON.parse(json); | ||
this.blockCRDT.applyOperations(operations); | ||
} | ||
|
||
// TODO: Define a proper encoding strategy for updates (e.g., CBOR, JSON, VarInt) | ||
// and decide on the structure of operation messages (insert, delete, etc.) | ||
encodeUpdate(): Uint8Array { | ||
const operations: Operation[] = this.blockCRDT.collectOperations(); // 가정 | ||
const json = JSON.stringify(operations); | ||
return new TextEncoder().encode(json); | ||
} | ||
|
||
private mergeUpdates(updates: Uint8Array[]): Uint8Array { | ||
return updates.reduce((acc, cur) => { | ||
const merged = new Uint8Array(acc.length + cur.length); | ||
merged.set(acc); | ||
merged.set(cur, acc.length); | ||
return merged; | ||
}); | ||
} | ||
|
||
getText(blockId: string): string { | ||
const block = this.blockCRDT.getBlock(blockId); | ||
return block.charCRDT.getText(); | ||
} | ||
} |
This file contains hidden or 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,45 @@ | ||
import type { Socket as ServerSocket } from "socket.io"; | ||
import type { Socket as ClientSocket } from "socket.io-client"; | ||
import type { NoctaDoc } from "./NoctaDoc"; | ||
import { Operation } from "../operations"; | ||
|
||
export class ClientNoctaRealm { | ||
constructor( | ||
private socket: ClientSocket, | ||
private doc: NoctaDoc, | ||
) { | ||
this.socket.on("operation", (ops: Operation[]) => { | ||
this.doc.blockCRDT.applyOperations(ops); | ||
}); | ||
} | ||
|
||
sendOperations(ops: Operation[]) { | ||
this.socket.emit("operation", ops); | ||
} | ||
|
||
setCaret(blockId: string, charId: string) { | ||
// TODO: 브로드캐스트 방식 변경 가능 | ||
this.socket.emit("setCaret", { blockId, charId }); | ||
} | ||
} | ||
// TODO: io 매개변수 : 특정 room번호에 송신 | ||
export class ServerNoctaRealm { | ||
constructor( | ||
private socket: ServerSocket, | ||
minjungw00 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
private doc: NoctaDoc, | ||
) { | ||
this.socket.on("operation", (ops: Operation[]) => { | ||
this.doc.blockCRDT.applyOperations(ops); | ||
this.socket.broadcast.emit("operation", ops); | ||
}); | ||
this.socket.on("requestInit", () => { | ||
const ops = this.doc.blockCRDT.collectOperations(); | ||
this.socket.emit("initDoc", ops); | ||
}); | ||
} | ||
|
||
setCaret(blockId: string, charId: string) { | ||
// TODO: room 기반으로 전파하려면 socket.to(room).emit 등 사용 | ||
this.socket.broadcast.emit("setCaret", { blockId, charId }); | ||
} | ||
} |
This file contains hidden or 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,114 @@ | ||
import { Operation, InsertOperation } from "../operations"; | ||
import { CharCRDT } from "./CharCRDT"; | ||
import { BlockNode } from "../model/Block"; | ||
|
||
export class BlockCRDT { | ||
private blocks: Map<string, BlockNode> = new Map(); | ||
public clientId: string; | ||
private headId: string | null = null; | ||
private tailId: string | null = null; | ||
|
||
constructor(clientId: string) { | ||
this.clientId = clientId; | ||
} | ||
|
||
insertBlockAfter(prevId: string | null, blockId: string, type: string): void { | ||
const newBlock: BlockNode = { | ||
id: blockId, | ||
type, | ||
prevId, | ||
nextId: null, | ||
charCRDT: new CharCRDT(this.clientId), | ||
}; | ||
|
||
if (prevId === null) { | ||
// Insert at head | ||
const oldHead = this.headId ? this.blocks.get(this.headId) : null; | ||
newBlock.nextId = this.headId; | ||
if (oldHead) oldHead.prevId = blockId; | ||
this.headId = blockId; | ||
if (!this.tailId) this.tailId = blockId; | ||
} else { | ||
const prevBlock = this.blocks.get(prevId); | ||
if (!prevBlock) throw new Error(`Block ${prevId} not found`); | ||
const { nextId } = prevBlock; | ||
newBlock.nextId = nextId; | ||
prevBlock.nextId = blockId; | ||
|
||
if (nextId) { | ||
const nextBlock = this.blocks.get(nextId); | ||
if (nextBlock) nextBlock.prevId = blockId; | ||
} else { | ||
this.tailId = blockId; | ||
} | ||
} | ||
|
||
this.blocks.set(blockId, newBlock); | ||
} | ||
|
||
deleteBlock(blockId: string): void { | ||
const block = this.blocks.get(blockId); | ||
if (!block) return; | ||
|
||
const { prevId, nextId } = block; | ||
|
||
if (prevId) { | ||
const prev = this.blocks.get(prevId); | ||
if (prev) prev.nextId = nextId; | ||
} else { | ||
this.headId = nextId; | ||
} | ||
|
||
if (nextId) { | ||
const next = this.blocks.get(nextId); | ||
if (next) next.prevId = prevId; | ||
} else { | ||
this.tailId = prevId; | ||
} | ||
|
||
this.blocks.delete(blockId); | ||
} | ||
|
||
getBlocks(): BlockNode[] { | ||
const result: BlockNode[] = []; | ||
let currentId = this.headId; | ||
while (currentId) { | ||
const node = this.blocks.get(currentId); | ||
if (!node) break; | ||
result.push(node); | ||
currentId = node.nextId; | ||
} | ||
return result; | ||
} | ||
getBlock(blockId: string): BlockNode { | ||
const block = this.blocks.get(blockId); | ||
if (!block) throw new Error(`Block ${blockId} not found`); | ||
return block; | ||
} | ||
|
||
collectOperations(): Operation[] { | ||
const ops: Operation[] = []; | ||
let currentId = this.headId; | ||
while (currentId) { | ||
const block = this.blocks.get(currentId); | ||
if (!block) break; | ||
ops.push({ | ||
type: "insertBlock", | ||
node: block, | ||
}); | ||
currentId = block.nextId; | ||
} | ||
return ops; | ||
} | ||
|
||
applyOperations(operations: Operation[]): void { | ||
for (const op of operations) { | ||
if (op.type === "insertBlock") { | ||
const block = op.node; | ||
if (!this.blocks.has(block.id)) { | ||
this.insertBlockAfter(this.tailId, block.id, block.type); | ||
} | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.