strokedb 0.0.2

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.
Files changed (59) hide show
  1. data/CONTRIBUTORS +7 -0
  2. data/CREDITS +13 -0
  3. data/README +44 -0
  4. data/bin/sdbc +2 -0
  5. data/lib/config/config.rb +161 -0
  6. data/lib/data_structures/inverted_list.rb +297 -0
  7. data/lib/data_structures/point_query.rb +24 -0
  8. data/lib/data_structures/skiplist.rb +302 -0
  9. data/lib/document/associations.rb +107 -0
  10. data/lib/document/callback.rb +11 -0
  11. data/lib/document/coercions.rb +57 -0
  12. data/lib/document/delete.rb +28 -0
  13. data/lib/document/document.rb +684 -0
  14. data/lib/document/meta.rb +261 -0
  15. data/lib/document/slot.rb +199 -0
  16. data/lib/document/util.rb +27 -0
  17. data/lib/document/validations.rb +704 -0
  18. data/lib/document/versions.rb +106 -0
  19. data/lib/document/virtualize.rb +82 -0
  20. data/lib/init.rb +57 -0
  21. data/lib/stores/chainable_storage.rb +57 -0
  22. data/lib/stores/inverted_list_index/inverted_list_file_storage.rb +56 -0
  23. data/lib/stores/inverted_list_index/inverted_list_index.rb +49 -0
  24. data/lib/stores/remote_store.rb +172 -0
  25. data/lib/stores/skiplist_store/chunk.rb +119 -0
  26. data/lib/stores/skiplist_store/chunk_storage.rb +21 -0
  27. data/lib/stores/skiplist_store/file_chunk_storage.rb +44 -0
  28. data/lib/stores/skiplist_store/memory_chunk_storage.rb +37 -0
  29. data/lib/stores/skiplist_store/skiplist_store.rb +217 -0
  30. data/lib/stores/store.rb +5 -0
  31. data/lib/sync/chain_sync.rb +38 -0
  32. data/lib/sync/diff.rb +126 -0
  33. data/lib/sync/lamport_timestamp.rb +81 -0
  34. data/lib/sync/store_sync.rb +79 -0
  35. data/lib/sync/stroke_diff/array.rb +102 -0
  36. data/lib/sync/stroke_diff/default.rb +21 -0
  37. data/lib/sync/stroke_diff/hash.rb +186 -0
  38. data/lib/sync/stroke_diff/string.rb +116 -0
  39. data/lib/sync/stroke_diff/stroke_diff.rb +9 -0
  40. data/lib/util/blankslate.rb +42 -0
  41. data/lib/util/ext/blank.rb +50 -0
  42. data/lib/util/ext/enumerable.rb +36 -0
  43. data/lib/util/ext/fixnum.rb +16 -0
  44. data/lib/util/ext/hash.rb +22 -0
  45. data/lib/util/ext/object.rb +8 -0
  46. data/lib/util/ext/string.rb +35 -0
  47. data/lib/util/inflect.rb +217 -0
  48. data/lib/util/java_util.rb +9 -0
  49. data/lib/util/lazy_array.rb +54 -0
  50. data/lib/util/lazy_mapping_array.rb +64 -0
  51. data/lib/util/lazy_mapping_hash.rb +46 -0
  52. data/lib/util/serialization.rb +29 -0
  53. data/lib/util/trigger_partition.rb +136 -0
  54. data/lib/util/util.rb +38 -0
  55. data/lib/util/xml.rb +6 -0
  56. data/lib/view/view.rb +55 -0
  57. data/script/console +70 -0
  58. data/strokedb.rb +75 -0
  59. metadata +148 -0
@@ -0,0 +1,21 @@
1
+ module StrokeDB
2
+ class ChunkStorage
3
+ include ChainableStorage
4
+
5
+ attr_accessor :authoritative_source
6
+
7
+ def initialize(opts={})
8
+ end
9
+
10
+ def find(uuid)
11
+ unless result = read(chunk_path(uuid))
12
+ if authoritative_source
13
+ result = authoritative_source.find(uuid)
14
+ save!(result, authoritative_source) if result
15
+ end
16
+ end
17
+ result
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,44 @@
1
+ module StrokeDB
2
+ class FileChunkStorage < ChunkStorage
3
+ attr_accessor :path
4
+
5
+ def initialize(opts={})
6
+ opts = opts.stringify_keys
7
+ @path = opts['path']
8
+ end
9
+
10
+ def delete!(chunk_uuid)
11
+ FileUtils.rm_rf(chunk_path(chunk_uuid))
12
+ end
13
+
14
+ def clear!
15
+ FileUtils.rm_rf @path
16
+ end
17
+
18
+ private
19
+
20
+ def perform_save!(chunk)
21
+ FileUtils.mkdir_p @path
22
+ write(chunk_path(chunk.uuid), chunk)
23
+ end
24
+
25
+ def read(path)
26
+ return nil unless File.exist?(path)
27
+ raw_chunk = StrokeDB.deserialize(IO.read(path))
28
+ Chunk.from_raw(raw_chunk) do |chunk|
29
+ chunk.next_chunk = find(chunk.next_chunk_uuid) if chunk.next_chunk_uuid
30
+ end
31
+ end
32
+
33
+ def write(path, chunk)
34
+ File.open path, "w+" do |f|
35
+ f.write StrokeDB.serialize(chunk.to_raw)
36
+ end
37
+ end
38
+
39
+ def chunk_path(uuid)
40
+ "#{@path}/#{uuid}"
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,37 @@
1
+ module StrokeDB
2
+ class MemoryChunkStorage < ChunkStorage
3
+ attr_accessor :chunks_cache
4
+
5
+ def initialize(opts={})
6
+ @chunks_cache = {}
7
+ end
8
+
9
+ def delete!(chunk_uuid)
10
+ write(chunk_path(chunk_uuid), nil)
11
+ end
12
+
13
+ def clear!
14
+ @chunks_cache.clear
15
+ end
16
+
17
+ private
18
+
19
+ def perform_save!(chunk)
20
+ write(chunk_path(chunk.uuid), chunk)
21
+ end
22
+
23
+ def read(path)
24
+ @chunks_cache[path]
25
+ end
26
+
27
+ def write(path, chunk)
28
+ @chunks_cache[path] = chunk
29
+ end
30
+
31
+ def chunk_path(uuid)
32
+ uuid
33
+ end
34
+
35
+
36
+ end
37
+ end
@@ -0,0 +1,217 @@
1
+ module StrokeDB
2
+ class SkiplistStore < Store
3
+ include Enumerable
4
+ attr_accessor :chunk_storage, :cut_level, :index_store
5
+
6
+ def initialize(opts={})
7
+ opts = opts.stringify_keys
8
+ @chunk_storage = opts['storage']
9
+ @cut_level = opts['cut_level'] || 4
10
+ @index_store = opts['index']
11
+ autosync! unless opts['noautosync']
12
+ raise "Missing chunk storage" unless @chunk_storage
13
+ end
14
+
15
+ def find(uuid, version=nil, opts = {})
16
+ uuid_version = uuid + (version ? ".#{version}" : "")
17
+ master_chunk = @chunk_storage.find('MASTER')
18
+ return nil unless master_chunk # no master chunk yet
19
+ chunk_uuid = master_chunk.find_nearest(uuid_version, nil)
20
+ return nil unless chunk_uuid # no chunks in master chunk yet
21
+ chunk = @chunk_storage.find(chunk_uuid)
22
+ return nil unless chunk
23
+
24
+ raw_doc = chunk.find(uuid_version)
25
+
26
+ if raw_doc
27
+ return raw_doc if opts[:no_instantiation]
28
+ doc = Document.from_raw(self, raw_doc.freeze)
29
+ doc.extend(VersionedDocument) if version
30
+ return doc
31
+ end
32
+ nil
33
+ end
34
+
35
+ def search(*args)
36
+ return [] unless @index_store
37
+ @index_store.find(*args)
38
+ end
39
+
40
+ def exists?(uuid, version=nil)
41
+ !!find(uuid, version, :no_instantiation => true)
42
+ end
43
+
44
+ def head_version(uuid)
45
+ raw_doc = find(uuid, nil, :no_instantiation => true)
46
+ return raw_doc['version'] if raw_doc
47
+ nil
48
+ end
49
+
50
+ def save!(doc)
51
+ master_chunk = find_or_create_master_chunk
52
+ next_timestamp
53
+
54
+ insert_with_cut(doc.uuid, doc, master_chunk) unless doc.is_a?(VersionedDocument)
55
+ insert_with_cut("#{doc.uuid}.#{doc.version}", doc, master_chunk)
56
+
57
+ update_master_chunk!(doc, master_chunk)
58
+ end
59
+
60
+ def save_as_head!(doc)
61
+ master_chunk = find_or_create_master_chunk
62
+ insert_with_cut(doc.uuid, doc, master_chunk)
63
+ update_master_chunk!(doc, master_chunk)
64
+ end
65
+
66
+
67
+ def full_dump
68
+ puts "Full storage dump:"
69
+ m = @chunk_storage.find('MASTER')
70
+ puts "No master!" unless m
71
+ m.each do |node|
72
+ puts "[chunk: #{node.key}]"
73
+ chunk = @chunk_storage.find(node.value)
74
+ if chunk
75
+ chunk.each do |node|
76
+ puts " [doc: #{node.key}] => {uuid: #{node.value['__uuid__']}, version: #{node.value['version']}, previous_version: #{node.value['previous_version']}"
77
+ end
78
+ else
79
+ puts " nil! (but in MASTER somehow?...)"
80
+ end
81
+ end
82
+ end
83
+
84
+ def each(options = {})
85
+ return nil unless m = @chunk_storage.find('MASTER') # no master chunk yet
86
+ after = options[:after_timestamp]
87
+ include_versions = options[:include_versions]
88
+ m.each do |node|
89
+ chunk = @chunk_storage.find(node.value)
90
+ next unless chunk
91
+ next if after && chunk.timestamp <= after
92
+
93
+ chunk.each do |node|
94
+ next if after && (node.timestamp <= after)
95
+ if uuid_match = node.key.match(/^#{UUID_RE}$/) || (include_versions && uuid_match = node.key.match(/#{UUID_RE}./) )
96
+ yield Document.from_raw(self, node.value)
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ def timestamp
103
+ @timestamp ||= (lts = find_or_create_master_chunk.timestamp) ? LTS.from_raw(lts) : LTS.zero(uuid)
104
+ end
105
+ def next_timestamp
106
+ @timestamp = timestamp.next
107
+ end
108
+
109
+ def uuid
110
+ return @uuid if @uuid
111
+ master_chunk = @chunk_storage.find('MASTER')
112
+ unless master_chunk
113
+ @uuid = Util.random_uuid
114
+ else
115
+ @uuid = master_chunk.store_uuid
116
+ end
117
+ @uuid
118
+ end
119
+
120
+ def document
121
+ find(uuid) || StoreInfo.create!(self,:kind => 'skiplist', :uuid => uuid)
122
+ end
123
+
124
+ def empty?
125
+ !@chunk_storage.find('MASTER')
126
+ end
127
+
128
+ def inspect
129
+ "#<Skiplist store #{uuid}#{empty? ? " (empty)" : ""}>"
130
+ end
131
+
132
+ def autosync!
133
+ @autosync_mutex ||= Mutex.new
134
+ @autosync = nil if @autosync && !@autosync.status
135
+ at_exit { stop_autosync! }
136
+ @autosync ||= Thread.new do
137
+ until @stop_autosync
138
+ @autosync_mutex.synchronize { chunk_storage.sync_chained_storages! }
139
+ sleep(1)
140
+ end
141
+ end
142
+ end
143
+
144
+ def stop_autosync!
145
+ if @autosync_mutex
146
+ @autosync_mutex.synchronize { @stop_autosync = true; chunk_storage.sync_chained_storages! }
147
+ end
148
+ end
149
+
150
+
151
+ private
152
+
153
+ def insert_with_cut(uuid, doc, master_chunk)
154
+ chunk_uuid = master_chunk.find_nearest(uuid)
155
+ unless chunk_uuid && chunk = @chunk_storage.find(chunk_uuid)
156
+ chunk = Chunk.new(@cut_level)
157
+ end
158
+ a, b = chunk.insert(uuid, doc.to_raw, nil, timestamp.counter)
159
+ [a,b].compact.each do |chunk|
160
+ chunk.store_uuid = self.uuid
161
+ chunk.timestamp = timestamp.counter
162
+ end
163
+ # if split
164
+ if b
165
+ # rename chunk if the first chunk inconsistency detected
166
+ if a.uuid != a.first_uuid
167
+ old_uuid = a.uuid
168
+ a.uuid = a.first_uuid
169
+ @chunk_storage.save!(a)
170
+ master_chunk.insert(a.uuid, a.uuid)
171
+ # remove old chunk
172
+ @chunk_storage.delete!(old_uuid)
173
+ master_chunk.delete(old_uuid)
174
+ else
175
+ @chunk_storage.save!(a)
176
+ master_chunk.insert(a.uuid, a.uuid)
177
+ end
178
+ @chunk_storage.save!(b)
179
+ master_chunk.insert(b.uuid, b.uuid)
180
+ else
181
+ @chunk_storage.save!(a)
182
+ master_chunk.insert(a.uuid, a.uuid)
183
+ end
184
+ end
185
+
186
+ def find_or_create_master_chunk
187
+ if master_chunk = @chunk_storage.find('MASTER')
188
+ return master_chunk
189
+ end
190
+ master_chunk = Chunk.new(999)
191
+ master_chunk.uuid = 'MASTER'
192
+ master_chunk.store_uuid = uuid
193
+ @chunk_storage.save!(master_chunk)
194
+ master_chunk
195
+ end
196
+
197
+
198
+ def update_master_chunk!(doc, master_chunk)
199
+ @chunk_storage.save!(master_chunk)
200
+
201
+ # Update index
202
+ if @index_store
203
+ if doc.previous_version
204
+ raw_pdoc = find(doc.uuid, doc.previous_version, :no_instantiation => true)
205
+ pdoc = Document.from_raw(self, raw_pdoc.freeze, :skip_callbacks => true)
206
+ pdoc.extend(VersionedDocument)
207
+ @index_store.delete(pdoc)
208
+ end
209
+ @index_store.insert(doc)
210
+ @index_store.save!
211
+ end
212
+ end
213
+
214
+
215
+
216
+ end
217
+ end
@@ -0,0 +1,5 @@
1
+ module StrokeDB
2
+ class Store
3
+ end
4
+ StoreInfo = Meta.new(:uuid => STORE_INFO_UUID)
5
+ end
@@ -0,0 +1,38 @@
1
+ module StrokeDB
2
+ # You may mix-in this into specific sync implementations
3
+ module ChainSync
4
+ # We have 2 chains as an input: our chain ("to") and
5
+ # a foreign chain ("from"). We're going to calculate
6
+ # the difference between those chains to know how to
7
+ # implement synchronization.
8
+ #
9
+ # There're two cases:
10
+ # 1) from is a subset of to -> nothing to sync
11
+ # 2) to is a subset of from -> fast-forward merge
12
+ # 3) else: merge case: return base, head_from & head_to
13
+ def sync_chains(from, to)
14
+ common = from & to
15
+ raise NonMatchingChains, "no common element found" if common.empty?
16
+ base = common[common.size - 1]
17
+ ifrom = from.index(base)
18
+ ito = to.index(base)
19
+
20
+ # from: -------------base
21
+ # to: -----base----head
22
+ if ifrom == from.size - 1
23
+ :up_to_date
24
+
25
+ # from: -----base----head
26
+ # to: -------------base
27
+ elsif ito == to.size - 1
28
+ [ :fast_forward, from[ifrom..-1] ]
29
+
30
+ # from: -----base--------head
31
+ # to: --------base-----head
32
+ else
33
+ [ :merge, from[ifrom..-1], to[ito..-1] ]
34
+ end
35
+ end
36
+ class NonMatchingChains < Exception; end
37
+ end
38
+ end
data/lib/sync/diff.rb ADDED
@@ -0,0 +1,126 @@
1
+ require 'diff/lcs'
2
+ module StrokeDB
3
+
4
+ class SlotDiffStrategy
5
+ def self.diff(from, to)
6
+ to
7
+ end
8
+ end
9
+
10
+ class DefaultSlotDiff < SlotDiffStrategy
11
+ def self.diff(from, to)
12
+ unless from.class == to.class # if value types are not the same
13
+ to # then return new value
14
+ else
15
+ case to
16
+ when /@##{UUID_RE}/, /@##{UUID_RE}.#{VERSION_RE}/
17
+ to
18
+ when Array, String
19
+ ::Diff::LCS.diff(from, to).map do |d|
20
+ d.map do |change|
21
+ change.to_a
22
+ end
23
+ end
24
+ when Hash
25
+ ::Diff::LCS.diff(from.sort_by{|e| e.to_s}, to.sort_by{|e| e.to_s}).map do |d|
26
+ d.map do |change|
27
+ [change.to_a.first, {change.to_a.last.first => change.to_a.last.last}]
28
+ end
29
+ end
30
+ else
31
+ to
32
+ end
33
+ end
34
+ end
35
+
36
+ def self.patch(from, patch)
37
+ case from
38
+ when /@##{UUID_RE}/, /@##{UUID_RE}.#{VERSION_RE}/
39
+ patch
40
+ when String, Array
41
+ lcs_patch = patch.map do |d|
42
+ d.map do |change|
43
+ ::Diff::LCS::Change.from_a(change)
44
+ end
45
+ end
46
+ ::Diff::LCS.patch!(from, lcs_patch)
47
+ when Hash
48
+ lcs_patch = patch.map do |d|
49
+ d.map_with_index do |change, index|
50
+ ::Diff::LCS::Change.from_a([change.first, index, [change.last.keys.first, change.last.values.first]])
51
+ end
52
+ end
53
+ diff = ::Diff::LCS.patch!(from.sort_by{|e| e.to_s}, lcs_patch)
54
+ hash = {}
55
+ diff.each do |v|
56
+ hash[v.first] = v.last
57
+ end
58
+ hash
59
+ else
60
+ patch
61
+ end
62
+ end
63
+ end
64
+
65
+ Diff = Meta.new(:uuid => DIFF_UUID) do
66
+
67
+ on_initialization do |diff|
68
+ diff.added_slots = {} unless diff[:added_slots]
69
+ diff.removed_slots = {} unless diff[:removed_slots]
70
+ diff.updated_slots = {} unless diff[:updated_slots]
71
+ diff.send!(:compute_diff) if diff.new?
72
+ end
73
+
74
+ def different?
75
+ !updated_slots.empty? || !removed_slots.empty? || !added_slots.empty?
76
+ end
77
+
78
+ def patch!(document)
79
+ added_slots.each_pair do |addition, value|
80
+ document[addition] = value
81
+ end
82
+ removed_slots.keys.each do |removal|
83
+ document.remove_slot!(removal)
84
+ end
85
+ updated_slots.each_pair do |update, value|
86
+ if sk = strategy_class_for(update)
87
+ document[update] = sk.patch(document[update], value)
88
+ else
89
+ document[update] =value
90
+ end
91
+ end
92
+ end
93
+
94
+
95
+ protected
96
+
97
+ def compute_diff
98
+ additions = to.slotnames - from.slotnames
99
+ additions.each do |addition|
100
+ self.added_slots[addition] = to[addition]
101
+ end
102
+ removals = from.slotnames - to.slotnames
103
+ removals.each do |removal|
104
+ self.removed_slots[removal] = from[removal]
105
+ end
106
+ updates = (to.slotnames - additions - ['version']).select {|slotname| to[slotname] != from[slotname]}
107
+ updates.each do |update|
108
+ unless sk = strategy_class_for(update)
109
+ self.updated_slots[update] = to[update]
110
+ else
111
+ self.updated_slots[update] = sk.diff(from[update], to[update])
112
+ end
113
+ end
114
+ end
115
+
116
+ def strategy_class_for(slotname)
117
+ if from.meta && strategy = from.meta["diff_strategy_#{slotname}"]
118
+ _strategy_class = strategy.camelize.constantize rescue nil
119
+ return _strategy_class if _strategy_class && _strategy_class.ancestors.include?(SlotDiffStrategy)
120
+ end
121
+ false
122
+ end
123
+
124
+ end
125
+
126
+ end
@@ -0,0 +1,81 @@
1
+ module StrokeDB
2
+ class LamportTimestamp
3
+ MAX_COUNTER = 2**64
4
+ BASE = 16
5
+ BASE_LENGTH = 16
6
+
7
+ attr_reader :counter, :uuid
8
+
9
+ def initialize(c = 0, __uuid = Util.random_uuid)
10
+ if c > MAX_COUNTER
11
+ raise CounterOverflow.new, "Max counter value is 2**64"
12
+ end
13
+ @counter = c
14
+ @uuid = __uuid
15
+ end
16
+ def next
17
+ LamportTimestamp.new(@counter + 1, @uuid)
18
+ end
19
+ def next!
20
+ @counter += 1
21
+ self
22
+ end
23
+ def dup
24
+ LamportTimestamp.new(@counter, @uuid)
25
+ end
26
+ def marshal_dump
27
+ @counter.to_s(BASE).rjust(BASE_LENGTH, '0') + @uuid
28
+ end
29
+ def marshal_load(dumped)
30
+ @counter = dumped[0, BASE_LENGTH].to_i(BASE)
31
+ @uuid = dumped[BASE_LENGTH, 36]
32
+ self
33
+ end
34
+
35
+ def to_json
36
+ marshal_dump.to_json
37
+ end
38
+
39
+ # Raw format
40
+ def self.from_raw(raw_string)
41
+ new.marshal_load(raw_string)
42
+ end
43
+ def to_raw
44
+ marshal_dump
45
+ end
46
+
47
+ def to_s
48
+ marshal_dump
49
+ end
50
+ def <=>(other)
51
+ primary = (@counter <=> other.counter)
52
+ primary == 0 ? (@uuid <=> other.uuid) : primary
53
+ end
54
+ def ==(other)
55
+ @counter == other.counter && @uuid == other.uuid
56
+ end
57
+ def <(other)
58
+ (self <=> other) < 0
59
+ end
60
+ def <=(other)
61
+ (self <=> other) <= 0
62
+ end
63
+ def >(other)
64
+ (self <=> other) > 0
65
+ end
66
+ def >=(other)
67
+ (self <=> other) >= 0
68
+ end
69
+ def self.zero(__uuid = Util.random_uuid)
70
+ ts = new(0)
71
+ ts.instance_variable_set(:@uuid, __uuid)
72
+ ts
73
+ end
74
+ def self.zero_string
75
+ "0"*BASE_LENGTH + NIL_UUID
76
+ end
77
+ class CounterOverflow < Exception; end
78
+ end
79
+ LTS = LamportTimestamp
80
+ end
81
+
@@ -0,0 +1,79 @@
1
+ module StrokeDB
2
+
3
+ SynchronizationReport = Meta.new(:uuid => SYNCHRONIZATION_REPORT_UUID) do
4
+ on_new_document do |report|
5
+ report.conflicts = []
6
+ report.added_documents = []
7
+ report.fast_forwarded_documents = []
8
+ report.non_matching_documents = []
9
+ end
10
+ end
11
+
12
+ SynchronizationConflict = Meta.new(:uuid => SYNCHRONIZATION_CONFLICT_UUID) do
13
+ def resolve!
14
+ # by default, do nothing
15
+ end
16
+ end
17
+
18
+ class Store
19
+ def sync!(docs, _timestamp=nil)
20
+ _timestamp_counter = timestamp.counter
21
+ report = SynchronizationReport.new(self, :store_document => document, :timestamp => _timestamp_counter)
22
+ existing_chain = {}
23
+ docs.group_by {|doc| doc.uuid}.each_pair do |uuid, versions|
24
+ doc = find(uuid)
25
+ existing_chain[uuid] = doc.versions.all_versions if doc
26
+ end
27
+ case _timestamp
28
+ when Numeric
29
+ @timestamp = LTS.new(_timestamp, timestamp.uuid)
30
+ when LamportTimestamp
31
+ @timestamp = LTS.new(_timestamp.counter, timestamp.uuid)
32
+ else
33
+ end
34
+ docs.each {|doc| save!(doc) unless exists?(doc.uuid, doc.version)}
35
+ docs.group_by {|doc| doc.uuid}.each_pair do |uuid, versions|
36
+ incoming_chain = find(uuid, versions.last.version).versions.all_versions
37
+ if existing_chain[uuid].nil? or existing_chain[uuid].empty? # It is a new document
38
+ added_doc = find(uuid, versions.last.version)
39
+ save_as_head!(added_doc)
40
+ report.added_documents << added_doc
41
+ else
42
+ begin
43
+ sync = sync_chains(incoming_chain.reverse, existing_chain[uuid].reverse)
44
+ rescue NonMatchingChains
45
+ # raise NonMatchingDocumentCondition.new(uuid) # that will definitely leave garbage in the store (FIXME?)
46
+ non_matching_doc = find(uuid)
47
+ report.non_matching_documents << non_matching_doc
48
+ next
49
+ end
50
+ resolution = sync.is_a?(Array) ? sync.first : sync
51
+ case resolution
52
+ when :up_to_date
53
+ # nothing to do
54
+ when :merge
55
+ report.conflicts << SynchronizationConflict.create!(self, :document => find(uuid), :rev1 => sync[1], :rev2 => sync[2])
56
+ when :fast_forward
57
+ fast_forwarded_doc = find(uuid, sync[1].last)
58
+ save_as_head!(fast_forwarded_doc)
59
+ report.fast_forwarded_documents << fast_forwarded_doc
60
+ else
61
+ raise "Invalid sync resolution #{resolution}"
62
+ end
63
+ end
64
+ end
65
+ report.conflicts.each do |conflict|
66
+ if resolution_strategy = conflict.document.meta[:resolution_strategy]
67
+ conflict.metas << resolution_strategy
68
+ conflict.save!
69
+ end
70
+ conflict.resolve!
71
+ end
72
+ report.save!
73
+ end
74
+ private
75
+
76
+ include ChainSync
77
+
78
+ end
79
+ end