Skip to content

Commit

Permalink
feature(requests): add support for removing a single request cache by…
Browse files Browse the repository at this point in the history
… key(#515) (#516)

New function 'deleteRequestsCache'

resolves #515
  • Loading branch information
smnsht authored Mar 25, 2024
1 parent f097fb8 commit 9dbc51d
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 44 deletions.
10 changes: 10 additions & 0 deletions docs/docs/features/requests/requests-cache.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,13 @@ import { clearRequestsCache } from '@ngneat/elf-requests';

store.update(clearRequestsCache());
```

### `deleteRequestsCache`

```ts
import { deleteRequestsCache } from '@ngneat/elf-requests';

store.update(deleteRequestsCache('keyOne'));

store.update(deleteRequestsCache(['keyOne', 'keyTwo']));
```
1 change: 1 addition & 0 deletions packages/requests/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export {
selectRequestCache,
selectIsRequestCached,
clearRequestsCache,
deleteRequestsCache,
updateRequestsCache,
} from './lib/requests-cache';

Expand Down
121 changes: 92 additions & 29 deletions packages/requests/src/lib/requests-cache.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createState, Store } from '@ngneat/elf';
import {
CacheState,
clearRequestsCache,
deleteRequestsCache,
getRequestCache,
isRequestCached,
selectIsRequestCached,
Expand All @@ -15,9 +16,8 @@ import { expectTypeOf } from 'expect-type';
import { createRequestsCacheOperator } from '..';

describe('requestsCache', () => {
const { state, config } = createState(
withRequestsCache<'users' | `user-${string}`>()
);
const { state, config } =
createState(withRequestsCache<'users' | `user-${string}`>());
const store = new Store({ state, config, name: 'users' });

it('should work', () => {
Expand Down Expand Up @@ -60,14 +60,13 @@ describe('requestsCache', () => {
// It's partial not full
expect(store.query(isRequestCached('users'))).toBeFalsy();
expect(
store.query(isRequestCached('users', { value: 'partial' }))
store.query(isRequestCached('users', { value: 'partial' })),
).toBeTruthy();
});

it('should updateRequestCache', () => {
const { state, config } = createState(
withRequestsCache<'users' | `user-${string}`>()
);
const { state, config } =
createState(withRequestsCache<'users' | `user-${string}`>());
const store = new Store({ state, config, name: 'users' });
const key = 'users';

Expand All @@ -78,7 +77,7 @@ describe('requestsCache', () => {
store.update(updateRequestCache(key, { value: 'partial' }));
expect(store.query(isRequestCached(key))).toBeFalsy();
expect(
store.query(isRequestCached(key, { value: 'partial' }))
store.query(isRequestCached(key, { value: 'partial' })),
).toBeTruthy();

store.update(updateRequestCache(key, { value: 'none' }));
Expand All @@ -87,9 +86,8 @@ describe('requestsCache', () => {
});

it('should skipWhileCached', () => {
const { state, config } = createState(
withRequestsCache<'users' | `user-${string}`>()
);
const { state, config } =
createState(withRequestsCache<'users' | `user-${string}`>());
const store = new Store({ state, config, name: 'users' });
const skipWhileUsersCached = createRequestsCacheOperator(store);

Expand All @@ -113,9 +111,8 @@ describe('requestsCache', () => {
});

it('should uphold ttl', () => {
const { state, config } = createState(
withRequestsCache<'users' | `user-${string}`>()
);
const { state, config } =
createState(withRequestsCache<'users' | `user-${string}`>());
const store = new Store({ state, config, name: 'users' });

jest.useFakeTimers();
Expand All @@ -124,25 +121,25 @@ describe('requestsCache', () => {
store.update(updateRequestCache(ttlRequestKey, { ttl: 1000 }));

expect(
store.query(isRequestCached(ttlRequestKey, { value: 'full' }))
store.query(isRequestCached(ttlRequestKey, { value: 'full' })),
).toBeTruthy();

jest.advanceTimersByTime(2000);

expect(
store.query(isRequestCached(ttlRequestKey, { value: 'full' }))
store.query(isRequestCached(ttlRequestKey, { value: 'full' })),
).toBeFalsy();

store.update(updateRequestCache(ttlRequestKey, { ttl: 1000 }));

expect(
store.query(isRequestCached(ttlRequestKey, { value: 'full' }))
store.query(isRequestCached(ttlRequestKey, { value: 'full' })),
).toBeTruthy();

jest.advanceTimersByTime(2000);

expect(
store.query(isRequestCached(ttlRequestKey, { value: 'full' }))
store.query(isRequestCached(ttlRequestKey, { value: 'full' })),
).toBeFalsy();

jest.useRealTimers();
Expand Down Expand Up @@ -175,10 +172,77 @@ describe('requestsCache', () => {
});
});

describe('deleteRequestsCache', () => {
let store: Store;

beforeEach(() => {
const { state, config } = createState(withRequestsCache<'qux' | 'fred'>());

store = new Store({ state, config, name: 'users' });

store.update(
updateRequestsCache({
qux: {
value: 'full',
},
fred: {
value: 'full',
},
}),
);

expect(store.getValue()).toMatchInlineSnapshot(`
Object {
"requestsCache": Object {
"fred": Object {
"value": "full",
},
"qux": Object {
"value": "full",
},
},
}
`);
});

it('should clear single key', () => {
store.update(deleteRequestsCache('qux'));

expect(store.getValue()).toMatchInlineSnapshot(`
Object {
"requestsCache": Object {
"fred": Object {
"value": "full",
},
"qux": Object {
"value": "none",
},
},
}
`);
});

it('should clear all keys', () => {
store.update(deleteRequestsCache(['qux', 'fred']));

expect(store.getValue()).toMatchInlineSnapshot(`
Object {
"requestsCache": Object {
"fred": Object {
"value": "none",
},
"qux": Object {
"value": "none",
},
},
}
`);
});
});

test('updateRequestsCache', () => {
const { state, config } = createState(
withRequestsCache<'foo' | 'bar' | 'baz'>()
);
const { state, config } =
createState(withRequestsCache<'foo' | 'bar' | 'baz'>());

const store = new Store({ state, config, name: 'users' });

Expand All @@ -187,7 +251,7 @@ test('updateRequestsCache', () => {
foo: {
value: 'partial',
},
})
}),
);

expect(store.getValue()).toMatchSnapshot();
Expand All @@ -200,7 +264,7 @@ test('updateRequestsCache', () => {
bar: {
value: 'full',
},
})
}),
);

expect(store.getValue()).toMatchSnapshot();
Expand All @@ -211,24 +275,23 @@ test('updateRequestsCache', () => {
});

test('updateRequestsCache with ttl', () => {
const { state, config } = createState(
withRequestsCache<'foo' | 'bar' | 'baz'>()
);
const { state, config } =
createState(withRequestsCache<'foo' | 'bar' | 'baz'>());

const store = new Store({ state, config, name: 'users' });

jest.useFakeTimers();

store.update(
updateRequestsCache(['foo', 'bar'], { value: 'partial', ttl: 1000 })
updateRequestsCache(['foo', 'bar'], { value: 'partial', ttl: 1000 }),
);

expect(
store.query(isRequestCached('foo', { value: 'partial' }))
store.query(isRequestCached('foo', { value: 'partial' })),
).toBeTruthy();

expect(
store.query(isRequestCached('bar', { value: 'partial' }))
store.query(isRequestCached('bar', { value: 'partial' })),
).toBeTruthy();

jest.advanceTimersByTime(2000);
Expand Down
38 changes: 23 additions & 15 deletions packages/requests/src/lib/requests-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export type CacheState = {
};

export function withRequestsCache<Keys extends string>(
initialValue?: Record<Keys, CacheState>
initialValue?: Record<Keys, CacheState>,
): PropsFactory<{ requestsCache: Record<Keys, CacheState> }, EmptyConfig> {
return {
props: {
Expand All @@ -44,29 +44,29 @@ export function withRequestsCache<Keys extends string>(
}

export function selectRequestCache<S extends RequestsCacheState>(
key: CacheRecordKeys<S>
key: CacheRecordKeys<S>,
): OperatorFunction<S, CacheState> {
return pipe(
distinctUntilKeyChanged('requestsCache'),
select((state) => getRequestCache(key)(state))
select((state) => getRequestCache(key)(state)),
);
}

export function updateRequestsCache<S extends RequestsCacheState>(
keys: Array<CacheRecordKeys<S>>,
value: CacheState | { value: CacheState['value']; ttl?: number }
value: CacheState | { value: CacheState['value']; ttl?: number },
): Reducer<S>;
export function updateRequestsCache<S extends RequestsCacheState>(
requests: Partial<
Record<
CacheRecordKeys<S>,
CacheState | { value: CacheState['value']; ttl?: number }
>
>
>,
): Reducer<S>;
export function updateRequestsCache<S extends RequestsCacheState>(
requestsOrKeys: any,
value?: any
value?: any,
): Reducer<S> {
let normalized = requestsOrKeys;

Expand Down Expand Up @@ -99,7 +99,7 @@ export function updateRequestsCache<S extends RequestsCacheState>(

export function updateRequestCache<S extends RequestsCacheState>(
key: CacheRecordKeys<S>,
{ ttl, value: v }: { ttl?: number; value?: CacheState['value'] } = {}
{ ttl, value: v }: { ttl?: number; value?: CacheState['value'] } = {},
): Reducer<S> {
const data = {
value: v ?? 'full',
Expand All @@ -120,7 +120,7 @@ export function updateRequestCache<S extends RequestsCacheState>(
}

export function getRequestCache<S extends RequestsCacheState>(
key: CacheRecordKeys<S>
key: CacheRecordKeys<S>,
): Query<S, CacheState> {
return function (state: S) {
const cacheValue =
Expand All @@ -141,30 +141,30 @@ export function getRequestCache<S extends RequestsCacheState>(

export function selectIsRequestCached<S extends RequestsCacheState>(
key: Parameters<typeof isRequestCached>[0],
options?: { value?: CacheState['value'] }
options?: { value?: CacheState['value'] },
): OperatorFunction<S, boolean> {
return pipe(
distinctUntilKeyChanged('requestsCache'),
select((state) => isRequestCached(key, options)(state))
select((state) => isRequestCached(key, options)(state)),
);
}

export function isRequestCached<S extends RequestsCacheState>(
key: OrArray<CacheRecordKeys<S>>,
options?: { value?: CacheState['value'] }
options?: { value?: CacheState['value'] },
) {
return function (state: S) {
const type = options?.value ?? 'full';
return coerceArray(key).some(
(k) => getRequestCache(k)(state).value === type
(k) => getRequestCache(k)(state).value === type,
);
};
}

export function skipWhileCached<S extends RequestsCacheState, T>(
store: Store<StoreDef<S>>,
key: OrArray<CacheRecordKeys<S>>,
options?: { value?: CacheState['value']; returnSource?: Observable<any> }
options?: { value?: CacheState['value']; returnSource?: Observable<any> },
): MonoTypeOperatorFunction<T> {
return function (source: Observable<T>) {
if (store.query(isRequestCached(key, { value: options?.value }))) {
Expand All @@ -176,11 +176,11 @@ export function skipWhileCached<S extends RequestsCacheState, T>(
}

export function createRequestsCacheOperator<S extends RequestsCacheState>(
store: Store<StoreDef<S>>
store: Store<StoreDef<S>>,
) {
return function <T>(
key: CacheRecordKeys<S>,
options?: Parameters<typeof skipWhileCached>[2]
options?: Parameters<typeof skipWhileCached>[2],
) {
return skipWhileCached<S, T>(store, key, options);
};
Expand All @@ -194,3 +194,11 @@ export function clearRequestsCache<S extends RequestsCacheState>(): Reducer<S> {
};
};
}

export function deleteRequestsCache<S extends RequestsCacheState>(
keys: OrArray<CacheRecordKeys<S>>,
): Reducer<S> {
return updateRequestsCache(coerceArray(keys), {
value: 'none',
});
}

0 comments on commit 9dbc51d

Please sign in to comment.