hyper-resource 1.0.0.lap34

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.
@@ -0,0 +1,300 @@
1
+ module HyperRecord
2
+ module ClientInstanceMethods
3
+
4
+ def initialize(record_hash = {})
5
+ # initalize internal data structures
6
+ record_hash = {} if record_hash.nil?
7
+ @properties_hash = {}
8
+ @changed_properties_hash = {}
9
+ @relations = {}
10
+ @rest_methods_hash = {}
11
+ self.class.rest_methods.keys.each { |method| @rest_methods_hash[method] = {} }
12
+ @destroyed = false
13
+ # for reactivity
14
+ @fetch_states = {}
15
+ @state_key = "#{self.class.to_s}_#{self.object_id}"
16
+ @observers = Set.new
17
+ @registered_collections = Set.new
18
+
19
+ _initialize_from_hash(record_hash)
20
+
21
+ # for reactivity
22
+ _register_observer
23
+ end
24
+
25
+ def _initialize_from_hash(record_hash)
26
+ reflections.keys.each do |relation|
27
+ if record_hash.has_key?(relation)
28
+ @fetch_states[relation] = 'f' # fetched
29
+ if reflections[relation][:kind] == :has_many
30
+ if record_hash[relation].nil?
31
+ @relations[relation] = HyperRecord::Collection.new([], self, relation)
32
+ else
33
+ @relations[relation] = self.class._convert_array_to_collection(record_hash[relation], self, relation)
34
+ end
35
+ else
36
+ @relations[relation] = self.class._convert_json_hash_to_record(record_hash[relation])
37
+ end
38
+ else
39
+ unless @fetch_states[collection] == 'f'
40
+ if reflections[relation][:kind] == :has_many
41
+ @relations[relation] = HyperRecord::Collection.new([], self, relation)
42
+ else
43
+ @relations[relation] = nil
44
+ end
45
+ @fetch_states[relation] = 'n' # not fetched
46
+ end
47
+ end
48
+ record_hash.delete(relation)
49
+ end
50
+
51
+ @properties_hash = record_hash
52
+
53
+ # change state
54
+ _mutate_state
55
+
56
+ # cache in global cache
57
+ self.class._record_cache[@properties_hash[:id]] = self if @properties_hash.has_key?(:id)
58
+ end
59
+
60
+ ### reactive api
61
+
62
+ def destroy
63
+ destroy_record
64
+ nil
65
+ end
66
+
67
+ def destroyed?
68
+ @destroyed
69
+ end
70
+
71
+ def link(other_record)
72
+ link_record(other_record)
73
+ self
74
+ end
75
+
76
+ def method_missing(method, arg)
77
+ if method.end_with?('=')
78
+ _register_observer
79
+ @changed_properties_hash[method.chop] = arg
80
+ else
81
+ _register_observer
82
+ if @changed_properties_hash.has_key?(method)
83
+ @changed_properties_hash[method]
84
+ else
85
+ @properties_hash[method]
86
+ end
87
+ end
88
+ end
89
+
90
+ def reflections
91
+ self.class.reflections
92
+ end
93
+
94
+ def reset
95
+ _register_observer
96
+ @changed_properties_hash = {}
97
+ end
98
+
99
+ def resource_base_uri
100
+ self.class.resource_base_uri
101
+ end
102
+
103
+ def rest_method_force_updates(method_name)
104
+ @rest_methods_hash[method_name][:force] = true
105
+ end
106
+
107
+ def rest_method_unforce_updates(method_name)
108
+ @rest_methods_hash[method_name][:force] = false
109
+ end
110
+
111
+ def save
112
+ save_record
113
+ self
114
+ end
115
+
116
+ def to_hash
117
+ _register_observer
118
+ res = @properties_hash.dup
119
+ res.merge!(@changed_properties_hash)
120
+ res
121
+ end
122
+
123
+ def to_s
124
+ _register_observer
125
+ @properties_hash.to_s
126
+ end
127
+
128
+ def unlink(other_record)
129
+ unlink_record(other_record, observer)
130
+ self
131
+ end
132
+
133
+ ### promise api
134
+
135
+ def destroy_record
136
+ _local_destroy
137
+ self.class._promise_delete("#{resource_base_uri}/#{@properties_hash[:id]}").then do |response|
138
+ nil
139
+ end.fail do |response|
140
+ error_message = "Destroying record #{self} failed!"
141
+ `console.error(error_message)`
142
+ response
143
+ end
144
+ end
145
+
146
+ def link_record(other_record, relation_name = nil)
147
+ _register_observer
148
+ called_from_collection = relation_name ? true : false
149
+ relation_name = other_record.class.to_s.underscore.pluralize unless relation_name
150
+ if reflections.has_key?(relation_name)
151
+ self.send(relation_name).push(other_record) unless called_from_collection
152
+ else
153
+ relation_name = other_record.class.to_s.underscore
154
+ raise "No collection for record of type #{other_record.class}" unless reflections.has_key?(relation_name)
155
+ self.send("#{relation_name}=", other_record) unless called_from_collection
156
+ end
157
+ payload_hash = other_record.to_hash
158
+ self.class._promise_post("#{resource_base_uri}/#{self.id}/relations/#{relation_name}.json", { data: payload_hash }).then do |response|
159
+ other_record.instance_variable_get(:@properties_hash).merge!(response.json[other_record.class.to_s.underscore])
160
+ _notify_observers
161
+ self
162
+ end.fail do |response|
163
+ error_message = "Linking record #{other_record} to #{self} failed!"
164
+ `console.error(error_message)`
165
+ response
166
+ end
167
+ end
168
+
169
+ def save_record
170
+ _register_observer
171
+ payload_hash = @properties_hash.merge(@changed_properties_hash) # copy hash, becasue we need to delete some keys
172
+ (%i[id created_at updated_at] + reflections.keys).each do |key|
173
+ payload_hash.delete(key)
174
+ end
175
+ if @properties_hash[:id] && ! (@changed_properties_hash.has_key?(:id) && @changed_properties_hash[:id].nil?)
176
+ reset
177
+ self.class._promise_patch("#{resource_base_uri}/#{@properties_hash[:id]}", { data: payload_hash }).then do |response|
178
+ @properties_hash.merge!(response.json[self.class.to_s.underscore])
179
+ _notify_observers
180
+ self
181
+ end.fail do |response|
182
+ error_message = "Saving record #{self} failed!"
183
+ `console.error(error_message)`
184
+ response
185
+ end
186
+ else
187
+ reset
188
+ self.class._promise_post(resource_base_uri, { data: payload_hash }).then do |response|
189
+ @properties_hash.merge!(response.json[self.class.to_s.underscore])
190
+ _notify_observers
191
+ self
192
+ end.fail do |response|
193
+ error_message = "Creating record #{self} failed!"
194
+ `console.error(error_message)`
195
+ response
196
+ end
197
+ end
198
+ end
199
+
200
+ def unlink_record(other_record, relation_name = nil)
201
+ _register_observer
202
+ called_from_collection = collection_name ? true : false
203
+ relation_name = other_record.class.to_s.underscore.pluralize unless relation_name
204
+ raise "No relation for record of type #{other_record.class}" unless reflections.has_key?(relation_name)
205
+ self.send(relation_name).delete_if { |cr| cr == other_record } unless called_from_collection
206
+ self.class._promise_delete("#{resource_base_uri}/#{@properties_hash[:id]}/relations/#{relation_name}.json?record_id=#{other_record.id}").then do |response|
207
+ _notify_observers
208
+ self
209
+ end.fail do |response|
210
+ error_message = "Unlinking #{other_record} from #{self} failed!"
211
+ `console.error(error_message)`
212
+ response
213
+ end
214
+ end
215
+
216
+ ### internal
217
+
218
+ def _local_destroy
219
+ _register_observer
220
+ @destroyed = true
221
+ self.class._record_cache.delete(@properties_hash[:id])
222
+ @registered_collections.dup.each do |collection|
223
+ collection.delete(self)
224
+ end
225
+ @registered_collections = Set.new
226
+ _notify_observers
227
+ end
228
+
229
+ def _notify_observers
230
+ mutate.record_state(`Date.now() + Math.random()`)
231
+ @observers.each do |observer|
232
+ React::State.set_state(observer, @state_key, `Date.now() + Math.random()`)
233
+ end
234
+ @observers = Set.new
235
+ self.class._notify_class_observers
236
+ end
237
+
238
+ def _register_collection(collection)
239
+ @registered_collections << collection
240
+ end
241
+
242
+ def _register_observer
243
+ observer = React::State.current_observer
244
+ if observer
245
+ React::State.get_state(observer, @state_key)
246
+ @observers << observer # @observers is a set, observers get added only once
247
+ end
248
+ end
249
+
250
+ def _unregister_collection(collection)
251
+ @registered_collections.delete(collection)
252
+ end
253
+
254
+ def _update_record(data)
255
+ if data.has_key?(:relation)
256
+ if data.has_key?(:cause)
257
+ # this creation of variables for things that could be done in one line
258
+ # are a workaround for safari, to get it updating correctly
259
+ klass_name = data[:cause][:record_type]
260
+ c_record_class = Object.const_get(klass_name)
261
+ if c_record_class._record_cache.has_key?(data[:cause][:id])
262
+ c_record = c_record_class.find(data[:cause][:id])
263
+ if `Date.parse(#{c_record.updated_at}) >= Date.parse(#{data[:cause][:updated_at]})`
264
+ if @fetch_states[data[:relation]] == 'f'
265
+ if send(data[:relation]).include?(c_record)
266
+ return
267
+ end
268
+ end
269
+ end
270
+ end
271
+ end
272
+ relation_fetch_state = @fetch_states[data[:relation]]
273
+ if relation_fetch_state == 'f'
274
+ @fetch_states[data[:relation]] = 'u'
275
+ send(data[:relation])
276
+ end
277
+ return
278
+ end
279
+ if data[:destroyed]
280
+ return if self.destroyed?
281
+ @remotely_destroyed = true
282
+ _local_destroy
283
+ return
284
+ end
285
+ if @properties_hash[:updated_at] && data[:updated_at]
286
+ return if `Date.parse(#{@properties_hash[:updated_at]}) >= Date.parse(#{data[:updated_at]})`
287
+ end
288
+ self.class._promise_get("#{resource_base_uri}/#{@properties_hash[:id]}.json").then do |response|
289
+ klass_key = self.class.to_s.underscore
290
+ self._initialize_from_hash(response.json[klass_key]) if response.json[klass_key]
291
+ _notify_observers
292
+ self
293
+ end.fail do |response|
294
+ error_message = "#{self} failed to update!"
295
+ `console.error(error_message)`
296
+ response
297
+ end
298
+ end
299
+ end
300
+ end
@@ -0,0 +1,39 @@
1
+ module HyperRecord
2
+ class Collection < Array
3
+ def initialize(array, record = nil, relation_name = nil)
4
+ @record = record
5
+ @relation_name = relation_name
6
+ if array
7
+ array.each do |record|
8
+ record._register_collection(self)
9
+ end
10
+ end
11
+ @record._notify_observers if @record
12
+ array ? super(array) : super
13
+ end
14
+
15
+ def <<(other_record)
16
+ if @record && @relation_name
17
+ @record.link_record(other_record, @relation_name)
18
+ end
19
+ other_record._register_collection(self)
20
+ @record._notify_observers if @record
21
+ super(other_record)
22
+ end
23
+
24
+ def delete(other_record)
25
+ if @record && @relation_name && !other_record.instance_variable_get(:@remotely_destroyed)
26
+ @record.unlink_record(other_record, @relation_name)
27
+ end
28
+ other_record._unregister_collection(self)
29
+ @record._notify_observers if @record
30
+ super(other_record)
31
+ end
32
+
33
+ def push(other_record)
34
+ other_record._register_collection(self)
35
+ @record._notify_observers if @record
36
+ super(other_record)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,23 @@
1
+ module HyperRecord
2
+ class DummyValue
3
+ def intialize(type_class)
4
+ @backing_value = type_class.new
5
+ end
6
+
7
+ def method_missing(name, *args)
8
+ if args
9
+ @backing_value.send(name, args)
10
+ else
11
+ @backing_value.send(name)
12
+ end
13
+ end
14
+
15
+ def acts_as_string?
16
+ true
17
+ end
18
+
19
+ def to_s
20
+ "..."
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ module HyperRecord
2
+ module ServerClassMethods
3
+ def rest_method(name, options = { default_result: '...' }, &block)
4
+ rest_methods[name] = options
5
+ rest_methods[name][:params] = block.arity
6
+ define_method(name) do |*args|
7
+ if args.size > 0
8
+ instance_exec(*args, &block)
9
+ else
10
+ instance_exec(&block)
11
+ end
12
+ end
13
+ end
14
+
15
+ def rest_methods
16
+ @rest_methods_hash ||= {}
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,109 @@
1
+ module Hyperloop
2
+ module Resource
3
+ class ClientDrivers
4
+ include React::IsomorphicHelpers
5
+
6
+ class << self
7
+ attr_reader :opts
8
+ end
9
+
10
+ if RUBY_ENGINE != 'opal'
11
+
12
+ prerender_footer do |controller|
13
+ # next if Hyperloop.transport == :none
14
+ # if defined?(PusherFake)
15
+ # path = ::Rails.application.routes.routes.detect do |route|
16
+ # route.app == Hyperloop::Engine ||
17
+ # (route.app.respond_to?(:app) && route.app.app == Hyperloop::Engine)
18
+ # end.path.spec
19
+ # pusher_fake_js = PusherFake.javascript(
20
+ # auth: { headers: { 'X-CSRF-Token' => controller.send(:form_authenticity_token) } },
21
+ # authEndpoint: "#{path}/hyperloop-pusher-auth"
22
+ # )
23
+ # end
24
+
25
+ config_hash = {
26
+ resource_transport: Hyperloop.resource_transport,
27
+ resource_api_base_path: Hyperloop.resource_api_base_path,
28
+ session_id: controller.session.id,
29
+ current_user_id: (controller.current_user.id if controller.current_user),
30
+ form_authenticity_token: controller.send(:form_authenticity_token)
31
+ }
32
+ case Hyperloop.resource_transport
33
+ when :action_cable
34
+ when :pusher
35
+ config_hash[:pusher] = {
36
+ key: Hyperloop.pusher[:key],
37
+ cluster: Hyperloop.pusher[:cluster],
38
+ encrypted: Hyperloop.pusher[:encrypted],
39
+ client_logging: Hyperloop.pusher[:client_logging] ? true : false
40
+ }
41
+ when :pusher_fake
42
+ end
43
+
44
+ "<script type='text/javascript'>\n"\
45
+ "window.HyperloopOpts = #{config_hash.to_json};\n"\
46
+ "Opal.Hyperloop.$const_get('Resource').$const_get('ClientDrivers').$initialize_client_drivers_on_boot();\n"\
47
+ "</script>\n"
48
+ end
49
+
50
+ else
51
+
52
+ def self.initialize_client_drivers_on_boot
53
+
54
+ return if @initialized
55
+
56
+ @initialized = true
57
+ @opts = {}
58
+
59
+ if on_opal_client?
60
+
61
+ @opts = Hash.new(`window.HyperloopOpts`)
62
+ @opts[:hyper_record_update_channel] = "hyper-record-update-channel-#{@opts[:session_id]}"
63
+ case @opts[:resource_transport]
64
+ when :pusher
65
+ if @opts[:pusher][:client_logging] && `window.console && window.console.log`
66
+ `Pusher.log = function(message) {window.console.log(message);}`
67
+ end
68
+
69
+ h = nil
70
+ pusher_api = nil
71
+ %x{
72
+ h = {
73
+ encrypted: #{@opts[:pusher][:encrypted]},
74
+ cluster: #{@opts[:pusher][:cluster]}
75
+
76
+ };
77
+ pusher_api = new Pusher(#{@opts[:pusher][:key]}, h)
78
+ }
79
+ @opts[:pusher][:pusher_api] = pusher_api
80
+ @opts[:pusher][:channel] = pusher_api.JS.subscribe(@opts[:hyper_record_update_channel])
81
+ @opts[:pusher][:channel].JS.bind('update', `function(data){
82
+ return Opal.Hyperloop.$const_get('Resource').$const_get('ClientDrivers').$process_notification(Opal.Hash.$new(data));
83
+ }`)
84
+
85
+ when opts[:resource_transport] == :action_cable
86
+ opts[:action_cable_consumer] =
87
+ `ActionCable.createConsumer.apply(ActionCable, #{[*opts[:action_cable_consumer_url]]})`
88
+ Hyperloop.connect(*opts[:auto_connect])
89
+ end
90
+ end
91
+ end
92
+
93
+ def self.process_notification(data)
94
+ record_class = Object.const_get(data[:record_type])
95
+ if data[:scope]
96
+ scope_fetch_state = record_class._class_fetch_states[data[:scope]]
97
+ if scope_fetch_state == 'f'
98
+ record_class._class_fetch_states[data[:scope]] = 'u'
99
+ record_class.send(data[:scope])
100
+ end
101
+ elsif record_class.record_cached?(data[:id])
102
+ record = record_class.find(data[:id])
103
+ record._update_record(data)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end