# 5. 설계(Architecture)

## 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
classDiagram
Error <|-- RPCErrorWrapper
class RPCErrorWrapper {
    +data?: unknown
    +code: RPCErrorCode
    +toJSON(): Inline
}
MessageEvent <|-- RPCNotificationEvent
class RPCNotificationEvent {
    +notification: ChunkNotification
}
```
```mermaid
classDiagram
class ViewModelBase {
    +updateAsSource(path: string, updatedAt: number): void
}
<<Interface>> ViewModelBase
ViewModelBase <|.. IViewModel
class IViewModel {
    +pageView: IPageViewModel
}
<<Interface>> IViewModel
IPageViewModel <|.. BlankPage
class BlankPage {
    +type: string
    +updateAsSource(_path: string, _updatedAt: number): void
}
IViewModel <|.. ViewModel
class ViewModel {
    +pageView: IPageViewModel
    +updateAsSource(path: string, updatedAt: number): void
}
ViewModelBase <|.. IPageViewModel
class IPageViewModel {
    +type: string
}
<<Interface>> IPageViewModel
IPageViewModel <|.. IDocumentViewModel
class IDocumentViewModel {
    +updateOnNotification(notification: ChunkNotification): void
}
<<Interface>> IDocumentViewModel
class ChunkListMutator {
    +add(i?: number | undefined, chunkContent?: ChunkContent | undefined): void
    +create(i?: number | undefined): void
    +addFromText(i: number, text: string): void
    +del(id: string): void
    +move(id: string, pos: number): void
}
<<Interface>> ChunkListMutator
class ChunkListState {
    +chunks: Chunk[]
    +cloen(): ChunkListState
}
class ChunkListStateMutator
<<Interface>> ChunkListStateMutator
class ChunkListHistory {
    +history: ChunkListHistoryElem[]
    +limit: number
    -applyLast(mutator: ChunkListStateMutator, updatedAt: number): void
    +current: ChunkListState
    +currentUpdatedAt: number
    +revoke(): void
    +apply(mutator: ChunkListStateMutator, updatedAt: number): boolean
}
class ChunkMutator {
    +setType(t: ChunkContentType): void
    +setContent(s: string): void
}
<<Interface>> ChunkMutator
IDocumentViewModel <|.. DocumentViewModel
class DocumentViewModel {
    +type: "document"
    +docPath: string
    +chunks: Chunk[]
    +history: ChunkListHistory
    +buffer: Inline
    +seq: number
    +tags: string[]
    +updatedAt: number
    +tagsUpdatedAt: number
    +updateOnNotification(notification: ChunkNotification): void
    -updateMark(updatedAt: number): void
    +apply(mutator: ChunkListStateMutator, updatedAt: number, seq: number, refresh?: boolean): void
    +updateAsSource(_path: string, _updatedAt: number): void
    +useChunks(): [Chunk[], ChunkListMutator]
    +useTags(): [string[], (tags: string[]) => Promise<void>]
    +useChunk(chunk_arg: Chunk): [Chunk, ChunkMutator]
}
IViewModel ..> "1" IPageViewModel
ViewModel ..> "1" IPageViewModel
ChunkListHistory ..> "1" ChunkListStateMutator
ChunkListHistory ..> "1" ChunkListState
DocumentViewModel ..> "1" ChunkListHistory
DocumentViewModel ..> "1" ChunkListStateMutator
DocumentViewModel ..> "1" ChunkListMutator
DocumentViewModel ..> "1" ChunkMutator
```
```mermaid
classDiagram
class ChunkListStateMutator
<<Interface>> ChunkListStateMutator
class ChunkListStateAddMutator
class ChunkListStateDeleteMutator
class ChunkListStateModifyMutator
class ChunkListStateMoveMutator
ChunkListStateMutator <|.. ChunkListStateAddMutator
ChunkListStateMutator <|.. ChunkListStateDeleteMutator
ChunkListStateMutator <|.. ChunkListStateModifyMutator
ChunkListStateMutator <|.. ChunkListStateMoveMutator
```
```mermaid
classDiagram
class IRPCMessageManager {
    +opened: boolean
    +close(): void
    +sendNotification(notification: ChunkNotification): void
    +addEventListener(name: "notification", listener: RPCMessageMessagerEventListener): void
    +addEventListener(name: string, listener: EventListenerOrEventListenerObject): void
    +removeEventListener(name: "notification", listener: RPCMessageMessagerEventListener): void
    +removeEventListener(name: string, listener: EventListenerOrEventListenerObject): void
    +invokeMethod(m: RPCMessageBody): Promise<RPCResponse>
}
<<Interface>> IRPCMessageManager
IRPCMessageManager <|.. RPCMessageManager
class RPCMessageManager {
    -callbackList: Map<number, RPCCallback>
    -curId: number
    -ws?: WebSocket | undefined
    +opened: boolean
    +open(url: string | URL, protocals?: string | undefined): Promise<void>
    +close(): void
    -genId(): number
    +genHeader(): Inline
    +send(message: RPCMethod): Promise<RPCResponse>
    +sendNotification(message: ChunkNotification): void
    +addEventListener(type: "notification", callback: RPCMessageMessagerEventListener): void
    +removeEventListener(type: "notification", callback: RPCMessageMessagerEventListener): void
    +invokeMethod(m: RPCMessageBody): Promise<RPCResponse>
}
class FsDirEntry {
    +name: string
    +isDirectory: boolean
    +isFile: boolean
    +isSymlink: boolean
}
<<Interface>> FsDirEntry
class FsStatInfo {
    +isFile: boolean
    +isDirectory: boolean
    +isSymlink: boolean
    +size: number
    +mtime: Date | null
    +atime: Date | null
    +birthtime: Date | null
}
<<Interface>> FsStatInfo
FsStatInfo <|.. FsGetResult
class FsGetResult {
    +entries?: FsDirEntry[] | undefined
}
<<Interface>> FsGetResult
class IFsEventMap {
    +modify: (this: IFsManager, event: MessageEvent<NotImplemented>) => void
    +create: (this: IFsManager, event: MessageEvent<NotImplemented>) => void
    +delete: (this: IFsManager, event: MessageEvent<NotImplemented>) => void
}
<<Interface>> IFsEventMap
class IFsManager {
    +get(path: string): Promise<Response>
    +getStat(path: string): Promise<FsGetResult>
    +upload(filePath: string, data: BodyInit): Promise<number>
    +delete(filePath: string): Promise<number>
    +mkdir(path: string): Promise<number>
    +addEventListener(name: string, listener: EventListenerOrEventListenerObject): void
    +removeEventListener(name: string, listener: EventListenerOrEventListenerObject): void
    +dispatchEvent(event: Event): boolean
}
<<Interface>> IFsManager
IFsManager <|.. FsManager
class FsManager {
    -manager: RPCMessageManager
    -prefix: string
    +get(path: string): Promise<Response>
    +getStat(filePath: string): Promise<FsGetResult>
    +upload(filePath: string, data: BodyInit): Promise<number>
    +mkdir(filePath: string): Promise<number>
    +delete(filePath: string): Promise<number>
}
FsGetResult ..> "*" FsDirEntry
IFsEventMap ..> "1" IFsManager
IFsManager ..> "1" FsGetResult
FsManager ..> "1" RPCMessageManager
FsManager ..> "1" FsGetResult
```

## 5.2 의사코드(Pseudo Code)

의사코드는 다음과 같이 진행된다.

### 5.2.1 서버 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
}
``` 
마침내 엔드포인트에 도달하게 되면 웹소켓을 얻어내고 새로운 연결을 등록한다.
여기서 reqeust session으로 유저를 얻어내서 접속하는 것에 대해 주의하라.
```
Input: req Request
Output: Response
fn RPCHandleEndpoint(req){
    ws, res = upgradeWebSocket(req)
    user = getUserInfo(req.header)
    conn = new Connection(ws, user)    
    registerParticipant(conn)
    res
}
```
이제부터 `RPCMethod` 메세지를 받을 수 있다.
메세지가 오면 함수 `handleMessage` 가 실행이 호출된다.
```
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
}
```

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

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

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

먼저 `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();
    }
}
```

### 5.2.3 다른 작업들

```
module chunk {
  type mode = Read | Write

  struct Chunk {
    id: string
    content: string
    type: string
  }

  newChunk() {
    { id      = uuid()
    ; content = ""
    ; type    = ""
    }
  }

  chunkViewer(chunk : Chunk, focusedChunk : State<string>, 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 mode becomes Write { focusedChunk = chunk.id }
    when focusedChunk is changed { mode = Read }

    when editbutton is clicked { mode = (mode = Read) ? Write : Read }
    when deletebutton is clicked { deleteThis() }
    when settypebutton is clicked { chunk.type = prompt() }

    return c
  }
}
```

```
module search {
  searchWord(chunks, word) {
      return doc.chunks.concat_map((s) => s.matchAll(word))
  }

  searchWordPrompt(chunks: Chunk.chunk list) {
    var word = prompt()
    var results = searchWord(chunks, word)

    var c = new Component(results)

    when result in results is selected {
      moveto(result.location)
      close()
    }
  }

  when Ctrl-F is pressed { searchWordPrompt() }
}
```

```
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, focusedChunk, () => 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) }

    return c
  }
}
```

```
module filelist {
  fileList(dir : Directory, open: (File) => void) : Component {
    var c = new Component(
      filelist
    )

    filelist = dir.files().map((f) => button(f))

    when button(f) is clicked {
      open(f)
    }

    return c
  }
}
```

```
module settings {
  settings() : Component {
    var c = new Component(
      language = select("korean", "english")
      theme = select("light", "dark")
    )

    when language(l) is selected {
      global context.lang = l
    }

    when theme(t) is selected {
      global context.theme = t
    }

    return c
  }
}

```
module frontend {
  main() : Component {
    var docv

    var open = (f) => {
      with doc = openfile(f) {
        docv = document.documentViewer(doc)
      }
    }

    var filelist = filelist.fileList(rootdir, open)

    var c = new Component(
      document = docv
      filelist = filelist
    )

    return c
  }
}
```
