19 KiB
19 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
classDiagram
Error <|-- RPCErrorWrapper
class RPCErrorWrapper {
+data?: unknown
+code: RPCErrorCode
+toJSON(): Inline
}
MessageEvent <|-- RPCNotificationEvent
class RPCNotificationEvent {
+notification: ChunkNotification
}
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
classDiagram
class ChunkListStateMutator
<<Interface>> ChunkListStateMutator
class ChunkListStateAddMutator
class ChunkListStateDeleteMutator
class ChunkListStateModifyMutator
class ChunkListStateMoveMutator
ChunkListStateMutator <|.. ChunkListStateAddMutator
ChunkListStateMutator <|.. ChunkListStateDeleteMutator
ChunkListStateMutator <|.. ChunkListStateModifyMutator
ChunkListStateMutator <|.. ChunkListStateMoveMutator
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 의사코드
의사코드는 다음과 같이 진행된다.
서버 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();
}
}
다른 작업들
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
} }