Class: ReactiveRecord::Collection
- Defined in:
- lib/reactive_record/active_record/reactive_record/collection.rb
Defined Under Namespace
Classes: DummySet
Instance Attribute Summary collapse
-
#client_collection ⇒ Object
readonly
Returns the value of attribute client_collection.
-
#parent ⇒ Object
writeonly
Sets the attribute parent.
-
#pre_sync_related_records ⇒ Object
readonly
Returns the value of attribute pre_sync_related_records.
-
#scope_description ⇒ Object
writeonly
Sets the attribute scope_description.
-
#vector ⇒ Object
readonly
todo move following to a separate module related to scope updates ******************.
Class Method Summary collapse
- .apply_to_all_collections(method, record, dont_gather) ⇒ Object
-
.sync_scopes(broadcast) ⇒ Object
sync_scopes takes a newly broadcasted record change and updates all relevant currently active scopes This is particularly hard when the client proc is specified.
Instance Method Summary collapse
- #==(other_collection) ⇒ Object
- #[](index) ⇒ Object
- #_count_internal(load_from_client) ⇒ Object
- #all ⇒ Object
- #apply_scope(name, *vector) ⇒ Object
- #build_child_scope(scope_description, *scope_vector) ⇒ Object
- #child_scopes ⇒ Object
-
#collect(*args, &block) ⇒ Object
WHY IS THIS NEEDED? Perhaps it was just for debug.
- #collector? ⇒ Boolean
- #count ⇒ Object (also: #length)
- #delete(item) ⇒ Object
- #delete_internal(item) ⇒ Object
- #dup_for_sync ⇒ Object
- #empty? ⇒ Boolean
- #filter? ⇒ Boolean
- #filter_records(related_records) ⇒ Object
- #force_push(item) ⇒ Object
- #gather_related_records(record, related_records = Set.new) ⇒ Object
-
#initialize(target_klass, owner = nil, association = nil, *vector) ⇒ Collection
constructor
A new instance of Collection.
- #internal_replace(new_array) ⇒ Object
-
#joins_with?(record) ⇒ Boolean
is it necessary to check @association in the next 2 methods???.
- #klass ⇒ Object
- #link_child(child) ⇒ Object
- #link_to_parent ⇒ Object
- #live_scopes ⇒ Object
- #loading? ⇒ Boolean
- #merge_related_records(record, related_records) ⇒ Object
- #method_missing(method, *args, &block) ⇒ Object
- #observed ⇒ Object
-
#proxy_association ⇒ Object
def each_known_child [*collection, *client_pushes].each { |i| yield i } end.
- #push(item) ⇒ Object (also: #<<)
- #push_and_update_belongs_to(id) ⇒ Object
- #related_records_for(record) ⇒ Object
-
#reload_from_db(force = nil) ⇒ Object
end of stuff to move.
- #replace(new_array) ⇒ Object
- #set_belongs_to(child) ⇒ Object
- #set_count_state(val) ⇒ Object
- #set_pre_sync_related_records(related_records, _record = nil) ⇒ Object
- #sort!(*args, &block) ⇒ Object
- #sync_collection_with_parent ⇒ Object
-
#sync_scopes(related_records, record, filtering = true) ⇒ Object
NOTE sync_scopes is overridden in scope_description.rb.
- #to_s ⇒ Object
- #unsaved_children ⇒ Object
-
#update_child(item) ⇒ Object
appointment.doctor = doctor_value (i.e. through association is changing) means appointment.doctor_value.patients << appointment.patient and we have to appointment.doctor(current value).patients.delete(appointment.patient).
Constructor Details
#initialize(target_klass, owner = nil, association = nil, *vector) ⇒ Collection
Returns a new instance of Collection.
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 28 def initialize(target_klass, owner = nil, association = nil, *vector) @owner = owner # can be nil if this is an outer most scope @association = association @target_klass = target_klass if owner and !owner.id and vector.length <= 1 @collection = [] elsif vector.length > 0 @vector = vector elsif owner @vector = owner.backing_record.vector + [association.attribute] else @vector = [target_klass] end @scopes = {} end |
Dynamic Method Handling
This class handles dynamic methods through the method_missing method
#method_missing(method, *args, &block) ⇒ Object
523 524 525 526 527 528 529 530 531 532 533 534 535 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 523 def method_missing(method, *args, &block) if [].respond_to? method all.send(method, *args, &block) elsif ScopeDescription.find(@target_klass, method) apply_scope(method, *args) elsif args.count == 1 && method.start_with?('find_by_') apply_scope(:find_by, method.sub(/^find_by_/, '') => args.first) elsif @target_klass.respond_to?(method) && ScopeDescription.find(@target_klass, "_#{method}") apply_scope("_#{method}", *args).first else super end end |
Instance Attribute Details
#client_collection ⇒ Object (readonly)
Returns the value of attribute client_collection.
384 385 386 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 384 def client_collection @client_collection end |
#parent=(value) ⇒ Object (writeonly)
Sets the attribute parent
97 98 99 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 97 def parent=(value) @parent = value end |
#pre_sync_related_records ⇒ Object (readonly)
Returns the value of attribute pre_sync_related_records.
98 99 100 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 98 def @pre_sync_related_records end |
#scope_description=(value) ⇒ Object (writeonly)
Sets the attribute scope_description
96 97 98 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 96 def scope_description=(value) @scope_description = value end |
#vector ⇒ Object (readonly)
todo move following to a separate module related to scope updates ******************
95 96 97 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 95 def vector @vector end |
Class Method Details
.apply_to_all_collections(method, record, dont_gather) ⇒ Object
150 151 152 153 154 155 156 157 158 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 150 def apply_to_all_collections(method, record, dont_gather) = Set.new if dont_gather Base.outer_scopes.each do |collection| unless dont_gather = collection.(record) end collection.send method, , record end end |
.sync_scopes(broadcast) ⇒ Object
sync_scopes takes a newly broadcasted record change and updates all relevant currently active scopes This is particularly hard when the client proc is specified. For example consider this scope:
class TestModel < ApplicationRecord
scope :quicker, -> { where(completed: true) }, client: -> { completed }
end
and this slice of reactive code:
DIV { "quicker.count = #{TestModel.quicker.count}" }
then on the server this code is executed:
TestModel.last.update(completed: false)
This will result in the changes being broadcast to the client, which may cauase the value of TestModel.quicker.count to increase or decrease. Of course we may not actually have the all the records, perhaps we just have the aggregate count.
To determine this sync_scopes first asks if the record being changed is in the scope given its value
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 130 def sync_scopes(broadcast) # record_with_current_values will return nil if data between # the broadcast record and the value on the client is out of sync # not running set_pre_sync_related_records will cause sync scopes # to refresh all related scopes React::State.bulk_update do record = broadcast.record_with_current_values apply_to_all_collections( :set_pre_sync_related_records, record, broadcast.new? ) if record record = broadcast.record_with_new_values apply_to_all_collections( :sync_scopes, record, record.destroyed? ) record.backing_record.sync_unscoped_collection! if record.destroyed? || broadcast.new? end end |
Instance Method Details
#==(other_collection) ⇒ Object
81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 81 def ==(other_collection) observed return !@collection unless other_collection.is_a? Collection other_collection.observed my_children = (@collection || []).select { |target| target != @dummy_record } if other_collection other_children = (other_collection.collection || []).select { |target| target != other_collection.dummy_record } return false unless my_children == other_children unsaved_children.to_a == other_collection.unsaved_children.to_a else my_children.empty? && unsaved_children.empty? end end |
#[](index) ⇒ Object
69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 69 def [](index) observed if (@collection || all).length <= index and @dummy_collection (@collection.length..index).each do |i| new_dummy_record = ReactiveRecord::Base.new_from_vector(@target_klass, nil, *@vector, "*#{i}") new_dummy_record.attributes[@association.inverse_of] = @owner if @association && !@association.through_association? @collection << new_dummy_record end end @collection[index] end |
#_count_internal(load_from_client) ⇒ Object
326 327 328 329 330 331 332 333 334 335 336 337 338 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 326 def _count_internal(load_from_client) # when count is called on a leaf, count_internal is called for each # ancestor. Only the outermost count has load_from_client == true observed if @collection @collection.count elsif @count ||= ReactiveRecord::Base.fetch_from_db([*@vector, "*count"]) @count else ReactiveRecord::Base.load_from_db(nil, *@vector, "*count") if load_from_client @count = 1 end end |
#all ⇒ Object
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 52 def all observed @dummy_collection.notify if @dummy_collection unless @collection @collection = [] if ids = ReactiveRecord::Base.fetch_from_db([*@vector, "*all"]) ids.each do |id| @collection << @target_klass.find_by(@target_klass.primary_key => id) end else @dummy_collection = ReactiveRecord::Base.load_from_db(nil, *@vector, "*all") @dummy_record = self[0] end end @collection end |
#apply_scope(name, *vector) ⇒ Object
240 241 242 243 244 245 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 240 def apply_scope(name, *vector) description = ScopeDescription.find(@target_klass, name) collection = build_child_scope(description, *description.name, *vector) collection.reload_from_db if name == "#{description.name}!" collection end |
#build_child_scope(scope_description, *scope_vector) ⇒ Object
251 252 253 254 255 256 257 258 259 260 261 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 251 def build_child_scope(scope_description, *scope_vector) child_scopes[scope_vector] ||= begin new_vector = @vector new_vector += [scope_vector] unless new_vector.nil? || scope_vector.empty? child_scope = Collection.new(@target_klass, nil, nil, *new_vector) child_scope.scope_description = scope_description child_scope.parent = self child_scope.extend ScopedCollection child_scope end end |
#child_scopes ⇒ Object
247 248 249 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 247 def child_scopes @child_scopes ||= {} end |
#collect(*args, &block) ⇒ Object
WHY IS THIS NEEDED? Perhaps it was just for debug
347 348 349 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 347 def collect(*args, &block) all.collect(*args, &block) end |
#collector? ⇒ Boolean
210 211 212 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 210 def collector? false end |
#count ⇒ Object Also known as: length
340 341 342 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 340 def count _count_internal(true) end |
#delete(item) ⇒ Object
486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 486 def delete(item) unsaved_children.delete(item) notify_of_change( if @owner && @association && !@association.through_association? inverse_of = @association.inverse_of if (backing_record = item.backing_record) && item.attributes[inverse_of] == @owner # the if prevents double update if delete is being called from << (see << above) backing_record.update_belongs_to(inverse_of, nil) end delete_internal(item) { @owner.backing_record.sync_has_many(@association.attribute) } else delete_internal(item) end ) end |
#delete_internal(item) ⇒ Object
502 503 504 505 506 507 508 509 510 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 502 def delete_internal(item) if collection all.delete(item) elsif !@count.nil? @count -= 1 end yield if block_given? # was yield item, but item is not used item end |
#dup_for_sync ⇒ Object
44 45 46 47 48 49 50 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 44 def dup_for_sync self.dup.instance_eval do @collection = @collection.dup if @collection @scopes = @scopes.dup self end end |
#empty? ⇒ Boolean
517 518 519 520 521 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 517 def empty? # should be handled by method missing below, but opal-rspec does not deal well # with method missing, so to test... all.empty? end |
#filter? ⇒ Boolean
176 177 178 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 176 def filter? true end |
#filter_records(related_records) ⇒ Object
214 215 216 217 218 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 214 def filter_records() # possibly we should never get here??? scope_args = @vector.last.is_a?(Array) ? @vector.last[1..-1] : [] @scope_description.filter_records(, scope_args) end |
#force_push(item) ⇒ Object
426 427 428 429 430 431 432 433 434 435 436 437 438 439 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 426 def force_push(item) return delete(item) if item.destroyed? # pushing a destroyed item is the same as removing it all << item unless all.include? item # does this use == if so we are okay... update_child(item) if item.id and @dummy_record @dummy_record.id = item.id # we cant use == because that just means the objects are referencing # the same backing record. @collection.reject { |i| i.object_id == @dummy_record.object_id } @dummy_record = @collection.detect { |r| r.backing_record.vector.last =~ /^\*[0-9]+$/ } @dummy_collection = nil end notify_of_change self end |
#gather_related_records(record, related_records = Set.new) ⇒ Object
161 162 163 164 165 166 167 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 161 def (record, = Set.new) (record, ) live_scopes.each do |collection| collection.(record, ) end end |
#internal_replace(new_array) ⇒ Object
459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 459 def internal_replace(new_array) # not tested if you do all[n] where n > 0... this will create additional dummy items, that this will not sync up. # probably just moving things around so the @dummy_collection and @dummy_record are updated AFTER the new items are pushed # should work. if @dummy_collection @dummy_collection.notify array = new_array.is_a?(Collection) ? new_array.collection : new_array @collection.each_with_index do |r, i| r.id = new_array[i].id if array[i] and array[i].id and !r.new? and r.backing_record.vector.last =~ /^\*[0-9]+$/ end end @collection.dup.each { |item| delete(item) } if @collection # this line is a big nop I think @collection = [] if new_array.is_a? Collection @dummy_collection = new_array.dummy_collection @dummy_record = new_array.dummy_record new_array.collection.each { |item| self << item } if new_array.collection else @dummy_collection = @dummy_record = nil new_array.each { |item| self << item } end notify_of_change new_array end |
#joins_with?(record) ⇒ Boolean
is it necessary to check @association in the next 2 methods???
182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 182 def joins_with?(record) klass = record.class if @association&.through_association @association.through_association.klass == record.class elsif @target_klass == klass true elsif !klass.inheritance_column false elsif klass.base_class == @target_class klass < @target_klass elsif klass.base_class == klass @target_klass < klass end end |
#klass ⇒ Object
359 360 361 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 359 def klass @target_klass end |
#link_child(child) ⇒ Object
275 276 277 278 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 275 def link_child(child) live_scopes << child link_to_parent end |
#link_to_parent ⇒ Object
263 264 265 266 267 268 269 270 271 272 273 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 263 def link_to_parent return if @linked @linked = true if @parent @parent.link_child self sync_collection_with_parent unless collection else ReactiveRecord::Base.add_to_outer_scopes self end all if collector? # force fetch all so the collector can do its job end |
#live_scopes ⇒ Object
220 221 222 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 220 def live_scopes @live_scopes ||= Set.new end |
#loading? ⇒ Boolean
512 513 514 515 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 512 def loading? all # need to force initialization at this point @dummy_collection.loading? end |
#merge_related_records(record, related_records) ⇒ Object
169 170 171 172 173 174 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 169 def (record, ) if filter? && joins_with?(record) .merge((record)) end end |
#observed ⇒ Object
305 306 307 308 309 310 311 312 313 314 315 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 305 def observed return if @observing || ReactiveRecord::Base.data_loading? begin @observing = true link_to_parent reload_from_db(true) if @out_of_date React::State.get_state(self, :collection) ensure @observing = false end end |
#proxy_association ⇒ Object
def each_known_child
[*collection, *client_pushes].each { |i| yield i }
end
355 356 357 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 355 def proxy_association @association || self # returning self allows this to work with things like Model.all end |
#push(item) ⇒ Object Also known as: <<
404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 404 def push(item) item.itself # force get of at least the id if collection self.force_push item else unsaved_children << item update_child(item) @owner.backing_record.sync_has_many(@association.attribute) if @owner && @association if !@count.nil? @count += item.destroyed? ? -1 : 1 notify_of_change self end end self end |
#push_and_update_belongs_to(id) ⇒ Object
363 364 365 366 367 368 369 370 371 372 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 363 def push_and_update_belongs_to(id) # example collection vector: TestModel.find(1).child_models.harrybarry # harrybarry << child means that # child.test_model = 1 # so... we go back starting at this collection and look for the first # collection with an owner... that is our guy child = proxy_association.klass.find(id) push child set_belongs_to child end |
#related_records_for(record) ⇒ Object
197 198 199 200 201 202 203 204 205 206 207 208 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 197 def (record) return [] unless @association attrs = record.attributes return [] unless attrs[@association.inverse_of] == @owner if !@association.through_association [record] elsif (source = attrs[@association.source]) [source] else [] end end |
#reload_from_db(force = nil) ⇒ Object
end of stuff to move
294 295 296 297 298 299 300 301 302 303 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 294 def reload_from_db(force = nil) if force || React::State.has_observers?(self, :collection) @out_of_date = false ReactiveRecord::Base.load_from_db(nil, *@vector, '*all') if @collection ReactiveRecord::Base.load_from_db(nil, *@vector, '*count') else @out_of_date = true end self end |
#replace(new_array) ⇒ Object
451 452 453 454 455 456 457 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 451 def replace(new_array) unsaved_children.clear new_array = new_array.to_a return self if new_array == @collection Base.load_data { internal_replace(new_array) } notify_of_change new_array end |
#set_belongs_to(child) ⇒ Object
374 375 376 377 378 379 380 381 382 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 374 def set_belongs_to(child) if @owner # TODO this is major broken...current child.send("#{@association.inverse_of}=", @owner) if @association && !@association.through_association elsif @parent @parent.set_belongs_to(child) end child end |
#set_count_state(val) ⇒ Object
317 318 319 320 321 322 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 317 def set_count_state(val) unless ReactiveRecord::WhileLoading.has_observers? React::State.set_state(self, :collection, collection, true) end @count = val end |
#set_pre_sync_related_records(related_records, _record = nil) ⇒ Object
224 225 226 227 228 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 224 def (, _record = nil) #related_records = related_records.intersection([*@collection]) <- deleting this works @pre_sync_related_records = #in_this_collection related_records <- not sure if this works live_scopes.each { |scope| scope.(@pre_sync_related_records) } end |
#sort!(*args, &block) ⇒ Object
422 423 424 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 422 def sort!(*args, &block) replace(sort(*args, &block)) end |
#sync_collection_with_parent ⇒ Object
280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 280 def sync_collection_with_parent if @parent.collection if @parent.collection.empty? @collection = [] elsif filter? @collection = filter_records(@parent.collection) end elsif @parent._count_internal(false).zero? # just changed this from count.zero? @count = 0 end end |
#sync_scopes(related_records, record, filtering = true) ⇒ Object
NOTE sync_scopes is overridden in scope_description.rb
231 232 233 234 235 236 237 238 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 231 def sync_scopes(, record, filtering = true) #related_records = related_records.intersection([*@collection]) #related_records = in_this_collection related_records live_scopes.each { |scope| scope.sync_scopes(, record, filtering) } notify_of_change unless .empty? ensure @pre_sync_related_records = nil end |
#to_s ⇒ Object
100 101 102 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 100 def to_s "<Coll-#{object_id} owner: #{@owner}, parent: #{@parent} - #{vector}>" end |
#unsaved_children ⇒ Object
13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 13 def unsaved_children old_uc_already_being_called = @uc_already_being_called if @owner && @association @unsaved_children ||= Set.new unless @uc_already_being_called @uc_already_being_called = true end else @unsaved_children ||= DummySet.new end @unsaved_children ensure @uc_already_being_called = old_uc_already_being_called end |
#update_child(item) ⇒ Object
appointment.doctor = doctor_value (i.e. through association is changing) means appointment.doctor_value.patients << appointment.patient and we have to appointment.doctor(current value).patients.delete(appointment.patient)
390 391 392 393 394 395 396 397 398 399 400 401 402 |
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 390 def update_child(item) backing_record = item.backing_record if backing_record && @owner && @association && !@association.through_association? && item.attributes[@association.inverse_of] != @owner inverse_of = @association.inverse_of current_association = item.attributes[inverse_of] backing_record.virgin = false unless backing_record.data_loading? backing_record.update_belongs_to(inverse_of, @owner) if current_association && current_association.attributes[@association.attribute] current_association.attributes[@association.attribute].delete(item) end @owner.backing_record.sync_has_many(@association.attribute) end end |