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,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