|
1 | 1 | //. # Fluture Node |
2 | 2 | //. |
3 | | -//. Common Node API's wrapped to return [Fluture][] Futures. |
| 3 | +//. FP-style HTTP and streaming utils for Node based on [Fluture][]. |
| 4 | +//. |
| 5 | +//. ```console |
| 6 | +//. $ npm install fluture fluture-node |
| 7 | +//. ``` |
4 | 8 | //. |
5 | 9 | //. ## API |
6 | 10 |
|
7 | | -import Future from 'fluture/index.js'; |
| 11 | +import http from 'http'; |
| 12 | +import https from 'https'; |
| 13 | +import qs from 'querystring'; |
| 14 | +import {Readable, pipeline} from 'stream'; |
| 15 | + |
| 16 | +import { |
| 17 | + Future, |
| 18 | + attempt, |
| 19 | + chain, |
| 20 | + encase, |
| 21 | + map, |
| 22 | + mapRej, |
| 23 | + reject, |
| 24 | + resolve, |
| 25 | +} from 'fluture/index.js'; |
| 26 | + |
| 27 | +//. ### EventEmitter |
8 | 28 |
|
9 | 29 | //# once :: String -> EventEmitter -> Future Error a |
10 | 30 | //. |
@@ -38,9 +58,50 @@ export const once = event => emitter => Future ((rej, res) => { |
38 | 58 | return removeListeners; |
39 | 59 | }); |
40 | 60 |
|
41 | | -//# buffer :: ReadableStream a -> Future Error (Array a) |
| 61 | +//. ### Buffer |
| 62 | + |
| 63 | +//# encode :: Charset -> Buffer -> Future Error String |
| 64 | +//. |
| 65 | +//. Given an encoding and a [Buffer][], returns a Future of the result of |
| 66 | +//. encoding said buffer using the given encoding. The Future will reject |
| 67 | +//. with an Error if the encoding is unknown. |
| 68 | +//. |
| 69 | +//. ```js |
| 70 | +//. > encode ('utf8') (Buffer.from ('Hello world!')); |
| 71 | +//. 'Hello world!' |
| 72 | +//. ``` |
| 73 | +export const encode = encoding => buffer => ( |
| 74 | + mapRej (e => new Error (e.message)) |
| 75 | + (attempt (() => buffer.toString (encoding))) |
| 76 | +); |
| 77 | + |
| 78 | +//. ### Stream |
| 79 | + |
| 80 | +//# streamOf :: Buffer -> Future a (Readable Buffer) |
42 | 81 | //. |
43 | | -//. Buffer all data on a Stream into a Future of an Array. |
| 82 | +//. Given a [Buffer][], returns a Future of a [Readable][] stream which will |
| 83 | +//. emit the given Buffer before ending. |
| 84 | +//. |
| 85 | +//. The stream is wrapped in a Future because creation of a stream causes |
| 86 | +//. side-effects if it's not consumed in time, making it safer to pass it |
| 87 | +//. around wrapped in a Future. |
| 88 | +export const streamOf = encase (buf => new Readable ({ |
| 89 | + highWaterMark: buf.byteLength, |
| 90 | + read: function() { |
| 91 | + if (this._pushed || this.push (buf)) { this.push (null); } |
| 92 | + this._pushed = true; |
| 93 | + }, |
| 94 | +})); |
| 95 | + |
| 96 | +//# emptyStream :: Future a (Readable Buffer) |
| 97 | +//. |
| 98 | +//. A [Readable][] stream which ends after emiting zero bytes. Can be useful |
| 99 | +//. as an empty [`request`](#request) body, for example. |
| 100 | +export const emptyStream = streamOf (Buffer.alloc (0)); |
| 101 | + |
| 102 | +//# buffer :: Readable a -> Future Error (Array a) |
| 103 | +//. |
| 104 | +//. Buffer all data on a [Readable][] stream into a Future of an Array. |
44 | 105 | //. |
45 | 106 | //. When the Future is cancelled, it removes any trace of |
46 | 107 | //. itself from the Stream. |
@@ -77,6 +138,18 @@ export const buffer = stream => Future ((rej, res) => { |
77 | 138 | return removeListeners; |
78 | 139 | }); |
79 | 140 |
|
| 141 | +//# bufferString :: Charset -> Readable Buffer -> Future Error String |
| 142 | +//. |
| 143 | +//. A version of [`buffer`](#buffer) specialized in Strings. |
| 144 | +//. |
| 145 | +//. Takes a charset and a [Readable][] stream of [Buffer][]s, and returns |
| 146 | +//. a Future containing a String with the fully buffered and encoded result. |
| 147 | +export const bufferString = charset => stream => ( |
| 148 | + chain (encode (charset)) (map (Buffer.concat) (buffer (stream))) |
| 149 | +); |
| 150 | + |
| 151 | +//. ### Event Loop |
| 152 | + |
80 | 153 | //# instant :: b -> Future a b |
81 | 154 | //. |
82 | 155 | //. Resolves a Future with the given value in the next tick, |
@@ -109,6 +182,256 @@ export const immediate = x => Future ((rej, res) => { |
109 | 182 | return () => { clearImmediate (job); }; |
110 | 183 | }); |
111 | 184 |
|
112 | | -//. [Fluture]: https://github.com/fluture-js/Fluture |
| 185 | +//. ### Http |
| 186 | +//. |
| 187 | +//. The functions below are to be used in compositions such as the one shown |
| 188 | +//. below, in order to cover a wide variety of HTTP-related use cases. |
| 189 | +//. |
| 190 | +//. ```js |
| 191 | +//. import {map, chain, chainRej, encase, fork} from 'fluture/index.js'; |
| 192 | +//. import {retrieve, |
| 193 | +//. acceptStatus, |
| 194 | +//. autoBufferResponse, |
| 195 | +//. responseToError} from './index.js'; |
| 196 | +//. |
| 197 | +//. const rejectUnacceptable = res => ( |
| 198 | +//. acceptStatus (200) (res) |
| 199 | +//. .pipe (chainRej (responseToError)) |
| 200 | +//. ); |
| 201 | +//. |
| 202 | +//. retrieve ('https://api.github.com/users/Avaq') ({'User-Agent': 'Avaq'}) |
| 203 | +//. .pipe (chain (rejectUnacceptable)) |
| 204 | +//. .pipe (chain (autoBufferResponse)) |
| 205 | +//. .pipe (chain (encase (JSON.parse))) |
| 206 | +//. .pipe (map (avaq => avaq.name)) |
| 207 | +//. .pipe (fork (console.error) (console.log)); |
| 208 | +//. ``` |
| 209 | +//. |
| 210 | +//. The example above will either: |
| 211 | +//. |
| 212 | +//. 1. log `"Aldwin Vlasblom"` to the terminal if nothing weird happens; or |
| 213 | +//. 2. log an error to the console if: |
| 214 | +//. * a network error occurs; |
| 215 | +//. * the response code is not 200; or |
| 216 | +//. * the JSON is malformed. |
| 217 | +//. |
| 218 | +//. Note that we were in control of how an unexpected status was treated, |
| 219 | +//. how an erroneous response would be formatted as an error message, |
| 220 | +//. whether the response would be parsed as JSON, and how a failure of parsing |
| 221 | +//. the JSON would have been treated. |
| 222 | +//. |
| 223 | +//. The goal of the functions below us to give you as much control over HTTP |
| 224 | +//. requests as possible, while still keeping boilerplate low by leveraging |
| 225 | +//. function composition. |
| 226 | +//. |
| 227 | +//. This contrasts with many of the popular HTTP client libraries out there, |
| 228 | +//. which often make decisions for you, taking away control in an attempt to |
| 229 | +//. provide a smoother usage experience. |
| 230 | + |
| 231 | +// defaultCharset :: String |
| 232 | +const defaultCharset = 'utf8'; |
| 233 | + |
| 234 | +// defaultContentType :: String |
| 235 | +const defaultContentType = 'text/plain; charset=' + defaultCharset; |
| 236 | + |
| 237 | +// charsetRegex :: RegExp |
| 238 | +const charsetRegex = /\bcharset=([^;\s]+)/; |
| 239 | + |
| 240 | +// mimeTypes :: StrMap Mimetype |
| 241 | +const mimeTypes = { |
| 242 | + form: 'application/x-www-form-urlencoded; charset=utf8', |
| 243 | + json: 'application/json; charset=utf8', |
| 244 | +}; |
| 245 | + |
| 246 | +// getRequestModule :: String -> Future Error Module |
| 247 | +const getRequestModule = protocol => { |
| 248 | + switch (protocol) { |
| 249 | + case 'https:': return resolve (https); |
| 250 | + case 'http:': return resolve (http); |
| 251 | + default: return reject (new Error (`Unsupported protocol '${protocol}'`)); |
| 252 | + } |
| 253 | +}; |
| 254 | + |
| 255 | +//# request :: Object -> Url -> Readable Buffer -> Future Error IncomingMessage |
| 256 | +//. |
| 257 | +//. This is the "lowest level" function for making HTTP requests. It does not |
| 258 | +//. handle buffering, encoding, content negotiation, or anything really. |
| 259 | +//. For most use cases, you can use one of the more specialized functions: |
| 260 | +//. |
| 261 | +//. * [`send`](#send): Make a generic HTTP request. |
| 262 | +//. * [`retrieve`](#retrieve): Make a GET request. |
| 263 | +//. |
| 264 | +//. Given an Object of [http options][], a String containing the request URL, |
| 265 | +//. and a [Readable][] stream of [Buffer][]s to be sent as the request body, |
| 266 | +//. returns a Future which makes an HTTP request and resolves with its |
| 267 | +//. [IncomingMessage][]. If the Future is cancelled, the request is aborted. |
| 268 | +//. |
| 269 | +//. ```js |
| 270 | +//. import {attempt, chain} from 'fluture/index.js'; |
| 271 | +//. import {createReadStream} from 'fs'; |
| 272 | +//. |
| 273 | +//. const sendBinary = request ({ |
| 274 | +//. method: 'POST', |
| 275 | +//. headers: {'Transfer-Encoding': 'chunked'}, |
| 276 | +//. }); |
| 277 | +//. |
| 278 | +//. const eventualBody = attempt (() => createReadStream ('./data.bin')); |
| 279 | +//. |
| 280 | +//. eventualBody.pipe (chain (sendBinary ('https://example.com'))); |
| 281 | +//. ``` |
| 282 | +//. |
| 283 | +//. If you want to use this function to transfer a stream of data, don't forget |
| 284 | +//. to set the Transfer-Encoding header to "chunked". |
| 285 | +export const request = options => url => body => { |
| 286 | + const location = new URL (url); |
| 287 | + const makeRequest = lib => Future ((rej, res) => { |
| 288 | + const req = lib.request (location, options); |
| 289 | + req.once ('response', res); |
| 290 | + pipeline (body, req, e => e && rej (e)); |
| 291 | + return () => { |
| 292 | + req.removeListener ('response', res); |
| 293 | + req.abort (); |
| 294 | + }; |
| 295 | + }); |
| 296 | + return chain (makeRequest) (getRequestModule (location.protocol)); |
| 297 | +}; |
| 298 | + |
| 299 | +//# retrieve :: Url -> StrMap String -> Future Error IncomingMessage |
| 300 | +//. |
| 301 | +//. A version of [`request`](#request) specialized in the `GET` method. |
| 302 | +//. |
| 303 | +//. Given a URL and a StrMap of request headers, returns a Future which |
| 304 | +//. makes a GET requests to the given resource. |
| 305 | +//. |
| 306 | +//. ```js |
| 307 | +//. retrieve ('https://api.github.com/users/Avaq') ({'User-Agent': 'Avaq'}) |
| 308 | +//. ``` |
| 309 | +export const retrieve = url => headers => ( |
| 310 | + chain (request ({headers}) (url)) (emptyStream) |
| 311 | +); |
| 312 | + |
| 313 | +//# send :: Mimetype -> Method -> Url -> StrMap String -> Buffer -> Future Error IncomingMessage |
| 314 | +//. |
| 315 | +//. A version of [`request`](#request) for sending arbitrary data to a server. |
| 316 | +//. There's also more specific versions for sending common types of data: |
| 317 | +//. |
| 318 | +//. * [`sendJson`](#sendJson) sends JSON stringified data. |
| 319 | +//. * [`sendForm`](#sendForm) sends form encoded data. |
| 320 | +//. |
| 321 | +//. Given a MIME type, a request method, a URL, a StrMap of headers, and |
| 322 | +//. finally a Buffer, returns a Future which will send the Buffer to the |
| 323 | +//. server at the given URL using the given request method, telling it the |
| 324 | +//. buffer contains data of the given MIME type. |
| 325 | +//. |
| 326 | +//. This function will always send the Content-Type and Content-Length headers, |
| 327 | +//. alongside the provided headers. Manually provoding either of these headers |
| 328 | +//. override those generated by this function. |
| 329 | +export const send = mime => method => url => extraHeaders => buf => { |
| 330 | + const headers = Object.assign ({ |
| 331 | + 'Content-Type': mime, |
| 332 | + 'Content-Length': buf.byteLength, |
| 333 | + }, extraHeaders); |
| 334 | + return chain (request ({method, headers}) (url)) (streamOf (buf)); |
| 335 | +}; |
| 336 | + |
| 337 | +//# sendJson :: Method -> String -> StrMap String -> JsonValue -> Future Error IncomingMessage |
| 338 | +//. |
| 339 | +//. A version of [`send`](#send) specialized in sending JSON. |
| 340 | +//. |
| 341 | +//. Given a request method, a URL, a StrMap of headers and a JavaScript plain |
| 342 | +//. object, returns a Future which sends the object to the server at the |
| 343 | +//. given URL after JSON-encoding it. |
| 344 | +//. |
| 345 | +//. ```js |
| 346 | +//. sendJson ('PUT') |
| 347 | +//. ('https://example.com/users/bob') |
| 348 | +//. ({Authorization: 'Bearer asd123'}) |
| 349 | +//. ({name: 'Bob', email: 'bob@example.com'}); |
| 350 | +//. ``` |
| 351 | +export const sendJson = method => url => headers => json => { |
| 352 | + const buf = Buffer.from (JSON.stringify (json)); |
| 353 | + return send (mimeTypes.json) (method) (url) (headers) (buf); |
| 354 | +}; |
| 355 | + |
| 356 | +//# sendForm :: Method -> String -> StrMap String -> JsonValue -> Future Error IncomingMessage |
| 357 | +//. |
| 358 | +//. A version of [`send`](#send) specialized in sending form data. |
| 359 | +//. |
| 360 | +//. Given a request method, a URL, a StrMap of headers and a JavaScript plain |
| 361 | +//. object, returns a Future which sends the object to the server at the |
| 362 | +//. given URL after www-form-urlencoding it. |
| 363 | +//. |
| 364 | +//. ```js |
| 365 | +//. sendForm ('POST') |
| 366 | +//. ('https://example.com/users/create') |
| 367 | +//. ({}) |
| 368 | +//. ({name: 'Bob', email: 'bob@example.com'}); |
| 369 | +//. ``` |
| 370 | +export const sendForm = method => url => headers => form => { |
| 371 | + const buf = Buffer.from (qs.stringify (form)); |
| 372 | + return send (mimeTypes.form) (method) (url) (headers) (buf); |
| 373 | +}; |
| 374 | + |
| 375 | +//# acceptStatus :: Number -> IncomingMessage -> Future IncomingMessage IncomingMessage |
| 376 | +//. |
| 377 | +//. This function "tags" an [IncomingMessage][] based on a given status code. |
| 378 | +//. If the response status matches the given status code, the returned Future |
| 379 | +//. will resolve. If it doesn't, the returned Future will reject. |
| 380 | +//. |
| 381 | +//. The idea is that you can compose this function with one that returns an |
| 382 | +//. IncomingMessage, and reject any responses that don't meet the expected |
| 383 | +//. status code. In combination with [`responseToError`](#responseToError), |
| 384 | +//. you can then flatten it back into the outer Future. |
| 385 | +//. |
| 386 | +//. The usage example under the [Http](#http) section shows this. |
| 387 | +export const acceptStatus = code => res => ( |
| 388 | + code === res.statusCode ? resolve (res) : reject (res) |
| 389 | +); |
| 390 | + |
| 391 | +//# bufferResponse :: Charset -> IncomingMessage -> Future Error String |
| 392 | +//. |
| 393 | +//. A version of [`buffer`](#buffer) specialized in [IncomingMessage][]s. |
| 394 | +//. |
| 395 | +//. See also [`autoBufferResponse`](#autoBufferResponse). |
| 396 | +//. |
| 397 | +//. Given a charset and an IncomingMessage, returns a Future with the buffered, |
| 398 | +//. encoded, message body. |
| 399 | +export const bufferResponse = charset => message => ( |
| 400 | + mapRej (e => new Error ('Failed to buffer response: ' + e.message)) |
| 401 | + (bufferString (charset) (message)) |
| 402 | +); |
| 403 | + |
| 404 | +//# autoBufferResponse :: IncomingMessage -> Future Error String |
| 405 | +//. |
| 406 | +//. Given an IncomingMessage, buffers and decodes the message body using the |
| 407 | +//. charset provided in the message headers. Falls back to UTF-8 if the |
| 408 | +//. charset was not provided. |
| 409 | +//. |
| 410 | +//. Returns a Future with the buffered, encoded, message body. |
| 411 | +export const autoBufferResponse = message => { |
| 412 | + const contentType = message.headers['content-type'] || defaultContentType; |
| 413 | + const parsed = charsetRegex.exec (contentType); |
| 414 | + const charset = parsed == null ? defaultCharset : parsed[1]; |
| 415 | + return bufferResponse (charset) (message); |
| 416 | +}; |
| 417 | + |
| 418 | +//# responseToError :: IncomingMessage -> Future Error a |
| 419 | +//. |
| 420 | +//. Given a response, returns a *rejected* Future of an instance of Error |
| 421 | +//. with a message based on the content of the response. |
| 422 | +export const responseToError = message => ( |
| 423 | + autoBufferResponse (message) |
| 424 | + .pipe (chain (body => reject (new Error ( |
| 425 | + `Unexpected ${message.statusMessage} (${message.statusCode}) response. ` + |
| 426 | + `Response body:\n\n${body.split ('\n').map (x => ` ${x}`).join ('\n')}` |
| 427 | + )))) |
| 428 | +); |
| 429 | + |
113 | 430 | //. [`process.nextTick`]: https://nodejs.org/api/process.html#process_process_nexttick_callback_args |
114 | 431 | //. [`setImmediate`]: https://nodejs.org/api/timers.html#timers_setimmediate_callback_args |
| 432 | + |
| 433 | +//. [Buffer]: https://nodejs.org/api/buffer.html#buffer_class_buffer |
| 434 | +//. [Fluture]: https://github.com/fluture-js/Fluture |
| 435 | +//. [http options]: https://nodejs.org/api/http.html#http_http_request_url_options_callback |
| 436 | +//. [IncomingMessage]: https://nodejs.org/api/http.html#http_class_http_incomingmessage |
| 437 | +//. [Readable]: https://nodejs.org/api/stream.html#stream_class_stream_readable |
0 commit comments