Last active
March 6, 2020 22:42
-
-
Save diestrin/e7e71030f15e3e91e350a666cd769e1a to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Observable, of } from 'rxjs'; | |
import { tap, share } from 'rxjs/operators'; | |
import { Injectable, isDevMode } from '@angular/core'; | |
import { makeStateKey, TransferState } from '@angular/platform-browser'; | |
import { HttpInterceptor, HttpRequest, HttpHandler, HttpResponse } from '@angular/common/http'; | |
import { EnvService } from './env.service'; | |
const STATE_KEY = 'state-key'; | |
const StateKey = makeStateKey<Object>(STATE_KEY); | |
@Injectable({ | |
providedIn: 'root' | |
}) | |
export class CacheService implements HttpInterceptor { | |
private cache = new Map<string, any>(); | |
constructor(env: EnvService, private transfer: TransferState) { | |
if (env.isBrowser) { | |
this.rehydrate(transfer.get(StateKey, {})); | |
} | |
} | |
public intercept(httpRequest: HttpRequest<any>, handler: HttpHandler) { | |
// Don't cache if | |
// 1. It's not a GET request | |
// 2. If URI is not supposed to be cached | |
if (httpRequest.method !== 'GET' || !this.has(httpRequest.url)) { | |
return handler.handle(httpRequest); | |
} | |
// Also leave scope of resetting already cached data for a URI | |
if (httpRequest.headers.get('reset-cache')) { | |
this.set(httpRequest.urlWithParams, undefined); | |
} | |
// Checked if there is cached data for this URI | |
const lastResponse = this.get(httpRequest.urlWithParams); | |
if (lastResponse) { | |
// In case of parallel requests to same URI, | |
// return the request already in progress | |
// otherwise return the last cached data | |
return (lastResponse instanceof Observable) | |
? lastResponse : of(lastResponse.clone()); | |
} | |
// If the request of going through for first time | |
// then let the request proceed and cache the response | |
const requestHandle = handler.handle(httpRequest) | |
.pipe( | |
tap((stateEvent) => { | |
if (stateEvent instanceof HttpResponse) { | |
this.set(httpRequest.urlWithParams, stateEvent.clone()); | |
} | |
}), | |
share() | |
); | |
// Meanwhile cache the request Observable to handle parallel request | |
this.set(httpRequest.urlWithParams, requestHandle); | |
return requestHandle; | |
} | |
/** | |
* check if there is a value in our store | |
*/ | |
public has(key: string|number): boolean { | |
const _key = this.normalizeKey(key); | |
return this.cache.has(_key); | |
} | |
/** | |
* store our state | |
*/ | |
public set(key: string|number, value: any): Map<string, any> { | |
const _key = this.normalizeKey(key); | |
this.cache.set(_key, value); | |
this.transfer.set(StateKey, this.dehydrate()); | |
return this.cache; | |
} | |
/** | |
* get our cached value | |
*/ | |
public get(key: string|number): any { | |
const _key = this.normalizeKey(key); | |
return this.cache.get(_key); | |
} | |
/** | |
* release memory refs | |
*/ | |
public clear(): void { | |
this.cache.clear(); | |
this.transfer.set(StateKey, {}); | |
} | |
/** | |
* convert to json for the client | |
*/ | |
public dehydrate(): any { | |
const json = {}; | |
this.cache.forEach((value: any, key: string) => { | |
if (value.value instanceof Observable) { | |
// If there's any value in cache unresolved, don't include it | |
return; | |
} | |
return json[key] = value; | |
}); | |
return json; | |
} | |
/** | |
* convert server json into out initial state | |
*/ | |
public rehydrate(json: any): void { | |
Object.keys(json).forEach((key: string) => { | |
const _key = this.normalizeKey(key); | |
const value = json[_key]; | |
this.cache.set(_key, value); | |
}); | |
} | |
/** | |
* allow JSON.stringify to work | |
*/ | |
public toJSON(): any { | |
return this.dehydrate(); | |
} | |
/** | |
* convert numbers into strings | |
*/ | |
public normalizeKey(key: string | number): string { | |
if (isDevMode() && this._isInvalidValue(key)) { | |
throw new Error('Please provide a valid key to save in the CacheService'); | |
} | |
return key + ''; | |
} | |
public _isInvalidValue(key: any): boolean { | |
return key === null || | |
key === undefined || | |
key === 0 || | |
key === '' || | |
typeof key === 'boolean' || | |
Number.isNaN(key as number); | |
} | |
public inject(): void { } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { isPlatformBrowser, isPlatformServer } from '@angular/common'; | |
import { makeStateKey, TransferState } from '@angular/platform-browser'; | |
import { Inject, Injectable, Injector, PLATFORM_ID } from '@angular/core'; | |
const envKey = makeStateKey('process.env'); | |
// Variables exposed to front-end, don't expose unnecessary values | |
const publicEnvVars = [ | |
'MY_ENV_VAR' | |
]; | |
@Injectable({ | |
providedIn: 'root' | |
}) | |
export class EnvService { | |
public isServer = isPlatformServer(this.platformId); | |
public isBrowser = isPlatformBrowser(this.platformId); | |
public request = this.isServer ? this.injector.get('request') : {}; | |
public response = this.isServer ? this.injector.get('response') : {}; | |
public hostname = this.request.hostname || ''; | |
public get vars() { | |
const env = this.injector.get('env'); | |
if (this.isServer) { | |
return env; | |
} | |
return { ...env, ...this.transfer.get(envKey, {}) }; | |
} | |
public omnitureAccount = this.vars.omnitureAccount || ''; | |
public apiVars = { | |
apiUrl: this.vars.MY_API_URL, | |
apiSecret: this.vars.MY_API_SECRET | |
}; | |
public navigator = this.isBrowser ? navigator : { | |
userAgent: 'node' | |
}; | |
public envDependencies = { | |
jquery: async () => (await this.injector.get('browser:jquery')).default as typeof import('jquery') | |
}; | |
constructor( | |
private injector: Injector, | |
@Inject(PLATFORM_ID) | |
private platformId: Object, | |
private transfer: TransferState | |
) { | |
if (this.isServer) { | |
// Expose to browser only certain variables | |
transfer.set(envKey, publicEnvVars.reduce((vars, key) => ({ | |
...vars, | |
[key]: this.vars[key] | |
}), {})); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Injectable } from '@angular/core'; | |
import { EnvService } from './env'; | |
/** | |
* prefix for identify in localStorage | |
*/ | |
const prefix = 'brand.'; | |
/** | |
* Save information into the localStorage | |
*/ | |
@Injectable({ | |
providedIn: 'root' | |
}) | |
export class LocalStorage { | |
public localStorage: any; | |
constructor(private env: EnvService) { | |
if (env.isBrowser) { | |
this.localStorage = window.localStorage; | |
} | |
} | |
/** | |
* Directly adds a value to local storage | |
* | |
* @example | |
* ``` | |
* localStorageService.add('library','angular'); | |
* ``` | |
*/ | |
public add(key: string, value: any, prfx: string = prefix): boolean { | |
if (this.env.isServer) { | |
return false; | |
} | |
// 0 and "" is allowed as a value but let's limit other falsey values like "undefined" | |
if (!value && value !== 0 && value !== '') { | |
return false; | |
} | |
const stringifyedValue = typeof value !== 'string' ? JSON.stringify(value) : value; | |
try { | |
this.localStorage.setItem(prfx + key, stringifyedValue); | |
} catch (e) { | |
return false; | |
} | |
return true; | |
} | |
/** | |
* Directly get a value from local storage | |
* | |
* @example | |
* ``` | |
* localStorageService.get('library'); // returns 'angular' | |
* ``` | |
*/ | |
public get(key: string, prfx: string = prefix): any { | |
if (this.env.isServer) { | |
return null; | |
} | |
let item = this.localStorage.getItem(prfx + key); | |
item = item || this.localStorage.getItem(key); | |
try { | |
item = JSON.parse(item); | |
} catch (e) { | |
// do nothing, item already has the value | |
} | |
if (!item && item !== 0 && item !== '') { | |
return null; | |
} | |
return item; | |
} | |
/** | |
* Remove an item from local storage | |
* | |
* @example | |
* ``` | |
* localStorageService.remove('library'); | |
* // removes the key/value pair of library='angular'; | |
* ``` | |
*/ | |
public remove(key: string, prfx: string = prefix): boolean { | |
if (this.env.isServer) { | |
return false; | |
} | |
try { | |
this.localStorage.removeItem(prfx + key); | |
} catch (e) { | |
return false; | |
} | |
return true; | |
} | |
/** | |
* Remove all data for this app from local storage | |
* Should be used mostly for development purposes | |
* | |
* @example | |
* ``` | |
* localStorageService.clearAll(); | |
* ``` | |
*/ | |
public clearAll(): boolean { | |
if (this.env.isServer) { | |
return false; | |
} | |
const prefixLength = prefix.length; | |
for (const key in this.localStorage) { | |
// Only remove items that are for this app | |
if (key.substr(0, prefixLength) === prefix) { | |
try { | |
this.remove(key.substr(prefixLength)); | |
} catch (e) { | |
return false; | |
} | |
} | |
} | |
return true; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { enableProdMode } from '@angular/core'; | |
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; | |
import { AppModule } from './app/app.module'; | |
import { environment } from './environments/environment'; | |
import { getProviders } from './providers.browser' | |
if (environment.production) { | |
enableProdMode(); | |
} | |
platformBrowserDynamic(getProviders()).bootstrapModule(AppModule); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { StaticProvider } from '@angular/core'; | |
import { environment } from './environments/environment'; | |
export function loadJquery() { | |
return import('jquery'); | |
} | |
export function getProviders(): StaticProvider[] { | |
return [ | |
{ | |
provide: 'env', | |
useValue: environment | |
}, | |
{ | |
provide: 'browser:jquery', | |
useFactory: loadJquery, | |
deps: [] | |
} | |
]; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { StaticProvider } from '@angular/core'; | |
const cookies = require('cookies'); | |
import { environment } from './environments/environment'; | |
export function getProviders(options): StaticProvider[] { | |
return [ | |
{ | |
provide: 'request', | |
useValue: options.req | |
}, | |
{ | |
provide: 'response', | |
useValue: options.res | |
}, | |
{ | |
provide: 'env', | |
useValue: { ...environment, ...process.env } | |
}, | |
{ | |
provide: 'node:cookies', | |
useValue: cookies | |
} | |
]; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'zone.js/dist/zone-node'; | |
import { ngExpressEngine } from '@nguniversal/express-engine'; | |
import * as express from 'express'; | |
import { join } from 'path'; | |
import { AppServerModule } from './src/main.server'; | |
import { APP_BASE_HREF } from '@angular/common'; | |
import { getProviders } from './providers.server.ts'; | |
// The Express app is exported so that it can be used by serverless Functions. | |
export function app() { | |
const server = express(); | |
const distFolder = join(process.cwd(), 'dist/express-engine-ivy/browser'); | |
server.engine('html', ngExpressEngine({ | |
bootstrap: AppServerModule | |
})); | |
server.set('view engine', 'html'); | |
server.set('views', distFolder); | |
// TODO: implement data requests securely | |
server.get('/api/*', (req, res) => { | |
res.status(404).send('data requests are not supported'); | |
}); | |
// Serve static files from /browser | |
server.get('*.*', express.static(distFolder, { | |
maxAge: '1y' | |
})); | |
// All regular routes use the Universal engine | |
server.get('*', (req, res) => { | |
res.render('index', { | |
req, | |
res, | |
providers: [ | |
{ provide: APP_BASE_HREF, useValue: req.baseUrl }, | |
...getProviders({req, res}) | |
] | |
}); | |
}); | |
return server; | |
} | |
function run() { | |
const port = process.env.PORT || 4000; | |
// Start up the Node server | |
const server = app(); | |
server.listen(port, () => { | |
console.log(`Node Express server listening on http://localhost:${port}`); | |
}); | |
} | |
// Webpack will replace 'require' with '__webpack_require__' | |
// '__non_webpack_require__' is a proxy to Node 'require' | |
// The below code is to ensure that the server is run only when not requiring the bundle. | |
declare const __non_webpack_require__: NodeRequire; | |
const mainModule = __non_webpack_require__.main; | |
if (mainModule && mainModule.filename === __filename) { | |
run(); | |
} | |
export * from './src/main.server'; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment