From 17ddad6a0f0e8d02fb440f150e452204ddb20cbc Mon Sep 17 00:00:00 2001 From: Mason Payne Date: Sat, 23 Mar 2019 02:34:47 -0600 Subject: [PATCH] add uber cache and some streams for test data --- package.json | 9 +-- src/app/User.ts | 43 +++++++++++++ src/app/app-modules.service.ts | 8 +++ src/app/app.module.ts | 1 + .../components/sidebar/sidebar.component.css | 3 + .../components/sidebar/sidebar.component.html | 7 ++- .../components/sidebar/sidebar.component.ts | 56 ++++++++++++----- src/app/profile.service.spec.ts | 15 +++++ src/app/profile.service.ts | 48 ++++++++++++++ src/app/uber-cache.service.spec.ts | 15 +++++ src/app/uber-cache.service.ts | 63 +++++++++++++++++++ src/polyfills.ts | 1 + yarn.lock | 9 ++- 13 files changed, 257 insertions(+), 21 deletions(-) create mode 100644 src/app/User.ts create mode 100644 src/app/profile.service.spec.ts create mode 100644 src/app/profile.service.ts create mode 100644 src/app/uber-cache.service.spec.ts create mode 100644 src/app/uber-cache.service.ts diff --git a/package.json b/package.json index 44bea9a..25fd959 100644 --- a/package.json +++ b/package.json @@ -36,8 +36,9 @@ "bootstrap-material-design": "4.1.1", "bootstrap-notify": "3.1.3", "chartist": "0.11.0", - "classlist.js": "1.1.20150312", + "classlist.js": "^1.1.20150312", "core-js": "2.4.1", + "event-source-polyfill": "^1.0.5", "express": "4.16.3", "googleapis": "28.1.0", "hammerjs": "2.0.8", @@ -47,10 +48,11 @@ "popper.js": "1.14.3", "rxjs": "6.3.3", "rxjs-compat": "6.3.3", - "web-animations-js": "2.3.1", + "web-animations-js": "^2.3.1", "zone.js": "0.8.26" }, "devDependencies": { + "@angular-devkit/build-angular": "~0.6.3", "@angular/cli": "6.0.3", "@angular/compiler-cli": "7.0.2", "@angular/language-service": "7.0.2", @@ -71,7 +73,6 @@ "protractor": "5.3.1", "ts-node": "5.0.1", "tslint": "5.9.1", - "typescript": "3.1.6", - "@angular-devkit/build-angular": "~0.6.3" + "typescript": "3.1.6" } } diff --git a/src/app/User.ts b/src/app/User.ts new file mode 100644 index 0000000..0e8ccb3 --- /dev/null +++ b/src/app/User.ts @@ -0,0 +1,43 @@ +interface Claims { + // https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims + sub?: string; + name?: string; + given_name?: string; + family_name?: string; + middle_name?: string; + nickname?: string; + preferred_username?: string; + profile?: string; // url to their profile + picture?: string; // url to their image ending in .png, .jpg, etc... + website?: string; + email: string; + email_verified?: boolean; + gender?: string; // usually 'male' or 'female' + birthdate?: string; // 'yyyy-mm-dd' + zoneinfo?: string; // e.g. 'Europe/Paris' or 'America/Los_Angeles' + local?: string; // e.g. 'en-US' or 'fr-CA' some implementations us underscore + phone_number?: string; + phone_number_verified?: boolean; + address?: Address; + updated_at?: number; // seconds since the epoc when this object was last updated +} + +interface User { + claims: Claims; + customers: string[]; +} + +interface Address { + formatted: string; + street_address: string; + locality: string; // e.g. city + region: string; // e.g. state, province, prefecture or region + postal_code: string; // zip coed or postal code + country: string; +} + +export { + User, + Claims, + Address +} diff --git a/src/app/app-modules.service.ts b/src/app/app-modules.service.ts index 5a1de33..7a42535 100644 --- a/src/app/app-modules.service.ts +++ b/src/app/app-modules.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { Observable, throwError } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { AvailableAppModule } from './AppModulesTypes'; @@ -9,9 +10,16 @@ import { AvailableAppModule } from './AppModulesTypes'; providedIn: 'root' }) export class AppModulesService { + // default this value to the user's last time logging in + private showTestDataSource = new BehaviorSubject(true); + public showTestData = this.showTestDataSource.asObservable(); constructor(private http: HttpClient) { } + public toggleShowTestData(newValue) { + this.showTestDataSource.next(newValue); + } + public getAvailableAppModules() { // returns all app modules and indicates the customers for which the user can use the modules return this.http.get('/api/v1/modules').pipe(catchError(this.handleError)); diff --git a/src/app/app.module.ts b/src/app/app.module.ts index a75d20e..3968bb5 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -12,6 +12,7 @@ import { AppComponent } from './app.component'; // Services import { AppModulesService } from './app-modules.service'; import { LinkifyService } from './linkify.service'; +import { UberCacheService } from './uber-cache.service'; // Pipes import { LinkifyPipe } from './linkify.pipe'; diff --git a/src/app/components/sidebar/sidebar.component.css b/src/app/components/sidebar/sidebar.component.css index e69de29..605c448 100644 --- a/src/app/components/sidebar/sidebar.component.css +++ b/src/app/components/sidebar/sidebar.component.css @@ -0,0 +1,3 @@ +ul.nav a.test-active, ul.nav a.test-active > i { + color: #ff9800 +} diff --git a/src/app/components/sidebar/sidebar.component.html b/src/app/components/sidebar/sidebar.component.html index 19ecffd..4781a30 100644 --- a/src/app/components/sidebar/sidebar.component.html +++ b/src/app/components/sidebar/sidebar.component.html @@ -56,11 +56,16 @@ diff --git a/src/app/components/sidebar/sidebar.component.ts b/src/app/components/sidebar/sidebar.component.ts index 8e83eed..6845476 100644 --- a/src/app/components/sidebar/sidebar.component.ts +++ b/src/app/components/sidebar/sidebar.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { AppModulesService } from '../../app-modules.service'; +import { ProfileService } from '../../profile.service'; import { AvailableAppModule } from '../../AppModulesTypes'; import { LinkifyService } from '../../linkify.service'; @@ -11,6 +12,7 @@ declare interface RouteInfo { title: string; icon: string; class: string; + key?: string; } export const ROUTES: RouteInfo[] = [ { @@ -22,45 +24,52 @@ export const ROUTES: RouteInfo[] = [ { path: '/user-profile', title: 'User Profile', - icon:'user-alt', + icon: 'user-alt', class: '' }, { path: '/authentication', title: 'Authentication', - icon:'key', + icon: 'key', class: '' }, { path: '/authorization', title: 'Authorization', - icon:'shield-alt', + icon: 'shield-alt', class: '' }, { path: '/feature-switches', title: 'Feature Switches', - icon:'toggle-on', + icon: 'toggle-on', class: '' }, { path: '/short-links', title: 'Short Links', - icon:'link', + icon: 'link', class: '' }, { path: '/email', title: 'Email', - icon:'envelope', + icon: 'envelope', class: '' }, - // { path: '/table-list', title: 'Table List', icon:'content_paste', class: '' }, - // { path: '/typography', title: 'Typography', icon:'library_books', class: '' }, - // { path: '/icons', title: 'Icons', icon:'bubble_chart', class: '' }, - // { path: '/maps', title: 'Maps', icon:'location_on', class: '' }, - // { path: '/notifications', title: 'Notifications', icon:'notifications', class: '' }, - // { path: '/upgrade', title: 'Upgrade to PRO', icon:'unarchive', class: 'active-pro' }, + { + path: '/test-data', + title: 'View test data', + key: 'test-data', + icon: 'toggle-on', + class: '' + }, + // { path: '/table-list', title: 'Table List', icon: 'content_paste', class: '' }, + // { path: '/typography', title: 'Typography', icon: 'library_books', class: '' }, + // { path: '/icons', title: 'Icons', icon: 'bubble_chart', class: '' }, + // { path: '/maps', title: 'Maps', icon: 'location_on', class: '' }, + // { path: '/notifications', title: 'Notifications', icon: 'notifications', class: '' }, + // { path: '/upgrade', title: 'Upgrade to PRO', icon: 'unarchive', class: 'active-pro' }, ]; @Component({ @@ -69,12 +78,26 @@ export const ROUTES: RouteInfo[] = [ styleUrls: ['./sidebar.component.css'] }) export class SidebarComponent implements OnInit { - menuItems: any[]; + public menuItems: any[]; + public showTestData: boolean = false; constructor( private appModulesService: AppModulesService, private linkify: LinkifyService, - ) { } + private profileService: ProfileService + ) { + appModulesService.showTestData.subscribe((newValue: boolean) => { + this.showTestData = newValue; + }); + + profileService.currentUser().subscribe((user) => { + console.log(user); + }); + + profileService.currentUser().subscribe((user) => { + console.log(user); + }); + } ngOnInit() { this.menuItems = ROUTES.filter(menuItem => menuItem); @@ -92,10 +115,15 @@ export class SidebarComponent implements OnInit { // // self.appModules = availableModules; // }); } + isMobileMenu() { if ($(window).width() > 991) { return false; } return true; }; + + toggleTestMode() { + this.appModulesService.toggleShowTestData(!this.showTestData); + } } diff --git a/src/app/profile.service.spec.ts b/src/app/profile.service.spec.ts new file mode 100644 index 0000000..1fed938 --- /dev/null +++ b/src/app/profile.service.spec.ts @@ -0,0 +1,15 @@ +import { TestBed, inject } from '@angular/core/testing'; + +import { ProfileService } from './profile.service'; + +describe('ProfileService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ProfileService] + }); + }); + + it('should be created', inject([ProfileService], (service: ProfileService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/src/app/profile.service.ts b/src/app/profile.service.ts new file mode 100644 index 0000000..eccdfca --- /dev/null +++ b/src/app/profile.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@angular/core'; +import { Observable, throwError } from 'rxjs'; +import { map, catchError } from 'rxjs/operators'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; + +import { UberCacheService } from './uber-cache.service'; + +import { User } from './User'; + +@Injectable({ + providedIn: 'root' +}) +export class ProfileService { + + constructor( + private http: HttpClient, + private uberCache: UberCacheService + ) { } + + private requestCurrentUser(): Observable { + return this.http.get('/api/v1/profile/me').pipe(catchError(this.handleError)).pipe(map((res: UserResponse) => res.user)); + } + + public currentUser(): Observable { + return this.uberCache.getRequest(this.requestCurrentUser.bind(this), `/api/v1/profile/events`, 'userUpdated'); + } + + private handleError(error: HttpErrorResponse) { + if (error.error instanceof ErrorEvent) { + // client or network error + console.error('An error occurred:', error.error.message); + } else { + // backend returned an error + console.error( + `Backend returned code ${error.status}, ` + + `body was: ${error.error}` + ); + } + return throwError( + `An eror occurred` + ); + } +} + +interface UserResponse { + status: string; + user: User +} diff --git a/src/app/uber-cache.service.spec.ts b/src/app/uber-cache.service.spec.ts new file mode 100644 index 0000000..8a6da44 --- /dev/null +++ b/src/app/uber-cache.service.spec.ts @@ -0,0 +1,15 @@ +import { TestBed, inject } from '@angular/core/testing'; + +import { UberCacheService } from './uber-cache.service'; + +describe('UberCacheService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [UberCacheService] + }); + }); + + it('should be created', inject([UberCacheService], (service: UberCacheService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/src/app/uber-cache.service.ts b/src/app/uber-cache.service.ts new file mode 100644 index 0000000..62e496d --- /dev/null +++ b/src/app/uber-cache.service.ts @@ -0,0 +1,63 @@ +import { Injectable } from '@angular/core'; +import { Observable, Subject, merge } from 'rxjs'; +import { switchMap, shareReplay } from 'rxjs/operators'; + +import { NativeEventSource, EventSourcePolyfill } from 'event-source-polyfill'; +const EventSource = NativeEventSource || EventSourcePolyfill; +// OR: may also need to set as global property +// global.EventSource = NativeEventSource || EventSourcePolyfill; + +const CACHE_SIZE = 1; + +// Creates a cache of requests that are automatically busted based on server sent events + +@Injectable({ + providedIn: 'root' +}) +export class UberCacheService { + private cache$: CacheStore; + + constructor() { + this.cache$ = {}; + } + + public getRequest(request: ()=>Observable, eventUrl: string, eventName: string): Observable { + if (!this.cache$[eventUrl]) { + const updateStream = this.createUpdateStream(eventUrl, eventName); + const initialCurrentUser$ = request(); + const reload$ = updateStream.pipe(switchMap(() => request())); + const newCache = merge(initialCurrentUser$, reload$).pipe(shareReplay(CACHE_SIZE)); + this.cache$[eventUrl] = { + cache: newCache, + stream: updateStream + }; + } + return this.cache$[eventUrl].cache; + } + + private createUpdateStream(eventUrl: string, eventName: string) { + const eventStream = new Subject(); + const es = new EventSource(eventUrl); + es.addEventListener(eventName, (evt: StreamEvent) => { + console.log(evt.data); + eventStream.next(); + }); + return eventStream; + } +} + +interface StreamEvent { + data?: string; + event?: string; + id?: string; + retry?: number; +} + +interface CacheStore { + [name: string]: CacheTools; +} + +interface CacheTools { + cache: Observable; + stream: Subject; +} diff --git a/src/polyfills.ts b/src/polyfills.ts index eff4884..91f892c 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -66,3 +66,4 @@ import 'zone.js/dist/zone'; // Included with Angular CLI. * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 */ // import 'intl'; // Run `npm install --save intl`. +import 'event-source-polyfill'; diff --git a/yarn.lock b/yarn.lock index f940a70..0633020 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1690,7 +1690,7 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -classlist.js@1.1.20150312: +classlist.js@^1.1.20150312: version "1.1.20150312" resolved "https://registry.yarnpkg.com/classlist.js/-/classlist.js-1.1.20150312.tgz#1d70842f7022f08d9ac086ce69e5b250f2c57789" integrity sha1-HXCEL3Ai8I2awIbOaeWyUPLFd4k= @@ -2789,6 +2789,11 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= +event-source-polyfill@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/event-source-polyfill/-/event-source-polyfill-1.0.5.tgz#b34be2740a685a8dc65ae750065fc983538ffcfe" + integrity sha512-PdStgZ3+G2o2gjqsBYbV4931ByVmwLwSrX7mFgawCL+9I1npo9dwAQTnWtNWXe5IY2P8+AbbPteeOueiEtRCUA== + event-target-shim@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" @@ -8545,7 +8550,7 @@ wbuf@^1.1.0, wbuf@^1.7.3: dependencies: minimalistic-assert "^1.0.0" -web-animations-js@2.3.1: +web-animations-js@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/web-animations-js/-/web-animations-js-2.3.1.tgz#3a6d9bc15196377a90f8e2803fa5262165b04510" integrity sha1-Om2bwVGWN3qQ+OKAP6UmIWWwRRA=