From 3751423c5f2b14491f9e059eebc6868405524407 Mon Sep 17 00:00:00 2001 From: Varun Sethu Date: Mon, 13 Mar 2023 23:43:49 +1100 Subject: [PATCH 1/5] adding OT documentation --- backend/editor/OT/README.md | 163 ++++++++++++++++++++ backend/editor/OT/client_view.go | 4 +- backend/editor/OT/docs/editor_arch.png | Bin 0 -> 38992 bytes backend/editor/OT/docs/operation_parsing.md | 103 +++++++++++++ backend/editor/OT/main.go | 4 +- 5 files changed, 270 insertions(+), 4 deletions(-) create mode 100644 backend/editor/OT/README.md create mode 100644 backend/editor/OT/docs/editor_arch.png create mode 100644 backend/editor/OT/docs/operation_parsing.md diff --git a/backend/editor/OT/README.md b/backend/editor/OT/README.md new file mode 100644 index 00000000..16a1e183 --- /dev/null +++ b/backend/editor/OT/README.md @@ -0,0 +1,163 @@ +# The Concurrent Editor +This is a rather full on package so this README document will outline how exactly the (OT) Concurrent Editor. As a high level overview though the concurrent editor is an OT based editor thats heavily influenced by Google's WAVE algorithm, Wave was Google's predecessor to Google docs and heavily inspired the architecture for Google Docs. Our editor uses the same underlying architecture as Wave except the difference lies in the type of operations we are sending back and forth between the server. Wave was originally designed to operate with operations that modified unstructured data, our operations modify structured JSON data. + +Before reading the rest of this document I highly recommend you read through the resources linked in the base [README.md](../../README.md). + +## High Level Architecture +At a very high level our editor server consists of 3 distinct layers. + - **The data layer** + - This layer is basically the server's copy of the current state of the document. This is what all incoming operations will end up modifying. Our initial design for the data layer involved a singular struct modelling the entire document that we modified using reflection, this however proved rather tricky due to the intricacies of Go's reflection system so we ended up moving to an AST based approach. Currently the data layer is just the AST for the JSON within the document, operations modify this AST directly. To prevent corrupted documents we have various data integrity checks utilising reflection. + - **The client layer** + - The client layer is an internal representation of an active client connection. Whenever a client connects to our server a new client object is allocated to represent their connection. + - **The document server layer** + - The server layer is a big object that models a singular document being edited, it maintains a list of all active client objects that are connected to it and the current state of its AST. + +![Editor Architecture](./docs/editor_arch.png) + +## Starting a Document +I personally feel like its rather easy to understand a system if you understand how it achieves its key features. This section is about what exactly happens when a user clicks the "edit" button on a document and constructs an edit session. What type of objects are created? Where do they live? Etc. + +### The connection starts +So the user clicks the "edit" button, this instantiates a HTTP request thats handled by the HTTP handler in `main.go`: `func EditEndpoint(w http.ResponseWriter, r *http.Request)`. This handler takes the incoming request, looks up the requested document and if it exists upgrades the connection to a Websocket connection. + +After upgrading the connection to a websocket connection the handler then asks the `DocumentServerFactory` to either create or fetch the object modelling an active edit session for the requested document. If the document does not already have an active edit session the `DocumentServerFactor` will proceed to read the document from disk, parse it (construct an AST for it) and return a `DocumentServer`. + +After the `DocumentServer` is fetched a client object is allocated and registered with the document server and a new goroutine is spun up to handle incoming operations from the client. The handler code is as follows (note it is subject to change): +```go +func EditEndpoint(w http.ResponseWriter, r *http.Request) { + requestedDocument := // parse request body + targetServer := GetDocumentServerFactoryInstance().FetchDocumentServer(requestedDoc) + + wsClient := newClient(ws) + commPipe, terminatePipe := targetServer.connectClient(wsClient) + + go wsClient.run(commPipe, terminatePipe) +} +``` +Note that during the connection process the document server ended up returning a `pipe`, this `pipe` is actually just a small function that the client (or at least the client's shadow on the server) can use to propagate messages to the server it is connected to. + +### The client applies an operation +So the client has just applied an operation to their local document, the frontend has captured this and set it to the server via websockets, now what? 😳. If you remember back to the previous code snippet the last bit of code spun up a goroutine to run the `wsClient.run` function, this function is an infinite loop that is constantly reading from the websocket and forwarding the operations to the document server. The `wsClient.run` function at the moment of typing up this document looks something like: +```go +func (c *clientView) run(serverPipe pipe, signalDeparturePipe alertLeaving) { + for { + select { + case <-c.sendOp: + // push the operation down the websocket + // send an acknowledgement + break + + case <-c.sendAcknowledgement: + // push the acknowledgement down the websocket + break + + case <-c.sendTerminateSignal: + // looks like we've been told to terminate by the documentServer + // propagate this to the client and close this connection + c.socket.Close() + return + + default: + if _, msg, err := c.socket.ReadMessage(); err == nil { + // push the update to the documentServer + if request, err := operations.ParseOperation(string(msg)); err == nil { + serverPipe(request) + } + } else { + // todo: push a terminate signal to the client, also tell the server we're leaving + signalDeparturePipe() + c.socket.Close() + } + } + } +} +``` +The bit of interest is what happens in the `default` branch of the select statement (we will talk about the other branches later). Within this branch we attempt to read something from the websocket and then parse that (we will cover parsing a little later as its surprisingly complicated), we then use the `serverPipe` mentioned previously to send that request to the document server. + +This `serverPipe` is a function built by the `document_server` during connection it is a closure returned by the `buildClientPipe` method. The function is relatively intense and hands on so a lot of details have been left out here +```go +func (s *documentServer) buildClientPipe(clientID int, workerWorkHandle chan func(), workerKillHandle chan empty) func(operations.Operation) { + return func(op operations.Operation) { + // this could also just be captured from the outer func + clientState := s.clients[clientID] + thisClient := clientState.clientView + + // ... skipped implementation details + + // spin up a goroutine to push this operation to the server + // we do this in a goroutine to prevent deadlocking + go func() { + defer func() { + clientState.canSendOps = true + thisClient.sendAcknowledgement <- empty{} + }() + + clientState.canSendOps = false + + // apply op to clientView states + s.stateLock.Lock() + + // apply the operation locally and log the new operation + transformedOperation := s.transformOperation(op) + s.operationHistory = append(s.operationHistory, transformedOperation) + + // apply the transformed operation locally (note that this is being applied against the server's state) + if !transformedOperation.IsNoOp { + newState, err := op.ApplyTo(s.state) + if err != nil { + log.Fatal(err) + clientState.sendTerminateSignal <- empty{} + } else { + s.state = newState + } + } + + s.stateLock.Unlock() + + // propagate updates to all connected clients except this one + // if we send it to this clientView then we may deadlock the server and clientView + s.clientsLock.Lock() + for id, connectedClient := range s.clients { + if id == clientID { + continue + } + + // push update + connectedClient.sendOp <- transformedOperation + } + s.clientsLock.Unlock() + } + } +} +``` +so whenever we get an operation from the client we: + 1. communicate it to the document_server via a pipe + 2. the operation is then transformed against the entire log of operations the server has applied + 3. the operation is then applied to the server's representation of the document + 4. the operation is then communicate to all other clients + +### The document server wants to propagate operations +If you remember previously how it was mentioned that the document server "propagates" operations to the clients? It does that by sending these operations down a channel maintained by each client +```go +type clientView struct { + socket *websocket.Conn + + sendOp chan operations.Operation + sendAcknowledgement chan empty + sendTerminateSignal chan empty +} +``` +`sendOp`, the `wsClient.run` function is constantly listening for messages down these channels and actions on them accordingly. + +## Operation Parsing & Application +For information regarding how operation parsing and application works see ![this](./docs/operation_parsing.md) document. + +## Lock Acquisition Order +To prevent deadlocks we have a defined lock acquisition order for each type, thankfully theres not many as most synchronization is achieved using channels. They are as follows. + +`document_server` + 1. document state lock + 2. client lock + +`server factory` + 1. active servers lock diff --git a/backend/editor/OT/client_view.go b/backend/editor/OT/client_view.go index 7ebbbd5e..2d0eb94b 100644 --- a/backend/editor/OT/client_view.go +++ b/backend/editor/OT/client_view.go @@ -33,7 +33,7 @@ func newClient(socket *websocket.Conn) *clientView { // them down the websocket, it also pulls stuff up the websocket // the documentServer will use the appropriate channels to communicate // updates to the client, namely: sendOp and sendAcknowledgement -func (c *clientView) run(serverPipe pipe, terminatePipe alertLeaving) { +func (c *clientView) run(serverPipe pipe, signalDeparturePipe alertLeaving) { for { select { case <-c.sendOp: @@ -59,7 +59,7 @@ func (c *clientView) run(serverPipe pipe, terminatePipe alertLeaving) { } } else { // todo: push a terminate signal to the client, also tell the server we're leaving - terminatePipe() + signalDeparturePipe() c.socket.Close() } } diff --git a/backend/editor/OT/docs/editor_arch.png b/backend/editor/OT/docs/editor_arch.png new file mode 100644 index 0000000000000000000000000000000000000000..32af70450cb0c3aabcdf7475460d030553393362 GIT binary patch literal 38992 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

Uo445R zcN+NXMn1u9#A~|f?yD%!zPs)xV==;CK5LibZeN^1Ddp&}_o@nu*BI|p`Ozm*b7Ns( z!M6-V({GwAc(+5)Rz%21-!0|>|H*q5j5b7OYu=9%OCZ_v<={TC-PAh0{7Kb}#Yorr z#yzh&oU}|0VnMd+F8asJr@8(hWBa``tvKD{V{;}U4+3l61EuAdl>Ov?sIU(%XfHFM z2SzZ(1t%@m$c}B}MJ6S875?_w-OzMVGM(3A;Z|ba(mT`wW zegp18PKm^d?$-MD>}@L`$B#3w&mS%v4NfQB_r|a7QQ|_74J&FX1=hT&n_)4V1Nq6p z{|9^D8P!zqz8M9vfryHLG)1LJ?;TMAmEJ*0klv9RY64=RBT6rVARtA0Zvm;&LXl1g zf)IM7h7gkM4gUUn&Ys;5yWe)t+4+={xp(f|_s)IiooAl+xz^{EhsOvkR;aqeBgh(t zF+}H6M{4C~T^%**FS=4*Pk@|yP;@2Z)t2P#Gb?!UR+VI(S@B)$I>%ZQYs@%?RF9Q< zF!FaElSZI2LGidSzH@8N5pFRG3-I=-8GE?hv9b&%?I)p((P_tq`!>-%i0Et6hSr2#Q}W9}gQ%R?j^;!e@CMS0BENhu*Z-)bag!rl4R zQKBm}S(Rt|`y`{t4k3`Bafrtg4#A|aJG=Ahyb9pz<6_H%SbHEH&SI8K^mA!5~z?- z?ox*fd?2nOh|t`ZwfGE=O!B0(twiz4CCW12h{7+Usp*T#8=Teq>j zJ^OtjAE2?I<|X$qOQz-s8@(E2A<~=z^ls$HxI%bE-Rej`(x-j)?&yPT0IzWZ=vPxw z-HU>Rxgm-SNaXq_ccLSS3hxj?fGu~g3wpo3J5tUZ+SBdF-4NdbUV@07#TNBNP%R!! z5|CJCbc7IhzQt0B_vbnzNtcmT)PvsX3;qH6!~0HO6=D}nPTEPkM3uxR1&4v**dt%- z;3a?Mb5Gz@_!i1Lw8bc6X}Xk?9r&>-bH5Iq_Zq6Ds=(_o;L4rP%5N?-ZIzay+_}&G z+z!cS{Y&tFz=8|%DREzAf@86YF8LkT&KO_`KKKbR(PkE59sPOxmDp$0ILmOSmm_4y z7p9*69aS+|MG^AS=-Gzzt-tRRA^4|0z8*k^RT;22XY?GYzh%H6D}GOP$saE8G3U2$ z)2q9$v!Fe`MjZPx5Wx|LpdDCH0peg@t%*KnwdZ9v78%)>1*KV}^Xp8JpB2x?Uix{( zl#+E8w~N?n!Iy1)?r`hc3r9f(cQRUMbMe(z5t5(1e~;x+t^yf^-iPZz|H>+fPLwC07+760QT}u|73&Y zU={faFXD{$$wVAS_0gfk($6=UKhkUc zpn<{fup~Q@Z}uY%kQDdU%;vZ9Z};D+nB)W0a!}iy@OQs3P}00g_Q@VB zXMWFj=_2b}zxf^LEOv&qvlC5T`v!|2)@418ez~`tm9>hWbp)elNt0RS}F)s%Bbck?x@Sp~TX|Vgj+L3#Ech5uRN;;U{A&({(TSiB)%h6d;M_6m3E?NA5qD z%U~*&O^-Nma}nh6g>kdgVB%XXJJ;9Eu!aebW&%nfq7v(hl^>9xI?jH+zJ>4{V`2 z2qU7kMC5LLhmfT;0i;YcF^)kk@#fLb^S$8QAAMBXwPqT zFV{Q5ENRwJ|8*GK8&QB@pz?g#%Bn39;2Mx64ah|*i8p=1+g0MIj~r%^OV7m0s}<;^ zT}E-cRJKT=m`O4Oen3}o|;z83foy60Ey?ar<1bw@a*FaN*(O94p9>hS&C zE=GL?VTYt`xa?^Y3sS}V^JW&$sFNzmQZzsbu`aWh8Y^o1#zmzZ49GN+r6z&WR$;V8 z(%E}tC6vIMdHRo~_CAe$w7jU0{=dt%feKwn6_L?PT5o~j3KZ&(t{qI{qCIBuH(xAK z{JRT^oXRD(4Qrj$k*6$@G1Z1T)BpDKDeQV)*#!{YM}94BI5>oWJ{GF+SX0@t@8Vxh^EMrr?#`Qpo?H+C{FQn)=+43Ty#I`|p}rlyJp9 zm+wxU_B$k3f!ZF`F)+C^@WI_5q3tVwA7?QCgEx`EEBe^~3gzQ^xHl^%Mvk=NEh5vsm& zFXJM%kV|sxKS<4N31_T(Q;lw*^G>`2PXumVa3NrN$;MJ;+b~pVhp` z&18?o`#MkblZ`Ay7^ThEGqLC`R&!iX&Qsqr3*sV!YCsv@iZ>YYj{3lfxS0ed4!ET9 zdMQ6g6ts^#RR%1xLK8|2ntgCfy0`4~s_)FIDGq#Hex%kK)>qWa!2%QvChWad;~%G4 z0dlX)R=oX_x?Zj?16oI(g35D6i1_~`d;IB(`K=Wo>-$@McK-w!@R`3eAuMf%ke%-* zQHID@o%}{mn(=>jo1%?KuZDxGIg!0W$QQ$}D_hVk*4bD2diL7aH%1OjVl(V?b^JNY zhW->LT~U?ql9ICBJ2<(K>}^8T*5R~5(k#5u1+1ZgA*=nM-?I>9PeC{2?F5j|P-x(0 z9bOv%68*^C#kwy~pHd7gZ!cTgtqHd7;0hp3a3Gh-ud^Gv``b&| z1W`e0*q%X9ynQwQ-T=P z{1nLl22#QSI4l~_8#ZYz&$2x26(ep419N*>@)WI`>UNLZdTi#mBqsdNIAG15o&`@k zdwU#O9fl;cp&RMXX{SdAH{Om;ZAMNX-Lk zjnpMz0$5Qry{{s*`AA(SKedv(7zdv(K7BnY3?+0;etA{uYT4rKio_U59i%1IC`WdH z*(PLZ=WbBEv~-<8FIC{875VCHnJksR(hJSZz-4A8a*(>#_e-D10B_n4oGeox3GIn{T;(3dq~N6KL`KjJkr*G*0#a(HRVUMBw=0p zlGkTfEnb&R7))XtsqZLv(hx(kua;JNx!z*|kWBr13XG_mi`}b~uLZ12Jj39)ld!|&ty;`Qty_!vr4f?j8 z{2DOEm!!UwUT}#PG|LGr(a7O8X^k)1Dx^6C>(vDo8KW5hWYOQ;g zo)S=!J$vvx)o1kWskI$_EemZ3B7JFR+TP#-!35NN6_4w&H`xJM8V0S_O0qV5If`4L z2C09bfNaj=Mbl*=-nujCJ%~Zn5mD88n zZgOsJyoLQD$QY7LDquaYYrn^O+UaRur)?2(%NK7-vYrN31FeIZm(P*!&3k3^+`oe+ zpPfIw+kwHnmeYSyCt1%oJ_LLu=fi@#rqaW*o(yM_8a#Ht}ReBI|PUswN^GCT7awkAdSso-CkL$~b*vobBb zCU8iE3>=Gjq)zIsQd}hl-4>@Co05SIRie}Qyo|R3ik;2+Kbx+2g!?<>d_SN9xwxDf zLcN5Mo3z;lDf1&Pp&Dd@*LvZFL15~A_MmC}s3684RT-kBsg4#!!CQB3L+ckZ)B>`u zE1nP5RL3J|Q31CAa$cS=u;Z)t7e>y1CS_c%H6MKtXebUx86Ul9#QrSH z7@d8C2u_}p=?MoT`H?yK?P z$?tS3yNnYR$=+RO2sk5+w=X@omD$_rF(3AYV4%wDKd(g%Ld5`oNPbyCZNFx- zeui8GZ(fYKlF2#k}NuCB~tCzB$?0E1+$7Hh#<~`#2K;%b`mX=Ag-LOovcTb=0D>`i+_qd}0MPkEjRcLC) zh323|pSmjqXd=zJIVFfW;?(=<2~wrUB?WwbV}baz4f3SEmsJyayDg7CW(yvFQHuK7 z{bElG*ic?!teI9)ulG0J4T&oZE`ipExO^r-gMQ+=+k#b6mx3SCKg+F|q|EuuhWX&U zMeC8V!s8VT<|HxFF(Y#@fEQCI>~7E!O2yL(_MvcTO71&413L9x1K|ioPrXAN<3nO^ zgJeDo-;LR)YRbK*M{O+0#D_>U3-~)l5rc0DTKd^kiNFAKVtv(pKk1J@uQhJhNS2vU z^=dreSryPpRUY{=84&~+=zqqM*>mRk;M454X^4n$ynkQAu%_I}euC>C)|+usUr z-!V%XZkIQ*RWtr>C~y}&@{Z{-^K>FuQIY^gs>TOjOXVhnra~J&i!2Z12+AMYKd`vo z-ZEDUx18-m_nC+`Y^F_VxfibT+(4Ci9%=?bk-Oe>`?z0^;Z+=_Du>h{=9tqFCYqaU zvYS~P7`A|wSUs>Z!TeBmQBp#eI;RxePPTa3y=O>-i*}nb=X1TU7a6ntM-v3Kbq_pU@0;qh1 zo2*dVHV~oomli@HM&})&w4qi9jz4slcXzn>y;tF)PhVKfHOj)i57Hu!IG^jxm%tPz z0FS&+$x~L>XgihEVqcZ56Q2Dnfj7$MmWgo1V1OcX0;E4lVuQ-p4F^S`UCdK|UPDFg zwkFMT2_ij|CA9UYGE*nb3*aI&pDuHsZBw~_>PZJ8Lstdm@sG4pdo{yvYuH@YPXtAQ zH1<1J**g^u#|m_3Do=cxR96#q4?gPz)XVv8621Ge3auQoTq~r~9+FIqn%gv3G`mbI zu_$CHeyIw~!GB!H@?SH$aX|4C$0}fqrP+oyTzJ$?it-F>h)yy42{+SrAU&Nm`bz^! z(*~kPPzcT4v$upFvYLpGP<&n2B-U-4DU=hC+xR|Tep`953V05%Ze%()(sa*gah>e_ zbU3?j8}!e63$^kxN^B6EteR$Zo^Lc-73Y&*# z+N4fH$l;>71utPK`J#=XCV%Dpr_>g(^)D!8F7N3n>5zXcH^)h)n+aGRb zJH#--Cf;D;Ukya^$6NQ3WRnN4QT+Cq2j8a}@me12zW>~z1xD&Ilw-H8GHlue-x~FU z&M1GlS2tG|ggM?vBYhwx)!XY-c8@Ah`*CE?<8E zhba=#D_~lb!KD~Hy2*SQu(5X!J+KTgJ$1#^ypy0--VeHo&$I<7CLXjH=u&DHQ8niR z+BNSy;fQmAIYGvn14{>G@R~}Jx{X#42Q=%VnDRt_eWj`bF_X50m%{!7B1jP2$_G%~ zCg;r>Hj`aofyYZxZe;_EY0Dq&45$vKc#_AMIqNXZ{Gal?jU-RTor z%?BMaY-_^yz3L7Q34WcmRG@Xe(=lwQ{PSy=U?8dz&GF${upS3Um(o4Q##lh=(*p|M zjo;~`O(N4wJ48l@)(uq{vi~Y~A0SF|1cbg|Tfq-aA|~93MA{|=yz~xJR?E@$e6zBD zZLVT4@76XIp8yit)yDZ3h_WniB^%c|!*QSIAL8n}JcEhx>7$8xTQ_qO4CT`J0&QY~ znmMM#BO(dkVvT8)+m=>;av7Z{RK9S-5qHs6@`M1%5B1LdfB8yx?Gx3~ms>@bsnd7G z7G;fyn;VTvbw>(G6b|v{41!chfo{Q1n8ZL1kH`y}5JP3$_pnqYVsGowlbE(gZrX}e z--=(K7eVKGO@ zD>dcRl9xHnhU3{#N?Jox4!(TWGbu_rK~qkOTvQ$I=DF`Sgh>sf9!q~m+*|DpT3jQe zQw0b8%2FTMcrQO6JUBksBpH{Nos#W1&*nKi!>wDZ>LaWq|#-E9} z4I@ZeZV=Z}lP+480#tIk25oB$Z(jg6;TJ>04m^yYHtd}n>r`UL1+si0SI6Qe&BthN zJsJKz6{56VnUZxu;z{;OyEQHKNi^fbI7GN=Lc|MaW&f(0Ya4#L$-$$YM0&infuVXy z?uGqe!09~8jU2U;rW14g7&AsfGVPm(Z!w0lgaTg}L7JTfO(2)S$nj^O@^j&a!uSmlXO!3Se?{_%>E&DKB+%TkFO)Q2ae)i6^$uM3cgEa zO%>s3*HqV>gB5+EmF7{B%PnoV&2*ik_v6NhzEOhMZ18daUIt~`Y~^uG2sUyS!ix;!Ap4}62A+F9WgF%^d{%`uN{G%2Mj z<-n*xLJg;}2S`0)bwiX-=(>sMh&vT1#q8f|U2pOAt&V{G@T}@1clkQ20bNx#Oy=x4lPys`S^M`3`<6e)Kqnqg| z)Wqo{vo^ftu289U3JW!8Hl8L82sN$9K)Pvxhn{kki$QDd1G&`{lLAfGMuC4|QT<-! z+qbcsr|?FVFt-S_qzw|uh=30r9uvK;!Jb|$eQ&EnOS&Y# zg{-6`mi9W?4A<{h+Cm%sPG)2UO^q3kXDC7Hi5ySIlLdJ|IB(%s}5 zjy7q0UW=R~Tg56FRe^svQ|*^6DhX*fE2ygbG&LhS|k06z$P$({D=Q2ynyxqL~8%-WBJhU)M5{aQ*; zn@ZIOnKf<56ZEjvi<9pBYCm)r3&q0rBTXfRDQ2# z)RqC+;C^RN2>lCGQcM?H2LRLMrHu^nkb4Zk$0AGKHhBNm9k$AsE}*hBk9l=^C7)jp znm(Mu(-T9XO(ev;KUy8=N!uF-?`8ua%$oQKBPh1wKR0NkD*}7y+2;ymqS%kS;4`qt zX?KPS&!mX?#MJ@5y!=Wl@$fy?jhO1)W5z{(BZgz&;&L5PCHq+(k}BG)&*{O|RG zzzxetSNQCNIrfonWt2vC9>B66_TQAuqX6EkiJ$q^YwKdmfc1se-m$s^NOiU{FeZLe ztXoll%7<#Z{2kMD$$h^@aoYFcoUUp=vKeJ$$4#1rGjWOC}diA5S>Tvz|DWNAn<8ZGEz=1|JTFV|6AaC4zPdQraxQhwakMow2oS{!3y)Q zAZ8LIvJ&C$4&wbEO(owh#bv5%+t#3p{YVp&nXOhR;?EAQQBm4V##tg{rC3YNepA+U z`lLIk8!Z=%0=}Lw=P8*wS%lfGhr2r;NalhoX!!FMBg)@0V?2a z4B>%nvu|1mXWt5|L=nHMj#W?LhN{LsgON!DEyk8KH9Jj`AD&CJGr)XM5UomLBB7hJ!xPJ0bCk}|)9RMit+Wk;oE!o7VlC<6|^ ztEeo}PQ%+7isLf8ksx@-?1aCNdDXv55&CQfikq-SeDA_;VW6H`wts!ZVWdc(rY#1< zCV__Oo&x%BmVka~x>w`a6t-MEJuW+=spt)TooT-`%M8 zn%x{-3AJr(?#W=dE5q!o;p+aZy{fX=%-EAIy%rx?o{jUMkhN?yuu8wqPh|oTr>^c? zSdW733{bj;6Zl13fpzM!^1{7&(}EKN%4ZfH<7!Ul00dZ+U*x9>PZfQC9h!tp;#9Wb zq^Zj7c0TT^mBnHqlUD}zXAjiu3nsn8WI!cx7m`%-t>n^(g!9H)mj58Nm?Go$Gw)s;@D@K$z2=$9#bEf)V$ zn`y_UmB28ShTCd%Ap?osk&`_buf>BMCM`knZ-P_~L)%cWX8(W@r31&XDEC$U``b@j zu`P;ble^{}&(jXzrEZQ@M}fi)%qkDpe~qB?MO~P?H=+>Ht|H%5bq1{6WGY-W*}i|q zeCjJWqnEuFb9tw3EA_qR{&7VY3emW%0I`dWlT9YFmF4x?@uT>LeGxsz@QM--vs>O_ zN+ta%b)2^Pp}s)82NI%REZuiUNuIo}+g{+0eryo&-QDNBz14?8u&|V8HGV4H@di7^ z!E*c8spR6;kF}w|1Reuk%809D=GH`j! z9*cA^imduAhwt#;4cVQAxmE0)0`}B2*U;BY8++1?>lBYZvC|B^jgboQ#B&b@>_lD@Xz7Q<^E;hgQ(qHAioCdjGNCs# z6h!;(wyd6P+Y5egvTErB;g=p$Sy)5CsSU4)Q0x!j?4*vN?Jl1n&AE8?vDh<6W7M)^ zk8_=%xp7v7SPBahldxbMLAfgQaM3G}VIi1etZRR9@4YQgSD;r*NTh8PE*ix!2CI}^ zI3d7rxqU97#9(1As+Kz2;|t0{OI&%kpQd|K;48L2GYM%`xJJ23yS$j4_p_6jVjPPD zX^&&?9Ogb&GhuExU_F!dn4(gq=*Fzicg$0^eVnc0_I6oCY#)4sK++o1w%;~wm|v28 zWz_~*|BJpEuf6=@?`wn_DWvJa}(-Kx9J13tSDQ+ zVp|s;b(%*huNCI}4)TXao>@yto>J&|tz7XP3^nk7`09c$u(uOsr8;N7SmoKCs;W@eDzD(#uTy{;xr-$D^>6jp*>B@(_A+tp$-OCs zd%@8Jj@H>_|41}cyK2+O#ATN8tD|gFF7E?@vw?J$KY?}*!d9wyQ*`ZHIwiOfS~@?j zgp5BJf>E7wIQ44jrjZt+T0uAmOoNH|w_lFt@g8Gey;;hA$?GVIdd3o%huv@!c*MwB zzMtuBj-YCyOw5n>P?Zi5I=aq?GxMA=ZFkl3MUM0aR3RqTAyQPI-;cY-wTGuz=y(=7 zS)~7h#rfc7w6@m{1{^%=Z1`qBpOJ?J3_3QQN!zWRk+5NxTN)p%VxGCW(cG+gx9w(} z5|ezqzqnMw;heYU?1R^@e3F|cCG%vT8q9N6ATol9Yim7s=giGE-CarYz2_*j__p>c zO1)6Ceu!+0-FQGW!Nb_HU`T>;;oOcSc4yY&sGrH?r^iySW9nwlBM;*4AT3KNbw-be z#}o~3RZh{bwJ|j&q-ydIg*CD}=Ca3$AS%4vpC>FJf#}udM+48h3<^m#FtBo}DPw)l zc!M8>tM@Nt^G}>R=lm(^z__&$0QWt9H*I7dV637o=zU(_qsxw1FhPbqZ#b{=#nCeV z>V|Auo6*4^2N7C==lu%cu`cI5GU)ESmoW8>7koIwjNR{xEy*Y8j30+z^U%-_K3_XQ zZMX7TUHP3sx~I|~j#*x+P0!GuFO}dQ0>PVcJDCx-zd9~i%EhuvPhU=9i0j>sPJhTT z1;YfwjG5Hq;2o~)UYt7PIPC;%v=5`?ekaaLZgGmWhqRiNBpZvVZ*K7l$WE(L2SDwT z`s*#dcHy$O&Ldmug9|%NOmpn~8oVD<6Jhw)M93xsq){*sW@Gq^PgH;0uO-=kn1=I4 zXh>1h)ujewNuRzu^c!^!d=@oBQB+G^L7j5@uNje+&q`VJ444XxOXs>yBJx1S`>7Eq z%x39^=fmve`yL0e`-8=_1v1CUxx5>#6ADy`DcWoD8)#Ao$t5{EHGpog1H zJK>m#F{~m!NHDF@_DRumH*x;Q*Jh>bT63!{Se4)D`b*F9xz}zn?B)GErZ5l|Y*0|x znagH-E}W8xCw4(NUe_q&lBXn>?B^1LcBeKa)2STvSnPAeoKE=Tm|6N3{ciQl?fOrA z9+1{#ft){3d2oD0KZwEOFWqEHT~cW68WfP$2tXSkt9PbCY-0}SmC9Dd#{^>wyQHmQ z?Cqi=S;BDdD$U<(cY+zbg0DRunJqVlcl?6E*M2fK z;_`y0*Z5V2i|MMHdcu)b535O5Vk>uyM0jUyh_$)Xp2pG*pXDkvTMyFX1WYBnGQ$<| z_@-o<)$RbMiR%U6gpaET>PQ^Q)Z;fBg9oi|ozCy-ymzGy(Mc%m z5WABR*EMHxE{8!JR4w2cc?+XMm$eN7L(eBEttjggCq&>#4gQgcn2??ZnZL-B-RG!(82c z&Y;1@RL;mJ^cI6Z!Rqt+#KkfvZW0ofFuXHrlfukP`Cmy73%Rx?DnD zZfhzjFaEZE0mBL3Aieg~w#z4;^`6w?$UIkT!szlh1o= znKq^r;(@Jj?fZ~WHU+JDsvHsivgkPB4^>i>rk_JBxYW@bGy z2xJ5cg|No?J86C%U6y90SWP$hlt{L|HC(6mWf0mLXZ_GgJPZPAwyZ&ONRM>5Ct$Jm ztrGN@*-z4{z^g1isz2UfO8NcWxtkL&DEU=P;2L;|l|IJgahiocPwb9YB1^J3$D%*B zX(+%F9;%J#jpBN|uUk+6cS}W+Bgs=W{KsnXNyp{5MYkhj)BXNDeouy64M0`3Pd0JG zD|uPqo#w2#Vs+=kPqOqzfkxKbkNk}-?+Os>l0R>hX((*A_$Y(E2feqnQ9eL`OC1S% z*Kelq+z8{357z%B8TT=$gX%%#WunY-yGjcPVf*vfR?jcNvy60nJTUu!JyK{5kGo;P zSCc!f*oM&ZU8q6c)Vqoddq~~OP1uf?@x}V7ZaP1r8yKeqQmISxicjzd8$?VJMM?~8 z%ZrYY)T8N!`M>PqU&{62%m?h{ijKc3b3RYtm2KQ_%z^g{c_?Vx?A)-G!yg|=$DcZtpW|NV zJ6Kv6jfY^lA-B7mBu?rN>h$~Pt4K895l9S(cBabPTqKN)jQ?&o)FHIWT#n)*e0tLb23oFA&!!@>9XDWNE78dub*+sXs z9X_&73QoXEO}zLfe+pw{4ffa&q|FWAf<`NLQ;mhMFy`)%sRv~N&m^8+KO@o36f$0X z$x>`BSwh|Ig@?s80wa16rJ5z1FWzt`XL2?_=ZOI&g(0wwq&#e`9g;7nD4QfAurAT! zo>MenY=G+aBPye_J`ZXZF=zM#OR4jvJ@|Qi$GnGPlDr$=C4-(7i<>(eNs17a{7yGF zrl`Hf$_r;RD=^i4%$|K?7Q$L_nI)U5H?mAxb?zmJSEjLN{Y_8q1;Brhr9siHx3mES z?wmtE+%~EH!S^7DsoC(amMV`F9T73j=$+S)8qFBLNyqe>;-#~-a=l<1<*u&4={UH; zXII%P9$zfX_8F`)X)gtI66tIaR$u|}hh!31{DbzDMe3>+t5*wacvV`CPSEpGm!6Xp zZ043YmBe~qqC^x<5QFileK}VVIFi-k*%eq_k()A&sz7#xaK+leQH_ z7Y8qUsQVc}(hTm`P#uW4aKt_yxyw&QT4E{E{J?ob17p1aGqWxCL_574u2iAYz-4ev zm}fB4Jcu)ulvRjb-eJ5E9_bbGy^I|?SNft`Y4YMulT3T+Tnt;skdHMo2?gnHt=S;- z%#e{OOM}H91u-Ie!p7{7@dMg-D}7hwptDD3c~O`vIa+}cA!ZQ;FMZ031srycaNot1;U`cxaYPi#3O(**tNNpGxA4oym9wuxfYASqDP0)SM^!Hc%Dq z{ME5*Bsa51`{OtTFOR#=4LCcUZZKu;qN zUUa+`&CQ7F_P*;k(mymWj=F5uH7IU+i?YU(>O0)b*H072nKj&hg-Afg%vaea-yu5M z_MagG0%y4dyo-+IZ>T(5D{CB%ifV8a)VUFMxnV)wjL*5HX_+(hZk)b~Ec5?7F&wXlLEmB_aor=C`bb9E}}6Ubykc zY5sjdcR4uAxJUo32yt}U*>mYhX7Hj-%!2xMkXHgBZ?FW?-Jb-&>P|Np zG7Z*~WVj!kRkRJemBEmAXOWHpzvHI?xg9hr5bN1ql+sgV8^sqj_jXawnei(x_QFiF zw~~2nNxbu8E|IXm2B}O2w%mQTnnh=o10Ot3yWu%~t7Xmh^_vQUC*E6K(P;*LM1!-m z<(q6k28%#K+41=L?S&ZEO2ab)MIY^S`j*{^?O#IU+({F;)7YdsS2t!ii%U*4v+o*E zid9t8wjgGm?W3ABX5~dj*%G5?CM?BEa_+-8OIE^}EQ$Sq4#L_ajGxIk+HP*Tkf6f9 zBU|dVIt3Yu+d3?sNHvpn@@13*3EhV|gnQ<4Ec4g^DwV z(VaxD?ZfS+B+QNwpT@3Vw0uDibM7}ZwWwJg*A>%O^y=NI6p9tG6poQnDdD`#1=(aW ze8e}TyeR60rmHw9ZfWPhPIm+>47elp17B#+qgM@-QjNg~z5~Wc=SWtVyYt~m`4E~~ zk4q>))=0Y1`^s47y@TejOyoHv|M|Xu?>T$VFtkQzmt!fLKjp~HpF$I=ZURuAA-y_oZ)Y3s)bSn=x@OZ9~uBA@!v%N=Je4BFFm)FuxfJpYu`h11F? zNTzs5m%)_Jt1a>^+EwR~Uy9ko^}@V81qrGKJzxkW*4Ou$_W~Yr{k$@JDL3J2DC;V@e8147#4Pi;REAn zs1>@6yshI;cQyv2VqB(K1Qt@#7z z|Gd7g99PF{aHc#f`iq!@+e`(jQ{w0MjzQD+v@u5KVsZdxT&8&4-Wayj4CZX$7W2Rq-L$_rP(n1Esq5_e=hZQU4vOiElMh2z7u+#LQtd6?w85=3O8Ds z3!5~%1G+RPGA+*+y%u9<^Ruq^Gsk?G@?V$z}qs8Oh3!g%K3O7?fgE9 z|Ab(_ds>PMvF%sHnLT1u zVQw*>|F~fO9)ML~1w3;|Psh7-%)SDl0QG9A!Q*|8sL+^RAzlH_;y(Tx)3;p(jD%Oq z(-iLdJn>bfd~C&N z2|r2?nkmJFo(UPZ<5IQE3u=zOtz=Wd!gCN9uBs*42fQ4GwN&4xA3njhU^cC zr$;VXy3H?&axCpv!2Q;7IKIBW-NNE^Zc!gAeLCe-%kwVWdvdi%s_YxDM*7CAv7Awj z%hCr~)Y{-|mHK9kZnGtj-`+0Irb$>|Ka+9`koP;|`D9dP_xL-B=9 zrNH4dx&UQVaTGC|Xpm_LY%eo4v{K31cv-EUV+=zxX9as0fI(m zN9gU8n39AVUM8bol!CRG`Uxd~>{o_Lu`>&a3uM)r@y(z&m=&V3jP3vUQpM~f8K3WI z)+>GRCrTE-K^;VU9p(i;FpV>08{?SLyF%R=0)e?FOLfCrWfi4&W;n8S_A`G*SU!iT zXaI)WNM3`G3~CDLTxC*7{y4+6UO7s z8OAaNQKJC{p|yc(d;Nb3W#;>UXe;DQQQy;n*UwB#0oa#4t|Ix_wA6Y!y1D_?J7>&X zb%H#fLKPP9R*WuelI{YqD`p|>pjh%oKfHZk2f>h&;RLYSDrN3o&s+20%*R5AmJr(& zch-1*eV;Kko4M{|wi|E4ZyS<=uoLbOSp(ZH>Pw?tf$VJ77m>!L^&3YF>% z@6U4}^>-YfI4WhkeXd+h<<$VbC+vrCI!1P8L&wo7Lb-07Dx`fYA!tJMqKy3NqBtGs z*4OnQ{`tvj$+`N#+JmGDySNsM59_tZ8#`QT!Dr78x>v-e&l`(9cki8V{c9R9uOUmG zsoNT*u(ZCk{Qi|`XXrqdyYzYKInBm4ZY2g2R>=84;Wmr?27fzhrdm42NDI4$R!I-9 z=VQBe!i?;p#jB#%Y^z|Vmmqz(imjGb1JnB~umETbI z@yy%$+=^{v97wDHP0egFqE|l0?DqZn=h8e++Ll*VJgYTn3XaO88g#+5WkACLb7-mr z#ibpbsqdiR2LIH=;oyO})5klXOkv0K8iM59n2-oM$cHP}7%*o=L2WN0l#U&#JO8L# z(Y8D@hPQym8N-9tCqwih69=fb(`E2qzy^9r zUGJfS>*Q`oe>PuAk*A}xL1o<9DU-Td;1(gNUz}yjqMUaeFz{C%`yWr-Q!Y$Bq0+EH zXUH^8^?#c_7^J0V*l2o&NO{zZ)|RaIdgLp`^q;+Y?K_n%>_=_C_m8$zo#9*- zv-|Qcd_YPc`MfqzJmH#&(0j$Y4?|GoS__+~{kH5D4yBr;zONPiP;)HgTe80G(L z_B#P2a6#skL(1Zu5;Cdk|JkIshfOyD)=LT8Q9;lLAbV7Qld}WZ)eoa&SQ!9rntF() zOhSGFKwio34q5sRaKfq`+ys!H0FFl{fBq15G=a=Gsb=&(Wu)B9Oj9F&*{*zkMMjT# z;T2Hzi}4>><)-U)eLp~nIjG}tZ4~|QJdN1~N>KIsHbm}ZX*-V0teR*y<6=F=!Ds3O zHH7~M_DENPn>x@mbX#3-2w-yPEet8aZvgC^q)dq{H-KtZt_cMQKFRqRKxa9T<^MXn znFDKbe<{G*IgylaJ%lWNJs7%#<8_ZDu=;{M`Zmh#73v2XQ@e z-|rRuqxAS(g&gM%ZJIGsfP`>2o6uSR(*d44;H@Sl9)Jfja}}Kl|I2=e%&?0lcjVcp zr7bcAbR3v^j$*gtPFXIGxR7HsR%QuyDZn8U<{eAl0rs1*{odYM-*#Le@HnDTnPg6o zV>g*3lPRu1cG)2pcO)@>f8S}&c>4glVZz~~|7}|K|EK@9pJ65d45ux*SHPys5GYi5 zud@ucM9ytHaM6^rw61|La7oRDMWM3!sX!06{QPxW{@(7&;^()33)<>|d$boc?x-&R ze9`xMT*>o&ty>yF_UiUNuFwXy!Gu@sSnx(}-F+ML>EF~C7*2FIE|h-Wzf9V!Dqz_e zU`!;NTmuKTPvh?UA3rn#7vv-a-zstZzQ209+-;y|q|Z7npDebkv8_5ty>_YfrsDHQ o>vz6CcO(F~j3y)DZ2$KE+^J{$JIqX`F9hWyPgg&ebxsLQ08;1!Z2$lO literal 0 HcmV?d00001 diff --git a/backend/editor/OT/docs/operation_parsing.md b/backend/editor/OT/docs/operation_parsing.md new file mode 100644 index 00000000..8a3242b1 --- /dev/null +++ b/backend/editor/OT/docs/operation_parsing.md @@ -0,0 +1,103 @@ +# Operation Parsing & Application + +Surprisingly most of the complexity in the OT editor lives in the operation parsing and application logic. This document aims to help demystify some of it. As a quick aside, everything regarding operations can be found in the `operation` folder. + +## The operation struct +Operations (in Go) are defined as a struct, the operations we receive from the client conform to this structure and are parsed using a JSON parsing library (more on that later as there is a bit of complexity behind this). +```go +type ( + // EditType is the underlying type of an edit + EditType int + + // OperationModel defines an simple interface an operation must implement + OperationModel interface { + TransformAgainst(op OperationModel, applicationType EditType) (OperationModel, OperationModel) + Apply(parentNode cmsjson.AstNode, applicationIndex int, applicationType EditType) (cmsjson.AstNode, error) + } + + // Operation is the fundamental incoming type from the frontend + Operation struct { + Path []int + OperationType EditType + AcknowledgedServerOps int + + IsNoOp bool + Operation OperationModel + } +) +``` +The above code snippet uniquely defines an operation. Operations take a path to where in the document AST they are being applied (see the paper on tree based transform functions) and the physical operation being applied. The operation being applied (`OperationModel`) is actually an interface and is the reason why parsing operations is more complex than it seems. This interface defines two functions, one for transforming a operation against another operation and one for applying an operation to an AST. + +The reason why `OperationModel` is an interface is because there are several distinct types of operations we can apply for varying types. Theres different operations for editing an `integer`, `array`, `boolean`, `object` and `string` field. Each of these define their own transformation functions and application logic so in order to maintain a clean abstraction we use an interface. As an example this is how the `string` operation type implements this interface ![here](../operations/string_operation.go), its a rather intense implementation since it also implements string based transform functions. + + +## Operation application +Recall that the `document_server` maintains an abstract syntax tree for the current JSON document, this AST is exactly what `cmsjson.AstNode` is, the document server maintains the **root node**. When applying an operation to a document we invoke the `Operation.ApplyTo` function +```go +func (op Operation) ApplyTo(document cmsjson.AstNode) (cmsjson.AstNode, error) { + parent, _, err := Traverse(document, op.Path) + if err != nil { + return nil, fmt.Errorf("failed to apply operation %v at target site: %w", op, err) + } + + applicationIndex := op.Path[len(op.Path)-1] + return op.Operation.Apply(parent, applicationIndex, op.OperationType) +} +``` +this function traverses the document AST (as defined by the path) and applies the operation to the node pointed at by the path. The final application makes use of the `Apply` function within `OperationModel`. + +## Operation Parsing +So as pointed out earlier, parsing is rather tricky as our `Operation` struct contains an interface and the native JSON parsing lib in Go does not support interfaces. To get around this problem we wrote our own JSON unmarshaller based on the `goson` json parser, we call this unmarshaller `cmsjson`. + +The `cmsjson` library expects a full list of all types that implement an interface we wish to parse into, this list of types is defined ![here](../operations/json_config.go) +```go +var CmsJsonConf = cmsjson.Configuration{ + RegisteredTypes: map[reflect.Type]map[string]reflect.Type{ + /// .... + + // Type registrations for the OperationModel + reflect.TypeOf((*OperationModel)(nil)).Elem(): { + "integerOperation": reflect.TypeOf(IntegerOperation{}), + "booleanOperation": reflect.TypeOf(BooleanOperation{}), + "stringOperation": reflect.TypeOf(StringOperation{}), + + "arrayOperation": reflect.TypeOf(ArrayOperation{}), + "objectOperation": reflect.TypeOf(ObjectOperation{}), + }, + }, +} +``` +the configuration is in essence a mapping between the the `reflect.Type` representation of the interface and the `reflect.type` representation of every struct that "implements it". Implements is in quote as there is no way to statically verify this at compile time, instead if any of these config options are invalid a runtime error will be thrown during parsing. Usage of the `cmsjson` library for the most part is rather simple (thanks to generics in Go :D). An example can be found in the `ParseOperation` function within `operation_model.go`. + +Interally the `cmsjson` library determines what struct to unmarshall into based on a `$type` attribute in a JSON object. Examples can be found in the test suite for the `cmsjson` library ![here](../../../pkg/cmsjson/cmsjson_test.go). + +### More on `cmsjson` +This should ideally be under the `cmsjson` documentation but the library not only handles JSON unmarhsalling/marshalling but also exposes methods for constructing ASTs from a specific JSON document, this is used rather extensively by the `object_operation.go` object model to convert a document component to an AST. Once again, this is further documented within the `cmsjson` package, for the most part the package gives us the following interface for interacting with ASTs. +```go + // jsonNode is the internal implementation of AstNode, *jsonNode @implements AstNode + // AstNode is a simple interface that represents a node in our JSON AST, we have a few important constraints that should be enforced by any implementation of the AstNode, those constraints are: + // - An ASTNode is either a: JsonPrimitive, JsonObject or a JsonArray + // - GetKey can return nil indicating that it is JUST a value + // - Since a node can be either a JsonPrimitive, JsonObject or a JsonArray: + // - 2 of the three functions: JsonPrimitive(), JsonObject(), JsonArray() will return nil (indicating the node is not of that type) while one will return an actual value + // - We are guaranteed that one of these functions will return a value + // - All implementations of AstNode must conform to this specification (there is no way within the Go type system to enforce this unfortunately :( ) + // - Note that the reflect.Type returned by JsonArray is the type of the array, ie if it was an array of integers then the reflect.type is an integer + // - Note that jsonNode implements AstNode (indirectly), AstNode is of the form: + AstNode interface { + GetKey() string + + JsonPrimitive() (interface{}, reflect.Type) + JsonObject() ([]AstNode, reflect.Type) + JsonArray() ([]AstNode, reflect.Type) + + // Update functions, if the underlying type does not match then an error is thrown + // ie if you perform an "UpdatePrimitive" on a JSONObject node + UpdateOrAddPrimitiveElement(AstNode) error + UpdateOrAddArrayElement(int, AstNode) error + UpdateOrAddObjectElement(int, AstNode) error + + RemoveArrayElement(int) error + } +``` +If you look carefully, you can see how we attempted to emulate sum types using interfaces 😛. \ No newline at end of file diff --git a/backend/editor/OT/main.go b/backend/editor/OT/main.go index a4d6acfc..5a60b8bf 100644 --- a/backend/editor/OT/main.go +++ b/backend/editor/OT/main.go @@ -33,7 +33,7 @@ func EditEndpoint(w http.ResponseWriter, r *http.Request) { wsClient := newClient(ws) targetServer := GetDocumentServerFactoryInstance().FetchDocumentServer(uuid.MustParse(requestedDocument[0])) - commPipe, terminatePipe := targetServer.connectClient(wsClient) + commPipe, signalDeparturePipe := targetServer.connectClient(wsClient) - go wsClient.run(commPipe, terminatePipe) + go wsClient.run(commPipe, signalDeparturePipe) } From 81ff926a255bde90d6c3b55130c1a1bc86eb3729 Mon Sep 17 00:00:00 2001 From: Varun Sethu Date: Mon, 13 Mar 2023 23:50:42 +1100 Subject: [PATCH 2/5] fixing indentation --- backend/editor/OT/docs/operation_parsing.md | 50 ++++++++++----------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/backend/editor/OT/docs/operation_parsing.md b/backend/editor/OT/docs/operation_parsing.md index 8a3242b1..974ec71c 100644 --- a/backend/editor/OT/docs/operation_parsing.md +++ b/backend/editor/OT/docs/operation_parsing.md @@ -74,30 +74,30 @@ Interally the `cmsjson` library determines what struct to unmarshall into based ### More on `cmsjson` This should ideally be under the `cmsjson` documentation but the library not only handles JSON unmarhsalling/marshalling but also exposes methods for constructing ASTs from a specific JSON document, this is used rather extensively by the `object_operation.go` object model to convert a document component to an AST. Once again, this is further documented within the `cmsjson` package, for the most part the package gives us the following interface for interacting with ASTs. ```go - // jsonNode is the internal implementation of AstNode, *jsonNode @implements AstNode - // AstNode is a simple interface that represents a node in our JSON AST, we have a few important constraints that should be enforced by any implementation of the AstNode, those constraints are: - // - An ASTNode is either a: JsonPrimitive, JsonObject or a JsonArray - // - GetKey can return nil indicating that it is JUST a value - // - Since a node can be either a JsonPrimitive, JsonObject or a JsonArray: - // - 2 of the three functions: JsonPrimitive(), JsonObject(), JsonArray() will return nil (indicating the node is not of that type) while one will return an actual value - // - We are guaranteed that one of these functions will return a value - // - All implementations of AstNode must conform to this specification (there is no way within the Go type system to enforce this unfortunately :( ) - // - Note that the reflect.Type returned by JsonArray is the type of the array, ie if it was an array of integers then the reflect.type is an integer - // - Note that jsonNode implements AstNode (indirectly), AstNode is of the form: - AstNode interface { - GetKey() string - - JsonPrimitive() (interface{}, reflect.Type) - JsonObject() ([]AstNode, reflect.Type) - JsonArray() ([]AstNode, reflect.Type) - - // Update functions, if the underlying type does not match then an error is thrown - // ie if you perform an "UpdatePrimitive" on a JSONObject node - UpdateOrAddPrimitiveElement(AstNode) error - UpdateOrAddArrayElement(int, AstNode) error - UpdateOrAddObjectElement(int, AstNode) error - - RemoveArrayElement(int) error - } +// jsonNode is the internal implementation of AstNode, *jsonNode @implements AstNode +// AstNode is a simple interface that represents a node in our JSON AST, we have a few important constraints that should be enforced by any implementation of the AstNode, those constraints are: +// - An ASTNode is either a: JsonPrimitive, JsonObject or a JsonArray +// - GetKey can return nil indicating that it is JUST a value +// - Since a node can be either a JsonPrimitive, JsonObject or a JsonArray: +// - 2 of the three functions: JsonPrimitive(), JsonObject(), JsonArray() will return nil (indicating the node is not of that type) while one will return an actual value +// - We are guaranteed that one of these functions will return a value +// - All implementations of AstNode must conform to this specification (there is no way within the Go type system to enforce this unfortunately :( ) +// - Note that the reflect.Type returned by JsonArray is the type of the array, ie if it was an array of integers then the reflect.type is an integer +// - Note that jsonNode implements AstNode (indirectly), AstNode is of the form: +AstNode interface { + GetKey() string + + JsonPrimitive() (interface{}, reflect.Type) + JsonObject() ([]AstNode, reflect.Type) + JsonArray() ([]AstNode, reflect.Type) + + // Update functions, if the underlying type does not match then an error is thrown + // ie if you perform an "UpdatePrimitive" on a JSONObject node + UpdateOrAddPrimitiveElement(AstNode) error + UpdateOrAddArrayElement(int, AstNode) error + UpdateOrAddObjectElement(int, AstNode) error + + RemoveArrayElement(int) error +} ``` If you look carefully, you can see how we attempted to emulate sum types using interfaces 😛. \ No newline at end of file From 2946431750d823706a5709d251a845726ad8fc8a Mon Sep 17 00:00:00 2001 From: Varun Sethu Date: Thu, 16 Mar 2023 10:57:32 +1100 Subject: [PATCH 3/5] Apply suggestions from code review :sunglasses: Co-authored-by: Gary Sun --- backend/editor/OT/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/editor/OT/README.md b/backend/editor/OT/README.md index 16a1e183..889410b4 100644 --- a/backend/editor/OT/README.md +++ b/backend/editor/OT/README.md @@ -1,12 +1,12 @@ # The Concurrent Editor -This is a rather full on package so this README document will outline how exactly the (OT) Concurrent Editor. As a high level overview though the concurrent editor is an OT based editor thats heavily influenced by Google's WAVE algorithm, Wave was Google's predecessor to Google docs and heavily inspired the architecture for Google Docs. Our editor uses the same underlying architecture as Wave except the difference lies in the type of operations we are sending back and forth between the server. Wave was originally designed to operate with operations that modified unstructured data, our operations modify structured JSON data. +As a high level overview though the concurrent editor is an OT based editor thats heavily influenced by Google's Wave algorithm, Wave was Google's predecessor to Google docs and heavily inspired the architecture for Google Docs. Our editor uses the same underlying architecture as Wave except the difference lies in the type of operations we are sending between the server and client. Wave was designed to operate with operations that modified unstructured data, while our operations modify structured JSON data. Before reading the rest of this document I highly recommend you read through the resources linked in the base [README.md](../../README.md). ## High Level Architecture At a very high level our editor server consists of 3 distinct layers. - **The data layer** - - This layer is basically the server's copy of the current state of the document. This is what all incoming operations will end up modifying. Our initial design for the data layer involved a singular struct modelling the entire document that we modified using reflection, this however proved rather tricky due to the intricacies of Go's reflection system so we ended up moving to an AST based approach. Currently the data layer is just the AST for the JSON within the document, operations modify this AST directly. To prevent corrupted documents we have various data integrity checks utilising reflection. + - This layer is the server's copy of the current state of the document. This layer is what all incoming operations will modify. Our initial design for the data layer involved a singular struct modelling the entire document that we modified using reflection. This proved tricky due to the intricacies of Go's reflection system so we moved to an AST based approach. Currently the data layer is just the AST for the JSON of the document, and operations modify this AST directly. To prevent corrupted documents we have various data integrity checks utilising reflection. - **The client layer** - The client layer is an internal representation of an active client connection. Whenever a client connects to our server a new client object is allocated to represent their connection. - **The document server layer** From f47553eabe659baa926b74e7b8621193b61c96c9 Mon Sep 17 00:00:00 2001 From: Varun Sethu Date: Sat, 3 Jun 2023 12:21:41 +1000 Subject: [PATCH 4/5] Update backend/editor/OT/README.md Co-authored-by: Gary Sun --- backend/editor/OT/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/editor/OT/README.md b/backend/editor/OT/README.md index 889410b4..66c2f60e 100644 --- a/backend/editor/OT/README.md +++ b/backend/editor/OT/README.md @@ -18,7 +18,7 @@ At a very high level our editor server consists of 3 distinct layers. I personally feel like its rather easy to understand a system if you understand how it achieves its key features. This section is about what exactly happens when a user clicks the "edit" button on a document and constructs an edit session. What type of objects are created? Where do they live? Etc. ### The connection starts -So the user clicks the "edit" button, this instantiates a HTTP request thats handled by the HTTP handler in `main.go`: `func EditEndpoint(w http.ResponseWriter, r *http.Request)`. This handler takes the incoming request, looks up the requested document and if it exists upgrades the connection to a Websocket connection. +When the user clicks the "edit" button, this instantiates a HTTP request that's handled by the HTTP handler in `main.go`: `func EditEndpoint(w http.ResponseWriter, r *http.Request)`. This handler takes the incoming request, looks up the requested document and if it exists upgrades the connection to a WebSocket connection. This is important as a WebSocket connection allows for bidirectional communication between the client and server in real time without needing either to poll for updates After upgrading the connection to a websocket connection the handler then asks the `DocumentServerFactory` to either create or fetch the object modelling an active edit session for the requested document. If the document does not already have an active edit session the `DocumentServerFactor` will proceed to read the document from disk, parse it (construct an AST for it) and return a `DocumentServer`. From a009bf971a59b2e25eca8b2a15623f12b766d96c Mon Sep 17 00:00:00 2001 From: Varun Sethu Date: Sat, 3 Jun 2023 12:22:34 +1000 Subject: [PATCH 5/5] Apply suggestions from code review Co-authored-by: Gary Sun --- backend/editor/OT/README.md | 16 ++++++++-------- backend/editor/OT/docs/operation_parsing.md | 15 +++++++++------ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/backend/editor/OT/README.md b/backend/editor/OT/README.md index 66c2f60e..b77902e6 100644 --- a/backend/editor/OT/README.md +++ b/backend/editor/OT/README.md @@ -20,21 +20,21 @@ I personally feel like its rather easy to understand a system if you understand ### The connection starts When the user clicks the "edit" button, this instantiates a HTTP request that's handled by the HTTP handler in `main.go`: `func EditEndpoint(w http.ResponseWriter, r *http.Request)`. This handler takes the incoming request, looks up the requested document and if it exists upgrades the connection to a WebSocket connection. This is important as a WebSocket connection allows for bidirectional communication between the client and server in real time without needing either to poll for updates -After upgrading the connection to a websocket connection the handler then asks the `DocumentServerFactory` to either create or fetch the object modelling an active edit session for the requested document. If the document does not already have an active edit session the `DocumentServerFactor` will proceed to read the document from disk, parse it (construct an AST for it) and return a `DocumentServer`. +After upgrading the connection to a WebSocket connection the handler then asks the `DocumentServerFactory` to either create or fetch the object modelling an active edit session for the requested document. If the document does not already have an active edit session the `DocumentServerFactory` will proceed to read the document from disk, parse it (convert it from a text file to a go struct) and constructs a `DocumentServer`. This `DocumentServer` is responsible for managing the current state of the document, tracking the clients editing the document, and keeping the history of the operations. -After the `DocumentServer` is fetched a client object is allocated and registered with the document server and a new goroutine is spun up to handle incoming operations from the client. The handler code is as follows (note it is subject to change): +After the `DocumentServer` is created / fetched, a client object is allocated and registered with the document server and a new goroutine is spun up to handle incoming operations from the client. The handler code is as follows (note it is subject to change): ```go func EditEndpoint(w http.ResponseWriter, r *http.Request) { requestedDocument := // parse request body targetServer := GetDocumentServerFactoryInstance().FetchDocumentServer(requestedDoc) wsClient := newClient(ws) - commPipe, terminatePipe := targetServer.connectClient(wsClient) + commPipe, signalDeparturePipe := targetServer.connectClient(wsClient) - go wsClient.run(commPipe, terminatePipe) + go wsClient.run(commPipe, signalDeparturePipe) } ``` -Note that during the connection process the document server ended up returning a `pipe`, this `pipe` is actually just a small function that the client (or at least the client's shadow on the server) can use to propagate messages to the server it is connected to. +During the connection process the document server returned two "pipes". A "pipe" is a function that the client (or at least the client's shadow on the server) can use to propagate messages to the server it is connected to. ### The client applies an operation So the client has just applied an operation to their local document, the frontend has captured this and set it to the server via websockets, now what? 😳. If you remember back to the previous code snippet the last bit of code spun up a goroutine to run the `wsClient.run` function, this function is an infinite loop that is constantly reading from the websocket and forwarding the operations to the document server. The `wsClient.run` function at the moment of typing up this document looks something like: @@ -74,7 +74,7 @@ func (c *clientView) run(serverPipe pipe, signalDeparturePipe alertLeaving) { ``` The bit of interest is what happens in the `default` branch of the select statement (we will talk about the other branches later). Within this branch we attempt to read something from the websocket and then parse that (we will cover parsing a little later as its surprisingly complicated), we then use the `serverPipe` mentioned previously to send that request to the document server. -This `serverPipe` is a function built by the `document_server` during connection it is a closure returned by the `buildClientPipe` method. The function is relatively intense and hands on so a lot of details have been left out here +This `serverPipe` is a closure returned by the `buildClientPipe` method during the connection setup with the `DocumentServer`. The function is relatively intense so a lot of details have been left out here. ```go func (s *documentServer) buildClientPipe(clientID int, workerWorkHandle chan func(), workerKillHandle chan empty) func(operations.Operation) { return func(op operations.Operation) { @@ -130,11 +130,11 @@ func (s *documentServer) buildClientPipe(clientID int, workerWorkHandle chan fun } } ``` -so whenever we get an operation from the client we: +So whenever we get an operation from the client we: 1. communicate it to the document_server via a pipe 2. the operation is then transformed against the entire log of operations the server has applied 3. the operation is then applied to the server's representation of the document - 4. the operation is then communicate to all other clients + 4. the operation is then communicated to all other clients ### The document server wants to propagate operations If you remember previously how it was mentioned that the document server "propagates" operations to the clients? It does that by sending these operations down a channel maintained by each client diff --git a/backend/editor/OT/docs/operation_parsing.md b/backend/editor/OT/docs/operation_parsing.md index 974ec71c..a29758a9 100644 --- a/backend/editor/OT/docs/operation_parsing.md +++ b/backend/editor/OT/docs/operation_parsing.md @@ -3,12 +3,9 @@ Surprisingly most of the complexity in the OT editor lives in the operation parsing and application logic. This document aims to help demystify some of it. As a quick aside, everything regarding operations can be found in the `operation` folder. ## The operation struct -Operations (in Go) are defined as a struct, the operations we receive from the client conform to this structure and are parsed using a JSON parsing library (more on that later as there is a bit of complexity behind this). +Operations (in Go) are defined as a struct. The operations we receive from the client conform to this structure and are parsed using a JSON parsing library (more on that later as there is a bit of complexity behind this). ```go type ( - // EditType is the underlying type of an edit - EditType int - // OperationModel defines an simple interface an operation must implement OperationModel interface { TransformAgainst(op OperationModel, applicationType EditType) (OperationModel, OperationModel) @@ -25,14 +22,20 @@ type ( Operation OperationModel } ) +// EditType is an enum with `int` as the base type +type EditType int +const ( + Add EditType = iota + Remove +) ``` The above code snippet uniquely defines an operation. Operations take a path to where in the document AST they are being applied (see the paper on tree based transform functions) and the physical operation being applied. The operation being applied (`OperationModel`) is actually an interface and is the reason why parsing operations is more complex than it seems. This interface defines two functions, one for transforming a operation against another operation and one for applying an operation to an AST. -The reason why `OperationModel` is an interface is because there are several distinct types of operations we can apply for varying types. Theres different operations for editing an `integer`, `array`, `boolean`, `object` and `string` field. Each of these define their own transformation functions and application logic so in order to maintain a clean abstraction we use an interface. As an example this is how the `string` operation type implements this interface ![here](../operations/string_operation.go), its a rather intense implementation since it also implements string based transform functions. +The reason why `OperationModel` is an interface is because there are several distinct types of operations we can apply for varying types. Theres different operations for editing an `integer`, `array`, `boolean`, `object` and `string` field. Each of these define their own transformation functions and application logic, so in order to maintain a clean abstraction we use an interface. As an example this is how the `string` operation type implements this interface ![here](../operations/string_operation.go), its a rather intense implementation since it also implements string based transform functions. ## Operation application -Recall that the `document_server` maintains an abstract syntax tree for the current JSON document, this AST is exactly what `cmsjson.AstNode` is, the document server maintains the **root node**. When applying an operation to a document we invoke the `Operation.ApplyTo` function +Recall that the `document_server` maintains an abstract syntax tree for the current JSON document, this AST is implemented by `cmsjson.AstNode`, and the document server maintains the **root node**. When applying an operation to a document we invoke the `Operation.ApplyTo` function ```go func (op Operation) ApplyTo(document cmsjson.AstNode) (cmsjson.AstNode, error) { parent, _, err := Traverse(document, op.Path)