meangirls 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|