Coder Social home page Coder Social logo

ezzabuzaid / angular-caching Goto Github PK

View Code? Open in Web Editor NEW
1.0 1.0 0.0 221 KB

Cache API requests in Angular

Home Page: https://dev.to/ezzabuzaid/angular-api-caching-2p12

License: MIT License

JavaScript 5.33% TypeScript 23.60% HTML 70.85% CSS 0.22%

angular-caching's Introduction

Angular Caching

This article will take you through the way to efficiently handle the HTTP client request and cache them inside different browser storage.

For clarity about what Iโ€™m going to talk about, the full project is available to browse through Github.

So, Caching is a way to store data (response in our case) in storage to quickly access it later on.

There're many advantages of caching in general so I'll just point out some of what the Front End interested in

  • Reduce the number of requests
  • Improved responsiveness by retrieving the response immediately

When to use

  • Response that doesn't frequently change
  • Request that the application addressing frequently
  • Show some data while there's no internet connection to provide an offline experience

and there's a lot of other use cases and it all depends on your business case.

Implementation

first of all, you need to run npm install @ezzabuzaid/document-storage , we will use this library to facilitate and unify different storage access, of course, you can use whatever you see suitable

declare an entry class that will represent the entry in the cache

/**
 * class Represent the entry within the cache
 */
export class HttpCacheEntry {
    constructor(
        /**
         * Request URL
         *
         * will be used as a key to associate it with the response
         */
        public url: string,
        /**
         * the incoming response
         *
         * the value will be saved as a string and before fetching the data we will map it out to HttpResponse again
         */
        public value: HttpResponse<any>,
        /**
         * Maximum time for the entry to stay in the cache
         */
        public ttl: number
    ) { }
}

create an injection token to deal with dependency injection. for our case, we need to register it for application-wide so we provide it in the root. I'm using IndexedDB here but it's your call to choose.

export const INDEXED_DATABASE = new InjectionToken<AsyncDatabase>(
    'INDEXED_DB_CACHE_DATABASE',
    {
        providedIn: 'root',
        factory: () => new AsyncDatabase(new IndexedDB('cache'))
    }
);

here is a list of available storages

  1. LocalStorage
  2. SessionStorage
  3. IndexedDB
  4. InMemory
  5. WebSql
  6. Cache API
  7. Cookie

after setup the storage we need to implement the save and retrieve functionality

@Injectable({
    providedIn: 'root'
})
export class HttpCacheHelper {
    private collection: AsyncCollection<HttpCacheEntry> = null;

    constructor(
        @Inject(INDEXED_DATABASE) indexedDatabase: AsyncDatabase,
    ) {
        // collection is a method the came from `document-storage` library to originze /
        // the data in different namespaces, so here we defined 'CACHE' namespace to
        // save all cache related things to it
        // collection provide different method to store are retrive data
        this.collection = indexedDatabase.collection('CACHE');
    }

    /**
     *
     * @param url: request URL including the path params
     * @param value: the request-response
     * @param ttl: the maximum time for the entry to stay in the cache before invalidating it
     *
     * Save the response in the cache for a specified time
     *
     */
    public set(url: string, value: HttpResponse<any>, ttl: number) {
        return this.collection.set(new HttpCacheEntry(url, value, ttl));
    }

    /**
     *
     * @param url: request URL including the path params
     *
     * Retrieve the response from the cache database and map it to HttpResponse again.
     *
     * if TTL end, the response will be deleted and null will return
     */
    public get(url: string) {
        return from(this.collection.get((entry) => entry.url === url))
            .pipe(
                switchMap((entry) => {
                    if (entry && this.dateElapsed(entry.ttl ?? 0)) {
                        return this.invalidateCache(entry);
                    }
                    return of(entry);
                }),
                map(response => response && new HttpResponse(response.value)),
            );
    }

    /**
     * Clear out the entire cache database
     */
    public clear() {
        return this.collection.clear();
    }

    private invalidateCache(entry: Entity<HttpCacheEntry>) {
        return this.collection.delete(entry.id).then(_ => null);
    }

    private dateElapsed(date: number) {
        return date < Date.now();
    }
}

all that you need now is to inject the HttpCacheHelper and use the set and get functions

we will use set and get functions later on in the interceptor as another layer to make the code clear as possible.

Cache Invalidation

Imagine that the data is saved in storage and everything works as expected, but the server database has been updated, and eventually, you want to update the data in the browser storage to match what you have in the server. there are different approaches to achieve this, like open WebSocket/SSE connection to notify the browser for an update, set an expiry time for your data (TTL) or by versioning your cache so when you change the version the old data became invalid

  • TTL

Time To Live is a way to set the limited lifetime for a record so we can know in further when it will become a stall

it's implemented in the above example where we check if the TTL is expired

  • Version key

We can replace the TTL with version key so instead of checking if the date elapsed we can check if the version changed I can see two approaches

  1. Using the version that specified in package.json
  2. Retrieve version from API

e.g: the current version will be stored with the cache entry and whenever you fetch the data again you check if the version of the cache entry equal to the application version then you can either return the entry or delete it

for more clarification about how to deal with package json version I would suggest to read this [article](https://medium.com/@tolvaly.zs/how-to-version-number-angular-6-applications-4436c03a3bd3#:~:text=Briefly%3A%20we%20will%20import%20the, should%20already%20have%20these%20prerequisites).

  • WebSocket/SSE

  • On-Demand

Make the user responsible for fetching the latest data from the server

  • Meta-Request

you can use head request for example to

Usage

import { Injectable } from '@angular/core';
import { HttpClient, HttpResponse } from '@angular/common/http';
import { HttpCacheHelper, HttpCacheEntry } from './cache/cache.helper';
import { switchMap, tap } from 'rxjs/operators';
import { of } from 'rxjs';

@Injectable()
export class ExampleService {

    constructor(
        private httpClient: HttpClient,
        private httpCacheHelper: HttpCacheHelper
    ) { }

    getData() {
        const url = '/endpoint-url';
        // Check if there's data in the cache
        this.httpCacheHelper.get(url)
            .pipe(switchMap(response => {
                // return the cached data if available
                if (response) {
                    return of(response);
                }
                // fetch data from the server
                return this.httpClient.get<HttpResponse<any>>(url, { observe: 'response' })
                    .pipe(tap((response) => {
                        // save the response in order to use it in subsequent requests
                        this.httpCacheHelper.set(url, response, 60);
                    }))
            }));
    }

}

First, we check if the data is in the cache and return if available, if not, we do call the backend to fetch the data then we save it in the cache to make it available in the subsequent calls

Caching Strategy

there's a different way to decide how and when to fetch the data from client cache or server, like the one we implemented is called Cache First strategy

  1. cache first

Implies that the cache has a higher priority to fetch data from

  1. network first

As opposite, fetch data from the network and if an error occurred or no internet connection use cache as a fallback

  • Please note that the above strategies work with read request*

also, there are ways to cache the read requests e.g: you have a dashboard that tracks user movements and you don't need to submit every move, therefore you can save all the movement in the cache and after a certain time you submit it

I'm not going to explain caching for a written request, just know that it's possible.

Summary

  1. Each store has it's own characteristics.
  2. Cache invalidation is a must and you should always guarantee that you have the latest data.

Database just fancy name.

angular-caching's People

Contributors

ezzabuzaid avatar

Stargazers

 avatar

Watchers

 avatar

angular-caching's Issues

how does one cast as specific types?

Your caching code is elegant, but I'm afraid I'm wrapped around the axle trying to use it.

the httpCacheHelper.get method returns a type of Observable<HttpResponse>
whereas my uncacheable method allows me to cast the expected type.
http.get<Sector[]>(api).subscribe(result => {this.sectors = result; }, error => console.error(error));

I'm wondering if you might have an example of such?

Part 1 Update.

Angular API Caching

Update (Date Here): change the implementation to use localForage library.

This article will take you through the way to efficiently handle the HTTP client request and cache them inside different browser storage.

For clarity about what Iโ€™m going to talk about, the full project is available to browse through Github.

What Is Caching

Caching is a way to store data that is often in use (HTTP response in our case) in storage to quickly access it later on. So instead of asking for the same data twice, you store a copy of it, and later on, you got that copy instead of the original.

How It Works

  1. A request made to server X to get data Y.
  2. The request goes through the cache to check if there's a copy from point 4.
  3. No copy found so the request will be delegated to the actual server. otherwise, jump to point 5.
  4. The server returned data Y and pass a copy through the cache.
  5. The request initiator gets the data to deal with it.
  6. Data will remain in cache till it expires or the cache is cleared.

That is the basic flow of how cache works.

There're many advantages of caching in general so I'll just point out some of what the Front End interested in

When/Why To Use Cache

  • Reduce the number of requests.
  • Store response that doesn't frequently change.
  • Request that the application addressing frequently.
  • Improve responsiveness by retrieving the response immediately.
  • Show some content while you requesting the fresh content.
  • Show some data while there's no internet connection to provide an offline experience

And of course, there might be other use cases depends on your custom usage.

Where To Store The Cache Data

  1. LocalStorage
  2. SessionStorage
  3. IndexedDB
  4. WebSql
  5. Cache API
  6. Cookie
  7. And InMemory ๐Ÿ˜

We will use localForage library that acts as one interface for most of the mentioned storages, of course, you can use whatever you see suitable.

you can install from here npm install localforage

Example Usage

import { Injectable } from '@angular/core';
import { HttpClient, HttpResponse } from '@angular/common/http';

@Injectable()
export class ExampleService {

    constructor(
        private httpClient: HttpClient,
    ) { }

    getData() {
        const url = '/endpoint-url';
        return this.httpClient.get(url);
    }

}

Sample showing an example service that gets data from a server

import * as localforage from 'localforage';
const store = localforage.createInstance({
    driver: localforage.INDEXEDDB, /** Here where you can change the storage */
    version: 1.0,
    storeName: 'cache',
});

Configure a store, I'm using IndexedDB but feel free to use anything else.


Going back to the start.

  1. Initiate a request to server X to get data Y. (the request initiator is getData method )
  2. The request goes through the cache to check if there's a copy from point 4.
  3. No copy found so the request will be delegated to the actual server. otherwise, jump to point 5.
  4. The server returned data Y and pass a copy through the cache.
  5. The request initiator gets the data to deal with it.
  6. Data will remain in cache till it expires or the cache is cleared.
getData() {
    const url = 'https://jsonplaceholder.typicode.com/todos/1';
    return defer(() => store.getItem(url) /** step 2 */)
        .pipe(switchMap((cachedData) => {
            if (cachedData === null || cachedData === undefined) { /** step 3 */
                return this.httpClient.get(url) /** step 1 */
                    .pipe(switchMap((newData) => store.setItem(url, newData)));
            }
            return of(cachedData); /** step 5 */
        }))
}

defer used to create an Observable from Promise.

More about caching.

Caching Strategy

You might not want always to return the data from the cache perhaps you want only to hit the cache if the server call failed in this case you have something to show to the user or you want to hit the cache and the server so you have something to present quickly from the cache in the same time you're getting the fresh data from the server.

  1. Cache First (Cache Falling Back to Network)
    This implies that the cache has a higher priority to fetch data from. what we already did above.

  2. Network First (Network Falling Back to Cache)
    As opposite, fetch data from the network and if an error occurred or no internet connection use cache as a fallback

  3. Stale-While-Revalidate
    Return data from the cache while fetching the new data from the server.

Read more on web.dev

Resources
https://web.dev/service-worker-caching-and-http-caching/

Implement caching strategies

  1. Cache First (Cache Falling Back to Network)
  2. Network First (Network Falling Back to Cache)
  3. Network Only
  4. Cache Only
  5. Stale-While-Revalidate

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.