Skip to content

Commit

Permalink
feat(common): add a common module with TransferHttpCacheModule (#823)
Browse files Browse the repository at this point in the history
  • Loading branch information
vikerman authored Oct 11, 2017
1 parent 2a12463 commit 23a89ec
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 0 deletions.
5 changes: 5 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ rm -rf dist

set -x

npm run build:common || exit 1

cp modules/common/package.json dist/common/package.json
cp modules/common/README.md dist/common/README.md

npm run build:express-engine || exit 1

cp modules/express-engine/package.json dist/express-engine/package.json
Expand Down
36 changes: 36 additions & 0 deletions modules/common/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Angular Universal Common Module

This is the common Angular Universal module that is common across server-side rendering app
irrespective of the rendering engine.

The package can be installed using:

`npm install @nguniversal/common --save`

## TransferHttpCacheModule

`TransferHttpCacheModule` installs a Http interceptor that avoids duplicate `HttpClient` requests
on the client, for requests that were already made when the application was rendered on the server
side.

When the module is installed in the application `NgModule`, it will intercept `HttpClient` requests
on the server and store the response in the `TransferState` key-value store. This is transferred to the client, which then uses it to respond to the same `HttpClient` requests on the client.

### Usage

To use the `TransferHttpCacheModule` just install it as part of the top-level App module.

That's it!

```ts
import {TransferHttpCacheModule} from ‘@nguniversal/common’;

@NgModule({
imports: [
BrowserModule.withServerTransition({appId: ‘my-app’}),
TransferHttpCacheModule,
],
bootstrap: [MyApp]
})
export class AppBrowserModule() {}
```
1 change: 1 addition & 0 deletions modules/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { TransferHttpCacheModule } from './src/transfer_http';
37 changes: 37 additions & 0 deletions modules/common/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@nguniversal/common",
"main": "index.js",
"types": "index.d.ts",
"version": "5.0.0-beta.1",
"description": "Angular Universal common ",
"homepage": "https://github.com/angular/universal",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/angular/universal"
},
"bugs": {
"url": "https://github.com/angular/universal/issues"
},
"config": {
"engine-strict": true
},
"engines": {
"node": ">= 6.9.0",
"npm": ">= 3"
},
"peerDependencies": {
"@angular/common": "^5.0.0-beta.7",
"@angular/core": "^5.0.0-beta.7"
},
"devDependencies": {
"@angular/animations": "^5.0.0-beta.7",
"@angular/common": "^5.0.0-beta.7",
"@angular/core": "^5.0.0-beta.7",
"@angular/platform-browser": "^5.0.0-beta.7",
"@angular/platform-server": "^5.0.0-beta.7",
"rxjs": "^5.2.0",
"typescript": "2.4.2",
"zone.js": "^0.8.12"
}
}
100 changes: 100 additions & 0 deletions modules/common/src/transfer_http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {HTTP_INTERCEPTORS, HttpEvent, HttpHandler, HttpHeaders, HttpInterceptor, HttpRequest, HttpResponse} from '@angular/common/http';
import {ApplicationRef, Injectable, NgModule} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {of} from 'rxjs/observable/of';
import {filter} from 'rxjs/operator/filter';
import {first} from 'rxjs/operator/first';
import {toPromise} from 'rxjs/operator/toPromise';
import {_do} from 'rxjs/operator/do';

import {BrowserTransferStateModule, TransferState, makeStateKey} from '@angular/platform-browser';

export interface TransferHttpResponse {
body?: any | null;
headers?: {[k: string]: string[]};
status?: number;
statusText?: string;
url?: string;
}

function getHeadersMap(headers: HttpHeaders) {
const headersMap: {[name: string]: string[]} = {};
for (const key of headers.keys()) {
headersMap[key] = headers.getAll(key)!;
}
return headersMap;
}

@Injectable()
export class TransferHttpCacheInterceptor implements HttpInterceptor {

private isCacheActive = true;

private invalidateCacheEntry(url: string) {
this.transferState.remove(makeStateKey<TransferHttpResponse>('G.' + url));
this.transferState.remove(makeStateKey<TransferHttpResponse>('H.' + url));
}

constructor(appRef: ApplicationRef, private transferState: TransferState) {
// Stop using the cache if the application has stabilized, indicating initial rendering is
// complete.
toPromise
.call(first.call(filter.call(appRef.isStable, (isStable: boolean) => isStable)))
.then(() => { this.isCacheActive = false; });
}

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Stop using the cache if there is a mutating call.
if (req.method !== 'GET' && req.method !== 'HEAD') {
this.isCacheActive = false;
this.invalidateCacheEntry(req.url);
}

if (!this.isCacheActive) {
// Cache is no longer active. Pass the request through.
return next.handle(req);
}

const key = (req.method === 'GET' ? 'G.' : 'H.') + req.url;
const storeKey = makeStateKey<TransferHttpResponse>(key);

if (this.transferState.hasKey(storeKey)) {
// Request found in cache. Respond using it.
const response = this.transferState.get(storeKey, {} as TransferHttpResponse);
return of(new HttpResponse<any>({
body: response.body,
headers: new HttpHeaders(response.headers),
status: response.status,
statusText: response.statusText,
url: response.url,
}));
} else {
// Request not found in cache. Make the request and cache it.
const httpEvent = next.handle(req);
return _do.call(httpEvent, (event: HttpEvent<any>) => {
if (event instanceof HttpResponse) {
this.transferState.set(storeKey, {
body: event.body,
headers: getHeadersMap(event.headers),
status: event.status,
statusText: event.statusText,
url: event.url!,
});
}
});
}
}
}

/**
* An NgModule used in conjunction with `ServerTransferHttpCacheModule` to transfer cached HTTP
* calls from the server to the client application.
*/
@NgModule({
imports: [BrowserTransferStateModule],
providers: [
TransferHttpCacheInterceptor,
{provide: HTTP_INTERCEPTORS, useExisting: TransferHttpCacheInterceptor, multi: true},
],
})
export class TransferHttpCacheModule {}
13 changes: 13 additions & 0 deletions modules/common/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/common",
"rootDir": "./"
},
"angularCompilerOptions": {
"skipTemplateCodegen": true
},
"files": [
"index.ts"
]
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"scripts": {
"ngc": "ngc",
"jasmine": "jasmine",
"build:common": "ngc -p modules/common/tsconfig.json",
"build:express-engine": "ngc -p modules/express-engine/tsconfig.json",
"build:aspnetcore-engine": "ngc -p modules/aspnetcore-engine/tsconfig.json",
"build:hapi-engine": "ngc -p modules/hapi-engine/tsconfig.json",
Expand Down

0 comments on commit 23a89ec

Please sign in to comment.