diff --git a/README.md b/README.md index 340baca..cbd6cab 100644 --- a/README.md +++ b/README.md @@ -8,20 +8,6 @@ Lightweight library providing peer to peer CDN functionality -## **This is work in progress!** - -You can speed up the process of development. Check [help wanted](https://github.com/vardius/peer-cdn/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) issues and [contribute](https://github.com/vardius/peer-cdn/blob/master/CONTRIBUTING.md#development) - -### Things to consider: -- peer matching algorithms (ways of improving - pick best direction to go from here, beta version keeps it simple - pick first) -- browser support [WebRTC](https://webrtc.org) -- browser support [`client.postMessage()`](https://developer.mozilla.org/en-US/docs/Web/API/Client/postMessage#Browser_compatibility) -- media supported (there might be issues with range request) - -For now I know there might be some issues with: -- [`client.postMessage()`](https://developer.mozilla.org/en-US/docs/Web/API/Client/postMessage#Browser_compatibility) problems on **Google Chrome Version 64.0.3282.167 (Official Build) (64-bit)** however works on **Mozilla Firefox Quantum 58.0.2 (64-bit)** -- [range requests](https://github.com/vardius/peer-cdn/issues/7) - 📖 ABOUT ================================================== Contributors: @@ -114,7 +100,7 @@ if ("serviceWorker" in navigator) { ```js // import peer-cdn into service worker -self.importScripts("https://github.com/vardius/peer-cdn/blob/v1.0.4-beta/dist/index.js"); +self.importScripts("https://github.com/vardius/peer-cdn/blob/v1.0.5-beta/dist/index.js"); const { CachePlugin, DelegatePlugin, NetworkPlugin, strategies: { ordered }} = PeerCDN; diff --git a/dist/index.es.js b/dist/index.es.js index 514e222..f254f45 100644 --- a/dist/index.es.js +++ b/dist/index.es.js @@ -962,10 +962,19 @@ var Middleware = /*#__PURE__*/function () { case 5: response = _context2.sent; - composed.put && composed.put(response); + + if (!composed.put) { + _context2.next = 9; + break; + } + + _context2.next = 9; + return composed.put(response); + + case 9: return _context2.abrupt("return", response); - case 8: + case 10: case "end": return _context2.stop(); } @@ -1016,28 +1025,35 @@ var Middleware = /*#__PURE__*/function () { response = _context3.sent; if (!response) { - _context3.next = 9; + _context3.next = 11; + break; + } + + if (!x.put) { + _context3.next = 10; break; } - // pass response to put method - x.put && x.put(response); + _context3.next = 10; + return x.put(response); + + case 10: return _context3.abrupt("return", { get: function get() { return response; } }); - case 9: - _context3.next = 11; + case 11: + _context3.next = 13; return b(request); - case 11: + case 13: y = _context3.sent; - _context3.next = 14; + _context3.next = 16; return y.get(); - case 14: + case 16: response = _context3.sent; return _context3.abrupt("return", { get: function get() { @@ -1046,7 +1062,7 @@ var Middleware = /*#__PURE__*/function () { put: _this3._composeHandlers(y.put, x.put) }); - case 16: + case 18: case "end": return _context3.stop(); } @@ -1070,17 +1086,49 @@ var Middleware = /*#__PURE__*/function () { // If handler is not a function, mock it funcs = funcs.map(function (func) { - return function () { - if (typeof func === "function") { - func.apply(void 0, arguments); - } - }; + return /*#__PURE__*/asyncToGenerator( /*#__PURE__*/regenerator.mark(function _callee4() { + var _args4 = arguments; + return regenerator.wrap(function _callee4$(_context4) { + while (1) { + switch (_context4.prev = _context4.next) { + case 0: + if (!(typeof func === "function")) { + _context4.next = 3; + break; + } + + _context4.next = 3; + return func.apply(void 0, _args4); + + case 3: + case "end": + return _context4.stop(); + } + } + }, _callee4); + })); }); return funcs.reduce(function (a, b) { - return function () { - a.apply(void 0, arguments); - b.apply(void 0, arguments); - }; + return /*#__PURE__*/asyncToGenerator( /*#__PURE__*/regenerator.mark(function _callee5() { + var _args5 = arguments; + return regenerator.wrap(function _callee5$(_context5) { + while (1) { + switch (_context5.prev = _context5.next) { + case 0: + _context5.next = 2; + return a.apply(void 0, _args5); + + case 2: + _context5.next = 4; + return b.apply(void 0, _args5); + + case 4: + case "end": + return _context5.stop(); + } + } + }, _callee5); + })); }); } }]); @@ -1369,6 +1417,9 @@ var Cache = /*#__PURE__*/function () { this.getMiddleware = this.getMiddleware.bind(this); this.clearOldCaches = this.clearOldCaches.bind(this); + this._createPartialResponse = this._createPartialResponse.bind(this); + this._parseRangeHeader = this._parseRangeHeader.bind(this); + this._calculateEffectiveBoundaries = this._calculateEffectiveBoundaries.bind(this); } // Middleware factory function for fetch event @@ -1386,42 +1437,55 @@ var Cache = /*#__PURE__*/function () { while (1) { switch (_context.prev = _context.next) { case 0: - if (!request.headers.has("range")) { - _context.next = 2; + _context.prev = 0; + _context.next = 3; + return caches.match(request); + + case 3: + response = _context.sent; + + if (response) { + _context.next = 8; break; } - return _context.abrupt("return", null); - - case 2: - _context.prev = 2; - _context.next = 5; - return caches.match(request); + _context.next = 7; + return caches.match(request.url); - case 5: + case 7: response = _context.sent; + case 8: if (!response) { - _context.next = 8; + _context.next = 12; break; } + if (!request.headers.has("range")) { + _context.next = 11; + break; + } + + return _context.abrupt("return", _this._createPartialResponse(request, response)); + + case 11: return _context.abrupt("return", response); - case 8: + case 12: return _context.abrupt("return", null); - case 11: - _context.prev = 11; - _context.t0 = _context["catch"](2); + case 15: + _context.prev = 15; + _context.t0 = _context["catch"](0); + return _context.abrupt("return", null); - case 14: + case 19: case "end": return _context.stop(); } } - }, _callee, null, [[2, 11]]); + }, _callee, null, [[0, 15]]); })); function get() { @@ -1430,22 +1494,46 @@ var Cache = /*#__PURE__*/function () { return get; }(), - put: function put(response) { - // do not cache ranged responses - // https://github.com/vardius/peer-cdn/issues/7 - if (request.headers.has("range")) { - return; - } // IMPORTANT: Clone the response. A response is a stream - // and because we want the browser to consume the response - // as well as the cache consuming the response, we need - // to clone it so we have two streams. - - - var responseToCache = response.clone(); - caches.open(_this.names.peerFetch).then(function (cache) { - cache.put(request, responseToCache); - }); - } + put: function () { + var _put = asyncToGenerator( /*#__PURE__*/regenerator.mark(function _callee2(response) { + var cache, responseToCache; + return regenerator.wrap(function _callee2$(_context2) { + while (1) { + switch (_context2.prev = _context2.next) { + case 0: + _context2.prev = 0; + _context2.next = 3; + return caches.open(_this.names.peerFetch); + + case 3: + cache = _context2.sent; + // IMPORTANT: Clone the response. A response is a stream + // and because we want the browser to consume the response + // as well as the cache consuming the response, we need + // to clone it so we have two streams. + responseToCache = response.clone(); + cache.put(request, responseToCache); + _context2.next = 11; + break; + + case 8: + _context2.prev = 8; + _context2.t0 = _context2["catch"](0); + + case 11: + case "end": + return _context2.stop(); + } + } + }, _callee2, null, [[0, 8]]); + })); + + function put(_x) { + return _put.apply(this, arguments); + } + + return put; + }() }; } // Clears old cache, function used in activate event handler @@ -1469,6 +1557,133 @@ var Cache = /*#__PURE__*/function () { })); }); } + }, { + key: "_createPartialResponse", + value: function () { + var _createPartialResponse2 = asyncToGenerator( /*#__PURE__*/regenerator.mark(function _callee3(request, originalResponse) { + var rangeHeader, boundaries, originalBlob, effectiveBoundaries, slicedBlob, slicedBlobSize, slicedResponse; + return regenerator.wrap(function _callee3$(_context3) { + while (1) { + switch (_context3.prev = _context3.next) { + case 0: + if (!(originalResponse.status === 206)) { + _context3.next = 2; + break; + } + + return _context3.abrupt("return", originalResponse); + + case 2: + rangeHeader = request.headers.get('range'); + + if (rangeHeader) { + _context3.next = 5; + break; + } + + throw new Error('no-range-header'); + + case 5: + boundaries = this._parseRangeHeader(rangeHeader); + _context3.next = 8; + return originalResponse.blob(); + + case 8: + originalBlob = _context3.sent; + effectiveBoundaries = this._calculateEffectiveBoundaries(originalBlob, boundaries.start, boundaries.end); + slicedBlob = originalBlob.slice(effectiveBoundaries.start, effectiveBoundaries.end); + slicedBlobSize = slicedBlob.size; + slicedResponse = new Response(slicedBlob, { + // Status code 206 is for a Partial Content response. + // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/206 + status: 206, + statusText: 'Partial Content', + headers: originalResponse.headers + }); + slicedResponse.headers.set('Content-Length', String(slicedBlobSize)); + slicedResponse.headers.set('Content-Range', "bytes ".concat(effectiveBoundaries.start, "-").concat(effectiveBoundaries.end - 1, "/") + originalBlob.size); + return _context3.abrupt("return", slicedResponse); + + case 16: + case "end": + return _context3.stop(); + } + } + }, _callee3, this); + })); + + function _createPartialResponse(_x2, _x3) { + return _createPartialResponse2.apply(this, arguments); + } + + return _createPartialResponse; + }() + }, { + key: "_parseRangeHeader", + value: function _parseRangeHeader(rangeHeader) { + var normalizedRangeHeader = rangeHeader.trim().toLowerCase(); + + if (!normalizedRangeHeader.startsWith('bytes=')) { + throw new Error('unit-must-be-bytes', { + normalizedRangeHeader: normalizedRangeHeader + }); + } // Specifying multiple ranges separate by commas is valid syntax, but this + // library only attempts to handle a single, contiguous sequence of bytes. + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range#Syntax + + + if (normalizedRangeHeader.includes(',')) { + throw new Error('single-range-only', { + normalizedRangeHeader: normalizedRangeHeader + }); + } + + var rangeParts = /(\d*)-(\d*)/.exec(normalizedRangeHeader); // We need either at least one of the start or end values. + + if (!rangeParts || !(rangeParts[1] || rangeParts[2])) { + throw new Error('invalid-range-values', { + normalizedRangeHeader: normalizedRangeHeader + }); + } + + return { + start: rangeParts[1] === '' ? undefined : Number(rangeParts[1]), + end: rangeParts[2] === '' ? undefined : Number(rangeParts[2]) + }; + } + }, { + key: "_calculateEffectiveBoundaries", + value: function _calculateEffectiveBoundaries(blob, start, end) { + var blobSize = blob.size; + + if (end && end > blobSize || start && start < 0) { + throw new Error('range-not-satisfiable', { + size: blobSize, + end: end, + start: start + }); + } + + var effectiveStart; + var effectiveEnd; + + if (start !== undefined && end !== undefined) { + effectiveStart = start; // Range values are inclusive, so add 1 to the value. + + effectiveEnd = end + 1; + } else if (start !== undefined && end === undefined) { + effectiveStart = start; + effectiveEnd = blobSize; + } else if (end !== undefined && start === undefined) { + effectiveStart = blobSize - end; + effectiveEnd = blobSize; + } + + return { + start: effectiveStart, + end: effectiveEnd + }; + } }]); return Cache; @@ -1612,7 +1827,7 @@ var Delegate = /*#__PURE__*/function () { while (1) { switch (_context.prev = _context.next) { case 0: - if (!request.headers.has("range")) { + if (event.clientId) { _context.next = 2; break; } @@ -1620,50 +1835,43 @@ var Delegate = /*#__PURE__*/function () { return _context.abrupt("return", null); case 2: - if (event.clientId) { - _context.next = 4; - break; - } - - return _context.abrupt("return", null); - - case 4: - _context.next = 6; + _context.next = 4; return clients.get(event.clientId); - case 6: + case 4: client = _context.sent; msgClient = new MessageClient(_this.timeoutAfter); - _context.prev = 8; - _context.next = 11; + _context.prev = 6; + _context.next = 9; return msgClient.sendMessageToClient(client, { url: request.url }); - case 11: + case 9: response = _context.sent; if (!response) { - _context.next = 14; + _context.next = 12; break; } return _context.abrupt("return", response); - case 14: + case 12: return _context.abrupt("return", null); - case 17: - _context.prev = 17; - _context.t0 = _context["catch"](8); + case 15: + _context.prev = 15; + _context.t0 = _context["catch"](6); + return _context.abrupt("return", null); - case 20: + case 19: case "end": return _context.stop(); } } - }, _callee, null, [[8, 17]]); + }, _callee, null, [[6, 15]]); })); function get() { @@ -9322,42 +9530,35 @@ var Peer = /*#__PURE__*/function () { while (1) { switch (_context.prev = _context.next) { case 0: - if (!request.headers.has("range")) { - _context.next = 2; - break; - } - - return _context.abrupt("return", null); - - case 2: - _context.prev = 2; - _context.next = 5; + _context.prev = 0; + _context.next = 3; return _this.client.match(request); - case 5: + case 3: response = _context.sent; if (!response) { - _context.next = 8; + _context.next = 6; break; } return _context.abrupt("return", response); - case 8: + case 6: return _context.abrupt("return", null); - case 11: - _context.prev = 11; - _context.t0 = _context["catch"](2); + case 9: + _context.prev = 9; + _context.t0 = _context["catch"](0); + return _context.abrupt("return", null); - case 14: + case 13: case "end": return _context.stop(); } } - }, _callee, null, [[2, 11]]); + }, _callee, null, [[0, 9]]); })); function get() { diff --git a/dist/index.es.js.gz b/dist/index.es.js.gz index 29dd560..4390e90 100644 Binary files a/dist/index.es.js.gz and b/dist/index.es.js.gz differ diff --git a/dist/index.js b/dist/index.js index bae4d3b..4440918 100644 --- a/dist/index.js +++ b/dist/index.js @@ -968,10 +968,19 @@ case 5: response = _context2.sent; - composed.put && composed.put(response); + + if (!composed.put) { + _context2.next = 9; + break; + } + + _context2.next = 9; + return composed.put(response); + + case 9: return _context2.abrupt("return", response); - case 8: + case 10: case "end": return _context2.stop(); } @@ -1022,28 +1031,35 @@ response = _context3.sent; if (!response) { - _context3.next = 9; + _context3.next = 11; + break; + } + + if (!x.put) { + _context3.next = 10; break; } - // pass response to put method - x.put && x.put(response); + _context3.next = 10; + return x.put(response); + + case 10: return _context3.abrupt("return", { get: function get() { return response; } }); - case 9: - _context3.next = 11; + case 11: + _context3.next = 13; return b(request); - case 11: + case 13: y = _context3.sent; - _context3.next = 14; + _context3.next = 16; return y.get(); - case 14: + case 16: response = _context3.sent; return _context3.abrupt("return", { get: function get() { @@ -1052,7 +1068,7 @@ put: _this3._composeHandlers(y.put, x.put) }); - case 16: + case 18: case "end": return _context3.stop(); } @@ -1076,17 +1092,49 @@ // If handler is not a function, mock it funcs = funcs.map(function (func) { - return function () { - if (typeof func === "function") { - func.apply(void 0, arguments); - } - }; + return /*#__PURE__*/asyncToGenerator( /*#__PURE__*/regenerator.mark(function _callee4() { + var _args4 = arguments; + return regenerator.wrap(function _callee4$(_context4) { + while (1) { + switch (_context4.prev = _context4.next) { + case 0: + if (!(typeof func === "function")) { + _context4.next = 3; + break; + } + + _context4.next = 3; + return func.apply(void 0, _args4); + + case 3: + case "end": + return _context4.stop(); + } + } + }, _callee4); + })); }); return funcs.reduce(function (a, b) { - return function () { - a.apply(void 0, arguments); - b.apply(void 0, arguments); - }; + return /*#__PURE__*/asyncToGenerator( /*#__PURE__*/regenerator.mark(function _callee5() { + var _args5 = arguments; + return regenerator.wrap(function _callee5$(_context5) { + while (1) { + switch (_context5.prev = _context5.next) { + case 0: + _context5.next = 2; + return a.apply(void 0, _args5); + + case 2: + _context5.next = 4; + return b.apply(void 0, _args5); + + case 4: + case "end": + return _context5.stop(); + } + } + }, _callee5); + })); }); } }]); @@ -1375,6 +1423,9 @@ this.getMiddleware = this.getMiddleware.bind(this); this.clearOldCaches = this.clearOldCaches.bind(this); + this._createPartialResponse = this._createPartialResponse.bind(this); + this._parseRangeHeader = this._parseRangeHeader.bind(this); + this._calculateEffectiveBoundaries = this._calculateEffectiveBoundaries.bind(this); } // Middleware factory function for fetch event @@ -1392,42 +1443,55 @@ while (1) { switch (_context.prev = _context.next) { case 0: - if (!request.headers.has("range")) { - _context.next = 2; + _context.prev = 0; + _context.next = 3; + return caches.match(request); + + case 3: + response = _context.sent; + + if (response) { + _context.next = 8; break; } - return _context.abrupt("return", null); - - case 2: - _context.prev = 2; - _context.next = 5; - return caches.match(request); + _context.next = 7; + return caches.match(request.url); - case 5: + case 7: response = _context.sent; + case 8: if (!response) { - _context.next = 8; + _context.next = 12; break; } + if (!request.headers.has("range")) { + _context.next = 11; + break; + } + + return _context.abrupt("return", _this._createPartialResponse(request, response)); + + case 11: return _context.abrupt("return", response); - case 8: + case 12: return _context.abrupt("return", null); - case 11: - _context.prev = 11; - _context.t0 = _context["catch"](2); + case 15: + _context.prev = 15; + _context.t0 = _context["catch"](0); + return _context.abrupt("return", null); - case 14: + case 19: case "end": return _context.stop(); } } - }, _callee, null, [[2, 11]]); + }, _callee, null, [[0, 15]]); })); function get() { @@ -1436,22 +1500,46 @@ return get; }(), - put: function put(response) { - // do not cache ranged responses - // https://github.com/vardius/peer-cdn/issues/7 - if (request.headers.has("range")) { - return; - } // IMPORTANT: Clone the response. A response is a stream - // and because we want the browser to consume the response - // as well as the cache consuming the response, we need - // to clone it so we have two streams. - - - var responseToCache = response.clone(); - caches.open(_this.names.peerFetch).then(function (cache) { - cache.put(request, responseToCache); - }); - } + put: function () { + var _put = asyncToGenerator( /*#__PURE__*/regenerator.mark(function _callee2(response) { + var cache, responseToCache; + return regenerator.wrap(function _callee2$(_context2) { + while (1) { + switch (_context2.prev = _context2.next) { + case 0: + _context2.prev = 0; + _context2.next = 3; + return caches.open(_this.names.peerFetch); + + case 3: + cache = _context2.sent; + // IMPORTANT: Clone the response. A response is a stream + // and because we want the browser to consume the response + // as well as the cache consuming the response, we need + // to clone it so we have two streams. + responseToCache = response.clone(); + cache.put(request, responseToCache); + _context2.next = 11; + break; + + case 8: + _context2.prev = 8; + _context2.t0 = _context2["catch"](0); + + case 11: + case "end": + return _context2.stop(); + } + } + }, _callee2, null, [[0, 8]]); + })); + + function put(_x) { + return _put.apply(this, arguments); + } + + return put; + }() }; } // Clears old cache, function used in activate event handler @@ -1475,6 +1563,133 @@ })); }); } + }, { + key: "_createPartialResponse", + value: function () { + var _createPartialResponse2 = asyncToGenerator( /*#__PURE__*/regenerator.mark(function _callee3(request, originalResponse) { + var rangeHeader, boundaries, originalBlob, effectiveBoundaries, slicedBlob, slicedBlobSize, slicedResponse; + return regenerator.wrap(function _callee3$(_context3) { + while (1) { + switch (_context3.prev = _context3.next) { + case 0: + if (!(originalResponse.status === 206)) { + _context3.next = 2; + break; + } + + return _context3.abrupt("return", originalResponse); + + case 2: + rangeHeader = request.headers.get('range'); + + if (rangeHeader) { + _context3.next = 5; + break; + } + + throw new Error('no-range-header'); + + case 5: + boundaries = this._parseRangeHeader(rangeHeader); + _context3.next = 8; + return originalResponse.blob(); + + case 8: + originalBlob = _context3.sent; + effectiveBoundaries = this._calculateEffectiveBoundaries(originalBlob, boundaries.start, boundaries.end); + slicedBlob = originalBlob.slice(effectiveBoundaries.start, effectiveBoundaries.end); + slicedBlobSize = slicedBlob.size; + slicedResponse = new Response(slicedBlob, { + // Status code 206 is for a Partial Content response. + // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/206 + status: 206, + statusText: 'Partial Content', + headers: originalResponse.headers + }); + slicedResponse.headers.set('Content-Length', String(slicedBlobSize)); + slicedResponse.headers.set('Content-Range', "bytes ".concat(effectiveBoundaries.start, "-").concat(effectiveBoundaries.end - 1, "/") + originalBlob.size); + return _context3.abrupt("return", slicedResponse); + + case 16: + case "end": + return _context3.stop(); + } + } + }, _callee3, this); + })); + + function _createPartialResponse(_x2, _x3) { + return _createPartialResponse2.apply(this, arguments); + } + + return _createPartialResponse; + }() + }, { + key: "_parseRangeHeader", + value: function _parseRangeHeader(rangeHeader) { + var normalizedRangeHeader = rangeHeader.trim().toLowerCase(); + + if (!normalizedRangeHeader.startsWith('bytes=')) { + throw new Error('unit-must-be-bytes', { + normalizedRangeHeader: normalizedRangeHeader + }); + } // Specifying multiple ranges separate by commas is valid syntax, but this + // library only attempts to handle a single, contiguous sequence of bytes. + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range#Syntax + + + if (normalizedRangeHeader.includes(',')) { + throw new Error('single-range-only', { + normalizedRangeHeader: normalizedRangeHeader + }); + } + + var rangeParts = /(\d*)-(\d*)/.exec(normalizedRangeHeader); // We need either at least one of the start or end values. + + if (!rangeParts || !(rangeParts[1] || rangeParts[2])) { + throw new Error('invalid-range-values', { + normalizedRangeHeader: normalizedRangeHeader + }); + } + + return { + start: rangeParts[1] === '' ? undefined : Number(rangeParts[1]), + end: rangeParts[2] === '' ? undefined : Number(rangeParts[2]) + }; + } + }, { + key: "_calculateEffectiveBoundaries", + value: function _calculateEffectiveBoundaries(blob, start, end) { + var blobSize = blob.size; + + if (end && end > blobSize || start && start < 0) { + throw new Error('range-not-satisfiable', { + size: blobSize, + end: end, + start: start + }); + } + + var effectiveStart; + var effectiveEnd; + + if (start !== undefined && end !== undefined) { + effectiveStart = start; // Range values are inclusive, so add 1 to the value. + + effectiveEnd = end + 1; + } else if (start !== undefined && end === undefined) { + effectiveStart = start; + effectiveEnd = blobSize; + } else if (end !== undefined && start === undefined) { + effectiveStart = blobSize - end; + effectiveEnd = blobSize; + } + + return { + start: effectiveStart, + end: effectiveEnd + }; + } }]); return Cache; @@ -1618,7 +1833,7 @@ while (1) { switch (_context.prev = _context.next) { case 0: - if (!request.headers.has("range")) { + if (event.clientId) { _context.next = 2; break; } @@ -1626,50 +1841,43 @@ return _context.abrupt("return", null); case 2: - if (event.clientId) { - _context.next = 4; - break; - } - - return _context.abrupt("return", null); - - case 4: - _context.next = 6; + _context.next = 4; return clients.get(event.clientId); - case 6: + case 4: client = _context.sent; msgClient = new MessageClient(_this.timeoutAfter); - _context.prev = 8; - _context.next = 11; + _context.prev = 6; + _context.next = 9; return msgClient.sendMessageToClient(client, { url: request.url }); - case 11: + case 9: response = _context.sent; if (!response) { - _context.next = 14; + _context.next = 12; break; } return _context.abrupt("return", response); - case 14: + case 12: return _context.abrupt("return", null); - case 17: - _context.prev = 17; - _context.t0 = _context["catch"](8); + case 15: + _context.prev = 15; + _context.t0 = _context["catch"](6); + return _context.abrupt("return", null); - case 20: + case 19: case "end": return _context.stop(); } } - }, _callee, null, [[8, 17]]); + }, _callee, null, [[6, 15]]); })); function get() { @@ -9328,42 +9536,35 @@ while (1) { switch (_context.prev = _context.next) { case 0: - if (!request.headers.has("range")) { - _context.next = 2; - break; - } - - return _context.abrupt("return", null); - - case 2: - _context.prev = 2; - _context.next = 5; + _context.prev = 0; + _context.next = 3; return _this.client.match(request); - case 5: + case 3: response = _context.sent; if (!response) { - _context.next = 8; + _context.next = 6; break; } return _context.abrupt("return", response); - case 8: + case 6: return _context.abrupt("return", null); - case 11: - _context.prev = 11; - _context.t0 = _context["catch"](2); + case 9: + _context.prev = 9; + _context.t0 = _context["catch"](0); + return _context.abrupt("return", null); - case 14: + case 13: case "end": return _context.stop(); } } - }, _callee, null, [[2, 11]]); + }, _callee, null, [[0, 9]]); })); function get() { diff --git a/dist/index.js.gz b/dist/index.js.gz index f8dc468..bed5565 100644 Binary files a/dist/index.js.gz and b/dist/index.js.gz differ diff --git a/dist/server.index.js b/dist/server.index.js new file mode 100644 index 0000000..f6c9f10 --- /dev/null +++ b/dist/server.index.js @@ -0,0 +1,81 @@ +'use strict'; + +const os = require("os"); +const SocketIO = require("socket.io"); +const { SignalingEventType } = require("peer-data"); + +const PeerEventType = { PEER: "PEER" }; + +function PeerCdnServer(server, callback) { + const io = SocketIO.listen(server); + io.on("connection", function (socket) { + function log() { + socket.emit("log", ...arguments); + } + + function onConnect(id) { + socket.join(id); + } + + function onDisconnect(id) { + socket.leave(id); + } + + socket.on("message", function (event) { + event.caller = { + id: socket.id + }; + + log("SERVER_LOG", event); + + switch (event.type) { + case SignalingEventType.CONNECT: + onConnect(event.room.id); + socket.broadcast.to(event.room.id).emit("message", event); + break; + case SignalingEventType.DISCONNECT: + onDisconnect(event.room.id); + socket.broadcast.to(event.room.id).emit("message", event); + break; + case SignalingEventType.OFFER: + case SignalingEventType.ANSWER: + case SignalingEventType.CANDIDATE: + socket.broadcast.to(event.callee.id).emit("message", event); + break; + case PeerEventType.PEER: + // todo: we should pick best peer and ask only one socket to connect + socket.broadcast.emit("message", event); + break; + default: + if (callback) { + callback(socket, event); + } else { + socket.broadcast.to(event.room.id).emit("message", event); + } + } + }); + + socket.on("ipaddr", function () { + var ifaces = os.networkInterfaces(); + for (var dev in ifaces) { + ifaces[dev].forEach(function (details) { + if (details.family === "IPv4" && details.address !== "127.0.0.1") { + socket.emit("ipaddr", details.address); + } + }); + } + }); + + socket.on("disconnect", function () { + socket.broadcast.emit({ + type: SignalingEventType.DISCONNECT, + caller: { id: socket.id }, + callee: null, + room: null, + data: null + }); + }); + }); +} + +module.exports = PeerCdnServer; diff --git a/example/server.js b/example/server.js index 1316765..fda13fd 100644 --- a/example/server.js +++ b/example/server.js @@ -3,7 +3,7 @@ const fspath = require("path"); const cookieParser = require("cookie-parser"); const http = require("http"); const fs = require("fs"); -const PeerCdnServer = require("../src/server"); // require("peer-cdn/src/server") +const PeerCdnServer = require("../src/server"); // require("peer-cdn/server") const PeerEventType = { PEER: "PEER" }; const port = process.env.PORT || 3000; diff --git a/example/sw.js b/example/sw.js index 4a898c1..157498b 100644 --- a/example/sw.js +++ b/example/sw.js @@ -3,7 +3,7 @@ // import peer-cdn into service worker // this path is exposed with server - self.importScripts("/peer-cdn/index.js"); // self.importScripts("https://github.com/vardius/peer-cdn/blob/v1.0.4-beta/dist/index.js"); + self.importScripts("/peer-cdn/index.js"); // self.importScripts("https://github.com/vardius/peer-cdn/blob/v1.0.5-beta/dist/index.js"); const { CachePlugin, DelegatePlugin, NetworkPlugin, strategies: { ordered }} = PeerCDN; @@ -21,6 +21,13 @@ networkPlugin.getMiddleware ); + // Test range requests + cdn.GET("/movie.mp4", ordered, + cachePlugin.getMiddleware, + delegatePlugin.getMiddleware, + networkPlugin.getMiddleware + ); + // We need to register service worker events // cdn.register() will add listeners for install, activate and fetch // gaining required control diff --git a/package.json b/package.json index d6f6dc2..1e33910 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "peer-cdn", - "version": "1.0.4-beta", + "version": "1.0.5-beta", "description": "Lightweight library providing peer to peer CDN functionality", "main": "dist/index.js", "module": "dist/index.es.js", diff --git a/rollup.config.js b/rollup.config.js index 5a8d712..ac6753b 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -34,27 +34,43 @@ if (isProd) { ); } -const config = { - input: "src/index.js", - external: external, - plugins: plugins, - treeshake: true, - output: [ - { - name: pkg.name, - exports: "named", - file: pkg.module, - format: "es", - sourcemap: !isProd, - }, - { - name: pkg.name, - exports: "named", - file: pkg.main, - format: "umd", - sourcemap: !isProd, - }, - ], -}; +const config = [ + { + input: "src/index.js", + external: external, + plugins: plugins, + treeshake: true, + output: [ + { + name: pkg.name, + exports: "named", + file: pkg.module, + format: "es", + sourcemap: !isProd, + }, + { + name: pkg.name, + exports: "named", + file: pkg.main, + format: "umd", + sourcemap: !isProd, + }, + ], + }, + { + input: "src/server/index.js", + external: external, + treeshake: true, + output: [ + { + name: pkg.name, + exports: "named", + file: "dist/server.index.js", + format: "cjs", + sourcemap: !isProd, + } + ], + } +]; export default config; diff --git a/src/plugins/Cache.js b/src/plugins/Cache.js index 1e31082..9d1bef4 100644 --- a/src/plugins/Cache.js +++ b/src/plugins/Cache.js @@ -12,6 +12,9 @@ export default class Cache { this.getMiddleware = this.getMiddleware.bind(this); this.clearOldCaches = this.clearOldCaches.bind(this); + this._createPartialResponse = this._createPartialResponse.bind(this); + this._parseRangeHeader = this._parseRangeHeader.bind(this); + this._calculateEffectiveBoundaries = this._calculateEffectiveBoundaries.bind(this); } // Middleware factory function for fetch event @@ -20,41 +23,48 @@ export default class Cache { return { get: async () => { - // do not cache ranged responses - // https://github.com/vardius/peer-cdn/issues/7 - if (request.headers.has("range")) { - return null; - } - try { // caches.match() will look for a cache entry in all of the caches available to the service worker. // It's an alternative to first opening a specific named cache and then matching on that. - const response = await caches.match(request); + let response = await caches.match(request); + if (!response) { + // fallback to cache for other requests + response = await caches.match(request.url); + } if (response) { + if (request.headers.has("range")) { + return this._createPartialResponse(request, response); + } + return response; } return null; } catch (e) { + if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line no-console + console.error("CachePlugin: get error: ", e) + } + return null; } }, - put: response => { - // do not cache ranged responses - // https://github.com/vardius/peer-cdn/issues/7 - if (request.headers.has("range")) { - return; - } - - // IMPORTANT: Clone the response. A response is a stream - // and because we want the browser to consume the response - // as well as the cache consuming the response, we need - // to clone it so we have two streams. - const responseToCache = response.clone(); - - caches.open(this.names.peerFetch).then(function (cache) { + put: async response => { + try { + const cache = await caches.open(this.names.peerFetch) + + // IMPORTANT: Clone the response. A response is a stream + // and because we want the browser to consume the response + // as well as the cache consuming the response, we need + // to clone it so we have two streams. + const responseToCache = response.clone(); cache.put(request, responseToCache); - }); + } catch (e) { + if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line no-console + console.error("CachePlugin: put error: ", e) + } + } } }; } @@ -77,4 +87,97 @@ export default class Cache { ); }); } + + async _createPartialResponse(request, originalResponse) { + if (originalResponse.status === 206) { + return originalResponse; + } + + const rangeHeader = request.headers.get('range'); + if (!rangeHeader) { + throw new Error('no-range-header'); + } + + const boundaries = this._parseRangeHeader(rangeHeader); + const originalBlob = await originalResponse.blob(); + + const effectiveBoundaries = this._calculateEffectiveBoundaries( + originalBlob, boundaries.start, boundaries.end); + + const slicedBlob = originalBlob.slice(effectiveBoundaries.start, + effectiveBoundaries.end); + const slicedBlobSize = slicedBlob.size; + + const slicedResponse = new Response(slicedBlob, { + // Status code 206 is for a Partial Content response. + // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/206 + status: 206, + statusText: 'Partial Content', + headers: originalResponse.headers, + }); + + slicedResponse.headers.set('Content-Length', String(slicedBlobSize)); + slicedResponse.headers.set('Content-Range', + `bytes ${effectiveBoundaries.start}-${effectiveBoundaries.end - 1}/` + + originalBlob.size); + + return slicedResponse; + } + + _parseRangeHeader(rangeHeader) { + const normalizedRangeHeader = rangeHeader.trim().toLowerCase(); + if (!normalizedRangeHeader.startsWith('bytes=')) { + throw new Error('unit-must-be-bytes', {normalizedRangeHeader}); + } + + // Specifying multiple ranges separate by commas is valid syntax, but this + // library only attempts to handle a single, contiguous sequence of bytes. + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range#Syntax + if (normalizedRangeHeader.includes(',')) { + throw new Error('single-range-only', {normalizedRangeHeader}); + } + + const rangeParts = /(\d*)-(\d*)/.exec(normalizedRangeHeader); + // We need either at least one of the start or end values. + if (!rangeParts || !(rangeParts[1] || rangeParts[2])) { + throw new Error('invalid-range-values', {normalizedRangeHeader}); + } + + return { + start: rangeParts[1] === '' ? undefined : Number(rangeParts[1]), + end: rangeParts[2] === '' ? undefined : Number(rangeParts[2]), + }; + } + + _calculateEffectiveBoundaries(blob, start, end) { + const blobSize = blob.size; + + if ((end && end > blobSize) || (start && start < 0)) { + throw new Error('range-not-satisfiable', { + size: blobSize, + end, + start, + }); + } + + let effectiveStart; + let effectiveEnd; + + if (start !== undefined && end !== undefined) { + effectiveStart = start; + // Range values are inclusive, so add 1 to the value. + effectiveEnd = end + 1; + } else if (start !== undefined && end === undefined) { + effectiveStart = start; + effectiveEnd = blobSize; + } else if (end !== undefined && start === undefined) { + effectiveStart = blobSize - end; + effectiveEnd = blobSize; + } + + return { + start: effectiveStart, + end: effectiveEnd, + }; + } } diff --git a/src/plugins/Delegate.js b/src/plugins/Delegate.js index f7e8bf4..14d16d9 100644 --- a/src/plugins/Delegate.js +++ b/src/plugins/Delegate.js @@ -19,12 +19,6 @@ export default class Delegate { return { get: async () => { - // do not cache ranged responses - // https://github.com/vardius/peer-cdn/issues/7 - if (request.headers.has("range")) { - return null; - } - // Exit early if we don't have access to the client. // Eg, if it's cross-origin. if (!event.clientId) return null; @@ -42,6 +36,11 @@ export default class Delegate { return null; } catch (e) { + if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line no-console + console.error("DelegatePlugin: get error: ", e) + } + return null; } } diff --git a/src/plugins/Peer.js b/src/plugins/Peer.js index cd6d44d..1a597bc 100644 --- a/src/plugins/Peer.js +++ b/src/plugins/Peer.js @@ -23,12 +23,6 @@ export default class Peer { return { get: async () => { - // do not cache ranged responses - // https://github.com/vardius/peer-cdn/issues/7 - if (request.headers.has("range")) { - return null; - } - try { // this.match() will look for an entry in all of the peers available to the service worker. const response = await this.client.match(request); @@ -38,6 +32,10 @@ export default class Peer { return null; } catch (e) { + if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line no-console + console.error("PeerPlugin: get error: ", e) + } return null; } }, diff --git a/src/router/Middleware.js b/src/router/Middleware.js index f066ecf..52b46f3 100644 --- a/src/router/Middleware.js +++ b/src/router/Middleware.js @@ -31,7 +31,9 @@ export default class Middleware { const composed = await this._composePlugins(middleware)(request); const response = await composed.get(); - composed.put && composed.put(response); + if (composed.put) { + await composed.put(response) + } return response; }; @@ -50,7 +52,9 @@ export default class Middleware { let response = await x.get(); if (response) { // pass response to put method - x.put && x.put(response); + if (x.put) { + await x.put(response) + } return { get: () => response, @@ -70,15 +74,15 @@ export default class Middleware { // Composes handler methods for previous middleware into single one _composeHandlers(...funcs) { // If handler is not a function, mock it - funcs = funcs.map(func => (...args) => { + funcs = funcs.map(func => async (...args) => { if (typeof func === "function") { - func(...args); + await func(...args); } }); - return funcs.reduce((a, b) => (...args) => { - a(...args); - b(...args); + return funcs.reduce((a, b) => async (...args) => { + await a(...args); + await b(...args); }); } } diff --git a/website/docs/server.md b/website/docs/server.md index b65e0a1..5bf3f9b 100644 --- a/website/docs/server.md +++ b/website/docs/server.md @@ -24,7 +24,7 @@ There is an easy setup, simply import server from **[peer-cdn](https://github.co ```js const http = require("http"); const express = require("express"); -const PeerCdnServer = require("peer-cdn/src/server"); +const PeerCdnServer = require("peer-cdn/server"); const app = express(); diff --git a/website/docs/sw.md b/website/docs/sw.md index 50f5d6d..71ff283 100644 --- a/website/docs/sw.md +++ b/website/docs/sw.md @@ -7,7 +7,7 @@ sidebar_label: Service Worker ## Example ```js - self.importScripts("https://github.com/vardius/peer-cdn/blob/v1.0.4-beta/dist/index.js"); + self.importScripts("https://github.com/vardius/peer-cdn/blob/v1.0.5-beta/dist/index.js"); const { CachePlugin, DelegatePlugin, NetworkPlugin, strategies: { ordered }} = PeerCDN; diff --git a/website/src/pages/en/index.js b/website/src/pages/en/index.js index e76d69e..1e7dc11 100644 --- a/website/src/pages/en/index.js +++ b/website/src/pages/en/index.js @@ -17,7 +17,7 @@ const pre = "```"; const baseCodeExample = `${pre}js -self.importScripts("https://github.com/vardius/peer-cdn/blob/v1.0.4-beta/dist/index.js"); +self.importScripts("https://github.com/vardius/peer-cdn/blob/v1.0.5-beta/dist/index.js"); const {CachePlugin, DelegatePlugin, NetworkPlugin, strategies: { ordered }} = PeerCDN;