├── README.md ├── src ├── index.js ├── helpers.js ├── try.js └── source.js ├── tests └── index.js ├── .gitignore ├── package.json └── LICENSE /README.md: -------------------------------------------------------------------------------- 1 | # mostly-http 2 | > HTTP requests powered by Most.js 3 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const { Stream } = require('most') 2 | const HttpEvent = require('./source') 3 | 4 | module.exports.client = (url, opts = {}) => new Stream(new HttpEvent(url, opts)) 5 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | const keys = obj => Object.keys(obj) 2 | 3 | module.exports.dissoc = (str, obj) => 4 | keys(obj).reduce((acc, k) => k !== str ? (acc[k] = obj[k], acc) : acc, {}) 5 | -------------------------------------------------------------------------------- /src/try.js: -------------------------------------------------------------------------------- 1 | module.exports.tryEvent = function tryEvent(time, value, sink) { 2 | try { 3 | sink.event(time, value) 4 | } catch (err) { 5 | sink.error(time, err) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | import { describe, given, it, equals, assert } from '45' 2 | import { client } from '../src/index.js' 3 | 4 | const url = 'https://httpbin.org/get' 5 | 6 | export const test = describe('http client', [ 7 | given('given a url', [ 8 | it('can GET it', () => { 9 | return client(url) 10 | .map(resp => resp.json()) 11 | .map(obj => equals(url, obj.url)) 12 | }) 13 | ]) 14 | ]) 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | *.swp 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mostly/http", 3 | "version": "1.0.0", 4 | "description": "HTTP requests powered by Most.js", 5 | "main": "src/index.js", 6 | "dependencies": { 7 | "most": "^1.2.2" 8 | }, 9 | "devDependencies": { 10 | "45": "^1.7.0" 11 | }, 12 | "scripts": { 13 | "test": "45" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+ssh://git@github.com/davidchase/mostly-http.git" 18 | }, 19 | "keywords": [ 20 | "most", 21 | "http", 22 | "request", 23 | "https", 24 | "fetch", 25 | "get" 26 | ], 27 | "author": "David Chase ", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/davidchase/mostly-http/issues" 31 | }, 32 | "homepage": "https://github.com/davidchase/mostly-http#readme" 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 David Chase 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/source.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const https = require('https') 3 | const { parse } = require('url') 4 | const { dissoc } = require('./helpers') 5 | const { tryEvent } = require('./try') 6 | 7 | module.exports = class HttpEvent { 8 | constructor(url, opts) { 9 | this.url = url 10 | this.opts = opts 11 | } 12 | 13 | _sendResponse(res, chunks, time, sink) { 14 | const { statusCode, statusMessage, headers } = res 15 | const buffer = Buffer.concat(chunks) 16 | tryEvent(time, { 17 | statusCode, 18 | statusMessage, 19 | headers, 20 | ok: (statusCode / 200 | 0) === 1, 21 | buffer, 22 | text: () => buffer.toString(), 23 | json: () => JSON.parse(buffer.toString()) 24 | }, sink) 25 | sink.end(time) 26 | } 27 | 28 | _requestHandler(sink, time, scheduler) { 29 | const self = this 30 | const chunks = [] 31 | return res => { 32 | if (res.headers.location && res.statusCode >= 300 && res.statusCode < 400) { 33 | self.url = res.headers.location 34 | self.run(sink, scheduler) 35 | return 36 | } 37 | res.on('data', chunk => chunks.push(chunk)) 38 | res.once('end', self._sendResponse.bind(null, res, chunks, time, sink)) 39 | } 40 | } 41 | run(sink, scheduler) { 42 | const time = scheduler.now() 43 | const url = this.url 44 | const opts = this.opts 45 | const error = sink.error.bind(sink, time) 46 | 47 | const options = dissoc('body', Object.assign({}, parse(url), opts)) 48 | const body = opts.body || '' 49 | const protocol = options.protocol === 'https:' ? https : http 50 | 51 | const req = protocol.request(options, this._requestHandler(sink, time, scheduler)) 52 | req.on('error', error) 53 | req.on('timeout', () => (req.abort(), error(new Error('Request timed out')))) 54 | req.end(body) 55 | 56 | return { 57 | dispose: () => req.abort() 58 | } 59 | } 60 | } 61 | --------------------------------------------------------------------------------