(function ($) {
/**
* @typedef {{
* _deprecated: Array<string> | undefined
* }} XOpatStorage.SchemaElement
*
* @typedef {Object.<string, XOpatStorage.SchemaElement>} XOpatStorage.Schema
*
* @typedef {{
* id: string,
* schema: XOpatStorage.Schema | undefined,
* strictSchema: boolean | undefined
* }} XOpatStorage.StorageOptions
*/
class xoStorage {
/**
* @returns number
*/
get length() {
throw `${this.constructor.name}::length must be implemented!`;
}
clear() {
throw `${this.constructor.name}::clear must be implemented!`;
}
/**
* @returns any
*/
getItem(key) {
throw `${this.constructor.name}::getItem must be implemented!`;
}
/**
* @returns string
*/
key(index) {
throw `${this.constructor.name}::key must be implemented!`;
}
removeItem(key) {
throw `${this.constructor.name}::removeItem must be implemented!`;
}
setItem(key, value) {
throw `${this.constructor.name}::setItem must be implemented!`;
}
}
class xoCookieStorage extends xoStorage {
/**
* Builder-pattern option setter for cookies storage.
* Subsequent setItem calls must inherit these options.
* @param {object} options
* @returns xoCookieStorage
*/
with(options) {
throw `${this.constructor.name}::setItem must be implemented!`;
}
}
class xoAsyncStorage {
/**
* @returns Promise<number>
*/
get length() {
throw `${this.constructor.name}::length must be implemented!`;
}
/**
* @returns Promise<void>
*/
async clear() {
throw `${this.constructor.name}::clear must be implemented!`;
}
/**
* @returns Promise<any>
*/
async getItem(key) {
throw `${this.constructor.name}::getItem must be implemented!`;
}
/**
* @returns Promise<string>
*/
async key(index) {
throw `${this.constructor.name}::key must be implemented!`;
}
/**
* @returns Promise<void>
*/
async removeItem(key) {
throw `${this.constructor.name}::removeItem must be implemented!`;
}
/**
* @returns Promise<void>
*/
async setItem(key, value) {
throw `${this.constructor.name}::setItem must be implemented!`;
}
}
const storageAPI = Object.keys(window.Storage.prototype);
function errInstanceApi(instance, keys) {
for (let key of keys) {
if (!key in instance) return `method ${key} is not implemented!`;
}
return false;
}
function errClassApi(cls, keys) {
cls = cls.prototype;
for (let key of keys) {
if (!key in cls) return `method ${key} is not implemented!`;
}
return false;
}
/**
* Data Api Proxy Base Class. Private class.
*/
class APIProxy {
/**
* @param {XOpatStorage.StorageOptions} options
*/
constructor(options) {
if (!options?.id && options.id !== "") {
throw "Data Store: invalid configuration: missing options.id!";
}
const staticSelf = this.constructor;
if (!staticSelf._implementation) {
throw "Data Store: invalid configuration: no implementation was registered for the storage!";
}
const uid = options.id;
this.__id = (uid && !uid.endsWith(".")) ? (uid+".") : uid;
this.constructor._used = true;
this.__storage = staticSelf._implementsClass ? new staticSelf._implementation() : staticSelf._implementation;
const schema = options.schema;
if (schema) {
options.strictSchema = options.strictSchema ?? true;
this.validateKey = (key, withSuffix=true) => {
const ref = schema[key];
if (ref) {
if (!key) return uid;
if (withSuffix) return this.__id + key;
return key;
}
if (options.strictSchema) {
throw `${this.constructor.name}: invalid schema key '${key}' for data '${options.id}' in a strict mode!`;
}
return key;
}
} else {
this.validateKey = (key, withSuffix=true) => {
if (!key) return uid;
if (withSuffix) return this.__id + key;
return key;
};
}
if (schema) {
this.deprecatedKeys = (key) => {
// validateKey always called first
const ref = schema[key];
return ref && ref._deprecated || [];
}
} else {
this.deprecatedKeys = (key) => [];
}
}
get id() {
return this.__id;
}
getStore() {
return this.__storage;
}
static _implementation = null;
static _implementsClass = true;
static _used = false;
/**
* Register a storage implementation for the particular data proxy.
* @param {function(new:Storage)|function(new:AsyncStorage)} Class
*/
static register(Class) {
console.warn("Storage::register() is depreacted: use registerClass!");
return this.registerClass(Class);
}
/**
* Register a storage implementation for the particular data proxy.
* @param {function(new:Storage)|function(new:AsyncStorage)} Class
*/
static registerClass(Class) {
if (this._used) throw "Cannot register a storage implementation after it had been already used!";
const err = errClassApi(Class, storageAPI);
if (err) throw `XOpatStorage.<*>:registerClass ${err} - ${Class}`;
this._implementation = Class;
this._implementsClass = true;
}
static registerInstance(instance) {
if (this._used) throw "Cannot register a storage implementation after it had been already used!";
const err = errInstanceApi(instance, storageAPI);
if (err) throw `XOpatStorage.<*>:registerInstance ${err} - ${instance}`;
this._implementation = instance;
this._implementsClass = false;
}
static registered() {
return !!this._implementation;
}
}
/**
* Synchronous Data Generic API. Private class.
* @extends APIProxy
* @private
*/
class SyncAPIProxy extends APIProxy {
constructor(options) {
super(options);
}
/**
*
* @param {any} key
* @param {any} defaultValue returned only in case undefined would be returned
* @return {*|undefined} value to store, or undefined in the default value is missing
*/
get(key, defaultValue=undefined) {
let value = this.__storage.getItem(this.validateKey(key));
if (value === undefined) {
//todo not prefix deprecated keys? must be able to configure
for (let dKey of this.deprecatedKeys(key)) {
value = this.__storage.getItem(this.validateKey(dKey, false));
if (value !== undefined) break;
}
}
if (value === "false") value = false;
else if (value === "true") value = true;
if (defaultValue !== undefined) {
return value === null || value === undefined ? defaultValue : value;
}
return value;
}
/**
* @param {string} key
* @param {string} value
*/
set(key, value) {
key = this.validateKey(key);
this.__storage.setItem(key, value);
}
/**
* @param {string} key
* @param key
*/
delete(key) {
key = this.validateKey(key);
this.__storage.removeItem(key);
}
}
/**
* Asynchronous Data Generic API. Private class.
* @extends APIProxy
* @private
*/
class AsyncAPIProxy extends APIProxy {
constructor(options) {
super(options);
}
/**
*
* @param {any} key
* @param {any} defaultValue returned only in case undefined would be returned
* @return {Promise<*|undefined>} value to store, or undefined in the default value is missing
*/
async get(key, defaultValue=undefined) {
let value = await this.__storage.getItem(this.validateKey(key));
if (value === undefined) {
for (let dKey of this.deprecatedKeys(key)) {
value = await this.__storage.getItem(this.validateKey(dKey, false));
if (value !== undefined) break;
}
}
if (value === "false") value = false;
else if (value === "true") value = true;
if (defaultValue !== undefined) {
return value === null || value === undefined ? defaultValue : value;
}
return value;
}
/**
* @param {string} key
* @param {string} value
* @return Promise<void>
*/
async set(key, value) {
key = this.validateKey(key);
await this.__storage.setItem(key, value);
}
/**
* @param {string} key
* @param key
*/
async delete(key) {
key = this.validateKey(key);
await this.__storage.removeItem(key);
}
}
/**
* Storage Namespace for xOpat.
* @namespace XOpatStorage
*/
$.XOpatStorage = {
/**
* Storage API replacement: window.Storage cannot be instantiated.
* see https://developer.mozilla.org/en-US/docs/Web/API/Storage
* @type {XOpatStorage.Storage}
*/
Storage: xoStorage,
/**
* Storage API with extension for cookies.
* This storage allows .with(...).setItem(...) syntax
* to pass set options explicitly.
*/
CookieStorage: xoCookieStorage,
/**
* Similar to Storage, AsyncStorage supports asynchronous storage interface.
* see https://developer.mozilla.org/en-US/docs/Web/API/Storage
* @type {XOpatStorage.AsyncStorage}
* @memberOf XOpatStorage
*/
AsyncStorage: xoAsyncStorage,
/**
* Data Interface for persistent storage of data items.
*
* This Data class is by default used to save plugin data within HTTP POST.
* Apps should extend and use this class to store their data to desired endpoints.
*
* @type {XOpatStorage.Data}
* @extends AsyncAPIProxy
* @memberOf XOpatStorage
*/
Data: class extends AsyncAPIProxy {},
/**
* Cache Interface for storage of configuration / metadata.
* Cache is meant for cached user configuration and settings to avoid repetitive UI flows.
*
* This storage _can_ be persistent. This interface must be sync: if you use async server access,
* make sure to e.g. prefetch the data in given context.
*
* The default implementation stores this data within browser local storage.
* @type {XOpatStorage.Cache}
* @extends SyncAPIProxy
* @memberOf XOpatStorage
*/
Cache: class extends SyncAPIProxy {
/**
* @param {any} key
* @param {any} defaultValue returned only in case undefined would be returned
* @return {any} value to store, or undefined in the default value is missing
*/
get(key, defaultValue=undefined) {
// !!! Without cache=false, this would be infinite loop getOption calls this method too
if (!APPLICATION_CONTEXT.getOption("bypassCache", false, false)) {
return super.get(key, defaultValue);
}
return defaultValue;
}
/**
* @param {string} key
* @param {string} value
*/
set(key, value) {
// !!! Without cache=false, this would be infinite loop getOption calls this method too
if (!APPLICATION_CONTEXT.getOption("bypassCache", false, false)) {
super.set(key, value);
}
}
},
/**
* Cookie Interface for storage of configuration / metadata.
* Cookies should be used only when Cache cannot be used, e.g. ensuring
* token security, or caching values only for certain amount of time.
*
* This storage is NOT persistent. This interface must be sync: if you use async server access,
* make sure to e.g. prefetch the data in given context.
*
* Note that this class _should_ behave like common cookies, e.g. have expiration, share data on common domain/path etc.
*
* The default implementation stores this data within browser cookies.
* @type {XOpatStorage.Cookies}
* @extends SyncAPIProxy
* @memberOf XOpatStorage
*/
Cookies: class extends SyncAPIProxy {
constructor(options) {
super(options, xoCookieStorage); //allow also xoCookieStorage
}
/**
* @param {any} key
* @param {any} defaultValue returned only in case undefined would be returned
* @return {any} value to store, or undefined in the default value is missing
*/
get(key, defaultValue=undefined) {
if (!APPLICATION_CONTEXT.getOption("bypassCookies", false, false)) {
return super.get(key, defaultValue);
}
return defaultValue;
}
/**
* Provide cookie setter with setting options
* @param {object} options
* @return XOpatStorage.Cookies
*/
with(options) {
if (typeof this.__storage.with === "function") {
this.__storage.with(options);
} else {
console.warn("Current cookie storage does not support with() option setter.");
this.with = function () { return this; }; //register as no-op
}
return this;
}
/**
* @param {string} key
* @param {string} value
*/
set(key, value) {
if (!APPLICATION_CONTEXT.getOption("bypassCookies", false, false)) {
super.set(key, value);
}
}
}
}
})(window);