@omniconvert/server-side-testing-sdk
Version:
TypeScript SDK for server-side A/B testing and experimentation
260 lines • 9.14 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.LocalStorageDriver = void 0;
/**
* LocalStorage-based storage driver for browser environments
* Replaces Redis functionality with browser localStorage
*/
class LocalStorageDriver {
constructor(ttl = LocalStorageDriver.DEFAULT_TTL) {
this.ttl = ttl;
this.cleanupExpiredItems();
}
/**
* Get data from localStorage
*/
get(key) {
const storageKey = this.buildKey(key);
const ttlKey = this.buildTtlKey(key);
try {
// Check if item has expired
const expirationTime = localStorage.getItem(ttlKey);
if (expirationTime) {
const expiration = parseInt(expirationTime, 10);
if (Date.now() > expiration) {
// Item has expired, remove it
this.remove(key);
return null;
}
}
const compressedValue = localStorage.getItem(storageKey);
if (!compressedValue) {
return null;
}
// Try to decompress the value (if it was compressed)
try {
return this.decompress(compressedValue);
}
catch {
// If decompression fails, return the value as-is
return compressedValue;
}
}
catch (error) {
console.error('LocalStorageDriver::get error:', error);
return null;
}
}
/**
* Save data to localStorage
*/
save(key, data) {
const storageKey = this.buildKey(key);
const ttlKey = this.buildTtlKey(key);
try {
// Serialize data if it's not already a string
let serializedData;
if (typeof data === 'string') {
serializedData = data;
}
else {
serializedData = JSON.stringify(data);
}
// Compress the data to save space
const compressedData = this.compress(serializedData);
// Set expiration time
const expirationTime = Date.now() + this.ttl * 1000;
// Store both the data and its expiration time
localStorage.setItem(storageKey, compressedData);
localStorage.setItem(ttlKey, expirationTime.toString());
return true;
}
catch (error) {
console.error('LocalStorageDriver::save error:', error);
// If localStorage is full, try to cleanup and retry once
if (error instanceof DOMException && error.name === 'QuotaExceededError') {
this.cleanupExpiredItems();
try {
const compressedData = this.compress(typeof data === 'string' ? data : JSON.stringify(data));
const expirationTime = Date.now() + this.ttl * 1000;
localStorage.setItem(storageKey, compressedData);
localStorage.setItem(ttlKey, expirationTime.toString());
return true;
}
catch {
console.warn('LocalStorage quota exceeded even after cleanup');
}
}
return false;
}
}
/**
* Remove an item from localStorage
*/
remove(key) {
const storageKey = this.buildKey(key);
const ttlKey = this.buildTtlKey(key);
try {
localStorage.removeItem(storageKey);
localStorage.removeItem(ttlKey);
}
catch (error) {
console.error('LocalStorageDriver::remove error:', error);
}
}
/**
* Clear all items with our prefix
*/
clear() {
try {
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith(LocalStorageDriver.KEY_PREFIX)) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(key => localStorage.removeItem(key));
}
catch (error) {
console.error('LocalStorageDriver::clear error:', error);
}
}
/**
* Get all keys with our prefix
*/
getAllKeys() {
const keys = [];
try {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith(LocalStorageDriver.KEY_PREFIX)) {
// Remove prefix and TTL suffix to get original key
const originalKey = key
.substring(LocalStorageDriver.KEY_PREFIX.length + 1)
.replace(LocalStorageDriver.TTL_SUFFIX, '');
if (!key.endsWith(LocalStorageDriver.TTL_SUFFIX) && !keys.includes(originalKey)) {
keys.push(originalKey);
}
}
}
}
catch (error) {
console.error('LocalStorageDriver::getAllKeys error:', error);
}
return keys;
}
/**
* Check if localStorage is available
*/
static isAvailable() {
try {
const testKey = '__localStorage_test__';
localStorage.setItem(testKey, 'test');
localStorage.removeItem(testKey);
return true;
}
catch {
return false;
}
}
/**
* Get storage usage information
*/
getStorageInfo() {
try {
let used = 0;
// Calculate used space for our keys
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith(LocalStorageDriver.KEY_PREFIX)) {
const value = localStorage.getItem(key);
used += (key.length + (value?.length || 0)) * 2; // * 2 for UTF-16 encoding
}
}
// Estimate total localStorage capacity (varies by browser, typically 5-10MB)
const total = 5 * 1024 * 1024; // 5MB default estimate
const available = total - used;
return { used, total, available };
}
catch (error) {
console.error('LocalStorageDriver::getStorageInfo error:', error);
return { used: 0, total: 0, available: 0 };
}
}
/**
* Build the storage key with prefix
*/
buildKey(key) {
return `${LocalStorageDriver.KEY_PREFIX}_${key}`;
}
/**
* Build the TTL key
*/
buildTtlKey(key) {
return `${this.buildKey(key)}${LocalStorageDriver.TTL_SUFFIX}`;
}
/**
* Compress data (simple base64 encoding for now)
* In a real implementation, you might want to use actual compression algorithms
*/
compress(data) {
try {
// Simple compression using btoa (base64 encoding)
// This doesn't actually compress but ensures data integrity
return btoa(encodeURIComponent(data));
}
catch {
// If btoa fails, return data as-is
return data;
}
}
/**
* Decompress data
*/
decompress(data) {
try {
// Decompress using atob (base64 decoding)
return decodeURIComponent(atob(data));
}
catch {
// If decompression fails, assume data is not compressed
return data;
}
}
/**
* Clean up expired items to free up space
*/
cleanupExpiredItems() {
try {
const now = Date.now();
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith(LocalStorageDriver.KEY_PREFIX) && key.endsWith(LocalStorageDriver.TTL_SUFFIX)) {
const expirationTime = localStorage.getItem(key);
if (expirationTime && parseInt(expirationTime, 10) < now) {
// This TTL has expired
const originalKey = key
.substring(LocalStorageDriver.KEY_PREFIX.length + 1)
.replace(LocalStorageDriver.TTL_SUFFIX, '');
keysToRemove.push(originalKey);
}
}
}
// Remove expired items
keysToRemove.forEach(key => this.remove(key));
if (keysToRemove.length > 0) {
// Cleaned up expired items (debug logging removed)
}
}
catch (error) {
console.error('LocalStorageDriver::cleanupExpiredItems error:', error);
}
}
}
exports.LocalStorageDriver = LocalStorageDriver;
LocalStorageDriver.TTL_SUFFIX = '_ttl';
LocalStorageDriver.KEY_PREFIX = 'omniconvert';
LocalStorageDriver.DEFAULT_TTL = 31536000; // One year in seconds
//# sourceMappingURL=LocalStorageDriver.js.map