/**
* @classdesc
* Conflict-safe storage extending a ve.init.SafeStorage instance
*
* Implements conflict handling for localStorage:
* Any time the storage is used and it is detected that another process has
* modified the underlying data, all the managed keys are restored from an
* in-memory cache. There is no merging of data, all managed keys are either
* completely overwritten, or deleted if they were not originally set.
*
* This would be namespaced ve.init.ConflictableStorage, but as a generated class
* it is never exported.
*
* @class ve.init.ConflictableStorage
* @extends ve.init.SafeStorage
* @hideconstructor
*/
/**
* Create a conflictable storage object, extending a safe storage object.
*
* @param {ve.init.SafeStorage} storage Storage class to extend
* @return {ve.init.ConflictableStorage} [description]
*/
ve.init.createConflictableStorage = function ( storage ) {
const conflictKey = '__conflictId';
const EXPIRY_PREFIX = '_EXPIRY_';
/**
* @constructor
* @param {Storage|undefined} store The Storage instance to wrap around
*/
function ConflictableStorage() {
// Parent constructor
ConflictableStorage.super.apply( this, arguments );
this.storageMayConflict = false;
this.conflictBackup = {};
this.conflictableKeys = {};
this.conflictId = null;
}
/* Inheritance */
// Dynamically extend the class of the storage object, in case
// it is a sub-class of SafeStorage.
const ParentStorage = storage.constructor;
OO.inheritClass( ConflictableStorage, ParentStorage );
/* Methods */
/**
* @inheritdoc
*/
ConflictableStorage.prototype.set = function ( key, value ) {
if ( key === conflictKey ) {
throw new Error( 'Can\'t set key ' + conflictKey + ' directly.' );
}
if ( this.storageMayConflict ) {
if ( this.isConflicted() ) {
this.overwriteFromBackup();
}
if ( Object.prototype.hasOwnProperty.call( this.conflictableKeys, key ) ) {
this.conflictBackup[ key ] = value;
}
}
// Parent method
return ConflictableStorage.super.prototype.set.apply( this, arguments );
};
/**
* @inheritdoc
*/
ConflictableStorage.prototype.remove = function ( key ) {
if ( key === conflictKey ) {
throw new Error( 'Can\'t remove key ' + conflictKey + ' directly.' );
}
if ( this.storageMayConflict ) {
if ( this.isConflicted() ) {
this.overwriteFromBackup();
}
if ( Object.prototype.hasOwnProperty.call( this.conflictableKeys, key ) ) {
delete this.conflictBackup[ key ];
}
}
// Parent method
return ConflictableStorage.super.prototype.remove.apply( this, arguments );
};
/**
* @inheritdoc
*/
ConflictableStorage.prototype.get = function () {
if ( this.isConflicted() ) {
this.overwriteFromBackup();
}
// Parent method
return ConflictableStorage.super.prototype.get.apply( this, arguments );
};
/**
* @inheritdoc
*/
ConflictableStorage.prototype.setExpires = function ( key ) {
// Parent method
ConflictableStorage.super.prototype.setExpires.apply( this, arguments );
if ( this.storageMayConflict ) {
if ( Object.prototype.hasOwnProperty.call( this.conflictableKeys, key ) ) {
let expiryAbsolute = null;
try {
expiryAbsolute = this.store.getItem( EXPIRY_PREFIX + key );
} catch ( e ) {}
if ( expiryAbsolute ) {
this.conflictBackup[ EXPIRY_PREFIX + key ] = expiryAbsolute;
} else {
delete this.conflictBackup[ EXPIRY_PREFIX + key ];
}
}
}
};
/**
* Check if another process has written to the shared storage, leaving
* our data in a conflicted state.
*
* @return {boolean} Data is conflicted
*/
ConflictableStorage.prototype.isConflicted = function () {
if ( !this.storageMayConflict ) {
return false;
}
// Read directly from store to avoid any caching used by sub-classes
try {
return this.store.getItem( conflictKey ) !== this.conflictId;
} catch ( e ) {
return false;
}
};
/**
* Overwrite data in the store from our in-memory backup
*
* Only keys added in #addConflictableKeys are restored
*/
ConflictableStorage.prototype.overwriteFromBackup = function () {
// Call parent method directly when setting conflict key
ConflictableStorage.super.prototype.set.call( this, conflictKey, this.conflictId );
for ( const key in this.conflictableKeys ) {
if ( Object.prototype.hasOwnProperty.call( this.conflictBackup, key ) && this.conflictBackup[ key ] !== null ) {
const expiryKey = EXPIRY_PREFIX + key;
const expiryAbsolute = this.conflictBackup[ expiryKey ];
let expiry = null;
if ( expiryAbsolute ) {
expiry = expiryAbsolute - Math.floor( Date.now() / 1000 );
}
// Call parent methods directly when restoring
ConflictableStorage.super.prototype.set.call( this, key, this.conflictBackup[ key ], expiry );
} else {
ConflictableStorage.super.prototype.remove.call( this, key, this.conflictBackup[ key ] );
}
}
};
/**
* Add keys which will need to be conflict-aware
*
* @param {Object} keys Object with conflict-aware keys as keys, and value set to true
*/
ConflictableStorage.prototype.addConflictableKeys = function ( keys ) {
ve.extendObject( this.conflictableKeys, keys );
this.storageMayConflict = true;
if ( !this.conflictId ) {
this.conflictId = Math.random().toString( 36 ).slice( 2 );
// Call parent method directly when setting conflict key
ConflictableStorage.super.prototype.set.call( this, conflictKey, this.conflictId );
}
for ( const key in keys ) {
if ( Object.prototype.hasOwnProperty.call( keys, key ) ) {
this.conflictBackup[ key ] = this.get( key );
let expiryAbsolute = null;
try {
expiryAbsolute = this.store.getItem( EXPIRY_PREFIX + key );
} catch ( e ) {}
if ( expiryAbsolute ) {
this.conflictBackup[ EXPIRY_PREFIX + key ] = expiryAbsolute;
}
}
}
};
return new ConflictableStorage( storage.store );
};