Skip to content

Commit 7924a65

Browse files
Implement cache priming prototype
1 parent 5ff3e9a commit 7924a65

7 files changed

Lines changed: 114 additions & 3 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System;
2+
using System.Net;
3+
using System.Net.Http;
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNet.Mvc.Rendering;
6+
using Microsoft.Extensions.Logging;
7+
using Newtonsoft.Json;
8+
9+
namespace Microsoft.AspNet.AngularServices {
10+
public static class PrimeCacheHelper {
11+
public static async Task<HtmlString> PrimeCache(this IHtmlHelper html, string url) {
12+
// TODO: Consider deduplicating the PrimeCache calls (that is, if there are multiple requests to precache
13+
// the same URL, only return nonempty for one of them). This will make it easier to auto-prime-cache any
14+
// HTTP requests made during server-side rendering, without risking unnecessary duplicate requests.
15+
16+
if (string.IsNullOrEmpty(url)) {
17+
throw new ArgumentException("Value cannot be null or empty", "url");
18+
}
19+
20+
try {
21+
var request = html.ViewContext.HttpContext.Request;
22+
var baseUri = new Uri(string.Concat(request.Scheme, "://", request.Host.ToUriComponent(), request.PathBase.ToUriComponent(), request.Path.ToUriComponent(), request.QueryString.ToUriComponent()));
23+
var fullUri = new Uri(baseUri, url);
24+
var response = await new HttpClient().GetAsync(fullUri.ToString());
25+
var responseBody = await response.Content.ReadAsStringAsync();
26+
return new HtmlString(FormatAsScript(url, response.StatusCode, responseBody));
27+
} catch (Exception ex) {
28+
var logger = (ILogger)html.ViewContext.HttpContext.ApplicationServices.GetService(typeof (ILogger));
29+
if (logger != null) {
30+
logger.LogWarning("Error priming cache for URL: " + url, ex);
31+
}
32+
return new HtmlString(string.Empty);
33+
}
34+
}
35+
36+
private static string FormatAsScript(string url, HttpStatusCode responseStatusCode, string responseBody)
37+
{
38+
return string.Format(@"<script>window.__preCachedResponses = window.__preCachedResponses || {{}}; window.__preCachedResponses[{0}] = {1};</script>",
39+
JsonConvert.SerializeObject(url),
40+
JsonConvert.SerializeObject(new { statusCode = responseStatusCode, body = responseBody })
41+
);
42+
}
43+
}
44+
}

Microsoft.AspNet.AngularServices/npm/build.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ builder.config({
1212
'angular2-aspnet/*': 'dist/*'
1313
},
1414
meta: {
15-
'angular2/*': { build: false }
15+
'angular2/*': { build: false },
16+
'@reactivex/*': { build: false }
1617
}
1718
});
1819

Microsoft.AspNet.AngularServices/npm/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "angular2-aspnet",
3-
"version": "0.0.1",
3+
"version": "0.0.3",
44
"description": "Helpers for Angular 2 apps built on ASP.NET",
55
"main": "./dist/Exports",
66
"scripts": {
@@ -15,7 +15,7 @@
1515
"author": "Microsoft",
1616
"license": "Apache-2.0",
1717
"peerDependencies": {
18-
"angular2": "^2.0.0-alpha.44"
18+
"angular2": "2.0.0-alpha.44"
1919
},
2020
"devDependencies": {
2121
"systemjs-builder": "^0.14.11",
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { provide, Injectable, Provider } from 'angular2/core';
2+
import { Connection, ConnectionBackend, Http, XHRBackend, RequestOptions, Request, RequestMethods, Response, ResponseOptions, ReadyStates } from 'angular2/http';
3+
4+
@Injectable()
5+
export class CachePrimedConnectionBackend extends ConnectionBackend {
6+
private _preCachedResponses: PreCachedResponses;
7+
8+
constructor(private _underlyingBackend: ConnectionBackend, private _baseResponseOptions: ResponseOptions) {
9+
super();
10+
this._preCachedResponses = (<any>window).__preCachedResponses || {};
11+
}
12+
13+
public createConnection(request: Request): Connection {
14+
let cacheKey = request.url;
15+
if (request.method === RequestMethods.Get && this._preCachedResponses.hasOwnProperty(cacheKey)) {
16+
return new CacheHitConnection(request, this._preCachedResponses[cacheKey], this._baseResponseOptions);
17+
} else {
18+
return this._underlyingBackend.createConnection(request);
19+
}
20+
}
21+
}
22+
23+
class CacheHitConnection implements Connection {
24+
readyState: ReadyStates;
25+
request: Request;
26+
response: any;
27+
28+
constructor (req: Request, cachedResponse: PreCachedResponse, baseResponseOptions: ResponseOptions) {
29+
this.request = req;
30+
this.readyState = ReadyStates.Done;
31+
32+
// Workaround for difficulty consuming CommonJS default exports in TypeScript. Note that it has to be a dynamic
33+
// 'require', and not an 'import' statement, because the module isn't available on the server.
34+
// All this badness goes away with the next update of Angular 2, as it exposes Observable directly from angular2/core.
35+
// --
36+
// The current version of Angular exposes the following SystemJS module directly (it is *not* coming from the
37+
// @reactivex/rxjs NPM package - it's coming from angular2).
38+
let obsCtor: any = require('@reactivex/rxjs/dist/cjs/Observable');
39+
this.response = new obsCtor(responseObserver => {
40+
let response = new Response(new ResponseOptions({ body: cachedResponse.body, status: cachedResponse.statusCode }));
41+
responseObserver.next(response);
42+
responseObserver.complete();
43+
});
44+
}
45+
}
46+
47+
declare var require: any; // Part of the workaround mentioned below. Can remove this after updating Angular.
48+
49+
interface PreCachedResponses {
50+
[url: string]: PreCachedResponse;
51+
}
52+
53+
interface PreCachedResponse {
54+
statusCode: number;
55+
body: string;
56+
}
57+
58+
export const CACHE_PRIMED_HTTP_PROVIDERS = [
59+
provide(Http, {
60+
useFactory: (xhrBackend, requestOptions, responseOptions) => new Http(new CachePrimedConnectionBackend(xhrBackend, responseOptions), requestOptions),
61+
deps: [XHRBackend, RequestOptions, ResponseOptions]
62+
}),
63+
];
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
export * from './CachePrimedHttp';
12
export * from './Validation';

Microsoft.AspNet.AngularServices/npm/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"target": "es5",
55
"sourceMap": false,
66
"declaration": true,
7+
"experimentalDecorators": true,
78
"noLib": false,
89
"outDir": "./dist"
910
},

Microsoft.AspNet.AngularServices/project.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"Microsoft.CSharp": "4.0.1-beta-*",
2020
"System.Collections": "4.0.11-beta-*",
2121
"System.Linq": "4.0.1-beta-*",
22+
"System.Net.Http": "4.0.1-beta-*",
2223
"System.Runtime": "4.0.21-beta-*",
2324
"System.Threading": "4.0.11-beta-*"
2425
}

0 commit comments

Comments
 (0)