A data caching interface for the front-end. It has pub-sub capabilities, and focuses on reading from/writing to localStorage or other cache implementation (e.g. IndexedDB).
npm install @jackcom/adi-cacheducksADI DescriptionADI) is a pub-sub cache manager for the front-end. It is used for predictable data-fetching and caching in a front-end application.\ADI focuses on retrieving locally-stored application data, independent of UI frameworks, programming paradigms, or cache implementations. ---
* ADI: The App data interface instance generated for you by the library.
* cache: any data cache implementation, such as IndexedDB or localForage in browsers. This MUST be provided to ADI on initialization.
Note that the cache MUST be supplied to ADI on initialization, since the latter writes to/reads from the former during the application lifecycle.
---
createDataCacheAPI( cacheMap: ADICacheDBMap )\cacheMap, a key-value store where every key is the name of a storage or cache instance (e.g. name of a table in IndexedDB) and the value of the key is an object that implements ADIDBInterface, as below:typescript
// Below, we create a cacheMap for a "users" table in indexedDB.
// The "cachemap" is where you put ALL your local db APIs.
const cacheMap = {
users: {
async clearItems() { ... },
async listItems(opts: ListQueryOpts) { ... },
async getItem(id: any) { ... },
async putItem(id: any, val: DBUser) { ... },
async removeItem(id: any): { ... },
}
}// The fun part: create your ADI instance
const ADI = createDataCacheAPI( cacheMap )
ADI.onApplicationStart()
// Now you can asynchronously fetch or list items from the db
ADI.publishItem(someId, "users").then( ... )
ADI.listItems({ cacheKey: "users" }).then( ... )
// Get notified when the db is updated
ADI.subscribeToCaches((key: string, val: any, cache: string) => {
// ...
}, ["users"])
`createDataCacheAPI( ... ) returns an AppDataInterface with the following methods:$3
`typescript
interface AppDataInterface {{
/* Write an incoming value to the supplied cache, or remove the supplied key if value is falsy (undefined or null). /
cacheItem(key: string, value: any, cacheKey?: string): any;
/* Write incoming values to their respective caches, or remove the supplied key if items[x].value is falsy (undefined or null). /
cacheMultiple(items: CacheItemArgs[]): void;
/* Asserts whether the ADI instance has been initialized with a call to onApplicationStart(). /
isInitialized(): boolean;
/* Retrieve (or optionally fetch, cache, and return) data from a db/cache /
getItem(
key: string,
cacheKey?: string,
fallback?: () => Promise
): Promise;
/* Retrieve a list from the specified cache. /
listItems(
opts: ListQueryOpts,
fallback?: () => Promise
): Promise>;
/* Reset ADI to pre-initialized state. Disables reading from/writing to cache: use on [ user disconnect, app pause, etc ] /
onApplicationEnd(): void;
/* Initializes the ADI and gets it ready for front-end (or other app) interaction /
onApplicationStart(): void;
/* Notify subscribers with data from a db/cache /
publishItem(
key: string,
cacheKey?: string,
fallback?: () => Promise
): void;
/* Notify subscribers with a retrieved list. /
publishItems(opts: ListQueryOpts, fallback?: () => Promise): void;
/* Remove data from the cache (or localStorage if no cacheKey) /
removeItem(key: string, cacheKey?: string): any;
/* Subscribe to ADI instance for notifications when a cached value (or a cache) is changed. /
subscribe(listener: KeyValConsumer): Unsubscriber;
/* Subscribe to ADI instance for notifications when a cached value (or a cache) is changed. /
subscribeToCaches(
listener: KeyValConsumer,
caches: string[],
withinBounds?: (updatedKey: string, newVal?: any, cache?: string) => boolean
): Unsubscriber;
};
}
`Application Data Interface | Methods
Lifecycle
$3
Asserts whether the ADI instance has been initialized with a call to onApplicationStart(). While this value is false, any call to a non-subscribe method on the ADI instance will throw a gigantic flaming error.$3
This function initializes the ADI and gets it ready for front-end (or other app) interaction. It must be called before using any instance methods (excluding subscribe and subscribeToCaches), or ADI will throw an error.`typescript
const dataAPI = createDataCacheAPI( cacheMap );dataAPI.onApplicationStart();
dataAPI.cacheItem( ... ) // no errors
`
$3
Reset ADI to pre-initialized state. Disables reading from/writing to cache: use on [ user disconnect, app pause, etc ]#### Example - Vue (contrived)
`typescript
{
beforeUnmount() {
dataAPI.onApplicationEnd()
}
}
`#### React - (functional -- also contrived)
`typescript
useEffect(() => {
// Return function ref for component/app cleanup
return dataAPI.onApplicationEnd
})
`Data Storage and Retrieval
$3
Clear all items from the specified cache. If no cache is specified, ADI will attempt to clear localStorage.\
If the key all is provided as the cache, ADI will attempt to clear both localStorage and all dbs that implement clearItem. $3
Retrieve a list from the specified cache. Takes an optional fallback function to fetch the data if it is not in cache. This will NOT write anything to cache or notify subscribers, since notification requires a single#### Example
`typescript
const dataAPI = createDataCacheAPI( cacheMap );dataAPI.onApplicationStart();
// Optional: subscribe either component or some global state to cache
const subscriber = (k, v, c) => {
if (k === "someKey") // update view using value "v" ...
}
const unsubscribeCache = dataAPI.subscribe(subscriber);
// Option 1: await the response and use it right away
const a = await dataAPI.listItems({ cacheKey: "someDB" });
// Option 2: Unidirectional data-flow (cache -> state -> UI),
dataAPI.subscribeToCaches((k, v) => { ... }, ["someDB"]);
// Call "publishItem" and wait for a subscription to be triggered.
dataAPI.publishItems({ cacheKey: "someDB" });
`
$3
Retrieve data from the cache. Takes an optional fallback function to fetch the data if it is not in cache. If used, the fallback response will be cached for future reference, and subscribers will be notified. Note: if a
cache key is not supplied, ADI will attempt to read from localStorage.`typescript
const dataAPI = createDataCacheAPI( cacheMap );dataAPI.onApplicationStart();
// Optional: subscribe either component or some global state to cache
const subscriber = (k, v, c) => {
if (k === "someKey") // update view using value "v" ...
}
const unsubscribeCache = dataAPI.subscribe(subscriber);
// Option 1: await the response and use it right away
const a = await dataAPI.getItem("someKey")
// Option 2: unidirectional data-flow (cache -> state -> UI),
// call "publishItem" and wait for a subscription to be triggered.
dataAPI.subscribeToCaches( ..., ["someCache"]);
dataAPI.publishItem("someKey", "someCache");
// Stop listening
unsubscribe();
`
$3
$3
Write an incoming value to the supplied cache implementation, or remove any existing value with the supplied key if value is falsy (undefined or null).\
ADI.cacheMultiple will call listItems on all changed caches after the operation is complete.
`typescript
const dataAPI = createDataCacheAPI( cacheMap );
dataAPI.onApplicationStart();// removes 'someKey' from localStorage
dataAPI.cacheItem("someKey");
// adds value 'someValue' with 'someKey' to localStorage
dataAPI.cacheItem("someKey", someValue);
// writes value 'someValue' of 'someKey' to 'someTable'
dataAPI.cacheItem("someKey", someValue, "someTable");
`$3
Remove data from the cache. If a cache key is not supplied, ADI will attempt to remove the key from localStorage.`typescript
const dataAPI = createDataCacheAPI( cacheMap );dataAPI.onApplicationStart();
// Remove from localStorage
dataAPI.removeItem("someKey");
// Remove from some table
dataAPI.removeItem("someKey", "someTable");
`
---Publishing and subscribing to changes
Note that the ADI will always broadcast changes to cache (i.e. whenever ADI.cacheItem or ADI.cacheMultiple is called). Otherwise, you can selectively retrieve and use data from a cache, or retrieve and publish data to all subscribers without receiving a return value.$3
Retrieve and broadcast a list using the options specified in opts. Supports paginated queries/responses if implemented.
$3
Like getItem, retrieves a single item from cache, but broadcasts to all subscribers without returning the item.$3
$3
Subscribe to ADI instance for notifications when a cached value (or a cache) is changed. The listener function should expect the following args:
`typescript
type ListenerFn = {
( updatedKey: string, newVal: any, cache?: string ): any
}
`
If an updated key is "all", then the ADI just fetched or updated all items in the specified cache. This should never be triggered for localStorage.Application Data Interface | Types and Interfaces
ADICacheDBMap
A key-value store that is required to initialize an ADI instance.`typescript
interface ADICacheDBMap = { [name: string]: ADIDBInterface };
`
ADIDBInterface
A key-value object that serves as an API for your local database (e.g. IndexedDB or localForage). The object must have the following methods, `typescript
type ADIDBInterface = {
listItems(opts: ListQueryOpts): Promise>;
getItem(id: any): Promise;
putItem(id: any, val: any): Promise;
removeItem(id: any): Promise;
}
`
where the generic represents your database model.\
(Example: if your database model is User, then listItems should return a list of type User[])ListQueryOpts
A key-value object with options for enhanced storage queries. Allows the developer to implement paginated responses.
`typescript
type ListQueryOpts = {
cacheKey: string,
page?: number;
resultsPerPage?: number;
orderBy?: string;
};const opts: ListQueryOpts = { ... }
ADI.listItems(opts).then( ... );
`
ListQueryOpts
A key-value response (from local cache/db) to a "list-all" query.\
Allows the developer to implement paginated responses.`typescript
type PaginatedDBResults = {
totalResults?: number;
totalPages?: number;
resultsPerPage?: number;
data: T[];
page?: number;
};const opts: ListQueryOpts = { ... }
const response: PaginatedDBResult = await ADI.listItems(opts);
response.data.forEach( ... )
response.page ...
`
---Using the
ADI in an AppAn in-memory store works well enough for small applications, but some additional caching is required as you begin to scale. This can come with issues you have multiple components either updating or reading from a cache store.
For maximum profit, it is important to identify a single source of truth for your app data.
The expected/assumed flow of data is:
remote -> [ ADI -> (cache) -> ] -> [ App state -> ] UI component
where
[ App state ] represents some state manager like Redux, Vuex, or raphsducks. It is entirely optional, since a component can also subscribe to the ADI directly. As you can see, the ADI only deals with receiving the data from some source, caching, and then emitting it. Using the ADI, the following would have the same effect:
> - Component subscribes to some global
state using onChange listener
> - On load, check state for data
> - If not found, call ADI.publishItem( dataKey )
> - Await state updates via onChange listener
> - Use data from state in onChange listenerOR
> - Component subscribes to ADI using
onChange listener
> - On load, Component calls ADI.publishItem( dataKey )
> - onChange is triggered
> - Use data in onChange listenerOR
> - On load, Component calls
ADI.publishItem( dataKey )
> - Awaits response, and stores in a const dataDevelopment
First install dependencies:
`sh
npm install
`To create a production build:
`sh
npm run build-prod
`To create a development build:
`sh
npm run build-dev
`Running
`sh
node dist/bundle.js
`Testing
To run unit tests:
`sh
npm test
``Made with createapp.dev