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,15 @@
1
+ if RUBY_ENGINE == 'opal'
2
+ module Hyperloop
3
+ def self.current_user_id
4
+ Hyperloop::Resource::ClientDrivers.opts[:current_user_id]
5
+ end
6
+ end
7
+ else
8
+ module Hyperloop
9
+ define_setting(:pusher, {})
10
+ define_setting(:redis_instance, nil)
11
+ define_setting(:resource_api_base_path, '/api')
12
+ define_setting(:resource_transport, :pusher)
13
+ define_setting(:valid_record_class_params, [])
14
+ end
15
+ end
@@ -0,0 +1,310 @@
1
+ module Hyperloop
2
+ module Resource
3
+ # {HTTP} is used to perform a `XMLHttpRequest` in ruby. It is a simple wrapper
4
+ # around `XMLHttpRequest`
5
+ #
6
+ # # Making requests
7
+ #
8
+ # To create a simple request, {HTTP} exposes class level methods to specify
9
+ # the HTTP action you wish to perform. Each action accepts the url for the
10
+ # request, as well as optional arguments passed as a hash:
11
+ #
12
+ # HTTP.get("/users/1.json")
13
+ # HTTP.post("/users", payload: data)
14
+ #
15
+ # The supported `HTTP` actions are:
16
+ #
17
+ # * {HTTP.get}
18
+ # * {HTTP.post}
19
+ # * {HTTP.put}
20
+ # * {HTTP.delete}
21
+ # * {HTTP.patch}
22
+ # * {HTTP.head}
23
+ #
24
+ # # Handling responses
25
+ #
26
+ # Responses can be handled using either a simple block callback, or using a
27
+ # {Promise} returned by the request.
28
+ #
29
+ # ## Using a block
30
+ #
31
+ # All HTTP action methods accept a block which can be used as a simple
32
+ # handler for the request. The block will be called for both successful as well
33
+ # as unsuccessful requests.
34
+ #
35
+ # HTTP.get("/users/1") do |request|
36
+ # puts "the request has completed!"
37
+ # end
38
+ #
39
+ # This `request` object will simply be the instance of the {HTTP} class which
40
+ # wraps the native `XMLHttpRequest`. {HTTP#ok?} can be used to quickly determine
41
+ # if the request was successful.
42
+ #
43
+ # HTTP.get("/users/1") do |request|
44
+ # if request.ok?
45
+ # puts "request was success"
46
+ # else
47
+ # puts "something went wrong with request"
48
+ # end
49
+ # end
50
+ #
51
+ # The {HTTP} instance will always be the only object passed to the block.
52
+ #
53
+ # ## Using a Promise
54
+ #
55
+ # If no block is given to one of the action methods, then a {Promise} is
56
+ # returned instead. See the standard library for more information on Promises.
57
+ #
58
+ # HTTP.get("/users/1").then do |req|
59
+ # puts "response ok!"
60
+ # end.fail do |req|
61
+ # puts "response was not ok"
62
+ # end
63
+ #
64
+ # When using a {Promise}, both success and failure handlers will be passed the
65
+ # {HTTP} instance.
66
+ #
67
+ # # Accessing Response Data
68
+ #
69
+ # All data returned from an HTTP request can be accessed via the {HTTP} object
70
+ # passed into the block or promise handlers.
71
+ #
72
+ # - {#ok?} - returns `true` or `false`, if request was a success (or not).
73
+ # - {#body} - returns the raw text response of the request
74
+ # - {#status_code} - returns the raw {HTTP} status code as integer
75
+ # - {#json} - tries to convert the body response into a JSON object
76
+ class HTTP
77
+ # All valid {HTTP} action methods this class accepts.
78
+ #
79
+ # @see HTTP.get
80
+ # @see HTTP.post
81
+ # @see HTTP.put
82
+ # @see HTTP.delete
83
+ # @see HTTP.patch
84
+ # @see HTTP.head
85
+ ACTIONS = %w[get post put delete patch head]
86
+
87
+ # @!method self.get(url, options = {}, &block)
88
+ #
89
+ # Create a {HTTP} `get` request.
90
+ #
91
+ # @example
92
+ # HTTP.get("/foo") do |req|
93
+ # puts "got data: #{req.data}"
94
+ # end
95
+ #
96
+ # @param url [String] url for request
97
+ # @param options [Hash] any request options
98
+ # @yield [self] optional block to handle response
99
+ # @return [Promise, nil] optionally returns a promise
100
+
101
+ # @!method self.post(url, options = {}, &block)
102
+ #
103
+ # Create a {HTTP} `post` request. Post data can be supplied using the
104
+ # `payload` options. Usually this will be a hash which will get serialized
105
+ # into a native javascript object.
106
+ #
107
+ # @example
108
+ # HTTP.post("/bar", payload: data) do |req|
109
+ # puts "got response"
110
+ # end
111
+ #
112
+ # @param url [String] url for request
113
+ # @param options [Hash] optional request options
114
+ # @yield [self] optional block to yield for response
115
+ # @return [Promise, nil] returns a {Promise} unless block given
116
+
117
+ # @!method self.put(url, options = {}, &block)
118
+
119
+ # @!method self.delete(url, options = {}, &block)
120
+
121
+ # @!method self.patch(url, options = {}, &block)
122
+
123
+ # @!method self.head(url, options = {}, &block)
124
+
125
+ ACTIONS.each do |action|
126
+ define_singleton_method(action) do |url, options = {}, &block|
127
+ new.send(action, url, options, block)
128
+ end
129
+
130
+ define_method(action) do |url, options = {}, &block|
131
+ send(action, url, options, block)
132
+ end
133
+ end
134
+
135
+ attr_reader :body, :error_message, :method, :status_code, :url, :xhr
136
+
137
+ def initialize
138
+ @ok = true
139
+ end
140
+
141
+ def self.active?
142
+ jquery_active_requests = 0
143
+ %x{
144
+ if (typeof jQuery !== "undefined" && typeof jQuery.active !== "undefined" && jQuery.active !== null) {
145
+ jquery_active_requests = jQuery.active;
146
+ }
147
+ }
148
+ (jquery_active_requests + @active_requests) > 0
149
+ end
150
+
151
+ def self.active_requests
152
+ @active_requests ||= 0
153
+ @active_requests
154
+ end
155
+
156
+ def self.incr_active_requests
157
+ @active_requests ||= 0
158
+ @active_requests += 1
159
+ end
160
+
161
+ def self.decr_active_requests
162
+ @active_requests ||= 0
163
+ @active_requests -= 1
164
+ if @active_requests < 0
165
+ `console.log("Ooops, Hyperloop::HTTP active_requests out of sync!")`
166
+ @active_requests = 0
167
+ end
168
+ end
169
+
170
+ def send(method, url, options, block)
171
+ @method = method
172
+ @url = url
173
+ @payload = options.delete :payload
174
+ @handler = block
175
+ %x{
176
+ var payload_to_send = null;
177
+ var content_type = null;
178
+ if (typeof(this.payload) === 'string') {
179
+ payload_to_send = this.payload;
180
+ }
181
+ else if (this.payload != nil) {
182
+ payload_to_send = this.payload.$to_json();
183
+ content_type = 'application/json';
184
+ }
185
+
186
+ var xhr = new XMLHttpRequest();
187
+
188
+ xhr.onreadystatechange = function() {
189
+ if(xhr.readyState === XMLHttpRequest.DONE) {
190
+ self.$class().$decr_active_requests();
191
+ if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
192
+ return #{succeed(`xhr.responseText`, `xhr.status`, `xhr`)};
193
+ } else {
194
+ return #{fail(`xhr`, `xhr.status`, `xhr.statusText`)};
195
+ }
196
+ }
197
+ }
198
+ xhr.open(this.method.toUpperCase(), this.url);
199
+ if (payload_to_send !== null && content_type !== null) {
200
+ xhr.setRequestHeader("Content-Type", content_type);
201
+ }
202
+ if (options["$has_key?"]("headers")) {
203
+ var headers = options['$[]']("headers");
204
+ var keys = headers.$keys();
205
+ var keys_length = keys.length;
206
+ for (var i=0; i < keys_length; i++) {
207
+ xhr.setRequestHeader( keys[i], headers['$[]'](keys[i]) );
208
+ }
209
+ }
210
+ if (payload_to_send !== null) {
211
+ self.$class().$incr_active_requests();
212
+ xhr.send(payload_to_send);
213
+ } else {
214
+ self.$class().$incr_active_requests();
215
+ xhr.send();
216
+ }
217
+ }
218
+
219
+ @handler ? self : promise
220
+ end
221
+
222
+ # Parses the http response body through json. If the response is not
223
+ # valid JSON then an error will very likely be thrown.
224
+ #
225
+ # @example Getting JSON content
226
+ # HTTP.get("api.json") do |response|
227
+ # puts response.json
228
+ # end
229
+ #
230
+ # # => {"key" => 1, "bar" => 2, ... }
231
+ #
232
+ # @return [Hash, Array] returns the parsed json
233
+ def json
234
+ @json ||= JSON.parse(@body)
235
+ end
236
+
237
+ # Returns true if the request succeeded, false otherwise.
238
+ #
239
+ # @example
240
+ # HTTP.get("/some/url") do |response|
241
+ # if response.ok?
242
+ # alert "Yay!"
243
+ # else
244
+ # alert "Aww :("
245
+ # end
246
+ #
247
+ # @return [true, false] true if request was successful
248
+ def ok?
249
+ @ok
250
+ end
251
+
252
+ # Returns the value of the specified response header.
253
+ #
254
+ # @param key [String] name of the header to get
255
+ # @return [String] value of the header
256
+ # @return [nil] if the header +key+ was not in the response
257
+ def get_header(key)
258
+ %x{
259
+ var value = #@xhr.getResponseHeader(#{key});
260
+ return (value === null) ? nil : value;
261
+ }
262
+ end
263
+
264
+ def inspect
265
+ "#<HTTP @url=#{@url} @method=#{@method}>"
266
+ end
267
+
268
+ private
269
+
270
+ def promise
271
+ return @promise if @promise
272
+
273
+ @promise = Promise.new.tap { |promise|
274
+ @handler = proc { |res|
275
+ if res.ok?
276
+ promise.resolve res
277
+ else
278
+ promise.reject res
279
+ end
280
+ }
281
+ }
282
+ end
283
+
284
+ def succeed(data, status, xhr)
285
+ %x{
286
+ #@body = data;
287
+ #@xhr = xhr;
288
+ #@status_code = xhr.status;
289
+
290
+ if (typeof(data) === 'object') {
291
+ #@json = #{ JSON.from_object `data` };
292
+ }
293
+ }
294
+
295
+ @handler.call self if @handler
296
+ end
297
+
298
+ def fail(xhr, status, error)
299
+ %x{
300
+ #@body = xhr.responseText;
301
+ #@xhr = xhr;
302
+ #@status_code = xhr.status;
303
+ }
304
+
305
+ @ok = false
306
+ @handler.call self if @handler
307
+ end
308
+ end
309
+ end
310
+ end
@@ -0,0 +1,146 @@
1
+ module Hyperloop
2
+ module Resource
3
+ module PubSub
4
+ def self.included(base)
5
+ base.extend(Hyperloop::Resource::PubSub::ClassMethods)
6
+ end
7
+
8
+ module ClassMethods
9
+ def _pusher_client
10
+ @pusher_client ||= Pusher::Client.new(
11
+ app_id: Hyperloop.pusher[:app_id],
12
+ key: Hyperloop.pusher[:key],
13
+ secret: Hyperloop.pusher[:secret],
14
+ cluster: Hyperloop.pusher[:cluster]
15
+ )
16
+ end
17
+ end
18
+
19
+ def publish_relation(base_record, relation_name, record = nil)
20
+ subscribers = Hyperloop.redis_instance.hgetall("HRPS__#{base_record.class}__#{base_record.id}__#{relation_name}")
21
+ time_now = Time.now.to_f
22
+ scrub_time = time_now - 24.hours.to_f
23
+ message = {
24
+ record_type: base_record.class.to_s,
25
+ id: base_record.id,
26
+ updated_at: base_record.updated_at,
27
+ relation: relation_name
28
+ }
29
+ if record
30
+ message[:cause] = {}
31
+ message[:cause][:record_type] = record.class.to_s
32
+ message[:cause][:id] = record.id
33
+ message[:cause][:updated_at] = record.updated_at
34
+ end
35
+ subscribers.each do |session_id, last_requested|
36
+ if last_requested.to_f < scrub_time
37
+ Hyperloop.redis_instance.hdel("HRPS__#{base_record.class}__#{base_record.id}__#{relation_name}", session_id)
38
+ next
39
+ end
40
+ if Hyperloop.resource_transport == :pusher
41
+ self.class._pusher_client.trigger("hyper-record-update-channel-#{session_id}", 'update', message)
42
+ end
43
+ end
44
+ end
45
+
46
+ def publish_record(record)
47
+ subscribers = Hyperloop.redis_instance.hgetall("HRPS__#{record.class}__#{record.id}")
48
+ time_now = Time.now.to_f
49
+ scrub_time = time_now - 24.hours.to_f
50
+
51
+ message = {
52
+ record_type: record.class.to_s,
53
+ id: record.id,
54
+ updated_at: record.updated_at
55
+ }
56
+ message[:destroyed] = true if record.destroyed?
57
+
58
+ subscribers.each_slice(50) do |slice|
59
+ channel_array= []
60
+ slice.each do |session_id, last_requested|
61
+ if last_requested.to_f < scrub_time
62
+ Hyperloop.redis_instance.hdel("HRPS__#{record.class}__#{record.id}", session_id)
63
+ next
64
+ end
65
+ channel_array << "hyper-record-update-channel-#{session_id}"
66
+ end
67
+ if Hyperloop.resource_transport == :pusher && channel_array.size > 0
68
+ self.class._pusher_client.trigger(channel_array, 'update', message)
69
+ end
70
+ end
71
+ Hyperloop.redis_instance.del("HRPS__#{record.class}__#{record.id}") if record.destroyed?
72
+ end
73
+
74
+ def publish_scope(record_class, scope_name)
75
+ subscribers = Hyperloop.redis_instance.hgetall("HRPS__#{record_class}__scope__#{scope_name}")
76
+ time_now = Time.now.to_f
77
+ scrub_time = time_now - 24.hours.to_f
78
+ subscribers.each do |session_id, last_requested|
79
+ if last_requested.to_f < scrub_time
80
+ Hyperloop.redis_instance.hdel("HRPS__#{record_class}__scope__#{scope_name}", session_id)
81
+ next
82
+ end
83
+ message = {
84
+ record_type: record_class.to_s,
85
+ scope: scope_name
86
+ }
87
+ if Hyperloop.resource_transport == :pusher
88
+ self.class._pusher_client.trigger("hyper-record-update-channel-#{session_id}", 'update', message)
89
+ end
90
+ end
91
+ end
92
+
93
+ def subscribe_relation(relation, base_record = nil, relation_name = nil)
94
+ return unless session.id
95
+ time_now = Time.now.to_f.to_s
96
+ session_id = session.id.to_s
97
+ Hyperloop.redis_instance.pipelined do
98
+ if relation.is_a?(Enumerable)
99
+ # has_many
100
+ relation.each do |record|
101
+ Hyperloop.redis_instance.hset("HRPS__#{record.class}__#{record.id}", session_id, time_now)
102
+ end
103
+ elsif !relation.nil?
104
+ # has_one, belongs_to
105
+ Hyperloop.redis_instance.hset("HRPS__#{relation.class}__#{relation.id}", session_id, time_now)
106
+ end
107
+ Hyperloop.redis_instance.hset("HRPS__#{base_record.class}__#{base_record.id}__#{relation_name}", session_id, time_now) if base_record && relation_name
108
+ end
109
+ end
110
+
111
+ def subscribe_record(record)
112
+ return unless session.id
113
+ Hyperloop.redis_instance.hset "HRPS__#{record.class}__#{record.id}", session.id.to_s, Time.now.to_f.to_s
114
+ end
115
+
116
+ def subscribe_scope(collection, record_class = nil, scope_name = nil)
117
+ return unless session.id
118
+ time_now = Time.now.to_f.to_s
119
+ session_id = session.id.to_s
120
+ Hyperloop.redis_instance.pipelined do
121
+ if collection.is_a?(Enumerable)
122
+ collection.each do |record|
123
+ Hyperloop.redis_instance.hset("HRPS__#{record.class}__#{record.id}", session_id, time_now)
124
+ end
125
+ end
126
+ Hyperloop.redis_instance.hset("HRPS__#{record_class}__scope__#{scope_name}", session_id, time_now) if record_class && scope_name
127
+ end
128
+ end
129
+
130
+ def pub_sub_relation(relation, base_record, relation_name, causing_record = nil)
131
+ subscribe_relation(relation, base_record, relation_name)
132
+ publish_relation(base_record, relation_name, causing_record)
133
+ end
134
+
135
+ def pub_sub_record(record)
136
+ subscribe_record(record)
137
+ publish_record(record)
138
+ end
139
+
140
+ def pub_sub_scope(collection, record_class, scope_name)
141
+ subscribe_scope(collection, record_class, scope_name)
142
+ publish_scope(record_class, scope_name)
143
+ end
144
+ end
145
+ end
146
+ end