UNPKG

@akanass/ng-universal-transfer-http

Version:

TransferHttpCacheModule installs a Http interceptor that avoids duplicate HttpClient requests on the client

791 lines (785 loc) 23.4 kB
import { InjectionToken, Injectable, Optional, Inject, ApplicationRef, PLATFORM_ID, NgModule } from '@angular/core'; import { HttpResponse, HttpHeaders, HttpErrorResponse, HTTP_INTERCEPTORS } from '@angular/common/http'; import { makeStateKey, TransferState, BrowserTransferStateModule } from '@angular/platform-browser'; import { isPlatformServer } from '@angular/common'; import createHash from 'create-hash'; import { stringify } from 'flatted/esm'; import { of, merge, from, throwError } from 'rxjs'; import { flatMap, filter, tap, first, map, defaultIfEmpty, toArray } from 'rxjs/operators'; /** * @fileoverview added by tsickle * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** @type {?} */ const NG_UNIVERSAL_TRANSFER_HTTP_CONFIG = new InjectionToken('ng_universal_transfer_http_config'); /** * @fileoverview added by tsickle * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class TransferHttpCacheConfigService { /** * Class constructor * @param {?} _transferHttpCacheConfig */ constructor(_transferHttpCacheConfig) { this._transferHttpCacheConfig = _transferHttpCacheConfig; this._config = { prodMode: true }; if (this._transferHttpCacheConfig !== null) { Object.assign(this._config, this._transferHttpCacheConfig); } } /** * Returns private property _config * @return {?} */ get config() { return this._config; } } TransferHttpCacheConfigService.decorators = [ { type: Injectable } ]; /** @nocollapse */ TransferHttpCacheConfigService.ctorParameters = () => [ { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [NG_UNIVERSAL_TRANSFER_HTTP_CONFIG,] }] } ]; /** * @fileoverview added by tsickle * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class TransferHttpCacheInterceptor { /** * Class constructor * @param {?} _appRef * @param {?} _transferState * @param {?} _platformId * @param {?} _configService */ constructor(_appRef, _transferState, _platformId, _configService) { this._appRef = _appRef; this._transferState = _transferState; this._platformId = _platformId; this._configService = _configService; this._id = 0; this._serverStateDataStoreKey = makeStateKey('server_state_data'); this._lastIdStoreKey = makeStateKey('server_state_last_id'); this._isCacheActivatedStoreKey = makeStateKey('is_cache_activated'); this._initCacheProcess(); } /** * Initialize cache process * @private * @return {?} */ _initCacheProcess() { // initialize cache flag for the current platform of(of(isPlatformServer(this._platformId))) .pipe(flatMap((/** * @param {?} isServer * @return {?} */ (isServer) => merge(isServer .pipe(filter((/** * @param {?} _ * @return {?} */ _ => !!_)), tap((/** * @param {?} _ * @return {?} */ _ => this._isCacheActivated = _)), tap((/** * @param {?} _ * @return {?} */ _ => this._transferState.set(this._isCacheActivatedStoreKey, _)))), isServer .pipe(filter((/** * @param {?} _ * @return {?} */ _ => !_)), tap((/** * @param {?} _ * @return {?} */ _ => this._transferState.hasKey(this._isCacheActivatedStoreKey) ? this._isCacheActivated = this._transferState.get(this._isCacheActivatedStoreKey, true) : this._isCacheActivated = _)))))), flatMap((/** * @return {?} */ () => // Stop using the cache if the application has stabilized, indicating initial rendering is complete // or if we are in development mode. merge(of(this._configService.config.prodMode) .pipe(filter((/** * @param {?} _ * @return {?} */ _ => !_)), tap((/** * @return {?} */ () => console.log('TransferHttpCacheModule is in the development mode. ' + 'Enable the production mode with Server Side Rendering.')))), this._appRef.isStable .pipe(filter((/** * @param {?} _ * @return {?} */ _ => !!_)))) .pipe(first())))).subscribe((/** * @return {?} */ () => this._isCacheActivated = false)); } /** * Interceptor process * @param {?} req * @param {?} next * @return {?} */ intercept(req, next) { return of(of(this._isCacheActivated)) .pipe(flatMap((/** * @param {?} isCacheActivated * @return {?} */ (isCacheActivated) => merge(isCacheActivated .pipe(filter((/** * @param {?} _ * @return {?} */ _ => !_)), flatMap((/** * @return {?} */ () => next.handle(req) .pipe(tap((/** * @return {?} */ () => this._cleanServerState())))))), isCacheActivated .pipe(filter((/** * @param {?} _ * @return {?} */ _ => !!_)), flatMap((/** * @return {?} */ () => this._transferStateProcess(req, next)))))))); } /** * Function to clean all data in server state * @private * @return {?} */ _cleanServerState() { merge(this._getLastId(false) .pipe(tap((/** * @return {?} */ () => this._transferState.remove(this._lastIdStoreKey)))), this._getServerStateData(false) .pipe(tap((/** * @param {?} _ * @return {?} */ _ => _.forEach((/** * @param {?} __ * @return {?} */ __ => this._transferState.remove(makeStateKey(this._createHash(`${__.reqKey}_${__.id}`))))))), tap((/** * @return {?} */ () => this._transferState.remove(this._serverStateDataStoreKey)))), of(of(this._transferState.hasKey(this._isCacheActivatedStoreKey))) .pipe(flatMap((/** * @param {?} hasKey * @return {?} */ (hasKey) => hasKey .pipe(filter((/** * @param {?} _ * @return {?} */ _ => !!_)), tap((/** * @return {?} */ () => this._transferState.remove(this._isCacheActivatedStoreKey)))))))) .subscribe((/** * @return {?} */ () => undefined), (/** * @param {?} e * @return {?} */ e => { throw (e); })); } /** * Transfer state process * @private * @param {?} req * @param {?} next * @return {?} */ _transferStateProcess(req, next) { return this._createKey(req) .pipe(flatMap((/** * @param {?} storeKey * @return {?} */ storeKey => of(of(this._transferState.hasKey(storeKey))) .pipe(flatMap((/** * @param {?} hasKey * @return {?} */ hasKey => merge(this._hasKeyProcess(hasKey, storeKey), this._hasNotKeyProcess(req, next, hasKey, storeKey)))))))); } /** * Creates transfer state key's store * @private * @param {?} req * @return {?} */ _createKey(req) { this._id++; return of(of(isPlatformServer(this._platformId))) .pipe(flatMap((/** * @param {?} isServer * @return {?} */ (isServer) => merge(isServer .pipe(filter((/** * @param {?} _ * @return {?} */ _ => !!_)), flatMap((/** * @return {?} */ () => this._serverKey(req)))), isServer .pipe(filter((/** * @param {?} _ * @return {?} */ _ => !_)), flatMap((/** * @return {?} */ () => this._clientKey(req)))))))); } /** * Function to get state data and create client key for current request * @private * @param {?} req * @return {?} */ _clientKey(req) { return this._requestFormatted(req) .pipe(map((/** * @param {?} _ * @return {?} */ (_) => this._createHash(stringify(_)))), flatMap((/** * @param {?} reqKey * @return {?} */ reqKey => this._getServerStateData() .pipe(flatMap((/** * @param {?} _ * @return {?} */ _ => from(_))), filter((/** * @param {?} _ * @return {?} */ _ => _.reqKey === reqKey)), defaultIfEmpty(undefined), flatMap((/** * @param {?} _ * @return {?} */ _ => !!_ ? of(_) : throwError(new Error('Request missing in server state data')))), flatMap((/** * @param {?} serverState * @return {?} */ serverState => this._getLastId() .pipe(flatMap((/** * @param {?} _ * @return {?} */ _ => !_ || this._id > _ ? throwError(new Error('Wrong id for server state data')) : of(this._id))), map((/** * @param {?} _ * @return {?} */ _ => _ === serverState.id ? _ : serverState.id)), map((/** * @param {?} id * @return {?} */ id => this._createHash(`${reqKey}_${id}`))), map((/** * @param {?} key * @return {?} */ key => makeStateKey(key)))))))))); } /** * Function to get last id from server * @private * @param {?=} _throwError * @return {?} */ _getLastId(_throwError = true) { return of(this._lastIdStoreKey) .pipe(flatMap((/** * @param {?} storeKey * @return {?} */ storeKey => of(of(this._transferState.hasKey(storeKey))) .pipe(flatMap((/** * @param {?} hasKey * @return {?} */ hasKey => merge(hasKey .pipe(filter((/** * @param {?} _ * @return {?} */ _ => !_)), flatMap((/** * @return {?} */ () => of(_throwError) .pipe(filter((/** * @param {?} __ * @return {?} */ __ => !!__)), flatMap((/** * @return {?} */ () => throwError(new Error('Missing server state last id')))))))), hasKey .pipe(filter((/** * @param {?} _ * @return {?} */ _ => !!_)), map((/** * @return {?} */ () => this._transferState.get(storeKey, 0))))))))))); } /** * Function to get server state data * @private * @param {?=} _throwError * @return {?} */ _getServerStateData(_throwError = true) { return of(this._serverStateDataStoreKey) .pipe(flatMap((/** * @param {?} storeKey * @return {?} */ storeKey => of(of(this._transferState.hasKey(storeKey))) .pipe(flatMap((/** * @param {?} hasKey * @return {?} */ hasKey => merge(hasKey .pipe(filter((/** * @param {?} _ * @return {?} */ _ => !_)), flatMap((/** * @return {?} */ () => of(_throwError) .pipe(filter((/** * @param {?} __ * @return {?} */ __ => !!__)), flatMap((/** * @return {?} */ () => throwError(new Error('Missing server state data')))))))), hasKey .pipe(filter((/** * @param {?} _ * @return {?} */ _ => !!_)), map((/** * @return {?} */ () => this._transferState.get(storeKey, (/** @type {?} */ ([]))))))))))))); } /** * Function to create server key and store state data for current request * @private * @param {?} req * @return {?} */ _serverKey(req) { return this._requestFormatted(req) .pipe(map((/** * @param {?} _ * @return {?} */ (_) => this._createHash(stringify(_)))), tap((/** * @param {?} reqKey * @return {?} */ reqKey => this._storeServerStateData(reqKey))), map((/** * @param {?} reqKey * @return {?} */ reqKey => this._createHash(`${reqKey}_${this._id}`))), map((/** * @param {?} key * @return {?} */ key => makeStateKey(key)))); } /** * Function to store server state data * @private * @param {?} reqKey * @return {?} */ _storeServerStateData(reqKey) { of(this._serverStateDataStoreKey) .pipe(flatMap((/** * @param {?} storeKey * @return {?} */ storeKey => of(of(this._transferState.hasKey(storeKey))) .pipe(flatMap((/** * @param {?} hasKey * @return {?} */ hasKey => merge(hasKey .pipe(filter((/** * @param {?} _ * @return {?} */ _ => !_)), map((/** * @return {?} */ () => (/** @type {?} */ ([]))))), hasKey .pipe(filter((/** * @param {?} _ * @return {?} */ _ => !!_)), map((/** * @return {?} */ () => this._transferState.get(storeKey, (/** @type {?} */ ([]))))), flatMap((/** * @param {?} serverStateData * @return {?} */ serverStateData => !!serverStateData.find((/** * @param {?} _ * @return {?} */ _ => _.reqKey === reqKey)) ? throwError(new Error('Request already stored in server state data')) : of(serverStateData))))))), tap((/** * @return {?} */ () => this._transferState.set(this._lastIdStoreKey, this._id))))))) .subscribe((/** * @param {?} serverStateData * @return {?} */ serverStateData => this._transferState.set(this._serverStateDataStoreKey, serverStateData.concat({ id: this._id, reqKey }))), (/** * @param {?} e * @return {?} */ e => { throw (e); })); } /** * Process when key exists in transfer state * @private * @param {?} hasKey * @param {?} storeKey * @return {?} */ _hasKeyProcess(hasKey, storeKey) { return hasKey .pipe(filter((/** * @param {?} _ * @return {?} */ _ => !!_)), map((/** * @return {?} */ () => of(this._transferState.get(storeKey, (/** @type {?} */ ({})))))), flatMap((/** * @param {?} obs * @return {?} */ (obs) => merge(obs .pipe(filter((/** * @param {?} _ * @return {?} */ _ => _.status < 400)), map((/** * @param {?} response * @return {?} */ (response) => new HttpResponse({ body: response.body, headers: new HttpHeaders(response.headers), status: response.status, statusText: response.statusText, url: response.url, })))), obs .pipe(filter((/** * @param {?} _ * @return {?} */ _ => _.status >= 400)), flatMap((/** * @param {?} response * @return {?} */ (response) => throwError(new HttpErrorResponse({ error: response.error, headers: new HttpHeaders(response.headers), status: response.status, statusText: response.statusText, url: response.url, }))))))))); } /** * Process when key doesn't exist in transfer state * @private * @param {?} req * @param {?} next * @param {?} hasKey * @param {?} storeKey * @return {?} */ _hasNotKeyProcess(req, next, hasKey, storeKey) { return hasKey .pipe(filter((/** * @param {?} _ * @return {?} */ _ => !_)), flatMap((/** * @return {?} */ () => next.handle(req) .pipe(tap((/** * @param {?} event * @return {?} */ (event) => of(event) .pipe(filter((/** * @param {?} evt * @return {?} */ evt => evt instanceof HttpResponse))) .subscribe((/** * @param {?} evt * @return {?} */ (evt) => this._transferState.set(storeKey, { body: evt.body, headers: this._getHeadersMap(evt.headers), status: evt.status, statusText: evt.statusText, // tslint:disable-next-line:no-non-null-assertion url: (/** @type {?} */ (evt.url)), })))), (/** * @param {?} error * @return {?} */ (error) => of(error) .pipe(filter((/** * @param {?} err * @return {?} */ err => err instanceof HttpErrorResponse))) .subscribe((/** * @param {?} err * @return {?} */ (err) => this._transferState.set(storeKey, { error: err.error, headers: this._getHeadersMap(err.headers), status: err.status, statusText: err.statusText, // tslint:disable-next-line:no-non-null-assertion url: (/** @type {?} */ (err.url)), }))))))))); } /** * Creates Headers Map * @private * @param {?} headers * @return {?} */ _getHeadersMap(headers) { // tslint:disable-next-line:no-non-null-assertion return headers.keys().reduce((/** * @param {?} acc * @param {?} curr * @return {?} */ (acc, curr) => Object.assign(acc, { [curr]: (/** @type {?} */ (headers.getAll(curr))) })), {}); } /** * Function to create sha256 hash * @private * @param {?} data * @return {?} */ _createHash(data) { return createHash('sha256').update(data).digest('hex'); } /** * Returns HttpRequest with value of header inside url & urlWithParams * @private * @param {?} req * @param {?} headerName * @return {?} */ _replaceWithHeader(req, headerName) { return of(of(this._getHeadersMap(req.headers)[headerName])) .pipe(flatMap((/** * @param {?} obs * @return {?} */ (obs) => merge(obs.pipe(filter((/** * @param {?} _ * @return {?} */ (_) => !!_ && !!_.length)), map((/** * @param {?} _ * @return {?} */ (_) => of(_[_.length - 1]))), flatMap((/** * @param {?} o * @return {?} */ (o) => merge(o.pipe(filter((/** * @param {?} _ * @return {?} */ _ => !!_)), flatMap((/** * @param {?} headerValue * @return {?} */ (headerValue) => merge(this._formatUrlWithHeaderValue(req.url, headerValue), this._formatUrlWithHeaderValue(req.urlWithParams, headerValue)).pipe(toArray(), map((/** * @param {?} _ * @return {?} */ _ => (/** @type {?} */ (Object.assign({}, req, { url: _[0], urlWithParams: _[1] }))))))))), o.pipe(filter((/** * @param {?} _ * @return {?} */ _ => !_)), flatMap((/** * @return {?} */ () => throwError(new Error(`Missing header '${headerName}' value inside request to generate state key`))))))))), obs.pipe(filter((/** * @param {?} _ * @return {?} */ _ => !_ || !_.length)), flatMap((/** * @return {?} */ () => throwError(new Error(`Missing header '${headerName}' value inside request to generate state key`))))))))); } /** * Replace url with header value * @private * @param {?} url * @param {?} headerValue * @return {?} */ _formatUrlWithHeaderValue(url, headerValue) { return of(url) .pipe(map((/** * @param {?} _ * @return {?} */ (_) => _.split('://')[1].split('/'))), map((/** * @param {?} _ * @return {?} */ (_) => _.map((/** * @param {?} s * @param {?} i * @return {?} */ (s, i) => i === 0 ? headerValue : s)))), map((/** * @param {?} _ * @return {?} */ (_) => _.join('/')))); } /** * Returns the good request object to create hash * @private * @param {?} req * @return {?} */ _requestFormatted(req) { return of(of(this._configService.config.headerNameToOverrideUrlInKeyCachingGeneration)) .pipe(flatMap((/** * @param {?} obs * @return {?} */ (obs) => merge(obs.pipe(filter((/** * @param {?} _ * @return {?} */ _ => !!_)), flatMap((/** * @param {?} _ * @return {?} */ _ => this._replaceWithHeader(req, _)))), obs.pipe(filter((/** * @param {?} _ * @return {?} */ _ => !_)), map((/** * @return {?} */ () => req))))))); } } TransferHttpCacheInterceptor.decorators = [ { type: Injectable } ]; /** @nocollapse */ TransferHttpCacheInterceptor.ctorParameters = () => [ { type: ApplicationRef }, { type: TransferState }, { type: undefined, decorators: [{ type: Inject, args: [PLATFORM_ID,] }] }, { type: TransferHttpCacheConfigService } ]; /** * @fileoverview added by tsickle * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class TransferHttpCacheModule { /** * @param {?} config * @return {?} */ static withConfig(config) { return { ngModule: TransferHttpCacheModule, providers: [{ provide: NG_UNIVERSAL_TRANSFER_HTTP_CONFIG, useValue: config }] }; } } TransferHttpCacheModule.decorators = [ { type: NgModule, args: [{ imports: [BrowserTransferStateModule], providers: [ TransferHttpCacheConfigService, { provide: HTTP_INTERCEPTORS, useClass: TransferHttpCacheInterceptor, multi: true }, ], },] } ]; export { TransferHttpCacheModule, TransferHttpCacheConfigService as ɵa, NG_UNIVERSAL_TRANSFER_HTTP_CONFIG as ɵb, TransferHttpCacheInterceptor as ɵc }; //# sourceMappingURL=akanass-ng-universal-transfer-http.js.map