New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@lion/ajax

Package Overview
Dependencies
Maintainers
1
Versions
82
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@lion/ajax

Thin wrapper around fetch with support for interceptors.

  • 0.7.0
  • Source
  • npm
  • Socket score

Version published
Weekly downloads
246
increased by161.7%
Maintainers
1
Weekly downloads
 
Created
Source

Ajax

import { html } from '@lion/core';
import { renderLitAsNode } from '@lion/helpers';
import { ajax, AjaxClient, cacheRequestInterceptorFactory, cacheResponseInterceptorFactory } from '@lion/ajax';
import '@lion/helpers/sb-action-logger';

const getCacheIdentifier = () => {
  let userId = localStorage.getItem('lion-ajax-cache-demo-user-id');
  if (!userId) {
    localStorage.setItem('lion-ajax-cache-demo-user-id', '1');
    userId = '1';
  }
  return userId;
}

const cacheOptions = {
  useCache: true,
  timeToLive: 1000 * 60 * 10, // 10 minutes
};

ajax.addRequestInterceptor(cacheRequestInterceptorFactory(getCacheIdentifier, cacheOptions));
ajax.addResponseInterceptor(
  cacheResponseInterceptorFactory(getCacheIdentifier, cacheOptions),
);

export default {
  title: 'Ajax/Ajax',
};

ajax is a small wrapper around fetch which:

  • Allows globally registering request and response interceptors
  • Throws on 4xx and 5xx status codes
  • Prevents network request if a request interceptor returns a response
  • Supports a JSON request which automatically encodes/decodes body request and response payload as JSON
  • Adds accept-language header to requests based on application language
  • Adds XSRF header to request if the cookie is present

How to use

Installation

npm i --save @lion/ajax

Relation to fetch

ajax delegates all requests to fetch. ajax.request and ajax.requestJson have the same function signature as window.fetch, you can use any online resource to learn more about fetch. MDN is a great start.

Example requests

GET request
export const getRequest = () => {
  const actionLogger = renderLitAsNode(html`<sb-action-logger></sb-action-logger>`);
  const fetchHandler = (name) => {
    ajax.request(`./packages/ajax/docs/${name}.json`)
      .then(response => response.json())
      .then(result => {
        actionLogger.log(JSON.stringify(result, null, 2));
      });
  }
  return html`
    <style>
      sb-action-logger {
        --sb-action-logger-max-height: 300px;
      }
    </style>
    <button @click=${() => fetchHandler('pabu')}>Fetch Pabu</button>
    <button @click=${() => fetchHandler('naga')}>Fetch Naga</button>
    ${actionLogger}
  `;
}
POST request
import { ajax } from '@lion/ajax';

const response = await ajax.request('/api/users', {
  method: 'POST',
  body: JSON.stringify({ username: 'steve' }),
});
const newUser = await response.json();

JSON requests

We usually deal with JSON requests and responses. With requestJson you don't need to specifically stringify the request body or parse the response body.

The result will have the Response object on .response property, and the decoded json will be available on .body.

GET JSON request
export const getJsonRequest = () => {
  const actionLogger = renderLitAsNode(html`<sb-action-logger></sb-action-logger>`);
  const fetchHandler = (name) => {
    ajax.requestJson(`./packages/ajax/docs/${name}.json`)
      .then(result => {
        console.log(result.response);
        actionLogger.log(JSON.stringify(result.body, null, 2));
      });
  }
  return html`
    <style>
      sb-action-logger {
        --sb-action-logger-max-height: 300px;
      }
    </style>
    <button @click=${() => fetchHandler('pabu')}>Fetch Pabu</button>
    <button @click=${() => fetchHandler('naga')}>Fetch Naga</button>
    ${actionLogger}
  `;
}
POST JSON request
import { ajax } from '@lion/ajax';

const { response, body } = await ajax.requestJson('/api/users', {
  method: 'POST',
  body: { username: 'steve' },
});

Error handling

Different from fetch, ajax throws when the server returns a 4xx or 5xx, returning the request and response:

export const errorHandling = () => {
  const actionLogger = renderLitAsNode(html`<sb-action-logger></sb-action-logger>`);
  const fetchHandler = async () => {
    try {
      const users = await ajax.requestJson('/api/users');
    } catch (error) {
      if (error.response) {
        if (error.response.status === 400) {
          // handle a specific status code, for example 400 bad request
        } else {
          actionLogger.log(error);
        }
      } else {
        // an error happened before receiving a response, ex. an incorrect request or network error
        actionLogger.log(error);
      }
    }
  }
  return html`
    <style>
      sb-action-logger {
        --sb-action-logger-max-height: 300px;
      }
    </style>
    <button @click=${fetchHandler}>Fetch</button>
    ${actionLogger}
  `;
}

Fetch Polyfill

For IE11 you will need a polyfill for fetch. You should add this on your top level layer, e.g. your application.

This is the polyfill we recommend. It also has a section for polyfilling AbortController

Ajax Cache

A caching library that uses @lion/ajax and adds cache interceptors to provide caching for use in frontend services.

The request interceptor's main goal is to determine whether or not to return the cached object. This is done based on the options that are being passed.

The response interceptor's goal is to determine when to cache the requested response, based on the options that are being passed.

Getting started

Consume the global ajax instance and add the interceptors to it, using a cache configuration which is applied on application level. If a developer wants to add specifics to cache behavior they have to provide a cache config per action (get, post, etc.) via cacheOptions field of local ajax config, see examples below.

Note: make sure to add the interceptors only once. This is usually done on app-level

import {
  ajax,
  cacheRequestInterceptorFactory,
  cacheResponseInterceptorFactory,
} from '@lion-web/ajax.js';

const globalCacheOptions = {
  useCache: true,
  timeToLive: 1000 * 60 * 5, // 5 minutes
};
// Cache is removed each time an identifier changes,
// for instance when a current user is logged out
const getCacheIdentifier = () => getActiveProfile().profileId;

ajax.addRequestInterceptor(cacheRequestInterceptorFactory(getCacheIdentifier, globalCacheOptions));
ajax.addResponseInterceptor(
  cacheResponseInterceptorFactory(getCacheIdentifier, globalCacheOptions),
);

const { response, body } = await ajax.requestJson('/my-url');

Alternatively, most often for subclassers, you can extend or import AjaxClient yourself, and pass cacheOptions when instantiating the ajax singleton.

import { AjaxClient } from '@lion/ajax';

export const ajax = new AjaxClient({
  cacheOptions: {
    useCache: true,
    timeToLive: 1000 * 60 * 5, // 5 minutes
    getCacheIdentifier: () => getActiveProfile().profileId,
  },
})

Ajax cache example

Let's assume that we have a user session, for this demo purposes we already created an identifier function for this and set the cache interceptors.

We can see if a response is served from the cache by checking the response.fromCache property, which is either undefined for normal requests, or set to true for responses that were served from cache.

export const cache = () => {
  const actionLogger = renderLitAsNode(html`<sb-action-logger></sb-action-logger>`);
  const fetchHandler = (name) => {
    ajax.requestJson(`./packages/ajax/docs/${name}.json`)
      .then(result => {
        actionLogger.log(`From cache: ${result.response.fromCache || false}`);
        actionLogger.log(JSON.stringify(result.body, null, 2));
      });
  }
  return html`
    <style>
      sb-action-logger {
        --sb-action-logger-max-height: 300px;
      }
    </style>
    <button @click=${() => fetchHandler('pabu')}>Fetch Pabu</button>
    <button @click=${() => fetchHandler('naga')}>Fetch Naga</button>
    ${actionLogger}
  `;
}

You can also change the cache options per request, which is handy if you don't want to remove and re-add the interceptors for a simple configuration change.

In this demo, when we fetch naga, we always pass useCache: false so the Response is never a cached one.

export const cacheActionOptions = () => {
  const actionLogger = renderLitAsNode(html`<sb-action-logger></sb-action-logger>`);
  const fetchHandler = (name) => {
    let actionCacheOptions;
    if (name === 'naga') {
      actionCacheOptions = {
        useCache: false,
      }
    }

    ajax.requestJson(`./packages/ajax/docs/${name}.json`, { cacheOptions: actionCacheOptions })
      .then(result => {
        actionLogger.log(`From cache: ${result.response.fromCache || false}`);
        actionLogger.log(JSON.stringify(result.body, null, 2));
      });
  }
  return html`
    <style>
      sb-action-logger {
        --sb-action-logger-max-height: 300px;
      }
    </style>
    <button @click=${() => fetchHandler('pabu')}>Fetch Pabu</button>
    <button @click=${() => fetchHandler('naga')}>Fetch Naga</button>
    ${actionLogger}
  `;
}

Invalidating cache

Invalidating the cache, or cache busting, can be done in multiple ways:

  • Going past the timeToLive of the cache object
  • Changing cache identifier (e.g. user session or active profile changes)
  • Doing a non GET request to the cached endpoint
    • Invalidates the cache of that endpoint
    • Invalidates the cache of all other endpoints matching invalidatesUrls and invalidateUrlsRegex
Time to live

In this demo we pass a timeToLive of three seconds. Try clicking the fetch button and watch fromCache change whenever TTL expires.

After TTL expires, the next request will set the cache again, and for the next 3 seconds you will get cached responses for subsequent requests.

export const cacheTimeToLive = () => {
  const actionLogger = renderLitAsNode(html`<sb-action-logger></sb-action-logger>`);
  const fetchHandler = () => {
    ajax.requestJson(`./packages/ajax/docs/pabu.json`, {
      cacheOptions: {
        timeToLive: 1000 * 3, // 3 seconds
      }
    })
      .then(result => {
        actionLogger.log(`From cache: ${result.response.fromCache || false}`);
        actionLogger.log(JSON.stringify(result.body, null, 2));
      });
  }
  return html`
    <style>
      sb-action-logger {
        --sb-action-logger-max-height: 300px;
      }
    </style>
    <button @click=${fetchHandler}>Fetch Pabu</button>
    ${actionLogger}
  `;
}
Changing cache identifier

For this demo we use localStorage to set a user id to '1'.

Now we will allow you to change this identifier to invalidate the cache.

export const changeCacheIdentifier = () => {
  const actionLogger = renderLitAsNode(html`<sb-action-logger></sb-action-logger>`);
  const fetchHandler = () => {
    ajax.requestJson(`./packages/ajax/docs/pabu.json`)
      .then(result => {
        actionLogger.log(`From cache: ${result.response.fromCache || false}`);
        actionLogger.log(JSON.stringify(result.body, null, 2));
      });
  }

  const changeUserHandler = () => {
    const currentUser = parseInt(localStorage.getItem('lion-ajax-cache-demo-user-id'), 10);
    localStorage.setItem('lion-ajax-cache-demo-user-id', `${currentUser + 1}`);
  }

  return html`
    <style>
      sb-action-logger {
        --sb-action-logger-max-height: 300px;
      }
    </style>
    <button @click=${fetchHandler}>Fetch Pabu</button>
    <button @click=${changeUserHandler}>Change user</button>
    ${actionLogger}
  `;
}
Non-GET request

In this demo we show that by doing a PATCH request, you invalidate the cache of the endpoint for subsequent GET requests.

Try clicking the GET pabu button twice so you see a cached response. Then click the PATCH pabu button, followed by another GET, and you will see that this one is not served from cache, because the PATCH invalidated it.

The rationale is that if a user does a non-GET request to an endpoint, it will make the client-side caching of this endpoint outdated. This is because non-GET requests usually in some way mutate the state of the database through interacting with this endpoint. Therefore, we invalidate the cache, so the user gets the latest state from the database on the next GET request.

Ignore the browser errors when clicking PATCH buttons, JSON files (our mock database) don't accept PATCH requests.

export const nonGETRequest = () => {
  const actionLogger = renderLitAsNode(html`<sb-action-logger></sb-action-logger>`);
  const fetchHandler = (name, method) => {
    ajax.requestJson(`./packages/ajax/docs/${name}.json`, { method })
      .then(result => {
        actionLogger.log(`From cache: ${result.response.fromCache || false}`);
        actionLogger.log(JSON.stringify(result.body, null, 2));
      });
  }
  return html`
    <style>
      sb-action-logger {
        --sb-action-logger-max-height: 300px;
      }
    </style>
    <button @click=${() => fetchHandler('pabu', 'GET')}>GET Pabu</button>
    <button @click=${() => fetchHandler('pabu', 'PATCH')}>PATCH Pabu</button>
    <button @click=${() => fetchHandler('naga', 'GET')}>GET Naga</button>
    <button @click=${() => fetchHandler('naga', 'PATCH')}>PATCH Naga</button>
    ${actionLogger}
  `;
}
Invalidate Rules

There are two kinds of invalidate rules:

  • invalidateUrls (array of URL like strings)
  • invalidateUrlsRegex (RegExp)

If a non-GET method is fired, by default it only invalidates its own endpoint. Invalidating /api/users cache by doing a PATCH, will not invalidate /api/accounts cache.

However, in the case of users and accounts, they may be very interconnected, so perhaps you do want to invalidate /api/accounts when invalidating /api/users.

This is what the invalidate rules are for.

In this demo, invalidating the pabu endpoint will invalidate naga, but not the other way around.

For invalidateUrls you need the full URL e.g. <protocol>://<domain>:<port>/<url> so it's often easier to use invalidateUrlsRegex

export const invalidateRules = () => {
  const actionLogger = renderLitAsNode(html`<sb-action-logger></sb-action-logger>`);
  const fetchHandler = (name, method) => {
    const actionCacheOptions = {};
    if (name === 'pabu') {
      actionCacheOptions.invalidateUrlsRegex = /\/packages\/ajax\/docs\/naga.json/;
    }

    ajax.requestJson(`./packages/ajax/docs/${name}.json`, {
      method,
      cacheOptions: actionCacheOptions,
    })
      .then(result => {
        actionLogger.log(`From cache: ${result.response.fromCache || false}`);
        actionLogger.log(JSON.stringify(result.body, null, 2));
      });
  }
  return html`
    <style>
      sb-action-logger {
        --sb-action-logger-max-height: 300px;
      }
    </style>
    <button @click=${() => fetchHandler('pabu', 'GET')}>GET Pabu</button>
    <button @click=${() => fetchHandler('pabu', 'PATCH')}>PATCH Pabu</button>
    <button @click=${() => fetchHandler('naga', 'GET')}>GET Naga</button>
    <button @click=${() => fetchHandler('naga', 'PATCH')}>PATCH Naga</button>
    ${actionLogger}
  `;
}

Keywords

FAQs

Package last updated on 25 Feb 2021

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc