expdata

Typescript library providing expiring data storage for LocalStorage and IndexedDB.

Usage no npm install needed!

<script type="module">
  import expdata from 'https://cdn.skypack.dev/expdata';
</script>

README

ExpData

Asynchronous Typescript library for storing data in a directory-like key structure for a specified period of time with a common interface for localStorage and indexedDB.

Overview and Storage Types

While this library is being used by its creators on Angular 2 projects, the implementation is can be used on any type of site. It's strength is the common functionality between all the implementations.

When you start a project you might think you will be storing a small amount of data, so you opt for a quick localStorage solution. As the project progresses you find your storage needs increase dramatically and indexedDB will be required. Changing over is as simple as changing the type of your storage provider from ExpiringLocalData to ExpiringIndexedData. All of the methods are the same in both classes, so the rest of the code will not need to be changed. Here is an overview of the classes:

ExpiringData is the abstract base class for the three built-in data providers. They are ExpiringLocalData, ExpiringIndexedData and ExpiringEmptyData. ExpiringData provides three public member functions for saving, retrieving and removing data using specialized directory-like keys. Data is not editable but can be overwritten.

ExpiringLocalData

ExpiringLocalData extends ExpiringData and stores data in localStorage. If localStorage is disabled, it will fall back to an in-memory mock localStorage implementation named MockLocalStorage.

ExpiringIndexedData

ExpiringIndexedData extends ExpiringData and stores data in indexedDB. If indexedDB is disabled, it will not store or retrieve any data but will always return true

ExpiringEmptyData

ExpiringEmptyData does not store or retrieve any data. It is included for implementations that require an ExpiringData object, but you do not want to actually store any data. This is handy during development when you are simultaneously working on the frontend and backend api and do not want the data cached.

Storing Data

ExpiringData.setData(key: string, data: any, lifetime?: Date|number|string) : Observable<boolean>

Parameters:

  • key Must be an item based key of the form 'element' or 'group1|group2|...|groupN|element'. Examples would be 'favorites|color' or 'myFavoriteColor'. The only restrictions are that it cannot end in a pipe character ('|') or an asterisk ('*').
  • data The data you want to store. Can be any object, array, string, number, boolean. It cannot store data structures with functions. The functions are removed in the JSON.stringify() process.
  • lifetime (optional)
    1. If null or empty, data will never expire.
    2. If a Javascript Date object, this represents the date and time that the data will expire on.
    3. If a number, this is the getTime() representation (in milliseconds) of a javascript Date object on which the data will expire.
    4. If a string, should be of the form '[integer][time unit]'. Valid time units are 's' for seconds, 'm' for minutes, 'h' for hours and 'd' for days. Examples:
      • '3h' = Expires 3 hours from now
      • '5d' = Expires 5 days from now

There is a maximum lifetime of roughly 50 years past current time. Any lifetime received greater than this maximum will be set to the maximum lifetime.

Examples:

import { ExpiringLocalData } from "expdata";
var local = new ExpiringLocalData();

local.setData('favorites|color', 'red', '3d'); // store for 3 days
local.setData('favorites|numbers', [7, 22], '2h'); // store array for 2 hours

var favWebsite = {name: 'NPM', url: 'https://www.npmjs.com/'};
local.setData('favorites|website', favWebsite, '30m'); // store object for 30 minutes

// full example, with event handling
local.setData('favorites|fruit', 'apple', '30d')
    .subscribe(
        () => { console.log("Data successfully set..."); },
        e => { console.log("Error setting expiring data ...", e); },
        () => { console.log("Setting expiring data completed."); }
    );

Retrieving Data

ExpiringData.getData(key: string, defaultData: any) : Observable<any>;

Parameters:

  • key Must be Elemental of the form 'elementName' or 'group1|...|groupN|elementName'. Examples would be 'myFavoriteColor' or 'favorites|color'. The only restrictions are that it cannot end in a pipe character ('|') or an asterisk ('*').
  • defaultData (optional) The data you would like reflected back if no cached data is found. If not provided, a null value becomes the default.

Examples:

import { ExpiringIndexedDb } from "expdata";
var indexed = new ExpiringIndexedDb();
var defaultDataGridSettings = {sortColumn: "ID", sortOrder: "descending"};
var myDataGridSettings = {sortColumn: "LastName", sortOrder: "ascending"};
indexed.setData("myComponent|gridSettings", myDataGridSettings, "30m")
    .subscribe(
        () => {... data successfully set ...},
        () => {... error setting data ...}
    );

indexed.getData("myComponent|gridSettings", defaultDataGridSettings);
// returns {sortColumn: "LastName", sortOrder: "ascending"}

// ... wait 30 minutes for data to expire ...
indexed.getData("myComponent|gridSettings", defaultDataGridSettings);
// returns {sortColumn: "ID", sortOrder: "descending"}

Removing Data

ExpiringData.removeData(key: string);

Parameters:

  • key Can be Elemental or Group key. Elemental keys are of the form 'elementName' or 'group1|...|groupN|elementName'. Group keys are of the form 'group1|...|groupN|*'. Elemental keys remove a single element from storage while group keys remove the entire group and the root element. Special key '*' removes all data in storage.
import { ExpiringLocalData } from "expdata";
var local = new ExpiringLocalData();

// ********* All functions return via Observable.subscribe() *******************
// set some test data
local.setData('a', 'data-a');
local.setData('b', 'data-b');
local.setData('c', 'data-c');
local.setData('a|a', 'data-aa');
local.setData('a|b', 'data-ab');
local.setData('a|c', 'data-ac');
local.setData('b|a', 'data-ba');
local.setData('b|a|a', 'data-baa');
local.setData('b|a|b', 'data-bab');

// remove a single element
local.removeData('a|c');
local.getData('a|c'); // -> null
local.getData('a|b'); // -> 'data-ab'

// remove a level 1 group
local.removeData('a|*');
local.getData('a'); // -> null
local.getData('a|a'); // -> null
local.getData('a|b'); // -> null

// remove a level 2 group
local.removeData('b|a|*');
local.getData('b|a'); // -> null
local.getData('b|a|a'); // -> null
local.getData('b|a|b'); // -> null
local.getData('c'); // -> 'data-c'

// remove all remaining elements
local.removeData('*');
local.getData('b'); // -> null
local.getData('c'); // -> null

What is this for?

The most common implementations would be to store user preferences or component specific data. The directory-like structure of the keys helps with this. For instance, when a user signs in you can get their userId and store their preferences in the 'user|100|...' group (e.g. 'user|100|gridSettings'). If a different user signs in, they can be stored in the 'user|101|...' group. The data can be segregated for multiple users and deleting all the data for an individual user can be accomplished with removing group 'user|N|*'.

What's Next

While the library is handy to store component data on an individual basis, the kind of data you will most likely be storing are responses from an http/xhr request. It sure would be nice if this library could be coupled with an xhr wrapper to simplify this process. For Angular 2, there is! It's called exphttp (currently in progress).