bowser 0.4.3 → 0.5.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.
data/opal/bowser/http.rb CHANGED
@@ -8,6 +8,13 @@ module Bowser
8
8
  module HTTP
9
9
  module_function
10
10
 
11
+ # Send a request to the given URL
12
+ #
13
+ # @param url [String] the URL to send the request to
14
+ # @keywordparam method [String] the HTTP method, defaults to GET
15
+ # @keywordparam headers [Hash] the HTTP headers to send with the request
16
+ # @keywordparam data [Hash, String, nil] the data to send with the request,
17
+ # only useful for POST, PATCH, or PUT requests
11
18
  def fetch(url, method: :get, headers: {}, data: nil, &block)
12
19
  promise = Promise.new
13
20
  request = Request.new(method, url)
@@ -20,6 +27,13 @@ module Bowser
20
27
  promise
21
28
  end
22
29
 
30
+ # Shorthand method for sending a request with JSON data
31
+ #
32
+ # @param url [String] the URL to send the request to
33
+ # @param data [Hash, String, nil] the data to send with the request,
34
+ # only useful for POST, PATCH, or PUT requests. Otherwise, use `fetch`.
35
+ # @keywordparam method [String] the HTTP method, defaults to GET
36
+ # @keywordparam content_type [String] the MIME type of the request
23
37
  def upload(url, data, content_type: 'application/json', method: :post, &block)
24
38
  promise = Promise.new
25
39
  request = Request.new(method, url)
@@ -32,6 +46,15 @@ module Bowser
32
46
  promise
33
47
  end
34
48
 
49
+ # Upload files from a file input field
50
+ #
51
+ # @param url [String] the URL to send the request to
52
+ # @param files [Bowser::FileList] the files to attach
53
+ # @keywordparam key [String] the name to use for the POST param, defaults to `"files"`
54
+ # @keywordparam key_suffix [String] the suffix for the key, defaults to
55
+ # `"[]"`. This is how several web frameworks determine that the key should
56
+ # be treated as an array.
57
+ # @keywordparam method [String] the HTTP method to use, defaults to `"POST"`
35
58
  def upload_files(url, files, key: 'files', key_suffix: '[]', method: :post, &block)
36
59
  promise = Promise.new
37
60
  request = Request.new(method, url)
@@ -49,10 +72,17 @@ module Bowser
49
72
  promise
50
73
  end
51
74
 
75
+ # Upload a single file from a file input field
76
+ #
77
+ # @param url [String] the URL to send the request to
78
+ # @param file [Bowser::File] the file to attach
79
+ # @keywordparam key [String] the name to use for the POST param, defaults to `"files"`
80
+ # @keywordparam method [String] the HTTP method to use, defaults to `"POST"`
52
81
  def upload_file(url, file, key: 'file', method: :post, &block)
53
82
  upload_files(url, [file], key: key, key_suffix: nil, method: method, &block)
54
83
  end
55
84
 
85
+ # @api private
56
86
  def connect_events_to_promise(request, promise)
57
87
  request.on :load do
58
88
  promise.resolve request.response
@@ -1,6 +1,8 @@
1
1
  module Bowser
2
2
  module HTTP
3
+ # Data to be attached to a form or sent in with a Bowser::HTTP::Request
3
4
  class FormData
5
+ # @param attributes [Hash, nil] the attributes to attach
4
6
  def initialize attributes={}
5
7
  @native = `new FormData()`
6
8
  attributes.each do |key, value|
@@ -14,6 +16,8 @@ module Bowser
14
16
  end
15
17
  end
16
18
 
19
+ # @param key [String] the name of the attribute
20
+ # @param value [String] the value of the attribute
17
21
  def append key, value
18
22
  data = if `!!value['native']`
19
23
  `value['native']`
@@ -3,6 +3,7 @@ require 'bowser/event_target'
3
3
 
4
4
  module Bowser
5
5
  module HTTP
6
+ # A Ruby object representing an HTTP request to a remote server.
6
7
  class Request
7
8
  include EventTarget
8
9
 
@@ -15,6 +16,8 @@ module Bowser
15
16
  LOADING = 3
16
17
  DONE = 4
17
18
 
19
+ # @param method [String] the HTTP method to use
20
+ # @param url [String] the URL to send the request to
18
21
  def initialize(method, url, native: `new XMLHttpRequest()`)
19
22
  @native = native
20
23
  @method = method
@@ -22,6 +25,11 @@ module Bowser
22
25
  @response = Response.new(@native)
23
26
  end
24
27
 
28
+ # Send the HTTP request
29
+ #
30
+ # @keywordparam data [Hash, Bowser::HTTP::FormData, nil] the data to send
31
+ # with the request.
32
+ # @keywordparam headers [Hash] the HTTP headers to attach to the request
25
33
  def send(data: {}, headers: {})
26
34
  `#@native.open(#{method}, #{url})`
27
35
  @data = data
@@ -40,6 +48,10 @@ module Bowser
40
48
  self
41
49
  end
42
50
 
51
+ # Set the headers to the keys and values of the specified hash
52
+ #
53
+ # @param headers [Hash] the hash whose keys and values should be added as
54
+ # request headers
43
55
  def headers= headers
44
56
  @headers = headers
45
57
  headers.each do |attr, value|
@@ -47,30 +59,37 @@ module Bowser
47
59
  end
48
60
  end
49
61
 
62
+ # @return [Boolean] true if this request is a POST request, false otherwise
50
63
  def post?
51
64
  method == :post
52
65
  end
53
66
 
67
+ # @return [Boolean] true if this request is a GET request, false otherwise
54
68
  def get?
55
69
  method == :get
56
70
  end
57
71
 
72
+ # @return [Numeric] the numeric readyState of the underlying XMLHttpRequest
58
73
  def ready_state
59
74
  `#@native.readyState`
60
75
  end
61
76
 
77
+ # @return [Boolean] true if this request has been sent, false otherwise
62
78
  def sent?
63
79
  ready_state >= OPENED
64
80
  end
65
81
 
82
+ # @return [Boolean] true if we have received response headers, false otherwise
66
83
  def headers_received?
67
84
  ready_state >= HEADERS_RECEIVED
68
85
  end
69
86
 
87
+ # @return [Boolean] true if we are currently downloading the response body, false otherwise
70
88
  def loading?
71
89
  ready_state == LOADING
72
90
  end
73
91
 
92
+ # @return [Boolean] true if the response has been completed, false otherwise
74
93
  def done?
75
94
  ready_state >= DONE
76
95
  end
@@ -2,27 +2,35 @@ require 'json'
2
2
 
3
3
  module Bowser
4
4
  module HTTP
5
+ # Ruby class representing an HTTP response from a remote server
5
6
  class Response
7
+ # @param xhr [JS] a native XMLHttpRequest object
6
8
  def initialize xhr
7
9
  @xhr = xhr
8
10
  end
9
11
 
12
+ # @return [Numeric] the HTTP status code of the response
10
13
  def code
11
14
  `#@xhr.status`
12
15
  end
13
16
 
17
+ # @return [String] the body of the response as a string
14
18
  def body
15
19
  `#@xhr.response`
16
20
  end
17
21
 
22
+ # @return [Hash, Array, String] the response body deserialized from JSON
18
23
  def json
19
24
  @json ||= JSON.parse(body) if `#{body} !== undefined`
20
25
  end
21
26
 
27
+ # @return [Boolean] true if this was a successful response (2xx-3xx), false otherwise
22
28
  def success?
23
29
  (200...400).cover? code
24
30
  end
31
+ alias ok? success?
25
32
 
33
+ # @return [Boolean] true if this represents a failed response (4xx-5xx)
26
34
  def fail?
27
35
  !success?
28
36
  end
@@ -0,0 +1,244 @@
1
+ require 'bowser/event_target'
2
+ require 'promise'
3
+
4
+ module Bowser
5
+ class IndexedDB
6
+ name = %w(
7
+ indexedDB
8
+ mozIndexedDB
9
+ webkitIndexedDB
10
+ msIndexedDB
11
+ ).find { |name| `!!window[#{name}]` }
12
+ NATIVE = `window[#{name}]`
13
+
14
+ def initialize name, version: 1
15
+ @thens = []
16
+
17
+ request = Request.new(`#{NATIVE}.open(#{name}, #{version})`)
18
+ request.on :error do |event|
19
+ `console.error(event)`
20
+ end
21
+ request.on :success do |event|
22
+ @native = event.target.result
23
+
24
+ @thens.each(&:call)
25
+ @thens.clear
26
+ end
27
+
28
+ request.on :upgradeneeded do |event|
29
+ puts 'upgradeneeded'
30
+ @native = event.target.result
31
+
32
+ if block_given?
33
+ yield self
34
+ puts 'upgraded'
35
+ else
36
+ raise ArgumentError, "You must provide a block to `#{self.class}.new` in order to set up the database if the user's browser does not have it."
37
+ end
38
+ end
39
+ end
40
+
41
+ def create_object_store name, key_path: `undefined`, unique: false
42
+ ObjectStore.new(`#@native.createObjectStore(#{name}, { keyPath: #{key_path} })`)
43
+ end
44
+
45
+ def delete_object_store name
46
+ `#@native.deleteObjectStore(#{name})`
47
+ rescue `TypeError` => e
48
+ # If the object store doesn't exist, we do nothing. The store not existing
49
+ # is the end state we want after this method call anyway.
50
+ end
51
+
52
+ def transaction name, mode=:readonly
53
+ Transaction.new(`#@native.transaction(#{name}, #{mode})`)
54
+ end
55
+
56
+ def then &block
57
+ if @native
58
+ block.call
59
+ else
60
+ @thens << block
61
+ end
62
+ end
63
+
64
+ def deserialize klass, native
65
+ `Object.assign(#{klass.allocate}, #{native})`
66
+ end
67
+
68
+ class ObjectStore
69
+ def initialize native
70
+ @native = native
71
+ end
72
+
73
+ def create_index name, key_path=name, unique: false
74
+ `#@native.createIndex(#{name}, #{key_path}, { unique: #{unique} })`
75
+ end
76
+
77
+ def add object
78
+ `#@native.add(#{object})`
79
+ end
80
+
81
+ def put object
82
+ `#@native.put(#{object})`
83
+ end
84
+
85
+ def delete key
86
+ p = Promise.new
87
+ key = yield Query.new if block_given?
88
+
89
+ request = Request.new(`#@native.delete(#{key})`)
90
+ request.on(:success) { p.resolve }
91
+ request.on(:error) { |error| p.reject error }
92
+
93
+ p
94
+ end
95
+
96
+ def get key
97
+ p = Promise.new
98
+ key = yield Query.new if block_given?
99
+
100
+ request = Request.new(`#@native.get(#{key})`)
101
+ request.on :success do |event|
102
+ js_obj = `#{event.target}.result`
103
+ if `!!#{js_obj}`
104
+ p.resolve `Object.assign(#{klass.allocate}, #{js_obj})`
105
+ else
106
+ p.resolve nil
107
+ end
108
+ end
109
+ request.on(:error) { |event| p.reject event.target.result }
110
+
111
+ p
112
+ end
113
+
114
+ def get_all klass, count: `undefined`
115
+ p = Promise.new
116
+ query = block_given? ? yield(Query.new) : `undefined`
117
+
118
+ request = Request.new(`#@native.getAll(#{query}, #{count})`)
119
+ request.on :success do |event|
120
+ p.resolve event.target.result.map { |js_obj|
121
+ `delete #{js_obj}.$$id` # Remove old Ruby runtime metadata
122
+ `Object.assign(#{klass.allocate}, #{js_obj})`
123
+ }
124
+ end
125
+ request.on :error do |event|
126
+ p.reject event.target.result
127
+ end
128
+
129
+ p
130
+ end
131
+
132
+ def index name
133
+ Index.new(`#@native.index(#{name})`)
134
+ end
135
+ end
136
+
137
+ class Index
138
+ def initialize native
139
+ @native = native
140
+ end
141
+
142
+ def get klass, key
143
+ p = Promise.new
144
+ key = yield Query.new if block_given?
145
+
146
+ request = Request.new(`#@native.get(#{key})`)
147
+ request.on :success do |event|
148
+ begin
149
+ p.resolve `Object.assign(#{klass.allocate}, #{event.target.result})`
150
+ rescue NoMethodError # Happens when there is no result above :-\
151
+ p.resolve nil
152
+ end
153
+ end
154
+
155
+ request.on :error do |event|
156
+ p.reject event.target.result
157
+ end
158
+
159
+ p
160
+ end
161
+
162
+ def get_all klass, count: `undefined`
163
+ p = Promise.new
164
+
165
+ query = if block_given?
166
+ yield Query.new
167
+ else
168
+ `undefined`
169
+ end
170
+
171
+ request = Request.new(`#@native.getAll(#{query}, #{count})`)
172
+ request.on :success do |event|
173
+ p.resolve event.target.result.map { |js_obj|
174
+ `delete #{js_obj}.$$id` # Remove old Ruby runtime metadata
175
+ `Object.assign(#{klass.allocate}, #{js_obj})`
176
+ }
177
+ end
178
+ request.on :error do |event|
179
+ p.reject event.target.result
180
+ end
181
+
182
+ p
183
+ end
184
+ end
185
+
186
+ class Query
187
+ name = %w(
188
+ IDBKeyRange
189
+ msIDBKeyRange
190
+ mozIDBKeyRange
191
+ webkitIDBKeyRange
192
+ ).find { |name| `!!window[#{name}]` }
193
+ NATIVE = `window[#{name}]`
194
+
195
+ def > value
196
+ lower_bound value, true
197
+ end
198
+
199
+ def >= value
200
+ lower_bound value
201
+ end
202
+
203
+ def < value
204
+ upper_bound value, true
205
+ end
206
+
207
+ def <= value
208
+ upper_bound value
209
+ end
210
+
211
+ def == value
212
+ `#{NATIVE}.only(#{value})`
213
+ end
214
+
215
+ def lower_bound value, exclusive=false
216
+ `#{NATIVE}.lowerBound(value, exclusive)`
217
+ end
218
+
219
+ def upper_bound value, exclusive=false
220
+ `#{NATIVE}.upperBound(value, exclusive)`
221
+ end
222
+ end
223
+
224
+ class Transaction
225
+ include Bowser::EventTarget
226
+
227
+ def initialize native
228
+ @native = native
229
+ end
230
+
231
+ def object_store name=`#@native.objectStoreNames[0]`
232
+ ObjectStore.new(`#@native.objectStore(#{name})`)
233
+ end
234
+ end
235
+
236
+ class Request
237
+ include Bowser::EventTarget
238
+
239
+ def initialize native
240
+ @native = native
241
+ end
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,43 @@
1
+ require 'promise'
2
+
3
+ module Bowser
4
+ class ServiceWorker
5
+ NotSupported = Class.new(StandardError)
6
+
7
+ def self.register path, options={}
8
+ p = Promise.new
9
+
10
+ if supported?
11
+ %x{
12
+ navigator.serviceWorker.register(#{path}, #{options.to_n})
13
+ .then(#{proc { |reg| p.resolve new(reg) }})
14
+ .catch(#{proc { |error| p.fail error}});
15
+ }
16
+ else
17
+ p.reject NotSupported.new('Service worker is not supported in this browser')
18
+ end
19
+
20
+ p
21
+ end
22
+
23
+ def self.ready &block
24
+ `navigator.serviceWorker.ready.then(#{proc { |sw| block.call new(sw) }})`
25
+ end
26
+
27
+ def self.supported?
28
+ `'serviceWorker' in navigator`
29
+ end
30
+
31
+ def initialize native
32
+ @native = native
33
+ end
34
+
35
+ def scope
36
+ `#@native.scope`
37
+ end
38
+
39
+ def to_n
40
+ @native
41
+ end
42
+ end
43
+ end