hyper-mesh 1.0.0.lap27 → 1.0.0.lap28

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +6 -2
  4. data/Gemfile +0 -1
  5. data/Rakefile +2 -2
  6. data/hyper-mesh.gemspec +1 -1
  7. data/lib/active_record_base.rb +39 -27
  8. data/lib/hyper-mesh.rb +6 -1
  9. data/lib/hypermesh/version.rb +1 -1
  10. data/lib/object/tap.rb +7 -0
  11. data/lib/reactive_record/active_record/associations.rb +14 -3
  12. data/lib/reactive_record/active_record/base.rb +1 -2
  13. data/lib/reactive_record/active_record/class_methods.rb +120 -67
  14. data/lib/reactive_record/active_record/error.rb +17 -12
  15. data/lib/reactive_record/active_record/errors.rb +374 -0
  16. data/lib/reactive_record/active_record/instance_methods.rb +58 -67
  17. data/lib/reactive_record/active_record/reactive_record/backing_record_inspector.rb +1 -4
  18. data/lib/reactive_record/active_record/reactive_record/base.rb +129 -234
  19. data/lib/reactive_record/active_record/reactive_record/collection.rb +51 -18
  20. data/lib/reactive_record/active_record/reactive_record/column_types.rb +5 -3
  21. data/lib/reactive_record/active_record/reactive_record/dummy_value.rb +6 -4
  22. data/lib/reactive_record/active_record/reactive_record/getters.rb +133 -0
  23. data/lib/reactive_record/active_record/reactive_record/isomorphic_base.rb +99 -87
  24. data/lib/reactive_record/active_record/reactive_record/lookup_tables.rb +54 -0
  25. data/lib/reactive_record/active_record/reactive_record/operations.rb +2 -1
  26. data/lib/reactive_record/active_record/reactive_record/scoped_collection.rb +1 -1
  27. data/lib/reactive_record/active_record/reactive_record/setters.rb +194 -0
  28. data/lib/reactive_record/active_record/reactive_record/while_loading.rb +4 -5
  29. data/lib/reactive_record/active_record_error.rb +4 -0
  30. data/lib/reactive_record/broadcast.rb +55 -18
  31. data/lib/reactive_record/permissions.rb +5 -4
  32. data/lib/reactive_record/scope_description.rb +14 -6
  33. data/lib/reactive_record/server_data_cache.rb +119 -70
  34. metadata +16 -13
  35. data/lib/reactive_record/active_record/reactive_record/reactive_set_relationship_helpers.rb +0 -189
@@ -0,0 +1,54 @@
1
+ module ReactiveRecord
2
+ module LookupTables
3
+ def initialize_lookup_tables
4
+ @records = Hash.new { |hash, key| hash[key] = [] }
5
+ @records_by_id = `{}`
6
+ @records_by_vector = `{}`
7
+ @records_by_object_id = `{}`
8
+ @class_scopes = Hash.new { |hash, key| hash[key] = {} }
9
+ @waiting_for_save = Hash.new { |hash, key| hash[key] = [] }
10
+ end
11
+
12
+ def class_scopes(model)
13
+ @class_scopes[model.base_class]
14
+ end
15
+
16
+ def waiting_for_save(model)
17
+ @waiting_for_save[model]
18
+ end
19
+
20
+ def wait_for_save(model, &block)
21
+ @waiting_for_save[model] << block
22
+ end
23
+
24
+ def clear_waiting_for_save(model)
25
+ @waiting_for_save[model] = []
26
+ end
27
+
28
+ def lookup_by_object_id(object_id)
29
+ `#{@records_by_object_id}[#{object_id}]`.ar_instance
30
+ end
31
+
32
+ def set_object_id_lookup(record)
33
+ `#{@records_by_object_id}[#{record.object_id}] = #{record}`
34
+ end
35
+
36
+ def lookup_by_id(*args) # model and id
37
+ `#{@records_by_id}[#{args}]` || nil
38
+ end
39
+
40
+ def set_id_lookup(record)
41
+ `#{@records_by_id}[#{[record.model, record.id]}] = #{record}`
42
+ end
43
+
44
+ def lookup_by_vector(vector)
45
+ `#{@records_by_vector}[#{vector}]` || nil
46
+ end
47
+
48
+ def set_vector_lookup(record, vector)
49
+ record.vector = vector
50
+ `delete #{@records_by_vector}[#{record.vector}]`
51
+ `#{@records_by_vector}[#{vector}] = record`
52
+ end
53
+ end
54
+ end
@@ -74,6 +74,7 @@ module ReactiveRecord
74
74
  param :acting_user, nils: true
75
75
  param models: []
76
76
  param associations: []
77
+ param :save, type: :boolean
77
78
  param :validate, type: :boolean
78
79
 
79
80
  step do
@@ -82,7 +83,7 @@ module ReactiveRecord
82
83
  params.associations.map(&:with_indifferent_access),
83
84
  params.acting_user,
84
85
  params.validate,
85
- true
86
+ params.save
86
87
  )
87
88
  end
88
89
  end
@@ -53,7 +53,7 @@ module ReactiveRecord
53
53
  else
54
54
  @count += (after - before).count
55
55
  @count -= (before - after).count
56
- notify_of_change self # TODO remove self .... and retest
56
+ notify_of_change self # TODO: remove self .... and retest
57
57
  end
58
58
  end
59
59
  after
@@ -0,0 +1,194 @@
1
+ module ReactiveRecord
2
+ module Setters
3
+ def set_attr_value(attr, raw_value)
4
+ set_common(attr, raw_value) { |value| update_simple_attribute(attr, value) }
5
+ end
6
+
7
+ def set_ar_aggregate(aggr, raw_value)
8
+ set_common(aggr.attribute, raw_value) do |value, attr|
9
+ @attributes[attr] ||= aggr.klass.new if new?
10
+ abr = @attributes[attr].backing_record
11
+ abr.virgin = false
12
+ map = value.attributes if value
13
+ aggr.mapped_attributes.each do |mapped_attr|
14
+ abr.update_aggregate_attribute mapped_attr, map && map[mapped_attr]
15
+ end
16
+ return @attributes[attr]
17
+ end
18
+ end
19
+
20
+ def set_non_ar_aggregate(aggregation, raw_value)
21
+ set_common(aggregation.attribute, raw_value) do |value, attr|
22
+ if data_loading?
23
+ @synced_attributes[attr] = aggregation.deserialize(aggregation.serialize(value))
24
+ else
25
+ changed = !@synced_attributes.key?(attr) || @synced_attributes[attr] != value
26
+ end
27
+ set_attribute_change_status_and_notify attr, changed, value
28
+ end
29
+ end
30
+
31
+ def set_has_many(assoc, raw_value)
32
+ set_common(assoc.attribute, raw_value) do |value, attr|
33
+ # create a new collection to hold value, shove it in, and return the new collection
34
+ # the replace method will take care of updating the inverse belongs_to links as
35
+ # the collection is overwritten
36
+ collection = Collection.new(assoc.klass, @ar_instance, assoc)
37
+ collection.replace(value || [])
38
+ @synced_attributes[attr] = value if data_loading?
39
+ set_attribute_change_status_and_notify attr, value != @synced_attributes[attr], collection
40
+ return collection
41
+ end
42
+ end
43
+
44
+ def set_belongs_to(assoc, raw_value)
45
+ set_common(assoc.attribute, raw_value) do |value, attr|
46
+ if assoc.inverse.collection?
47
+ update_has_many_through_associations assoc, value
48
+ update_inverse_collections assoc, value
49
+ else
50
+ update_inverse_attribute assoc, value
51
+ end
52
+ # itself will just reactively read the value (a model instance) by doing a .id
53
+ update_belongs_to attr, value.itself
54
+ end
55
+ end
56
+
57
+ def sync_has_many(attr)
58
+ set_change_status_and_notify_only attr, @attributes[attr] != @synced_attributes[attr]
59
+ end
60
+
61
+ def update_simple_attribute(attr, value)
62
+ if data_loading?
63
+ @synced_attributes[attr] = value
64
+ else
65
+ changed = !@synced_attributes.key?(attr) || @synced_attributes[attr] != value
66
+ end
67
+ set_attribute_change_status_and_notify attr, changed, value
68
+ end
69
+
70
+ alias update_belongs_to update_simple_attribute
71
+ alias update_aggregate_attribute update_simple_attribute
72
+
73
+ private
74
+
75
+ def set_common(attr, value)
76
+ value = convert(attr, value)
77
+ @virgin = false unless data_loading?
78
+ if !@destroyed && (
79
+ !@attributes.key?(attr) ||
80
+ @attributes[attr].is_a?(Base::DummyValue) ||
81
+ @attributes[attr] != value)
82
+ yield value, attr
83
+ end
84
+ value
85
+ end
86
+
87
+ def set_attribute_change_status_and_notify(attr, changed, new_value)
88
+ if @virgin
89
+ @attributes[attr] = new_value
90
+ else
91
+ change_status_and_notify_helper(attr, changed) do |had_key, current_value|
92
+ @attributes[attr] = new_value
93
+ if !data_loading? ||
94
+ (on_opal_client? && had_key && current_value.loaded? && current_value != new_value)
95
+ React::State.set_state(self, attr, new_value, data_loading?)
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ def set_change_status_and_notify_only(attr, changed)
102
+ return if @virgin
103
+ change_status_and_notify_helper(attr, changed) do
104
+ React::State.set_state(self, attr, nil) unless data_loading?
105
+ end
106
+ end
107
+
108
+ def change_status_and_notify_helper(attr, changed)
109
+ empty_before = changed_attributes.empty?
110
+ # TODO: confirm this works:
111
+ # || data_loading? added so that model.new can be wrapped in a ReactiveRecord.load_data
112
+ if !changed || data_loading?
113
+ changed_attributes.delete(attr)
114
+ elsif !changed_attributes.include?(attr)
115
+ changed_attributes << attr
116
+ end
117
+ yield @attributes.key?(attr), @attributes[attr]
118
+ return unless empty_before != changed_attributes.empty?
119
+ if on_opal_client? && !data_loading?
120
+ React::State.set_state(self, '!CHANGED!', !changed_attributes.empty?, true)
121
+ end
122
+ return unless aggregate_owner
123
+ aggregate_owner.set_change_status_and_notify_only(
124
+ attr, !@attributes[attr].backing_record.changed_attributes.empty?
125
+ )
126
+ end
127
+
128
+ def update_inverse_attribute(association, value)
129
+ # when updating the inverse attribute of a belongs_to that is itself a belongs_to
130
+ # (i.e. 1-1 relationship) we clear the existing inverse value and then
131
+ # write the current record to the new value
132
+ current_value = @attributes[association.attribute]
133
+ inverse_attr = association.inverse.attribute
134
+ current_value.attributes[inverse_attr] = nil unless current_value.nil?
135
+ return if value.nil?
136
+ value.attributes[inverse_attr] = @ar_instance
137
+ return if data_loading?
138
+ React::State.set_state(value.backing_record, inverse_attr, @ar_instance)
139
+ end
140
+
141
+ def update_inverse_collections(association, value)
142
+ # when updating an inverse attribute of a belongs_to that is a has_many (i.e. a collection)
143
+ # we need to first remove the current associated value (if non-nil), then add the new
144
+ # value to the collection. If the inverse collection is not yet initialized we do it here.
145
+ current_value = @attributes[association.attribute]
146
+ inverse_attr = association.inverse.attribute
147
+ if value.nil?
148
+ current_value.attributes[inverse_attr].delete(@ar_instance) unless current_value.nil?
149
+ else
150
+ value.backing_record.push_onto_collection(@model, association.inverse, @ar_instance)
151
+ end
152
+ end
153
+
154
+ def push_onto_collection(model, association, ar_instance)
155
+ @attributes[association.attribute] ||= Collection.new(model, @ar_instance, association)
156
+ @attributes[association.attribute] << ar_instance
157
+ end
158
+
159
+ def update_has_many_through_associations(association, value)
160
+ association.through_associations.each { |ta| update_through_association(ta, value) }
161
+ association.source_associations.each { |sa| update_source_association(sa, value) }
162
+ end
163
+
164
+ def update_through_association(ta, new_belongs_to_value)
165
+ # appointment.doctor = doctor_new_value (i.e. through association is changing)
166
+ # means appointment.doctor_new_value.patients << appointment.patient
167
+ # and we have to appointment.doctor_current_value.patients.delete(appointment.patient)
168
+ source_value = @attributes[ta.source]
169
+ current_belongs_to_value = @attributes[ta.inverse.attribute]
170
+ return unless source_value
171
+ unless current_belongs_to_value.nil? || current_belongs_to_value.attributes[ta.attribute].nil?
172
+ current_belongs_to_value.attributes[ta.attribute].delete(source_value)
173
+ end
174
+ return unless new_belongs_to_value
175
+ new_belongs_to_value.attributes[ta.attribute] ||= Collection.new(ta.klass, new_belongs_to_value, ta)
176
+ new_belongs_to_value.attributes[ta.attribute] << source_value
177
+ end
178
+
179
+ def update_source_association(sa, new_source_value)
180
+ # appointment.patient = patient_value (i.e. source is changing)
181
+ # means appointment.doctor.patients.delete(appointment.patient)
182
+ # means appointment.doctor.patients << patient_value
183
+ belongs_to_value = @attributes[sa.inverse.attribute]
184
+ current_source_value = @attributes[sa.source]
185
+ return unless belongs_to_value
186
+ unless belongs_to_value.attributes[sa.attribute].nil? || current_source_value.nil?
187
+ belongs_to_value.attributes[sa.attribute].delete(current_source_value)
188
+ end
189
+ return unless new_source_value
190
+ belongs_to_value.attributes[sa.attribute] ||= Collection.new(sa.klass, belongs_to_value, sa)
191
+ belongs_to_value.attributes[sa.attribute] << new_source_value
192
+ end
193
+ end
194
+ end
@@ -17,7 +17,7 @@ module ReactiveRecord
17
17
  result = block.call.itself
18
18
  if @loads_pending
19
19
  @blocks_to_load ||= []
20
- @blocks_to_load << [Base.last_fetch_at, promise, block]
20
+ @blocks_to_load << [Base.current_fetch_id, promise, block]
21
21
  else
22
22
  promise.resolve result
23
23
  end
@@ -36,7 +36,7 @@ module ReactiveRecord
36
36
  if Base.pending_fetches.count > 0
37
37
  true
38
38
  else # this happens when for example loading foo.x results in somebody looking at foo.y while foo.y is still being loaded
39
- ReactiveRecord::WhileLoading.loaded_at Base.last_fetch_at
39
+ ReactiveRecord::WhileLoading.loaded_at Base.current_fetch_id
40
40
  ReactiveRecord::WhileLoading.quiet!
41
41
  false
42
42
  end
@@ -53,8 +53,8 @@ module ReactiveRecord
53
53
  @load_stack << @loads_pending
54
54
  @loads_pending = nil
55
55
  result = block.call(failure)
56
- if check_loads_pending and !failure
57
- @blocks_to_load << [Base.last_fetch_at, promise, block]
56
+ if check_loads_pending && !failure
57
+ @blocks_to_load << [Base.current_fetch_id, promise, block]
58
58
  else
59
59
  promise.resolve result
60
60
  end
@@ -297,7 +297,6 @@ if RUBY_ENGINE == 'opal'
297
297
  def reactive_record_link_set_while_loading_container_class
298
298
  node = dom_node
299
299
  loading = (waiting_on_resources ? `true` : `false`)
300
- #puts "******* reactive_record_link_set_while_loading_container_class #{self} #{node} #{loading}"
301
300
  %x{
302
301
  if (typeof node === "undefined" || node === null) return;
303
302
  var while_loading_container_id = node.getAttribute('data-reactive_record_while_loading_container_id');
@@ -0,0 +1,4 @@
1
+ module ActiveRecord
2
+ class ActiveRecordError < StandardError
3
+ end
4
+ end
@@ -23,7 +23,7 @@ module ReactiveRecord
23
23
  operation: operation,
24
24
  salt: salt,
25
25
  authorization: authorization
26
- )
26
+ ).tap { |p| raise p.error if p.rejected? }
27
27
  end unless RUBY_ENGINE == 'opal'
28
28
 
29
29
  class SendPacket < Hyperloop::ServerOp
@@ -54,9 +54,7 @@ module ReactiveRecord
54
54
  if params.operation == :destroy
55
55
  ReactiveRecord::Collection.sync_scopes broadcast
56
56
  else
57
- ReactiveRecord::Base.when_not_saving(broadcast.klass) do
58
- ReactiveRecord::Collection.sync_scopes broadcast
59
- end
57
+ ReactiveRecord::Collection.sync_scopes broadcast.process_previous_changes
60
58
  end
61
59
  end
62
60
  end
@@ -137,47 +135,86 @@ module ReactiveRecord
137
135
  @klass = record.class.name
138
136
  @record = data
139
137
  record.backing_record.destroyed = false
140
- @record.merge!(id: record.id) if record.id
138
+ @record[:id] = record.id if record.id
141
139
  record.backing_record.destroyed = @destroyed
142
140
  @backing_record = record.backing_record
143
- attributes = record.backing_record.attributes
144
- data.each do |k, v|
145
- next if klass.reflect_on_association(k) || attributes[k] == v
146
- @previous_changes[k] = [attributes[k], v]
147
- end
141
+ @previous_changes = record.changes
142
+ # attributes = record.attributes
143
+ # data.each do |k, v|
144
+ # next if klass.reflect_on_association(k) || attributes[k] == v
145
+ # @previous_changes[k] = [attributes[k], v]
146
+ # end
148
147
  self
149
148
  end
150
149
 
151
150
  def receive(params)
152
151
  @destroyed = params.operation == :destroy
153
- @is_new = params.operation == :create
154
152
  @channels ||= Hyperloop::IncomingBroadcast.open_channels.intersection params.channels
155
- #raise 'synchromesh security violation' unless @channels.include? params.channels
156
153
  @received << params.channel
157
154
  @klass ||= params.klass
158
155
  @record.merge! params.record
159
156
  @previous_changes.merge! params.previous_changes
160
- @backing_record = ReactiveRecord::Base.exists?(klass, params.record[:id])
161
- yield complete! if @channels == @received
157
+ ReactiveRecord::Base.when_not_saving(klass) do
158
+ @backing_record = ReactiveRecord::Base.exists?(klass, params.record[:id])
159
+ @is_new = params.operation == :create && !@backing_record
160
+ yield complete! if @channels == @received
161
+ end
162
162
  end
163
163
 
164
164
  def complete!
165
165
  self.class.in_transit.delete @id
166
166
  end
167
167
 
168
+ def value_changed?(attr, value)
169
+ attrs = @backing_record.synced_attributes
170
+ return true if attr == @backing_record.primary_key
171
+ return attrs[attr] != @backing_record.convert(attr, value) if attrs.key?(attr)
172
+
173
+ assoc = klass.reflect_on_association_by_foreign_key attr
174
+
175
+ return value unless assoc
176
+ child = attrs[assoc.attribute]
177
+ return value != child.id if child
178
+ value
179
+ end
180
+
181
+ def integrity_check
182
+ @previous_changes.each do |attr, value|
183
+ next if @record.key?(attr) && @record[attr] == value.last
184
+ React::IsomorphicHelpers.log "Broadcast contained change to #{attr} -> #{value.last} "\
185
+ "without corresponding value in attributes (#{@record}).\n",
186
+ :error
187
+ raise "Broadcast Integrity Error"
188
+ end
189
+ end
190
+
191
+ def process_previous_changes
192
+ return self unless @backing_record
193
+ integrity_check
194
+ return self if destroyed?
195
+ @record.dup.each do |attr, value|
196
+ next if value_changed?(attr, value)
197
+ @record.delete(attr)
198
+ @previous_changes.delete(attr)
199
+ end
200
+ self
201
+ end
202
+
168
203
  def merge_current_values(br)
169
204
  current_values = Hash[*@previous_changes.collect do |attr, values|
170
205
  value = attr == :id ? record[:id] : values.first
171
206
  if br.attributes.key?(attr) &&
172
207
  br.attributes[attr] != br.convert(attr, value) &&
173
208
  br.attributes[attr] != br.convert(attr, values.last)
174
- puts "warning #{attr} has changed locally - will force a reload.\n"\
175
- "local value: #{br.attributes[attr]} remote value: #{br.convert(attr, value)}->#{br.convert(attr, values.last)}"
209
+ React::IsomorphicHelpers.log "warning #{attr} has changed locally - will force a reload.\n"\
210
+ "local value: #{br.attributes[attr]} remote value: #{br.convert(attr, value)}->#{br.convert(attr, values.last)}",
211
+ :warning
176
212
  return nil
177
213
  end
178
214
  [attr, value]
179
- end.compact.flatten].merge(br.attributes)
180
- klass._react_param_conversion(current_values)
215
+ end.compact.flatten]
216
+ # TODO: verify - it used to be current_values.merge(br.attributes)
217
+ klass._react_param_conversion(br.attributes.merge(current_values))
181
218
  end
182
219
  end
183
220
  end
@@ -88,10 +88,11 @@ class ActiveRecord::Base
88
88
  alias belongs_to_without_reactive_record_add_is_method belongs_to
89
89
 
90
90
  def belongs_to(attr_name, scope = nil, options = {})
91
- define_method "#{attr_name}_is?".to_sym do |model|
92
- send(options[:foreign_key] || "#{attr_name}_id") == model.id
91
+ belongs_to_without_reactive_record_add_is_method(attr_name, scope, options).tap do
92
+ define_method "#{attr_name}_is?".to_sym do |model|
93
+ self.class.reflections[attr_name].foreign_key == model.id
94
+ end
93
95
  end
94
- belongs_to_without_reactive_record_add_is_method(attr_name, scope, options)
95
96
  end
96
97
  end
97
98
 
@@ -103,7 +104,7 @@ class ActiveRecord::Base
103
104
  self.acting_user = old
104
105
  self
105
106
  else
106
- raise ReactiveRecord::AccessViolation, "for #{permission}(#{args})"
107
+ raise Hyperloop::AccessViolation, "for #{permission}(#{args})"
107
108
  end
108
109
  end
109
110