# 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
}
<<Interface>> 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<string, T>
    +get(id: string): T
    +set(id: string, value: T): void
    +delete(id: string): void
    +saveToFile(path: string): Promise<void>
    +loadFromFile(path: string): Promise<void>
}
class UserSession {
    +id: string
    +superuser: boolean
    +expiredAt: number
    +permissionSet: PermissionDescriptor
}
<<Interface>> 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<string, string>
    +body?: BodyInit
    +setStatus(status: Status): this
    +setHeader(key: string, value: string): this
    +setBody(body: BodyInit): this
    +redirect(location: string): this
    +build(): Response
}
class MatchContext
<<Interface>> MatchContext
class Router~T~ {
    +match(path: string, ctx: MatchContext): T
}
<<Interface>> Router
Router <|.. TreeRouter
class TreeRouter~T~ {
    -staticNode: Record<string, TreeRouter<T>>
    -simpleParamNode?: SimpleParamNode<T>
    -regexParamNodes: Map<string, RegexNode<T>>
    -fallbackNode?: Router<T>
    -elem?: T
    -findRouter(path: string, ctx: MatchContext): [TreeRouter<T>, string]
    +match(path: string, ctx?: MatchContext): T
    +register(path: string, elem: T): this
    +registerRouter(path: string, router: Router<T>): void
    -setOrMerge(elem: RegElem<T>): void
    -registerPath(path: string, elem: RegElem<T>): void
    -singleRoute(p: string): SingleRouteOutput<T>
}
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
}
<<Interface>> 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<void>
    +parse(content: unknown[]): void
    +save(): Promise<void>
}
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
}
<<Interface>> 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<string, Participant>
    +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<Participant>
    +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<ActiveDocumentObject>
    +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
}
<<interface>> 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
}
```

문서 작업도 마찬가지로 이루어집니다.

### 클라이언트의 메세지 처리 동기화

클라이언트에서는 다음과 같은 일이 일어납니다.

먼저 `notification`을 받습니다. 그러면 모든 `DocumentViewModel`에게 이벤트를 전달합니다.
그리고 각각의 `DocumentViewModel`은 자기 문서에 일어난 일인지 확인하고 `ChunkListMutator`를 만들어서
문서에 적용합니다.
```
Input: e RPCNotification
Input: this document view model

fn updateOnNotification(this,notification){
    { docPath, method, seq, updatedAt } = notification.params;
    if (docPath !== this.docPath) return;
    mutator =  ChunkListMutatorFactory.createFrom(method);
    this.apply(mutator, updatedAt, seq);
}
```

문서의 레디큐에 mutator를 집어넣고 `seq` 번호가 기다리는 것이면 실행하고 업데이트합니다.

```
Input: mutator ChunkListMutator
Input: updatedAt Date
Input: seq number
Input: this Document View Model

//readyQueue is priority queue.
fn apply(this, mutator, updatedAt, seq){
    this.readyQueue.push({mutator, updatedAt, seq})

    if(this.readyQueue.lenght < some limit){
        while(!readyQueue.empty() &&
            this.readyQueue.top().seq === this.seq + 1)
        {
            mutator, updatedAt, seq = this.readyQueue.pop();
            mutator(this.chunks);
            this.seq = seq;
            this.updatedAt = updatedAt; 
        }
        this.dispatch(new Event("chunksChange"));
    }
    else {
        document.refresh();
    }
}
```

### 다른 작업들

```
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) }
  }
}
```