Skip to content

Commit

Permalink
feat: 🎸 add stateManagement mode
Browse files Browse the repository at this point in the history
  • Loading branch information
NetanelBasal committed Jul 12, 2021
1 parent d00e474 commit 61c59f8
Show file tree
Hide file tree
Showing 11 changed files with 78 additions and 11 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
## Features

✅ HTTP Caching <br>
✅ State Management Mode<br>
✅ Local Storage Support <br>
✅ Handles Simultaneous Requests<br>
✅ Automatic & Manual Cache Busting <br>
Expand Down Expand Up @@ -66,6 +67,28 @@ export class UsersService {

It's as simple as that.

## State Management Mode
When working with state management like `Akita` or `ngrx`, there is no need to save the data both in the cache and in the store because the store is the single source of truth. In such a case, the only thing we want is an indication of whether the data is in the cache.

We can change the mode option to `stateManagement`:
```ts
import { withCache } from '@ngneat/cashew';

@Injectable()
export class UsersService {
constructor(private http: HttpClient) {}

getUsers() {
return this.http.get('api/users', {
context: withCache({
mode: 'stateManagement'
})
});
}
}
```
Now instead of saving the actual response in the cache, it'll save a `boolean` and will return by default an `EMPTY` observable when the `boolean` resolves to `true`. You can change the returned source by using the `returnSource` option.

## Local Storage

By default, caching is done to app memory. To switch to using local storage instead simply add:
Expand Down
2 changes: 2 additions & 0 deletions projects/ngneat/cashew/src/lib/cache-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { InjectionToken } from '@angular/core';

export interface HttpCacheConfig {
strategy: 'implicit' | 'explicit';
mode: 'stateManagement' | 'cache';
ttl: number;
responseSerializer?: (value: any) => any;
}

export const defaultConfig: HttpCacheConfig = {
strategy: 'explicit',
mode: 'cache',
ttl: 3_600_000 // One hour
};

Expand Down
7 changes: 6 additions & 1 deletion projects/ngneat/cashew/src/lib/cache-context.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { HttpContext, HttpContextToken, HttpRequest } from '@angular/common/http';
import { HttpContext, HttpContextToken, HttpRequest, HttpResponse } from '@angular/common/http';
import { EMPTY, Observable } from 'rxjs';
import { CacheBucket } from './cache-bucket';
import { HttpCacheConfig } from './cache-config';

export interface ContextOptions {
cache?: boolean;
Expand All @@ -13,6 +15,8 @@ export interface ContextOptions {
key: string
): boolean;
context?: HttpContext;
mode?: HttpCacheConfig['mode'];
returnSource?: Observable<HttpResponse<any>>;
}

export const CACHE_CONTEXT = new HttpContextToken<ContextOptions>(() => ({}));
Expand All @@ -21,6 +25,7 @@ export function withCache(options: ContextOptions = {}) {
const { context, ...remainingOptions } = options;
return (context ?? new HttpContext()).set(CACHE_CONTEXT, {
cache: true,
returnSource: EMPTY,
...remainingOptions
});
}
20 changes: 16 additions & 4 deletions projects/ngneat/cashew/src/lib/cache-interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,15 @@ export class HttpCacheInterceptor implements HttpInterceptor {

const key = this.keySerializer.serialize(request, context);

const { cache = this.config.strategy === 'implicit', ttl, bucket, clearCachePredicate, version } = context;
const {
cache = this.config.strategy === 'implicit',
ttl,
bucket,
clearCachePredicate,
version,
mode,
returnSource
} = context;

if (version) {
const versions = this.httpCacheManager._getVersions();
Expand Down Expand Up @@ -89,14 +97,18 @@ export class HttpCacheInterceptor implements HttpInterceptor {
log(`${key} was returned from the cache`);
}

return of(this.httpCacheManager.get(key));
return mode === 'stateManagement' ? returnSource! : of(this.httpCacheManager.get(key));
}

const shared = next.handle(request).pipe(
tap(event => {
if (event instanceof HttpResponse) {
const cache = this.httpCacheManager._resolveResponse(event);
this.httpCacheManager._set(key, cache, ttl || this.config.ttl);
if (mode === 'stateManagement') {
this.httpCacheManager._set(key, true, ttl || this.config.ttl);
} else {
const cache = this.httpCacheManager._resolveResponse(event);
this.httpCacheManager._set(key, cache, ttl || this.config.ttl);
}
}
}),
finalize(() => queue.delete(key)),
Expand Down
4 changes: 2 additions & 2 deletions projects/ngneat/cashew/src/lib/cache-manager.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class HttpCacheManager {
}

get<T = any>(key: string): HttpResponse<T> {
return this._resolveResponse<T>(this.storage.get(key)!);
return this._resolveResponse<T>(this.storage.get(key)! as HttpResponse<any>);
}

has(key: string) {
Expand Down Expand Up @@ -113,7 +113,7 @@ export class HttpCacheManager {
return false;
}

_set(key: string, response: HttpResponse<any>, ttl: number) {
_set(key: string, response: HttpResponse<any> | boolean, ttl: number) {
this.storage.set(key, response);
this.ttlManager.set(key, ttl);
}
Expand Down
2 changes: 1 addition & 1 deletion projects/ngneat/cashew/src/lib/cache-storage.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { HttpResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';

export abstract class HttpCacheStorage extends Map<string, HttpResponse<any>> {}
export abstract class HttpCacheStorage extends Map<string, HttpResponse<any> | boolean> {}

@Injectable()
export class DefaultHttpCacheStorage extends HttpCacheStorage {}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class HttpCacheLocalStorage extends HttpCacheStorage {
return super.has(createKey(key)) || !!storage.getItem(createKey(key));
}

get(key: string): HttpResponse<any> {
get(key: string): HttpResponse<any> | boolean {
const cacheValue = super.get(createKey(key));

if (cacheValue) {
Expand Down
5 changes: 3 additions & 2 deletions projects/ngneat/cashew/src/lib/specs/cache-context.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { HttpContext, HttpContextToken } from '@angular/common/http';
import { EMPTY } from 'rxjs';
import { CACHE_CONTEXT, withCache } from '../cache-context';

describe('withCache', () => {
Expand All @@ -11,7 +12,7 @@ describe('withCache', () => {
expect(result === existingContext).toBeTruthy();
const allTokens = Array.from(result.keys());
expect(allTokens.length).toEqual(2);
expect(result.get(CACHE_CONTEXT)).toEqual({ cache: true, ttl: 60000 });
expect(result.get(CACHE_CONTEXT)).toEqual({ cache: true, ttl: 60000, returnSource: EMPTY });
expect(result.get(token)).toEqual(42);
});

Expand All @@ -20,7 +21,7 @@ describe('withCache', () => {
expect(result).toBeDefined();
const allTokens = Array.from(result.keys());
expect(allTokens.length).toEqual(1);
expect(result.get(CACHE_CONTEXT)).toEqual({ cache: true, ttl: 60000 });
expect(result.get(CACHE_CONTEXT)).toEqual({ cache: true, ttl: 60000, returnSource: EMPTY });
expect(result.get(token)).toEqual(0);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,12 @@ describe('HttpCacheInterceptor', () => {
expect(handler.handle).toHaveBeenCalledTimes(1);
});

it('should work with mode stateManagement', () => {
call(request({ cache: true, key: 'foo', mode: 'stateManagement' }), 2);
expect(handler.handle).toHaveBeenCalledTimes(1);
expect((httpCacheInterceptor as any).httpCacheManager.storage.get('foo')).toEqual(true);
});

describe('clearCachePredicate', () => {
it('should NOT clear the cache when return false', () => {
call(
Expand Down
5 changes: 5 additions & 0 deletions src/app/todos/todos.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ <h2>Load Simultaneous</h2>

<hr />

<h2>State Management</h2>
<button (click)="stateManagement()">Load</button>

<hr />

<h2>Bucket of Ids</h2>
<button (click)="getById(1)">Todo 1</button>
<button (click)="getById(2)">Todo 2</button>
Expand Down
13 changes: 13 additions & 0 deletions src/app/todos/todos.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,19 @@ export class TodosComponent {
});
}

stateManagement() {
this.http
.get('https://jsonplaceholder.typicode.com/todos', {
context: withCache({
key: 'testState',
mode: 'stateManagement'
})
})
.subscribe(res => {
console.log(`testState`, res);
});
}

clearTodosCache() {
this.manager.delete(this.todosBucket);
}
Expand Down

0 comments on commit 61c59f8

Please sign in to comment.