@v4fire/core
Version:
V4Fire core library
366 lines (300 loc) • 8.1 kB
text/typescript
/*!
* V4Fire Core
* https://github.com/V4Fire/Core
*
* Released under the MIT license
* https://github.com/V4Fire/Core/blob/master/LICENSE
*/
/**
* [[include:core/kv-storage/README.md]]
* @packageDocumentation
*/
import { convertIfDate } from 'core/json';
import {
syncLocalStorage,
asyncLocalStorage,
syncSessionStorage,
asyncSessionStorage
} from 'core/kv-storage/engines';
import type {
SyncStorage,
SyncStorageNamespace,
AsyncStorage,
AsyncStorageNamespace,
ClearFilter,
StorageEngine
} from 'core/kv-storage/interface';
export * from 'core/kv-storage/interface';
/**
* API for synchronous local storage
*
* @example
* ```js
* local.set('foo', 'bar');
* local.get('foo'); // 'foo'
* ```
*/
export const local = factory(syncLocalStorage);
/**
* API for asynchronous local storage
*
* @example
* ```js
* asyncLocal.set('foo', 'bar').then(async () => {
* console.log(await asyncLocal.get('foo')); // 'foo'
* });
* ```
*/
export const asyncLocal = factory(asyncLocalStorage, true);
/**
* API for synchronous session storage
*
* @example
* ```js
* session.set('foo', 'bar');
* session.get('foo'); // 'foo'
* ```
*/
export const session = factory(syncSessionStorage);
/**
* API for asynchronous session storage
*
* @example
* ```js
* asyncSession.set('foo', 'bar').then(async () => {
* console.log(await asyncSession.get('foo')); // 'foo'
* });
* ```
*/
export const asyncSession = factory(asyncSessionStorage, true);
/**
* Alias for a has method of the synchronous local storage API
*
* @alias
* @see [[local]]
*/
export const has = local.has.bind(local);
/**
* Alias for a get method of the synchronous local storage API
*
* @alias
* @see [[local]]
*/
export const get = local.get.bind(local);
/**
* Alias for a set method of the synchronous local storage API
*
* @alias
* @see [[local]]
*/
export const set = local.set.bind(local);
/**
* Alias for a remove method of the synchronous local storage API
*
* @alias
* @see [[local]]
*/
export const remove = local.remove.bind(local);
/**
* Alias for a clear method of the synchronous local storage API
*
* @alias
* @see [[local]]
*/
export const clear = local.clear.bind(local);
/**
* Alias for a namespace method of the synchronous local storage API
*
* @alias
* @see [[local]]
*
* @example
* ```js
* const storage = namespace('REQUEST_STORAGE');
* storage.set('foo', 'bar');
* storage.get('foo'); // 'foo'
* local.get('foo'); // undefined
* ```
*/
export const namespace = local.namespace.bind(local);
/**
* Creates a new kv-storage API with the specified engine
*
* @param engine
* @param async - if true, then the storage is implemented async interface
*
* @example
* ```js
* const storage = factory(window.localStorage);
* storage.set('foo', 'bar');
* storage.get('foo'); // 'foo'
* ```
*/
export function factory(engine: StorageEngine, async: true): AsyncStorage;
export function factory(engine: StorageEngine, async?: false): SyncStorage;
export function factory(engine: StorageEngine, async?: boolean): AsyncStorage | SyncStorage {
let
has,
get,
set,
remove,
clear,
keys;
try {
// eslint-disable-next-line @typescript-eslint/unbound-method
get = engine.getItem ?? engine.get;
if (Object.isFunction(get)) {
get = get.bind(engine);
} else {
throw new ReferenceError('A method to get a value from the storage is not defined');
}
// eslint-disable-next-line @typescript-eslint/unbound-method
set = engine.setItem ?? engine.set;
if (Object.isFunction(set)) {
set = set.bind(engine);
} else {
throw new ReferenceError('A method to set a value to the storage is not defined');
}
// eslint-disable-next-line @typescript-eslint/unbound-method
remove = engine.removeItem ?? engine.remove ?? engine.delete;
if (Object.isFunction(remove)) {
remove = remove.bind(engine);
} else {
throw new ReferenceError('A method to remove a value from the storage is not defined');
}
{
// eslint-disable-next-line @typescript-eslint/unbound-method
const _ = engine.exists ?? engine.exist ?? engine.includes ?? engine.has;
has = Object.isFunction(_) ? _.bind(engine) : undefined;
}
{
// eslint-disable-next-line @typescript-eslint/unbound-method
const _ = engine.clear ?? engine.clearAll ?? engine.truncate;
clear = Object.isFunction(_) ? _.bind(engine) : undefined;
}
{
// eslint-disable-next-line @typescript-eslint/unbound-method
const _ = engine.keys;
keys = Object.isFunction(_) ? _.bind(engine) : () => Object.keys(engine);
}
} catch {
throw new TypeError('Invalid storage driver');
}
type WrappedFn<T> =
(val?: T) => any;
function wrap(val?: undefined): CanPromise<undefined>;
function wrap<T>(val: T): CanPromise<T>;
function wrap<T, R extends WrappedFn<T>>(
val: CanUndef<T>,
action: R
): CanPromise<R extends (val: CanUndef<T>) => infer R ? R extends Promise<infer RV> ? RV : R : unknown>;
function wrap<T, R extends WrappedFn<T>>(val?: T, action?: R): CanUndef<CanPromise<T | ReturnType<R>>> {
if (async) {
return (async () => {
// eslint-disable-next-line require-atomic-updates
val = await val;
if (action) {
return action(val);
}
return val;
})();
}
if (action) {
return action(val);
}
return val;
}
const obj = {
has(key: string, ...args: unknown[]): CanPromise<boolean> {
if (has != null) {
return wrap(has(key, ...args));
}
return wrap(get(key, ...args), (v) => v != null);
},
get<T>(key: string, ...args: unknown[]): CanPromise<T> {
return wrap(get(key, ...args), (v) => {
if (v == null || v === 'undefined') {
return;
}
return Object.parse(v, convertIfDate);
});
},
set(key: string, value: unknown, ...args: unknown[]): CanPromise<void> {
return wrap(set(key, Object.trySerialize(value), ...args), () => undefined);
},
remove(key: string, ...args: unknown[]): CanPromise<void> {
return wrap(remove(key, ...args), () => undefined);
},
clear<T>(filter?: ClearFilter<T>, ...args: unknown[]): CanPromise<void> {
if (filter || clear == null) {
if (async) {
return (async () => {
for (const key of await keys()) {
const
el = await obj.get<T>(key);
if (filter == null || Object.isTruly(filter(el, key))) {
await remove(key, ...args);
}
}
})();
}
for (const key of keys()) {
const
el = <T>obj.get(key);
if (filter == null || Object.isTruly(filter(el, key))) {
remove(key, ...args);
}
}
return;
}
return wrap(clear(...args), () => undefined);
},
namespace(name: string): AsyncStorageNamespace | SyncStorageNamespace {
const
k = (key) => `${name}.${key}`;
return <ReturnType<typeof obj['namespace']>>{
has(key: string, ...args: unknown[]): CanPromise<boolean> {
return obj.has(k(key), ...args);
},
get<T>(key: string, ...args: unknown[]): CanPromise<T> {
return obj.get(k(key), ...args);
},
set(key: string, value: unknown, ...args: unknown[]): CanPromise<void> {
return obj.set(k(key), value, ...args);
},
remove(key: string, ...args: unknown[]): CanPromise<void> {
return obj.remove(k(key), ...args);
},
clear<T>(filter?: ClearFilter<T>, ...args: unknown[]): CanPromise<void> {
const
prfx = `${name}.`;
if (async) {
return (async () => {
for (const key of await keys()) {
if (!String(key).startsWith(prfx)) {
continue;
}
const
el = await obj.get<T>(key);
if (filter == null || Object.isTruly(filter(el, key))) {
await remove(key, ...args);
}
}
})();
}
for (const key of keys()) {
if (!String(key).startsWith(prfx)) {
continue;
}
const
el = <T>obj.get(key);
if (filter == null || Object.isTruly(filter(el, key))) {
remove(key, ...args);
}
}
}
};
}
};
return <AsyncStorage | SyncStorage>obj;
}