meangirls 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +3 -0
- data/README.markdown +243 -0
- data/Rakefile +1 -0
- data/lib/meangirls.rb +58 -0
- data/lib/meangirls/counter.rb +33 -0
- data/lib/meangirls/crdt.rb +15 -0
- data/lib/meangirls/g_counter.rb +82 -0
- data/lib/meangirls/lww_set.rb +168 -0
- data/lib/meangirls/or_set.rb +134 -0
- data/lib/meangirls/set.rb +63 -0
- data/lib/meangirls/two_phase_set.rb +92 -0
- data/lib/meangirls/version.rb +3 -0
- data/meangirls.gemspec +24 -0
- data/spec/counter.rb +58 -0
- data/spec/crdt.rb +16 -0
- data/spec/g_counter.rb +14 -0
- data/spec/init.rb +9 -0
- data/spec/lww_set.rb +65 -0
- data/spec/or_set.rb +41 -0
- data/spec/prob.rb +93 -0
- data/spec/run +21 -0
- data/spec/set.rb +186 -0
- data/spec/two_phase_set.rb +37 -0
- metadata +159 -0
@@ -0,0 +1,168 @@
|
|
1
|
+
class Meangirls::LWWSet < Meangirls::Set
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
class Pair
|
5
|
+
attr_accessor :add, :remove
|
6
|
+
def initialize(add, remove)
|
7
|
+
@add = add
|
8
|
+
@remove = remove
|
9
|
+
end
|
10
|
+
|
11
|
+
def ==(o)
|
12
|
+
o.kind_of? Pair and
|
13
|
+
add == o.add and
|
14
|
+
remove == o.remove
|
15
|
+
end
|
16
|
+
|
17
|
+
def exists_a?
|
18
|
+
return false unless @add
|
19
|
+
return true unless @remove
|
20
|
+
@add >= @remove
|
21
|
+
end
|
22
|
+
|
23
|
+
def exists_r?
|
24
|
+
return false unless @add
|
25
|
+
return true unless @remove
|
26
|
+
@add > @remove
|
27
|
+
end
|
28
|
+
|
29
|
+
def inspect
|
30
|
+
"(#{add.inspect}, #{remove.inspect})"
|
31
|
+
end
|
32
|
+
|
33
|
+
# Merge with another pair, taking the largest add and largest delete stamp.
|
34
|
+
def merge(other)
|
35
|
+
unless other
|
36
|
+
return clone
|
37
|
+
end
|
38
|
+
|
39
|
+
Pair.new(
|
40
|
+
[add, other.add].compact.max,
|
41
|
+
[remove, other.remove].compact.max
|
42
|
+
)
|
43
|
+
end
|
44
|
+
alias | merge
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.biases
|
48
|
+
['a', 'r']
|
49
|
+
end
|
50
|
+
|
51
|
+
attr_accessor :e
|
52
|
+
attr_accessor :bias
|
53
|
+
def initialize(hash = nil)
|
54
|
+
@e = {}
|
55
|
+
@bias = 'a'
|
56
|
+
|
57
|
+
if hash
|
58
|
+
raise ArgumentError, "hash must contain e" unless hash['e']
|
59
|
+
@bias = hash['bias'] if hash['bias']
|
60
|
+
hash['e'].each do |list|
|
61
|
+
element, add, delete = list
|
62
|
+
@e[element] = Pair.new(add, delete).merge(@e[element])
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Inserts e into the set with a default generated timestamp.
|
68
|
+
# Your clocks ARE synchronized, right? WRONG!
|
69
|
+
def <<(e)
|
70
|
+
add(e, timestamp)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Strict equality: both adds and removes are equal.
|
74
|
+
def ==(other)
|
75
|
+
other.kind_of? self.class and
|
76
|
+
e == other.e
|
77
|
+
end
|
78
|
+
|
79
|
+
# Add e, with an optional timestamp.
|
80
|
+
def add(e, time = timestamp)
|
81
|
+
merge_element! e, Pair.new(time, nil)
|
82
|
+
self
|
83
|
+
end
|
84
|
+
|
85
|
+
def as_json
|
86
|
+
{
|
87
|
+
'type' => type,
|
88
|
+
'e' => @e.map { |e, pair|
|
89
|
+
[e, pair.add, pair.remove]
|
90
|
+
}
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
def clone
|
95
|
+
c = super
|
96
|
+
c.e = e.clone
|
97
|
+
c
|
98
|
+
end
|
99
|
+
|
100
|
+
# Delete e from self, with optional timestamp.
|
101
|
+
def delete(e, time = timestamp)
|
102
|
+
merge_element! e, Pair.new(nil, time)
|
103
|
+
e
|
104
|
+
end
|
105
|
+
|
106
|
+
# Is e an element of the set?
|
107
|
+
def include?(e)
|
108
|
+
return false unless pair = @e[e]
|
109
|
+
case bias
|
110
|
+
when 'a'
|
111
|
+
pair.exists_a?
|
112
|
+
when 'r'
|
113
|
+
pair.exists_r?
|
114
|
+
else
|
115
|
+
raise RuntimeError, "unsupported bias #{bias.inspect}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Merge with another lww-set
|
120
|
+
def merge(other)
|
121
|
+
unless other.kind_of? self.class
|
122
|
+
raise ArgumentError, "other must be a #{self.class}"
|
123
|
+
end
|
124
|
+
|
125
|
+
c = clone
|
126
|
+
other.e.each do |element, pair|
|
127
|
+
c.merge_element!(element, pair)
|
128
|
+
end
|
129
|
+
c
|
130
|
+
end
|
131
|
+
|
132
|
+
# Mutates self to update the value for e with the given pair.
|
133
|
+
def merge_element!(e, pair)
|
134
|
+
if cur = @e[e]
|
135
|
+
@e[e] = cur | pair
|
136
|
+
else
|
137
|
+
@e[e] = pair
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def to_set
|
142
|
+
case bias
|
143
|
+
when 'a'
|
144
|
+
@e.inject(Set.new) do |s, l|
|
145
|
+
element, pair = l
|
146
|
+
s << element if pair.exists_a?
|
147
|
+
s
|
148
|
+
end
|
149
|
+
when 'r'
|
150
|
+
@e.inject(Set.new) do |s, l|
|
151
|
+
element, pair = l
|
152
|
+
s << element if pair.exists_r?
|
153
|
+
s
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Default timestamps are the present time in UTC as ISO 8601 strings, WITH a
|
159
|
+
# seconds fraction guaranteed to be unique and monotonically increasing for
|
160
|
+
# all use of Meangirls.
|
161
|
+
def timestamp
|
162
|
+
Meangirls.timestamp
|
163
|
+
end
|
164
|
+
|
165
|
+
def type
|
166
|
+
'lww-set'
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
class Meangirls::ORSet < Meangirls::Set
|
2
|
+
class Pair
|
3
|
+
attr_accessor :adds, :removes
|
4
|
+
def initialize(adds = [], removes = [])
|
5
|
+
@adds = adds
|
6
|
+
@removes = removes
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_s
|
10
|
+
inspect
|
11
|
+
end
|
12
|
+
|
13
|
+
def inspect
|
14
|
+
"(#{adds.inspect}, #{removes.inspect})"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.biases
|
19
|
+
['a']
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_accessor :e
|
23
|
+
def initialize(hash = nil)
|
24
|
+
@e = {}
|
25
|
+
|
26
|
+
if hash
|
27
|
+
raise ArgumentError, 'hash must contain e' unless hash['e']
|
28
|
+
hash['e'].each do |list|
|
29
|
+
element, adds, removes = list
|
30
|
+
merge_internal! element, Pair.new(adds, removes)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Inserts e into the set.
|
36
|
+
def <<(e)
|
37
|
+
add e
|
38
|
+
end
|
39
|
+
|
40
|
+
# Strict equality: all adds/removes match for every element.
|
41
|
+
# TODO: slow
|
42
|
+
def ==(other)
|
43
|
+
other.kind_of? self.class and
|
44
|
+
(@e.keys | other.e.keys).all? do |e, pair|
|
45
|
+
a = @e[e] and b = other.e[e] and
|
46
|
+
uaeq(a.adds, b.adds) and
|
47
|
+
uaeq(a.removes, b.removes)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Inserts e into the set. Tag will be randomly generated if not given.
|
52
|
+
def add(e, tag = Meangirls.tag)
|
53
|
+
pair = (@e[e] ||= Pair.new)
|
54
|
+
pair.adds |= [tag]
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
def as_json
|
59
|
+
{
|
60
|
+
'type' => type,
|
61
|
+
'e' => @e.map do |e, pair|
|
62
|
+
[e, pair.adds, pair.removes]
|
63
|
+
end
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
def bias
|
68
|
+
'a'
|
69
|
+
end
|
70
|
+
|
71
|
+
# UGH defensive copying
|
72
|
+
def clone
|
73
|
+
c = super
|
74
|
+
c.e = {}
|
75
|
+
@e.each do |e, pair|
|
76
|
+
c.merge_internal! e, pair.clone
|
77
|
+
end
|
78
|
+
c
|
79
|
+
end
|
80
|
+
|
81
|
+
# Deletes e from self by cancelling all known tags (or a specific tag if
|
82
|
+
# given.) Returns nil if no changes, e otherwise.
|
83
|
+
def delete(e, tag = nil)
|
84
|
+
pair = @e[e] or return
|
85
|
+
new = pair.adds - pair.removes
|
86
|
+
return if new.empty?
|
87
|
+
pair.removes += new
|
88
|
+
e
|
89
|
+
end
|
90
|
+
|
91
|
+
# Merge with another OR-Set and return the merged copy.
|
92
|
+
def merge(other)
|
93
|
+
unless other.kind_of? self.class
|
94
|
+
raise ArgumentError, "other must be a #{self.class}"
|
95
|
+
end
|
96
|
+
|
97
|
+
copy = clone
|
98
|
+
@e.each do |e, pair|
|
99
|
+
copy.merge_internal! e, pair
|
100
|
+
end
|
101
|
+
other.e.each do |e, pair|
|
102
|
+
copy.merge_internal! e, pair
|
103
|
+
end
|
104
|
+
copy
|
105
|
+
end
|
106
|
+
|
107
|
+
# Updates self with new adds and removes for an element.
|
108
|
+
def merge_internal!(element, pair)
|
109
|
+
if my = @e[element]
|
110
|
+
my.adds |= pair.adds
|
111
|
+
my.removes |= pair.removes
|
112
|
+
else
|
113
|
+
@e[element] = pair
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def to_set
|
118
|
+
s = Set.new
|
119
|
+
@e.each do |element, pair|
|
120
|
+
s << element unless (pair.adds - pair.removes).empty?
|
121
|
+
end
|
122
|
+
s
|
123
|
+
end
|
124
|
+
|
125
|
+
def type
|
126
|
+
'or-set'
|
127
|
+
end
|
128
|
+
|
129
|
+
# Unordered array equality
|
130
|
+
# TODO: slow
|
131
|
+
def uaeq(a, b)
|
132
|
+
(a - b).empty? and (b - a).empty?
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
class Meangirls::Set < Meangirls::CRDT
|
2
|
+
require 'meangirls/two_phase_set'
|
3
|
+
require 'meangirls/lww_set'
|
4
|
+
require 'meangirls/or_set'
|
5
|
+
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
# Add all elements of other to a copy of self.
|
9
|
+
def |(other)
|
10
|
+
other.inject(clone) do |copy, e|
|
11
|
+
copy << e
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Return a copy of self where [all elements not present in other] have been
|
16
|
+
# deleted.
|
17
|
+
def &(other)
|
18
|
+
(to_set - other.to_set).inject(clone) do |copy, e|
|
19
|
+
copy.delete e
|
20
|
+
copy
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Add all elements of other to a copy of self.
|
25
|
+
def +(other)
|
26
|
+
other.inject(clone) do |copy, e|
|
27
|
+
copy << e
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Remove all elements of other from a copy of self.
|
32
|
+
def -(other)
|
33
|
+
other.inject(clone) do |copy, e|
|
34
|
+
copy.delete e
|
35
|
+
copy
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Loose equality: present elements of each set are equal
|
40
|
+
def ===(other)
|
41
|
+
to_set == other.to_set
|
42
|
+
end
|
43
|
+
|
44
|
+
# Iterate over all present elements
|
45
|
+
def each(&block)
|
46
|
+
to_set.each(&block)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Are there no elements present?
|
50
|
+
def empty?
|
51
|
+
size == 0
|
52
|
+
end
|
53
|
+
|
54
|
+
# How many elements are present in the set?
|
55
|
+
def size
|
56
|
+
to_set.size
|
57
|
+
end
|
58
|
+
|
59
|
+
# Convert to an array
|
60
|
+
def to_a
|
61
|
+
to_set.to_a
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
class Meangirls::TwoPhaseSet < Meangirls::Set
|
2
|
+
def self.biases
|
3
|
+
['r']
|
4
|
+
end
|
5
|
+
|
6
|
+
attr_accessor :a, :r
|
7
|
+
def initialize(hash = nil)
|
8
|
+
if hash
|
9
|
+
raise ArgumentError, "hash must contain a" unless hash['a']
|
10
|
+
raise ArgumentError, "hash must contain r" unless hash['r']
|
11
|
+
|
12
|
+
@a = Set.new hash['a']
|
13
|
+
@r = Set.new hash['r']
|
14
|
+
else
|
15
|
+
# Empty set
|
16
|
+
@a = Set.new
|
17
|
+
@r = Set.new
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Inserts e into the set. Raises ReinsertNotAllowed if e was previously
|
22
|
+
# deleted.
|
23
|
+
def <<(e)
|
24
|
+
if @r.include? e
|
25
|
+
raise Meangirls::ReinsertNotAllowed
|
26
|
+
end
|
27
|
+
|
28
|
+
@a << e
|
29
|
+
self
|
30
|
+
end
|
31
|
+
alias add <<
|
32
|
+
|
33
|
+
# Strict equality: both adds and removes for both 2p-sets are equal.
|
34
|
+
def ==(other)
|
35
|
+
other.kind_of? self.class and
|
36
|
+
a == other.a and
|
37
|
+
r == other.r
|
38
|
+
end
|
39
|
+
|
40
|
+
def as_json
|
41
|
+
{
|
42
|
+
'type' => type,
|
43
|
+
'a' => a.to_a,
|
44
|
+
'r' => r.to_a
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
def bias
|
49
|
+
'r'
|
50
|
+
end
|
51
|
+
|
52
|
+
def clone
|
53
|
+
c = super
|
54
|
+
c.a = a.clone
|
55
|
+
c.r = r.clone
|
56
|
+
c
|
57
|
+
end
|
58
|
+
|
59
|
+
# Deletes e from self. Raises DeleteNotAllowed if e does not presently
|
60
|
+
# exist.
|
61
|
+
def delete(e)
|
62
|
+
unless @a.include? e
|
63
|
+
raise Meangirls::DeleteNotAllowed
|
64
|
+
end
|
65
|
+
@r << e
|
66
|
+
e
|
67
|
+
end
|
68
|
+
|
69
|
+
# Merge with another 2p-set.
|
70
|
+
def merge(other)
|
71
|
+
unless other.kind_of? self.class
|
72
|
+
raise ArgumentError, "other must be a #{self.class}"
|
73
|
+
end
|
74
|
+
|
75
|
+
self.class.new(
|
76
|
+
'a' => (a | other.a),
|
77
|
+
'r' => (r | other.r)
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
def include?(e)
|
82
|
+
@a.include? e and not @r.include? e
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_set
|
86
|
+
@a - @r
|
87
|
+
end
|
88
|
+
|
89
|
+
def type
|
90
|
+
'2p-set'
|
91
|
+
end
|
92
|
+
end
|