hyper-model 0.6.0 → 0.99.0

Sign up to get free protection for your applications and to get access to all the features.
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