diff --git a/tools/template/architecture.md b/tools/template/architecture.md new file mode 100644 index 0000000..b334901 --- /dev/null +++ b/tools/template/architecture.md @@ -0,0 +1,385 @@ +# 5. 설계 + +## 5.1 UML + +### 5.1.1 Server Side UML + +```mermaid +classDiagram +class PermissionDescriptor { + +canRead(path: string): boolean + +canWrite(path: string): boolean + +canCustom(path: string, options: any): boolean +} +<> PermissionDescriptor +PermissionDescriptor <|.. PermissionImpl +class PermissionImpl { + +basePath: string + +writable: boolean + +canRead(path: string): boolean + +canWrite(path: string): boolean + +canCustom(_path: string, _options: any): boolean +} +SessionStore o-- UserSession +UserSession *-- PermissionDescriptor +class SessionStore~T~ { + +sessions: Record + +get(id: string): T + +set(id: string, value: T): void + +delete(id: string): void + +saveToFile(path: string): Promise + +loadFromFile(path: string): Promise +} +class UserSession { + +id: string + +superuser: boolean + +expiredAt: number + +permissionSet: PermissionDescriptor +} +<> UserSession +``` +```mermaid +classDiagram +Router <|.. FileServeRouter +class FileServeRouter { + +fn: Handler + +match(path: string, _ctx: MatchContext): any +} +class MethodRouterBuilber { + +handlers: MethodRouter + +get(handler: Handler): this + +post(handler: Handler): this + +put(handler: Handler): this + +delete(handler: Handler): this + +build(): Handler +} +class ResponseBuilder { + +status: Status + +headers: Record + +body?: BodyInit + +setStatus(status: Status): this + +setHeader(key: string, value: string): this + +setBody(body: BodyInit): this + +redirect(location: string): this + +build(): Response +} +class MatchContext +<> MatchContext +class Router~T~ { + +match(path: string, ctx: MatchContext): T +} +<> Router +Router <|.. TreeRouter +class TreeRouter~T~ { + -staticNode: Record> + -simpleParamNode?: SimpleParamNode + -regexParamNodes: Map> + -fallbackNode?: Router + -elem?: T + -findRouter(path: string, ctx: MatchContext): [TreeRouter, string] + +match(path: string, ctx?: MatchContext): T + +register(path: string, elem: T): this + +registerRouter(path: string, router: Router): void + -setOrMerge(elem: RegElem): void + -registerPath(path: string, elem: RegElem): void + -singleRoute(p: string): SingleRouteOutput +} +MethodRouter <-- MethodRouterBuilber: Create +Router <|.. MethodRouter +Router <|.. FsRouter +Response <-- ResponseBuilder: Create +``` + +```mermaid +classDiagram +class ChunkMethodAction { + +action: (doc: ActiveDocumentObject) => ChunkMethodHistory + +checkConflict: (m: ChunkMethodHistory) => boolean + +trySolveConflict: (m: ChunkMethodHistory) => boolean +} +<> ChunkMethodAction +ChunkMethodAction <|.. ChunkCreateAction +class ChunkCreateAction { + +params: ChunkCreateMethod + +action(doc: ActiveDocumentObject): ChunkCreateHistory + +checkConflict(m: ChunkMethodHistory): boolean + +trySolveConflict(m: ChunkMethodHistory): boolean +} +ChunkMethodAction <|.. ChunkDeleteAction +class ChunkDeleteAction { + +params: ChunkDeleteMethod + +action(doc: ActiveDocumentObject): ChunkRemoveHistory + +checkConflict(_m: ChunkMethodHistory): boolean + +trySolveConflict(_m: ChunkMethodHistory): boolean +} +ChunkMethodAction <|.. ChunkModifyAction +class ChunkModifyAction { + +params: ChunkModifyMethod + +action(doc: ActiveDocumentObject): ChunkModifyHistory + +checkConflict(m: ChunkMethodHistory): boolean + +trySolveConflict(_m: ChunkMethodHistory): boolean +} +ChunkMethodAction <|.. ChunkMoveAction +class ChunkMoveAction { + +params: ChunkMoveMethod + +action(doc: ActiveDocumentObject): ChunkMoveHistory + +checkConflict(_m: ChunkMethodHistory): boolean + +trySolveConflict(_m: ChunkMethodHistory): boolean +} +``` +```mermaid +classDiagram +DocumentObject <|.. FileDocumentObject +class FileDocumentObject { + +docPath: string + +chunks: Chunk[] + +tags: string[] + +updatedAt: number + +tagsUpdatedAt: number + +open(): Promise + +parse(content: unknown[]): void + +save(): Promise +} +class Participant { + +id: string + +user: UserSession + +send(data: string): void + +addEventListener(type: T, listener: (this: WebSocket, event: WebSocketEventMap[T]) => void): void + +removeEventListener(type: T, listener: (this: WebSocket, event: WebSocketEventMap[T]) => void): void + +close(): void +} +<> Participant +Participant <|.. Connection +class Connection { + +id: string + +user: UserSession + +socket: WebSocket + +send(data: string): void + +addEventListener(type: T, listener: (this: WebSocket, event: WebSocketEventMap[T]) => void): void + +removeEventListener(type: T, listener: (this: WebSocket, event: WebSocketEventMap[T]) => void): void + +close(code?: number, reason?: string): void +} +class ParticipantList { + +connections: Map + +add(id: string, p: Participant): void + +get(id: string): any + +remove(id: string): void + +unicast(id: string, message: string): void + +broadcast(message: string): void +} +FileDocumentObject <|-- ActiveDocumentObject +class ActiveDocumentObject { + +conns: Set + +history: DocHistory[] + +maxHistory: number + +join(conn: Participant): void + +leave(conn: Participant): void + +updateDocHistory(method: ChunkMethodHistory): void + +broadcastMethod(method: ChunkMethod, updatedAt: number, exclude?: Participant): void +} +class DocumentStore { + +documents: Inline + +open(conn: Participant, docPath: string): Promise + +close(conn: Participant, docPath: string): void + +closeAll(conn: Participant): void +} +ParticipantList o-- Participant +DocumentStore o-- ActiveDocumentObject +``` +```mermaid +classDiagram +class IFsWatcher{ + addEventListener(): void + onNofity(e: FileWatchEvent): void +} +<> IFsWatcher +class FsWatcherImpl{ + onNotify(e: FileWatchEvent): void +} + +``` +### 5.1.2 Client Side UML + +```mermaid + +``` + +## 5.2 의사코드 + +의사코드는 다음과 같이 진행된다. + +### 서버 RPC 메세지 처리 + +클라이언트는 RPC를 진행하기 위해 웹소켓을 연결합니다. 웹소켓의 주소 `/ws`에 +도달하기 위해서 먼저 라우팅이 진행이 됩니다. 라우팅은 `TreeRouter` 클래스에서 + 진행됩니다. +``` +Input: req Request +Output: Response +fn Route(req){ + for node in HandlerNodes { + if(req.url prefix is matching){ + node.Handler[matching](req) + } + } + response with 404 Not Found +} +``` +마침내 엔드포인트에 도달하게 되면 웹소켓을 얻어내고 새로운 연결을 등록합니다. +``` +Input: req Request +Output: Response +fn RPCHandleEndpoint(req){ + ws, res = upgradeWebSocket(req) + user = getUserInfo(req.header) + conn = new Connection(ws, user) + registerParticipant(conn) + res +} +``` +이제부터 메세지를 받을 수 있습니다. +메세지가 오면 이 함수가 실행이 됩니다. +``` +Input: message on send +Output: response +fn Connection.handleMessage(msg: string){ + data = JSON.parse(msg) + check format of data + dispatch(data.method, data, this) +} +``` + +디스패치가 성공적으로 이루어지면 RPC 작업를 처리하는 함수에 도착합니다. +청크 작업을 예로 들겠습니다. 청크 작업에서는 권한을 확인하고 +명령의 충돌을 과거를 비교하며 다시 적용하며 해결합니다. +그리고 요구된 작업을 처리하고 문서의 `updatedAt`을 업데이트하고 +문서 업데이트 사실을 이 문서를 보고 있던 참여자에게 전파합니다. +``` +Input: conn Connection +Input: m RPCChunkMethod +Output: response + +fn ChunkOperation(conn, m){ + doc = DocStore.getDocument(m.docPath); + updatedAt = m.updatedAt; + action = getAction(m); + if !conn.userpermissionSet.canDo(Action) { + response with PermissionError; + } + + appliedList = doc.history.filter(x => x.updatedAt > updatedAt) + for m in appliedList { + if action.checkConflict(m) { + resolvedAction = action.tryResolveConflict(m) + if resolvedAction == Fail { + response with ConflictError + } + action = resolvedAction + } + } + + res = action.act(doc); + doc.updateHistory(res); + + subscribers = doc.getSubscribers(); + subscribers.broadcastNotification(m, exclude = conn); + response with res +} +``` + +문서 작업도 마찬가지로 이루어집니다. + +### 클라이언트의 처리 + +클라이언트에서는 다음과 같은 일이 일어납니다. + +``` + +``` + +### 다른 작업들 + +``` +global doc + +searchWord(word) { + words(doc) + |> filter((w, _) => w = word) + |> map((_, i) => i) +} + +searchWordPrompt() { + word = prompt() + wordPositions = searchWord(word) + highlight(wordPositions, length(word)) +} + +when Ctrl-F is pressed { searchWordPrompt() } +``` + +``` +module chunk { + type mode = Read | Write + + struct Chunk { + id: string + content: string + type: string + } + + newChunk() { + { id = uuid() + ; content = "" + ; type = "" + } + } + + chunkViewer(chunk : Chunk, deleteThis : () => void) : Component { + var mode = Read + + var c = new Component( + content { value = chunk.content } + settypebutton + editbutton + deletebutton + ) + + when mode becomes Read { chunk.content = content } + + when editbutton is clicked { mode = (mode = Read) ? Write : Read } + when deletebutton is clicked { deleteThis() } + when settypebutton is clicked { chunk.type = prompt() } + + return c + } +} + +module document { + struct Document { + title: string + path: Path + tags: string set + chunks: chunk.Chunk array + } + + documentViewer(doc: document) : Component { + var focusedChunk = null + + var c = new Component( + taglist { value: tags } + chunklist + ) + + delete(id) { + i = doc.chunks.find((c) => c.id = id) + doc.chunks.remove(i) + } + + chunklist = doc.chunks.concat_map((c, i) => + [ divider(i), chuknViewer(c, () => delete(c.id)) ]) + + when divider(i) clicked { doc.chunks.insert(i, c) } + when chunkViewer(c) is dropped on divider(i) { doc.chunks.move(c, i) } + } +} +``` \ No newline at end of file