diff --git a/Microsoft.AspNet.AngularServices/PrimeCacheHelper.cs b/Microsoft.AspNet.AngularServices/PrimeCacheHelper.cs new file mode 100644 index 0000000..a0f8859 --- /dev/null +++ b/Microsoft.AspNet.AngularServices/PrimeCacheHelper.cs @@ -0,0 +1,44 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace Microsoft.AspNet.AngularServices { + public static class PrimeCacheHelper { + public static async Task PrimeCache(this IHtmlHelper html, string url) { + // TODO: Consider deduplicating the PrimeCache calls (that is, if there are multiple requests to precache + // the same URL, only return nonempty for one of them). This will make it easier to auto-prime-cache any + // HTTP requests made during server-side rendering, without risking unnecessary duplicate requests. + + if (string.IsNullOrEmpty(url)) { + throw new ArgumentException("Value cannot be null or empty", "url"); + } + + try { + var request = html.ViewContext.HttpContext.Request; + var baseUri = new Uri(string.Concat(request.Scheme, "://", request.Host.ToUriComponent(), request.PathBase.ToUriComponent(), request.Path.ToUriComponent(), request.QueryString.ToUriComponent())); + var fullUri = new Uri(baseUri, url); + var response = await new HttpClient().GetAsync(fullUri.ToString()); + var responseBody = await response.Content.ReadAsStringAsync(); + return new HtmlString(FormatAsScript(url, response.StatusCode, responseBody)); + } catch (Exception ex) { + var logger = (ILogger)html.ViewContext.HttpContext.ApplicationServices.GetService(typeof (ILogger)); + if (logger != null) { + logger.LogWarning("Error priming cache for URL: " + url, ex); + } + return new HtmlString(string.Empty); + } + } + + private static string FormatAsScript(string url, HttpStatusCode responseStatusCode, string responseBody) + { + return string.Format(@"", + JsonConvert.SerializeObject(url), + JsonConvert.SerializeObject(new { statusCode = responseStatusCode, body = responseBody }) + ); + } + } +} \ No newline at end of file diff --git a/Microsoft.AspNet.AngularServices/npm/build.js b/Microsoft.AspNet.AngularServices/npm/build.js index 2a55704..f8fd313 100644 --- a/Microsoft.AspNet.AngularServices/npm/build.js +++ b/Microsoft.AspNet.AngularServices/npm/build.js @@ -12,7 +12,8 @@ builder.config({ 'angular2-aspnet/*': 'dist/*' }, meta: { - 'angular2/*': { build: false } + 'angular2/*': { build: false }, + '@reactivex/*': { build: false } } }); diff --git a/Microsoft.AspNet.AngularServices/npm/package.json b/Microsoft.AspNet.AngularServices/npm/package.json index 8709432..ec35af7 100644 --- a/Microsoft.AspNet.AngularServices/npm/package.json +++ b/Microsoft.AspNet.AngularServices/npm/package.json @@ -1,6 +1,6 @@ { "name": "angular2-aspnet", - "version": "0.0.1", + "version": "0.0.3", "description": "Helpers for Angular 2 apps built on ASP.NET", "main": "./dist/Exports", "scripts": { @@ -15,7 +15,7 @@ "author": "Microsoft", "license": "Apache-2.0", "peerDependencies": { - "angular2": "^2.0.0-alpha.44" + "angular2": "2.0.0-alpha.44" }, "devDependencies": { "systemjs-builder": "^0.14.11", diff --git a/Microsoft.AspNet.AngularServices/npm/src/CachePrimedHttp.ts b/Microsoft.AspNet.AngularServices/npm/src/CachePrimedHttp.ts new file mode 100644 index 0000000..e72f962 --- /dev/null +++ b/Microsoft.AspNet.AngularServices/npm/src/CachePrimedHttp.ts @@ -0,0 +1,63 @@ +import { provide, Injectable, Provider } from 'angular2/core'; +import { Connection, ConnectionBackend, Http, XHRBackend, RequestOptions, Request, RequestMethods, Response, ResponseOptions, ReadyStates } from 'angular2/http'; + +@Injectable() +export class CachePrimedConnectionBackend extends ConnectionBackend { + private _preCachedResponses: PreCachedResponses; + + constructor(private _underlyingBackend: ConnectionBackend, private _baseResponseOptions: ResponseOptions) { + super(); + this._preCachedResponses = (window).__preCachedResponses || {}; + } + + public createConnection(request: Request): Connection { + let cacheKey = request.url; + if (request.method === RequestMethods.Get && this._preCachedResponses.hasOwnProperty(cacheKey)) { + return new CacheHitConnection(request, this._preCachedResponses[cacheKey], this._baseResponseOptions); + } else { + return this._underlyingBackend.createConnection(request); + } + } +} + +class CacheHitConnection implements Connection { + readyState: ReadyStates; + request: Request; + response: any; + + constructor (req: Request, cachedResponse: PreCachedResponse, baseResponseOptions: ResponseOptions) { + this.request = req; + this.readyState = ReadyStates.Done; + + // Workaround for difficulty consuming CommonJS default exports in TypeScript. Note that it has to be a dynamic + // 'require', and not an 'import' statement, because the module isn't available on the server. + // All this badness goes away with the next update of Angular 2, as it exposes Observable directly from angular2/core. + // -- + // The current version of Angular exposes the following SystemJS module directly (it is *not* coming from the + // @reactivex/rxjs NPM package - it's coming from angular2). + let obsCtor: any = require('@reactivex/rxjs/dist/cjs/Observable'); + this.response = new obsCtor(responseObserver => { + let response = new Response(new ResponseOptions({ body: cachedResponse.body, status: cachedResponse.statusCode })); + responseObserver.next(response); + responseObserver.complete(); + }); + } +} + +declare var require: any; // Part of the workaround mentioned below. Can remove this after updating Angular. + +interface PreCachedResponses { + [url: string]: PreCachedResponse; +} + +interface PreCachedResponse { + statusCode: number; + body: string; +} + +export const CACHE_PRIMED_HTTP_PROVIDERS = [ + provide(Http, { + useFactory: (xhrBackend, requestOptions, responseOptions) => new Http(new CachePrimedConnectionBackend(xhrBackend, responseOptions), requestOptions), + deps: [XHRBackend, RequestOptions, ResponseOptions] + }), +]; diff --git a/Microsoft.AspNet.AngularServices/npm/src/Exports.ts b/Microsoft.AspNet.AngularServices/npm/src/Exports.ts index 897995d..6ef8fb3 100644 --- a/Microsoft.AspNet.AngularServices/npm/src/Exports.ts +++ b/Microsoft.AspNet.AngularServices/npm/src/Exports.ts @@ -1 +1,2 @@ +export * from './CachePrimedHttp'; export * from './Validation'; diff --git a/Microsoft.AspNet.AngularServices/npm/tsconfig.json b/Microsoft.AspNet.AngularServices/npm/tsconfig.json index c586e45..ecbeabd 100644 --- a/Microsoft.AspNet.AngularServices/npm/tsconfig.json +++ b/Microsoft.AspNet.AngularServices/npm/tsconfig.json @@ -4,6 +4,7 @@ "target": "es5", "sourceMap": false, "declaration": true, + "experimentalDecorators": true, "noLib": false, "outDir": "./dist" }, diff --git a/Microsoft.AspNet.AngularServices/project.json b/Microsoft.AspNet.AngularServices/project.json index 01fbeb6..8fca29b 100644 --- a/Microsoft.AspNet.AngularServices/project.json +++ b/Microsoft.AspNet.AngularServices/project.json @@ -19,6 +19,7 @@ "Microsoft.CSharp": "4.0.1-beta-*", "System.Collections": "4.0.11-beta-*", "System.Linq": "4.0.1-beta-*", + "System.Net.Http": "4.0.1-beta-*", "System.Runtime": "4.0.21-beta-*", "System.Threading": "4.0.11-beta-*" }