UNPKG

@spartacus/setup

Version:

Includes features that makes Spartacus and it's setup easier and streamlined.

442 lines (432 loc) 19.5 kB
import * as fs from 'fs'; import { REQUEST } from '@nguniversal/express-engine/tokens'; import { SERVER_REQUEST_URL, SERVER_REQUEST_ORIGIN } from '@spartacus/core'; function getRequestOrigin(req) { // If express is resolving and trusting X-Forwarded-Host, we want to take it // into an account to properly generate request origin. const trustProxyFn = req.app.get('trust proxy fn'); let forwardedHost = req.get('X-Forwarded-Host'); if (forwardedHost && trustProxyFn(req.connection.remoteAddress, 0)) { if (forwardedHost.indexOf(',') !== -1) { // Note: X-Forwarded-Host is normally only ever a // single value, but this is to be safe. forwardedHost = forwardedHost .substring(0, forwardedHost.indexOf(',')) .trimRight(); } return req.protocol + '://' + forwardedHost; } else { return req.protocol + '://' + req.get('host'); } } function getRequestUrl(req) { return getRequestOrigin(req) + req.originalUrl; } class RenderingCache { constructor(options) { this.options = options; this.renders = new Map(); } setAsRendering(key) { this.renders.set(key, { rendering: true }); } isRendering(key) { var _a; return !!((_a = this.renders.get(key)) === null || _a === void 0 ? void 0 : _a.rendering); } store(key, err, html) { var _a, _b; const entry = { err, html }; if ((_a = this.options) === null || _a === void 0 ? void 0 : _a.ttl) { entry.time = Date.now(); } if ((_b = this.options) === null || _b === void 0 ? void 0 : _b.cacheSize) { this.renders.delete(key); if (this.renders.size >= this.options.cacheSize) { this.renders.delete(this.renders.keys().next().value); } } this.renders.set(key, entry); } get(key) { return this.renders.get(key); } clear(key) { this.renders.delete(key); } isReady(key) { const entry = this.renders.get(key); const isRenderPresent = (entry === null || entry === void 0 ? void 0 : entry.html) || (entry === null || entry === void 0 ? void 0 : entry.err); return isRenderPresent && this.isFresh(key); } isFresh(key) { var _a, _b, _c, _d; if (!((_a = this.options) === null || _a === void 0 ? void 0 : _a.ttl)) { return true; } return Date.now() - ((_c = (_b = this.renders.get(key)) === null || _b === void 0 ? void 0 : _b.time) !== null && _c !== void 0 ? _c : 0) < ((_d = this.options) === null || _d === void 0 ? void 0 : _d.ttl); } } var RenderingStrategy; (function (RenderingStrategy) { RenderingStrategy[RenderingStrategy["ALWAYS_CSR"] = -1] = "ALWAYS_CSR"; RenderingStrategy[RenderingStrategy["DEFAULT"] = 0] = "DEFAULT"; RenderingStrategy[RenderingStrategy["ALWAYS_SSR"] = 1] = "ALWAYS_SSR"; })(RenderingStrategy || (RenderingStrategy = {})); /** * Returns the full url for the given SSR Request. */ const getDefaultRenderKey = getRequestUrl; /** * The rendered pages are kept in memory to be served on next request. If the `cache` is set to `false`, the * response is evicted as soon as the first successful response is successfully returned. */ class OptimizedSsrEngine { constructor(expressEngine, ssrOptions) { this.expressEngine = expressEngine; this.ssrOptions = ssrOptions; this.currentConcurrency = 0; this.renderingCache = new RenderingCache(this.ssrOptions); this.templateCache = new Map(); /** * When the config `reuseCurrentRendering` is enabled, we want perform * only one render for one rendering key and reuse the html result * for all the pending requests for the same rendering key. * Therefore we need to store the callbacks for all the pending requests * and invoke them with the html after the render completes. * * This Map should be used only when `reuseCurrentRendering` config is enabled. * It's indexed by the rendering keys. */ this.renderCallbacks = new Map(); } get engineInstance() { return this.renderResponse.bind(this); } /** * When SSR page can not be returned in time, we're returning index.html of * the CSR application. * The CSR application is returned with the "Cache-Control: no-store" response-header. This notifies external cache systems to not use the CSR application for the subsequent request. */ fallbackToCsr(response, filePath, callback) { response.set('Cache-Control', 'no-store'); callback(undefined, this.getDocument(filePath)); } getRenderingKey(request) { var _a; return ((_a = this.ssrOptions) === null || _a === void 0 ? void 0 : _a.renderKeyResolver) ? this.ssrOptions.renderKeyResolver(request) : getDefaultRenderKey(request); } getRenderingStrategy(request) { var _a; return ((_a = this.ssrOptions) === null || _a === void 0 ? void 0 : _a.renderingStrategyResolver) ? this.ssrOptions.renderingStrategyResolver(request) : RenderingStrategy.DEFAULT; } /** * When returns true, the server side rendering should be performed. * When returns false, the CSR fallback should be returned. * * We should not render, when there is already * a pending rendering for the same rendering key * (unless the `reuseCurrentRendering` config option is enabled) * OR when the concurrency limit is exceeded. */ shouldRender(request) { var _a, _b; const renderingKey = this.getRenderingKey(request); const concurrencyLimitExceeded = this.isConcurrencyLimitExceeded(renderingKey); const fallBack = this.renderingCache.isRendering(renderingKey) && !((_a = this.ssrOptions) === null || _a === void 0 ? void 0 : _a.reuseCurrentRendering); if (fallBack) { this.log(`CSR fallback: rendering in progress (${request === null || request === void 0 ? void 0 : request.originalUrl})`); } else if (concurrencyLimitExceeded) { this.log(`CSR fallback: Concurrency limit exceeded (${(_b = this.ssrOptions) === null || _b === void 0 ? void 0 : _b.concurrency})`); } return ((!fallBack && !concurrencyLimitExceeded && this.getRenderingStrategy(request) !== RenderingStrategy.ALWAYS_CSR) || this.getRenderingStrategy(request) === RenderingStrategy.ALWAYS_SSR); } /** * Checks for the concurrency limit * * @returns true if rendering this request would exceed the concurrency limit */ isConcurrencyLimitExceeded(renderingKey) { var _a, _b; // If we can reuse a pending render for this request, we don't take up a new concurrency slot. // In that case we don't exceed the concurrency limit even if the `currentConcurrency` // already reaches the limit. if (((_a = this.ssrOptions) === null || _a === void 0 ? void 0 : _a.reuseCurrentRendering) && this.renderingCache.isRendering(renderingKey)) { return false; } return ((_b = this.ssrOptions) === null || _b === void 0 ? void 0 : _b.concurrency) ? this.currentConcurrency >= this.ssrOptions.concurrency : false; } /** * Returns true, when the `timeout` option has been configured to non-zero value OR * when the rendering strategy for the given request is ALWAYS_SSR. * Otherwise, it returns false. */ shouldTimeout(request) { var _a; return (!!((_a = this.ssrOptions) === null || _a === void 0 ? void 0 : _a.timeout) || this.getRenderingStrategy(request) === RenderingStrategy.ALWAYS_SSR); } /** * Returns the timeout value. * * In case of the rendering strategy ALWAYS_SSR, it returns the config `forcedSsrTimeout`. * Otherwise, it returns the config `timeout`. */ getTimeout(request) { var _a, _b, _c, _d; return this.getRenderingStrategy(request) === RenderingStrategy.ALWAYS_SSR ? (_b = (_a = this.ssrOptions) === null || _a === void 0 ? void 0 : _a.forcedSsrTimeout) !== null && _b !== void 0 ? _b : 60000 : (_d = (_c = this.ssrOptions) === null || _c === void 0 ? void 0 : _c.timeout) !== null && _d !== void 0 ? _d : 0; } /** * If there is an available cached response for this rendering key, * it invokes the given render callback with the response and returns true. * * Otherwise, it returns false. */ returnCachedRender(request, callback) { var _a; const key = this.getRenderingKey(request); if (this.renderingCache.isReady(key)) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const cached = this.renderingCache.get(key); callback(cached.err, cached.html); if (!((_a = this.ssrOptions) === null || _a === void 0 ? void 0 : _a.cache)) { // we drop cached rendering if caching is disabled this.renderingCache.clear(key); } return true; } return false; } /** * Handles the request and invokes the given `callback` with the result html / error. * * The result might be ether: * - a CSR fallback with a basic `index.html` content * - a result rendered by the original Angular Universal express engine * - a result from the in-memory cache (which was previously rendered by Angular Universal express engine). */ renderResponse(filePath, options, callback) { const request = options.req; const response = options.res || options.req.res; if (this.returnCachedRender(request, callback)) { this.log(`Render from cache (${request === null || request === void 0 ? void 0 : request.originalUrl})`); return; } if (!this.shouldRender(request)) { this.fallbackToCsr(response, filePath, callback); return; } let requestTimeout; if (this.shouldTimeout(request)) { // establish timeout for rendering const timeout = this.getTimeout(request); requestTimeout = setTimeout(() => { requestTimeout = undefined; this.fallbackToCsr(response, filePath, callback); this.log(`SSR rendering exceeded timeout ${timeout}, fallbacking to CSR for ${request === null || request === void 0 ? void 0 : request.originalUrl}`, false); }, timeout); } else { // Here we respond with the fallback to CSR, but we don't `return`. // We let the actual rendering task to happen in the background // to eventually store the rendered result in the cache. this.fallbackToCsr(response, filePath, callback); } const renderingKey = this.getRenderingKey(request); const renderCallback = (err, html) => { var _a; if (requestTimeout) { // if request is still waiting for render, return it clearTimeout(requestTimeout); callback(err, html); this.log(`Request is resolved with the SSR rendering result (${request === null || request === void 0 ? void 0 : request.originalUrl})`); // store the render only if caching is enabled if ((_a = this.ssrOptions) === null || _a === void 0 ? void 0 : _a.cache) { this.renderingCache.store(renderingKey, err, html); } else { this.renderingCache.clear(renderingKey); } } else { // store the render for future use this.renderingCache.store(renderingKey, err, html); } }; this.handleRender({ filePath, options, renderCallback, request, }); } log(message, debug = true) { var _a; if (!debug || ((_a = this.ssrOptions) === null || _a === void 0 ? void 0 : _a.debug)) { console.log(message); } } /** Retrieve the document from the cache or the filesystem */ getDocument(filePath) { let doc = this.templateCache.get(filePath); if (!doc) { // fs.readFileSync could be missing in a browser, specifically // in a unit tests with { node: { fs: 'empty' } } webpack configuration doc = (fs === null || fs === void 0 ? void 0 : fs.readFileSync) ? fs.readFileSync(filePath, 'utf-8') : ''; this.templateCache.set(filePath, doc); } return doc; } /** * Delegates the render to the original _Angular Universal express engine_. * * In case when the config `reuseCurrentRendering` is enabled and **if there is already a pending * render task for the same rendering key**, it doesn't delegate a new render to Angular Universal. * Instead, it waits for the current rendering to complete and then reuse the result for all waiting requests. */ handleRender({ filePath, options, renderCallback, request, }) { var _a, _b; if (!((_a = this.ssrOptions) === null || _a === void 0 ? void 0 : _a.reuseCurrentRendering)) { this.startRender({ filePath, options, renderCallback, request, }); return; } const renderingKey = this.getRenderingKey(request); if (!this.renderCallbacks.has(renderingKey)) { this.renderCallbacks.set(renderingKey, []); } (_b = this.renderCallbacks.get(renderingKey)) === null || _b === void 0 ? void 0 : _b.push(renderCallback); if (!this.renderingCache.isRendering(renderingKey)) { this.startRender({ filePath, options, request, renderCallback: (err, html) => { // Share the result of the render with all awaiting requests for the same key: var _a; // Note: we access the Map at the moment of the render finished (don't store value in a local variable), // because in the meantime something might have deleted the value (i.e. when `maxRenderTime` passed). (_a = this.renderCallbacks .get(renderingKey)) === null || _a === void 0 ? void 0 : _a.forEach((cb) => cb(err, html)); // pass the shared result to all waiting rendering callbacks this.renderCallbacks.delete(renderingKey); }, }); } this.log(`Request is waiting for the SSR rendering to complete (${request === null || request === void 0 ? void 0 : request.originalUrl})`); } /** * Delegates the render to the original _Angular Universal express engine_. * * There is no way to abort the running render of Angular Universal. * So if the render doesn't complete in the configured `maxRenderTime`, * we just consider the render task as hanging (note: it's a potential memory leak!). * Later on, even if the render completes somewhen in the future, we will ignore * its result. */ startRender({ filePath, options, renderCallback, request, }) { var _a, _b; const renderingKey = this.getRenderingKey(request); // Setting the timeout for hanging renders that might not ever finish due to various reasons. // After the configured `maxRenderTime` passes, we consider the rendering task as hanging, // and release the concurrency slot and forget all callbacks waiting for the render's result. let maxRenderTimeout = setTimeout(() => { var _a; this.renderingCache.clear(renderingKey); maxRenderTimeout = undefined; this.currentConcurrency--; if ((_a = this.ssrOptions) === null || _a === void 0 ? void 0 : _a.reuseCurrentRendering) { this.renderCallbacks.delete(renderingKey); } this.log(`Rendering of ${request === null || request === void 0 ? void 0 : request.originalUrl} was not able to complete. This might cause memory leaks!`, false); }, (_b = (_a = this.ssrOptions) === null || _a === void 0 ? void 0 : _a.maxRenderTime) !== null && _b !== void 0 ? _b : 300000); // 300000ms == 5 minutes this.log(`Rendering started (${request === null || request === void 0 ? void 0 : request.originalUrl})`); this.renderingCache.setAsRendering(renderingKey); this.currentConcurrency++; this.expressEngine(filePath, options, (err, html) => { if (!maxRenderTimeout) { // ignore this render's result because it exceeded maxRenderTimeout this.log(`Rendering of ${request.originalUrl} completed after the specified maxRenderTime, therefore it was ignored.`, false); return; } clearTimeout(maxRenderTimeout); this.log(`Rendering completed (${request === null || request === void 0 ? void 0 : request.originalUrl})`); this.currentConcurrency--; renderCallback(err, html); }); } } /** * Returns Spartacus providers to be passed to the Angular express engine (in SSR) * * @param options */ function getServerRequestProviders() { return [ { provide: SERVER_REQUEST_URL, useFactory: getRequestUrl, deps: [REQUEST], }, { provide: SERVER_REQUEST_ORIGIN, useFactory: getRequestOrigin, deps: [REQUEST], }, ]; } /** * The wrapper over the standard ngExpressEngine, that provides tokens for Spartacus * @param ngExpressEngine */ class NgExpressEngineDecorator { /** * Returns the higher order ngExpressEngine with provided tokens for Spartacus * * @param ngExpressEngine */ static get(ngExpressEngine, optimizationOptions) { return decorateExpressEngine(ngExpressEngine, optimizationOptions); } } function decorateExpressEngine(ngExpressEngine, optimizationOptions = { concurrency: 20, timeout: 3000, }) { return function (setupOptions) { var _a; const engineInstance = ngExpressEngine(Object.assign(Object.assign({}, setupOptions), { providers: [ // add spartacus related providers ...getServerRequestProviders(), ...((_a = setupOptions.providers) !== null && _a !== void 0 ? _a : []), ] })); // apply optimization wrapper if optimization options were defined return optimizationOptions ? new OptimizedSsrEngine(engineInstance, optimizationOptions) .engineInstance : engineInstance; }; } /** * Generated bundle index. Do not edit. */ export { NgExpressEngineDecorator, OptimizedSsrEngine, RenderingCache, RenderingStrategy, getDefaultRenderKey, getServerRequestProviders }; //# sourceMappingURL=spartacus-setup-ssr.js.map