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.



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 29

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



480
481
482
483
484
485
486
487
488
489
490
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 480

def method_missing(method, *args, &block)
  if [].respond_to? method
    all.send(method, *args, &block)
  elsif ScopeDescription.find(@target_klass, method) || (args.count == 1 && method =~ /^find_by_/)
    apply_scope(method, *args)
  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.



343
344
345
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 343

def client_collection
  @client_collection
end

#parent=(value) ⇒ Object (writeonly)

Sets the attribute parent

Parameters:

  • value

    the value to set the attribute parent to.



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

def parent=(value)
  @parent = value
end

Returns the value of attribute pre_sync_related_records.



99
100
101
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 99

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.



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

def scope_description=(value)
  @scope_description = value
end

#vectorObject (readonly)

todo move following to a separate module related to scope updates ******************



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

def vector
  @vector
end

Class Method Details

.apply_to_all_collections(method, record, dont_gather) ⇒ Object



126
127
128
129
130
131
132
133
134
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 126

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



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 106

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



82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 82

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



70
71
72
73
74
75
76
77
78
79
80
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 70

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.backing_record.attributes[@association.inverse_of] = @owner if @association && !@association.through_association?
      @collection << new_dummy_record
    end
  end
  @collection[index]
end

#allObject



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 53

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



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

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



219
220
221
222
223
224
225
226
227
228
229
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 219

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



215
216
217
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 215

def child_scopes
  @child_scopes ||= {}
end

#collect(*args, &block) ⇒ Object

WHY IS THIS NEEDED? Perhaps it was just for debug



307
308
309
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 307

def collect(*args, &block)
  all.collect(*args, &block)
end

#collector?Boolean

Returns:

  • (Boolean)


179
180
181
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 179

def collector?
  false
end

#countObject Also known as: length



292
293
294
295
296
297
298
299
300
301
302
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 292

def count
  observed
  if @collection
    @collection.count
  elsif @count ||= ReactiveRecord::Base.fetch_from_db([*@vector, "*count"])
    @count
  else
    ReactiveRecord::Base.load_from_db(nil, *@vector, "*count")
    @count = 1
  end
end

#delete(item) ⇒ Object



444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 444

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) && backing_record.attributes[inverse_of] == @owner
        # the if prevents double update if delete is being called from << (see << above)
        backing_record.update_attribute(inverse_of, nil)
      end
      # forces a check if association contents have changed from synced values
      delete_internal(item) { @owner.backing_record.update_attribute(@association.attribute) }
    else
      delete_internal(item)
    end
  )
end

#delete_internal(item) {|item| ... } ⇒ Object

Yields:

  • (item)


461
462
463
464
465
466
467
468
469
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 461

def delete_internal(item)
  if collection
    all.delete(item)
  elsif !@count.nil?
    @count -= 1
  end
  yield item if block_given?
  item
end

#dup_for_syncObject



45
46
47
48
49
50
51
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 45

def dup_for_sync
  self.dup.instance_eval do
    @collection = @collection.dup if @collection
    @scopes = @scopes.dup
    self
  end
end

#empty?Boolean

should be handled by method missing below, but opal-rspec does not deal well with method missing, so to test…

Returns:

  • (Boolean)


476
477
478
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 476

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)


152
153
154
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 152

def filter?
  true
end

#filter_records(related_records) ⇒ Object



183
184
185
186
187
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 183

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



384
385
386
387
388
389
390
391
392
393
394
395
396
397
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 384

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


137
138
139
140
141
142
143
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 137

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



417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 417

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)


158
159
160
161
162
163
164
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 158

def joins_with?(record)
  if @association && @association.through_association
    @association.through_association.klass == record.class
  else
    @target_klass == record.class
  end
end

#klassObject



319
320
321
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 319

def klass
  @target_klass
end


243
244
245
246
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 243

def link_child(child)
  live_scopes << child
  link_to_parent
end


231
232
233
234
235
236
237
238
239
240
241
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 231

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



189
190
191
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 189

def live_scopes
  @live_scopes ||= Set.new
end

#loading?Boolean

Returns:

  • (Boolean)


471
472
473
474
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 471

def loading?
  all # need to force initialization at this point
  @dummy_collection.loading?
end


145
146
147
148
149
150
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 145

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

#observedObject



273
274
275
276
277
278
279
280
281
282
283
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 273

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



315
316
317
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 315

def proxy_association
  @association || self # returning self allows this to work with things like Model.all
end

#push(item) ⇒ Object Also known as: <<



362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 362

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



323
324
325
326
327
328
329
330
331
332
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 323

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


166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 166

def related_records_for(record)
  return [] unless @association
  attrs = record.backing_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



262
263
264
265
266
267
268
269
270
271
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 262

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



409
410
411
412
413
414
415
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 409

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



334
335
336
337
338
339
340
341
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 334

def set_belongs_to(child)
  if @owner
    child.send("#{@association.inverse_of}=", @owner) if @association
  elsif @parent
    @parent.set_belongs_to(child)
  end
  child
end

#set_count_state(val) ⇒ Object



285
286
287
288
289
290
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 285

def set_count_state(val)
  unless ReactiveRecord::WhileLoading.has_observers?
    React::State.set_state(self, :collection, collection, true)
  end
  @count = val
end


193
194
195
196
197
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 193

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



380
381
382
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 380

def sort!(*args, &block)
  replace(sort(*args, &block))
end

#sync_collection_with_parentObject



248
249
250
251
252
253
254
255
256
257
258
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 248

def sync_collection_with_parent
  if @parent.collection
    if @parent.collection.empty?
      @collection = []
    elsif filter?
      @collection = filter_records(@parent.collection)
    end
  elsif @parent.count.zero?
    @count = 0
  end
end

#sync_scopes(related_records, record, filtering = true) ⇒ Object



199
200
201
202
203
204
205
206
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 199

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



101
102
103
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 101

def to_s
  "<Coll-#{object_id} - #{vector}>"
end

#unsaved_childrenObject



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 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
      #@owner.backing_record.update_attribute(@association.attribute)
    end
  else
    @unsaved_children ||= DummySet.new
  end
  @unsaved_children
ensure
  @uc_already_being_called = old_uc_already_being_called
end

#update_child(item) ⇒ Object



350
351
352
353
354
355
356
357
358
359
360
# File 'lib/reactive_record/active_record/reactive_record/collection.rb', line 350

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_attribute(inverse_of, @owner)
    current_association.attributes[@association.attribute].delete(item) if current_association and current_association.attributes[@association.attribute]
    @owner.backing_record.update_attribute(@association.attribute) # forces a check if association contents have changed from synced values
  end
end