rxjs-response-cache
v1.0.1
Published
Global cache service for responses fetched by RxJS observables
Downloads
3
Maintainers
Readme
RxJS Response Cache
Have you ever struggled with efficiently caching API responses retrieved through observables?
It gets trickier, especially when dealing with dynamic filters like pagination in APIs. Or consider scenarios where you're confident that a response is unlikely to change during a user's visit to your website. However, deciding when to expire the cache becomes a challenge, potentially leading to users receiving outdated information or setting such a short expiration time that it becomes impractical.
In many cases, these challenges lead us to skip caching, resulting in less satisfying user experience and performance.
Now, let's dive into RxJS Response Cache and see how it streamlines caching.
The package automatically stores a diverse range of responses, assigning them unique keys generated from a combination of the URL and alphabetically sorted query parameters. This ensures that each distinct response is effortlessly stored and managed.
Also, with its use-stale-while-refreshing feature, not only does it reduce wait times, but it guarantees users consistently have the most up-to-date data at their fingertips.
Check the Live Demo
Main Features
- Global accessibility throughout the application.
- Uses stale data during refresh.
- Accelerates data access.
- Reduces network requests.
- Simplifies prefetching.
- Includes clear timeouts for precise caching control.
- Integrated DevTool for visual cache event inspection.
- Designed for easy use.
Document Main Sections
- Main Features
- Usage Examples
- Usage in Angular
- Cache Structure and Auto-Generated Keys
- Determining When to Use a Unique Identifier
- Prefetching
- Cleaning the Data
- Resetting the Cache
- Updating the Cache
- How Refreshing Works with RxJS Subscribers
- Multiple Instances
- Bulk Operations
- Null Values in Query Params
- Developer Tool
- API Reference
Usage Examples
Install the package:
npm install rxjs-response-cache --save
or
yarn add rxjs-response-cache
Instantiate the cache service at the root of your application or any other location within the components tree.
import ResponseCache from 'rxjs-response-cache';
const cache = new ResponseCache({
isDevMode: process.env.MODE === "development",
devtool: {
show: true,
},
});
See Configuration Available Parameters
Supply it as needed and start using it as follows:
Please Note that you can use the get()
method (which returns a new observable) in 2 ways:
- Using arrangedUrl
- Ignoring arrangedUrl
arrangedUrl is a part of the auto-generated key used by the service to store data.
It's a combination of provided url
, string query parameters (if they exist in url parameter),
defaultParams
and params
.
The values are alphabetically sorted, and empty strings, undefined, and null values are
automatically removed (null value removal can be configured).
For a deeper understanding, refer to the Cache Structure and Auto-Generated Keys section.
Method 1 ( Using arrangedUrl ):
const getPosts = () => {
return cache.get<Post[]>({
url: "posts",
defaultParams: {page: 1, "page-size": 20},
params: urlParamsObject,
observable: ({arrangedUrl}) => observable<Post[]>(arrangedUrl),
}).pipe(your_operations);
}
Method 2 ( Ignoring arrangedUrl argument and working with your own data ):
const getPosts = () => {
const url = "posts";
const params = urlParamsObject;
return cache.get<Post[]>({
url: url,
defaultParams: {page: 1, "page-size": 20},
params: params,
observable: () => observable<Post[]>(url, {params}),
}).pipe(your_operations);
}
Read the following section to understand when to use each method?
Best practice: Chain the pipe()
s to the get()
method, not the passed observable.
This ensures that the actual API response, not a potentially modified version, is stored in the cache,
and prevents potential bugs when working with the same API but different operations in separate modules.
Important Hint: Ensure that you also provide the parameters (if they exist) to the get() method. This is essential as the service uses all query parameters to generate unique keys.
Additionally, to achieve the best possible results from the service, always include your API default parameters when they can be altered by the end-user. This prevents the generation of two different keys for /posts and /posts?page=1, even though they are essentially the same.
Read the Cache Structure and Auto-Generated Keys section for more details.
See Get Method Available Parameters
And then:
getPost().subscribe();
Determining When to Use Second Method
You may opt for the second method only when there's a specific requirement that is ignored in arrangedUrl. In arrangedUrl, all empty strings, undefined, and null values are automatically removed (ignoring null values can be configured). Additionally, duplicated query parameters are overwritten, and you should concatenate them with commas if you genuinely need all of them. If this behavior doesn't meet your needs, consider using the second method and work with your own data.
Usage Example in Angular
Hint: Ensure you have read the Usage Example section first.
import ResponseCache from 'rxjs-response-cache';
function cacheFactory() {
return new ResponseCache({
isDevMode: isDevMode(),
devtool: your_desired_options,
});
}
@NgModule({
providers: [
{provide: ResponseCache, useFactory: cacheFactory},
],
})
And start using it in your services:
getPosts = () => {
return this.cache.get<Post[]>({
url: "posts",
observable: ({arrangedUrl}) => this._httpClient.get<Post[]>(arrangedUrl),
...other_params,
});
}
And then in your components:
getPost().subscribe();
Cache Structure and Auto-Generated Keys
The cache is a map of auto-generated keys and the data. For example, a code snippet like this:
const getPosts = () => {
return cache.get<Post[]>({
url: "posts",
defaultParams: {page: 1 },
params: {
page: url.query.page,
"start-date": some_date,
"end-date": some_date,
"some-other-param": is_undefined_for_now
},
observable: ({arrangedUrl}) => observable<Post[]>(arrangedUrl),
}).pipe(your_operations);
}
Will update the cache to this:
const cache = {
"posts? end-date=some_date & page=some_number & start-date=some_date": res
}
arrangedUrl
passed as an argument to your observable is essentially this auto-generated key.
Please note that the query parameters are sorted and undefined value is removed.
Best practice: Chain the pipe()
s to the get()
method, not the passed observable.
This ensures that the actual API response, not a potentially modified version, is stored in the cache,
and prevents potential bugs when working with the same API but different operations in separate modules.
Determining When to Use a Unique Identifier
This value, if present, will be added to the auto-generated key for storing the data. In most cases (99.99%), it's unnecessary. Consider using it only if you must differentiate between two data types which are going to generate the exact same key.
Prefetching
Simply subscribe to your API handler, and the result will be stored in the cache for later use.
getPost().subscribe();
Cleaning the Data
The clean() method allows you to remove specific data or multiple entries from the cache.
Hint: if you used uniqueIdentifier
, make sure to include it in the second parameter.
Note: The default behavior for queries is NOT based on an exact match.
Examples
Picture the cache in this state:
const cache = {
"posts?page=1" : data,
"posts?page=1&comments=true" : data,
"posts?page=2": data,
"tweaked_posts__posts?page=1" : tweakedData,
}
To clean all the keys containing "posts" & page=1 (matches the 2 first keys):
cache.clean('posts',{ queryParams: { page: 1} })
To clean one key, containing "posts" & page=1 (exact match):
cache.clean('posts',{ queryParams: { page: 1}, exact: true })
Please note that neither of the above examples removes the fourth and fifth keys because uniqueIdentifier is not included in the options.
To clean all the keys containing "posts" & uid=tweaked_posts (matches only the forth key):
cache.clean('posts',{ uniqueIdentifier: "tweaked_posts", queryParams: { comments: true} })
See Clean Method Available Parameters
Resetting the Cache
The reset()
method clears the entire cache.
cache.reset();
Updating the Cache
Coming Soon: Update functionality is slated for the next minor version release!
How Refreshing Works with RxJS Subscribers
If the data is not present in the cache, subscriber.next()
and subscriber.complete()
are triggered
when the request is resolved.
If the data is already present in the cache, subscriber.next()
is immediately triggered with the stale data.
By default, once the request is resolved, the newly fetched data is compared to the stale data. If they differ,
subscriber.next()
is invoked again with the fresh data, and ultimately, subscriber.complete()
is triggered.
This equality check can be disabled in the configuration, causing subscriber.next()
to be called twice,
even if the data is identical to the cached version.
Please note that you should stop rendering spinners and skeletons into the next()
function not the complete()
,
when using the refresh feature.
Multiple Instances
Using multiple instances of the service is supported, but the devtool should be used with one instance at a time.
Bulk Operations
The get()
method returns a new observable, so use it with bulk operations as usual.
Example:
const res = forkJoin({ foo: cache.get(), bar: cache.get() })
Null Values in Query Params
Null values are ignored from query parameters by default. This behavior can be changed in the cache configuration at instantiation.
See Configuration Available Parameters
Developer Tool
The integrated developer tool allows you to inspect the last state of the cache and its history of changes. Additionally, every event related to the cache will be logged in the tool.
See Devtool Available Parameters
API Reference
Configuration Parameters
| Name | Type | Description |
|:----------------------------------|:----------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| isDevMode | boolean | In dev mode, clear timeout IDs will be stored in local storage to be cleared in possible hot-reloads. This ensures that the devtool does not display incorrect information from previous loads during development.Additionally, the devtool is available only in dev mode. |
| paramsObjectOverwrites-UrlQueries | boolean [=true] | Determines how the service should behave if a query parameter is accidentally present in both the url parameter and the params parameter.Example: cache.get({url: "/posts?page=2", params: {page: 3}, observable:() => observable})
by default will be resolved to "/post?page=3"
. |
| preventSecondCallIfDataIsUnchanged | boolean [=true] | Determines whether the observable.next()
should be invoked again when the refreshed data is identical to the stale data.By default, the observable.next()
is invoked only once in such cases, optimizing to prevent unnecessary rerenders in applications.If desired, you can pass false
and perform your own check within your application.For a detailed explanation, please refer to the How Refreshing Works with RxJS Subscribers section. |
| removeNullValues | boolean [=true] | Determines whether null values should be removed from query parameters or not. |
| devtool | object [:?] | Developer tool configuration. See Devtool Available Parameters. |
Service Instance Methods & Properties
| Name | Type | Description | |:--------------|:---------|:-----------------------------------------------------------------------| | get() | function | Fetches data and stores the expected result in the cache. | | clean() | function | Allows you to remove specific data or multiple entries from the cache. | | reset() | function | Clears the entire cache. | | config | object | Configuration passed to the service. | | data | object | Stored data. | | observables | object | Stored observables. | | clearTimeouts | object | Active clear timeouts. |
Get Method Parameters
| Name | Type | Description |
|:-----------------|:-----------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| url | string | The endpoint address (may include query parameters or not). |
| observable | () => function | The callback function that returns an observable. It receives an object containing the arrangedUrl
as input.See Cache Structure and Auto-Generated Keys for details. |
| uniqueIdentifier | string [:?] | This value, if present, will be added to the auto-generated key for storing the data.See When to Use Unique Identifier . |
| defaultParams | object [:?] | The API's default query parameters.To optimize cache results, ensure to include them if they can be altered by the end-user. |
| params | object [:?] | The queryParams will overwrite the defaultParams, and by default (configurable), any query strings in the url parameter will also be overwritten. |
| refresh | boolean [=false] | Determines if the data should be refreshed on the next calls or noDetermines if the data should refresh on subsequent calls.By default, the API will be called only once.Passing true
is especially useful when you are unsure if the data will remain the same. This way, users receive the old data immediately and then see the newly fetched data if there are any changes. |
| clearTimeout | number [?:] | The time in milliseconds used to remove the data from the cache. |
Clean Method Parameters
| Name | Type | Description |
|:-------------------------|:-------------|:---------------------------------------------------------------------------------------------------------------------------------------------------|
| url | string | The endpoint address (may include query parameters or not).DO NOT include the uniqueIdentifier
part here. |
| options | object [?:] | Extra options for cleaning. |
| options.exact | boolean [?:] | Determines if the query should be based on an exact match or not. |
| options.uniqueIdentifier | string [?:] | Unique identifier.Note: If the key includes a unique identifier, you should pass it here, even if the query is not based on an exact match. |
| options.queryParams | object [?:] | Query Parameters. They will be sorted and truncated if they contain an empty string, undefined, or null (null is configurable). |
See Cleaning the data for examples.
Devtool Parameters
type DevtoolConfig = {
show?: boolean; // default = isDevMode && true
isOpenInitially?: boolean; // default = false
styles?: {
zIndex?: number; // default = 5000
toggleButtonPosition?: {
right?: number; // default = 25
bottom?: number; // default = 25
};
};
}