├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── changelog.md ├── index.ts ├── ng-http-sw-proxy-flow.png ├── ng-http-sw-proxy.xml ├── package.json ├── plugin ├── http-sw-proxy-plugin.ts └── index.ts ├── rollup.config.js ├── service-worker ├── rollup.js └── worker-basic.js ├── src ├── connectivity.service.ts ├── http-sw-proxy.module.ts ├── http-sw-proxy.service.spec.ts ├── http-sw-proxy.service.ts ├── index.ts └── store.ts ├── testbuild.sh └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | Thumbs.db 4 | .DS_Store 5 | *.js 6 | !rollup.config.js 7 | !service-worker/*.js 8 | *.map 9 | *.d.ts 10 | dist 11 | *.json 12 | !package.json 13 | !tsconfig.json 14 | .idea 15 | *.iml 16 | *.ngfactory.ts -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | Thumbs.db 4 | .DS_Store 5 | src 6 | !dist/src 7 | plugin 8 | !dist/plugin 9 | *.ngsummary.json 10 | *.iml 11 | rollup.config.js 12 | tsconfig.json 13 | *.ts 14 | !*.d.ts 15 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | before_deploy: 5 | - echo "B E F O R E D E P L O Y" 6 | - export DISPLAY=:99.0 7 | - sh -e /etc/init.d/xvfb start 8 | addons: 9 | chrome: stable 10 | deploy: 11 | provider: npm 12 | email: contact@maciejtreder.com 13 | api_key: 14 | secure: eZVlewYYx+pNlwVMKoYVyUMygJxqt9eeaqRfLM0vJm2m6Y3EEba2gPdHocE2HsJ672eIyBWD0ZpkK2CTTy6YmiXCbn4DqCaFBTzF4YPOJinPvwBV0qhO9hrddLJv9bzqXt57nsG2T2Fayb7lL9ppYIOa7V8NprmTNsXi7R8+o+nJOQlkGxJUClGgs+3x/IjMe+8TQqj40QyHWvhW8wa5okNvCL9ftPMmBvqqALTUzMsHnt4rUXZNqeD4wvHA4KF1xh6iPAihOurR3deGkbgIz4hnfDiLXasfqPJLIGKpXRg0Yz+jtoV6ZnV7qybYXqC3T+m/Zh5XjtvBL0r5RlI8MMiN2ge4Eq/pCg4U2W+0FI3r+gMX/FbIwvn8DbJlTk5pfJD28s5tpRCBK18KOT3hZpqYWrWmj9i12O2k2PDZll2hitXsob7LH7TAR1b8sO1xruFlToXA4F7whkzlqKIAl8lM0WeQn6NSP+jJhP4pLB4uoZMSnfZ80zusPB2OFNfUPHDgTkYhXAPXA8mpHg41xQUcGsSEe6FtzMOKvnUMbXZruIN1nAjr5XAnkkB6u9aq7XfB1S7V0gGgc9F8RJ7eZmI3uILUq9uKD6gkZtKXb1EM4v0KdfRvFoBcKEbarvXLXoHx+GQFIpn4eKvjAAv65wkmbbchl/o7L7GlJO397uc= 15 | on: 16 | tags: true 17 | branch: master 18 | repo: maciejtreder/ng-http-sw-proxy 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Maciej Sobala 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://badge.fury.io/js/ng-http-sw-proxy.svg)](https://badge.fury.io/js/ng-http-sw-proxy) 2 | [![Build Status](https://travis-ci.org/maciejtreder/ng-http-sw-proxy.svg?branch=master)](https://travis-ci.org/maciejtreder/serverless-apigw-binary) 3 | 4 | # ng-http-sw-proxy 5 | 6 | This service proxies Angular http traffic via service worker. It is collecting sent http requests in IndexedDB and providing them to service-worker 'sync' job. 7 | 8 | ### Workflow 9 | 10 | ![ng-http-sw-proxy flowchart](https://raw.githubusercontent.com/maciejtreder/ng-http-sw-proxy/master/ng-http-sw-proxy-flow.png) 11 | 12 | ### Installation 13 | 14 | ```bash 15 | npm install --save ng-http-sw-proxy 16 | cp -r node_modules/ng-http-sw-proxy/service-worker ./src 17 | ``` 18 | 19 | ### Compilation 20 | 21 | When compilation of your project is done, you need to combine service worker script. Rollup will automatically move it to your `dist` folder. 22 | ```bash 23 | node src/service-worker/rollup.js 24 | ``` 25 | 26 | ### Usage 27 | 28 | in your main module: 29 | ``` 30 | import { HttpSwProxyModule } from 'ng-http-sw-proxy'; 31 | 32 | @NgModule({ 33 | imports: [ 34 | HttpSwProxyModule, 35 | /* other modules*/ 36 | ], 37 | }) 38 | export class AppModule { 39 | } 40 | ``` 41 | 42 | After importing HttpSwProxyModule @angular http service is shadowed with the new one, from ng-http-sw-proxy. 43 | component/services looks like previous: 44 | ``` 45 | import { Http } from '@angular/http'; 46 | 47 | @Component({ 48 | /* component setup*/ 49 | }) 50 | export class HttpProxyDemoComponent { 51 | 52 | public response: Observable; 53 | 54 | constructor(private http: Http) {} 55 | 56 | public sendPost():void { 57 | this.response = this.http.post("testPost", {exampleKey: "exampleValue"}).map(res => res.json()); 58 | } 59 | } 60 | ``` 61 | 62 | Finally initialize service worker in your main file: 63 | ``` 64 | platformBrowserDynamic().bootstrapModule(BrowserAppModule).then(() => { 65 | if (process.env.NODE_ENV == 'production' && 'serviceWorker' in navigator) 66 | navigator.serviceWorker.register('./worker-basic.min.js').then(() => navigator.serviceWorker.ready); 67 | }); 68 | ``` 69 | 70 | ### Examples 71 | 72 | * [Angular Universal + AWS Lambda + API Gateway - binary support example](https://github.com/maciejtreder/angular-universal-serverless) ; [ Pass http requests via service worker - live demo ](https://www.angular-universal-serverless.maciejtreder.com/httpProxy) 73 | 74 | 75 | Something missing? More documentation? Bug fixes? All PRs welcome at https://github.com/maciejtreder/ng-http-sw-proxy -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # 2.1.x 2 | http proxy for angular 4.x.x 3 | 4 | 5 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from './plugin/index'; 2 | export * from './src/index'; -------------------------------------------------------------------------------- /ng-http-sw-proxy-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maciejtreder/ng-http-sw-proxy/9a519e95f33b27c3b9dc937fdda56ce5d35dcbbf/ng-http-sw-proxy-flow.png -------------------------------------------------------------------------------- /ng-http-sw-proxy.xml: -------------------------------------------------------------------------------- 1 | 7Vldb5swFP01qE+JAAfSPi5pu03apEl9WPvoBAe8GsyM89Vfv2uwIQ6kitSERtH6kOKLubbPPff6GBw0TTdfBc6TnzwizPHdaOOge8f3PS/w4Z+ybCvLeORVhljQSHdqDE/0jWijq61LGpHC6ig5Z5LmtnHOs4zMpWXDQvC13W3BmT1qjmPSMjzNMWtbf9NIJpX1NnAb+zdC48SM7Ln6zgzPX2PBl5kez/HRovyrbqfY+NL9iwRHfL1jQg8OmgrOZXWVbqaEKWwNbNVzjwfu1vMWJJNHPTDS85Bbs3YSARS6yYVMeMwzzB4a66RcH1EeXGglMmVw6cEl2VD5vHP9oroMA9XKpNg+6yfKRnPvD5FyqzmAl5KDqRn3B+e59ljNVE3v4GK1qeBLMde99AIlFjGpw1UjDQwmPCUwH+gjCMOSrmz3WFMprvs1cMKFRvQAukHlY4XZUnvdAq33EZdkI20gBSnoG56VHRRiOaeZLCcSTJzgHiyY0TgDwxzWTgQYVkRICgz+om+kNIrKaDE8I2xS83LKGRfluIaZBlnlgWycjvzSM2loa2E+6kZTexq4Q3SHNBC6HHjjqnk04Nr7LwVD49rMRnsdGLfGA18sCoj5fsDqOR4Xw/E5MsR9J0Oa5ovu2X+GhH1lyF0rQzJ+bQkyfjdB3KEbmDKsmTxyT5Ifg5Hb6fak+YH8/xEchp5d4PyPxe+kARq14pNgmJLvZkSuuXhtMKAcUH1sxU4kPJ0tYUqTdUIlecpxWTnWIP3skB7EuFWADkKJRmhvp9DhWDcyzDMqMdmVYK57GN6jqxHqWw31XevDdq0/sH2fvNSHLR4WBGQyhFHKvBzw75IUshUBkMi5uoSVYcYI47HAaS9k9Pay+raDi6iDi+EpqOi14IKKyVmZuXxWELEqS9bloBXuybEutMIzoVVT/WoT1+vI3J0T45lVWjt3aQYUlLtpCyd0+PkOkG5IdD9pbyMW3D1kb2DzEbkdhPTPRcj2ya9DFZ33IOH1fdQ2YFocRT1x1Owv1/smoxNevyd4UXs/ur5XGf6BY2+t871xYOvTwSUpfbOk3TJdqPiAWKBzJRyU3geIwbTMc6Cmov5lCf6Rf6TIqo0fKhp3Z6nJVaHQZcOzisZw3LNwMGdRq2r0pfnN4G3Rf1Nss/mN44c4VaTKZkVeL/Ei5Gww3nt5EgQ9yofb3nnp981L9Jm8RC1erjFVBXnBhWO9ELkcRt4dQcjgTIQ0xbZHQlp89M7Px8/8UNTxkk7XSX28Utv4QvD03RPW51ET+WOLm2HXa7vQPwk3odl8H61UUvMRGj38Aw== -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-http-sw-proxy", 3 | "version": "2.1.5", 4 | "description": "Proxy for angular http service. Schedules request send in service-worker and/or IndexedDB.", 5 | "main": "dist/bundles/ng-http-sw-proxy.umd.js", 6 | "module": "dist/index.js", 7 | "typings": "dist/index.d.ts", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/maciejtreder/ng-http-sw-proxy.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/maciejtreder/ng-http-sw-proxy/issues" 14 | }, 15 | "homepage": "https://github.com/maciejtreder/ng-http-sw-proxy#readme", 16 | "engines": { 17 | "node": ">=6.0" 18 | }, 19 | "scripts": { 20 | "test": "", 21 | "transpile": "ngc", 22 | "package": "rollup -c", 23 | "minify": "uglifyjs dist/bundles/ng-http-sw-proxy.umd.js --screw-ie8 --compress --mangle --comments --output dist/bundles/ng-http-sw-proxy.umd.min.js", 24 | "build": "rimraf dist && npm run transpile && npm run package && npm run minify", 25 | "prepublishOnly": "npm run build" 26 | }, 27 | "keywords": [ 28 | "angular", 29 | "angular2", 30 | "angular 2", 31 | "angular4", 32 | "service worker", 33 | "http proxy", 34 | "background sync", 35 | "sync", 36 | "http", 37 | "proxy" 38 | ], 39 | "author": "Maciej Treder ", 40 | "license": "MIT", 41 | "peerDependencies": { 42 | "@angular/common": ">=4.0.0", 43 | "@angular/core": ">=4.0.0" 44 | }, 45 | "dependencies": { 46 | "@angular/common": "^4.0.0", 47 | "@angular/core": "^4.0.0", 48 | "@angular/http": "^4.0.0", 49 | "@angular/platform-browser": "^4.0.0", 50 | "@angular/service-worker": "^1.0.0-beta.16", 51 | "idb": "^2.0.3", 52 | "rxjs": "^5.3.0" 53 | }, 54 | "devDependencies": { 55 | "@angular/compiler": "^4.2.4", 56 | "@angular/compiler-cli": "^4.2.4", 57 | "@types/node": "^8.0.7", 58 | "rimraf": "^2.6.1", 59 | "rollup": "^0.43.0", 60 | "typescript": "^2.3.4", 61 | "uglify-js": "^3.0.21" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /plugin/http-sw-proxy-plugin.ts: -------------------------------------------------------------------------------- 1 | import * as idb from 'idb'; 2 | import { Store } from '../src/store'; 3 | import { ReplaySubject } from 'rxjs'; 4 | 5 | 6 | export function HttpSwProxyPlugin() { 7 | return (worker:any) => new HttpSwProxyPluginImpl(worker); 8 | } 9 | 10 | export class HttpSwProxyPluginImpl { 11 | setup (ops:any) {} 12 | 13 | private promiseRegister: Map> = new Map>(); 14 | 15 | private store: Store = new Store(); 16 | 17 | private getMethod(id: number): string { 18 | switch (id) { 19 | case 0: return 'GET'; 20 | case 1: return 'POST'; 21 | case 2: return 'PUT'; 22 | case 3: return 'DELETE'; 23 | case 4: return 'OPTIONS'; 24 | case 5: return 'HEAD'; 25 | case 6: return 'PATCH'; 26 | } 27 | } 28 | 29 | private getContentType(body:any):string { 30 | if (body instanceof URLSearchParams) 31 | return 'application/x-www-form-urlencoded'; 32 | else if (body instanceof FormData) 33 | return 'multipart/form-data'; 34 | else if (body instanceof Blob) 35 | return 'application/octet-stream'; 36 | else if (body && typeof body === 'object') 37 | return 'application/json'; 38 | else 39 | return 'text/plain'; 40 | } 41 | 42 | private getBody(body:any):any { 43 | if (body && typeof body === 'object') 44 | return JSON.stringify(body) 45 | return body; 46 | } 47 | 48 | private getHeaders(request:any): Headers { 49 | let headers = new Headers(); 50 | if (request._body != null) 51 | headers.set('Content-Type', this.getContentType(request._body)); 52 | request.headers._headers.forEach((value: any, key:any) => headers.set(key, value)); 53 | return headers; 54 | } 55 | 56 | constructor(private worker:any){ 57 | self.addEventListener('sync', (event:any) => { 58 | event.waitUntil( 59 | this.store.requests('readonly').then(requests => requests.getAll()).then((requestes: any[]) => { 60 | return Promise.all(requestes.map((request) => { 61 | let promiseResolve: any; 62 | this.promiseRegister.set(request.id, > new Promise(resolve => promiseResolve = resolve)); 63 | return fetch(request.url, { 64 | method: this.getMethod(request.method), 65 | body: this.getBody(request._body), 66 | cache: "no-store" as RequestCache, 67 | headers: this.getHeaders(request) 68 | }).then(response => { 69 | this.store.requests('readwrite').then(requests => requests.delete(request.id)); 70 | var respToStore = { 71 | type: response.type, 72 | bodyUsed: response.bodyUsed, 73 | body: {}, 74 | headers: new Map(), 75 | ok: response.ok, 76 | status: response.status, 77 | statusText: response.statusText, 78 | url: response.url 79 | } 80 | 81 | let bodyPromise; 82 | if(response.headers.has('content-type')) { 83 | if(response.headers.get('content-type').indexOf('json') > -1 ) { 84 | bodyPromise = response.json(); 85 | } 86 | else if(response.headers.get('content-type').indexOf('text') > -1 ) { 87 | bodyPromise = response.text(); 88 | } 89 | else if(response.headers.get('content-type').indexOf('application/x-www-form-urlencoded') > -1 ) { 90 | bodyPromise = response.formData(); 91 | } 92 | else if(response.headers.get('content-type').indexOf('application/octet-stream') > -1 ) { 93 | bodyPromise = response.blob(); 94 | } 95 | else { 96 | bodyPromise = response.arrayBuffer(); 97 | } 98 | } 99 | response.headers.forEach((key: string, value:string) => { 100 | respToStore.headers.set(key, value); 101 | }); 102 | return bodyPromise.then((body:any) => { 103 | respToStore.body = body; 104 | this.store.responses('readwrite').then(responses => { 105 | responses.put({id: request.id, response: respToStore}); 106 | promiseResolve(true); 107 | }); 108 | return this.store.closeTransaction(); 109 | }) 110 | }); 111 | })).catch(err => { 112 | console.error(err); 113 | }); 114 | }) 115 | ); 116 | }); 117 | 118 | self.addEventListener('message', event => { 119 | if(typeof event.data == 'string' && event.data.indexOf("give me response ") > -1) { 120 | let key:string = event.data.substr(17); 121 | let interval:any; 122 | interval = setInterval(() => { 123 | if (this.promiseRegister.has(parseInt(key))) { 124 | clearInterval(interval); 125 | this.promiseRegister.get(parseInt(key)).then(value => { 126 | event.ports[0].postMessage("response is waiting: " + key); 127 | this.promiseRegister.delete(parseInt(key)); 128 | }); 129 | } 130 | }, 1); 131 | } 132 | }) 133 | } 134 | } -------------------------------------------------------------------------------- /plugin/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http-sw-proxy-plugin'; -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | entry: 'dist/index.js', 3 | dest: 'dist/bundles/ng-http-sw-proxy.umd.js', 4 | sourceMap: false, 5 | format: 'umd', 6 | moduleName: 'ng.http-sw-proxy', 7 | globals: { 8 | '@angular/core': 'ng.core', 9 | '@angular/common': 'ng.common', 10 | '@angular/http': 'ng.http', 11 | '@angular/service-worker': 'ng.service-worker', 12 | 'idb': 'idb', 13 | 'rxjs': 'Rx', 14 | 'rxjs/Observable': 'Rx', 15 | 'rxjs/ReplaySubject': 'Rx', 16 | 'rxjs/add/operator/map': 'Rx.Observable.prototype', 17 | 'rxjs/add/operator/mergeMap': 'Rx.Observable.prototype', 18 | 'rxjs/add/observable/fromEvent': 'Rx.Observable', 19 | 'rxjs/add/observable/of': 'Rx.Observable' 20 | }, 21 | external: ['@angular/core', '@angular/common', '@angular/http', '@angular/service-worker', 'idb', 'rxjs'] 22 | } -------------------------------------------------------------------------------- /service-worker/rollup.js: -------------------------------------------------------------------------------- 1 | const rollup = require('rollup'); 2 | const nodeResolve = require('rollup-plugin-node-resolve'); 3 | const commonJs = require('rollup-plugin-commonjs'); 4 | 5 | rollup.rollup({ 6 | entry: './src/service-worker/worker-basic.js', 7 | plugins: [ 8 | nodeResolve({jsnext: true, main: true}), 9 | commonJs({ 10 | include: 'node_modules/**', 11 | namedExports: { 12 | 'node_modules/jshashes/hashes.js': ['SHA1'] 13 | } 14 | }), 15 | ], 16 | 17 | }).then(bundle => bundle.write({ 18 | format: 'iife', 19 | dest: 'dist/worker-basic.min.js', 20 | })); -------------------------------------------------------------------------------- /service-worker/worker-basic.js: -------------------------------------------------------------------------------- 1 | import {bootstrapServiceWorker} from '@angular/service-worker/worker'; 2 | import {Dynamic, FreshnessStrategy, PerformanceStrategy} from '@angular/service-worker/plugins/dynamic'; 3 | import {ExternalContentCache} from '@angular/service-worker/plugins/external'; 4 | import {RouteRedirection} from '@angular/service-worker/plugins/routes'; 5 | import {StaticContentCache} from '@angular/service-worker/plugins/static'; 6 | import {Push} from '@angular/service-worker/plugins/push'; 7 | import {HttpSwProxyPlugin} from 'ng-http-sw-proxy/dist/plugin'; 8 | 9 | bootstrapServiceWorker({ 10 | manifestUrl: 'ngsw-manifest.json', 11 | plugins: [ 12 | StaticContentCache(), 13 | Dynamic([ 14 | new FreshnessStrategy(), 15 | new PerformanceStrategy(), 16 | ]), 17 | ExternalContentCache(), 18 | RouteRedirection(), 19 | Push(), 20 | HttpSwProxyPlugin() 21 | ], 22 | }); -------------------------------------------------------------------------------- /src/connectivity.service.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { PLATFORM_ID, Inject } from '@angular/core'; 3 | import { isPlatformBrowser } from '@angular/common' 4 | 5 | export class ConnectivityService { 6 | private platformId: Object; 7 | constructor(@Inject(PLATFORM_ID) platformId: any) { 8 | this.platformId = platformId; 9 | } 10 | public hasNetworkConnection():Observable { 11 | if (!isPlatformBrowser(this.platformId)) 12 | return Observable.of(true); 13 | return Observable.merge( 14 | Observable.of(navigator.onLine), 15 | Observable.fromEvent(window, 'online').map(() => true), 16 | Observable.fromEvent(window, 'offline').map(() => false) 17 | ); 18 | } 19 | } -------------------------------------------------------------------------------- /src/http-sw-proxy.module.ts: -------------------------------------------------------------------------------- 1 | import { Http, RequestOptions, XHRBackend, HttpModule } from '@angular/http'; 2 | import { NgModule, ApplicationRef } from '@angular/core' 3 | 4 | import { HttpSwProxy } from './http-sw-proxy.service'; 5 | import { Store } from './store'; 6 | import { ConnectivityService } from './connectivity.service'; 7 | 8 | export function httpFactory(backend: XHRBackend, defaultOptions: RequestOptions, appref: ApplicationRef, store: Store, connectivity: ConnectivityService) { 9 | return new HttpSwProxy(backend, defaultOptions, appref, store, connectivity); 10 | } 11 | 12 | @NgModule({ 13 | imports: [ 14 | HttpModule 15 | ], 16 | providers: [ 17 | Store, 18 | ConnectivityService, 19 | { 20 | provide: Http, 21 | useFactory: httpFactory, 22 | deps: [XHRBackend, RequestOptions, ApplicationRef, Store, ConnectivityService] 23 | } 24 | ] 25 | }) 26 | export class HttpSwProxyModule {} -------------------------------------------------------------------------------- /src/http-sw-proxy.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, inject, TestBed } from '@angular/core/testing'; 2 | import { HttpSwProxy } from './http-sw-proxy.service'; 3 | import { BaseRequestOptions, Http } from '@angular/http'; 4 | import { ApplicationRef } from '@angular/core'; 5 | import { Store } from './store'; 6 | import { ConnectivityService } from './connectivity.service'; 7 | import * as sinon from 'sinon'; 8 | import { MockBackend } from '@angular/http/testing'; 9 | import { ReplaySubject } from 'rxjs/ReplaySubject'; 10 | 11 | let connectionStub : any; 12 | const connectionObs: ReplaySubject = new ReplaySubject(); 13 | 14 | describe('sw-proxy tests', () => { 15 | beforeEach(() => { 16 | connectionStub = sinon.createStubInstance(ConnectivityService) 17 | connectionStub.hasNetworkConnection.returns(connectionObs); 18 | 19 | TestBed.configureTestingModule({ 20 | providers: [ 21 | Store, 22 | { provide: ConnectivityService, useValue: connectionStub }, 23 | ApplicationRef, 24 | { 25 | provide: Http, useFactory: (backend: MockBackend, options: BaseRequestOptions, appRef: ApplicationRef, store: Store, connService:ConnectivityService) => { 26 | return new HttpSwProxy(backend, options, appRef, store, connService); 27 | }, 28 | deps: [MockBackend, BaseRequestOptions, ApplicationRef, Store, ConnectivityService] 29 | }, 30 | MockBackend, 31 | BaseRequestOptions, 32 | ] 33 | }); 34 | }); 35 | 36 | it ('Should be able to construct', async(inject([Http], (http: Http) => { 37 | expect(http).toBeDefined(); 38 | }))); 39 | 40 | it ('Should queue requests when is offline', async(inject([Http, Store], (http: Http, store:Store) => { 41 | connectionObs.next(false); 42 | const spy = sinon.spy(store, 'requests'); 43 | 44 | http.get('someUrl').subscribe(); 45 | expect(spy.calledOnce).toBeTruthy('Request was not stored in DB'); 46 | }))); 47 | 48 | it ('Should not queue requests when is online', async(inject([Http, Store], (http: Http, store:Store) => { 49 | connectionObs.next(true); 50 | const spy = sinon.spy(store, 'requests'); 51 | 52 | http.get('someUrl').subscribe(); 53 | expect(spy.calledOnce).toBeFalsy('Request was stored in DB'); 54 | }))); 55 | }); -------------------------------------------------------------------------------- /src/http-sw-proxy.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Http, 3 | Response, 4 | RequestOptionsArgs, 5 | ResponseOptions, 6 | ResponseType, 7 | ResponseOptionsArgs, 8 | Headers, 9 | ConnectionBackend, 10 | RequestOptions, 11 | Request, 12 | RequestMethod, 13 | BaseRequestOptions 14 | } from '@angular/http'; 15 | 16 | import { ApplicationRef } from '@angular/core'; 17 | import { Observable, Observer } from 'rxjs'; 18 | 19 | import { Store } from './store'; 20 | import { ConnectivityService } from './connectivity.service'; 21 | 22 | function mergeOptions( 23 | defaultOpts: BaseRequestOptions, providedOpts: RequestOptionsArgs | undefined, 24 | method: RequestMethod, url: string) { 25 | const newOptions = defaultOpts; 26 | if (providedOpts) { 27 | // Hack so Dart can used named parameters 28 | return newOptions.merge(new RequestOptions({ 29 | method: providedOpts.method || method, 30 | url: providedOpts.url || url, 31 | search: providedOpts.search, 32 | params: providedOpts.params, 33 | headers: providedOpts.headers, 34 | body: providedOpts.body, 35 | withCredentials: providedOpts.withCredentials, 36 | responseType: providedOpts.responseType 37 | })); 38 | } 39 | return newOptions.merge(new RequestOptions({method, url})); 40 | } 41 | 42 | export class HttpSwProxy extends Http { 43 | 44 | private obsRegister: Map> = new Map>(); 45 | 46 | private isConnected: boolean = false; 47 | 48 | constructor(backend: ConnectionBackend, private defaultOptions: RequestOptions, private appref: ApplicationRef, private store: Store, private connectivity: ConnectivityService) { 49 | super(backend, defaultOptions); 50 | this.connectivity.hasNetworkConnection().subscribe(connected => this.isConnected = connected); 51 | } 52 | 53 | request(url: string | Request, options?: RequestOptionsArgs): Observable { 54 | if (typeof url == 'string') { 55 | url = new Request(mergeOptions(this.defaultOptions, options, RequestMethod.Get, url)); 56 | } 57 | return this.resolveRequest(url); 58 | } 59 | 60 | private resolveRequest(request: Request): Observable { 61 | if (this.isConnected) 62 | return super.request(request); 63 | else 64 | return this.passToDB(request); 65 | } 66 | 67 | private passToDB(request: Request): Observable { 68 | return Observable.create((subject:Observer) => { 69 | this.store.requests("readwrite").then(transaction => transaction.put(request)).then((key) => { 70 | if(process.env.NODE_ENV == 'production' && 'serviceWorker' in navigator) { 71 | navigator.serviceWorker.ready.then((swRegistration: ServiceWorkerRegistration) => { 72 | swRegistration.sync.register('request'); 73 | let messageChannel: MessageChannel = new MessageChannel(); 74 | messageChannel.port1.onmessage = (event: any) => { 75 | this.getResponseFromDB(key).subscribe(resp => subject.next(resp)); 76 | } 77 | navigator.serviceWorker.controller.postMessage("give me response " + key, [messageChannel.port2]); 78 | }); 79 | } 80 | else { 81 | this.obsRegister.set(key, subject); 82 | this.waitForConnectionAndSend(); 83 | } 84 | }); 85 | }); 86 | } 87 | 88 | private getResponseFromDB(messageId: any): Observable { 89 | return Observable.create((observer: Observer) => { 90 | this.store.responses('readwrite').then(transaction => transaction.get(messageId).then(entry => { 91 | transaction.delete(messageId); 92 | let response: Response; 93 | let headers = new Headers(); 94 | let responseType:ResponseType = ResponseType.Basic; 95 | 96 | for (var headerPair of entry.response.headers.entries()) { 97 | headers.set(headerPair[0], headerPair[1]); 98 | } 99 | 100 | switch (entry.response.type) { 101 | case "basic": 102 | { 103 | responseType = ResponseType.Basic; 104 | } 105 | } 106 | 107 | let responseOptions:ResponseOptions = new ResponseOptions({ 108 | body: entry.response.body, 109 | status: entry.response.status, 110 | headers: headers, 111 | statusText: entry.response.statusText, 112 | type: responseType, 113 | url: entry.response.url 114 | } as ResponseOptionsArgs); 115 | 116 | response = new Response(responseOptions); 117 | 118 | if(response.ok) 119 | observer.next( response); 120 | else 121 | observer.error( response); 122 | observer.complete; 123 | this.appref.tick(); 124 | })) 125 | }); 126 | } 127 | 128 | private waitForConnectionAndSend():void { 129 | this.connectivity.hasNetworkConnection().filter(connection => connection).subscribe(() => { 130 | this.store.requests('readwrite').then(transaction => { 131 | transaction.getAll().then((requests: any[]) => { 132 | requests.forEach(request => { 133 | transaction.delete(request.id); 134 | if (this.obsRegister.has(request.id)) { 135 | let toSend: Request = new Request({ 136 | method: request.method, 137 | url: request.url, 138 | withCredentials: request.withCredentials, 139 | headers: request.headers, 140 | body: request._body 141 | }); 142 | 143 | super.request(toSend).subscribe(resp => this.obsRegister.get(request.id).next(resp)); 144 | } 145 | }); 146 | }); 147 | }); 148 | }); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { HttpSwProxyModule } from './http-sw-proxy.module'; 2 | export { Store } from './store'; 3 | export { HttpSwProxy } from './http-sw-proxy.service'; 4 | export { ConnectivityService } from './connectivity.service'; -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import * as idb from 'idb'; 2 | import { ObjectStore } from 'idb'; 3 | 4 | export class Store { 5 | private db: any = null 6 | 7 | public init():Promise { 8 | if (this.db) { return Promise.resolve(this.db); } 9 | return idb.default.open('restRequest', 1, upgradeDb => { 10 | upgradeDb.createObjectStore('requests', { autoIncrement : true, keyPath: 'id' }); 11 | upgradeDb.createObjectStore('responses', {keyPath: 'id', autoIncrement: false}); 12 | }).then(database => { 13 | return this.db = database; 14 | }); 15 | } 16 | 17 | public requests(mode: string): Promise { 18 | return this.init().then(db => { 19 | return this.db.transaction('requests', mode).objectStore('requests'); 20 | }) 21 | } 22 | 23 | public responses(mode: string): Promise { 24 | return this.init().then(db => { 25 | return this.db.transaction('responses', mode).objectStore('responses'); 26 | }) 27 | } 28 | 29 | public closeTransaction():Promise { 30 | return this.db.transaction.complete; 31 | } 32 | } -------------------------------------------------------------------------------- /testbuild.sh: -------------------------------------------------------------------------------- 1 | git push build --delete newBuild 2 | git commit -m "new build" . 3 | npm run build 4 | 5 | git cob newBuild 6 | mv .gitignore ._gitignore 7 | cat .npmignore >> .gitignore 8 | git add . 9 | git commit -m "new build" . 10 | git push build 11 | git co master 12 | git b -D newBuild 13 | mv ._gitignore .gitignore -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "declaration": true, 5 | "stripInternal": true, 6 | "experimentalDecorators": true, 7 | "strictNullChecks": false, 8 | "noImplicitAny": true, 9 | "module": "es2015", 10 | "moduleResolution": "node", 11 | "paths": { 12 | "@angular/core": ["node_modules/@angular/core"], 13 | "@angular/http": ["node_modules/@angular/http"], 14 | "@angular/common": ["node_modules/@angular/common"], 15 | "@angular/platform-browser": ["node_modules/@angular/platform-browser"], 16 | "@angular/service-worker": ["node_modules/@angular/service-worker"], 17 | "idb": ["node_modules/idb/*"], 18 | "rxjs/*": ["node_modules/rxjs/*"] 19 | }, 20 | "rootDir": ".", 21 | "outDir": "dist", 22 | "sourceMap": true, 23 | "inlineSources": true, 24 | "target": "es5", 25 | "skipLibCheck": true, 26 | "lib": [ 27 | "es2015", 28 | "dom" 29 | ] 30 | }, 31 | "files": [ 32 | "index.ts" 33 | ], 34 | "angularCompilerOptions": { 35 | "strictMetadataEmit": true 36 | } 37 | } --------------------------------------------------------------------------------