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,54 @@
1
+ module ReactiveRecord
2
+ module LookupTables
3
+ def initialize_lookup_tables
4
+ @records = Hash.new { |hash, key| hash[key] = [] }
5
+ @records_by_id = `{}`
6
+ @records_by_vector = `{}`
7
+ @records_by_object_id = `{}`
8
+ @class_scopes = Hash.new { |hash, key| hash[key] = {} }
9
+ @waiting_for_save = Hash.new { |hash, key| hash[key] = [] }
10
+ end
11
+
12
+ def class_scopes(model)
13
+ @class_scopes[model.base_class]
14
+ end
15
+
16
+ def waiting_for_save(model)
17
+ @waiting_for_save[model]
18
+ end
19
+
20
+ def wait_for_save(model, &block)
21
+ @waiting_for_save[model] << block
22
+ end
23
+
24
+ def clear_waiting_for_save(model)
25
+ @waiting_for_save[model] = []
26
+ end
27
+
28
+ def lookup_by_object_id(object_id)
29
+ `#{@records_by_object_id}[#{object_id}]`.ar_instance
30
+ end
31
+
32
+ def set_object_id_lookup(record)
33
+ `#{@records_by_object_id}[#{record.object_id}] = #{record}`
34
+ end
35
+
36
+ def lookup_by_id(*args) # model and id
37
+ `#{@records_by_id}[#{args}]` || nil
38
+ end
39
+
40
+ def set_id_lookup(record)
41
+ `#{@records_by_id}[#{[record.model, record.id]}] = #{record}`
42
+ end
43
+
44
+ def lookup_by_vector(vector)
45
+ `#{@records_by_vector}[#{vector}]` || nil
46
+ end
47
+
48
+ def set_vector_lookup(record, vector)
49
+ record.vector = vector
50
+ `delete #{@records_by_vector}[#{record.vector}]`
51
+ `#{@records_by_vector}[#{vector}] = record`
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,107 @@
1
+ module ReactiveRecord
2
+ # redefine if you want to process errors (i.e. logging, rollbar, etc)
3
+ def self.on_fetch_error(e, params); end
4
+
5
+ # associations: {parent_id: record.object_id, attribute: attribute, child_id: assoc_record.object_id}
6
+ # models: {id: record.object_id, model: record.model.model_name.to_s, attributes: changed_attributes}
7
+
8
+ module Operations
9
+ # to make debug easier we convert all the object_id strings to be hex representation
10
+ class Base < Hyperloop::ControllerOp
11
+ param :acting_user, nils: true
12
+
13
+ FORMAT = '0x%x'
14
+
15
+ def self.serialize_params(hash)
16
+ hash['associations'].each do |assoc|
17
+ assoc['parent_id'] = FORMAT % assoc['parent_id']
18
+ assoc['child_id'] = FORMAT % assoc['child_id']
19
+ end if hash['associations']
20
+ hash['models'].each do |assoc|
21
+ assoc['id'] = FORMAT % assoc[:id]
22
+ end if hash['models']
23
+ hash
24
+ end
25
+
26
+ def self.deserialize_params(hash)
27
+ hash['associations'].each do |assoc|
28
+ assoc['parent_id'] = assoc['parent_id'].to_i(16)
29
+ assoc['child_id'] = assoc['child_id'].to_i(16)
30
+ end if hash['associations']
31
+ hash['models'].each do |assoc|
32
+ assoc['id'] = assoc['id'].to_i(16)
33
+ end if hash['models']
34
+ hash
35
+ end
36
+
37
+ def self.serialize_response(response)
38
+ response[:saved_models].each do |saved_model|
39
+ saved_model[0] = FORMAT % saved_model[0]
40
+ end if response.is_a?(Hash) && response[:saved_models]
41
+ response
42
+ end
43
+
44
+ def self.deserialize_response(response)
45
+ response[:saved_models].each do |saved_model|
46
+ saved_model[0] = saved_model[0].to_i(16)
47
+ end if response.is_a?(Hash) && response[:saved_models]
48
+ response
49
+ end
50
+ end
51
+ # fetch queued up records from the server
52
+ # subclass of ControllerOp so we can pass the controller
53
+ # along to on_error
54
+ class Fetch < Base
55
+ param :acting_user, nils: true
56
+ param models: []
57
+ param associations: []
58
+ param :pending_fetches
59
+ step do
60
+ ReactiveRecord::ServerDataCache[
61
+ params.models.map(&:with_indifferent_access),
62
+ params.associations.map(&:with_indifferent_access),
63
+ params.pending_fetches,
64
+ params.acting_user
65
+ ]
66
+ end
67
+ failed do |e|
68
+ # AccessViolations are already sent to on_error
69
+ Hyperloop.on_error(e, :fetch_error, params.to_h) unless e.is_a? Hyperloop::AccessViolation
70
+ raise e
71
+ end
72
+ end
73
+
74
+ class Save < Base
75
+ param :acting_user, nils: true
76
+ param models: []
77
+ param associations: []
78
+ param :save, type: :boolean
79
+ param :validate, type: :boolean
80
+
81
+ step do
82
+ ReactiveRecord::Base.save_records(
83
+ params.models.map(&:with_indifferent_access),
84
+ params.associations.map(&:with_indifferent_access),
85
+ params.acting_user,
86
+ params.validate,
87
+ params.save
88
+ )
89
+ end
90
+ end
91
+
92
+ class Destroy < Base
93
+ param :acting_user, nils: true
94
+ param :model
95
+ param :id
96
+ param :vector
97
+ step do
98
+ ReactiveRecord::Base.destroy_record(
99
+ params.model,
100
+ params.id,
101
+ params.vector,
102
+ params.acting_user
103
+ )
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,62 @@
1
+ module ReactiveRecord
2
+ # The base collection class works with relationships
3
+ # method overrides for scoped collections
4
+ module ScopedCollection
5
+ [:filter?, :collector?, :joins_with?, :related_records_for].each do |method|
6
+ define_method(method) { |*args| @scope_description.send method, *args }
7
+ end
8
+
9
+ def set_pre_sync_related_records(related_records, _record = nil)
10
+ @pre_sync_related_records = nil
11
+ ReactiveRecord::Base.catch_db_requests do
12
+ @pre_sync_related_records = filter_records(related_records)
13
+ live_scopes.each do |scope|
14
+ scope.set_pre_sync_related_records(@pre_sync_related_records)
15
+ end
16
+ end if filter?
17
+ end
18
+
19
+ def sync_scopes(related_records, record, filtering = true)
20
+ filtering =
21
+ @pre_sync_related_records && filtering &&
22
+ ReactiveRecord::Base.catch_db_requests do
23
+ related_records = update_collection(related_records)
24
+ end
25
+ reload_from_db if !filtering && joins_with?(record)
26
+ live_scopes.each { |scope| scope.sync_scopes(related_records, record, filtering) }
27
+ ensure
28
+ @pre_sync_related_records = nil
29
+ end
30
+
31
+ def update_collection(related_records)
32
+ if collector?
33
+ update_collector_scope(related_records)
34
+ else
35
+ related_records = filter_records(related_records)
36
+ update_filter_scope(@pre_sync_related_records, related_records)
37
+ end
38
+ end
39
+
40
+ def update_collector_scope(related_records)
41
+ current = Set.new([*@collection])
42
+ (related_records - @pre_sync_related_records).each { |r| current << r }
43
+ (@pre_sync_related_records - related_records).each { |r| current.delete(r) }
44
+ replace(filter_records(current))
45
+ Set.new([*@collection])
46
+ end
47
+
48
+ def update_filter_scope(before, after)
49
+ if (collection || !@count.nil?) && before != after
50
+ if collection
51
+ (after - before).each { |r| push r }
52
+ (before - after).each { |r| delete r }
53
+ else
54
+ @count += (after - before).count
55
+ @count -= (before - after).count
56
+ notify_of_change self # TODO: remove self .... and retest
57
+ end
58
+ end
59
+ after
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,194 @@
1
+ module ReactiveRecord
2
+ module Setters
3
+ def set_attr_value(attr, raw_value)
4
+ set_common(attr, raw_value) { |value| update_simple_attribute(attr, value) }
5
+ end
6
+
7
+ def set_ar_aggregate(aggr, raw_value)
8
+ set_common(aggr.attribute, raw_value) do |value, attr|
9
+ @attributes[attr] ||= aggr.klass.new if new?
10
+ abr = @attributes[attr].backing_record
11
+ abr.virgin = false
12
+ map = value.attributes if value
13
+ aggr.mapped_attributes.each do |mapped_attr|
14
+ abr.update_aggregate_attribute mapped_attr, map && map[mapped_attr]
15
+ end
16
+ return @attributes[attr]
17
+ end
18
+ end
19
+
20
+ def set_non_ar_aggregate(aggregation, raw_value)
21
+ set_common(aggregation.attribute, raw_value) do |value, attr|
22
+ if data_loading?
23
+ @synced_attributes[attr] = aggregation.deserialize(aggregation.serialize(value))
24
+ else
25
+ changed = !@synced_attributes.key?(attr) || @synced_attributes[attr] != value
26
+ end
27
+ set_attribute_change_status_and_notify attr, changed, value
28
+ end
29
+ end
30
+
31
+ def set_has_many(assoc, raw_value)
32
+ set_common(assoc.attribute, raw_value) do |value, attr|
33
+ # create a new collection to hold value, shove it in, and return the new collection
34
+ # the replace method will take care of updating the inverse belongs_to links as
35
+ # the collection is overwritten
36
+ collection = Collection.new(assoc.klass, @ar_instance, assoc)
37
+ collection.replace(value || [])
38
+ @synced_attributes[attr] = value if data_loading?
39
+ set_attribute_change_status_and_notify attr, value != @synced_attributes[attr], collection
40
+ return collection
41
+ end
42
+ end
43
+
44
+ def set_belongs_to(assoc, raw_value)
45
+ set_common(assoc.attribute, raw_value) do |value, attr|
46
+ if assoc.inverse.collection?
47
+ update_has_many_through_associations assoc, value
48
+ update_inverse_collections assoc, value
49
+ else
50
+ update_inverse_attribute assoc, value
51
+ end
52
+ # itself will just reactively read the value (a model instance) by doing a .id
53
+ update_belongs_to attr, value.itself
54
+ end
55
+ end
56
+
57
+ def sync_has_many(attr)
58
+ set_change_status_and_notify_only attr, @attributes[attr] != @synced_attributes[attr]
59
+ end
60
+
61
+ def update_simple_attribute(attr, value)
62
+ if data_loading?
63
+ @synced_attributes[attr] = value
64
+ else
65
+ changed = !@synced_attributes.key?(attr) || @synced_attributes[attr] != value
66
+ end
67
+ set_attribute_change_status_and_notify attr, changed, value
68
+ end
69
+
70
+ alias update_belongs_to update_simple_attribute
71
+ alias update_aggregate_attribute update_simple_attribute
72
+
73
+ private
74
+
75
+ def set_common(attr, value)
76
+ value = convert(attr, value)
77
+ @virgin = false unless data_loading?
78
+ if !@destroyed && (
79
+ !@attributes.key?(attr) ||
80
+ @attributes[attr].is_a?(Base::DummyValue) ||
81
+ @attributes[attr] != value)
82
+ yield value, attr
83
+ end
84
+ value
85
+ end
86
+
87
+ def set_attribute_change_status_and_notify(attr, changed, new_value)
88
+ if @virgin
89
+ @attributes[attr] = new_value
90
+ else
91
+ change_status_and_notify_helper(attr, changed) do |had_key, current_value|
92
+ @attributes[attr] = new_value
93
+ if !data_loading? ||
94
+ (on_opal_client? && had_key && current_value.loaded? && current_value != new_value)
95
+ React::State.set_state(self, attr, new_value, data_loading?)
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ def set_change_status_and_notify_only(attr, changed)
102
+ return if @virgin
103
+ change_status_and_notify_helper(attr, changed) do
104
+ React::State.set_state(self, attr, nil) unless data_loading?
105
+ end
106
+ end
107
+
108
+ def change_status_and_notify_helper(attr, changed)
109
+ empty_before = changed_attributes.empty?
110
+ # TODO: confirm this works:
111
+ # || data_loading? added so that model.new can be wrapped in a ReactiveRecord.load_data
112
+ if !changed || data_loading?
113
+ changed_attributes.delete(attr)
114
+ elsif !changed_attributes.include?(attr)
115
+ changed_attributes << attr
116
+ end
117
+ yield @attributes.key?(attr), @attributes[attr]
118
+ return unless empty_before != changed_attributes.empty?
119
+ if on_opal_client? && !data_loading?
120
+ React::State.set_state(self, '!CHANGED!', !changed_attributes.empty?, true)
121
+ end
122
+ return unless aggregate_owner
123
+ aggregate_owner.set_change_status_and_notify_only(
124
+ attr, !@attributes[attr].backing_record.changed_attributes.empty?
125
+ )
126
+ end
127
+
128
+ def update_inverse_attribute(association, value)
129
+ # when updating the inverse attribute of a belongs_to that is itself a belongs_to
130
+ # (i.e. 1-1 relationship) we clear the existing inverse value and then
131
+ # write the current record to the new value
132
+ current_value = @attributes[association.attribute]
133
+ inverse_attr = association.inverse.attribute
134
+ current_value.attributes[inverse_attr] = nil unless current_value.nil?
135
+ return if value.nil?
136
+ value.attributes[inverse_attr] = @ar_instance
137
+ return if data_loading?
138
+ React::State.set_state(value.backing_record, inverse_attr, @ar_instance)
139
+ end
140
+
141
+ def update_inverse_collections(association, value)
142
+ # when updating an inverse attribute of a belongs_to that is a has_many (i.e. a collection)
143
+ # we need to first remove the current associated value (if non-nil), then add the new
144
+ # value to the collection. If the inverse collection is not yet initialized we do it here.
145
+ current_value = @attributes[association.attribute]
146
+ inverse_attr = association.inverse.attribute
147
+ if value.nil?
148
+ current_value.attributes[inverse_attr].delete(@ar_instance) unless current_value.nil?
149
+ else
150
+ value.backing_record.push_onto_collection(@model, association.inverse, @ar_instance)
151
+ end
152
+ end
153
+
154
+ def push_onto_collection(model, association, ar_instance)
155
+ @attributes[association.attribute] ||= Collection.new(model, @ar_instance, association)
156
+ @attributes[association.attribute] << ar_instance
157
+ end
158
+
159
+ def update_has_many_through_associations(association, value)
160
+ association.through_associations.each { |ta| update_through_association(ta, value) }
161
+ association.source_associations.each { |sa| update_source_association(sa, value) }
162
+ end
163
+
164
+ def update_through_association(ta, new_belongs_to_value)
165
+ # appointment.doctor = doctor_new_value (i.e. through association is changing)
166
+ # means appointment.doctor_new_value.patients << appointment.patient
167
+ # and we have to appointment.doctor_current_value.patients.delete(appointment.patient)
168
+ source_value = @attributes[ta.source]
169
+ current_belongs_to_value = @attributes[ta.inverse.attribute]
170
+ return unless source_value
171
+ unless current_belongs_to_value.nil? || current_belongs_to_value.attributes[ta.attribute].nil?
172
+ current_belongs_to_value.attributes[ta.attribute].delete(source_value)
173
+ end
174
+ return unless new_belongs_to_value
175
+ new_belongs_to_value.attributes[ta.attribute] ||= Collection.new(ta.klass, new_belongs_to_value, ta)
176
+ new_belongs_to_value.attributes[ta.attribute] << source_value
177
+ end
178
+
179
+ def update_source_association(sa, new_source_value)
180
+ # appointment.patient = patient_value (i.e. source is changing)
181
+ # means appointment.doctor.patients.delete(appointment.patient)
182
+ # means appointment.doctor.patients << patient_value
183
+ belongs_to_value = @attributes[sa.inverse.attribute]
184
+ current_source_value = @attributes[sa.source]
185
+ return unless belongs_to_value
186
+ unless belongs_to_value.attributes[sa.attribute].nil? || current_source_value.nil?
187
+ belongs_to_value.attributes[sa.attribute].delete(current_source_value)
188
+ end
189
+ return unless new_source_value
190
+ belongs_to_value.attributes[sa.attribute] ||= Collection.new(sa.klass, belongs_to_value, sa)
191
+ belongs_to_value.attributes[sa.attribute] << new_source_value
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,16 @@
1
+ module ReactiveRecord
2
+ # The base collection class works with relationships
3
+ # method overrides for the unscoped collection
4
+ module UnscopedCollection
5
+ def set_pre_sync_related_records(related_records, _record = nil)
6
+ @pre_sync_related_records = related_records
7
+ live_scopes.each { |scope| scope.set_pre_sync_related_records(@pre_sync_related_records) }
8
+ end
9
+
10
+ def sync_scopes(related_records, record, filtering = true)
11
+ live_scopes.each { |scope| scope.sync_scopes(related_records, record, filtering) }
12
+ ensure
13
+ @pre_sync_related_records = nil
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,343 @@
1
+ module HyperMesh
2
+ def self.load(&block)
3
+ ReactiveRecord.load(&block)
4
+ end
5
+ end
6
+
7
+ module ReactiveRecord
8
+
9
+ # will repeatedly execute the block until it is loaded
10
+ # immediately returns a promise that will resolve once the block is loaded
11
+
12
+ def self.load(&block)
13
+ promise = Promise.new
14
+ @load_stack ||= []
15
+ @load_stack << @loads_pending
16
+ @loads_pending = nil
17
+ result = block.call.itself
18
+ if @loads_pending
19
+ @blocks_to_load ||= []
20
+ @blocks_to_load << [Base.current_fetch_id, promise, block]
21
+ else
22
+ promise.resolve result
23
+ end
24
+ @loads_pending = @load_stack.pop
25
+ promise
26
+ rescue Exception => e
27
+ React::IsomorphicHelpers.log "ReactiveRecord.load exception raised during initial load: #{e}", :error
28
+ end
29
+
30
+ def self.loads_pending!
31
+ @loads_pending = true
32
+ end
33
+
34
+ def self.check_loads_pending
35
+ if @loads_pending
36
+ if Base.pending_fetches.count > 0
37
+ true
38
+ else # this happens when for example loading foo.x results in somebody looking at foo.y while foo.y is still being loaded
39
+ ReactiveRecord::WhileLoading.loaded_at Base.current_fetch_id
40
+ ReactiveRecord::WhileLoading.quiet!
41
+ false
42
+ end
43
+ end
44
+ end
45
+
46
+ def self.run_blocks_to_load(fetch_id, failure = nil)
47
+ if @blocks_to_load
48
+ blocks_to_load_now = @blocks_to_load.select { |data| data.first == fetch_id }
49
+ @blocks_to_load = @blocks_to_load.reject { |data| data.first == fetch_id }
50
+ @load_stack ||= []
51
+ blocks_to_load_now.each do |data|
52
+ id, promise, block = data
53
+ @load_stack << @loads_pending
54
+ @loads_pending = nil
55
+ result = block.call(failure)
56
+ if check_loads_pending && !failure
57
+ @blocks_to_load << [Base.current_fetch_id, promise, block]
58
+ else
59
+ promise.resolve result
60
+ end
61
+ @loads_pending = @load_stack.pop
62
+ end
63
+ end
64
+ rescue Exception => e
65
+ React::IsomorphicHelpers.log "ReactiveRecord.load exception raised during retry: #{e}", :error
66
+ end
67
+
68
+
69
+ # Adds while_loading feature to React
70
+ # to use attach a .while_loading handler to any element for example
71
+ # div { "displayed if everything is loaded" }.while_loading { "displayed while I'm loading" }
72
+ # the contents of the div will be switched (using javascript classes) depending on the state of contents of the first block
73
+
74
+ # To notify React that something is loading use React::WhileLoading.loading!
75
+ # once everything is loaded then do React::WhileLoading.loaded_at message (typically a time stamp just for debug purposes)
76
+
77
+ class WhileLoading
78
+
79
+ include React::IsomorphicHelpers
80
+
81
+ before_first_mount do
82
+ @css_to_preload = ""
83
+ @while_loading_counter = 0
84
+ end
85
+
86
+ def self.get_next_while_loading_counter
87
+ @while_loading_counter += 1
88
+ end
89
+
90
+ def self.preload_css(css)
91
+ @css_to_preload += "#{css}\n"
92
+ end
93
+
94
+ def self.has_observers?
95
+ React::State.has_observers?(self, :loaded_at)
96
+ end
97
+
98
+ prerender_footer do
99
+ "<style>\n#{@css_to_preload}\n</style>".tap { @css_to_preload = ""}
100
+ end
101
+
102
+ if RUBY_ENGINE == 'opal'
103
+
104
+ # +: I DONT THINK WE USE opal-jquery in this module anymore - require 'opal-jquery' if opal_client?
105
+ # -: You think wrong. add_style_sheet uses the jQuery $, after_mount too, others too
106
+ # -: I removed those references. Now you think right.
107
+
108
+ include Hyperloop::Component::Mixin
109
+
110
+ param :loading
111
+ param :loaded_children
112
+ param :loading_children
113
+ param :element_type
114
+ param :element_props
115
+ param :display, default: ''
116
+
117
+ class << self
118
+
119
+ def loading?
120
+ @is_loading
121
+ end
122
+
123
+ def loading!
124
+ React::RenderingContext.waiting_on_resources = true
125
+ React::State.get_state(self, :loaded_at)
126
+ # this was moved to where the fetch is actually pushed on to the fetch array in isomorphic base
127
+ # React::State.set_state(self, :quiet, false)
128
+ @is_loading = true
129
+ end
130
+
131
+ def loaded_at(loaded_at)
132
+ React::State.set_state(self, :loaded_at, loaded_at)
133
+ @is_loading = false
134
+ end
135
+
136
+ def quiet?
137
+ React::State.get_state(self, :quiet)
138
+ end
139
+
140
+ def page_loaded?
141
+ React::State.get_state(self, :page_loaded)
142
+ end
143
+
144
+ def quiet!
145
+ React::State.set_state(self, :quiet, true)
146
+ after(1) { React::State.set_state(self, :page_loaded, true) } unless on_opal_server? or @page_loaded
147
+ @page_loaded = true
148
+ end
149
+
150
+ def add_style_sheet
151
+ # directly assigning the code to the variable triggers a opal 0.10.5 compiler bug.
152
+ unless @style_sheet_added
153
+ %x{
154
+ var style_el = document.createElement("style");
155
+ style_el.setAttribute("type", "text/css");
156
+ style_el.innerHTML = ".reactive_record_is_loading > .reactive_record_show_when_loaded { display: none; }\n" +
157
+ ".reactive_record_is_loaded > .reactive_record_show_while_loading { display: none; }";
158
+ document.head.append(style_el);
159
+ }
160
+ @style_sheet_added = true
161
+ end
162
+ end
163
+
164
+ end
165
+
166
+ before_mount do
167
+ @uniq_id = WhileLoading.get_next_while_loading_counter
168
+ WhileLoading.preload_css(
169
+ ".reactive_record_while_loading_container_#{@uniq_id} > :nth-child(1n+#{params.loaded_children.count+1}) {\n"+
170
+ " display: none;\n"+
171
+ "}\n"
172
+ )
173
+ end
174
+
175
+ after_mount do
176
+ @waiting_on_resources = params.loading
177
+ WhileLoading.add_style_sheet
178
+ node = dom_node
179
+ %x{
180
+ var nodes = node.querySelectorAll(':nth-child(-1n+'+#{params.loaded_children.count}+')');
181
+ nodes.forEach(
182
+ function(current_node, current_index, list_obj) {
183
+ if (current_node.className.indexOf('reactive_record_show_when_loaded') === -1) {
184
+ current_node.className = current_node.className + ' reactive_record_show_when_loaded';
185
+ }
186
+ }
187
+ );
188
+ nodes = node.querySelectorAll(':nth-child(1n+'+#{params.loaded_children.count+1}+')');
189
+ nodes.forEach(
190
+ function(current_node, current_index, list_obj) {
191
+ if (current_node.className.indexOf('reactive_record_show_while_loading') === -1) {
192
+ current_node.className = current_node.className + ' reactive_record_show_while_loading';
193
+ }
194
+ }
195
+ );
196
+ }
197
+ end
198
+
199
+ after_update do
200
+ @waiting_on_resources = params.loading
201
+ end
202
+
203
+ def render
204
+ props = params.element_props.dup
205
+ classes = [props[:class], props[:className], "reactive_record_while_loading_container_#{@uniq_id}"].compact.join(" ")
206
+ props.merge!({
207
+ "data-reactive_record_while_loading_container_id" => @uniq_id,
208
+ "data-reactive_record_enclosing_while_loading_container_id" => @uniq_id,
209
+ class: classes
210
+ })
211
+ React.create_element(params.element_type[0], props) do
212
+ params.loaded_children + params.loading_children
213
+ end.tap { |e| e.waiting_on_resources = params.loading }
214
+ end
215
+
216
+ end
217
+
218
+ end
219
+
220
+ end
221
+
222
+ module React
223
+
224
+ class Element
225
+
226
+ def while_loading(display = "", &loading_display_block)
227
+ loaded_children = []
228
+ loaded_children = block.call.dup if block
229
+ if display.respond_to? :as_node
230
+ display = display.as_node
231
+ loading_display_block = lambda { display.render }
232
+ elsif !loading_display_block
233
+ loading_display_block = lambda { display }
234
+ end
235
+ loading_children = RenderingContext.build do |buffer|
236
+ result = loading_display_block.call
237
+ result = result.to_s if result.try :acts_as_string?
238
+ result.span.tap { |e| e.waiting_on_resources = RenderingContext.waiting_on_resources } if result.is_a? String
239
+ buffer.dup
240
+ end
241
+
242
+ new_element = React.create_element(
243
+ ReactiveRecord::WhileLoading,
244
+ loading: waiting_on_resources,
245
+ loading_children: loading_children,
246
+ loaded_children: loaded_children,
247
+ element_type: [type],
248
+ element_props: properties)
249
+
250
+ RenderingContext.replace(self, new_element)
251
+ end
252
+
253
+ def hide_while_loading
254
+ while_loading
255
+ end
256
+
257
+ end
258
+ end
259
+
260
+ if RUBY_ENGINE == 'opal'
261
+ module Hyperloop
262
+ class Component
263
+ module Mixin
264
+
265
+ alias_method :original_component_did_mount, :component_did_mount
266
+
267
+ def component_did_mount(*args)
268
+ original_component_did_mount(*args)
269
+ reactive_record_link_to_enclosing_while_loading_container
270
+ reactive_record_link_set_while_loading_container_class
271
+ end
272
+
273
+ alias_method :original_component_did_update, :component_did_update
274
+
275
+ def component_did_update(*args)
276
+ original_component_did_update(*args)
277
+ reactive_record_link_set_while_loading_container_class
278
+ end
279
+
280
+ def reactive_record_link_to_enclosing_while_loading_container
281
+ # Call after any component mounts - attaches the containers loading id to this component
282
+ # Fyi, the while_loading container is responsible for setting its own link to itself
283
+ node = dom_node
284
+ %x{
285
+ if (typeof node === "undefined" || node === null) return;
286
+ var node_wl_attr = node.getAttribute('data-reactive_record_enclosing_while_loading_container_id');
287
+ if (node_wl_attr === null || node_wl_attr === "") {
288
+ var while_loading_container = node.closest('[data-reactive_record_while_loading_container_id]');
289
+ if (while_loading_container !== null) {
290
+ var container_id = while_loading_container.getAttribute('data-reactive_record_while_loading_container_id');
291
+ node.setAttribute('data-reactive_record_enclosing_while_loading_container_id', container_id);
292
+ }
293
+ }
294
+ }
295
+ end
296
+
297
+ def reactive_record_link_set_while_loading_container_class
298
+ node = dom_node
299
+ loading = (waiting_on_resources ? `true` : `false`)
300
+ %x{
301
+ if (typeof node === "undefined" || node === null) return;
302
+ var while_loading_container_id = node.getAttribute('data-reactive_record_while_loading_container_id');
303
+ if (#{!self.is_a?(ReactiveRecord::WhileLoading)} && while_loading_container_id !== null && while_loading_container_id !== "") {
304
+ return;
305
+ }
306
+ var enc_while_loading_container_id = node.getAttribute('data-reactive_record_enclosing_while_loading_container_id');
307
+ if (enc_while_loading_container_id !== null && enc_while_loading_container_id !== "") {
308
+ var while_loading_container = document.body.querySelector('[data-reactive_record_while_loading_container_id="'+enc_while_loading_container_id+'"]');
309
+ if (loading) {
310
+ node.className = node.className.replace(/reactive_record_is_loaded/g, '').replace(/ /g, ' ');
311
+ if (node.className.indexOf('reactive_record_is_loading') === -1) {
312
+ node.className = node.className + ' reactive_record_is_loading';
313
+ }
314
+ if (while_loading_container !== null) {
315
+ while_loading_container.className = while_loading_container.className.replace(/reactive_record_is_loaded/g, '').replace(/ /g, ' ');
316
+ if (while_loading_container.className.indexOf('reactive_record_is_loading') === -1) {
317
+ while_loading_container.className = while_loading_container.className + ' reactive_record_is_loading';
318
+ }
319
+ }
320
+ } else if (node.className.indexOf('reactive_record_is_loaded') === -1) {
321
+ if (while_loading_container_id === null || while_loading_container_id === "") {
322
+ node.className = node.className.replace(/reactive_record_is_loading/g, '').replace(/ /g, ' ');
323
+ if (node.className.indexOf('reactive_record_is_loaded') === -1) {
324
+ node.className = node.className + ' reactive_record_is_loaded';
325
+ }
326
+ }
327
+ if (while_loading_container.className.indexOf('reactive_record_is_loaded') === -1) {
328
+ var loading_children = while_loading_container.querySelectorAll('[data-reactive_record_enclosing_while_loading_container_id="'+enc_while_loading_container_id+'"].reactive_record_is_loading');
329
+ if (loading_children.length === 0) {
330
+ while_loading_container.className = while_loading_container.className.replace(/reactive_record_is_loading/g, '').replace(/ /g, ' ');
331
+ if (while_loading_container.className.indexOf('reactive_record_is_loaded') === -1) {
332
+ while_loading_container.className = while_loading_container.className + ' reactive_record_is_loaded';
333
+ }
334
+ }
335
+ }
336
+ }
337
+ }
338
+ }
339
+ end
340
+ end
341
+ end
342
+ end
343
+ end