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

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.
@@ -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.