SRS/tools/template/architecture.md
2022-05-14 14:30:58 +09:00

10 KiB

5. 설계

5.1 UML

5.1.1 Server Side UML

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
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
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
}
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
classDiagram
class IFsWatcher{
    addEventListener(): void
    onNofity(e: FileWatchEvent): void
}
<<interface>> IFsWatcher
class FsWatcherImpl{
    onNotify(e: FileWatchEvent): void
}

5.1.2 Client Side UML

 

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