Skip to content

Commit

Permalink
Implement cacheable local storage
Browse files Browse the repository at this point in the history
  • Loading branch information
safo6m committed Nov 17, 2020
1 parent 1c6027d commit a0eba20
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { from, Observable, of } from 'rxjs';
import { distinctUntilChanged, mergeMap } from 'rxjs/operators';
import { HalModel } from '../../models/hal.model';
import { HalDocument } from '../hal-document';
import { EtagHalStorage } from './etag-hal-storage';

export class CacheFirstFetchLaterStorage extends EtagHalStorage {
public makeGetRequestWrapper<T extends HalModel>(
urls: { originalUrl: string; cleanUrl: string; urlWithParams: string },
cachedResource: T | HalDocument<T>,
originalGetRequest$: Observable<T | HalDocument<T>>
): Observable<T | HalDocument<T>> {
if (cachedResource) {
return from([of(cachedResource), originalGetRequest$]).pipe(
mergeMap((request) => request),
distinctUntilChanged(this.areModelsEqual.bind(this))
);
}

return originalGetRequest$;
}

private areModelsEqual<T extends HalModel>(model1: T | HalDocument<T>, model2: T | HalDocument<T>): boolean {
const localModel1 = this.getRawStorageModel(model1.uniqueModelIdentificator);
const localModel2 = this.getRawStorageModel(model2.uniqueModelIdentificator);
return localModel1.etag === localModel2.etag;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export class EtagHalStorage extends HalStorage {
}
}

private getRawStorageModel<T extends HalModel>(uniqueModelIdentificator: string): StorageModel<T> {
protected getRawStorageModel<T extends HalModel>(uniqueModelIdentificator: string): StorageModel<T> {
return this.internalStorage[uniqueModelIdentificator];
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { CacheStrategy } from '../../enums/cache-strategy.enum';
import { SimpleHalStorage } from '../../classes/hal-storage/simple-hal-storage';
import { EtagHalStorage } from '../../classes/hal-storage/etag-hal-storage';
import { HalStorage } from './hal-storage';
import { CacheFirstFetchLaterStorage } from './cache-first-fetch-later.storage';

export type HalStorageType = SimpleHalStorage | EtagHalStorage;

export function createHalStorage(cacheStrategy: CacheStrategy = CacheStrategy.NONE): HalStorageType {
export function createHalStorage(cacheStrategy: CacheStrategy = CacheStrategy.NONE, storageInstance: HalStorage): HalStorageType {
let storage: HalStorageType;

switch (cacheStrategy) {
Expand All @@ -14,6 +16,15 @@ export function createHalStorage(cacheStrategy: CacheStrategy = CacheStrategy.NO
case CacheStrategy.ETAG:
storage = new EtagHalStorage();
break;
case CacheStrategy.CACHE_FIRST_FETCH_LATER:
storage = new CacheFirstFetchLaterStorage();
break;
case CacheStrategy.CUSTOM:
if (!storageInstance) {
throw new Error('When CacheStrategy.CUSTOM is specified, config.storage is required.');
}
storage = storageInstance;
break;
default:
throw new Error(`Unknown CacheStrategy: ${cacheStrategy}`);
break;
Expand Down
12 changes: 12 additions & 0 deletions projects/ngx-hal/src/lib/classes/hal-storage/hal-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { HalModel } from '../../models/hal.model';
import { HalDocument } from './../hal-document';
import { HttpResponse } from '@angular/common/http';
import { RequestOptions } from '../../types/request-options.type';
import { Observable } from 'rxjs';
import { ModelConstructor } from '../../types/model-constructor.type';

export abstract class HalStorage {
protected internalStorage: { [K: string]: any } = {};
Expand Down Expand Up @@ -29,4 +31,14 @@ export abstract class HalStorage {
public enrichRequestOptions(uniqueModelIdentificator: string, requestOptions: RequestOptions): void {
// noop
}

public makeGetRequestWrapper?<T extends HalModel>(
urls: { originalUrl: string; cleanUrl: string; urlWithParams: string },
cachedResource: T | HalDocument<T>,
originalGetRequest$: Observable<T | HalDocument<T>>,
requestOptions: RequestOptions,
modelClass: ModelConstructor<T>,
isSingleResource: boolean,
storePartialModels?: boolean
): Observable<T | HalDocument<T>>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export function DatastoreConfig(config: DatastoreOptions) {
const networkConfig = deepmergeWrapper<NetworkConfig>(DEFAULT_NETWORK_CONFIG, config.network || {});
Object.defineProperty(target.prototype, 'paginationClass', { value: config.paginationClass });
Object.defineProperty(target.prototype, 'cacheStrategy', { value: config.cacheStrategy });
Object.defineProperty(target.prototype, '_storage', { value: config.storage });
Object.defineProperty(target.prototype, 'networkConfig', { value: networkConfig, writable: true });
Reflect.defineMetadata(HAL_DATASTORE_DOCUMENT_CLASS_METADATA_KEY, config.halDocumentClass, target);
return target;
Expand Down
2 changes: 2 additions & 0 deletions projects/ngx-hal/src/lib/enums/cache-strategy.enum.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export enum CacheStrategy {
CACHE_FIRST_FETCH_LATER = 'CACHE_FIRST_FETCH_LATER',
CUSTOM = 'CUSTOM',
ETAG = 'ETAG',
NONE = 'NONE'
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { HalModel } from '../models/hal.model';
import { HalDocumentConstructor } from '../types/hal-document-construtor.type';
import { PaginationConstructor } from '../types/pagination.type';
import { CacheStrategy } from '../enums/cache-strategy.enum';
import { HalStorage } from '../classes/hal-storage/hal-storage';

export interface DatastoreOptions {
network?: NetworkConfig;
halDocumentClass?: HalDocumentConstructor<HalModel>;
paginationClass?: PaginationConstructor;
cacheStrategy?: CacheStrategy;
storage?: HalStorage;
}
94 changes: 70 additions & 24 deletions projects/ngx-hal/src/lib/services/datastore/datastore.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpResponse, HttpParams } from '@angular/common/http';
import { Observable, combineLatest, of, throwError } from 'rxjs';
import { map, flatMap, tap, catchError } from 'rxjs/operators';
import { Observable, combineLatest, of, throwError, from } from 'rxjs';
import { map, flatMap, tap, catchError, mergeMap, delay } from 'rxjs/operators';
import * as UriTemplate from 'uri-templates';
import { NetworkConfig, DEFAULT_NETWORK_CONFIG } from '../../interfaces/network-config.interface';
import { HalModel } from '../../models/hal.model';
Expand Down Expand Up @@ -32,12 +32,15 @@ import { RelationshipRequestDescriptor } from '../../types/relationship-request-
import { ensureRelationshipRequestDescriptors } from '../../utils/ensure-relationship-descriptors/ensure-relationship-descriptors.util';
import { RelationshipDescriptorMappings } from '../../types/relationship-descriptor-mappings.type';
import { EMBEDDED_PROPERTY_NAME } from '../../constants/hal.constant';
import { HalStorage } from '../../classes/hal-storage/hal-storage';

@Injectable()
export class DatastoreService {
public networkConfig: NetworkConfig = this['networkConfig'] || DEFAULT_NETWORK_CONFIG;
private cacheStrategy: CacheStrategy;
private internalStorage = createHalStorage(this.cacheStrategy);
private cacheStrategy: CacheStrategy; // set by Config decorator
// tslint:disable-next-line
private _storage: HalStorage; // set by Config decorator
private internalStorage = createHalStorage(this.cacheStrategy, this._storage);
protected httpParamsOptions?: object;
public paginationClass: PaginationConstructor;

Expand Down Expand Up @@ -272,7 +275,7 @@ export class DatastoreService {
return of(fetchedModels);
}

const httpRequest$ = this.makeGetRequest(url, requestsOptions.mainRequest, modelClass, isSingleResource, storePartialModels);
const httpRequest$ = this.makeGetRequestWrapper(url, requestsOptions, modelClass, isSingleResource, storePartialModels);

if (includeRelationships.length) {
return httpRequest$.pipe(
Expand All @@ -299,6 +302,37 @@ export class DatastoreService {
return httpRequest$;
}

private makeGetRequestWrapper<T extends HalModel>(
url: string,
requestsOptions: RequestsOptions,
modelClass: ModelConstructor<T>,
isSingleResource: boolean,
storePartialModels?: boolean
): Observable<HalDocument<T> | T> {
const originalGetRequest$: Observable<T | HalDocument<T>> = this.makeGetRequest(
url,
requestsOptions.mainRequest,
modelClass,
isSingleResource,
storePartialModels
);

if (this.storage.makeGetRequestWrapper) {
const { cleanUrl, urlWithParams, requestOptions: options } = this.extractRequestInfo(url, requestsOptions.mainRequest);
const cachedResoucesFromUrl = this.storage.get(decodeURIComponent(url)) || this.storage.get(decodeURIComponent(urlWithParams));
return this.storage.makeGetRequestWrapper(
{ cleanUrl, urlWithParams, originalUrl: url },
cachedResoucesFromUrl,
originalGetRequest$,
options,
modelClass,
storePartialModels
);
}

return originalGetRequest$;
}

private triggerFetchingModelRelationships<T extends HalModel>(
models: Array<T>,
includeRelationships: Array<RelationshipRequestDescriptor>,
Expand Down Expand Up @@ -668,32 +702,16 @@ export class DatastoreService {
singleResource: boolean,
storePartialModels?: boolean
): Observable<HalDocument<T> | T> {
const params: object = this.ensureParamsObject(requestOptions.params || {});
Object.assign(requestOptions, { params });

const options: any = deepmergeWrapper(DEFAULT_REQUEST_OPTIONS, this.networkConfig.globalRequestOptions, requestOptions);

this.storage.enrichRequestOptions(url, options);

const templatedUrl: string = (new UriTemplate(url)).fill(options.params);
const { cleanUrl, requestOptions: options, urlWithParams } = this.extractRequestInfo(url, requestOptions);

const urlQueryParams: object = getQueryParams(templatedUrl);
options.params = Object.assign(urlQueryParams, options.params);

const cleanUrl: string = removeQueryParams(templatedUrl);
const queryParamsString: string = makeQueryParamsString(options.params, true);
const urlWithParams = queryParamsString ? `${cleanUrl}?${queryParamsString}` : cleanUrl;

options.params = makeHttpParams(options.params, this.httpParamsOptions);

return this.http.get<T>(cleanUrl, options).pipe(
return this.http.get<T>(cleanUrl, options as any).pipe(
map((response: HttpResponse<T>) => {
const rawResource: RawHalResource = this.extractResourceFromResponse(response);
return this.processRawResource(rawResource, modelClass, singleResource, response, urlWithParams, storePartialModels);
}),
catchError((response: HttpResponse<T>) => {
if (response.status === 304) {
const cachedModel: T = this.storage.get(url);
const cachedModel: T = this.storage.get(url) || this.storage.get(response.url);

if (cachedModel) {
return of(cachedModel);
Expand All @@ -705,6 +723,34 @@ export class DatastoreService {
);
}

private extractRequestInfo(
url: string,
options: RequestOptions
): { cleanUrl: string, urlWithParams: string, requestOptions: RequestOptions } {
const params: object = this.ensureParamsObject(options.params || {});
Object.assign(options, { params });
const requestOptions: RequestOptions = deepmergeWrapper(DEFAULT_REQUEST_OPTIONS, this.networkConfig.globalRequestOptions, options);

this.storage.enrichRequestOptions(url, options);

const templatedUrl: string = (new UriTemplate(url)).fill(options.params);

const urlQueryParams: object = getQueryParams(templatedUrl);
requestOptions.params = Object.assign(urlQueryParams, requestOptions.params);

const cleanUrl: string = removeQueryParams(templatedUrl);
const queryParamsString: string = makeQueryParamsString(requestOptions.params, true);
const urlWithParams = queryParamsString ? `${cleanUrl}?${queryParamsString}` : cleanUrl;

requestOptions.params = makeHttpParams(requestOptions.params, this.httpParamsOptions);

return {
cleanUrl,
urlWithParams,
requestOptions
};
}

private ensureParamsObject(
params: HttpParams | { [param: string]: string | string[] } | object
): { [param: string]: string | string[]} | object {
Expand Down
5 changes: 5 additions & 0 deletions projects/ngx-hal/src/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ export * from './lib/models/hal.model';
export * from './lib/classes/hal-document';
export * from './lib/classes/pagination';

export * from './lib/classes/hal-storage/hal-storage';
export * from './lib/classes/hal-storage/etag-hal-storage';
export * from './lib/classes/hal-storage/simple-hal-storage';
export * from './lib/classes/hal-storage/cache-first-fetch-later.storage';

export * from './lib/enums/cache-strategy.enum';

export * from './lib/interfaces/network-config.interface';
Expand Down

0 comments on commit a0eba20

Please sign in to comment.