hyper-mesh 1.0.0.lap27 → 1.0.0.lap28

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 (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