hyper-model 1.0.alpha1.3 → 1.0.alpha1.8

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/.rspec +0 -1
  4. data/Gemfile +6 -5
  5. data/Rakefile +18 -6
  6. data/hyper-model.gemspec +12 -20
  7. data/lib/active_record_base.rb +95 -28
  8. data/lib/enumerable/pluck.rb +3 -2
  9. data/lib/hyper-model.rb +4 -1
  10. data/lib/hyper_model/version.rb +1 -1
  11. data/lib/hyper_react/input_tags.rb +2 -1
  12. data/lib/reactive_record/active_record/associations.rb +125 -35
  13. data/lib/reactive_record/active_record/base.rb +32 -0
  14. data/lib/reactive_record/active_record/class_methods.rb +125 -53
  15. data/lib/reactive_record/active_record/error.rb +2 -0
  16. data/lib/reactive_record/active_record/errors.rb +8 -4
  17. data/lib/reactive_record/active_record/instance_methods.rb +73 -5
  18. data/lib/reactive_record/active_record/public_columns_hash.rb +25 -26
  19. data/lib/reactive_record/active_record/reactive_record/backing_record_inspector.rb +22 -5
  20. data/lib/reactive_record/active_record/reactive_record/base.rb +50 -24
  21. data/lib/reactive_record/active_record/reactive_record/collection.rb +196 -63
  22. data/lib/reactive_record/active_record/reactive_record/dummy_polymorph.rb +22 -0
  23. data/lib/reactive_record/active_record/reactive_record/dummy_value.rb +27 -15
  24. data/lib/reactive_record/active_record/reactive_record/getters.rb +33 -10
  25. data/lib/reactive_record/active_record/reactive_record/isomorphic_base.rb +71 -44
  26. data/lib/reactive_record/active_record/reactive_record/lookup_tables.rb +5 -5
  27. data/lib/reactive_record/active_record/reactive_record/operations.rb +7 -1
  28. data/lib/reactive_record/active_record/reactive_record/scoped_collection.rb +3 -6
  29. data/lib/reactive_record/active_record/reactive_record/setters.rb +105 -68
  30. data/lib/reactive_record/active_record/reactive_record/while_loading.rb +22 -1
  31. data/lib/reactive_record/broadcast.rb +59 -25
  32. data/lib/reactive_record/interval.rb +3 -3
  33. data/lib/reactive_record/permissions.rb +1 -1
  34. data/lib/reactive_record/scope_description.rb +3 -2
  35. data/lib/reactive_record/server_data_cache.rb +78 -48
  36. data/polymorph-notes.md +143 -0
  37. metadata +52 -157
  38. data/Gemfile.lock +0 -440
@@ -14,15 +14,15 @@ module ReactiveRecord
14
14
  end
15
15
 
16
16
  def waiting_for_save(model)
17
- @waiting_for_save[model]
17
+ @waiting_for_save[model.base_class]
18
18
  end
19
19
 
20
20
  def wait_for_save(model, &block)
21
- @waiting_for_save[model] << block
21
+ @waiting_for_save[model.base_class] << block
22
22
  end
23
23
 
24
24
  def clear_waiting_for_save(model)
25
- @waiting_for_save[model] = []
25
+ @waiting_for_save[model.base_class] = []
26
26
  end
27
27
 
28
28
  def lookup_by_object_id(object_id)
@@ -33,8 +33,8 @@ module ReactiveRecord
33
33
  `#{@records_by_object_id}[#{record.object_id}] = #{record}`
34
34
  end
35
35
 
36
- def lookup_by_id(*args) # model and id
37
- `#{@records_by_id}[#{args}]` || nil
36
+ def lookup_by_id(model, id) # model and id
37
+ `#{@records_by_id}[#{[model.base_class, id]}]` || nil
38
38
  end
39
39
 
40
40
  def set_id_lookup(record)
@@ -12,6 +12,10 @@ module ReactiveRecord
12
12
 
13
13
  FORMAT = '0x%x'
14
14
 
15
+ class << self
16
+ attr_accessor :last_response_sent_at
17
+ end
18
+
15
19
  def self.serialize_params(hash)
16
20
  hash['associations'].each do |assoc|
17
21
  assoc['parent_id'] = FORMAT % assoc['parent_id']
@@ -38,6 +42,7 @@ module ReactiveRecord
38
42
  response[:saved_models].each do |saved_model|
39
43
  saved_model[0] = FORMAT % saved_model[0]
40
44
  end if response.is_a?(Hash) && response[:saved_models]
45
+ response[:sent_at] = Time.now.to_f
41
46
  response
42
47
  end
43
48
 
@@ -45,6 +50,7 @@ module ReactiveRecord
45
50
  response[:saved_models].each do |saved_model|
46
51
  saved_model[0] = saved_model[0].to_i(16)
47
52
  end if response.is_a?(Hash) && response[:saved_models]
53
+ Base.last_response_sent_at = response.delete(:sent_at)
48
54
  response
49
55
  end
50
56
  end
@@ -93,7 +99,7 @@ module ReactiveRecord
93
99
  class Destroy < Base
94
100
  param :acting_user, nils: true
95
101
  param :model
96
- param :id
102
+ param :id, nils: true
97
103
  param :vector
98
104
  step do
99
105
  ReactiveRecord::Base.destroy_record(
@@ -9,10 +9,10 @@ module ReactiveRecord
9
9
  def set_pre_sync_related_records(related_records, _record = nil)
10
10
  @pre_sync_related_records = nil
11
11
  ReactiveRecord::Base.catch_db_requests do
12
- puts "#{self}.set_pre_sync_related_records filter_records(#{related_records})"
12
+ # puts "#{self}.set_pre_sync_related_records filter_records(#{related_records})"
13
13
 
14
14
  @pre_sync_related_records = filter_records(related_records)
15
- puts "returns #{@pre_sync_related_records}"
15
+ # puts "returns #{@pre_sync_related_records}"
16
16
  live_scopes.each do |scope|
17
17
  scope.set_pre_sync_related_records(@pre_sync_related_records)
18
18
  end
@@ -35,9 +35,7 @@ module ReactiveRecord
35
35
  if collector?
36
36
  update_collector_scope(related_records)
37
37
  else
38
- puts "#{self}.update_collection calling filter_records(#{related_records})"
39
38
  related_records = filter_records(related_records)
40
- puts "returns #{related_records}"
41
39
  update_filter_scope(@pre_sync_related_records, related_records)
42
40
  end
43
41
  end
@@ -46,8 +44,7 @@ module ReactiveRecord
46
44
  current = Set.new([*@collection])
47
45
  (related_records - @pre_sync_related_records).each { |r| current << r }
48
46
  (@pre_sync_related_records - related_records).each { |r| current.delete(r) }
49
- puts "#{self}.update_collector_scope calling replace(filter_records(#{current}))"
50
- replace(filter_records(current).tap { |rr| puts "returns #{rr}" })
47
+ replace(filter_records(current))
51
48
  Set.new([*@collection])
52
49
  end
53
50
 
@@ -43,17 +43,26 @@ module ReactiveRecord
43
43
 
44
44
  def set_belongs_to(assoc, raw_value)
45
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
46
+ current_value = @attributes[assoc.attribute]
47
+ update_has_many_through_associations assoc, nil, current_value, :remove_member
48
+ update_has_many_through_associations assoc, nil, value, :add_member
49
+ remove_current_inverse_attribute assoc, nil, current_value
50
+ add_new_inverse_attribute assoc, nil, value
51
+ update_belongs_to attr, value.itself
54
52
  end
55
53
  end
56
54
 
55
+ def set_belongs_to_via_has_many(orig, value)
56
+ assoc = orig.inverse
57
+ attr = assoc.attribute
58
+ current_value = @attributes[attr]
59
+ update_has_many_through_associations assoc, orig, current_value, :remove_member
60
+ update_has_many_through_associations assoc, orig, value, :add_member
61
+ remove_current_inverse_attribute assoc, orig, current_value
62
+ add_new_inverse_attribute assoc, orig, value
63
+ update_belongs_to attr, value.itself
64
+ end
65
+
57
66
  def sync_has_many(attr)
58
67
  set_change_status_and_notify_only attr, @attributes[attr] != @synced_attributes[attr]
59
68
  end
@@ -85,7 +94,7 @@ module ReactiveRecord
85
94
  end
86
95
 
87
96
  def set_attribute_change_status_and_notify(attr, changed, new_value)
88
- if @virgin
97
+ if @virgin || @being_destroyed
89
98
  @attributes[attr] = new_value
90
99
  else
91
100
  change_status_and_notify_helper(attr, changed) do |had_key, current_value|
@@ -99,7 +108,7 @@ module ReactiveRecord
99
108
  end
100
109
 
101
110
  def set_change_status_and_notify_only(attr, changed)
102
- return if @virgin
111
+ return if @virgin || @being_destroyed
103
112
  change_status_and_notify_helper(attr, changed) do
104
113
  Hyperstack::Internal::State::Variable.set(self, attr, nil) unless data_loading?
105
114
  end
@@ -107,8 +116,6 @@ module ReactiveRecord
107
116
 
108
117
  def change_status_and_notify_helper(attr, changed)
109
118
  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
119
  if !changed || data_loading?
113
120
  changed_attributes.delete(attr)
114
121
  elsif !changed_attributes.include?(attr)
@@ -125,70 +132,100 @@ module ReactiveRecord
125
132
  )
126
133
  end
127
134
 
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
- Hyperstack::Internal::State::Variable.set(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?
135
+ # when updating the inverse attribute of a belongs_to that is itself a belongs_to
136
+ # (i.e. 1-1 relationship) we clear the existing inverse value and then
137
+ # write the current record to the new value
138
+
139
+ # when updating an inverse attribute of a belongs_to that is a has_many (i.e. a collection)
140
+ # we need to first remove the current associated value (if non-nil), then add the new
141
+ # value to the collection. If the inverse collection is not yet initialized we do it here.
142
+
143
+ # the above is split into three methods, because the inverse of apolymorphic belongs to may
144
+ # change from has_one to has_many. So we first deal with the current value, then
145
+ # update the new value which uses the push_onto_collection helper
146
+
147
+ def remove_current_inverse_attribute(association, orig, model)
148
+ return if model.nil?
149
+ inverse_association = association.inverse(model)
150
+ return if inverse_association == orig
151
+ if inverse_association.collection?
152
+ # note we don't have to check if the collection exists, since it must
153
+ # exist as at this ar_instance is already part of it.
154
+ model.attributes[inverse_association.attribute].delete(@ar_instance)
149
155
  else
150
- value.backing_record.push_onto_collection(@model, association.inverse, @ar_instance)
156
+ model.attributes[inverse_association.attribute] = nil
157
+ end
158
+ end
159
+
160
+ def add_new_inverse_attribute(association, orig, model)
161
+ return if model.nil?
162
+ inverse_association = association.inverse(model)
163
+ return if inverse_association == orig
164
+ if inverse_association.collection?
165
+ model.backing_record.push_onto_collection(@model, inverse_association, @ar_instance)
166
+ else
167
+ inverse_attr = inverse_association.attribute
168
+ model.attributes[inverse_attr] = @ar_instance
169
+ return if data_loading?
170
+ Hyperstack::Internal::State::Variable.set(model.backing_record, inverse_attr, @ar_instance)
151
171
  end
152
172
  end
153
173
 
154
174
  def push_onto_collection(model, association, ar_instance)
155
175
  @attributes[association.attribute] ||= Collection.new(model, @ar_instance, association)
156
- @attributes[association.attribute] << ar_instance
176
+ @attributes[association.attribute]._internal_push ar_instance
177
+ end
178
+
179
+ # class Membership < ActiveRecord::Base
180
+ # belongs_to :uzer
181
+ # belongs_to :memerable, polymorphic: true
182
+ # end
183
+ #
184
+ # class Project < ActiveRecord::Base
185
+ # has_many :memberships, as: :memerable, dependent: :destroy
186
+ # has_many :uzers, through: :memberships
187
+ # end
188
+ #
189
+ # class Group < ActiveRecord::Base
190
+ # has_many :memberships, as: :memerable, dependent: :destroy
191
+ # has_many :uzers, through: :memberships
192
+ # end
193
+ #
194
+ # class Uzer < ActiveRecord::Base
195
+ # has_many :memberships
196
+ # has_many :groups, through: :memberships, source: :memerable, source_type: 'Group'
197
+ # has_many :projects, through: :memberships, source: :memerable, source_type: 'Project'
198
+ # end
199
+
200
+ # membership.uzer = some_new_uzer (i.e. through association is changing)
201
+ # means membership.some_new_uzer.(groups OR projects) << uzer.memberable (depending on type of memberable)
202
+ # and we have to remove the current value of the source association (memerable) from the current uzer group or project
203
+ # and we have to then find any inverse has_many_through association (i.e. group or projects.uzers) and delete the
204
+ # current value from those collections and push the new value on
205
+
206
+ def update_has_many_through_associations(assoc, orig, value, method)
207
+ return if value.nil?
208
+ assoc.through_associations(value).each do |ta|
209
+ next if orig == ta
210
+ source_value = @attributes[ta.source]
211
+ # skip if source value is nil or if type of the association does not match type of source
212
+ next unless source_value.class.to_s == ta.source_type
213
+ ta.send method, source_value, value
214
+ ta.source_associations(source_value).each do |sa|
215
+ sa.send method, value, source_value
216
+ end
217
+ end
157
218
  end
158
219
 
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
220
+ # def remove_src_assoc(sa, source_value, current_value)
221
+ # source_inverse_collection = source_value.attributes[sa.attribute]
222
+ # source_inverse_collection.delete(current_value) if source_inverse_collection
223
+ # end
224
+ #
225
+ # def add_src_assoc(sa, source_value, new_value)
226
+ # source_value.attributes[sa.attribute] ||= Collection.new(sa.owner_class, source_value, sa)
227
+ # source_value.attributes[sa.attribute] << new_value
228
+ # end
163
229
 
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
230
  end
194
231
  end
@@ -353,7 +353,7 @@ module ReactiveRecord
353
353
 
354
354
  end
355
355
 
356
- def after_mount_and_update
356
+ def after_mount_and_update(*)
357
357
  @waiting_on_resources = @Loading
358
358
  node = dom_node
359
359
  %x{
@@ -478,6 +478,27 @@ if RUBY_ENGINE == 'opal'
478
478
  reactive_record_link_set_while_loading_container_class
479
479
  end
480
480
 
481
+ # This is required to support legacy browsers (Internet Explorer 9+)
482
+ # https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
483
+ `
484
+ if (typeof(Element) != 'undefined' && !Element.prototype.matches) {
485
+ Element.prototype.matches = Element.prototype.msMatchesSelector ||
486
+ Element.prototype.webkitMatchesSelector;
487
+ }
488
+
489
+ if (typeof(Element) != 'undefined' && !Element.prototype.closest) {
490
+ Element.prototype.closest = function(s) {
491
+ var el = this;
492
+
493
+ do {
494
+ if (el.matches(s)) return el;
495
+ el = el.parentElement || el.parentNode;
496
+ } while (el !== null && el.nodeType === 1);
497
+ return null;
498
+ };
499
+ }
500
+ `
501
+
481
502
  def reactive_record_link_to_enclosing_while_loading_container
482
503
  # Call after any component mounts - attaches the containers loading id to this component
483
504
  # Fyi, the while_loading container is responsible for setting its own link to itself
@@ -6,27 +6,35 @@ module ReactiveRecord
6
6
  # before the first broadcast.
7
7
  @public_columns_hash ||= ActiveRecord::Base.public_columns_hash
8
8
  Hyperstack::InternalPolicy.regulate_broadcast(model) do |data|
9
+ puts "Broadcast aftercommit hook: #{data}" if Hyperstack::Connection.show_diagnostics
10
+
9
11
  if !Hyperstack.on_server? && Hyperstack::Connection.root_path
10
- send_to_server(operation, data) rescue nil # fails if server no longer running so ignore
12
+ send_to_server(operation, data, model.__synchromesh_update_time) rescue nil # fails if server no longer running so ignore
11
13
  else
12
- SendPacket.run(data, operation: operation)
14
+ SendPacket.run(data, operation: operation, updated_at: model.__synchromesh_update_time)
13
15
  end
14
16
  end
15
17
  rescue ActiveRecord::StatementInvalid => e
16
18
  raise e unless e.message == "Could not find table 'hyperstack_connections'"
17
19
  end unless RUBY_ENGINE == 'opal'
18
20
 
19
- def self.send_to_server(operation, data)
21
+ def self.send_to_server(operation, data, updated_at)
20
22
  salt = SecureRandom.hex
21
23
  authorization = Hyperstack.authorization(salt, data[:channel], data[:broadcast_id])
22
24
  raise 'no server running' unless Hyperstack::Connection.root_path
23
- SendPacket.remote(
24
- Hyperstack::Connection.root_path,
25
- data,
26
- operation: operation,
27
- salt: salt,
28
- authorization: authorization
29
- ).tap { |p| raise p.error if p.rejected? }
25
+ Timeout::timeout(Hyperstack.send_to_server_timeout) do
26
+ SendPacket.remote(
27
+ Hyperstack::Connection.root_path,
28
+ data,
29
+ operation: operation,
30
+ updated_at: updated_at,
31
+ salt: salt,
32
+ authorization: authorization
33
+ ).tap { |p| raise p.error if p.rejected? }
34
+ end
35
+ rescue Timeout::Error
36
+ puts "\n********* FAILED TO RECEIVE RESPONSE FROM SERVER WITHIN #{Hyperstack.send_to_server_timeout} SECONDS. CHANGES WILL NOT BE SYNCED ************\n"
37
+ raise 'no server running'
30
38
  end unless RUBY_ENGINE == 'opal'
31
39
 
32
40
  class SendPacket < Hyperstack::ServerOp
@@ -40,6 +48,7 @@ module ReactiveRecord
40
48
  param :record
41
49
  param :operation
42
50
  param :previous_changes
51
+ param :updated_at
43
52
 
44
53
  unless RUBY_ENGINE == 'opal'
45
54
  validate do
@@ -63,11 +72,11 @@ module ReactiveRecord
63
72
  ReactiveRecord::Collection.sync_scopes broadcast.process_previous_changes
64
73
  end
65
74
  end
66
- end
75
+ end if RUBY_ENGINE == 'opal'
67
76
 
68
77
  def self.to_self(record, data = {})
69
78
  # simulate incoming packet after a local save
70
- operation = if record.new?
79
+ operation = if record.new_record?
71
80
  :create
72
81
  elsif record.destroyed?
73
82
  :destroy
@@ -81,7 +90,7 @@ module ReactiveRecord
81
90
 
82
91
  def record_with_current_values
83
92
  ReactiveRecord::Base.load_data do
84
- backing_record = @backing_record || klass.find(record[:id]).backing_record
93
+ backing_record = @backing_record || klass.find(record[klass.primary_key]).backing_record
85
94
  if destroyed?
86
95
  backing_record.ar_instance
87
96
  else
@@ -108,6 +117,10 @@ module ReactiveRecord
108
117
  @destroyed
109
118
  end
110
119
 
120
+ def local?
121
+ @is_local
122
+ end
123
+
111
124
  def klass
112
125
  Object.const_get(@klass)
113
126
  end
@@ -119,6 +132,7 @@ module ReactiveRecord
119
132
  # private
120
133
 
121
134
  attr_reader :record
135
+ attr_reader :updated_at
122
136
 
123
137
  def self.open_channels
124
138
  @open_channels ||= Set.new
@@ -128,7 +142,7 @@ module ReactiveRecord
128
142
  @in_transit ||= Hash.new { |h, k| h[k] = new(k) }
129
143
  end
130
144
 
131
- def initialize(id)
145
+ def initialize(id = nil)
132
146
  @id = id
133
147
  @received = Set.new
134
148
  @record = {}
@@ -137,19 +151,15 @@ module ReactiveRecord
137
151
 
138
152
  def local(operation, record, data)
139
153
  @destroyed = operation == :destroy
154
+ @is_local = true
140
155
  @is_new = operation == :create
141
156
  @klass = record.class.name
142
157
  @record = data
143
158
  record.backing_record.destroyed = false
144
- @record[:id] = record.id if record.id
159
+ @record[record.primary_key] = record.id if record.id
145
160
  record.backing_record.destroyed = @destroyed
146
161
  @backing_record = record.backing_record
147
162
  @previous_changes = record.changes
148
- # attributes = record.attributes
149
- # data.each do |k, v|
150
- # next if klass.reflect_on_association(k) || attributes[k] == v
151
- # @previous_changes[k] = [attributes[k], v]
152
- # end
153
163
  self
154
164
  end
155
165
 
@@ -160,10 +170,35 @@ module ReactiveRecord
160
170
  @klass ||= params.klass
161
171
  @record.merge! params.record
162
172
  @previous_changes.merge! params.previous_changes
173
+ @updated_at = params.updated_at
163
174
  ReactiveRecord::Base.when_not_saving(klass) do
164
- @backing_record = ReactiveRecord::Base.exists?(klass, params.record[:id])
165
- @is_new = params.operation == :create && !@backing_record
166
- yield complete! if @channels == @received
175
+ @backing_record = ReactiveRecord::Base.exists?(klass, params.record[klass.primary_key])
176
+
177
+ # first check to see if we already destroyed it and if so exit the block
178
+ break if @backing_record&.destroyed
179
+
180
+ # We ignore whether the record is being created or not, and just check and see if in our
181
+ # local copy we have ever loaded this id before. If we have then its not new to us.
182
+ # BUT if we are destroying a record then it can't be treated as new regardless.
183
+ # This is because we might be just doing a count on a scope and so no actual records will
184
+ # exist. Treating a destroyed record as "new" would cause us to first increment the
185
+ # scope counter and then decrement for the destroy, resulting in a nop instead of a -1 on
186
+ # the scope count.
187
+ @is_new = !@backing_record&.id_loaded? && !@destroyed
188
+
189
+ # it is possible that we are recieving data on a record for which we are also waiting
190
+ # on an an inital data load in which case we have not yet set the loaded id, so we
191
+ # set if now.
192
+ @backing_record&.loaded_id = params.record[klass.primary_key]
193
+
194
+ # once we have received all the data from all the channels (applies to create and update only)
195
+ # we yield and process the record
196
+
197
+ # pusher fake can send duplicate records which will result in a nil broadcast
198
+ # so we also check that before yielding
199
+ if @channels == @received && (broadcast = complete!)
200
+ yield broadcast
201
+ end
167
202
  end
168
203
  end
169
204
 
@@ -208,7 +243,7 @@ module ReactiveRecord
208
243
 
209
244
  def merge_current_values(br)
210
245
  current_values = Hash[*@previous_changes.collect do |attr, values|
211
- value = attr == :id ? record[:id] : values.first
246
+ value = attr == klass.primary_key ? record[klass.primary_key] : values.first
212
247
  if br.attributes.key?(attr) &&
213
248
  br.attributes[attr] != br.convert(attr, value) &&
214
249
  br.attributes[attr] != br.convert(attr, values.last)
@@ -219,7 +254,6 @@ module ReactiveRecord
219
254
  end
220
255
  [attr, value]
221
256
  end.compact.flatten(1)]
222
- # TODO: verify - it used to be current_values.merge(br.attributes)
223
257
  klass._react_param_conversion(br.attributes.merge(current_values))
224
258
  end
225
259
  end