Page MenuHomeWickedGov Phorge

kvStore.js
No OneTemporary

Size
13 KB
Referenced Files
None
Subscribers
None

kvStore.js

/**
* Module for key-value storage in localStorage, used by CentralNotice campaign
* mixins and in-banner JS.
*
* This class is made available at mw.centralNotice.kvStore
*/
( function () {
var KVStorageContext,
kvStore,
error = null,
campaignName = null,
bannerName = null,
category = null,
cookiesEnabled = null,
localStorageAvailable = null,
now = Math.round( Date.now() / 1000 ),
SEPARATOR = '|',
// | gets encoded in cookies, but is already in use in localStorage
SEPARATOR_IN_COOKIES = '!',
FIND_KEY_REGEX = /\|([^|]*)$/,
// Prefix for all localStorage keys.
// Must correspond with PREFIX_REGEX from kvStoreMaintenance.
PREFIX = 'CentralNoticeKV',
// In cookies, keep it short
PREFIX_IN_COOKIES = 'CN',
// Default TTL of KV store items is 1/2 year, in seconds
DEFAULT_ITEM_TTL = ( 365 / 2 ) * 60 * 60 * 24;
/**
* A context for key-value storage.
*
* @class KVStorageContext
* @param {string} key A unique string to identify this context, when using
* LocalStorage. Must not contain SEPARATOR.
* @param {string} keyInCookies A unique string to identify this context,
* when using cookies. Must not contain SEPARATOR_IN_COOKIES. (Distinct
* keys for cookies help keep cookies small, improving performance.)
*/
KVStorageContext = function ( key, keyInCookies ) {
this.key = key;
this.keyInCookies = keyInCookies;
};
/**
* Are cookies enabled on this client?
* TODO Should this go in core?
*/
function areCookiesEnabled() {
// On the first call, set a cookie and try to read it back
if ( cookiesEnabled === null ) {
mw.cookie.set( 'cookieTest', 'testVal' );
cookiesEnabled = ( mw.cookie.get( 'cookieTest' ) === 'testVal' );
// Clear it out
mw.cookie.set( 'cookieTest', null );
}
return cookiesEnabled;
}
/**
* Is LocalStorage available as a storage option? (Browser
* compatibility and certain user privacy options are required.)
*/
function isLocalStorageAvailable() {
if ( localStorageAvailable === null ) {
// For the KV store to work, the browser has to support
// localStorage, and not throw an error if we try to access or use
// it. (An error can be thrown if the user completely disables
// offline website data/cookies, and in a few other circumstances)
try {
if ( !window.localStorage ) {
localStorageAvailable = false;
} else {
localStorage.setItem( 'localStorageTest', 'testVal' );
localStorageAvailable =
( localStorage.getItem( 'localStorageTest' ) === 'testVal' );
localStorage.removeItem( 'localStorageTest' );
}
} catch ( e ) {
localStorageAvailable = false;
}
}
return localStorageAvailable;
}
/**
* Flag that a problem with key-value storage occurred, and log via mw.log.
*
* @param {string} message A message about the error
* @param {string} key
* @param {*} value
* @param {KVStorageContext} context
*/
function setError( message, key, value, context ) {
error = {
message: message,
key: key,
value: value,
context: context ? context.key : null,
time: new Date()
};
// If a campaign and/or a banner name have been set, include their names
// in the error
error.campaign = campaignName;
error.banner = bannerName;
mw.log( 'CentralNotice KV storage error: ' + JSON.stringify( error ) );
}
/**
* Return the actual key to be used in localStorage, for the given key and
* context.
*
* The key returned should be unique among all localStorage keys used by
* this site. It includes unique strings for centralNotice and context, and
* may also include the campaign name or category.
*
* @param {string} key
* @param {KVStorageContext} context
* @return {string}
*/
function makeKeyForLocalStorage( key, context ) {
var base = PREFIX + SEPARATOR + context.key + SEPARATOR;
switch ( context.key ) {
case kvStore.contexts.CAMPAIGN.key:
return base + campaignName + SEPARATOR + key;
case kvStore.contexts.CATEGORY.key:
return base + category + SEPARATOR + key;
case kvStore.contexts.GLOBAL.key:
return base + key;
default:
setError( 'Invalid KV storage context', key, null, context );
return base + 'invalidContext' + SEPARATOR + key;
}
}
/**
* Return the actual key to be used for a cookie (i.e., the cookie name)
* for the given key and context.
*
* Note: the key used in cookies contains the same information as the
* key used for localStorage, though the cookie key will be shorter.
*
* @param {string} key
* @param {KVStorageContext} context
* @return {string}
*/
function makeKeyForCookie( key, context ) {
var base = PREFIX_IN_COOKIES + SEPARATOR_IN_COOKIES +
context.keyInCookies + SEPARATOR_IN_COOKIES;
switch ( context.key ) {
case kvStore.contexts.CAMPAIGN.key:
return base + campaignName + SEPARATOR_IN_COOKIES + key;
case kvStore.contexts.CATEGORY.key:
return base + category + SEPARATOR_IN_COOKIES + key;
case kvStore.contexts.GLOBAL.key:
return base + key;
default:
setError( 'Invalid KV storage context', key, null, context );
return base + 'invalidContext' + SEPARATOR_IN_COOKIES + key;
}
}
function setLocalStorageItem( key, value, context, ttl ) {
var lsKey, encodedWrappedValue;
lsKey = makeKeyForLocalStorage( key, context );
encodedWrappedValue = JSON.stringify( {
expiry: ttl ? ( ttl * 86400 ) + now : DEFAULT_ITEM_TTL + now,
val: value
} );
// Write the value
try {
localStorage.setItem( lsKey, encodedWrappedValue );
// Check that it was written (it might not have been, if we're over
// the localStorage quota for this site, for example)
if ( localStorage.getItem( lsKey ) !== encodedWrappedValue ) {
setError( 'Couldn\'t write value', key, value, context );
return false;
}
return true;
} catch ( e ) {
setError( 'Couldn\'t write value due to LocalStorage exception ' +
e.toString(), key, value, context );
return false;
}
}
function setCookieItem( key, value, context, ttl ) {
return Boolean( $.cookie(
makeKeyForCookie( key, context ),
encodeURIComponent( JSON.stringify( value ) ),
{ expires: ttl, path: '/' }
) );
}
function getLocalStorageItem( key, context ) {
var lsKey = makeKeyForLocalStorage( key, context ),
rawValue, wrappedValue;
try {
rawValue = localStorage.getItem( lsKey );
} catch ( e ) {
setError( 'Couldn\'t read value due to LocalStorage exception ' +
e.toString(), key, null, context );
return null;
}
if ( rawValue === null ) {
return null;
}
try {
wrappedValue = JSON.parse( rawValue );
} catch ( e ) {
// FIXME: Consider detecting out-of-space errors and perform
// garbage-collection immediately, starting by removing the oldest
// expired (and unexpired) keys older than a certain threshold.
// If the JSON couldn't be parsed, log and return null (which is
// the same value we'd get if the key were not set).
if ( e instanceof SyntaxError ) {
setError( 'Couldn\'t parse value, removing. ' + e.message,
key, rawValue, context );
try {
localStorage.removeItem( lsKey );
} catch ( ex ) {
setError( 'Couldn\'t remove value due to LocalStorage exception ' +
ex.toString(), key, rawValue, context );
}
return null;
// For any other errors, set and re-throw
} else {
setError( 'Couldn\'t read value ' + e.message,
key, rawValue, context );
throw e;
}
}
if ( !wrappedValue.expiry || wrappedValue.expiry < now ) {
return null;
}
return wrappedValue.val;
}
function getCookieItem( key, context ) {
var storageKey = makeKeyForCookie( key, context ),
rawCookie = $.cookie( storageKey );
try {
return JSON.parse( decodeURIComponent( rawCookie ) );
} catch ( e ) {
// The cookie is probably corrupt. Remove.
$.removeCookie( storageKey, { path: '/' } );
return null;
}
}
function removeLocalStorageItem( key, context ) {
try {
localStorage.removeItem( makeKeyForLocalStorage( key, context ) );
} catch ( e ) {
setError( 'Couldn\'t remove value due to LocalStorage exception ' +
e.toString(), key, null, context );
}
}
function removeCookieItem( key, context ) {
$.removeCookie( makeKeyForCookie( key, context ), { path: '/' } );
}
/**
* Public API
*/
kvStore = {
/**
* Available key-value storage contexts
*
* @enum
* @readonly
*/
contexts: {
CAMPAIGN: new KVStorageContext( 'campaign', 'c' ),
CATEGORY: new KVStorageContext( 'category', 't' ),
GLOBAL: new KVStorageContext( 'global', 'g' )
},
/**
* Options for storing data with a cookie or with the kvStore
* (LocalStorage).
*
* @enum
* @readonly
*/
multiStorageOptions: {
LOCAL_STORAGE: 'kv_store',
COOKIE: 'cookie',
NO_STORAGE: 'no_storage'
},
/**
* Set the given value for the given key in the given context, using
* LocalStorage or a cookie. If the key already exists, its value will
* be overwritten.
*
* Value can be any type; will be json-encoded.
*
* Only when using LocalStorage: if the value was set, return true; if
* the value could not be set, we log the error via mw.log and return
* false. The error will be available via getError().
*
* Note: check isAvailable() before calling, or provide a
* multiStorageOption.
*
* Note: when using CAMPAIGN and CATEGORY contexts, ensure that you have
* set campaign and category, respectively. We don't check them here.
*
* @param {string} key
* @param {*} value
* @param {KVStorageContext} context
*
* @param {number} [ttl] Time to live for this item, in days; defaults
* to 1/2 a year. Null will trigger the default.
*
* @param {string} [multiStorageOption] A key from among
* kvStore.multiStorageOptions, to indicate how to store the item.
* Defaults to kvStore.multiStorageOptions.LOCAL_STORAGE.
*
* @return {boolean} true if the value could be set, false otherwise
*/
setItem: function ( key, value, context, ttl, multiStorageOption ) {
// Check validity of key
if ( ( key.indexOf( SEPARATOR ) !== -1 ) ||
( key.indexOf( SEPARATOR_IN_COOKIES ) !== -1 ) ) {
setError( 'Invalid key', key, value, context );
return false;
}
multiStorageOption =
multiStorageOption || kvStore.multiStorageOptions.LOCAL_STORAGE;
switch ( multiStorageOption ) {
case kvStore.multiStorageOptions.LOCAL_STORAGE:
return setLocalStorageItem( key, value, context, ttl );
case kvStore.multiStorageOptions.COOKIE:
return setCookieItem( key, value, context, ttl );
case kvStore.multiStorageOptions.NO_STORAGE:
return false;
default:
throw new Error( 'Unexpected multi-storage option' );
}
},
/**
* Get the stored value for the given key in the given context.
*
* Note: check isAvailable() before calling.
*
* @param {string} key
* @param {KVStorageContext} context
* @param {string} [multiStorageOption] A key from among
* kvStore.multiStorageOptions, to indicate how to store the item.
* Defaults to kvStore.multiStorageOptions.LOCAL_STORAGE.
*/
getItem: function ( key, context, multiStorageOption ) {
multiStorageOption =
multiStorageOption || kvStore.multiStorageOptions.LOCAL_STORAGE;
switch ( multiStorageOption ) {
case kvStore.multiStorageOptions.LOCAL_STORAGE:
return getLocalStorageItem( key, context );
case kvStore.multiStorageOptions.COOKIE:
return getCookieItem( key, context );
case kvStore.multiStorageOptions.NO_STORAGE:
return null;
default:
throw new Error( 'Unexpected multi-storage option' );
}
},
/**
* Remove the stored value for the given key in the given context
*
* Note: check isAvailable() before calling.
*
* @param {string} key
* @param {KVStorageContext} context
* @param {string} [multiStorageOption] A key from among
* kvStore.multiStorageOptions, to indicate how to store the item.
* Defaults to kvStore.multiStorageOptions.LOCAL_STORAGE.
*/
removeItem: function ( key, context, multiStorageOption ) {
multiStorageOption =
multiStorageOption || kvStore.multiStorageOptions.LOCAL_STORAGE;
switch ( multiStorageOption ) {
case kvStore.multiStorageOptions.LOCAL_STORAGE:
removeLocalStorageItem( key, context );
return;
case kvStore.multiStorageOptions.COOKIE:
removeCookieItem( key, context );
return;
case kvStore.multiStorageOptions.NO_STORAGE:
return;
default:
throw new Error( 'Unexpected multi-storage option' );
}
},
/**
* Convenience method to check for availability of storage without
* falling back to cookies.
*/
isAvailable: function () {
return ( kvStore.getMultiStorageOption( false ) !==
kvStore.multiStorageOptions.NO_STORAGE );
},
/**
* Determine the appropriate multi-storage option
*
* @param {boolean} cookieAllowed
* @return {string} A string key
*/
getMultiStorageOption: function ( cookieAllowed ) {
if ( isLocalStorageAvailable() ) {
return kvStore.multiStorageOptions.LOCAL_STORAGE;
}
if ( cookieAllowed && areCookiesEnabled() ) {
return kvStore.multiStorageOptions.COOKIE;
}
return kvStore.multiStorageOptions.NO_STORAGE;
},
/**
* If a KVStore error has occurred (during this page view), return an
* object with information about it. If no KVStore errors have occurred,
* return null.
*
* @return {?Object}
*/
getError: function () {
return error;
},
setNotAvailableError: function () {
setError( 'LocalStorage not available.', null, null );
},
setMaintenanceError: function ( lsKey ) {
var m = lsKey.match( FIND_KEY_REGEX ),
key = m ? m[ 1 ] : null;
setError( 'Error during KVStore maintenance.', key, null );
},
setCampaignName: function ( cName ) {
campaignName = cName;
},
setBannerName: function ( bName ) {
bannerName = bName;
},
setCategory: function ( c ) {
category = c;
}
};
module.exports = kvStore;
}() );

File Metadata

Mime Type
text/plain
Expires
Wed, Aug 19, 00:45 (5 d, 8 h ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
79/8d/3fa1bbb5e885665cd24e33940222
Default Alt Text
kvStore.js (13 KB)

Event Timeline