hyper-model 0.6.0 → 0.99.0

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.
Files changed (140) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +35 -41
  3. data/.rspec +2 -0
  4. data/.travis.yml +33 -0
  5. data/CHANGELOG.md +34 -0
  6. data/DOCS.md +735 -0
  7. data/Gemfile +7 -0
  8. data/Gemfile.lock +298 -224
  9. data/{LICENSE → LICENSE.txt} +6 -6
  10. data/README.md +51 -2
  11. data/Rakefile +18 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +7 -0
  14. data/codeship.database.yml +18 -0
  15. data/hyper-model.gemspec +62 -36
  16. data/lib/active_model_client_stubs.rb +16 -0
  17. data/lib/active_record_base.rb +331 -0
  18. data/{examples/chat-app/app/assets/images/.keep → lib/acts_as_string.rb} +0 -0
  19. data/lib/enumerable/pluck.rb +6 -0
  20. data/lib/hyper-model.rb +59 -8
  21. data/lib/hyper_model/version.rb +3 -0
  22. data/lib/hyper_react/input_tags.rb +47 -0
  23. data/lib/hyperloop/model/load.rb +1 -1
  24. data/lib/kernel/itself.rb +5 -0
  25. data/lib/object/tap.rb +7 -0
  26. data/lib/opal/equality_patches.rb +15 -0
  27. data/lib/opal/parse_patch.rb +14 -0
  28. data/lib/opal/set_patches.rb +8 -0
  29. data/lib/reactive_record/active_record/aggregations.rb +69 -0
  30. data/lib/reactive_record/active_record/associations.rb +118 -0
  31. data/lib/reactive_record/active_record/base.rb +10 -0
  32. data/lib/reactive_record/active_record/class_methods.rb +406 -0
  33. data/lib/reactive_record/active_record/error.rb +31 -0
  34. data/lib/reactive_record/active_record/errors.rb +374 -0
  35. data/lib/reactive_record/active_record/instance_methods.rb +187 -0
  36. data/lib/reactive_record/active_record/public_columns_hash.rb +44 -0
  37. data/lib/reactive_record/active_record/reactive_record/backing_record_inspector.rb +36 -0
  38. data/lib/reactive_record/active_record/reactive_record/base.rb +416 -0
  39. data/lib/reactive_record/active_record/reactive_record/collection.rb +558 -0
  40. data/lib/reactive_record/active_record/reactive_record/column_types.rb +75 -0
  41. data/lib/reactive_record/active_record/reactive_record/dummy_value.rb +236 -0
  42. data/lib/reactive_record/active_record/reactive_record/getters.rb +133 -0
  43. data/lib/reactive_record/active_record/reactive_record/isomorphic_base.rb +576 -0
  44. data/lib/reactive_record/active_record/reactive_record/lookup_tables.rb +54 -0
  45. data/lib/reactive_record/active_record/reactive_record/operations.rb +107 -0
  46. data/lib/reactive_record/active_record/reactive_record/scoped_collection.rb +62 -0
  47. data/lib/reactive_record/active_record/reactive_record/setters.rb +194 -0
  48. data/lib/reactive_record/active_record/reactive_record/unscoped_collection.rb +16 -0
  49. data/lib/reactive_record/active_record/reactive_record/while_loading.rb +343 -0
  50. data/lib/reactive_record/active_record_error.rb +4 -0
  51. data/lib/reactive_record/broadcast.rb +223 -0
  52. data/lib/reactive_record/engine.rb +11 -0
  53. data/lib/reactive_record/interval.rb +190 -0
  54. data/lib/reactive_record/permissions.rb +117 -0
  55. data/lib/reactive_record/pry.rb +13 -0
  56. data/lib/reactive_record/reactive_scope.rb +18 -0
  57. data/lib/reactive_record/scope_description.rb +121 -0
  58. data/lib/reactive_record/serializers.rb +7 -0
  59. data/lib/reactive_record/server_data_cache.rb +478 -0
  60. data/path_release_steps.md +9 -0
  61. metadata +399 -109
  62. data/CODE_OF_CONDUCT.md +0 -49
  63. data/examples/chat-app/.gitignore +0 -21
  64. data/examples/chat-app/Gemfile +0 -62
  65. data/examples/chat-app/Gemfile.lock +0 -309
  66. data/examples/chat-app/README.md +0 -3
  67. data/examples/chat-app/Rakefile +0 -6
  68. data/examples/chat-app/app/assets/config/manifest.js +0 -3
  69. data/examples/chat-app/app/assets/javascripts/application.js +0 -3
  70. data/examples/chat-app/app/assets/stylesheets/application.scss +0 -33
  71. data/examples/chat-app/app/controllers/application_controller.rb +0 -3
  72. data/examples/chat-app/app/controllers/home_controller.rb +0 -5
  73. data/examples/chat-app/app/hyperloop/components/app.rb +0 -12
  74. data/examples/chat-app/app/hyperloop/components/formatted_div.rb +0 -15
  75. data/examples/chat-app/app/hyperloop/components/input_box.rb +0 -26
  76. data/examples/chat-app/app/hyperloop/components/message.rb +0 -29
  77. data/examples/chat-app/app/hyperloop/components/messages.rb +0 -8
  78. data/examples/chat-app/app/hyperloop/components/nav.rb +0 -30
  79. data/examples/chat-app/app/hyperloop/models/application_record.rb +0 -3
  80. data/examples/chat-app/app/hyperloop/models/message.rb +0 -6
  81. data/examples/chat-app/app/hyperloop/operations/operations.rb +0 -13
  82. data/examples/chat-app/app/hyperloop/stores/message_store.rb +0 -17
  83. data/examples/chat-app/app/policies/application_policy.rb +0 -9
  84. data/examples/chat-app/app/views/layouts/application.html.erb +0 -12
  85. data/examples/chat-app/bin/bundle +0 -3
  86. data/examples/chat-app/bin/rails +0 -9
  87. data/examples/chat-app/bin/rake +0 -9
  88. data/examples/chat-app/bin/setup +0 -34
  89. data/examples/chat-app/bin/spring +0 -17
  90. data/examples/chat-app/bin/update +0 -29
  91. data/examples/chat-app/config.ru +0 -5
  92. data/examples/chat-app/config/application.rb +0 -12
  93. data/examples/chat-app/config/boot.rb +0 -3
  94. data/examples/chat-app/config/cable.yml +0 -9
  95. data/examples/chat-app/config/database.yml +0 -25
  96. data/examples/chat-app/config/environment.rb +0 -5
  97. data/examples/chat-app/config/environments/development.rb +0 -56
  98. data/examples/chat-app/config/environments/production.rb +0 -86
  99. data/examples/chat-app/config/environments/test.rb +0 -42
  100. data/examples/chat-app/config/initializers/application_controller_renderer.rb +0 -6
  101. data/examples/chat-app/config/initializers/assets.rb +0 -11
  102. data/examples/chat-app/config/initializers/backtrace_silencers.rb +0 -7
  103. data/examples/chat-app/config/initializers/cookies_serializer.rb +0 -5
  104. data/examples/chat-app/config/initializers/filter_parameter_logging.rb +0 -4
  105. data/examples/chat-app/config/initializers/hyperloop.rb +0 -6
  106. data/examples/chat-app/config/initializers/inflections.rb +0 -16
  107. data/examples/chat-app/config/initializers/mime_types.rb +0 -4
  108. data/examples/chat-app/config/initializers/new_framework_defaults.rb +0 -24
  109. data/examples/chat-app/config/initializers/session_store.rb +0 -3
  110. data/examples/chat-app/config/initializers/wrap_parameters.rb +0 -14
  111. data/examples/chat-app/config/locales/en.yml +0 -23
  112. data/examples/chat-app/config/puma.rb +0 -47
  113. data/examples/chat-app/config/routes.rb +0 -5
  114. data/examples/chat-app/config/secrets.yml +0 -22
  115. data/examples/chat-app/config/spring.rb +0 -6
  116. data/examples/chat-app/db/migrate/20170319194429_create_message.rb +0 -9
  117. data/examples/chat-app/db/schema.rb +0 -48
  118. data/examples/chat-app/db/seeds.rb +0 -7
  119. data/examples/chat-app/lib/assets/.keep +0 -0
  120. data/examples/chat-app/lib/tasks/.keep +0 -0
  121. data/examples/chat-app/log/.keep +0 -0
  122. data/examples/chat-app/public/404.html +0 -67
  123. data/examples/chat-app/public/422.html +0 -67
  124. data/examples/chat-app/public/500.html +0 -66
  125. data/examples/chat-app/public/apple-touch-icon-precomposed.png +0 -0
  126. data/examples/chat-app/public/apple-touch-icon.png +0 -0
  127. data/examples/chat-app/public/favicon.ico +0 -0
  128. data/examples/chat-app/public/robots.txt +0 -5
  129. data/examples/chat-app/test/controllers/.keep +0 -0
  130. data/examples/chat-app/test/fixtures/.keep +0 -0
  131. data/examples/chat-app/test/fixtures/files/.keep +0 -0
  132. data/examples/chat-app/test/helpers/.keep +0 -0
  133. data/examples/chat-app/test/integration/.keep +0 -0
  134. data/examples/chat-app/test/mailers/.keep +0 -0
  135. data/examples/chat-app/test/models/.keep +0 -0
  136. data/examples/chat-app/test/test_helper.rb +0 -10
  137. data/examples/chat-app/tmp/.keep +0 -0
  138. data/examples/chat-app/vendor/assets/javascripts/.keep +0 -0
  139. data/examples/chat-app/vendor/assets/stylesheets/.keep +0 -0
  140. data/lib/hyperloop/model/version.rb +0 -5
@@ -0,0 +1,558 @@
1
+ module ReactiveRecord
2
+
3
+ class Collection
4
+
5
+ class DummySet
6
+ def new
7
+ @master ||= super
8
+ end
9
+ def method_missing(*args)
10
+ end
11
+ end
12
+
13
+ def unsaved_children
14
+ old_uc_already_being_called = @uc_already_being_called
15
+ if @owner && @association
16
+ @unsaved_children ||= Set.new
17
+ unless @uc_already_being_called
18
+ @uc_already_being_called = true
19
+ end
20
+ else
21
+ @unsaved_children ||= DummySet.new
22
+ end
23
+ @unsaved_children
24
+ ensure
25
+ @uc_already_being_called = old_uc_already_being_called
26
+ end
27
+
28
+ def initialize(target_klass, owner = nil, association = nil, *vector)
29
+ @owner = owner # can be nil if this is an outer most scope
30
+ @association = association
31
+ @target_klass = target_klass
32
+ if owner and !owner.id and vector.length <= 1
33
+ @collection = []
34
+ elsif vector.length > 0
35
+ @vector = vector
36
+ elsif owner
37
+ @vector = owner.backing_record.vector + [association.attribute]
38
+ else
39
+ @vector = [target_klass]
40
+ end
41
+ @scopes = {}
42
+ end
43
+
44
+ def dup_for_sync
45
+ self.dup.instance_eval do
46
+ @collection = @collection.dup if @collection
47
+ @scopes = @scopes.dup
48
+ self
49
+ end
50
+ end
51
+
52
+ def all
53
+ observed
54
+ @dummy_collection.notify if @dummy_collection
55
+ unless @collection
56
+ @collection = []
57
+ if ids = ReactiveRecord::Base.fetch_from_db([*@vector, "*all"])
58
+ ids.each do |id|
59
+ @collection << @target_klass.find_by(@target_klass.primary_key => id)
60
+ end
61
+ else
62
+ @dummy_collection = ReactiveRecord::Base.load_from_db(nil, *@vector, "*all")
63
+ @dummy_record = self[0]
64
+ end
65
+ end
66
+ @collection
67
+ end
68
+
69
+ def [](index)
70
+ observed
71
+ if (@collection || all).length <= index and @dummy_collection
72
+ (@collection.length..index).each do |i|
73
+ new_dummy_record = ReactiveRecord::Base.new_from_vector(@target_klass, nil, *@vector, "*#{i}")
74
+ new_dummy_record.attributes[@association.inverse_of] = @owner if @association && !@association.through_association?
75
+ @collection << new_dummy_record
76
+ end
77
+ end
78
+ @collection[index]
79
+ end
80
+
81
+ def ==(other_collection)
82
+ observed
83
+ return !@collection unless other_collection.is_a? Collection
84
+ other_collection.observed
85
+ my_children = (@collection || []).select { |target| target != @dummy_record }
86
+ if other_collection
87
+ other_children = (other_collection.collection || []).select { |target| target != other_collection.dummy_record }
88
+ return false unless my_children == other_children
89
+ unsaved_children.to_a == other_collection.unsaved_children.to_a
90
+ else
91
+ my_children.empty? && unsaved_children.empty?
92
+ end
93
+ end
94
+ # todo move following to a separate module related to scope updates ******************
95
+ attr_reader :vector
96
+ attr_writer :scope_description
97
+ attr_writer :parent
98
+ attr_reader :pre_sync_related_records
99
+
100
+ def to_s
101
+ "<Coll-#{object_id} owner: #{@owner}, parent: #{@parent} - #{vector}>"
102
+ end
103
+
104
+ class << self
105
+
106
+ =begin
107
+ sync_scopes takes a newly broadcasted record change and updates all relevant currently active scopes
108
+ This is particularly hard when the client proc is specified. For example consider this scope:
109
+
110
+ class TestModel < ApplicationRecord
111
+ scope :quicker, -> { where(completed: true) }, client: -> { completed }
112
+ end
113
+
114
+ and this slice of reactive code:
115
+
116
+ DIV { "quicker.count = #{TestModel.quicker.count}" }
117
+
118
+ then on the server this code is executed:
119
+
120
+ TestModel.last.update(completed: false)
121
+
122
+ This will result in the changes being broadcast to the client, which may cauase the value of
123
+ TestModel.quicker.count to increase or decrease. Of course we may not actually have the all the records,
124
+ perhaps we just have the aggregate count.
125
+
126
+ To determine this sync_scopes first asks if the record being changed is in the scope given its value
127
+
128
+
129
+ =end
130
+ def sync_scopes(broadcast)
131
+ # record_with_current_values will return nil if data between
132
+ # the broadcast record and the value on the client is out of sync
133
+ # not running set_pre_sync_related_records will cause sync scopes
134
+ # to refresh all related scopes
135
+ React::State.bulk_update do
136
+ record = broadcast.record_with_current_values
137
+ apply_to_all_collections(
138
+ :set_pre_sync_related_records,
139
+ record, broadcast.new?
140
+ ) if record
141
+ record = broadcast.record_with_new_values
142
+ apply_to_all_collections(
143
+ :sync_scopes,
144
+ record, record.destroyed?
145
+ )
146
+ record.backing_record.sync_unscoped_collection! if record.destroyed? || broadcast.new?
147
+ end
148
+ end
149
+
150
+ def apply_to_all_collections(method, record, dont_gather)
151
+ related_records = Set.new if dont_gather
152
+ Base.outer_scopes.each do |collection|
153
+ unless dont_gather
154
+ related_records = collection.gather_related_records(record)
155
+ end
156
+ collection.send method, related_records, record
157
+ end
158
+ end
159
+ end
160
+
161
+ def gather_related_records(record, related_records = Set.new)
162
+ merge_related_records(record, related_records)
163
+ live_scopes.each do |collection|
164
+ collection.gather_related_records(record, related_records)
165
+ end
166
+ related_records
167
+ end
168
+
169
+ def merge_related_records(record, related_records)
170
+ if filter? && joins_with?(record)
171
+ related_records.merge(related_records_for(record))
172
+ end
173
+ related_records
174
+ end
175
+
176
+ def filter?
177
+ true
178
+ end
179
+
180
+ # is it necessary to check @association in the next 2 methods???
181
+
182
+ def joins_with?(record)
183
+ klass = record.class
184
+ if @association&.through_association
185
+ @association.through_association.klass == record.class
186
+ elsif @target_klass == klass
187
+ true
188
+ elsif !klass.inheritance_column
189
+ false
190
+ elsif klass.base_class == @target_class
191
+ klass < @target_klass
192
+ elsif klass.base_class == klass
193
+ @target_klass < klass
194
+ end
195
+ end
196
+
197
+ def related_records_for(record)
198
+ return [] unless @association
199
+ attrs = record.attributes
200
+ return [] unless attrs[@association.inverse_of] == @owner
201
+ if !@association.through_association
202
+ [record]
203
+ elsif (source = attrs[@association.source])
204
+ [source]
205
+ else
206
+ []
207
+ end
208
+ end
209
+
210
+ def collector?
211
+ false
212
+ end
213
+
214
+ def filter_records(related_records)
215
+ # possibly we should never get here???
216
+ scope_args = @vector.last.is_a?(Array) ? @vector.last[1..-1] : []
217
+ @scope_description.filter_records(related_records, scope_args)
218
+ end
219
+
220
+ def live_scopes
221
+ @live_scopes ||= Set.new
222
+ end
223
+
224
+ def set_pre_sync_related_records(related_records, _record = nil)
225
+ #related_records = related_records.intersection([*@collection]) <- deleting this works
226
+ @pre_sync_related_records = related_records #in_this_collection related_records <- not sure if this works
227
+ live_scopes.each { |scope| scope.set_pre_sync_related_records(@pre_sync_related_records) }
228
+ end
229
+
230
+ # NOTE sync_scopes is overridden in scope_description.rb
231
+ def sync_scopes(related_records, record, filtering = true)
232
+ #related_records = related_records.intersection([*@collection])
233
+ #related_records = in_this_collection related_records
234
+ live_scopes.each { |scope| scope.sync_scopes(related_records, record, filtering) }
235
+ notify_of_change unless related_records.empty?
236
+ ensure
237
+ @pre_sync_related_records = nil
238
+ end
239
+
240
+ def apply_scope(name, *vector)
241
+ description = ScopeDescription.find(@target_klass, name)
242
+ collection = build_child_scope(description, *description.name, *vector)
243
+ collection.reload_from_db if name == "#{description.name}!"
244
+ collection
245
+ end
246
+
247
+ def child_scopes
248
+ @child_scopes ||= {}
249
+ end
250
+
251
+ def build_child_scope(scope_description, *scope_vector)
252
+ child_scopes[scope_vector] ||= begin
253
+ new_vector = @vector
254
+ new_vector += [scope_vector] unless new_vector.nil? || scope_vector.empty?
255
+ child_scope = Collection.new(@target_klass, nil, nil, *new_vector)
256
+ child_scope.scope_description = scope_description
257
+ child_scope.parent = self
258
+ child_scope.extend ScopedCollection
259
+ child_scope
260
+ end
261
+ end
262
+
263
+ def link_to_parent
264
+ return if @linked
265
+ @linked = true
266
+ if @parent
267
+ @parent.link_child self
268
+ sync_collection_with_parent unless collection
269
+ else
270
+ ReactiveRecord::Base.add_to_outer_scopes self
271
+ end
272
+ all if collector? # force fetch all so the collector can do its job
273
+ end
274
+
275
+ def link_child(child)
276
+ live_scopes << child
277
+ link_to_parent
278
+ end
279
+
280
+ def sync_collection_with_parent
281
+ if @parent.collection
282
+ if @parent.collection.empty?
283
+ @collection = []
284
+ elsif filter?
285
+ @collection = filter_records(@parent.collection)
286
+ end
287
+ elsif @parent._count_internal(false).zero? # just changed this from count.zero?
288
+ @count = 0
289
+ end
290
+ end
291
+
292
+ # end of stuff to move
293
+
294
+ def reload_from_db(force = nil)
295
+ if force || React::State.has_observers?(self, :collection)
296
+ @out_of_date = false
297
+ ReactiveRecord::Base.load_from_db(nil, *@vector, '*all') if @collection
298
+ ReactiveRecord::Base.load_from_db(nil, *@vector, '*count')
299
+ else
300
+ @out_of_date = true
301
+ end
302
+ self
303
+ end
304
+
305
+ def observed
306
+ return if @observing || ReactiveRecord::Base.data_loading?
307
+ begin
308
+ @observing = true
309
+ link_to_parent
310
+ reload_from_db(true) if @out_of_date
311
+ React::State.get_state(self, :collection)
312
+ ensure
313
+ @observing = false
314
+ end
315
+ end
316
+
317
+ def set_count_state(val)
318
+ unless ReactiveRecord::WhileLoading.has_observers?
319
+ React::State.set_state(self, :collection, collection, true)
320
+ end
321
+ @count = val
322
+ end
323
+
324
+
325
+
326
+ def _count_internal(load_from_client)
327
+ # when count is called on a leaf, count_internal is called for each
328
+ # ancestor. Only the outermost count has load_from_client == true
329
+ observed
330
+ if @collection
331
+ @collection.count
332
+ elsif @count ||= ReactiveRecord::Base.fetch_from_db([*@vector, "*count"])
333
+ @count
334
+ else
335
+ ReactiveRecord::Base.load_from_db(nil, *@vector, "*count") if load_from_client
336
+ @count = 1
337
+ end
338
+ end
339
+
340
+ def count
341
+ _count_internal(true)
342
+ end
343
+
344
+ alias_method :length, :count
345
+
346
+ # WHY IS THIS NEEDED? Perhaps it was just for debug
347
+ def collect(*args, &block)
348
+ all.collect(*args, &block)
349
+ end
350
+
351
+ # def each_known_child
352
+ # [*collection, *client_pushes].each { |i| yield i }
353
+ # end
354
+
355
+ def proxy_association
356
+ @association || self # returning self allows this to work with things like Model.all
357
+ end
358
+
359
+ def klass
360
+ @target_klass
361
+ end
362
+
363
+ def push_and_update_belongs_to(id)
364
+ # example collection vector: TestModel.find(1).child_models.harrybarry
365
+ # harrybarry << child means that
366
+ # child.test_model = 1
367
+ # so... we go back starting at this collection and look for the first
368
+ # collection with an owner... that is our guy
369
+ child = proxy_association.klass.find(id)
370
+ push child
371
+ set_belongs_to child
372
+ end
373
+
374
+ def set_belongs_to(child)
375
+ if @owner
376
+ # TODO this is major broken...current
377
+ child.send("#{@association.inverse_of}=", @owner) if @association && !@association.through_association
378
+ elsif @parent
379
+ @parent.set_belongs_to(child)
380
+ end
381
+ child
382
+ end
383
+
384
+ attr_reader :client_collection
385
+
386
+ # appointment.doctor = doctor_value (i.e. through association is changing)
387
+ # means appointment.doctor_value.patients << appointment.patient
388
+ # and we have to appointment.doctor(current value).patients.delete(appointment.patient)
389
+
390
+ def update_child(item)
391
+ backing_record = item.backing_record
392
+ if backing_record && @owner && @association && !@association.through_association? && item.attributes[@association.inverse_of] != @owner
393
+ inverse_of = @association.inverse_of
394
+ current_association = item.attributes[inverse_of]
395
+ backing_record.virgin = false unless backing_record.data_loading?
396
+ backing_record.update_belongs_to(inverse_of, @owner)
397
+ if current_association && current_association.attributes[@association.attribute]
398
+ current_association.attributes[@association.attribute].delete(item)
399
+ end
400
+ @owner.backing_record.sync_has_many(@association.attribute)
401
+ end
402
+ end
403
+
404
+ def push(item)
405
+ item.itself # force get of at least the id
406
+ if collection
407
+ self.force_push item
408
+ else
409
+ unsaved_children << item
410
+ update_child(item)
411
+ @owner.backing_record.sync_has_many(@association.attribute) if @owner && @association
412
+ if !@count.nil?
413
+ @count += item.destroyed? ? -1 : 1
414
+ notify_of_change self
415
+ end
416
+ end
417
+ self
418
+ end
419
+
420
+ alias << push
421
+
422
+ def sort!(*args, &block)
423
+ replace(sort(*args, &block))
424
+ end
425
+
426
+ def force_push(item)
427
+ return delete(item) if item.destroyed? # pushing a destroyed item is the same as removing it
428
+ all << item unless all.include? item # does this use == if so we are okay...
429
+ update_child(item)
430
+ if item.id and @dummy_record
431
+ @dummy_record.id = item.id
432
+ # we cant use == because that just means the objects are referencing
433
+ # the same backing record.
434
+ @collection.reject { |i| i.object_id == @dummy_record.object_id }
435
+ @dummy_record = @collection.detect { |r| r.backing_record.vector.last =~ /^\*[0-9]+$/ }
436
+ @dummy_collection = nil
437
+ end
438
+ notify_of_change self
439
+ end
440
+
441
+ [:first, :last].each do |method|
442
+ define_method method do |*args|
443
+ if args.count == 0
444
+ all.send(method)
445
+ else
446
+ apply_scope(method, *args)
447
+ end
448
+ end
449
+ end
450
+
451
+ def replace(new_array)
452
+ unsaved_children.clear
453
+ new_array = new_array.to_a
454
+ return self if new_array == @collection
455
+ Base.load_data { internal_replace(new_array) }
456
+ notify_of_change new_array
457
+ end
458
+
459
+ def internal_replace(new_array)
460
+
461
+ # not tested if you do all[n] where n > 0... this will create additional dummy items, that this will not sync up.
462
+ # probably just moving things around so the @dummy_collection and @dummy_record are updated AFTER the new items are pushed
463
+ # should work.
464
+
465
+ if @dummy_collection
466
+ @dummy_collection.notify
467
+ array = new_array.is_a?(Collection) ? new_array.collection : new_array
468
+ @collection.each_with_index do |r, i|
469
+ r.id = new_array[i].id if array[i] and array[i].id and !r.new? and r.backing_record.vector.last =~ /^\*[0-9]+$/
470
+ end
471
+ end
472
+
473
+ @collection.dup.each { |item| delete(item) } if @collection # this line is a big nop I think
474
+ @collection = []
475
+ if new_array.is_a? Collection
476
+ @dummy_collection = new_array.dummy_collection
477
+ @dummy_record = new_array.dummy_record
478
+ new_array.collection.each { |item| self << item } if new_array.collection
479
+ else
480
+ @dummy_collection = @dummy_record = nil
481
+ new_array.each { |item| self << item }
482
+ end
483
+ notify_of_change new_array
484
+ end
485
+
486
+ def delete(item)
487
+ unsaved_children.delete(item)
488
+ notify_of_change(
489
+ if @owner && @association && !@association.through_association?
490
+ inverse_of = @association.inverse_of
491
+ if (backing_record = item.backing_record) && item.attributes[inverse_of] == @owner
492
+ # the if prevents double update if delete is being called from << (see << above)
493
+ backing_record.update_belongs_to(inverse_of, nil)
494
+ end
495
+ delete_internal(item) { @owner.backing_record.sync_has_many(@association.attribute) }
496
+ else
497
+ delete_internal(item)
498
+ end
499
+ )
500
+ end
501
+
502
+ def delete_internal(item)
503
+ if collection
504
+ all.delete(item)
505
+ elsif !@count.nil?
506
+ @count -= 1
507
+ end
508
+ yield if block_given? # was yield item, but item is not used
509
+ item
510
+ end
511
+
512
+ def loading?
513
+ all # need to force initialization at this point
514
+ @dummy_collection.loading?
515
+ end
516
+
517
+ def empty?
518
+ # should be handled by method missing below, but opal-rspec does not deal well
519
+ # with method missing, so to test...
520
+ all.empty?
521
+ end
522
+
523
+ def method_missing(method, *args, &block)
524
+ if [].respond_to? method
525
+ all.send(method, *args, &block)
526
+ elsif ScopeDescription.find(@target_klass, method)
527
+ apply_scope(method, *args)
528
+ elsif args.count == 1 && method.start_with?('find_by_')
529
+ apply_scope(:find_by, method.sub(/^find_by_/, '') => args.first)
530
+ elsif @target_klass.respond_to?(method) && ScopeDescription.find(@target_klass, "_#{method}")
531
+ apply_scope("_#{method}", *args).first
532
+ else
533
+ super
534
+ end
535
+ end
536
+
537
+ protected
538
+
539
+ def dummy_record
540
+ @dummy_record
541
+ end
542
+
543
+ def collection
544
+ @collection
545
+ end
546
+
547
+ def dummy_collection
548
+ @dummy_collection
549
+ end
550
+
551
+ def notify_of_change(value = nil)
552
+ React::State.set_state(self, "collection", collection) unless ReactiveRecord::Base.data_loading?
553
+ value
554
+ end
555
+
556
+ end
557
+
558
+ end