@applicaster/zapp-react-native-utils
Version:
Applicaster Zapp React Native utilities package
391 lines (330 loc) • 9.91 kB
text/typescript
import * as R from "ramda";
import { StorageLevel, StorageType } from "./consts";
import {
getNamespaceAndKey,
keyIsValid,
buildNamespaceKey,
savingResultIsSuccess,
} from "./utils";
import { utilsLogger } from "../../logger";
import {
sessionStorage as SessionStorage,
localStorage as LocalStorage,
secureStorage as SecureStorage,
} from "./storage";
export { StorageLevel };
type KeyNameObj = {
namespace: string;
key: string;
};
type KeyName = string | KeyNameObj;
type ParsedKeyAndNamespace = {
namespace?: string;
key?: string;
isValid: boolean;
};
export const NOTHING = null;
type Value = string | number | boolean | Array<any> | Object;
type ValueOrNothing = Value | typeof NOTHING;
type KeyProp = {
key: KeyName;
value: Value;
storageLevel?: StorageLevel;
};
const UtilsLogger = utilsLogger.addSubsystem("ContextKeysManager");
type Logger = {
warn: Function;
};
type Storage = {
getItem: (key: string, namespace: string) => Promise<Value>;
setItem: (key: string, value: Value, namespace: string) => Promise<boolean>;
removeItem: (key: string, namespace: string) => Promise<boolean>;
};
type Setup = {
logger?: Logger;
sessionStorage?: Storage;
localStorage?: Storage;
secureStorage?: Storage;
referenceStorage?: Storage;
};
export const REFERENCE_NAMESPACE = "applicaster.v2.references";
export class ContextKeysManager {
private static _instance: ContextKeysManager;
private _logger: Logger;
private _sessionStorage: Storage;
private _localStorage: Storage;
private _secureStorage: Storage;
private _referenceStorage: Storage;
public static get instance() {
if (!this._instance) {
const instance = new ContextKeysManager({
logger: UtilsLogger,
sessionStorage: SessionStorage,
localStorage: LocalStorage,
secureStorage: SecureStorage,
referenceStorage: SessionStorage,
});
this._instance = instance;
}
return this._instance;
}
// in case you need to tune dependency - use constructor
constructor({
logger = UtilsLogger,
sessionStorage = SessionStorage,
localStorage = LocalStorage,
secureStorage = SecureStorage,
referenceStorage = SessionStorage,
}: Setup) {
this._logger = logger;
this._sessionStorage = sessionStorage;
this._localStorage = localStorage;
this._secureStorage = secureStorage;
this._referenceStorage = referenceStorage;
}
private parseKey(key: KeyName): ParsedKeyAndNamespace {
const parsedKey = getNamespaceAndKey(key);
if (!keyIsValid(parsedKey)) {
this._logger.warn({
message:
"Provided key is not valid - expecing an object with namespace and key properties",
data: { key },
});
return { isValid: false };
}
return { isValid: true, ...parsedKey };
}
// GET
public async getKeyByReference(
key: string,
namespace: string
): Promise<ValueOrNothing> {
// 1) look for storage
// 2) if storage found - look value in it
// 3) if storage not found return NOTHING
return this._referenceStorage
.getItem(buildNamespaceKey(key, namespace), REFERENCE_NAMESPACE)
.then((storage: StorageType | unknown) => {
switch (storage) {
case StorageType.session:
return this._sessionStorage.getItem(key, namespace);
case StorageType.local:
return this._localStorage.getItem(key, namespace);
case StorageType.secure:
return this._secureStorage.getItem(key, namespace);
default:
// unknown storage
return NOTHING;
}
})
.catch((error) => {
// something went wrong
this._logger.warn({
message: error.message,
data: { key, namespace },
});
return NOTHING;
});
}
public async getKeys(
keys: Array<KeyName>
): Promise<Map<KeyName, ValueOrNothing>> {
const values = await Promise.all(keys.map((key) => this.getKey(key)));
return new Map(R.zip(keys, values));
}
public async getKey(key: KeyName): Promise<ValueOrNothing> {
const {
isValid: isValidKey,
key: parsedKey,
namespace: parsedNamespace,
} = this.parseKey(key);
if (!isValidKey) {
// for invalid key - return nothing
return NOTHING;
}
// at first - try to find by reference
// if not found, look in session > local > secure
const resultByReference = await this.getKeyByReference(
parsedKey,
parsedNamespace
);
if (!R.isNil(resultByReference)) {
return resultByReference;
}
// try to full-search
try {
const resultFromSessionStorage = await this._sessionStorage.getItem(
parsedKey,
parsedNamespace
);
if (!R.isNil(resultFromSessionStorage)) {
return resultFromSessionStorage;
}
const resultFromLocalStorage = await this._localStorage.getItem(
parsedKey,
parsedNamespace
);
if (!R.isNil(resultFromLocalStorage)) {
return resultFromLocalStorage;
}
const resultFromSecureStorage = await this._secureStorage.getItem(
parsedKey,
parsedNamespace
);
if (!R.isNil(resultFromSecureStorage)) {
return resultFromSecureStorage;
}
// nothing found
return NOTHING;
} catch (error) {
// something went wrong
this._logger.warn({
message: error.message,
data: { key },
});
return NOTHING;
}
}
// GET
// SET
public async setReferenceForKey(
key: string,
namespace: string,
storageLevel: StorageLevel
): Promise<boolean> {
return this._referenceStorage
.setItem(
buildNamespaceKey(key, namespace),
storageLevel,
REFERENCE_NAMESPACE
)
.then((result) => {
if (savingResultIsSuccess(result)) {
return true;
}
this._logger.warn({
message:
"While saving reference - reference storage return non expected value: true",
data: { key, namespace, storageLevel },
});
return false;
})
.catch((error) => {
// something went wrong
this._logger.warn({
message: error.message,
data: { key, namespace, storageLevel },
});
return false;
});
}
// for all keys where saving were succeed return true
// for others return false
public async setKeys(
keysProp: Array<KeyProp>
): Promise<Map<KeyName, boolean>> {
const keys = keysProp.map((keyProp) => keyProp.key);
const values = await Promise.all(
keysProp.map((keyProp) => this.setKey(keyProp))
);
return new Map(R.zip(keys, values));
}
// when succeed saving - return true
// otherwise return false
public async setKey({
key,
value,
storageLevel = StorageLevel.default,
}: KeyProp): Promise<boolean> {
const {
isValid: isValidKey,
key: parsedKey,
namespace: parsedNamespace,
} = this.parseKey(key);
if (!isValidKey) {
// for invalid key - return false
return false;
}
try {
switch (storageLevel) {
case StorageLevel.persisted:
return await Promise.all([
this._localStorage.setItem(parsedKey, value, parsedNamespace),
this.setReferenceForKey(parsedKey, parsedNamespace, storageLevel),
]).then(([setItemResult]) => {
return savingResultIsSuccess(setItemResult);
});
case StorageLevel.secure:
return await Promise.all([
this._secureStorage.setItem(parsedKey, value, parsedNamespace),
this.setReferenceForKey(parsedKey, parsedNamespace, storageLevel),
]).then(([setItemResult]) => {
return savingResultIsSuccess(setItemResult);
});
default:
return await Promise.all([
this._sessionStorage.setItem(parsedKey, value, parsedNamespace),
this.setReferenceForKey(parsedKey, parsedNamespace, storageLevel),
]).then(([setItemResult]) => {
return savingResultIsSuccess(setItemResult);
});
}
} catch (error) {
// something went wrong
this._logger.warn({
message: error.message,
data: { key, value, storageLevel },
});
return false;
}
}
// SET
// REMOVE
public async removeReferenceForKey(
key: string,
namespace: string
): Promise<boolean> {
return this._referenceStorage
.removeItem(buildNamespaceKey(key, namespace), REFERENCE_NAMESPACE)
.then(() => true)
.catch((error) => {
// something went wrong
this._logger.warn({
message: error.message,
data: { key, namespace },
});
return false;
});
}
public async removeKey(key: KeyName): Promise<boolean> {
const {
isValid: isValidKey,
key: parsedKey,
namespace: parsedNamespace,
} = this.parseKey(key);
if (!isValidKey) {
// for invalid key - return false
return false;
}
return Promise.all([
this._sessionStorage.removeItem(parsedKey, parsedNamespace),
this._localStorage.removeItem(parsedKey, parsedNamespace),
this._secureStorage.removeItem(parsedKey, parsedNamespace),
this.removeReferenceForKey(parsedKey, parsedNamespace),
])
.then(() => true)
.catch((error) => {
// something went wrong
this._logger.warn({
message: error.message,
data: { key },
});
return false;
});
}
public async removeKeys(keys: KeyName[]): Promise<Map<KeyName, boolean>> {
const values = await Promise.all(keys.map((key) => this.removeKey(key)));
return new Map(R.zip(keys, values));
}
// REMOVE
}