Class: ReactiveRecord::Collection

Inherits:
Object
  • Object
show all
Defined in:
lib/reactive_record/active_record/reactive_record/collection.rb

Defined Under Namespace

Classes: DummySet

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

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

Parameters:

  • value

    the value to set the attribute parent to.



97
98
99
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 97

def parent=(value)
  @parent = value
end

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
  @pre_sync_related_records
end

#scope_description=(value) ⇒ Object (writeonly)

Sets the attribute scope_description

Parameters:

  • value

    the value to set the attribute scope_description to.



96
97
98
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 96

def scope_description=(value)
  @scope_description = value
end

#vectorObject (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)
  related_records = Set.new if dont_gather
  Base.outer_scopes.each do |collection|
    unless dont_gather
      related_records = collection.gather_related_records(record)
    end
    collection.send method, related_records, 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

#allObject



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_scopesObject



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

Returns:

  • (Boolean)


210
211
212
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 210

def collector?
  false
end

#countObject 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_syncObject



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

Returns:

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

Returns:

  • (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(related_records)
  # possibly we should never get here???
  scope_args = @vector.last.is_a?(Array) ? @vector.last[1..-1] : []
  @scope_description.filter_records(related_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


161
162
163
164
165
166
167
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 161

def gather_related_records(record, related_records = Set.new)
  merge_related_records(record, related_records)
  live_scopes.each do |collection|
    collection.gather_related_records(record, related_records)
  end
  related_records
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???

Returns:

  • (Boolean)


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

#klassObject



359
360
361
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 359

def klass
  @target_klass
end


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


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_scopesObject



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

Returns:

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


169
170
171
172
173
174
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 169

def merge_related_records(record, related_records)
  if filter? && joins_with?(record)
    related_records.merge(related_records_for(record))
  end
  related_records
end

#observedObject



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_associationObject

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


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


224
225
226
227
228
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 224

def set_pre_sync_related_records(related_records, _record = nil)
  #related_records = related_records.intersection([*@collection]) <- deleting this works
  @pre_sync_related_records = related_records #in_this_collection related_records <- not sure if this works
  live_scopes.each { |scope| scope.set_pre_sync_related_records(@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_parentObject



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(related_records, record, filtering = true)
  #related_records = related_records.intersection([*@collection])
  #related_records = in_this_collection related_records
  live_scopes.each { |scope| scope.sync_scopes(related_records, record, filtering) }
  notify_of_change unless related_records.empty?
ensure
  @pre_sync_related_records = nil
end

#to_sObject



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_childrenObject



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