├── .gitignore ├── LICENSE ├── README.md ├── package-dist.json ├── package.json ├── rollup.config.js ├── src ├── http-cache.module.ts ├── http-cache.service.ts └── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | dist 3 | node_modules 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 David Guijarro 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 | # Angular HTTP Cache 2 | 3 | ## What's this? 4 | 5 | This project adds an "offline-first" approach to the regular Http service. Responses are saved to local persistence and then served from there on subsequent requests. 6 | 7 | ### How does it work? 8 | 9 | Basically, the module uses its own extended Http replacement service to save the response to every request into the browser's local persistence. 10 | 11 | So for every request, the service will first look for a matching response in its local persistence; if it exists, it will emit that response first. 12 | 13 | Regardless of the previous step, the service will perform the HTTP request and will emit updated data if necessary. 14 | 15 | ## How to use it? 16 | 17 | The module is a replacement for the native `HttpModule`, so it's intended to be easy to be dropped in. 18 | 19 | Its methods are identical to the native module. 20 | 21 | The code works fine with JSON-formatted HTTP responses. It can be also used for other types of responses, such as images, but __it hasn't been properly tested yet__, so please use with caution. 22 | 23 | ### Installing it 24 | 25 | ``` 26 | npm install ng-http-cache --save 27 | ``` 28 | 29 | ### Importing it 30 | 31 | ```js 32 | import { BrowserModule } from '@angular/platform-browser'; 33 | import { NgModule } from '@angular/core'; 34 | 35 | import { HttpCacheModule } from 'ng-http-cache'; 36 | 37 | @NgModule({ 38 | imports: [ 39 | BrowserModule, 40 | HttpCacheModule 41 | ] 42 | }) 43 | export class ExampleModule { } 44 | ``` 45 | 46 | ### Using it 47 | 48 | ```js 49 | import { Http } from '@angular/http'; 50 | export class ExampleComponent implements OnInit { 51 | constructor(private http: Http) { } 52 | 53 | ngOnInit() { 54 | this.http.get('http://api.example.com/example') 55 | // There's no need to 'map' the response, the service does it for you! 56 | .subscribe((resp) => { 57 | console.log(resp); 58 | }); 59 | } 60 | } 61 | ``` 62 | 63 | ## More stuff 64 | 65 | ### Get in touch 66 | 67 | Feel free to drop me a line if you have an issue, doubt, problem or suggestion, even just to tell me what you think. You can leave an issue here or give me a shout on [Twitter](http://twitter.com/davguij). 68 | 69 | ### To-do 70 | 1) Check the network status and don't make the request if offline. 71 | 2) Include unit tests. 72 | 3) Add JSONP support. 73 | 74 | ### License 75 | 76 | MIT 77 | -------------------------------------------------------------------------------- /package-dist.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-http-cache", 3 | "version": "1.2.0", 4 | "description": "Speed up your remote requests by automatically caching them on client and add support for offline navigation.", 5 | "keywords": [ 6 | "angular 2" 7 | ], 8 | "main": "bundles/angular-http-cache.umd.min.js", 9 | "module": "index.js", 10 | "typings": "index.d.ts", 11 | "author": { 12 | "name": "David Guijarro", 13 | "email": "guijarro.dav@gmail.com" 14 | }, 15 | "license": "MIT", 16 | "homepage": "https://github.com/davguij/angular-http-cache", 17 | "bugs": { 18 | "url": "https://github.com/davguij/angular-http-cache/issues" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/davguij/angular-http-cache.git" 23 | }, 24 | "dependencies": { 25 | "localforage": "^1.4.3", 26 | "lodash": "^4.17.4" 27 | }, 28 | "peerDependencies": { 29 | "@angular/core": "^2.4.0", 30 | "@angular/http": "^2.4.0", 31 | "reflect-metadata": "^0.1.8", 32 | "rxjs": "^5.0.1", 33 | "zone.js": "^0.7.2" 34 | } 35 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-http-cache", 3 | "version": "1.2.0", 4 | "description": "Speed up your remote requests by automatically caching them on client and add support for offline navigation.", 5 | "keywords": [ 6 | "angular 2" 7 | ], 8 | "author": { 9 | "name": "David Guijarro", 10 | "email": "guijarro.dav@gmail.com" 11 | }, 12 | "main": "index.js", 13 | "scripts": { 14 | "clean": "rimraf .tmp && rimraf dist", 15 | "transpile": "ngc", 16 | "package": "rollup -c", 17 | "minify": "./node_modules/uglify-js/bin/uglifyjs dist/bundles/angular-http-cache.umd.js --screw-ie8 --compress --mangle --comments --output dist/bundles/angular-http-cache.umd.min.js", 18 | "copy": "cpx './README.md' dist && cpx './package-dist.json' dist && renamer --find 'package-dist.json' --replace 'package.json' ./dist/*", 19 | "build": "npm run clean && npm run transpile && npm run package && npm run minify && npm run copy" 20 | }, 21 | "license": "MIT", 22 | "homepage": "https://github.com/davguij/angular-http-cache", 23 | "bugs": { 24 | "url": "https://github.com/davguij/angular-http-cache/issues" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/davguij/angular-http-cache.git" 29 | }, 30 | "devDependencies": { 31 | "@angular/compiler": "^2.4.4", 32 | "@angular/compiler-cli": "^2.4.4", 33 | "@types/localforage": "0.0.33", 34 | "@types/lodash": "^4.14.51", 35 | "cpx": "^1.5.0", 36 | "renamer": "^0.6.1", 37 | "rimraf": "^2.5.4", 38 | "rollup": "^0.41.4", 39 | "typescript": "^2.1.0", 40 | "uglify-js": "^2.7.5" 41 | }, 42 | "dependencies": { 43 | "@angular/core": "^2.4.0", 44 | "@angular/http": "^2.4.0", 45 | "localforage": "^1.4.3", 46 | "lodash": "^4.17.4", 47 | "rxjs": "^5.0.1" 48 | } 49 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | entry: 'dist/index.js', 3 | dest: 'dist/bundles/angular-http-cache.umd.js', 4 | sourceMap: false, 5 | format: 'umd', 6 | moduleName: 'ng.http-cache', 7 | globals: { 8 | '@angular/core': 'ng.core', 9 | '@angular/http': 'ng.http', 10 | 'rxjs': 'Rx', 11 | 'rxjs/Observable': 'Rx', 12 | 'rxjs/ReplaySubject': 'Rx', 13 | 'rxjs/add/operator/map': 'Rx.Observable.prototype', 14 | 'rxjs/add/operator/mergeMap': 'Rx.Observable.prototype', 15 | 'rxjs/add/observable/fromEvent': 'Rx.Observable', 16 | 'rxjs/add/observable/of': 'Rx.Observable' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/http-cache.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { HttpModule, Http, XHRBackend, ConnectionBackend, RequestOptions } from '@angular/http'; 3 | 4 | import { HttpCacheService } from './http-cache.service'; 5 | 6 | export function httpCacheService(backend: ConnectionBackend, defaultOptions: RequestOptions) { 7 | return new HttpCacheService(backend, defaultOptions); 8 | } 9 | 10 | @NgModule({ 11 | imports: [ 12 | HttpModule 13 | ], 14 | providers: [ 15 | { 16 | provide: Http, 17 | deps: [XHRBackend, RequestOptions], 18 | useFactory: httpCacheService 19 | } 20 | ], 21 | }) 22 | export class HttpCacheModule { } 23 | -------------------------------------------------------------------------------- /src/http-cache.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Http, ConnectionBackend, Headers, Request, RequestOptions, Response, RequestOptionsArgs } from '@angular/http'; 3 | import { Observable, Subscriber } from 'rxjs'; 4 | 5 | import * as _ from 'lodash'; 6 | import * as localForage from 'localforage'; 7 | 8 | @Injectable() 9 | export class HttpCacheService extends Http { 10 | 11 | constructor(backend: ConnectionBackend, defaultOptions: RequestOptions) { 12 | super(backend, defaultOptions); 13 | localForage.config({ 14 | name: 'HttpCache', 15 | storeName: 'endpoints' 16 | }); 17 | } 18 | 19 | request(req: string | Request, options?: RequestOptionsArgs): Observable { 20 | let url = typeof req === 'string' ? req : req.url; 21 | 22 | let reqResponse = new Observable((subscriber: Subscriber) => { 23 | Observable.fromPromise(localForage.getItem(url)) 24 | .subscribe((localData: Response) => { 25 | if (localData != null) { 26 | subscriber.next(localData); 27 | } 28 | super.request(req, options) 29 | .map(resp => { 30 | if (typeof resp === 'object') { 31 | return resp.json(); 32 | } else { 33 | return resp; 34 | } 35 | }) 36 | .subscribe((remoteData: Response) => { 37 | // TODO check if both remote and local data are different 38 | // if they are, avoid saving remote data to localStorage and .next() on subject 39 | if (_.isEqual(remoteData, localData)) { 40 | subscriber.complete(); 41 | } else { 42 | Observable.fromPromise(localForage.setItem(url, remoteData)).subscribe((saved) => { 43 | subscriber.next(remoteData); 44 | subscriber.complete(); 45 | }); 46 | } 47 | }); 48 | }); 49 | }); 50 | 51 | 52 | return reqResponse; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { HttpCacheModule } from './http-cache.module'; 2 | export { HttpCacheService } from './http-cache.service'; 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "", 4 | "declaration": true, 5 | "stripInternal": true, 6 | "experimentalDecorators": true, 7 | "strictNullChecks": true, 8 | "noImplicitAny": true, 9 | "module": "es2015", 10 | "moduleResolution": "node", 11 | "paths": { 12 | "@angular/core": [ 13 | "node_modules/@angular/core" 14 | ], 15 | "@angular/common": [ 16 | "node_modules/@angular/common" 17 | ], 18 | "rxjs/*": [ 19 | "node_modules/rxjs/*" 20 | ], 21 | "localforage": [ 22 | "node_modules/localforage" 23 | ], 24 | "lodash": [ 25 | "node_modules/lodash" 26 | ] 27 | }, 28 | "rootDir": "src", 29 | "outDir": "dist", 30 | "sourceMap": true, 31 | "inlineSources": true, 32 | "target": "es5", 33 | "skipLibCheck": true, 34 | "lib": [ 35 | "es2015", 36 | "dom" 37 | ] 38 | }, 39 | "files": [ 40 | "./src/index.ts" 41 | ], 42 | "angularCompilerOptions": { 43 | "strictMetadataEmit": true, 44 | "genDir": ".tmp" 45 | } 46 | } --------------------------------------------------------------------------------