View
527
Download
0
Category
Tags:
Preview:
Citation preview
Technical University of Denmark
02220 Distributed Systems
Canva - magic mix drawings
s131181 | Vlad Manea
s131185 | Laura-Ariadna Stroe
s131044 | Nanna Gudrun Hjaltalin
May 19, 2014
1
Table of contents
pag. 6 ...... 1. Introduction
pag. 6 ...... 2. Design
pag. 7 .......... 2.1. Paradigms
pag. 9 .............. 2.1.1. Event driven drawing
pag. 9 .............. 2.1.2. Client - Server
pag. 10 .............. 2.1.3. HTTP available server
pag. 11 .............. 2.1.4. TCP event based socket reliable communication
pag. 12 .............. 2.1.5. Branch driven repository
pag. 13 .......... 2.2. Architecture
pag. 13 .............. 2.2.1. System architecture
pag. 15 .............. 2.2.2. Client side event based drawing system
pag. 16 .............. 2.2.3. Full stack branching system
pag. 16 .................. 2.2.3.1. Branch internal representation
pag. 17 .................. 2.2.3.2. Overview of branch procedures
pag. 18 .................. 2.2.3.3. Checking out a branch
pag. 19 .................. 2.2.3.4. Saving a branch
pag. 20 .................. 2.2.3.5. Merging branches
pag. 21 .................. 2.2.3.6. Blocking and unblocking
pag. 22 .............. 2.2.4. Full stack chat system
pag. 23 .............. 2.2.5. Test scenarios
pag. 23 .................. 2.2.5.1. Chat test scenario
pag. 24 .................. 2.2.5.2. Drawing test scenario
pag. 26 .................. 2.2.5.3. Branching test scenario
pag. 29 .................. 2.2.5.4. Blocking test scenario
pag. 31 ...... 3. Implementation
2
pag. 31 .......... 3.1. Technologies
pag. 31 .............. 3.1.1. Node.js web framework
pag. 32 .............. 3.1.2. Fabric.js canvas drawing framework
pag. 32 .............. 3.1.3. Socket.IO web communication framework
pag. 32 .............. 3.1.4. Jasmine.js test framework
pag. 32 .......... 3.2. Workflow
pag. 32 .............. 3.2.1. Timeline
pag. 33 .............. 3.2.2. Tasks
pag. 34 .............. 3.2.3. Repository
pag. 34 .............. 3.2.4. Build
pag. 34 .............. 3.2.5. Deployment
pag. 35 .............. 3.2.6. Test
pag. 35 .............. 3.2.7. Team
pag. 35 ...... 4. Conclusions
pag. 36 ...... List of references
pag. 39 ...... Appendix A
pag. 39 .......... A.1. Piping command line API modules
pag. 39 .............. A.1.1. Strength
pag. 40 .............. A.1.2. Weakness
pag. 40 .............. A.1.3. Opportunity
pag. 40 .............. A.1.4. Threat
pag. 40 .......... A.2. Google Docs LaTeX addon
pag. 40 .............. A.2.1. Strength
pag. 40 .............. A.2.2. Weakness
pag. 40 .............. A.2.3. Opportunity
pag. 40 .............. A.2.4. Threat
pag. 40 .......... A.3. Biker real time tracking
3
pag. 41 .............. A.3.1. Strength
pag. 41 .............. A.3.2. Weakness
pag. 41 .............. A.3.3. Opportunity
pag. 41 .............. A.3.4. Threat
pag. 41 ...... Appendix B
pag. 41 .......... B1. Server side code
package.json
app.js
chat.js
config.js
http.js
io.js
repository.js
routes.js
server.js
pag. 54 .......... B.2. Client side code
circle.js
ellipse.js
line.js
polyline.js
rectangle.js
text.js
triangle.js
chat.js
index.js
modifier.js
painter.js
4
repository.js
socket.js
ui.js
style.css
index.jade
pag. 82 .......... B.3. Test code
server-repository-spec.js
server-chat-spec.js
pag. 91 ...... Appendix C
pag. 91 .......... C.1. A passing build
pag. 94 .......... C.2. A failing build
pag. 95 .......... C.3. The latest build and deploy runs
5
1. Introduction The stated purpose of this project is to implement a self-chosen distributed system.
Our aim was to find a real problem, so that its solution was complex enough to fit
the project requirements for the course, while simple enough to permit
implementation in the given time frame. We aimed for a project whose lifespan may
have been prolonged beyond the course, and looked for a challenging and rewarding
problem. We wished to learn many things while solving it, and enjoy our work.
We made use of existing project requirements, proposed in [1] as a baseline. After
brainstorming, we ended up with four ideas which met the objectives above: a
collaborative drawing application, a modular framework for piping commands, a
Google Docs LaTeX addon, and a real time biker tracker. We received approval for all
of these, and then decided by vote on the drawing application. The other projects
can be found in Appendix A.
Collaborative drawing is a problem already solved by Google with Drawings [2] and
Microsoft with Onenote [3]. Google also allows users to switch between different
versions of these diagrams in time. This model works great for playful draw and
simple diagrams made by small teams. But in a business environment, many times
parts of larger teams propose different drawings of diagrams, or work on parts of the
final drawing separately. The current solutions make “choosing the best and losing
the rest” a hard burden on the teams.
The strength of this project, aside from meeting our objectives above, consists of us
having worked with web technologies before. We already have the basics, and can
use these to learn more. Our work has the opportunity to jumpstart a useful and
open-source modular product for enterprise and students, while still being suited
for playful drawing. Two of us actually faced the problem at a different course.
Our weakness is that not all team members have prior knowledge of versioning and
branching, and none has experience in developing a system with these features. We
were threaten by the project taking more than planned, which might have required
us to prioritize the features and discard some.
With these in mind, we decided to tackle the problem. We wish to empower users
to not only work together, but also review and mix their work. To achieve this goal,
we have developed canva [4]: a universally available collaborative drawing
application, where groups of users can create multiple parts of the same drawing
independently, and then discuss and combine them into magic.
2. Design
Our system has been designed to implement the following features:
6
1. A canvas on the client, which allows users to create custom shapes; each
shape is defined by a module of its own
2. A branch based repository system, which allows users to share their drawings
with others, start from scratch or improve existing versions, and combine
their results by choosing only the relevant shapes
3. A system that allows users to work in real time on the same canvas, such
that only one saves changes made by anybody else in that group
4. A messaging system, which allows user conversation during canvas drawing
collaboration
Each feature is described in its corresponding subsection.
2.1. Paradigms
Our distributed system implements features based on a clear set of paradigms.
These can be seen at work in Fig. 1.
7
Fig. 1. The paradigms in our distributed system at work. There are two
clients who connect to the server to work on the same drawing. Both
connect to fetch the page, and then to establish the socket handshake.
Both clients concurrently check out a branch. Client 2 performs a
change by adding a circle, commits the change locally, and then saves
the branch onto the server. When client 1 checks out the branch again,
it will obtain the latest version, and will be in sync with client 2.
8
2.1.1. Event driven drawing
The HTML5 canvas tag [5] does not provide implementation for each of the
geometric shapes. This can be implemented, yet the canvas does not provide an out
of box event system for shape management, which would allow moving, rotating and
other operations.
We drew our attention upon frameworks that could help us. We were in search of a
drawing system which handled shape events on the canvas, and also had primitive
shapes implemented. Our task would now be to implement the drawing of these
primitive shapes by using the events we were provided.
For instance, when drawing a circle, one first clicks on the canvas surface, and then
moves the mouse onto a different position. In all this time, the user can see a
preview circle on the canvas, such that the first click location is its center, and the
current location defines the radius. When the user releases, the operation ends, and
the circle is added to the canvas. This flow is depicted in Fig. 2.
Fig. 2. Two events on the canvas define the center and the radius of a
circle. These events can be seen as transitions in a deterministic finite
state machine. We have created a DFSM for each shape type.
2.1.2. Client - Server
We need to implement a system that manages the clients and their drawings, such
that if a new change appears in one drawing, it will be made apparent to all involved
clients, but not more.
For example, if three teams of clients work on three separate drawings, each group
would have a set of clients working on the same drawing. We want the server to be
able to manage this group and other groups simultaneously and independently. This
situation is depicted in Fig. 3.
9
Fig. 3. The server handles groups separately. This means that a client
will be able to obtain the changes made by other clients in the same
group, but not other clients and groups.
The current browser based collaborative tools such as [2] implement the client -
server paradigm. We did not see the advantages of implementing a more
complicated, decentralized paradigm, such as peer to peer [6].
2.1.3. HTTP available server
Our clients are using the web for collaboration, so it naturally makes sense to use
the web protocol and technology there is. This way, users will not have to install any
runtime environment, such as JRE [7], before using our application.
The HTTP protocol [8] is widely known and used by devices worldwide. From our
project perspective, its main advantages are that it does require nothing more than
a browser on the client, and that it can be used on both desktop and mobile devices
out of the box, as in Fig. 4.
In our project we use an initial HTTP request for fetching the initial page. This makes
it a RIA [9], which allows easy user navigation: the person only has to use the client
to browse there and start drawing.
10
Fig. 4. Sequence diagram of a communication. HTTP is a understood by
many types of clients. This makes our application universally available.
2.1.4. TCP event based socket reliable communication
The information related to the drawing has to be sent and received accurately. In
order to achieve this, we need to ensure that information about the canvas is
reliably sent and received. It is crucial that each client is notified on any change, and
receives the correct change, if an active collaboration takes place. The reliability and
message order keeping properties of TCP allow us to keep the branches on the
client and server synchronized. On the other side, UDP is more suited for our
simple message transmission, and we considered implementing it there.
Message transmission reliability is only relevant in the context of an underlying
reliable system. In this case, one which is either single threaded or allows locks, so
that resources in the critical section are consistent at any time. The data structures
for storing branches and users have to be either accessed by a single thread, or
locked on write while reading. The implementation of this paradigm must therefore
have one of these two properties.
An example on how the sockets work is explained in Fig. 5. We assume that
someone adds a line on the canvas during collaboration, and we want every client to
be notified with the updated canvas, so they can continue drawing on top of it. A
missing or misordered package would prevent the client from receiving the updated
canvas, and would move it out of sync.
In order to have client notification, we went to sockets, which work in TCP only. We
are aware that chat messaging might also be done with UDP, for e.g., by using a
technology such as [10]. Since the messages are small information, we preferred to
use TCP and have a consistent codebase by using a TCP socket implementation.
11
Websockets is a set of protocols [11] which provides a full duplex reliable
communication channels over a TCP connection [12], and is supported by multiple
browsers such as Google Chrome, Internet Explorer, Firefox, Safari and Opera. It did
not make sense to use pure HTTP for update notifications during drawing
collaboration, because it had to be done in real time, and HTTP would still require
the client to send requests to the server, by using AJAX [13] or similar. With
websockets, a client can listen for changes coming from the server without polling,
and it can pass messages in both directions at the same time (full duplex). [14]
mentions a list of disadvantages of pure HTTP as opposed to websockets.
Three out of the five signs described in [15] apply to our project, which indicates a
true need for websockets. First, data must flow both ways and simultaneously.
Second, the reference claims that websockets also provide a 3xlatency decrease
and a 1000x space decrease. Third, we need to avoid the server polling.
Fig. 5. Sequence diagram of reliably emitting a change in the drawing,
in this case the red line. One client emits the change to the server
through the open socket, and then the server emits the change to all
sockets. Now all clients have received the new line, without extra
requests to the server.
2.1.5. Branch driven repository
In order to support different versions, or parts, of the same drawing, one must use a
system which is able to combine changes. The source control repositories [16], [17]
[18] have solved this problem already, by allowing a single version path where
clients sync, commit, update, and resolve.
The Git system also resolved the problem of working on different branches, which
would allow independent work and simpler merge different branches at the end. A
flow example for Git is [19].
For example, we consider a scenario in which there are two teams with different
versions of a solution to a common problem. The teams start from an older diagram,
12
and propose different improvements to it. Their proposals may have overlapping
parts. After they finish working on their individual branches, they might want both
solutions combined, meaning they want to keep the relevant parts from each of the
solutions, and discard the unnecessary data from both of them. Therefore, it is
necessary to have a branching system that gives the clients the possibility to also
merge, in our case, their drawings. Such a process is explained in Fig. 6.
Fig. 6. A branch driven repository. The circles represent new versions.
There is a master branch, from which all branches are created - here
depicted with the south-east arrows. From time to time, branch
integrations are performed - here depicted with the north-east arrows.
Some branch integrations might yield to conflicts which require
resolution, e.g., by merging.
2.2. Architecture
The client and server are implemented in a symmetrical way, such that module
cohesion increases. The client - server communication takes part by using socket
emit and socket-on events in the two sister socket modules, one on the server and
one on the client.
2.2.1. System architecture
The sister repository / chat modules on the server and on the client manage
branches / users at the server and client level, respectively. The client contains
additional modules for painting shapes, modifying shapes and interaction with the
UI.
The client also contains plug-in modules for various geometric shapes, which are
registered to the painter in the index file. We have applied a lightweight version of
the strategy pattern: when the user selects a shape name to be drawn, such as
square, then the square module is used to update the canvas.
The server additionally contains modules for HTTP communication and MVC route
management - there is a single route, which corresponds to the single rich page.
In Fig. 7 and Fig. 8 the client and server class diagrams can be seen.
13
Fig. 7. The client class diagram.
Fig. 8. The server class diagram.
14
In the prototype stage, all code was implemented in a single class . However, after 1
the prototype was functional, we separated the concerns in different modules. The
repository and chat implementations on both client and server were separated from
the socket managing class. The UI update functionality was also removed from the
socket managing class. This allowed the socket class to only know about the socket
instance, and the ui know about the jQuery objects. All responsibility regarding
branches was passed to the repository class, and all responsibility regarding
participants and messages was passed to the chat class.
The modular structure allowed us to implement and use the drawing and branching
features independently: we worked with figures in the canvas, and with plain
numbers as objects in the branches. This has proven beneficial in two situations.
The first was the integration of canvas objects into the branching system. Having all
implementation done in these two separate modules, we could very easily
integrate them. For instance, in order to commit the current state of the canvas, all
we had to do was to to pass the output of the canvas, i.e., the result of the function
canvas.toDatalessJSON()as the objectToCommitparameter of commitBranch,
and follow the same pattern in other branching functions. The entire integration took
around 15 minutes.
The second was the ability to test the server repository and chat functionality
independently, without requiring socket objects or mocks in the test harness.
2.2.2. Client side event based drawing system
We have implemented modules for the following geometrical shapes: circle, ellipse,
rectangle, triangle, line, polyline, and text box. The user can add one of these
shapes to the canvas by clicking, dragging and releasing. The farther the drag, the
larger the final object. A preview of the object is available when dragging.
Each module implements a deterministic finite state machine. For instance, the
circle module has three states. The first state corresponds to idle, and it is the start
state. The second state corresponds to a circle whose center is known, but whose
radius is unknown. The third state corresponds to a circle whose both center and
radius are known. When the user selects the circle button and then clicks on the
canvas, the DFSM goes from state 0 to state 1. At this state, the click location
corresponds to the center. When the user releases the click, the DFSM goes from
state 1 to state 2. The center and this location form a segment of length equal to the
radius, and the circle is drawn onto the canvas. Then the DFSM is triggered to
change state from 2 to 0, which is idle again. The DFSM manager automatically
moves it from any state to state 0 when a different DFSM is initiated. The creation of
a circle can also be seen in Fig. 2.
1Our programming language of choice does not use classes, yet it is possible to simulate a light set of properties and patterns that apply to classes, such as encapsulation and dependency injection.
15
This modular structure proves beneficial, because all shapes are treated the same
by the painter, and more modules can be easily added if registered in the index file.
This will be helpful if the project lifespan is increased as open-source, and more
developers adhere by writing shapes modules.
The user can globally choose the border and fill color of all objects to be next
created, and the border thickness. We have implemented a lightweight version of
the flyweight design pattern for this: the color and thickness attributes are simply
inserted into all objects. By using the text field, the user can add text, for e.g., if
willing to describe something on the picture. We can see the drawing shapes
existing in our app in Fig. 9.
Fig. 9. The states of a canvas during drawing shape types, in order:
a circle, a triangle, a line, an ellipse, a polyline, a text box.
2.2.3. Full stack branching system
We want the our clients to be able to draw not only at the same time on the same
branch, but also separately, with the possibility of combining the drawings when
they want. Our branch system consists of a master branch, and a set of other
branches, created from the master branch. When one branch is created from
another, a version is created on the new branch, such that the object in that version
is a clone of the object in the head of the branch from which the creation is done.
After separating from a branch, our clients need to merge their work with any other
branch.
2.2.3.1. Branch internal representation
The branching system can be seen as a directed acyclic graph. The node with zero
inner degree corresponds to a primordial empty version of the master branch. This
node is created as part of the main branch when the branching system initializes. A
path from the primordial master node to any node corresponds to the master
branch, followed by the branch this version node belongs to. A version is a fixed
state of the branch at a certain point in time. In our concrete case, states are in the
universe of geometrical shapes placed on a canvas. This layout can be seen in Fig.
10.
16
Fig. 10. Branching system internal layout, situated in both client and
server. A node in one branch may or may not point to a node in
another branch, but it always points to a version node. The exception
to this rule is the master primordial version node.
2.2.3.2. Overview of branch procedures
The checkoutBranch(branchName) procedure allows the retrieval of the latest
version of the branch with that name from the server. The master branch, along with
its primordial empty version, got created when the server initializes, and can be
checked out immediately: the user can either click the master button and then the
checkout button in order to checkout from the master branch. After the client is
checked out on the master branch, he can either start drawing in that branch, or
create another branch from it. The master branch can not be deleted, so that there
will always be a master branch.
To start working on a branch, the client has to checkout that branch first, which can
be done by pressing the button with the name of the branch and then pressing the
checkout button. If the user does not want to draw on the master canvas, he has to
create a new branch.
At any moment in time, createBranch(branchName)creates a new branch with
that name on the server, such that its primordial version points to the head of the
currently checked out branch. In order to create a branch the user has to click the
create button and then enter a name for the branch. At that point, a button with the
name of the new branch will appear next to the master branch button, so that from
this point on that branch can be checked out whenever necessary. This new branch
will have all the information from the master branch up to the point when it was
created. When a branch is created it is automatically added to the server. The user
cannot create a branch if the underlying branch is not updated, i.e., it has changes
not saved on the server. This situation is depicted in Fig. 11.
17
Fig. 11. Creating a branch. The dotted lines depict paths not saved on
the server. In the left drawing, some changes were made to the
underlying branch and were not saved; therefore, the create is
aborted. In the right drawing, the branch is up to date with the server;
therefore the create is completed. Every primordial node of the new
branch points to the underlying node.
To save the changes locally withcommitBranch(objectToCommit), the client must
use the commit button. This procedure saves the changes in the current branch,
without sending them to the server. The client cannot commit on a branch if it is not
the checked out branch.
The saveBranch(branch)procedure adds a new branch if the branch did not exist,
or attempts to save it on the server. If both existing server and incoming client
branches have different head versions, the save is aborted and a conflict notification
is sent back to the client.
The mergeBranch(branchName) procedure merges locally the branch with the
specified name into the current branch.
The deleteBranch(branchName) inactivates the branch with the provided name.
To delete a branch one must be checked out on that branch, and select the delete
button. From that moment on, the branch becomes globally inactive. We decided to
make it inactive rather than completely deleting it, because there might be other
branches who point to one node in the branch to be deleted. Once a branch is
inactive, it acts as if it does not exist anymore, meaning that one can no longer
operate on that branch.
2.2.3.3. Checking out a branch
In this section changes are considered committed changes, so they appear as head
versions in the branches. On branch checking out on the client, there will be one of
the following situations.
If one has checked out on a different old branch before, and it made changes in that
old branch, then the changes must be committed in the old branch before checking
out a new branch. Otherwise, the changes are discarded. This situation can be seen
in picture 1 of Fig. 12.
18
If the user wishes to check out a different branch, but there is no change in the old
branch, there is no problem. The user can simply check out the new branch. The
process can be seen in picture 2 of Fig. 12.
If the user wishes to check out the same branch, i.e., fetch the latest updates from
the server, there are four possible situations, as depicted in the pictures 3.1, 3.2,
3.3, 3.4 of Fig. 12.
1. None have changes. In this case, nothing happens.
2. The server branch has no changes, and the client has. In this case nothing
happens; the client can continue working on his branch.
3. The client branch has no changes, but the server has. In this case, the client
branch will be updated to the final version of the server branch.
4. Both have changes, and a conflict appears. To solve this, the user is asked to
merge the branches. The merge process is explained in the following pages.
Fig. 12. Checking out. A dashed line corresponds to a committed, but
not saved change. The red and magenta versions correspond to two
client branches. The green versions correspond to the server branch.
2.2.3.4. Saving a branch
Unlike the commit procedure, the save procedure is made on both client and
server. The user can save a branch by pressing the save button while being checked
out to that branch. There are two situations.
19
If the branch was not saved before, then it is now created and added to the server.
If the branch was saved before, then it follows that it already exists on the server. In
this case, there are four possible scenarios.
If neither the client, nor the server made any changes, then they have the same
versions, and their head versions are the same. In this case, nothing happens, since
there is no need for any update or merge.
If the client made changes to the branch, while the server did not, then the client is
ahead of the server, and his changes are to be added to the server. After this
procedure, both client and server have the same head version.
If the client made no changes, but the server did, then the server is ahead of the
client and nothing happens. To get the updated version of the branch, the client will
have to check out the branch.
If both server and client made changes, there is a conflict. There are three important
values, which we denote conflicting values. The first is the last common version of
the client and the server, i.e., the latest version where they have the same
information. The second is the last version in the client branch. The third is the last
version in the server branch. The save aborts, and the server requires the client to
check out to get the last version. At check out, the client will enter the 3.4 case
depicted in Fig. 12. Here the client is required to merge the conflicting branches.
It is easy to observe these four cases are similar to the four same branch check out
cases. The similarity is further explained in the following section.
2.2.3.5. Merging branches
The merge feature is used in two situations. First, at check out, upon a failed save -
if the user decides to check out. Second, when the user merges the heads of two
two different branches locally.
In this scenario, a user is faced with the situation in which both client and server
made changes, and was requested to merge. At this point, the user will see four
canvases instead of one. Merging in this case means moving objects from one
canvas to another, or to the recycle bin. These actions can be done by clicking on
each object in particular. If an object from the left or the right canvas is clicked, then
that object will move to the middle canvas, which represents how the merged
version in the branch will look after the process. If one clicks on an object in the
middle canvas, and that object is part of the common part of the two versions, then
that object will be sent to the recycle bin. If the object in the middle, which was
received by one of the left or right versions, is being clicked, then it is sent back to
the canvas it belonged to. Things sent to the recycle bin can be restored by being
clicked. This interaction ensures complete undo capability.
A scenario in which merge is required can be seen in Fig. 13. The canvas to the left
represents the changes committed by the client in this branch, and the canvas to
20
the right represents the changes saved on the server in this branch, by other clients.
The middle canvas initially depicts the last version in which both branches were
equal. Concretely, they both had the square, but the client the committed a circle,
while on the server there was a triangle added. In the image, the rectangle which is
in both versions, noted left and right, is faded out in gray and not selectable. This is
because clicking moves them to the middle canvas, but it already exists there. This
will only duplicate the object, putting them on top of each other. After all the
desired shapes are placed in the middle canvas, the user can click the ok button to
merge. The user can also cancel at any point, which also aborts the underlying check
out, if any.
Fig. 13. Merging branches. There are four canvases: your changes on
the left, their changes on the right, the baseline (and also final version
once the ok button is clicked) in the center, and the recycle bin below
2.2.3.6. Blocking and unblocking
We wish to allow the branching system to help users draw in two ways: first,
separately, so that any of their saved changes does not affect other users checked
out on that branch; and second, such that they can collaborate on drawing in real
time. In order to achieve these two separate objectives, we implemented a system
that blocks the save operation for all except one client, and allows for client
notification on any change other client is doing in that branch. This is where we
harness the power of sockets the most! At any time when there is no such block, a
client must check out the branch in order to receive the updated version of that
branch.
To let clients draw on the same canvas in real time, we created a blocked(data)
function. When a client blocks, it is given a unique token which validates its control
over saving that branch. A client can only block a branch if the branch is active,
which means it was not deleted, and if it is not already blocked by another client.
When a client blocked a branch, any other client can check out that branch and
make changes. The change is sent to the server, which notifies all participants that a
change has been made. Only the client having the token automatically commits and
21
saves the change in the blocked branch. If another client who checked out on that
branch tries to save, they will receive an error message, because in order to save,
the client must have the token that was given at blocking. When the client in control
of that branch wants to release control, it will turn the token back in, and unblock
the branch, making it available for blocking by others, as depicted in Fig. 14.
If the block lasted for more than a specified period of time, a new block attempt will
succeed. This is useful for the case when the client who blocked loses connection
and cannot unblock anymore.
Fig. 14. A scenario for the block and unblock. There are two clients.
Assuming the second and third blocks are issued before the timeout,
the second client can only block the branch successfully after the first
client unblocked that branch.
2.2.4. Full stack chat system
In order to allow users to communicate without leaving the drawing application, we
have developed a simple chat system on top of websockets. When a user connects
to our application and also when sends a message to the server, all other clients are
notified a new participant or message appeared, and their clients update the user
interfaces with the new data. We have used the tutorial [20] for learning how to work
with websockets and get our chat up and running.
In our chat each participant can register with his or her name, and start chatting
right away. If the user does not register with a name, he will appear as anonymous.
The participant is automatically registered as anonymous when he enters the
website. Even if two participants have the same name, there will be no conflict when
22
sending the information, because one object is identified not only by the name of
its author, but also by an the client socket id.
2.2.5. Test scenarios
We have split the testing of our application into automatic and manual.
We have tested automatically all responses the repository and the chat provide for
various operations. For each operation, we tested both valid and invalid input. This
provides us trust that the server performs correctly regardless of client message
structure . A sample test case can be seen in Fig. 15. 2
it("should trigger ok if token is valid", function() { var branchName = "existingBranch"; var participantId = 1; repo.saveBranch(
{branch: {name: branchName, nodes: [], active: true}}); var blockResponse = repo.block(
{participant: {id: participantId}, branch: {name: branchName}});
var result = repo.unblock({participant: {id: participantId},
branch: {name: branchName}, token: blockResponse.token});
expect(result.status).toEqual('OK'); expect(result.branch.name).toEqual(branchName); expect(repo.isBlocked(branchName)).not.toBeTruthy();
});
Fig. 15. Testing the invalid token.
On the client we could not make use of the server testing framework, due to
problems integrating the vanilla JavaScript files into the Node tests. We tried to no
avail to make use of the advice at [21].
We wanted to make sure our features, the chat, the drawing, the collaborative
drawing and the branching system would work before we move on to the next step.
Therefore we tested manually our application at every iteration.
In the last iteration of our project, we created and ran the following manual test
scenarios, covering the full stack.
2.2.5.1. Chat test scenario
Performed action Expected result
connect with a client there is no canvas in the user interface
2 We are aware of the fact that we did not perform security testing beyond the protocols we created for branching and blocking branches. This means that a missing token will be caught, while a spoofing attack on the token passing will not.
23
connect with the first client its name appears as anonymous among the participants in the user interface; its name is followed by the “you” mark
write a message and click share the message appears in the user interface
connect with the second client there is no message visible; its name appears among the participants, but the other appears too; its name is followed by the “you” mark
write a message and click share the message appears on both clients.
change name of the first client, and focus out of the field without sending any message
both clients get the updated name
This scenario run did not uncover issues.
2.2.5.2. Drawing test scenario
Performed action Expected result
connect with a client there is no canvas in the user interface
click the master (branch) button
click the check out button a popup appears to notify that the branch has been fetched
click the ok button to approve the canvas appears in the user interface
click the circle button
click the canvas, keep the button clicked, and move the mouse
a preview circle is dynamically generated while moving the mouse
release the click button of the mouse, while being still in the canvas
a circle is created, instead of the last preview circle
click the ellipse button
click the canvas, keep the button clicked, and move the mouse
a preview ellipse is dynamically generated while moving the mouse
release the click button of the mouse, while being still in the canvas
an ellipse is created, instead of the last preview ellipse
click stroke color, select a color, click outside of canvas or picker to select the color
24
click the rectangle button
click the canvas, keep the button clicked, and move the mouse
a preview rectangle is dynamically generated while moving the mouse
release the click button of the mouse, while being still in the canvas
a rectangle is created with the stroke color, instead of the last preview rectangle
move the stroke width slider
click the triangle button
click the canvas, keep the button clicked, and move the mouse
a preview triangle is dynamically generated while moving the mouse
release the click button of the mouse, while being still in the canvas
a triangle is created with the stroke color and width, instead of the last preview triangle
click the text button
click the canvas a popup asking for the text appears
introduce the text and click ok the text appeared, such that its top left corner is the point where clicked
This scenario run did not uncover issues.
2.2.5.3. Branching test scenario
Performed action Expected result
connect with the first client there is no canvas in the user interface
click the master (branch) button
click the check out button a popup appears to notify that the branch has been fetched
click the ok button to approve a canvas appears in the upper part of the user interface; assume the canvas has no content
draw a shape on the canvas the shape appears on the canvas
click the commit button a popup appears to notify that the change has been committed (locally, only on the client)
click the save button a popup appears to notify that the branch master has been saved
connect with a second client there is no canvas in the user interface
25
click the master (branch) button
click the check out button a popup appears to notify that the branch has been fetched
click the ok button to approve a canvas appears in the upper part of the user interface; the canvas has the content drawn by the first client
draw a shape on the canvas the shape appears on the canvas
click the commit button a popup appears to notify that the change has been committed (locally, only on the client)
click the ok button to approve
click the save button a popup appears to notify that the branch master has been saved
click the ok button to approve
switch to the first client nothing changed in the canvas for this client; there is only the shape drawn previously by this client
draw a shape on the canvas the shape appears on the canvas
click the commit button a popup appears to notify that the change has been committed (locally, only on the client)
click the ok button to approve
click the save button a popup appears to notify that the save was not possible because there is another save on this branch; the user is asked to check out the branch to get the latest changes
click the ok button to approve
click the master (branch) button
click the check out button the merge screen appears; the shape drawn initially is grey; the shape by this client is in the left; the shape by the other client is in the right; the shape drawn initially, which was saved on the server, appears in the middle canvas as well; the recycle bin canvas is empty
click the shape from your changes (the left canvas)
the shape moves from left to middle canvas
click the shape from their changes (the right canvas)
the shape moves from right to middle canvas
26
click the shape that was initially in the middle canvas
the shape moves from middle canvas to the recycle bin canvas
click the shape that was initially in the left canvas
the shape moves back from middle to left canvas
click the ok button to approve a popup appears to notify that the branch has been resolved; the canvas did not change
click the check out button a popup appears to notify that there was no change from the server; the canvas is updated so that it looks exactly like the final state of the middle canvas (having only the shape that was in the right)
click the ok button to approve
click save a popup appears to notify that the branch master was saved; this means that the conflict was resolved from the server perspective too
click the ok button to approve
switch to the second client its canvas contains the two shapes as before
click the check out button a popup appears to notify that the branch has been updated
click the ok button to approve the branch with only one objects, as saved by the first client after merging, is shown
click the create branch button a popup appears to request a name, say A
write a different name and click the ok button to approve
the branch button A appeared close to the master (branch) button
click the create branch button a popup appears to request a name, say B
write a different name and click the ok button to approve
the branch button B appeared close to the master (branch) button
click the A (branch) button
click the check out button a popup appears to notify that the branch has been checked out
click the ok button to approve no change in the canvas as compared with master
draw a shape in the canvas the shape appears on the canvas
click the commit button a popup appears to notify that the change has been committed (locally, only on the client)
27
click the B (branch) button
click the check out button a popup appears to notify that the branch has been checked out
click the ok button to approve no change in the canvas as compared with master
draw a shape in the canvas the shape appears on the canvas
click the commit button a popup appears to notify that the change has been committed (locally, only on the client)
click the ok button to approve
click the A (branch) button
click the merge button the user interface enters the merge screen; the three shapes look according to their origin: one active and one gray (inactive) on the left, one common in the middle, one active and one gray (inactive) on the right
click on the left active shape shape is moved from left to middle canvas
click on the right active shape shape is moved from right to middle canvas
click the ok button to approve a popup appears to notify that the branch merge has been resolved
click the ok button to approve
click the save button a popup appears to notify that the branch B was saved
click the ok button to approve
click the B (branch) button
click the check out button the resolved branch appears
switch to the first client the canvas is unchanged, and the B (branch) button appears
click the B (branch) button
click the check out button a popup appears to notify that the branch has been fetched
click the ok button to approve the canvas is populated as the B canvas was saved
click the A (branch) button
28
click the delete button a popup appears to notify the delete
click the ok button to approve the B (branch) button disappeared
switch to the second client the B (branch) button disappeared
This scenario run uncovered two issues in notifying the client on delete. They have
been found in the server logs and fixed immediately.
2.2.5.4. Blocking test scenario
Performed action Expected result
connect with a first client there is no canvas in the user interface
click the master (branch) button
click the check out button a popup appears to notify that the branch has been fetched
click the ok button to approve a canvas appears in the upper part of the user interface; assume the canvas has no content
click on the block button a popup appears to notify that the branch master was blocked
click the ok button to approve
connect with a second client there is no canvas in the user interface
click the master (branch) button
click the check out button a popup appears to notify that the branch has been fetched
click the ok button to approve a canvas appears in the upper part of the user interface; assume the canvas has no content
draw a shape on the canvas the shape is drawn on the canvas
switch to the first client the shape can be seen as updated by the second client; this happens because now the branch is in blocked save state
switch to the second client
click the save button a popup appears to notify that the branch could not be saved; this is because the branch has been blocked by another client who also got a token to unblock
click the ok button to approve
29
click the unblock button a popup appears to notify that the branch cannot be unblocked; this is because the branch has been blocked by another client who also got a token to unblock
click the ok button to approve
click the block button a popup appears to notify that the branch cannot be blocked; this is because the branch has been blocked by another client who also got a token to unblock
click the ok button to approve
switch to the first client
click the unblock button a popup appears to notify that the branch master was unblocked
click the ok button to approve
draw a shape on the canvas the shape is drawn on the canvas
switch to the second client the canvas does not include the last drawn shape; this is because the branch is not in blocked state anymore
draw a shape on the canvas the shape is drawn on the canvas
click the commit button a popup appears to notify that the change has been committed (locally, only on the client)
click the ok button to approve
click the save button a popup appears to notify that the branch master was saved
click the ok button to approve
switch to the first client
click the master (branch) button
click the check out button a popup appears to notify that the branch has been fetched
click the ok button to approve a canvas appears as it was saved by the other client
This scenario run did not uncover issues.
30
We have performed brief checks on the application to see that the features are
functional and work as expected on Chrome 34, Internet Explorer 11 and Firefox 29.
3. Implementation
We wished to find technologies that fit the solution to the distributed branch based
collaborative canvas problem, allowed us to use our prior programming knowledge,
learn new concepts along the way, and make the application programming language
consistent. We therefore avoided a multitude of unrelated technologies, and went
with only one language for the entire stack. This allowed us learn how to design,
develop and test one feature, and then apply what we learnt on other features. The
source code can be seen in Appendix B.
3.1. Technologies
In the context of universally available application, and by using the prior knowledge
of one team member, we went with the safe and reliable Node.js stack. Throughout
the project, we were able to make proper use of many node modules and gather
them into a coherent and modular product.
3.1.1. Node.js web framework
Node.js [22] is a software platform on top of JavaScript [23]. This platform was the
choice for our project, due to its benefits as compared with different other platforms
[24] [25], in the context of our project. We chose Node.js for a number of reasons.
Node.js implementation on the server is done in JavaScript, which is the de facto
language for client side web applications. This way we were able to use a singular
programming language, and apply our skills for a module while developing others.
One of our group members already implemented several applications and has
knowledge about Node.js, and this would make it easy for the rest of the group
members to ramp up, towards a working level of understanding.
Node.js has active communities [22], resourceful tutorials and forums for discussion
. In the same way as the other platforms considered, it is easy to install multiple
modules [26] and use them right away. For our project in particular, we used jade
[28] for writing easier client HTML code, jQuery [28] for HTML element management,
underscore [29] for faster search and retrieval in lists, and socket [link] for client and
server communication. Node.js has support for testing, via the jasmine [jasmine]
module.
A Node.js applications can also be easily built and tested continuously with
Codeship [30], and deployed on Heroku [link]. All these facilities are provided free of
charge.
31
3.1.2. Fabric.js canvas drawing framework
Fabric.js [31] is a JavaScript library which allows for easy creation and management of
objects drawn onto a canvas. It includes a few primitives, such as lines, circles,
rectangles and text boxes, which we implemented as modules in our project. We
used the provided event system, in order to construct the DFSM for each element.
The canvas comes decorated with events for object selection, movement, and
rotation, which would not come out of the box if we used the bare canvas HTML5
tag.
3.1.3. Socket.IO web communication framework
For our project, we needed to maintain persistent connection between the server
and the clients, that is able to send updates in real time, and be able to support
many clients at once.
Socket.io [32] is a JavaScript library for realtime web applications. It can be used
both client side in the browser, and server side in the node server. Socket.IO uses
the websocket protocol.
According to [32], Socket.IO does more than websocket, even if websocket is
selected as the transport and the user is browsing the website with a modern
browser. Certain features like heartbeats, timeouts and disconnection support are
vital to real time applications, but are not provided by the websocket API out of the
box.
According to [33], the Socket.IO stack runs on a single thread, which is also the
Node.js thread. This allows requests to be handled one by one, and discards the risk
of critical section inconsistencies when reading and writing the data structures
containing the participants and the branches on the server.
3.1.4. Jasmine.js test framework
Jasmine.js is a Node.js test framework for JavaScript. We decided to use Jasmine.js
to test our program because tests are written in JavaScript, the language of use in
our project, can be easily integrated into the continuous integration system. We
made use of tutorials [34], [35] and [36]. One of the group members had prior
knowledge of the framework and ramped up the team in using it.
3.2. Workflow
We have implemented this project in iterations. One iteration over the project
would have either added a new module, or would have integrated multiple modules.
3.2.1. Timeline
Before iterating the project, we ramped up and setup the repository, checked that
32
everybody can contribute to the project and everybody was provided the resources
to learn JavaScript, Node, Fabric and the other technologies we decided to use. We
used an online space to share written and video resources, and also set meetings.
Our first iteration over the project consisted of developing a simple and working
chat system as a prototype, using the technologies at hand. At the end of the first
iteration, we setup the build and deploy systems.
Our second iteration consisted of developing an offline canvas, where a single user
could draw shapes on the canvas. During this iteration, we distributed our work for
developing modules.
In our third iteration, we integrated the offline canvas into a system similar to the
chat system, so that multiple clients could collaboratively draw shapes.
In the fourth iteration, we created a branch system independent from the canvas
and chat, by using our knowledge from the chat. The branch system used bare
numbers as objects, instead of real canvas states.
In the fifth iteration, we integrated the online canvas with the branch system, by
using the output of one as input to the other. We also wrote automated tests for the
server at this point.
In the sixth iteration, we split the blocked and unblocked states of branches, in
order to allow both real time collaboration on drawing, i.e, fetching all changes from
any client on that branch and update all clients, and classic repository workflow for
the collaboration on drawing, i.e., requiring the client to check out a branch in order
to get the latest changes. At the end of the iteration, we performed the manual
scenario tests presented above.
3.2.2. Tasks
We split our work into features and tasks. We used Trello [37] for organizing the
tasks in the project, and assigning tasks to team members. It helped us always have
an overview over the whole project, what was already done and what was still to be
done, and who was where at any point in time.
We created five columns: Features, To Do, Doing, Ready and Done. First, every
feature is in the Feature column. Features are broken down to one or more tasks put
in the To Do column. When one or more members starts solving a task, he or she
moves that specific task to the Doing column; when it is done, it will be moved to
the Ready column. At that point, the task is functional and working on his or her
feature branch, pending merge with the master branch. When the task is merged
onto the master, it is moved from Ready to Done.
For a better view of our effort areas, we created labels for tasks: development,
testing, integration, documentation, build and management. The Trello up to date
board can be found at [38].
33
3.2.3. Repository
We needed a software for collaborative programming. It was important for each of us
to not interfere with the work of others, so we searched for a repository to work in
our own branches. We therefore decided to use a Git-based repository.
For our project we needed a source code hosting site that would allow us to create
and work in a private repository for free. We also wanted to be able to review or our
code at anytime. We considered a Git-based repository on GitHub [39] at first, but
unfortunately on GitHub you cannot have a private repository for free. We drawn our
attention on BitBucket [40], which also hosts Git-based repositories. Our repository
can be found at [41].
Not only it permits private free repositories, but also has easy features in the UI.
One of these is the pull request, which is used as a tool for code review. We used
this feature when the members could not meet in person. However, on the general
case, the team was cohesive enough to not have the problem of introducing faulty
code. Partly, this was possible due to the continuous integration system.
3.2.4. Build
We decided to try out a system in which we check in the code, and it appears in
“production”. This way we could quickly see how it looks, feels and runs on the web,
instead of our local repositories. Furthermore, it would help us run the tests
automatically with every check in. For this part, one team member had prior
knowledge of such products, while never setup a build and deploy system.
We found a very good build system Codeship [30], which is a continuous integration
and continuous deployment platform. It integrates BitBucket repositories and
Heroku deployed sites, and has easy support for Node.js. On the testing part, it
allowed any Node.j framework, in particular it worked with our choice Jasmine. In
order to run our tests on the platform, we made use of the tutorial at [42].
The continuous integration system had one drawback: it only had a limited number
of builds in the free version. For this reason, we had to make sure what we pushed
onto the repository was functional. We checked if the program was functional and
committed locally into our repositories, and only merged to master or pushed to
the server when we were feature complete.
3.2.5. Deployment
We used Heroku for hosting our website at [4]. Heroku is a cloud application
platform supporting several languages, including JavaScript, and the Node.js
framework. We found the logging feature [43] particularly useful one time, when the
live website crashed during testing.
34
3.2.6. Test
The Jasmine framework is primarily a Node.js module, which made it a simple step
in the Codeship set of commands to build and deploy. Complete build and
deployment tasks, along with test results, can be found in Appendix C.
3.2.7. Team
At the beginning of the project, all three members contributed to idea selection,
repository setup and research, on both web framework and drawing libraries.
In the initial stages of the project, we worked together on getting a chat prototype
up and running. Here Vlad implemented the code, and all three members did test.
We also collaborated all together for the first drawing module, which was a circle.
Then we split work: Laura and Vlad worked on the rectangle, Laura worked with
Nanna on the text and line modules, Nanna took the ellipse and Laura took the
triangle. This feature was the second most complex feature in the project.
A side task to improve the canvas, which consisted of adding color and weight for
the stroke, and also integration, was done by Vlad and Laura. The integration of all
drawing modules onto the canvas and the canvas server integration was
implemented by Vlad and tested by all the team during development.
The most complex feature of the project, which consisted of the branching system,
was designed by Vlad with help from Laura, and implemented by both through pair
programming. The merge operations and the block token system were designed by
both and implemented by Vlad. All team members tested the system on Heroku
during the entire scope of the project.
The testing tasks, except continuous testing, were split as follows: Vlad and Laura
worked on the automated tests and designed the scenario tests. The build and
deployment system was setup by Vlad.
We decided to write the documentation iteratively, such that members write
chapters that are reviewed by other members. For the documentation editing,
Nanna had a key role in technology chapters, and all team members contributed to
the chapters on architecture and workflow. We wrote the introduction and the
conclusions together.
4. Conclusions
We have found a challenging and rewarding problem. We are happy to claim we
provide a viable solution for the need of collaborative drawing, which is useful for
many people.
Our application might prove useful for enterprise and students, while still being
suited for playful drawing. This is why we thought it as a great headstart for an open
35
source project. We have a working repository, development, testing and deployment
system around it, which provide the infrastructure for a larger project. The project is
currently universally available.
Separating the workflow into multiple modules and tasks helped us not only plan our
work efficiently, but also have permanent knowledge of our status. We consider we
had a good approach in slicing the product into iterations. This allowed us to get
some features almost for free.
While not all team members have prior knowledge of versioning and branching, and
none has experience in developing a system with these features, we completed a
functional application by learning new things everyday. We were able to slice the
features such that we had a working product early.
Our initial plan was to develop a peer to peer and cloud backed application, yet we
realized that peer to peer does not solve this problem, and we did not have the
time resources to save data in the cloud. The future work would be focused on this,
and also on highly improved user experience and security.
The team members had the opportunity to improve their skills in designing and
implementing a distributed system, and get acquainted with a web communication
framework. All team members see this collaboration as a positive experience. There
were no dull moments, the project was interactive, and we enjoyed working
together.
List of references
1. Sabin Corneliu Buraga. Human Computer Interaction project proposal.
Alexandru Ioan Cuza University of Iasi. 2014
http://profs.info.uaic.ro/~busaco/teach/courses/hci/projects/index.html
2. Google. Drawings. 2014
https://docs.google.com/drawings/
3. Microsoft. OneNote. 2014
http://www.onenote.com/
4. Canva. Our project deployed on Heroku. 2014
http://canva.herokuapp.com/
5. Mark Pilgrim. Dive into HTML5. Let’s call it a draw(ing) interface. 2011
http://diveintohtml5.info/canvas.html
6. James Cope. QuickStudy: Peer-to-Peer network. 2002
http://www.computerworld.com/s/article/69883/Peer_to_Peer_Network
7. Java Programming Environment and the Java Runtime Environment. 2010
http://docs.oracle.com/cd/E19455-01/806-3461/6jck06gqd/index.html
36
8. W3C. HTTP - Hypertext Transfer Protocol. 2003
http://www.w3.org/Protocols/
9. QuinStreet Webopedia. Rich Internet Application. 2014
http://www.webopedia.com/TERM/R/Rich_Internet_Application.html
10. Joyent, Inc.UDP / Datagram Sockets Node.js v0.10.28 Manual &
Documentation. 2014
http://nodejs.org/api/dgram.html
11. Kaazing Corporation. About HTML5 WebSockets. 2013
http://www.websocket.org/aboutwebsocket.html
12. Information Sciences Institute. University of Southern California.
Transmission Control Protocol. RFC 793. 1981
http://www.ietf.org/rfc/rfc793.txt
13. Mozilla Developer Network and individual contributors. Ajax. 2014
https://developer.mozilla.org/en/docs/AJAX
14. StackOverflow. Do HTML WebSockets maintain an open connection for each
client? Does this scale?. 2013
http://stackoverflow.com/questions/4852702/do-html-websockets-maintain-a
n-open-connection-for-each-client-does-this-scale
15. Peter Lubbers. Five Signs You Need HTML5 WebSockets. 2008
http://peterlubbers.sys-con.com/node/1551694/mobile
16. Software Freedom Conservancy. git. Retrieved 2014
http://git-scm.com/
17. Apache. Subversion. 2011
http://subversion.apache.org/
18. Mercurial. Retrieved 2014
http://mercurial.selenic.com/
19. Atlassian. Feature Branch Workflow. Retrieved 2014
https://www.atlassian.com/git/workflows#!workflow-feature-branch
20. William Mora. Node.js Tutorial - Building a Chatroom with Express.js +
Socket.IO. 2013
http://www.williammora.com/2013/03/nodejs-tutorial-building-chatroom-with
.html
21. StackOverflow. Load “Vanilla” Javascript Libraries into Node.js. 2011
http://stackoverflow.com/questions/5171213/load-vanilla-javascript-libraries-i
nto-node-js
37
22. Joyent, Inc. node.js. Retrieved 2014
http://nodejs.org/about/
23. Mozilla Developer Network and individual contributors. JavaScript. 2014
https://developer.mozilla.org/en/docs/Web/JavaScript
24. Microsoft. ASP.net MVC Overview. 2014
http://www.asp.net/mvc/tutorials/older-versions/overview/asp-net-mvc-overv
iew
25. David Heinemeier Hansson. Ruby on Rails. Retrieved 2014
http://rubyonrails.org
26. Joyent, Inc. Node packaged modules. Retrieved 201
https://www.npmjs.org
27. Jade language. Retrieved 2014
http://jade-lang.com/
28. The JQuery foundation. jQuery. 2014
http://jquery.com/
29. Jeremy Ashkenas. underscore. GitHub. 2014
http://underscorejs.org/
30. Codeship. Retrieved 2014
https://www.codeship.io/
31. Juriy Zaytsev, Stefan Kienzle. Fabric.js. Retrieved 2014
http://fabricjs.com/
32. Guillermo Rauch. Socket.IO. Automattic. 2014
http://socket.io/
33. Codevate Limited. Developing a scalable real-time desktop or mobile
application with Socket.IO, Redis and HAProxy. 2013
http://www.codevate.com/blog/9-developing-a-scalable-real-time-desktop-or-
mobile-application-with-socketio-redis-and-haproxy
34. Rob Gravelle. Testing JavaScript Using the Jasmine Framework. 2014
http://www.htmlgoodies.com/beyond/javascript/testing-javascript-using-the-j
asmine-framework.html
35. Clemens Helm. Testing Tuesday #19: Testing node.js applications with
Jasmine. Video resource. Codeship. 2014
http://blog.codeship.io/2013/08/20/testing-tuesday-19-how-to-test-node-js-a
pplications-with-jasmine.html
36. Clemens Helm. Testing Tuesday #16: JavaScript Testing with Jasmine. Video
resource. Codeship. 2014
38
http://blog.codeship.io/2013/07/30/testing-tuesday-16-javascript-testing-with
-jasmine.html
37. Fog Creek Software, Inc. Trello. 2014
https://trello.com/
38. Canva Trello. Our task board. 2014
https://trello.com/b/BEYARHxo/canvadtu
39. GitHub, Inc. GitHub. 2014
https://github.com/
40. Atlassian. Bitbucket. 2014
https://bitbucket.org/
41. Canva Bitbucket. Our repository. 2014
https://bitbucket.org/vladmanea/ds-project
42. Clemens Helm. Testing Tuesday #20: Continuous Deployment for node.js
applications. Video resource. Codeship. 2014
http://blog.codeship.io/2013/08/27/testing-tuesday-20-continuous-deployme
nt-for-node-js-applications.html
43. Heroku. Logging. Retrieved 2014
https://devcenter.heroku.com/articles/logging
Appendix A The other three proposed ideas are described below:
A.1. Piping command line API modules
It takes days for developers to mash up various API results from the social web, and
all it comes up with is IO and a bit of processing. We wanted to develop a
framework for developers to mash-up API results quicker, by using command line
modules. Developers can pipe command lines which retrieve and forward API data.
For example, piping the commands against the Twitter and Facebook modules, such
as twitter get last 10 tweets @vladmanea {title, content, time} |
facebook post all messages vlad.manea {title, body:content,
moment:utc(time)}would fetch the last 10 tweets from Vlad’s Twitter account, and
post each tweet as a message on Vlad’s Facebook account. The accolades represent
the attribute mappings, and the last attribute is a call of an arbitrarily implemented
function inside the Facebook module on the output of the Twitter module. Based on
this paradigm, should it prove beneficial, API modules can be implemented and
perfected by the community, just like Passport did with authentication providers.
39
A.1.1. Strength
We can implement a powerful concept with cloud computation.
A.1.2. Weakness
While developing some sample modules, we might stumble upon programming
limits.
A.1.3. Opportunity
Make web developer life easy, and build up a community around our product.
A.1.4. Threat
The project might be related to distributed systems on a higher layer.
A.2. Google Docs LaTeX addon
Currently we have not seen a coherent, free collaborative editor for academic
documents. We wanted to develop an addon to be integrated with Google docs,
which would allow constructing LaTeX framework formulae. The Google Docs addon
technology is a new technology. For instance, a part of the document written in
LaTeX is collaboratively edited by more user, and then selected. A formula picture is
generated on the fly, and can be inserted in place. A nice to have would be making
use of the Google Docs version control to ensure undo back in LaTeX.
A.2.1. Strength
We can make use of a compiler in the Google Docs trustworthy environment.
A.2.2. Weakness
In this time frame not all formulae or functionality might be implemented.
A.2.3. Opportunity
Leverage the power of Google Docs for a more collaborative research world.
A.2.4. Threat
The project might relate to distributed systems on a higher layer.
A.3. Biker real time tracking
Going on a bike ride takes a lot of post-processing time. Current applications focus
on the competition and metrics after the fact only, which makes it a rather static
40
process. We wish to develop a simple application for peer-to-peer finding riding
friends nearby, and fast and furious races with them. Based on the location of your
fellow riders (e.g., your Google+ contacts), we tell you how you stack up against
them in a race all along Strandvejen.
A.3.1. Strength
We can make use of geolocation to make it a fun experience.
A.3.2. Weakness
May take more time to prove the concept feasible in practice.
A.3.3. Opportunity
Ride healthy, make it a fun experience, and perhaps improve some records?
A.3.4. Threat
The project might relate to distributed systems on a higher layer.
Appendix B
B1. Server side code
package.json
{
"name": "Drawr",
"version": "0.0.1",
"description": "Drawr",
"main": "server.js",
"author": "Vlad Manea, Laura Stroe, Nanna Hjaltalin",
"dependencies": {
"socket.io": "0.9.16",
"express": "3.4.8",
"jade": "1.3.1",
"jquery": "1.11.0",
"stylus": "0.43.1",
"underscore": "1.6.0"
},
"engines": {
"node": "0.10.x",
"npm": "1.2.x"
},
"scripts": {
"start": "node server.js"
}
}
41
app.js
var express = require('express');
var config = require('./config');
var routes = require('./routes');
var app = express();
if ('development' == app.get('env')) {
config.configDev(app);
}
if ('production' == app.get('env')) {
config.configProd(app);
}
module.exports = app;
chat.js
var _ = require('underscore');
function Chat() {
this.participants = [];
this.addParticipant = function(data) {
this.participants.push({id: data.id, name: data.name});
};
this.findParticipantById = function(participantId) {
return _.findWhere(this.participants, {id: participantId});
};
this.removeParticipant = function(participantId) {
this.participants = _.without(this.participants, _.findWhere(this.participants,
{id: participantId}));
};
};
Chat.prototype.newUser = function(data) {
this.addParticipant(data);
return {
status: 'OK',
participants: this.participants
};
};
Chat.prototype.nameChange = function(data, participantId) {
var participant = this.findParticipantById(participantId);
if (!participant) {
return {
status: 'ERROR',
situation: 'MISSING'
};
}
participant.name = data.name;
return {
status: 'OK'
};
42
};
Chat.prototype.disconnect = function(participantId) {
this.removeParticipant(participantId);
return {
status: 'OK'
};
};
module.exports = Chat;
config.js
var path = require('path');
var express = require('express');
// The configuration data.
exports.data = {
// Server configuration.
server: {
port: process.env.PORT || 5000
},
// Security configuration (not used for now).
security: {
cookieParserSecret: 'Secret'
},
// Route configuration.
route: {
path: path.join(__dirname, 'routes')
},
// Controller configuration.
controller: {
path: path.join(__dirname, 'controllers')
},
// Model configuration.
model: {
path: path.join(__dirname, 'models')
},
// View configuration.
view: {
path: path.join(__dirname, 'views'),
engine: 'jade'
},
// Style configuration.
style: {
path: path.join(__dirname, 'public'),
engine: 'stylus'
}
}
function configure(app) {
// Common section.
app.set('port', exports.data.server.port);
43
app.set('views', exports.data.view.path);
app.set('view engine', exports.data.view.engine);
app.use(express.logger('dev'));
app.use(express.json());
app.use(express.urlencoded());
app.use(express.cookieParser(exports.data.security.cookieParserSecret));
app.use(express.session());
app.use(app.router);
app.use(require(exports.data.style.engine).middleware(exports.data.style.path));
app.use(express.static(exports.data.style.path));
}
// The configuration function for dev.
exports.configDev = function(app) {
configure(app);
app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
};
// The configuration function for prod.
exports.configProd = function(app) {
configure(app);
app.use(express.errorHandler());
};
http.js
var http = require('http');
var app = require('./app');
var server = http.createServer(app);
server.listen(app.get('port'));
module.exports = server;
io.js
var _ = require('underscore');
var http = require('./http');
var io = require('socket.io').listen(http);
var Repository = require('./repository');
var repository = new Repository();
var Chat = require('./chat');
var chat = new Chat();
// Socket events
io.sockets.on('connection', function (socket) {
// User related
socket.on("newUser", function(data) {
var result = chat.newUser(data);
if (result.status == 'OK') {
io.sockets.emit("newConnection", {participants: result.participants});
socket.emit("updateBranchCreated", repository.getActiveBranches());
}
});
socket.on("nameChange", function(data) {
var result = chat.nameChange(data, socket.id);
44
if (result.status == 'OK') {
io.sockets.emit("nameChanged", {id: data.id, name: data.name});
return;
}
if (result.status == 'ERROR') {
switch (result.situation) {
case 'MISSING':
socket.emit('error', {message: 'Could not change name for missing
participant.'});
break;
}
}
});
socket.on("disconnect", function() {
var result = chat.disconnect(socket.id);
if (result.status == 'OK') {
io.sockets.emit("userDisconnected", {id: socket.id, sender: "system"});
}
});
// Figure related
socket.on("newFigure", function(data) {
if (data.branch && repository.isBlocked(data.branch.name)) {
io.sockets.emit("figureCreated", {id: socket.id, canvas: data.canvas, branch:
data.branch});
}
});
// Branch related
socket.on("block", function(data) {
var result = repository.block(data);
if (result.status == 'OK') {
socket.emit("blocked", {
branch: {
name: result.branch.name
},
token: result.token
});
return;
}
if (result.status == 'ERROR') {
switch (result.situation) {
case 'INVALID':
socket.emit('error', {message: 'Invalid request'});
break;
case 'MISSING':
socket.emit('error', {message: 'Could not find branch ' +
data.name});
break;
case 'INUSE':
socket.emit('error', {message: 'Could not block branch. There is
another participant in control.'});
break;
}
45
}
});
socket.on("unblock", function(data) {
var result = repository.unblock(data);
if (result.status == 'OK') {
socket.emit("unblocked", {
branch: {
name: result.branch.name
}
});
return;
}
if (result.status == 'ERROR') {
switch (result.situation) {
case 'INVALID':
socket.emit('error', {message: 'Invalid request'});
break;
case 'MISSING':
socket.emit('error', {message: 'Could not find branch ' +
data.name});
break;
case 'FORBIDDEN':
socket.emit('error', {message: 'Could not unblock branch. There is
another participant in control.'});
break;
}
}
});
socket.on("checkOutBranch", function(data) {
var result = repository.checkOutBranch(data);
if (result.status == 'OK') {
socket.emit("branchCheckedOut", result.branch);
return;
}
if (result.status == 'ERROR') {
switch (result.situation) {
case 'INVALID':
socket.emit('error', {message: 'Invalid request'});
break;
case 'MISSING':
socket.emit('error', {message: 'Could not find branch ' +
data.name});
break;
}
}
});
socket.on("saveBranch", function(data) {
var result = repository.saveBranch(data);
if (result.status == 'OK') {
switch (result.situation) {
case 'CREATED':
io.sockets.emit("updateBranchCreated",
repository.getActiveBranches());
break;
46
case 'SAVED':
socket.emit("branchSaved", {name: data.branch.name, automatic:
data.automatic});
break;
}
return;
}
if (result.status == 'WARNING') {
switch (result.situation) {
case 'UPTODATE':
socket.emit("warning", {message: "Did not save branch. Branch is
already up to date."});
break;
}
return;
}
if (result.status == 'ERROR') {
switch (result.situation) {
case 'INVALID':
socket.emit("error", {message: "Invalid request."});
break;
case 'MISSING':
socket.emit("error", {message: "Could not find branch " +
data.branch.name + "."});
break;
case 'CLIENTHEAD':
socket.emit("error", {message: "Client error, branch head was not
created."});
break;
case 'SERVERHEAD':
socket.emit("error", {message: "Server error, branch head was not
created."});
break;
case 'CONFLICT':
socket.emit("error", {message: "Could not save branch due to
another save. Checkout branch to get latest changes."});
break;
case 'FORBIDDEN':
socket.emit("error", {message: "Could not save branch. There is
another participantincontrol.Youmaytrytoblockthebranchifithasbeenblockedfor
a long time."});
break;
}
}
});
socket.on("deleteBranch", function(data) {
var result = repository.deleteBranch(data);
if (result.status == 'OK') {
socket.emit("branchDeleted", data.name);
// Remove the branch from the available branches in the client.
io.sockets.emit("updateBranchDeleted", data.name);
return;
}
if (result.status == 'ERROR') {
47
switch (result.situation) {
case 'INVALID':
socket.emit("error", {message: "Invalid request."});
break;
case 'MISSING':
socket.emit("error", {message: "Could notfindbranch"+ data.name
+ "."});
break;
case 'MASTER':
socket.emit("error", {message: "Cannot delete branch master."});
break;
}
}
});
});
module.exports = io;
repository.js
var _ = require('underscore');
var TIMEOUT_SECONDS = 300;
function Repository() {
this.branches = [];
this.branches.push({
name: 'master',
nodes: [{
id : {
numeric: 0,
author: {},
branch: 'master'
},
object: {objects: [], background: ""},
next: {},
saved: true
}],
active: true
});
this.blockades = [];
this.findBranchByName = function(branchName) {
return _.find(this.branches, function(branch) { return branch.name ==
branchName; });
};
this.removeBranch = function(branchName) {
this.branches = _.filter(this.branches, function(branch) { return branch.name
!= branchName; });
};
this.addBranch = function(branch) {
this.branches.push(branch);
};
this.updateBranch = function(branch) {
this.removeBranch(branch.name);
this.addBranch(branch);
};
48
this.markSavedBranch = function(branch) {
for (var index = branch.nodes.length - 1; index >= 0; --index) {
if (branch.nodes[index].saved == false) {
branch.nodes[index].saved = true;
} else {
return;
}
}
};
this.removeBlockade = function(branchName) {
this.blockades = _.filter(this.blockades, function(blockade) { return
blockade.name != branchName; });
};
this.addBlockade = function(blockade) {
this.blockades.push(blockade);
};
this.findBlockade = function(branchName) {
return _.find(this.blockades, function(blockade) { return blockade.name ==
branchName; });
};
};
Repository.prototype.isBlocked = function(branchName) {
return this.findBlockade(branchName);
};
Repository.prototype.block = function(data) {
if (!data || !data.participant || !data.participant.id || !data.branch ||
!data.branch.name) {
return {
status: 'ERROR',
situation: 'INVALID'
};
}
// See if the branch exists
var branchName = data.branch.name;
var foundBranch = this.findBranchByName(branchName);
if (!foundBranch || foundBranch.active == false) {
return {
status: 'ERROR',
situation: 'MISSING'
};
}
var now = new Date();
var participantId = data.participant.id;
var blockade = this.findBlockade(branchName);
if (blockade) {
// Check that the blocking time has expired
if (new Date(blockade.date.getTime() + TIMEOUT_SECONDS * 1000) < now) {
// This user can take control, and is given a replace token
blockade.participantId = participantId;
var token = Math.random();
blockade.token = token;
blockade.date = now;
49
return {
status: 'OK',
branch: {
name: branchName
},
token: token
};
}
var token = data.token;
if (!token || blockade.token != token) {
// This user has not authenticated
return {
status: 'ERROR',
situation: 'INUSE'
};
}
// This user can prolong his blocking control, and is given a new token
blockade.date = now;
blockade.token = Math.random();
return {
status: 'OK',
branch: {
name: branchName
},
token: blockade.token
};
}
// This user can take control, and is given an initial token
var token = Math.random();
this.addBlockade({
name: branchName,
participantId: participantId,
token: token,
date: now
});
return {
status: 'OK',
branch: {
name: branchName
},
token: token
};
};
Repository.prototype.unblock = function(data) {
if (!data || !data.participant || !data.participant.id || !data.branch ||
!data.branch.name) {
return {
status: 'ERROR',
situation: 'INVALID'
};
}
if (!data.token) {
return {
status: 'ERROR',
50
situation: 'FORBIDDEN'
};
}
// See if the branch exists
var branchName = data.branch.name;
var foundBranch = this.findBranchByName(branchName);
if (!foundBranch || foundBranch.active == false) {
return {
status: 'ERROR',
situation: 'MISSING'
};
}
var participantId = data.participant.id;
var token = data.token;
var blockade = this.findBlockade(branchName);
if (blockade && blockade.token != token) {
return {
status: 'ERROR',
situation: 'FORBIDDEN'
};
}
this.removeBlockade(branchName);
return {
status: 'OK',
branch: {
name: branchName
}
};
};
Repository.prototype.getActiveBranches = function() {
return _.where(this.branches, {active: true});
};
Repository.prototype.checkOutBranch = function(data) {
if (!data || !data.name) {
return {
status: 'ERROR',
situation: 'INVALID'
};
}
var branchName = data.name;
var foundBranch = this.findBranchByName(branchName);
if (!foundBranch || foundBranch.active == false) {
return {
status: 'ERROR',
situation: 'MISSING'
};
}
return {
status: 'OK',
branch: foundBranch
};
};
Repository.prototype.saveBranch = function(data) {
if (!data || !data.branch || !data.branch.name) {
51
return {
status: 'ERROR',
situation: 'INVALID'
};
}
var branchName = data.branch.name;
var foundBranch = this.findBranchByName(branchName);
if (!foundBranch) {
// I. Branch was not saved before => just save it now.
this.markSavedBranch(data.branch);
this.updateBranch(data.branch);
return {
status: 'OK',
situation: 'CREATED'
};
}
if (foundBranch.active == false) {
return {
status: 'ERROR',
situation: 'MISSING'
};
}
var blockade = this.findBlockade(branchName);
if (blockade) {
var token = data.token;
if (!token || blockade.token != token) {
return {
status: 'ERROR',
situation: 'FORBIDDEN'
};
}
}
var serverBranch = foundBranch;
var clientBranch = data.branch;
if (clientBranch.nodes.length <= 0) {
return {
status: 'ERROR',
situation: 'CLIENTHEAD'
};
}
if (serverBranch.nodes.length <= 0) {
return {
status: 'ERROR',
situation: 'SERVERHEAD'
};
}
var min = Math.min(clientBranch.nodes.length, serverBranch.nodes.length);
if (clientBranch.nodes.length > min) {
if (JSON.stringify(clientBranch.nodes[min - 1].id) ==
JSON.stringify(serverBranch.nodes[min - 1].id)) {
// II.2. Client made changes, server did not => add change to branch.
this.markSavedBranch(data.branch);
52
this.updateBranch(data.branch);
return {
status: 'OK',
situation: 'SAVED'
};
}
// II.4. Both made changes, reject change.
return {
status: 'ERROR',
situation: 'CONFLICT'
};
}
if (serverBranch.nodes.length > min) {
if (JSON.stringify(clientBranch.nodes[min - 1].id) ==
JSON.stringify(serverBranch.nodes[min - 1].id)) {
// II.3. Server made changes, client did not => do nothing.
return {
status: 'WARNING',
situation: 'UPTODATE'
};
}
// II.4. Both made changes, reject change.
return {
status: 'ERROR',
situation: 'CONFLICT'
};
}
if (JSON.stringify(clientBranch.nodes[min - 1].id) ==
JSON.stringify(serverBranch.nodes[min - 1].id)) {
// II.1. Nobody made changes => do nothing.
return {
status: 'WARNING',
situation: 'UPTODATE'
};
}
return {
status: 'ERROR',
situation: 'CONFLICT'
};
};
Repository.prototype.deleteBranch = function(data) {
if (!data || !data.name) {
return {
status: 'ERROR',
situation: 'INVALID'
};
}
var branchName = data.name;
if (branchName == 'master') {
return {
status: 'ERROR',
situation: 'MASTER'
};
53
}
var foundBranch = this.findBranchByName(branchName);
if (!foundBranch || foundBranch.active == false) {
return {
status: 'ERROR',
situation: 'MISSING'
};
}
// Deactivate the branch.
foundBranch.active = false;
return {
status: 'OK'
};
};
module.exports = Repository;
routes.js
var _ = require('underscore');
module.exports.configure = function(app, io) {
// GET method to obtain the index view
app.get("/", function(request, response) {
response.render("index");
});
// POST method to create a chat message
app.post("/message", function(request, response) {
var message = request.body.message;
if(_.isUndefined(message) || _.isEmpty(message.trim())) {
return response.json(400, {error: "Message is invalid"});
}
var name = request.body.name;
io.sockets.emit("incomingMessage", {message: message, name: name});
response.json(200, {message: "Message received"});
});
}
server.js
var app = require('./app');
var io = require('./io');
var routes = require('./routes');
routes.configure(app, io);
B.2. Client side code
circle.js
function CircleDrawer(canvas, modifier, socket) {
var iniPos;
54
var previewCircle;
var pressed = false;
this.mouseDown = function(options) {
iniPos = { x: options.e.clientX, y: options.e.clientY };
pressed = true;
};
this.mouseMove = function(options) {
if (!pressed) {
return;
}
var radius = Math.sqrt(Math.pow(options.e.clientX - iniPos.x, 2) +
Math.pow(options.e.clientY - iniPos.y, 2));
canvas.remove(previewCircle);
previewCircle = new fabric.Circle({
strokeWidth: modifier.get('preview-stroke-width'),
stroke: modifier.get('preview-stroke-color'),
fill: modifier.get('preview-fill-color'),
left: iniPos.x - radius,
top: iniPos.y - radius,
radius: radius
});
canvas.add(previewCircle);
};
this.mouseUp = function(options) {
pressed = false;
var radius = Math.sqrt(Math.pow(options.e.clientX - iniPos.x, 2) +
Math.pow(options.e.clientY - iniPos.y, 2));
canvas.remove(previewCircle);
if (iniPos.x == options.e.clientX && iniPos.x == options.e.clientX) {
return;
}
var circle = new fabric.Circle({
strokeWidth: modifier.get('actual-stroke-width'),
stroke: modifier.get('actual-stroke-color'),
fill: modifier.get('actual-fill-color'),
left: iniPos.x - radius,
top: iniPos.y - radius,
radius: radius
});
canvas.add(circle);
socket.figureCreated(canvas);
};
};
ellipse.js
function EllipseDrawer(canvas, modifier, socket) {
var pressed = false;
var previewEllipse;
var iniPos;
this.mouseDown = function(options) {
55
iniPos = { x: options.e.clientX, y: options.e.clientY };
pressed = true;
};
this.mouseMove = function(options) {
if (!pressed) {
return;
}
canvas.remove(previewEllipse);
var radiusx = Math.abs(options.e.clientX - iniPos.x);
var radiusy = Math.abs(options.e.clientY - iniPos.y);
previewEllipse = new fabric.Ellipse({
left:iniPos.x - radiusx,
top: iniPos.y - radiusy,
strokeWidth: modifier.get('preview-stroke-width'),
stroke: modifier.get('preview-stroke-color'),
fill: modifier.get('preview-fill-color'),
rx: radiusx,
ry: radiusy
});
canvas.add(previewEllipse);
};
this.mouseUp = function(options) {
pressed = false;
canvas.remove(previewEllipse);
var radiusx = Math.abs(options.e.clientX - iniPos.x);
var radiusy = Math.abs(options.e.clientY - iniPos.y);
var ellipse = new fabric.Ellipse({
left: iniPos.x - radiusx,
top: iniPos.y - radiusy,
strokeWidth: modifier.get('actual-stroke-width'),
stroke: modifier.get('actual-stroke-color'),
fill: modifier.get('actual-fill-color'),
rx: radiusx,
ry: radiusy
});
canvas.add(ellipse);
socket.figureCreated(canvas);
};
};
line.js
function LineDrawer(canvas, modifier, socket) {
var pressed = false;
var previewLine;
var iniPos;
this.mouseDown = function(options) {
iniPos = { x1: options.e.clientX, y1: options.e.clientY };
pressed = true;
};
56
this.mouseMove = function(options) {
if (!pressed) {
return;
}
canvas.remove(previewLine);
previewLine = new fabric.Line([iniPos.x1, iniPos.y1, options.e.clientX,
options.e.clientY], {
strokeWidth: modifier.get('preview-stroke-width'),
stroke: modifier.get('preview-stroke-color'),
});
canvas.add(previewLine);
};
this.mouseUp = function(options) {
pressed = false;
canvas.remove(previewLine);
var line = new fabric.Line([iniPos.x1, iniPos.y1, options.e.clientX,
options.e.clientY], {
strokeWidth: modifier.get('actual-stroke-width'),
stroke: modifier.get('actual-stroke-color'),
});
canvas.add(line);
socket.figureCreated(canvas);
};
};
polyline.js
function PolylineDrawer(canvas, modifier, socket) {
var points = [];
var previewLines = [];
this.mouseUp = function(options) {
points.push({x: options.e.clientX, y: options.e.clientY});
if (points.length <= 1) {
return;
}
var point1 = points[points.length - 2];
var point2 = points[points.length - 1];
var previewLine = new fabric.Line([point1.x, point1.y, point2.x, point2.y],{
strokeWidth: modifier.get('preview-stroke-width'),
stroke: modifier.get('preview-stroke-color')
});
previewLines.push(previewLine);
canvas.add(previewLine);
};
this.stateChanged = function() {
for (var index = 0; index < previewLines.length; ++index) {
var previewLine = previewLines[index];
57
canvas.remove(previewLine);
canvas.add(new fabric.Line([previewLine.x1, previewLine.y1,
previewLine.x2, previewLine.y2], {
strokeWidth: modifier.get('actual-stroke-width'),
stroke: modifier.get('actual-stroke-color'),
}));
}
socket.figureCreated(canvas);
while (previewLines.length > 0) {
previewLines.pop();
}
}
};
rectangle.js
function RectangleDrawer(canvas, modifier, socket) {
var pressed = false;
var previewRect;
var iniPos;
this.mouseDown = function(options) {
iniPos = { x: options.e.clientX, y: options.e.clientY };
pressed = true;
};
this.mouseMove = function(options) {
if (!pressed) {
return;
}
canvas.remove(previewRect);
previewRect = new fabric.Rect({
left: iniPos.x,
top: iniPos.y,
strokeWidth: modifier.get('preview-stroke-width'),
stroke: modifier.get('preview-stroke-color'),
fill: modifier.get('preview-fill-color'),
width: options.e.clientX - iniPos.x,
height: options.e.clientY - iniPos.y
});
canvas.add(previewRect);
};
this.mouseUp = function(options) {
pressed = false;
canvas.remove(previewRect);
var rect = new fabric.Rect({
left: iniPos.x,
top: iniPos.y,
strokeWidth: modifier.get('actual-stroke-width'),
stroke: modifier.get('actual-stroke-color'),
fill: modifier.get('actual-fill-color'),
width: options.e.clientX - iniPos.x,
height: options.e.clientY - iniPos.y
});
58
canvas.add(rect);
socket.figureCreated(canvas);
};
};
text.js
function TextDrawer(canvas, modifier, socket) {
this.mouseUp = function(options) {
var text = prompt("Enter text"," ");
if (text != null) {
var textbox = new fabric.Text(text, {
left: options.e.clientX,
top: options.e.clientY
});
canvas.add(textbox);
socket.figureCreated(canvas);
}
};
};
triangle.js
function TriangleDrawer(canvas, modifier, socket) {
var pressed = false;
var previewTriangle;
var iniPos;
this.mouseDown = function(options) {
iniPos = { x: options.e.clientX, y: options.e.clientY };
pressed = true;
};
this.mouseMove = function(options) {
if (!pressed) {
return;
}
canvas.remove(previewTriangle);
previewTriangle = new fabric.Triangle({
left: iniPos.x,
top: iniPos.y,
strokeWidth: modifier.get('preview-stroke-width'),
stroke: modifier.get('preview-stroke-color'),
fill: modifier.get('preview-fill-color'),
width: options.e.clientX - iniPos.x,
height: options.e.clientY - iniPos.y
});
canvas.add(previewTriangle);
};
this.mouseUp = function(options) {
pressed = false;
canvas.remove(previewTriangle);
if (iniPos.x == options.e.clientX && iniPos.x == options.e.clientX) {
59
return;
}
var triangle = new fabric.Triangle({
left: iniPos.x,
top: iniPos.y,
strokeWidth: modifier.get('actual-stroke-width'),
stroke: modifier.get('actual-stroke-color'),
fill: modifier.get('actual-fill-color'),
width: options.e.clientX - iniPos.x,
height: options.e.clientY - iniPos.y
});
canvas.add(triangle);
socket.figureCreated(canvas);
};
};
chat.js
function Chat() {
this.sessionId = '';
};
Chat.prototype.getSessionId = function() {
return this.sessionId;
}
Chat.prototype.setSessionId = function(newSessionId) {
this.sessionId = newSessionId;
}
index.js
$(document).on('ready', function() {
$('#merge').hide();
$('#workspace').hide();
var canvas = new fabric.Canvas('demo');
var modifier = new Modifier();
setupModifier(modifier);
setupModifierButtonEvents(modifier);
var painter = new Painter(canvas);
var chat = new Chat();
var ui = new UI();
var repository = new Repository(ui);
var socket = new Socket(document.domain, canvas, chat, repository, ui);
registerFigures(canvas, painter, modifier, socket);
registerFigureButtonEvents(painter);
registerCanvasEvents(canvas, painter);
setupChatControls(socket, ui);
setupBranchButtons(socket, canvas);
});
function setupModifier(modifier) {
60
// Specify preview shape attributes
modifier.set('preview-stroke-width', 10);
modifier.set('preview-stroke-color', 'gray');
modifier.set('preview-fill-color', 'lightgray');
// Specify actual attributes
modifier.set('actual-stroke-width', 10);
modifier.set('actual-stroke-color', 'black');
modifier.set('actual-fill-color', 'transparent');
};
function registerFigures(canvas, painter, modifier, socket) {
painter.registerDrawer('circle', new CircleDrawer(canvas, modifier, socket));
painter.registerDrawer('ellipse', new EllipseDrawer(canvas, modifier, socket));
painter.registerDrawer('rectangle', new RectangleDrawer(canvas, modifier,
socket));
painter.registerDrawer('triangle', new TriangleDrawer(canvas, modifier, socket));
painter.registerDrawer('line', new LineDrawer(canvas, modifier, socket));
painter.registerDrawer('polyline', new PolylineDrawer(canvas, modifier, socket));
painter.registerDrawer('text', new TextDrawer(canvas, modifier, socket));
};
function registerFigureButtonEvents(painter) {
$('#circle').on('click', function() { painter.setDrawer('circle') });
$('#ellipse').on('click', function() { painter.setDrawer('ellipse') });
$('#rectangle').on('click', function() { painter.setDrawer('rectangle') });
$('#triangle').on('click', function() { painter.setDrawer('triangle') });
$('#line').on('click', function() { painter.setDrawer('line') });
$('#polyline').on('click', function() { painter.setDrawer('polyline') });
$('#text').on('click', function() { painter.setDrawer('text') });
};
function registerCanvasEvents(canvas, painter) {
canvas.on('mouse:down', function(options) { painter.mouseDown(options); });
canvas.on('mouse:move', function(options) { painter.mouseMove(options); });
canvas.on('mouse:up', function(options) { painter.mouseUp(options); });
canvas.on('object:moving', function(options) { painter.clearDrawer(); });
canvas.on('object:scaling', function(options) { painter.clearDrawer(); });
canvas.on('object:rotating', function(options) { painter.clearDrawer(); });
canvas.on('object:modified', function(options) { painter.clearDrawer(); });
};
function setupModifierButtonEvents(modifier) {
// Setup the stroke color picker
$('#strokeColor').colorpicker();
$('#strokeColor').colorpicker().on('changeColor', function(ev) {
var color = ev.color.toRGB();
modifier.set('actual-stroke-color', 'rgba(' + color.r + ', ' + color.g + ', ' +
color.b + ', ' + color.a + ')');
});
// Setup the fill color picker
$('#fillColor').colorpicker();
$('#fillColor').colorpicker().on('changeColor', function(ev) {
var color = ev.color.toRGB();
modifier.set('actual-fill-color', 'rgba(' + color.r + ', ' + color.g + ', ' +
color.b + ', ' + color.a + ')');
});
// Setup the stroke size slider
61
$("#strokeWidth").change(function() {
modifier.set('preview-stroke-width', $(this).val());
modifier.set('actual-stroke-width', $(this).val());
});
};
function setupChatControls(socket, ui) {
$('#outgoingMessage').on('keydown', ui.outgoingMessageKeyDown);
$('#outgoingMessage').on('keyup', ui.outgoingMessageKeyUp);
$('#name').on('focusout', socket.nameFocusOut);
$('#send').on('click', ui.sendMessage);
};
function setupBranchButtons(socket, canvas) {
$('#checkOutBranch').on('click', function() {
var name = $('#branches button.selected').html();
if (name) {
socket.checkOutBranch(name);
} else {
alert("You did not select a branch from the list.");
}
});
$('#createBranch').on('click', function() {
var name = prompt("Please enter a branch name", "");
if (name) {
socket.createBranch(name);
} else {
alert("You did not specify a name.");
}
});
$('#deleteBranch').on('click', function() {
var name = $('#branches button.selected').html();
if (name) {
socket.deleteBranch(name);
} else {
alert("You did not select a branch from the list.");
}
});
$('#commitBranch').on('click', function() {
var obj = canvas.toDatalessJSON();
socket.commitBranch(obj, false);
});
$('#mergeBranch').on('click', function() {
var name = $('#branches button.selected').html();
if (name) {
socket.mergeBranch(name);
} else {
alert("You did not select a branch from the list.");
}
});
$('#saveBranch').on('click', function() {
socket.saveBranch();
});
$('#block').on('click', function() {
socket.block();
});
62
$('#unblock').on('click', function() {
socket.unblock();
});
}
modifier.js
function Modifier(canvas) {
var properties = [];
this.get = function(propertyName) {
return properties[propertyName];
}
this.set = function(propertyName, propertyValue) {
properties[propertyName] = propertyValue;
}
};
painter.js
function Painter(canvas) {
// Register drawer types
var currentDrawerName;
var drawers = [];
this.registerDrawer = function(drawerName, drawer) {
drawers[drawerName] = drawer;
}
this.setDrawer = function(drawerName) {
if (!drawers[drawerName]) {
throw "Illegal argument exception: " + drawerName + " is not registered!";
}
for (var drawer in drawers) {
if (!drawers[drawer].stateChanged) {
continue;
}
drawers[drawer].stateChanged();
}
currentDrawerName = drawerName;
}
this.clearDrawer = function() {
currentDrawerName = "";
}
this.mouseDown = function(options) {
if (!drawers[currentDrawerName]) {
return;
}
if (!drawers[currentDrawerName].mouseDown) {
return;
}
63
drawers[currentDrawerName].mouseDown(options);
}
this.mouseMove = function(options) {
if (!drawers[currentDrawerName]) {
return;
}
if (!drawers[currentDrawerName].mouseMove) {
return;
}
drawers[currentDrawerName].mouseMove(options);
}
this.mouseUp = function(options) {
if (!drawers[currentDrawerName]) {
return;
}
if (!drawers[currentDrawerName].mouseUp) {
return;
}
drawers[currentDrawerName].mouseUp(options);
}
};
repository.js
function Repository(ui) {
this.ui = ui;
this.branches = [];
this.blockades = [];
this.clientBranch;
this.removeBlockade = function(branchName) {
this.blockades = _.filter(this.blockades, function(blockade) { return
blockade.name != branchName; });
};
this.addBlockade = function(blockade) {
this.blockades.push(blockade);
};
this.findBlockade = function(branchName) {
return _.find(this.blockades, function(blockade) { return blockade.name ==
branchName; });
};
this.removeBranch = function(branchName) {
this.branches = _.filter(this.branches, function(branch) { return branch.name
!= branchName; });
};
this.addBranch = function(branch) {
this.branches.push(branch);
}
};
Repository.prototype.getBranches = function() {
64
return this.branches;
};
Repository.prototype.getBranch = function() {
return this.clientBranch;
};
Repository.prototype.getBranchByName = function(branchName) {
return _.find(this.branches, function(branch) { return branch.name ==
branchName; });
};
Repository.prototype.getNode = function() {
return this.clientBranch.nodes[this.clientBranch.nodes.length - 1];
}
Repository.prototype.getValue = function() {
if (!this.clientBranch) {
return {objects: [], background: ""};
}
return this.getNode().object;
};
Repository.prototype.createBranch = function(branchName) {
if (!this.clientBranch) {
return {
status: 'ERROR',
situation: 'CHECKOUT'
};
}
var existingBranch = this.getBranchByName(branchName);
if (existingBranch) {
return {
status: 'ERROR',
situation: 'RENAME'
};
}
// Restrict branch creation to saved this.branches only.
var motherNode = this.getNode();
if (motherNode.saved == false) {
return {
status: 'ERROR',
situation: 'SAVE'
};
}
var branch = {
name: branchName,
active: true,
nodes: [{
id: {
numeric: 0,
author: {
id: this.ui.getUserId(),
name: this.ui.getUserName()
},
branch: branchName
},
object: motherNode.object,
65
next: motherNode,
saved: false
}]
};
this.addBranch(branch);
return {
status: 'OK',
branch: branch
};
};
Repository.prototype.commitBranch = function(objectToCommit) {
if (!this.clientBranch) {
return {
status: 'ERROR',
situation: 'CHECKOUT'
};
}
this.clientBranch.nodes.push({
id: {
numeric: this.getNode().id.numeric + 1,
author: {
id: this.ui.getUserId(),
name: this.ui.getUserName()
},
branch: this.clientBranch.name
},
object: objectToCommit,
next: this.getNode(),
saved: false
});
return {
status: 'OK'
};
};
Repository.prototype.updateBranch = function(branch) {
this.removeBranch(branch.name);
this.addBranch(branch);
this.useBranch(branch);
};
Repository.prototype.useBranch = function(branch) {
this.clientBranch = branch;
};
Repository.prototype.revertCurrentBranch = function() {
if (!this.clientBranch) {
return;
}
for (var index = this.clientBranch.nodes.length - 1; index > 0; --index) {
if (this.clientBranch.nodes[index].saved == false) {
this.clientBranch.nodes.pop();
} else {
break;
}
}
};
66
Repository.prototype.deleteBranch = function(branchName) {
this.removeBranch(branchName);
var masterBranch = this.getBranchByName('master');
this.useBranch(masterBranch);
};
Repository.prototype.markSavedBranch = function(branchName) {
var branch = this.getBranchByName(branchName);
for (var index = branch.nodes.length - 1; index >= 0; --index) {
if (branch.nodes[index].saved == false) {
branch.nodes[index].saved = true;
} else {
break;
}
}
};
Repository.prototype.branchCheckedOut = function(serverBranch) {
if (!this.clientBranch) {
this.updateBranch(serverBranch);
this.useBranch(serverBranch);
return {
status: 'OK',
situation: 'FETCHED'
};
}
if (serverBranch.name != this.clientBranch.name) {
// We are checking out a different branch
this.clientBranch = this.getBranchByName(serverBranch.name);
return {
status: 'OK',
situation: 'SWITCHED'
};
}
// Find the last saved node
var index = this.clientBranch.nodes.length - 1;
while (index > 0 && this.clientBranch.nodes[index].saved == false) {
index--;
}
// We are checking out the same branch
if (serverBranch.nodes.length <= index + 1) {
// Server did no changes => return
return {
status: 'OK',
situation: 'KEPT'
};
}
// Server has changed something!
if (this.clientBranch.nodes.length - 1 > index) {
return {
status: 'ERROR',
situation: 'MERGE'
};
}
67
// Client did not change anything => update to latest version.
this.updateBranch(serverBranch);
this.useBranch(serverBranch);
return {
status: 'OK',
situation: 'UPDATED'
};
};
Repository.prototype.mergeBranch = function(branchName) {
if (!this.clientBranch) {
return {
status: 'ERROR',
situation: 'CHECKOUT'
};
}
var childBranch = this.getBranchByName(branchName);
if (!childBranch) {
return {
status: 'ERROR',
situation: 'CHILD'
};
}
return {
status: 'OK',
situation: 'MERGE'
};
};
Repository.prototype.getConflictingNodes = function(serverBranch) {
if (!this.clientBranch) {
return {};
}
// Find the last saved node
var index = this.clientBranch.nodes.length - 1;
while (index > 0 && this.clientBranch.nodes[index].saved == false) {
index--;
}
return {
theirs: serverBranch.nodes[serverBranch.nodes.length - 1],
yours: this.clientBranch.nodes[this.clientBranch.nodes.length - 1],
last: this.clientBranch.nodes[index]
};
};
Repository.prototype.getToken = function(branchName) {
var blockade = this.findBlockade(branchName);
return blockade ? blockade.token : "";
}
Repository.prototype.blocked = function(data) {
this.removeBlockade(data.branch.name);
this.addBlockade({
name: data.branch.name,
token: data.token
});
};
68
Repository.prototype.unblocked = function(data) {
this.removeBlockade(data.branch.name);
};
socket.js
function Socket(address, canvas, chat, repository, ui) {
var socket = io.connect(address);
var self = this;
// General socket events
socket.on('error', function (error) {
if (error.message) {
ui.notifyUser(error.message);
return;
}
console.log('Unable to connect to server', error);
});
socket.on('warning', function(warning) {
ui.notifyUser(warning.message);
});
// User related socket events
socket.on('connect', function () {
chat.setSessionId(socket.socket.sessionid);
socket.emit('newUser', {id: chat.getSessionId(), name: ui.getUserName()});
});
socket.on('newConnection', function (data) {
ui.updateParticipants(data.participants, chat.getSessionId());
});
socket.on('userDisconnected', function(data) {
ui.removeUser(data.id);
});
socket.on('nameChanged', function (data) {
ui.updateUser(data.id, data.name, chat.getSessionId());
});
socket.on('incomingMessage', function (data) {
ui.addMessage(data.message, data.name);
});
// Figure related socket events
socket.on('figureCreated', function (data) {
var clientBranch = repository.getBranch();
if (clientBranch && data.branch && clientBranch.name == data.branch.name) {
// Render the canvas
ui.showCanvas();
canvas.loadFromDatalessJSON(data.canvas);
canvas.renderAll();
// Try to save the branch with the token
69
var token = repository.getToken(clientBranch.name);
if (token) {
self.commitBranch(canvas.toDatalessJSON(), true);
self.saveBranch(true);
}
}
});
// Branch related socket events
socket.on('updateBranchCreated', function (branches) {
ui.updateBranches(branches);
});
socket.on('updateBranchDeleted', function(name) {
ui.removeBranch(name);
});
socket.on('branchCheckedOut', function(serverBranch) {
var response = repository.branchCheckedOut(serverBranch);
if (response.status == 'OK') {
switch (response.situation) {
case 'FETCHED':
ui.notifyUser("The branch has been fetched.");
break;
case 'UPDATED':
ui.notifyUser("The branch has been updated.");
break;
case 'LEFT':
ui.notifyUser("You have checked out on branch "+ serverBranch.name
+ ". You can always check out the branch you are leaving.");
break;
case 'KEPT':
ui.notifyUser("You have checked out on branch "+ serverBranch.name
+ ". No changes found.");
break;
}
ui.showCanvas();
canvas.loadFromDatalessJSON(repository.getValue());
canvas.renderAll();
return;
}
if (response.status == 'ERROR') {
switch (response.situation) {
case 'MERGE':
var conflictingNodes =
repository.getConflictingNodes(serverBranch);
var conflictingValues = {
theirs: JSON.stringify(conflictingNodes.theirs.object),
yours: JSON.stringify(conflictingNodes.yours.object),
last: JSON.stringify(conflictingNodes.last.object)
};
ui.initiateMerge(true, conflictingValues, repository,
serverBranch);
}
}
});
70
socket.on('branchCreated', function(branchName) {
repository.markSavedBranch(branchName);
ui.notifyUser("Branch " + branchName + " was created.");
});
socket.on('branchSaved', function(data) {
repository.markSavedBranch(data.name);
if (!data.automatic) {
ui.notifyUser("Branch " + data.name + " was saved.");
}
});
socket.on('branchDeleted', function(branchName) {
repository.deleteBranch(branchName);
ui.notifyUser("Branch " + branchName + " was deleted.");
});
socket.on('blocked', function(data) {
repository.blocked(data);
ui.notifyUser("Branch " + data.branch.name + " was blocked. Now all changes
happening in this branch will be saved on your behalf.");
});
socket.on('unblocked', function(data) {
repository.unblocked(data);
ui.notifyUser("Branch " + data.branch.name + " was unblocked.");
});
this.block = function() {
var branch = repository.getBranch();
var branchName = branch ? branch.name : "";
socket.emit("block", {
participant: {
id: chat.getSessionId()
},
branch: {
name: branchName
},
token: repository.getToken(branchName)
});
};
this.unblock = function() {
var branch = repository.getBranch();
var branchName = branch ? branch.name : "";
socket.emit("unblock", {
participant: {
id: chat.getSessionId()
},
branch: {
name: branchName
},
token: repository.getToken(branchName)
});
};
this.checkOutBranch = function(branchName) {
socket.emit("checkOutBranch", {id: chat.getSessionId(), name: branchName});
};
71
this.createBranch = function(branchName) {
var response = repository.createBranch(branchName);
if (response.status == 'OK') {
socket.emit("saveBranch", {id: chat.getSessionId(), branch:
response.branch, automatic: false});
return;
}
if (response.status == 'ERROR') {
switch (response.situation) {
case 'CHECKOUT':
ui.notifyUser("Could not create branch. Checkout a branch first.");
break;
case 'RENAME':
ui.notifyUser("Could not create branch. A branch with name " +
branchName + " already exists. Create a branch with a different name.");
break;
case 'SAVE':
ui.notifyUser("Could not create branch because there are unsaved
changes. Save the branch first.");
break;
}
}
};
this.mergeBranch = function(branchName) {
var response = repository.mergeBranch(branchName);
if (response.status == 'ERROR') {
switch (response.situation) {
case 'CHECKOUT':
ui.notifyUser("Could not merge branch. Checkout a branch first.");
break;
case 'CHILD':
ui.notifyUser("Could not merge branch. Child branch does not
exist.");
break;
}
return;
}
if (response.status == 'OK') {
switch (response.situation) {
case 'MERGE':
var childBranch = repository.getBranchByName(branchName);
var conflictingNodes = repository.getConflictingNodes(childBranch);
var conflictingValues = {
theirs: JSON.stringify(conflictingNodes.theirs.object),
yours: JSON.stringify(conflictingNodes.yours.object),
last: JSON.stringify(conflictingNodes.last.object)
};
ui.initiateMerge(false, conflictingValues, repository, null);
}
}
};
this.commitBranch = function(objectToCommit, automatic) {
var response = repository.commitBranch(objectToCommit);
72
if (response.status == 'OK') {
if (!automatic) {
ui.notifyUser("The change has been committed.");
}
return;
}
if (response.status == 'ERROR') {
switch (response.situation) {
case 'CHECKOUT':
ui.notifyUser("Could not commit branch. Checkout a branch first.");
break;
}
}
}
this.saveBranch = function(automatic) {
var clientBranch = repository.getBranch();
socket.emit("saveBranch", {id: chat.getSessionId(), branch: clientBranch,
token: repository.getToken(clientBranch ? clientBranch.name : ''), automatic:
automatic});
};
this.deleteBranch = function(branchName) {
socket.emit("deleteBranch", {id: chat.getSessionId(), name: branchName});
};
// Name related functions
this.nameFocusOut = function() {
socket.emit('nameChange', {id: chat.getSessionId(), name: ui.getUserName()});
};
// Figure related functions
this.figureCreated = function(canvas) {
socket.emit('newFigure', {
id: chat.getSessionId(),
canvas: canvas.toDatalessJSON(),
branch: repository.getBranch()
});
};
}
ui.js
function UI() {
var self = this;
this.notifyUser = function(message) {
alert(message);
};
this.updateParticipants = function(participants, userSessionId) {
$('#participants').html('');
for (var i = 0; i < participants.length; i++) {
$('#participants').append('<span id="user-' + participants[i].id + '">' +
participants[i].name + ' ' + (participants[i].id === userSessionId ?
'(You)' : '') + '<br /></span>');
73
if (participants[i].id === userSessionId) {
$('#name').val(participants[i].name).attr('data-id', participants[i].id);
}
}
};
this.removeUser = function(userId) {
$('#' + userId).remove();
};
this.updateUser = function(userId, userName, userSessionId) {
$('#user-' + userId).html(userName + ' ' + (userId=== userSessionId? '(You)':
'') + '<br />');
};
this.addMessage = function(message, userName) {
$('#messages').append('<hr /><b>' + userName + '</b><br />' + message);
};
this.updateBranches = function(branches) {
$('#branches').html('');
for (var index in branches) {
$('#branches').append('<button id="branch-' + branches[index].name + '">'
+ branches[index].name + '</button>');
}
$('#branches button').on('click', function(event) {
$(event.target).siblings().removeClass('selected');
$(event.target).addClass('selected');
});
};
this.removeBranch = function(branchName) {
$('#branch-' + branchName).remove();
};
this.sendMessage = function() {
var outgoingMessage = $('#outgoingMessage').val();
var name = self.getUserName();
$.ajax({
url: '/message',
type: 'POST',
dataType: 'json',
data: {message: outgoingMessage, name: name}
});
};
this.outgoingMessageKeyDown = function(event) {
if (event.which == 13) {
event.preventDefault();
if ($('#outgoingMessage').val().trim().length <= 0) {
return;
}
self.sendMessage();
$('#outgoingMessage').val('');
}
};
74
this.outgoingMessageKeyUp = function() {
var outgoingMessageValue = $('#outgoingMessage').val();
$('#send').attr('disabled', (outgoingMessageValue.trim()).length > 0 ? false :
true);
};
this.getUserName = function() {
return $('#name').val() ? $('#name').val() : "";
};
this.getUserId = function() {
return $('#name').attr('data-id') ? $('#name').attr('data-id') : "";
};
this.showCanvas = function() {
$('#workspace').show();
};
this.initiateMerge = function(isCheckout, conflictingValues, repository,
serverBranch) {
$('#merge').show();
var yours = new fabric.Canvas('yours');
var last = new fabric.Canvas('last');
var theirs = new fabric.Canvas('theirs');
var trash = new fabric.Canvas('trash');
yours.loadFromDatalessJSON(conflictingValues.yours);
yours.renderAll();
last.loadFromDatalessJSON(conflictingValues.last);
last.renderAll();
theirs.loadFromDatalessJSON(conflictingValues.theirs);
theirs.renderAll();
function objectExists(object) {
var lastObjects = last.getObjects();
for (var index in lastObjects) {
var lastObject = lastObjects[index];
if (JSON.stringify(object.toJSON()) ==
JSON.stringify(lastObject.toJSON())) {
return true;
}
}
return false;
};
var yoursObjects = yours.getObjects();
for (var index in yoursObjects) {
var object = yoursObjects[index];
object.lockMovementX = true;
object.lockMovementY = true;
object.lockRotation = true;
object.lockScalingX = true;
object.lockScalingY = true;
object.lockUniScaling = true;
if (objectExists(object)) {
object.selectable = false;
object.stroke = 'lightgray';
75
object.fill = 'lightgray';
}
};
yours.renderAll();
var theirsObjects = theirs.getObjects();
for (var index in theirsObjects) {
var object = theirsObjects[index];
object.lockMovementX = true;
object.lockMovementY = true;
object.lockRotation = true;
object.lockScalingX = true;
object.lockScalingY = true;
object.lockUniScaling = true;
if (objectExists(object)) {
object.selectable = false;
object.stroke = 'lightgray';
object.fill = 'lightgray';
}
};
theirs.renderAll();
var lastObjects = last.getObjects();
for (var index in lastObjects) {
var object = lastObjects[index];
object.lockMovementX = true;
object.lockMovementY = true;
object.lockRotation = true;
object.lockScalingX = true;
object.lockScalingY = true;
object.lockUniScaling = true;
}
last.renderAll();
yours.on("object:selected", function(event) {
var object = event.target;
if (!object.selectable) {
return;
}
object.comingFrom = 'yours';
last.add(object);
last.renderAll();
yours.remove(object);
yours.renderAll();
});
theirs.on("object:selected", function(event) {
var object = event.target;
if (!object.selectable) {
return;
}
object.comingFrom = 'theirs';
last.add(object);
last.renderAll();
theirs.remove(object);
theirs.renderAll();
});
last.on("object:selected", function(event) {
var object = event.target;
76
if (object.comingFrom == 'theirs') {
theirs.add(object);
theirs.renderAll();
} else if (object.comingFrom == 'yours') {
yours.add(object);
yours.renderAll();
} else {
trash.add(object);
trash.renderAll();
}
last.remove(object);
last.renderAll();
});
trash.on("object:selected", function(event) {
var object = event.target;
last.add(object);
last.renderAll();
trash.remove(object);
trash.renderAll();
});
$('#mergeok').off().on('click', function() {
var lastObjects = last.getObjects();
for (var index in lastObjects) {
var object = lastObjects[index];
delete object.comingFrom;
}
last.renderAll();
if (isCheckout) {
repository.updateBranch(serverBranch);
repository.useBranch(serverBranch);
}
repository.commitBranch(last.toDatalessJSON(), self.getUserName());
self.notifyUser("The branch merge has been resolved.");
$('#merge').hide();
});
$('#mergecancel').off().on('click', function() {
self.notifyUser("The branch merge has been aborted.");
$('#merge').hide();
});
};
}
style.css
body {
padding: 0;
font-family: 'Open Sans', sans-serif;
font-size: 1em;
}
index.jade
doctype html
77
html
head
meta(charset='utf-8')
meta(name='author', content='Vlad Manea')
meta(name='description' content='Canva')
meta(name='robots' content='all,index,follow')
script(type='text/javascript', src="/socket.io/socket.io.js")
script(type='text/javascript',
src="/scripts/lib/jquery.min.js")
script(type='text/javascript',
src="/scripts/lib/bootstrap.colorpicker.js")
script(type='text/javascript',
src="/scripts/lib/underscore.min.js")
script(type='text/javascript',
src="/scripts/lib/fabric.min.js")
script(type='text/javascript',
src="/scripts/drawing_modules/circle.js")
script(type='text/javascript',
src="/scripts/drawing_modules/ellipse.js")
script(type='text/javascript',
src="/scripts/drawing_modules/rectangle.js")
script(type='text/javascript',
src="/scripts/drawing_modules/triangle.js")
script(type='text/javascript',
src="/scripts/drawing_modules/line.js")
script(type='text/javascript',
src="/scripts/drawing_modules/polyline.js")
script(type='text/javascript',
src="/scripts/drawing_modules/text.js")
script(type='text/javascript', src="/scripts/ui.js")
script(type='text/javascript', src="/scripts/repository.js")
script(type='text/javascript', src="/scripts/chat.js")
script(type='text/javascript', src="/scripts/socket.js")
script(type='text/javascript', src="/scripts/modifier.js")
78
script(type='text/javascript', src="/scripts/painter.js")
script(type='text/javascript', src="/scripts/index.js")
link(rel='stylesheet',
href='http://fonts.googleapis.com/css?family=Open+Sans')
link(rel='stylesheet', href='/css/bootstrap.min.css')
link(rel='stylesheet', href='/css/bootstrap.colorpicker.css')
link(rel='stylesheet', href='/css/style.css')
title Canva
body
div#merge
table
tr
td Your
Changes
td Last
Checkout
td Their
Changes
tr
td
canvas#yours(width="400", height="200", style="border:1px solid #000000;")
td
canvas#last(width="400", height="200", style="border:1px solid #000000;")
td
canvas#theirs(width="400", height="200", style="border:1px solid #000000;")
tr
td
td
canvas#trash(width="400", height="200", style="border:1px solid #000000;")
79
td
tr
td
td Recycle
Bin
td
button#mergeok ok
button#mergecancel cancel
hr
div#workspace
canvas#demo(width="400", height="200",
style="border:1px solid #000000;")
button#circle Circle
button#ellipse Ellipse
button#rectangle Rectangle
button#triangle Triangle
button#line Line
button#polyline Polyline
button#text Text
a#strokeColor.btn.small(href="#",
data-color-format="hex", data-color="rgb(255, 255, 255)")
Change stroke color
a#fillColor.btn.small(href="#",
data-color-format="hex", data-color="rgb(255, 255, 255)")
Change fill color
input#strokeWidth(type="range",
min="1", max="20")
hr
h1 Branches
div#currentValue
80
div#branches
button#checkOutBranch checkout
button#createBranch create
button#commitBranch commit
button#saveBranch save
button#deleteBranch delete
button#mergeBranch merge
span |
button#block block
button#unblock unblock
hr
h1 Messages
div
div.inlineBlock
span Your name:
input(type="text", value="Anonymous Squirrel")#name
br
form#messageForm
textarea(rows="4", cols="50", placeholder="Share something!",
maxlength=200)#outgoingMessage
input(type="button", value="Share", disabled=true)#send
div.inlineBlock.topAligned
b Participants
br
div#participants
hr
div#messages
81
B.3. Test code
server-repository-spec.js
var Repository = require("../repository");
describe("Repository", function() {
var repo;
beforeEach(function() {
repo = new Repository();
});
describe("block", function() {
it("should trigger error invalid if no data", function() {
var result = repo.block();
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('INVALID');
});
it("should trigger error invalid if no data participant", function() {
var result = repo.block({branch: {name: 'name'}});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('INVALID');
});
it("should trigger error invalid if no data participant id", function() {
var result = repo.block({participant: {}, branch: {name: 'name'}});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('INVALID');
});
it("should trigger error invalid if no data branch", function() {
var result = repo.block({participant: {id: '1'}});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('INVALID');
});
it("should trigger error invalid if no data branch name", function() {
var result = repo.block({participant: {id: '1'}, branch: {}});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('INVALID');
});
it("should trigger error missing if branch name does not exist", function() {
var result = repo.block({participant: {id: '1'}, branch: {name:
'missingName'}});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('MISSING');
});
it("should trigger error missing if branch is inactive", function() {
var branchName = "existingBranch";
82
repo.saveBranch({name: branchName, active: false});
var result = repo.block({participant: {id: '1'}, branch: {name:
branchName}});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('MISSING');
});
it("should trigger ok if no block existed", function() {
var branchName = "existingBranch";
var participantId = 1;
repo.saveBranch({branch: {name: branchName, nodes: [], active: true}});
var result = repo.block({participant: {id: participantId}, branch: {name:
branchName}});
expect(result.status).toEqual('OK');
expect(result.branch.name).toEqual(branchName);
expect(result.token).toBeDefined();
expect(repo.isBlocked(branchName)).toBeTruthy();
});
it("should trigger error if no token", function() {
var branchName = "existingBranch";
var participantId = 1;
var res = repo.saveBranch({branch: {name: branchName, nodes: [], active:
true}});
repo.block({participant: {id: participantId}, branch: {name:
branchName}});
var result = repo.block({participant: {id: participantId}, branch: {name:
branchName}});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('INUSE');
});
it("should trigger error if token is invalid", function() {
var branchName = "existingBranch";
var participantId = 1;
repo.saveBranch({branch: {name: branchName, nodes: [], active: true}});
var blockResponse = repo.block({participant: {id: participantId}, branch:
{name: branchName}});
var result = repo.block({participant: {id: participantId}, branch: {name:
branchName}, token: blockResponse.token + "garbage"});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('INUSE');
});
it("should trigger ok if token is valid", function() {
var branchName = "existingBranch";
var participantId = 1;
repo.saveBranch({branch: {name: branchName, nodes: [], active: true}});
var blockResponse = repo.block({participant: {id: participantId}, branch:
{name: branchName}});
var result = repo.block({participant: {id: participantId}, branch: {name:
branchName}, token: blockResponse.token});
83
expect(result.status).toEqual('OK');
expect(result.branch.name).toEqual(branchName);
expect(result.token).toBeDefined();
expect(repo.isBlocked(branchName)).toBeTruthy();
});
});
describe("unblock", function() {
it("should trigger error invalid if no data", function() {
var result = repo.unblock();
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('INVALID');
});
it("should trigger error invalid if no data participant", function() {
var result = repo.unblock({branch: {name: 'name'}});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('INVALID');
});
it("should trigger error invalid if no data participant id", function() {
var result = repo.unblock({participant: {}, branch: {name: 'name'}});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('INVALID');
});
it("should trigger error invalid if no data branch", function() {
var result = repo.unblock({participant: {id: '1'}});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('INVALID');
});
it("should trigger error invalid if no data branch name", function() {
var result = repo.unblock({participant: {id: '1'}, branch: {}});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('INVALID');
});
it("should trigger error if no token", function() {
var branchName = "existingBranch";
var participantId = 1;
var result = repo.unblock({participant: {id: participantId}, branch:
{name: branchName}});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('FORBIDDEN');
});
it("should trigger error if token is invalid", function() {
var branchName = "existingBranch";
var participantId = 1;
repo.saveBranch({branch: {name: branchName, nodes: [], active: true}});
var blockResponse = repo.block({participant: {id: participantId}, branch:
{name: branchName}});
var result = repo.unblock({participant: {id: participantId}, branch:
84
{name: branchName}, token: blockResponse.token + "garbage"});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('FORBIDDEN');
});
it("should trigger error missing if branch name does not exist", function() {
var branchName = "existingBranch";
var result = repo.unblock({participant: {id: 1}, branch: {name:
'otherName'}, token: 'any'});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('MISSING');
});
it("should trigger error missing if branch is inactive", function() {
var branchName = "existingBranch";
repo.saveBranch({name: branchName, active: false});
var result = repo.unblock({participant: {id: '1'}, branch: {name:
branchName}, token: 'any'});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('MISSING');
});
it("should trigger ok if token is valid", function() {
var branchName = "existingBranch";
var participantId = 1;
repo.saveBranch({branch: {name: branchName, nodes: [], active: true}});
var blockResponse = repo.block({participant: {id: participantId}, branch:
{name: branchName}});
var result = repo.unblock({participant: {id: participantId}, branch:
{name: branchName}, token: blockResponse.token});
expect(result.status).toEqual('OK');
expect(result.branch.name).toEqual(branchName);
expect(repo.isBlocked(branchName)).not.toBeTruthy();
});
});
describe("getActiveBranches", function() {
it("should only return the active branches", function() {
var branchName1 = "bn1";
var branchName2 = "bn2";
var branchName3 = "bn3";
repo.saveBranch({branch: {name: branchName1, nodes: [], active: true}});
repo.saveBranch({branch: {name: branchName2, nodes: [], active:
false}});
repo.saveBranch({branch: {name: branchName3, nodes: [], active:
false}});
var result = repo.getActiveBranches();
// There is an extra master branch created by default!
expect(result.length).toEqual(2);
});
});
85
describe("checkOutBranch", function() {
it("should trigger error invalid if no data", function() {
var result = repo.checkOutBranch();
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('INVALID');
});
it("should trigger error invalid if no data name", function() {
var result = repo.checkOutBranch({});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('INVALID');
});
it("should trigger error missing if branch name does not exist", function() {
var result = repo.checkOutBranch({name: 'missingName'});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('MISSING');
});
it("should trigger error missing if branch is inactive", function() {
var branchName = "existingBranch";
repo.saveBranch({name: branchName, active: false});
var result = repo.checkOutBranch({name: branchName});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('MISSING');
});
it("should trigger ok", function() {
var branchName = "existingBranch";
var participantId = 1;
var branch = {name: branchName, nodes: [{id: 1}], active: true};
repo.saveBranch({branch: branch});
var result = repo.checkOutBranch({name: branchName});
expect(result.status).toEqual('OK');
expect(result.branch).toEqual(branch);
});
});
describe("deleteBranch", function() {
it("should trigger error invalid if no data", function() {
var result = repo.deleteBranch();
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('INVALID');
});
it("should trigger error invalid if no data name", function() {
var result = repo.deleteBranch({});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('INVALID');
});
it("should trigger error missing if branch name does not exist", function() {
var result = repo.deleteBranch({name: 'missingName'});
86
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('MISSING');
});
it("should trigger error missing if branch is inactive", function() {
var branchName = "existingBranch";
repo.saveBranch({name: branchName, active: false});
var result = repo.deleteBranch({name: branchName});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('MISSING');
});
it("should trigger error if branch is master", function() {
var branchName = "existingBranch";
var participantId = 1;
var oldActiveBranches = repo.getActiveBranches().length;
var result = repo.deleteBranch({name: 'master'});
var newActiveBranches = repo.getActiveBranches().length;
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('MASTER');
});
it("should trigger ok", function() {
var branchName = "existingBranch";
var participantId = 1;
repo.saveBranch({branch: {name: branchName, nodes: [], active: true}});
var oldActiveBranches = repo.getActiveBranches().length;
var result = repo.deleteBranch({name: branchName});
var newActiveBranches = repo.getActiveBranches().length;
expect(result.status).toEqual('OK');
expect(oldActiveBranches - newActiveBranches).toEqual(1);
});
});
describe("saveBranch", function() {
it("should trigger error invalid if no data", function() {
var result = repo.saveBranch();
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('INVALID');
});
it("should trigger error invalid if no data branch", function() {
var result = repo.saveBranch({participant: {id: '1'}});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('INVALID');
});
it("should trigger error invalid if no data branch name", function() {
var result = repo.saveBranch({participant: {id: '1'}, branch: {}});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('INVALID');
});
87
it("should trigger error missing if the branch exists but it is inactive",
function() {
var branchName = 'branchName';
repo.saveBranch({participant: {id: '1'}, branch: {name: branchName,
nodes: [], active: false}});
var result = repo.saveBranch({participant: {id: '1'}, branch: {name:
branchName, nodes: []}});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('MISSING');
});
it("should trigger error forbidden if the branch is blocked and there is no
token", function() {
var branchName = 'branchName';
var participantId = 1;
repo.saveBranch({participant: {id: participantId}, branch: {name:
branchName, nodes: [], active: true}});
repo.block({participant: {id: participantId}, branch: {name:
branchName}});
var result = repo.saveBranch({participant: {id: participantId}, branch:
{name: branchName, nodes: []}});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('FORBIDDEN');
});
it("should trigger error forbidden if the branch is blocked and the token is
invalid", function() {
var branchName = 'branchName';
var participantId = 1;
repo.saveBranch({participant: {id: participantId}, branch: {name:
branchName, nodes: [], active: true}});
var blockResponse = repo.block({participant: {id: participantId}, branch:
{name: branchName}});
var result = repo.saveBranch({participant: {id: participantId}, branch:
{name: branchName, nodes: []}, token: blockResponse.token + "garbage"});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('FORBIDDEN');
});
it("should trigger error client head if the client branch has no nodes",
function() {
var branchName = 'branchName';
var participantId = 1;
repo.saveBranch({participant: {id: participantId}, branch: {name:
branchName, nodes: [], active: true}});
var result = repo.saveBranch({participant: {id: participantId}, branch:
{name: branchName, nodes: []}});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('CLIENTHEAD');
});
it("should trigger error server head if the client branch has no nodes",
function() {
88
var branchName = 'branchName';
var participantId = 1;
repo.saveBranch({participant: {id: participantId}, branch: {name:
branchName, nodes: [], active: true}});
var result = repo.saveBranch({participant: {id: participantId}, branch:
{name: branchName, nodes: ['some node representation']}});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('SERVERHEAD');
});
it("should trigger error conflict if both client and server are ahead and client
has more", function() {
var branchName = 'branchName';
var participantId = 1;
repo.saveBranch({participant: {id: participantId}, branch: {name:
branchName, nodes: [{id: 1}, {id: 2}, {id: 3}], active: true}});
var result = repo.saveBranch({participant: {id: participantId}, branch:
{name: branchName, nodes: [{id: 1}, {id: 2}, {id: 5}, {id: 7}]}});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('CONFLICT');
});
it("should trigger error conflict if both client and server are ahead and server
has more", function() {
var branchName = 'branchName';
var participantId = 1;
repo.saveBranch({participant: {id: participantId}, branch: {name:
branchName, nodes: [{id: 1}, {id: 2}, {id: 5}, {id: 7}], active: true}});
var result = repo.saveBranch({participant: {id: participantId}, branch:
{name: branchName, nodes: [{id: 1}, {id: 2}, {id: 3}]}});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('CONFLICT');
});
it("should trigger error conflict if both client and server are ahead and server
has more", function() {
var branchName = 'branchName';
var participantId = 1;
repo.saveBranch({participant: {id: participantId}, branch: {name:
branchName, nodes: [{id: 1}, {id: 2}, {id: 5}, {id: 7}], active: true}});
var result = repo.saveBranch({participant: {id: participantId}, branch:
{name: branchName, nodes: [{id: 1}, {id: 2}, {id: 3}, {id: 4}]}});
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('CONFLICT');
});
it("should trigger ok saved if the client is ahead of the server", function() {
var branchName = 'branchName';
var participantId = 1;
repo.saveBranch({participant: {id: participantId}, branch: {name:
branchName, nodes: [{id: 1}, {id: 2}, {id: 3}], active: true}});
var result = repo.saveBranch({participant: {id: participantId}, branch:
{name: branchName, nodes: [{id: 1}, {id: 2}, {id: 3}, {id: 4}]}});
89
expect(result.status).toEqual('OK');
expect(result.situation).toEqual('SAVED');
});
it("should trigger warning up to date if the server is ahead of the client",
function() {
var branchName = 'branchName';
var participantId = 1;
repo.saveBranch({participant: {id: participantId}, branch: {name:
branchName, nodes: [{id: 1}, {id: 2}, {id: 3}, {id: 4}], active: true}});
var result = repo.saveBranch({participant: {id: participantId}, branch:
{name: branchName, nodes: [{id: 1}, {id: 2}, {id: 3}]}});
expect(result.status).toEqual('WARNING');
expect(result.situation).toEqual('UPTODATE');
});
it("should trigger warning up to date if none of the server andclientisahead",
function() {
var branchName = 'branchName';
var participantId = 1;
repo.saveBranch({participant: {id: participantId}, branch: {name: branchName,
nodes: [{id: 1}, {id: 2}, {id: 3}], active: true}});
var result = repo.saveBranch({participant: {id: participantId}, branch:
{name: branchName, nodes: [{id: 1}, {id: 2}, {id: 3}]}});
expect(result.status).toEqual('WARNING');
expect(result.situation).toEqual('UPTODATE');
});
it("should trigger ok if the branch did not exist", function() {
var oldActiveBranches = repo.getActiveBranches().length;
var result = repo.saveBranch({participant: {id: '1'}, branch: {name:
'newName', nodes: [], active: true}});
var newActiveBranches = repo.getActiveBranches().length;
expect(result.status).toEqual('OK');
expect(result.situation).toEqual('CREATED');
expect(newActiveBranches - oldActiveBranches).toEqual(1);
});
});
});
server-chat-spec.js
var Chat = require("../chat");
describe("Chat", function() {
var chat;
beforeEach(function() {
chat = new Chat();
});
describe("newUser", function() {
it("should trigger ok", function() {
var data = {id: 1, name: "John Doe"};
90
var result = chat.newUser(data);
expect(result.status).toEqual('OK');
expect(result.participants.length).toEqual(1);
expect(result.participants[0]).toEqual(data);
});
});
describe("nameChange", function() {
it("should trigger error missing if no participant", function() {
var data = {id: 1, name: "John Doe"};
var result = chat.nameChange(data, 1);
expect(result.status).toEqual('ERROR');
expect(result.situation).toEqual('MISSING');
});
it("should trigger ok if participant exists", function() {
var data = {id: 1, name: "John Doe"};
chat.newUser(data);
var result = chat.nameChange(data, 1);
expect(result.status).toEqual('OK');
});
});
describe("disconnect", function() {
it("should trigger ok", function() {
var data = {id: 1, name: "John Doe"};
chat.newUser(data);
var result1 = chat.disconnect(data, 1);
var result2 = chat.disconnect(data, 2);
expect(result1.status).toEqual('OK');
expect(result2.status).toEqual('OK');
});
});
});
Appendix C
C.1. A passing build The steps consist of:
1. the environment is exported
2. the master branch of the Bitbucket repository is cloned
3. the system moves to the working directory
4. the latest version of the master branch is checked out
5. the dependency cache is prepared
6. the virtual machine is prepared
7. the test framework is loaded as an external dependency
8. the tests are run in the spec folder
9. the Heroku app key is fetched
91
10. the access to Heroku is checked
11. the Heroku repository is added for push
12. the contents of the the Bitbucket branch are pushed to the Heroku repository
13. the Heroku application endpoint is checked
92
93
C.2. A failing build This build failed due to not being able to push to the Heroku repository. If one step
of the build fails, all subsequent steps fail, and the build itself fails.
94
C.3. The latest build and deploy runs This is an overview of the build and deploy status of the project.
95
96
Recommended