Skip to content
11 changes: 11 additions & 0 deletions assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,3 +277,14 @@ return `
</div>
`
}

// Check if local CRDT consistent with server CRDT
var check_btn = document.getElementById("check_btn");
check_btn.onclick = function() {
channel.push("check", {
value: crdt.toString()
}).receive("ok", resp => {
console.log(`Response: ${resp["flag"]}`)
})
}

5 changes: 4 additions & 1 deletion lib/codeshare/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ defmodule Codeshare.Application do
CodeshareWeb.Endpoint,
# Starts a worker by calling: Codeshare.Worker.start_link(arg)
# {Codeshare.Worker, arg},
CodeshareWeb.Presence
# Start Presence
CodeshareWeb.Presence,
# Start CRDT Supervisor
Codeshare.CRDT.Supervisor
]

# See https://hexdocs.pm/elixir/Supervisor.html
Expand Down
170 changes: 170 additions & 0 deletions lib/codeshare/crdt/character.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
defmodule Codeshare.Identifier do
@moduledoc """
List of identifiers define the 'position'
of a character in CRDT
"""
alias __MODULE__

defstruct [
position: 0,
siteID: -1
]

@doc """
Helper to convert payload from channel
to struct
"""
def to_struct(identifier_map) do
map = identifier_map
%Identifier{
position: map["position"],
siteID: map["siteID"]
}
end

@doc """
Convert to string representation
"""
def to_string(identifier) do
# Client and server have diff infinity values!
if identifier.siteID == 16777216 do
"[#{identifier.position}, Infinity]"
else
"[#{identifier.position}, #{identifier.siteID}]"
end
end

@doc """
Check if two identifiers are equal
"""
def is_equal(id1, id2) do
id1.position == id2.position && id1.siteID == id2.siteID
end

@doc """
is `id1` > `id2` ? [Identifiers]
"""
def is_greater(id1, id2) do
if id1.position == id2.position do
id1.siteID > id2.siteID
else
id1.position > id2.position
end
end

# TODO: Add struct validation?
end

defmodule Codeshare.Character do
@moduledoc """
Represensts each 'character' in
the CRDT text
"""
alias __MODULE__
alias Codeshare.Identifier

defstruct [
ch: "",
identifiers: [%Identifier{}]
]

@doc """
Helper to convert payload from channel
to struct
"""
def to_struct(character_map) do
map = character_map
%Character{
ch: map["ch"],
identifiers: to_identifiers_struct_list(map["identifiers"])
}
end

@doc """
Convert to string representation
"""
def to_string(character) do
"{#{character.ch}: [#{to_string_id_list(character.identifiers)}]}"
end

@doc """
Check if two characters are equal
"""
def is_equal(character1, character2) do
if character1.ch != character2.ch ||
length(character1.identifiers) != length(character2.identifiers) do
false
else
is_identifier_list_equal(character1.identifiers,
character2.identifiers)
end
end

@doc """
is `character1` > `character2` ?
"""
def is_greater(character1, character2) do
is_identifier_list_greater(character1.identifiers,
character2.identifiers)
end

# Helper functions

defp to_identifiers_struct_list(identifiers_map_list) do
if identifiers_map_list == [] do
[]
else
[h | t] = identifiers_map_list
[Identifier.to_struct(h) | to_identifiers_struct_list(t)]
end
end

defp is_identifier_list_equal(id_list1, id_list2) do
if id_list1 == [] && id_list2 == [] do
true
else
[id1 | id_list1] = id_list1
[id2 | id_list2] = id_list2

if Identifier.is_equal(id1, id2) do
is_identifier_list_equal(id_list1, id_list2)
else
false
end
end
end

defp is_identifier_list_greater(id_list1, id_list2) do

cond do
id_list1 == [] ->
false
id_list2 == [] ->
true
true ->
[id1 | id_list1] = id_list1
[id2 | id_list2] = id_list2

cond do
Identifier.is_greater(id1, id2) ->
true
Identifier.is_greater(id2, id1) ->
false
Identifier.is_equal(id1, id2) ->
is_identifier_list_greater(id_list1, id_list2)
end
end

end

defp to_string_id_list(id_list) do
[first_id | id_list] = id_list
if id_list == [] do
"#{Identifier.to_string(first_id)}"
else
"#{Identifier.to_string(first_id)}, #{to_string_id_list(id_list)}"
end
end

# TODO: Add struct validation?
end
196 changes: 196 additions & 0 deletions lib/codeshare/crdt/crdt.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
defmodule Codeshare.CRDT do
@moduledoc """
Manages the server-side CRDT
"""
use Agent
alias Codeshare.{Character, Identifier}

# [] arg required by supervisor
def start_link(_opts) do
fn -> [[
%Character{
ch: "",
identifiers: [%Identifier{
position: 0,
siteID: -1
}]
},
%Character{
ch: "",
identifiers: [%Identifier{
position: 1,
siteID: 16777216 #TODO: Infinity for now; think of something
}]
}
]]
end |> Agent.start_link()
# TODO: Registered with module name, which doesn't allow server crdt per session
# (It allows for only one server crdt process)
# Need another process to map session id with corresponding server crdt pid
end

@doc """
Put payload data recived from channel
into CRDT
"""
def put(crdt, payload) do
character = Character.to_struct(payload["character"])
case Map.get(payload, "type") do
"input" ->
remote_insert(crdt, character)
"delete" ->
remote_delete(crdt, character)
"inputnewline" ->
remote_insert_newline(crdt, character)
"deletenewline" ->
remote_delete_newline(crdt, character)
end
end

@doc """
Get CRDT data
"""
def get(crdt) do
Agent.get(crdt, & &1)
end

@doc """
Get CRDT string representation
"""
def get_string(crdt) do
Agent.get(crdt, & convert_to_string(&1))
end

def stop(crdt) do
Agent.stop(crdt)
end

# Helper functions

defp remote_insert(crdt, character) do
Agent.update(crdt, fn crdt -> insert_character(crdt, character) end)
end

defp remote_delete(crdt, character) do
Agent.update(crdt, fn crdt -> delete_character(crdt, character) end)
end

defp remote_insert_newline(crdt, character) do
Agent.update(crdt, fn crdt -> insert_newline(crdt, character) end)
end

defp remote_delete_newline(crdt, character) do
Agent.update(crdt, fn crdt -> delete_newline(crdt, character) end)
end

defp insert_character(crdt, character) do

[first_line | crdt] = crdt
last_ch = List.last(first_line) # NOTE: Takes linear time :(

if Character.is_greater(last_ch, character) do
[insert_character_on_line(first_line, character) | crdt]
else
[first_line | insert_character(crdt, character)]
end
end

defp insert_character_on_line(line, character) do

[first_ch | line] = line

if Character.is_greater(first_ch, character) do
[character | [ first_ch | line] ]
else
[first_ch | insert_character_on_line(line, character)]
end
end

defp delete_character(crdt, character) do

[first_line | crdt] = crdt
last_ch = List.last(first_line) # NOTE: Takes linear time :(

if Character.is_greater(last_ch, character) do
[delete_character_on_line(first_line, character) | crdt]
else
[first_line | delete_character(crdt, character)]
end
end

defp delete_character_on_line(line, character) do

[first_ch | line] = line

if Character.is_equal(first_ch, character) do
line
else
[first_ch | delete_character_on_line(line, character)]
end
end

defp insert_newline(crdt, character) do

[first_line | crdt] = crdt
last_ch = List.last(first_line) # NOTE: Takes linear time :(

if Character.is_greater(last_ch, character) do
insert_newline_on_line(first_line, character) ++ crdt
else
[first_line | insert_newline(crdt, character)]
end
end

defp insert_newline_on_line(line, character) do

[first_ch | line] = line

if Character.is_greater(first_ch, character) do
delimeter_ch = %Character{
ch: "",
identifiers: character.identifiers
}
line1 = [delimeter_ch]
line2 = [delimeter_ch | [first_ch | line] ]
[line1, line2]
else
lines = insert_newline_on_line(line, character)
[line1 | [line2]] = lines
line1 = [first_ch | line1]
[line1 | [line2]]
end
end

defp delete_newline(crdt, character) do

[first_line | crdt] = crdt
last_ch = List.last(first_line) # NOTE: Takes linear time :(

if Character.is_equal(last_ch, character) do
line1 = first_line |> Enum.reverse |> tl |> Enum.reverse
[[_ | line2] | crdt] = crdt
[line1++line2 | crdt]
else
[first_line | delete_newline(crdt, character)]
end
end

defp convert_to_string(crdt) do
if crdt == [] do
""
else
[first_line | crdt] = crdt
"#{convert_line_to_string(first_line)}\n#{convert_to_string(crdt)}"
end
end

defp convert_line_to_string(line) do
[first_ch | line] = line
if line == [] do
"#{Character.to_string(first_ch)}"
else
"#{Character.to_string(first_ch)}, #{convert_line_to_string(line)}"
end
end

end
Loading