아키텍쳐 추가
This commit is contained in:
parent
039df94182
commit
c7a1072080
385
tools/template/architecture.md
Normal file
385
tools/template/architecture.md
Normal file
@ -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
|
||||
}
|
||||
<<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
|
||||
}
|
||||
```
|
||||
|
||||
문서 작업도 마찬가지로 이루어집니다.
|
||||
|
||||
### 클라이언트의 처리
|
||||
|
||||
클라이언트에서는 다음과 같은 일이 일어납니다.
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
### 다른 작업들
|
||||
|
||||
```
|
||||
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) }
|
||||
}
|
||||
}
|
||||
```
|
Loading…
Reference in New Issue
Block a user