├── .gitignore ├── LICENSE ├── README.md ├── cli.js ├── example ├── app.js ├── index.html └── worker.js ├── index.js ├── package.json └── worker.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | example/bundle.js 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Mathias Buus 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # browser-server 2 | 3 | A HTTP "server" in the browser that uses a service worker to allow you to easily send back your own stream of data. 4 | 5 | ``` 6 | npm install browser-server 7 | ``` 8 | 9 | ## Usage 10 | 11 | First generate the service worker, using the browser-server command line tool 12 | 13 | ``` 14 | npm install -g browser-server 15 | # /demo is the prefix you want to intercept 16 | browser-server /demo > worker.js 17 | ``` 18 | 19 | Then create a simple app and browserify it 20 | 21 | ``` js 22 | var createServer = require('browser-server') 23 | var server = createServer() 24 | 25 | server.on('request', function (req, res) { 26 | console.log('intercepting request', req) 27 | res.end('hello world') 28 | }) 29 | 30 | server.on('ready', function () { 31 | fetch('/demo/test.txt').then(function (r) { 32 | return r.text() 33 | }).then(function (txt) { 34 | console.log('fetch returned', txt) 35 | }) 36 | }) 37 | ``` 38 | 39 | Then browserify this app 40 | 41 | ``` 42 | browserify app.js > bundle.js 43 | ``` 44 | 45 | And make a index.html page like this 46 | 47 | 48 | ``` html 49 | 50 | 51 | 52 | 53 | 54 | 55 | ``` 56 | 57 | Make sure the worker.js file is also stored in the same folder. 58 | 59 | Now serve the folder using a http server, fx 60 | 61 | ``` 62 | npm install -g http-server 63 | http-server . 64 | ``` 65 | 66 | Now if you open index.html you should see the server intercepting the request and returning hello world. 67 | 68 | Works for all http apis, including video/audio tags! 69 | 70 | ## License 71 | 72 | MIT 73 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs') 4 | 5 | var src = fs.readFileSync(__dirname + '/worker.js', 'utf-8') 6 | 7 | if (process.argv[2]) { 8 | src = src.replace('/browser-server/', process.argv[2]) 9 | } 10 | 11 | process.stdout.write(src) 12 | -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | var createServer = require('browser-server') 2 | var server = createServer() 3 | 4 | server.on('request', function (req, res) { 5 | console.log('intercepting request', req) 6 | res.end('hello world') 7 | }) 8 | 9 | server.on('ready', function () { 10 | fetch('/demo/test.txt').then(function (r) { 11 | return r.text() 12 | }).then(function (txt) { 13 | console.log('fetch returned', txt) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/worker.js: -------------------------------------------------------------------------------- 1 | var streams = [] 2 | var prefix = '/demo/' 3 | 4 | self.addEventListener('message', function (e) { 5 | if (e.data.id === -1 && e.data.prefix) { 6 | prefix = e.data.prefix 7 | return 8 | } 9 | 10 | var s = streams[e.data.id] 11 | if (s.started) s.ondata(e.data) 12 | else s.onstart(e.data) 13 | }) 14 | 15 | function onready (client) { 16 | client.postMessage({type: 'ready'}) 17 | } 18 | 19 | self.addEventListener('fetch', function (e) { 20 | var path = '/' + e.request.url.split('/').slice(3).join('/') 21 | if (path.indexOf(prefix) !== 0) return 22 | 23 | var headers = {} 24 | 25 | e.request.headers.forEach(function (val, name) { 26 | headers[name] = val 27 | }) 28 | 29 | var p = self.clients.get(e.clientId).then(function (client) { 30 | if (!client) { 31 | return fetch(e.request) 32 | } 33 | 34 | return new Promise(function (resolve, reject) { 35 | var pulling = false 36 | var controller 37 | // var encoder = new TextEncoder() 38 | var rs 39 | var id = streams.indexOf(null) 40 | if (id === -1) id = streams.push(null) - 1 41 | 42 | var req = { 43 | id: id, 44 | started: false, 45 | onstart: function (data) { 46 | if (data.skip) { 47 | streams[id] = null 48 | resolve(fetch(e.request)) 49 | return 50 | } 51 | 52 | req.started = true 53 | rs = new ReadableStream({ 54 | pull: function (c) { 55 | if (pulling) return 56 | pulling = true 57 | controller = c 58 | client.postMessage({ 59 | type: 'pull', 60 | id: id 61 | }) 62 | }, 63 | cancel: function () { 64 | console.log('was cancelled') 65 | } 66 | }) 67 | 68 | resolve(new Response(rs, {status: data.status, headers: data.headers, statusText: data.statusText})) 69 | }, 70 | ondata: function (data) { 71 | pulling = false 72 | if (!data.data) { 73 | controller.close() 74 | streams[id] = null 75 | } else { 76 | controller.enqueue(data.data) 77 | } 78 | }, 79 | onerror: function () { 80 | if (rs) rs.cancel() 81 | else reject(new Error('Request failed')) 82 | streams[id] = null 83 | } 84 | } 85 | 86 | streams[id] = req 87 | client.postMessage({ 88 | type: 'open', 89 | id: id, 90 | path: path, 91 | headers: headers 92 | }) 93 | }) 94 | }) 95 | 96 | e.respondWith(p) 97 | }) 98 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var events = require('events') 2 | var inherits = require('inherits') 3 | var stream = require('readable-stream') 4 | var status = require('http-status') 5 | 6 | module.exports = BrowserServer 7 | 8 | function BrowserServer () { 9 | if (!(this instanceof BrowserServer)) return new BrowserServer() 10 | events.EventEmitter.call(this) 11 | 12 | if (navigator.serviceWorker) { 13 | this._start() 14 | } 15 | } 16 | 17 | inherits(BrowserServer, events.EventEmitter) 18 | 19 | BrowserServer.prototype._start = function () { 20 | var streams = [] 21 | var self = this 22 | 23 | function onopen (e) { 24 | var ws = new stream.Writable() 25 | var headers = {} 26 | var first = true 27 | 28 | ws._callback = null 29 | ws._pending = null 30 | ws._wants = false 31 | 32 | ws.statusCode = 200 33 | 34 | ws.setHeader = function (name, val) { 35 | headers[name] = val 36 | } 37 | 38 | ws.getHeader = function (name, val) { 39 | return headers[name] 40 | } 41 | 42 | ws.on('finish', function () { 43 | ws._write(null, null, function () {}) 44 | }) 45 | 46 | ws._write = function (data, enc, cb) { 47 | if (first) { 48 | first = false 49 | navigator.serviceWorker.controller.postMessage({ 50 | id: e.data.id, 51 | status: ws.statusCode, 52 | statusText: status[ws.statusCode], 53 | headers: headers 54 | }) 55 | } 56 | 57 | if (ws._wants) { 58 | ws._wants = false 59 | 60 | var m = { 61 | id: e.data.id, 62 | data: data 63 | } 64 | 65 | navigator.serviceWorker.controller.postMessage(m) 66 | cb() 67 | return 68 | } 69 | 70 | ws._pending = data 71 | ws._callback = cb 72 | } 73 | 74 | streams[e.data.id] = ws 75 | self.emit('request', e.data, ws) 76 | } 77 | 78 | navigator.serviceWorker.addEventListener('message', function (e) { 79 | if (e.data.type === 'open') { 80 | onopen(e) 81 | return 82 | } 83 | 84 | var ws = streams[e.data.id] 85 | 86 | if (ws._callback) { 87 | var data = ws._pending 88 | var cb = ws._callback 89 | 90 | ws._pending = ws._callback = null 91 | 92 | var m = { 93 | id: e.data.id, 94 | data: data 95 | } 96 | 97 | navigator.serviceWorker.controller.postMessage(m) 98 | cb() 99 | return 100 | } 101 | 102 | ws._wants = true 103 | }) 104 | 105 | navigator.serviceWorker.register('/worker.js').then(function () { 106 | return navigator.serviceWorker.ready 107 | }).then(function (reg) { 108 | self.emit('ready') 109 | }) 110 | } 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browser-server", 3 | "version": "1.0.3", 4 | "description": "A HTTP \"server\" in the browser that uses a service worker to allow you to easily send back your own stream of data.", 5 | "main": "index.js", 6 | "dependencies": { 7 | "http-status": "^1.0.1", 8 | "inherits": "^2.0.3", 9 | "readable-stream": "^2.2.9" 10 | }, 11 | "devDependencies": { 12 | "browserify": "^14.3.0", 13 | "http-server": "^0.10.0" 14 | }, 15 | "scripts": { 16 | "demo": "browserify example/app.js > example/bundle.js && http-server example" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/mafintosh/browser-server.git" 21 | }, 22 | "author": "Mathias Buus (@mafintosh)", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/mafintosh/browser-server/issues" 26 | }, 27 | "bin": { 28 | "browser-server": "./cli.js" 29 | }, 30 | "homepage": "https://github.com/mafintosh/browser-server" 31 | } 32 | -------------------------------------------------------------------------------- /worker.js: -------------------------------------------------------------------------------- 1 | var streams = [] 2 | var prefix = '/browser-server/' 3 | 4 | self.addEventListener('message', function (e) { 5 | if (e.data.id === -1 && e.data.prefix) { 6 | prefix = e.data.prefix 7 | return 8 | } 9 | 10 | var s = streams[e.data.id] 11 | if (s.started) s.ondata(e.data) 12 | else s.onstart(e.data) 13 | }) 14 | 15 | self.addEventListener('fetch', function (e) { 16 | var path = '/' + e.request.url.split('/').slice(3).join('/') 17 | if (path.indexOf(prefix) !== 0) return 18 | 19 | var headers = {} 20 | 21 | e.request.headers.forEach(function (val, name) { 22 | headers[name] = val 23 | }) 24 | 25 | var p = self.clients.get(e.clientId).then(function (client) { 26 | if (!client) { 27 | return fetch(e.request) 28 | } 29 | 30 | return new Promise(function (resolve, reject) { 31 | var pulling = false 32 | var controller 33 | // var encoder = new TextEncoder() 34 | var rs 35 | var id = streams.indexOf(null) 36 | if (id === -1) id = streams.push(null) - 1 37 | 38 | var req = { 39 | id: id, 40 | started: false, 41 | onstart: function (data) { 42 | if (data.skip) { 43 | streams[id] = null 44 | resolve(fetch(e.request)) 45 | return 46 | } 47 | 48 | req.started = true 49 | rs = new ReadableStream({ 50 | pull: function (c) { 51 | if (pulling) return 52 | pulling = true 53 | controller = c 54 | client.postMessage({ 55 | type: 'pull', 56 | id: id 57 | }) 58 | }, 59 | cancel: function () { 60 | console.log('was cancelled') 61 | } 62 | }) 63 | 64 | resolve(new Response(rs, {status: data.status, headers: data.headers, statusText: data.statusText})) 65 | }, 66 | ondata: function (data) { 67 | pulling = false 68 | if (!data.data) { 69 | controller.close() 70 | streams[id] = null 71 | } else { 72 | controller.enqueue(data.data) 73 | } 74 | }, 75 | onerror: function () { 76 | if (rs) rs.cancel() 77 | else reject(new Error('Request failed')) 78 | streams[id] = null 79 | } 80 | } 81 | 82 | streams[id] = req 83 | client.postMessage({ 84 | type: 'open', 85 | id: id, 86 | path: path, 87 | headers: headers 88 | }) 89 | }) 90 | }) 91 | 92 | e.respondWith(p) 93 | }) 94 | 95 | self.addEventListener('install', function (event) { 96 | event.waitUntil(self.skipWaiting()) 97 | }) 98 | 99 | self.addEventListener('activate', function (event) { 100 | event.waitUntil(self.clients.claim()) 101 | }) 102 | --------------------------------------------------------------------------------