hyper-model 1.0.alpha1.4 → 1.0.alpha1.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)
@@ -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
@@ -106,9 +115,8 @@ module ReactiveRecord
106
115
  end
107
116
 
108
117
  def change_status_and_notify_helper(attr, changed)
118
+ return if @being_destroyed
109
119
  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
120
  if !changed || data_loading?
113
121
  changed_attributes.delete(attr)
114
122
  elsif !changed_attributes.include?(attr)
@@ -125,70 +133,100 @@ module ReactiveRecord
125
133
  )
126
134
  end
127
135
 
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?
136
+ # when updating the inverse attribute of a belongs_to that is itself a belongs_to
137
+ # (i.e. 1-1 relationship) we clear the existing inverse value and then
138
+ # write the current record to the new value
139
+
140
+ # when updating an inverse attribute of a belongs_to that is a has_many (i.e. a collection)
141
+ # we need to first remove the current associated value (if non-nil), then add the new
142
+ # value to the collection. If the inverse collection is not yet initialized we do it here.
143
+
144
+ # the above is split into three methods, because the inverse of apolymorphic belongs to may
145
+ # change from has_one to has_many. So we first deal with the current value, then
146
+ # update the new value which uses the push_onto_collection helper
147
+
148
+ def remove_current_inverse_attribute(association, orig, model)
149
+ return if model.nil?
150
+ inverse_association = association.inverse(model)
151
+ return if inverse_association == orig
152
+ if inverse_association.collection?
153
+ # note we don't have to check if the collection exists, since it must
154
+ # exist as at this ar_instance is already part of it.
155
+ model.attributes[inverse_association.attribute].delete(@ar_instance)
149
156
  else
150
- value.backing_record.push_onto_collection(@model, association.inverse, @ar_instance)
157
+ model.attributes[inverse_association.attribute] = nil
158
+ end
159
+ end
160
+
161
+ def add_new_inverse_attribute(association, orig, model)
162
+ return if model.nil?
163
+ inverse_association = association.inverse(model)
164
+ return if inverse_association == orig
165
+ if inverse_association.collection?
166
+ model.backing_record.push_onto_collection(@model, inverse_association, @ar_instance)
167
+ else
168
+ inverse_attr = inverse_association.attribute
169
+ model.attributes[inverse_attr] = @ar_instance
170
+ return if data_loading?
171
+ Hyperstack::Internal::State::Variable.set(model.backing_record, inverse_attr, @ar_instance)
151
172
  end
152
173
  end
153
174
 
154
175
  def push_onto_collection(model, association, ar_instance)
155
176
  @attributes[association.attribute] ||= Collection.new(model, @ar_instance, association)
156
- @attributes[association.attribute] << ar_instance
177
+ @attributes[association.attribute]._internal_push ar_instance
178
+ end
179
+
180
+ # class Membership < ActiveRecord::Base
181
+ # belongs_to :uzer
182
+ # belongs_to :memerable, polymorphic: true
183
+ # end
184
+ #
185
+ # class Project < ActiveRecord::Base
186
+ # has_many :memberships, as: :memerable, dependent: :destroy
187
+ # has_many :uzers, through: :memberships
188
+ # end
189
+ #
190
+ # class Group < ActiveRecord::Base
191
+ # has_many :memberships, as: :memerable, dependent: :destroy
192
+ # has_many :uzers, through: :memberships
193
+ # end
194
+ #
195
+ # class Uzer < ActiveRecord::Base
196
+ # has_many :memberships
197
+ # has_many :groups, through: :memberships, source: :memerable, source_type: 'Group'
198
+ # has_many :projects, through: :memberships, source: :memerable, source_type: 'Project'
199
+ # end
200
+
201
+ # membership.uzer = some_new_uzer (i.e. through association is changing)
202
+ # means membership.some_new_uzer.(groups OR projects) << uzer.memberable (depending on type of memberable)
203
+ # and we have to remove the current value of the source association (memerable) from the current uzer group or project
204
+ # and we have to then find any inverse has_many_through association (i.e. group or projects.uzers) and delete the
205
+ # current value from those collections and push the new value on
206
+
207
+ def update_has_many_through_associations(assoc, orig, value, method)
208
+ return if value.nil?
209
+ assoc.through_associations(value).each do |ta|
210
+ next if orig == ta
211
+ source_value = @attributes[ta.source]
212
+ # skip if source value is nil or if type of the association does not match type of source
213
+ next unless source_value.class.to_s == ta.source_type
214
+ ta.send method, source_value, value
215
+ ta.source_associations(source_value).each do |sa|
216
+ sa.send method, value, source_value
217
+ end
218
+ end
157
219
  end
158
220
 
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
221
+ # def remove_src_assoc(sa, source_value, current_value)
222
+ # source_inverse_collection = source_value.attributes[sa.attribute]
223
+ # source_inverse_collection.delete(current_value) if source_inverse_collection
224
+ # end
225
+ #
226
+ # def add_src_assoc(sa, source_value, new_value)
227
+ # source_value.attributes[sa.attribute] ||= Collection.new(sa.owner_class, source_value, sa)
228
+ # source_value.attributes[sa.attribute] << new_value
229
+ # end
163
230
 
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
231
  end
194
232
  end
@@ -20,13 +20,18 @@ module ReactiveRecord
20
20
  salt = SecureRandom.hex
21
21
  authorization = Hyperstack.authorization(salt, data[:channel], data[:broadcast_id])
22
22
  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? }
23
+ Timeout::timeout(Hyperstack.send_to_server_timeout) do
24
+ SendPacket.remote(
25
+ Hyperstack::Connection.root_path,
26
+ data,
27
+ operation: operation,
28
+ salt: salt,
29
+ authorization: authorization
30
+ ).tap { |p| raise p.error if p.rejected? }
31
+ end
32
+ rescue Timeout::Error
33
+ puts "\n********* FAILED TO RECEIVE RESPONSE FROM SERVER WITHIN #{Hyperstack.send_to_server_timeout} SECONDS. CHANGES WILL NOT BE SYNCED ************\n"
34
+ raise 'no server running'
30
35
  end unless RUBY_ENGINE == 'opal'
31
36
 
32
37
  class SendPacket < Hyperstack::ServerOp
@@ -67,7 +72,7 @@ module ReactiveRecord
67
72
 
68
73
  def self.to_self(record, data = {})
69
74
  # simulate incoming packet after a local save
70
- operation = if record.new?
75
+ operation = if record.new_record?
71
76
  :create
72
77
  elsif record.destroyed?
73
78
  :destroy
@@ -177,7 +182,12 @@ module ReactiveRecord
177
182
 
178
183
  # once we have received all the data from all the channels (applies to create and update only)
179
184
  # we yield and process the record
180
- yield complete! if @channels == @received
185
+
186
+ # pusher fake can send duplicate records which will result in a nil broadcast
187
+ # so we also check that before yielding
188
+ if @channels == @received && (broadcast = complete!)
189
+ yield broadcast
190
+ end
181
191
  end
182
192
  end
183
193
 
@@ -168,12 +168,12 @@ end
168
168
  module Kernel
169
169
  # (see Browser::Window#after)
170
170
  def after(time, &block)
171
- `setTimeout(#{block.to_n}, time * 1000)`
171
+ $window.after(time, &block)
172
172
  end
173
173
 
174
174
  # (see Browser::Window#after!)
175
175
  def after!(time, &block)
176
- `setTimeout(#{block.to_n}, time * 1000)`
176
+ $window.after!(time, &block)
177
177
  end
178
178
  end
179
179
 
@@ -187,4 +187,4 @@ class Proc
187
187
  def after!(time)
188
188
  $window.after!(time, &self)
189
189
  end
190
- end
190
+ end
@@ -93,8 +93,9 @@ module ReactiveRecord
93
93
  vector = []
94
94
  path.split('.').inject(@model) do |model, attribute|
95
95
  association = model.reflect_on_association(attribute)
96
- raise build_error(path, model, attribute) unless association
97
- vector = [association.inverse_of, *vector]
96
+ inverse_of = association.inverse_of if association
97
+ raise build_error(path, model, attribute) unless inverse_of
98
+ vector = [inverse_of, *vector]
98
99
  @joins[association.klass] << vector
99
100
  association.klass
100
101
  end
@@ -280,6 +280,8 @@ module ReactiveRecord
280
280
  if !cache_item.value || cache_item.value.is_a?(Array)
281
281
  # seeing as we just returning representative, no check is needed (its already checked)
282
282
  representative
283
+ elsif method == 'model_name'
284
+ cache_item.build_new_cache_item(timing(:active_record) { cache_item.value.model_name }, method, method)
283
285
  else
284
286
  begin
285
287
  secured_method = "__secure_remote_access_to_#{[*method].first}"
@@ -449,6 +451,13 @@ keys:
449
451
  loaded_collection[0].backing_record.sync_attributes(attrs)
450
452
  end
451
453
  target.replace loaded_collection
454
+ # we need to notify any observers of the collection. collection#replace
455
+ # will not notify if we are data_loading (which we are) so we will do it
456
+ # here. BUT we want the notification to occur after the current event
457
+ # completes so we wrap it a bulk_update
458
+ Hyperstack::Internal::State::Mapper.bulk_update do
459
+ Hyperstack::Internal::State::Variable.set(target, :collection, target.collection)
460
+ end
452
461
  end
453
462
 
454
463
  if id_value = tree["id"] and id_value.is_a? Array
@@ -486,9 +495,10 @@ keys:
486
495
  # we cannot use target.send "#{method}=" here because it might be a server method, which does not have a setter
487
496
  # a better fix might be something like target._internal_attribute_hash[method] = ...
488
497
  target.backing_record.set_attr_value(method, value.first) unless method == :id
489
- elsif value.is_a? Hash and value[:id] and value[:id].first and association = target.class.reflect_on_association(method)
498
+ elsif value.is_a?(Hash) && value[:id] && value[:id].first && (association = target.class.reflect_on_association(method))
490
499
  # not sure if its necessary to check the id above... is it possible to for the method to be an association but not have an id?
491
- new_target = ReactiveRecord::Base.find_by_id(association.klass, value[:id].first)
500
+ klass = value[:model_name] ? Object.const_get(value[:model_name].first) : association.klass
501
+ new_target = ReactiveRecord::Base.find_by_id(klass, value[:id].first)
492
502
  target.send "#{method}=", new_target
493
503
  elsif !(target.class < ActiveRecord::Base)
494
504
  new_target = target.send(*method)
@@ -500,7 +510,6 @@ keys:
500
510
  load_from_json(value, new_target) if new_target
501
511
  end
502
512
  rescue Exception => e
503
- # debugger
504
513
  raise e
505
514
  end
506
515
  end
@@ -0,0 +1,143 @@
1
+ ```ruby
2
+ class Picture < ApplicationRecord
3
+ belongs_to :imageable, polymorphic: true
4
+ end
5
+
6
+ class Employee < ApplicationRecord
7
+ has_many :pictures, as: :imageable
8
+ end
9
+
10
+ class Product < ApplicationRecord
11
+ has_many :pictures, as: :imageable
12
+ end
13
+ ```
14
+
15
+ product|employee.pictures -> works almost as normal has_many as far as Hyperstack client is concerned
16
+ imageable is the "alias" of product|employee. Its as if there is a class Imageable that is the superclass
17
+ of Product and Employee.
18
+
19
+ so has_many :pictures means the usual thing (i.e. there is a belongs_to relationship on Picture) its just that
20
+ the belongs_to will be belonging to :imageable instead of :employee or :product.
21
+
22
+ okay fine
23
+
24
+ the other way:
25
+
26
+ the problem is that picture.imageable while loading is pointing to a dummy class (sure call it Imageable)
27
+ so if we say picture.imageable.foo.bar.blat what we get is a dummy value that responds to all methods, and returns itself:
28
+
29
+ picture.imageable -> imageable123 .foo -> imageable123 .bar -> ... etc. but it is a dummy value that will cause a fetch of the actual imageable record (or nil).
30
+
31
+ .imageable should be able to leverage off of server_method.
32
+
33
+ server_method(:imageable, PolymorphicDummy.new(:imageable))
34
+
35
+ hmmmm....
36
+
37
+ really its like doing a picture.imageable.itself (?) (that may work Juuuust fine)
38
+
39
+ so picture.imageable returns this funky dummy value but does an across the wire request for picture.imageable (which should get imageable_id per a normal relationship) and also get picture.imageable_type.
40
+
41
+
42
+ start again....
43
+
44
+ what happens if we ignore (on the client) the polymorphic: and as: keys?
45
+
46
+ belongs_to :imageable
47
+
48
+ means there is a class Imageable, okay so we make one, and add has_many :pictures to it.
49
+
50
+
51
+ and again....
52
+
53
+ ```ruby
54
+ def imageable
55
+ if imageable_type.loaded? && imageable_id.loaded?
56
+ const_get(imageable_type).find(imageable_id)
57
+ else
58
+ DummyImageable.new(self)
59
+ end
60
+ end
61
+ ```
62
+
63
+ very close but will not work for cases like this:
64
+
65
+ ```ruby
66
+ pic = Picture.new
67
+ employee.pictures << pic
68
+ pic.imageable # FAIL... (until its been saved)
69
+ ...
70
+ ```
71
+
72
+ but still it may be as simple as overriding `<<` so that it sets type on imageable. But we still to have a proper belongs to relationship.
73
+
74
+ ```ruby
75
+ def imageable
76
+ if we already have the attribute set
77
+ return the attribute
78
+ else
79
+ set attribute to DummyPolyClass.new(self, 'imageable')
80
+ # DummyPolyClass init will set up a fetch of the actual imageable value
81
+ end
82
+ end
83
+
84
+ def imageable=(x)
85
+ # will it just work ?
86
+ end
87
+ ```
88
+
89
+ its all about the collection inverse. The inverse class of the has_many is the class containing the polymorphic belongs to. But the inverse of a polymorphic belongs to depends on the value. If the value is nil or a DummyPolyClass object then there is no inverse.
90
+
91
+ I think if inverse takes this into account then `<<` and `=` should just "work" (well almost) and probably everything else will to.
92
+
93
+ ### NOTES on the DummyPolyClass...
94
+
95
+ it needs to respond to reflect_on_all_associations, but just return an empty array. This way when we search for matching inverse attribute we won't find it.
96
+
97
+ ### Status
98
+
99
+ added model to inverse, inverse_of, find_inverse
100
+
101
+ if the relationship is a collection then we will always know the inverse.
102
+
103
+ The only time we might no know the inverse is if its NOT a collection (i.e. belongs_to)
104
+
105
+ So only places that are applying inverse to an association that is NOT a collection do we have to pass the model in.
106
+
107
+ All inverse_of method calls have been checked and updated
108
+
109
+ that leaves inverse which is only used in SETTERS hurray!
110
+
111
+
112
+ ### Latest thinking
113
+
114
+ going from `has_many / has_one as: ...` is easy its essentially setting the association foreign_key using the name supplied to the as:
115
+
116
+ The problem is going from the polymorphic belongs_to side.
117
+
118
+ We don't know the actual type we are loading which presents two problems.
119
+
120
+ First we just don't know the type. So if I say `Picture.find(1).imageable.foo.bar` I really can't do anything with foo and bar. This is solved by having a DummyPolymorph class, which responds to all missing methods with itself, and on creation sets up a vector to pull it the id, and type of the record being fetched. This will cause a second fetch to actually get `foo.bar` because we don't know what they are yet. (Its cool beacuse this is like Type inference actually, and I think we could eventually use a type inference system to get rid of the second fetch!!!)
121
+
122
+ Second we don't know the inverse of the relationship (since we don't know the type)
123
+
124
+ We can solve this by aliasing the inverse relationship (the one with the `as: SOMENAME` option) to be `has_many #{__hyperstack_polymorphic_inverse_of_#{SOMENAME}` and then defining method(s) against the relationship name. This way regardless of what the polymorphic relationship points to we know the inverse is `__hyperstack_polymorphic_inverse_of_#{SOMENAME}`.
125
+
126
+ If the inverse relationship is a has_many then we define
127
+ ```ruby
128
+ def #{RELATIONSHIP_NAME}
129
+ __hyperstack_polymorphic_inverse_of_#{SOMENAME}
130
+ end
131
+ ```
132
+
133
+ If the inverse relationship is a has_one we have to work a bit harder:
134
+ ```ruby
135
+ def #{RELATIONSHIP_NAME}
136
+ __hyperstack_polymorphic_inverse_of_#{SOMENAME}[0]
137
+ end
138
+ def #{RELATIONSHIP_NAME}=(x)
139
+ __hyperstack_polymorphic_inverse_of_#{SOMENAME}[0] = x # or perhaps we have to replace the array using the internal method in collection for that purpose.
140
+ end
141
+ ```
142
+
143
+ The remaining problem is that the server side will have no such relationships defined so we need to add the `has_many __hyperstack_polymorphic_inverse_of_#{SOMENAME} as: SOMENAME` server side.