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,44 @@
1
+ module Hyperloop
2
+ define_setting :public_model_directories, [File.join('app','hyperloop','models'), File.join('app','models','public')]
3
+ end
4
+
5
+ module ActiveRecord
6
+ # adds method to get the HyperMesh public column types
7
+ # this works because the public folder is currently required to be eager loaded.
8
+ class Base
9
+ def self.public_columns_hash
10
+ return @public_columns_hash if @public_columns_hash && Rails.env.production?
11
+ files = []
12
+ Hyperloop.public_model_directories.each do |dir|
13
+ dir_length = Rails.root.join(dir).to_s.length + 1
14
+ Dir.glob(Rails.root.join(dir, '**', '*.rb')).each do |file|
15
+ require_dependency(file) # still the file is loaded to make sure for development and test env
16
+ files << file[dir_length..-4]
17
+ end
18
+ end
19
+ @public_columns_hash = {}
20
+ # descendants only works for already loaded models!
21
+ descendants.each do |model|
22
+ if files.include?(model.name.underscore) && model.name.underscore != 'application_record'
23
+ @public_columns_hash[model.name] = model.columns_hash rescue nil # why rescue?
24
+ end
25
+ # begin
26
+ # @public_columns_hash[model.name] = model.columns_hash if model.table_name
27
+ # rescue Exception => e
28
+ # binding.pry
29
+ # @public_columns_hash = nil
30
+ # raise $!, "Could not read 'columns_hash' for #{model}: #{$!}", $!.backtrace
31
+ # end if files.include?(model.name.underscore) && model.name.underscore != 'application_record'
32
+ end
33
+ @public_columns_hash
34
+ end
35
+
36
+ def self.public_columns_hash_as_json
37
+ return @public_columns_hash_json if @public_columns_hash_json && Rails.env.production?
38
+ pch = public_columns_hash
39
+ return @public_columns_hash_json if @prev_public_columns_hash == pch
40
+ @prev_public_columns_hash = pch
41
+ @public_columns_hash_json = pch.to_json
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,36 @@
1
+ module ReactiveRecord
2
+ # inspection_details is used by client side ActiveRecord::Base
3
+ # runs down the possible states of a backing record and returns
4
+ # the appropriate string. The order of execution is important!
5
+ module BackingRecordInspector
6
+ def inspection_details
7
+ return error_details unless errors.empty?
8
+ return new_details if new?
9
+ return destroyed_details if destroyed
10
+ return loading_details unless @attributes.key? primary_key
11
+ return dirty_details unless changed_attributes.empty?
12
+ "[loaded id: #{id}]"
13
+ end
14
+
15
+ def error_details
16
+ id_str = "id: #{id} " unless new?
17
+ "[errors #{id_str}#{errors.messages}]"
18
+ end
19
+
20
+ def new_details
21
+ "[new #{attributes.select { |attr| column_type(attr) }}]"
22
+ end
23
+
24
+ def destroyed_details
25
+ "[destroyed id: #{id}]"
26
+ end
27
+
28
+ def loading_details
29
+ "[loading #{vector}]"
30
+ end
31
+
32
+ def dirty_details
33
+ "[changed id: #{id} #{changes}]"
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,416 @@
1
+ module ReactiveRecord
2
+ class Base
3
+ include BackingRecordInspector
4
+ include Setters
5
+ include Getters
6
+ extend LookupTables
7
+
8
+ # Its all about lazy loading. This prevents us from grabbing enormous association collections, or large attributes
9
+ # unless they are explicitly requested.
10
+
11
+ # During prerendering we get each attribute as its requested and fill it in both on the javascript side, as well as
12
+ # remember that the attribute needs to be part of the download to client.
13
+
14
+ # On the client we fill in the record data with empty values (the default value for the attribute,
15
+ # or one element collections) but only as the attribute
16
+ # is requested. Each request queues up a request to get the real data from the server.
17
+
18
+ # The ReactiveRecord class serves two purposes. First it is the unique data corresponding to the last known state of a
19
+ # database record. This means All records matching a specific database record are unique. This is unlike AR but is
20
+ # important both for the lazy loading and also so that when values change react can be informed of the change.
21
+
22
+ # Secondly it serves as name space for all the ReactiveRecord specific methods, so every AR Instance has a ReactiveRecord
23
+
24
+ # Because there is no point in generating a new ar_instance everytime a search is made we cache the first ar_instance created.
25
+ # Its possible however during loading to create a new ar_instances that will in the end point to the same record.
26
+
27
+ # VECTORS... are an important concept. They are the substitute for a primary key before a record is loaded.
28
+ # Vectors have the form [ModelClass, method_call, method_call, method_call...]
29
+
30
+ # Each method call is either a simple method name or an array in the form [method_name, param, param ...]
31
+ # Example [User, [find, 123], todos, active, [due, "1/1/2016"], title]
32
+ # Roughly corresponds to this query: User.find(123).todos.active.due("1/1/2016").select(:title)
33
+
34
+ attr_accessor :ar_instance
35
+ attr_accessor :vector
36
+ attr_accessor :model
37
+ attr_accessor :changed_attributes
38
+ attr_accessor :aggregate_owner
39
+ attr_accessor :aggregate_attribute
40
+ attr_accessor :destroyed
41
+ attr_accessor :updated_during
42
+ attr_accessor :synced_attributes
43
+ attr_accessor :virgin
44
+ attr_reader :attributes
45
+
46
+ # While data is being loaded from the server certain internal behaviors need to change
47
+ # for example all record changes are synced as they happen.
48
+ # This is implemented this way so that the ServerDataCache class can use pure active
49
+ # record methods in its implementation
50
+
51
+ def self.data_loading?
52
+ @data_loading
53
+ end
54
+
55
+ def data_loading?
56
+ self.class.data_loading?
57
+ end
58
+
59
+ def self.load_data(&block)
60
+ current_data_loading, @data_loading = [@data_loading, true]
61
+ yield
62
+ ensure
63
+ @data_loading = current_data_loading
64
+ end
65
+
66
+ def self.load_from_json(json, target = nil)
67
+ load_data { ServerDataCache.load_from_json(json, target) }
68
+ end
69
+
70
+ def self.find(model, attrs)
71
+ # will return the unique record with this attribute-value pair
72
+ # value cannot be an association or aggregation
73
+
74
+ # add the inheritance column if this is an STI subclass
75
+
76
+ inher_col = model.inheritance_column
77
+ if inher_col && model < model.base_class && !attrs.key?(inher_col)
78
+ attrs = attrs.merge(inher_col => model.model_name.to_s)
79
+ end
80
+
81
+ model = model.base_class
82
+ primary_key = model.primary_key
83
+
84
+ # already have a record with these attribute-value pairs?
85
+
86
+ record =
87
+ if (id_to_find = attrs[primary_key])
88
+ lookup_by_id(model, id_to_find)
89
+ else
90
+ @records[model].detect do |r|
91
+ !attrs.detect { |attr, value| r.synced_attributes[attr] != value }
92
+ end
93
+ end
94
+
95
+ unless record
96
+ # if not, and then the record may be loaded, but not have this attribute set yet,
97
+ # so find the id of of record with the attribute-value pair, and see if that is loaded.
98
+ # find_in_db returns nil if we are not prerendering which will force us to create a new record
99
+ # because there is no way of knowing the id.
100
+ if !attrs.key?(primary_key) && (id = find_in_db(model, attrs))
101
+ record = lookup_by_id(model, id) # @records[model].detect { |record| record.id == id}
102
+ attrs = attrs.merge primary_key => id
103
+ end
104
+ # if we don't have a record then create one
105
+ # (record = new(model)).vector = [model, [:find_by, attribute => value]] unless record
106
+ record ||= set_vector_lookup(new(model), [model, [:find_by, attrs]])
107
+ # and set the values
108
+ attrs.each { |attr, value| record.sync_attribute(attr, value) }
109
+ end
110
+ # finally initialize and return the ar_instance
111
+ record.set_ar_instance!
112
+ end
113
+
114
+ def self.new_from_vector(model, aggregate_owner, *vector)
115
+ # this is the equivilent of find but for associations and aggregations
116
+ # because we are not fetching a specific attribute yet, there is NO communication with the
117
+ # server. That only happens during find.
118
+ model = model.base_class
119
+
120
+ # do we already have a record with this vector? If so return it, otherwise make a new one.
121
+
122
+ # record = @records[model].detect { |record| record.vector == vector }
123
+ record = lookup_by_vector(vector)
124
+ unless record
125
+
126
+ record = new model
127
+ set_vector_lookup(record, vector)
128
+ end
129
+
130
+ record.set_ar_instance!
131
+
132
+ if aggregate_owner
133
+ record.aggregate_owner = aggregate_owner
134
+ record.aggregate_attribute = vector.last
135
+ aggregate_owner.attributes[vector.last] = record.ar_instance
136
+ end
137
+
138
+ record.ar_instance
139
+ end
140
+
141
+ def initialize(model, hash = {}, ar_instance = nil)
142
+ @model = model
143
+ @ar_instance = ar_instance
144
+ @synced_attributes = {}
145
+ @attributes = {}
146
+ @changed_attributes = []
147
+ @virgin = true
148
+ records[model] << self
149
+ Base.set_object_id_lookup(self)
150
+ end
151
+
152
+ def find(*args)
153
+ self.class.find(*args)
154
+ end
155
+
156
+ def new_from_vector(*args)
157
+ self.class.new_from_vector(*args)
158
+ end
159
+
160
+ def primary_key
161
+ @model.primary_key
162
+ end
163
+
164
+ def id
165
+ @attributes[primary_key]
166
+ end
167
+
168
+ def id=(value)
169
+ # value can be nil if we are loading an aggregate otherwise check if it already exists
170
+ # if !(value && (existing_record = records[@model].detect { |record| record.attributes[primary_key] == value}))
171
+ if !(value && (existing_record = Base.lookup_by_id(model, value)))
172
+ @attributes[primary_key] = value
173
+ Base.set_id_lookup(self)
174
+ else
175
+ @ar_instance.instance_variable_set(:@backing_record, existing_record)
176
+ existing_record.attributes.merge!(attributes) { |key, v1, v2| v1 }
177
+ end
178
+ value
179
+ end
180
+
181
+ def changed?(*args)
182
+ if args.count == 0
183
+ React::State.get_state(self, "!CHANGED!")
184
+ !changed_attributes.empty?
185
+ else
186
+ React::State.get_state(self, args[0])
187
+ changed_attributes.include? args[0]
188
+ end
189
+ end
190
+
191
+ def changed_attributes_and_values
192
+ Hash[changed_attributes.collect do |attr|
193
+ [attr, @attributes[attr]] if column_type(attr)
194
+ end.compact]
195
+ end
196
+
197
+ def changes
198
+ Hash[changed_attributes.collect do |attr|
199
+ [attr, [@synced_attributes[attr], @attributes[attr]]] if column_type(attr)
200
+ end.compact]
201
+ end
202
+
203
+ def errors
204
+ @errors ||= ActiveModel::Errors.new(self)
205
+ end
206
+
207
+ # called when we have a newly created record, to initialize
208
+ # any nil collections to empty arrays. We can do this because
209
+ # if its a brand new record, then any collections that are still
210
+ # nil must not have any children.
211
+
212
+ def initialize_collections
213
+ if (!vector || vector.empty?) && id && id != ''
214
+ Base.set_vector_lookup(self, [@model, [:find_by, @model.primary_key => id]])
215
+ end
216
+ Base.load_data do
217
+ @model.reflect_on_all_associations.each do |assoc|
218
+ next if !assoc.collection? || @attributes[assoc.attribute]
219
+ ar_instance.send("#{assoc.attribute}=", [])
220
+ end
221
+ end
222
+ end
223
+
224
+ # sync! now will also initialize any nil collections
225
+ def sync!(hash = {}) # does NOT notify (see saved! for notification)
226
+ # hash.each do |attr, value|
227
+ # @attributes[attr] = convert(attr, value)
228
+ # end
229
+ @synced_attributes = {}
230
+ hash.each { |attr, value| sync_attribute(attr, convert(attr, value)) }
231
+ @changed_attributes = []
232
+ @saving = false
233
+ errors.clear
234
+ # set the vector and clear collections - this only happens when a new record is saved
235
+ initialize_collections if (!vector || vector.empty?) && id && id != ''
236
+ self
237
+ end
238
+
239
+ # this keeps the unscoped collection up to date.
240
+ # @destroy_sync and @create_sync prevent multiple insertions
241
+ # to collections that just have a count
242
+ def sync_unscoped_collection!
243
+ if destroyed
244
+ return if @destroy_sync
245
+ @destroy_sync = true
246
+ else
247
+ return if @create_sync
248
+ @create_sync = true
249
+ end
250
+ model.unscoped << ar_instance
251
+ @synced_with_unscoped = !@synced_with_unscoped
252
+ end
253
+
254
+ def sync_attribute(attribute, value)
255
+
256
+ @synced_attributes[attribute] = @attributes[attribute] = value
257
+ Base.set_id_lookup(self) if attribute == primary_key
258
+
259
+ #@synced_attributes[attribute] = value.dup if value.is_a? ReactiveRecord::Collection
260
+
261
+ if value.is_a? Collection
262
+ @synced_attributes[attribute] = value.dup_for_sync
263
+ elsif aggregation = model.reflect_on_aggregation(attribute) and (aggregation.klass < ActiveRecord::Base)
264
+ value.backing_record.sync!
265
+ elsif aggregation
266
+ @synced_attributes[attribute] = aggregation.deserialize(aggregation.serialize(value))
267
+ elsif !model.reflect_on_association(attribute)
268
+ @synced_attributes[attribute] = JSON.parse(value.to_json)
269
+ end
270
+
271
+ @changed_attributes.delete(attribute)
272
+ value
273
+ end
274
+
275
+ # helper so we can tell if model exists. We need this so we can detect
276
+ # if a record has local changes that are out of sync.
277
+ def self.exists?(model, id)
278
+ Base.lookup_by_id(model, id)
279
+ end
280
+
281
+ def revert
282
+ @changed_attributes.dup.each do |attribute|
283
+ @ar_instance.send("#{attribute}=", @synced_attributes[attribute])
284
+ @attributes.delete(attribute) unless @synced_attributes.key?(attribute)
285
+ end
286
+ @changed_attributes = []
287
+ errors.clear
288
+ end
289
+
290
+ def saving!
291
+ React::State.set_state(self, self, :saving) unless data_loading?
292
+ @saving = true
293
+ end
294
+
295
+ def errors!(hash)
296
+ notify_waiting_for_save
297
+ errors.clear && return unless hash
298
+ hash.each do |attribute, messages|
299
+ messages.each do |message|
300
+ errors.add(attribute, message)
301
+ end
302
+ end
303
+ end
304
+
305
+ def saved!(save_only = nil) # sets saving to false AND notifies
306
+ notify_waiting_for_save
307
+ return self if save_only
308
+ if errors.empty?
309
+ React::State.set_state(self, self, :saved)
310
+ elsif !data_loading?
311
+ React::State.set_state(self, self, :error)
312
+ end
313
+ self
314
+ end
315
+
316
+ def self.when_not_saving(model, &block)
317
+ if @records[model].detect(&:saving?)
318
+ wait_for_save(model, &block)
319
+ else
320
+ yield model
321
+ end
322
+ end
323
+
324
+ def notify_waiting_for_save
325
+ @saving = false
326
+ self.class.notify_waiting_for_save(model)
327
+ end
328
+
329
+ def self.notify_waiting_for_save(model)
330
+ waiters = waiting_for_save(model)
331
+ return if waiters.empty? || @records[model].detect(&:saving?)
332
+ waiters.each { |waiter| waiter.call model }
333
+ clear_waiting_for_save(model)
334
+ end
335
+
336
+ def saving?
337
+ React::State.get_state(self, self)
338
+ @saving
339
+ end
340
+
341
+ def new?
342
+ !id && !vector
343
+ end
344
+
345
+ def set_ar_instance!
346
+ klass = self.class.infer_type_from_hash(model, @attributes)
347
+ @ar_instance = klass._new_without_sti_type_cast(self) unless @ar_instance.class == klass
348
+ @ar_instance
349
+ end
350
+
351
+ class << self
352
+ def infer_type_from_hash(klass, hash)
353
+ klass = klass.base_class
354
+ return klass unless hash
355
+ type = hash[klass.inheritance_column]
356
+ begin
357
+ return Object.const_get(type)
358
+ rescue Exception => e
359
+ message = "Could not subclass #{klass} as #{type}. Perhaps #{type} class has not been required. Exception: #{e}"
360
+ `console.error(#{message})`
361
+ end unless !type || type == ''
362
+ klass
363
+ end
364
+
365
+ attr_reader :outer_scopes
366
+
367
+ def default_scope
368
+ @class_scopes[:default_scope]
369
+ end
370
+
371
+ def unscoped
372
+ @class_scopes[:unscoped]
373
+ end
374
+
375
+ def add_to_outer_scopes(item)
376
+ @outer_scopes << item
377
+ end
378
+
379
+ # While evaluating scopes we want to catch any requests
380
+ # to the server. Once we catch any requests to the server
381
+ # then all the further scopes in that chain will be made
382
+ # at the server.
383
+
384
+ class DbRequestMade < RuntimeError; end
385
+
386
+ def catch_db_requests(return_val = nil)
387
+ @catch_db_requests = true
388
+ yield
389
+ rescue DbRequestMade => e
390
+ React::IsomorphicHelpers.log "Warning: request for server side data during scope evaluation: #{e.message}", :warning
391
+ return_val
392
+ ensure
393
+ @catch_db_requests = false
394
+ end
395
+
396
+ alias pre_synchromesh_load_from_db load_from_db
397
+
398
+ def load_from_db(*args)
399
+ raise DbRequestMade, args if @catch_db_requests
400
+ pre_synchromesh_load_from_db(*args)
401
+ end
402
+ end
403
+
404
+ def destroy_associations
405
+ @destroyed = false
406
+ model.reflect_on_all_associations.each do |association|
407
+ if association.collection?
408
+ @attributes[association.attribute].replace([]) if @attributes[association.attribute]
409
+ else
410
+ @ar_instance.send("#{association.attribute}=", nil)
411
+ end
412
+ end
413
+ @destroyed = true
414
+ end
415
+ end
416
+ end