javascript
exercises
exercises.js⚡javascript
/**
* ========================================
* 11.1 localStorage and sessionStorage - Exercises
* ========================================
*
* Practice web storage APIs.
* Complete each exercise by filling in the code.
*/
/**
* EXERCISE 1: Basic Storage Operations
*
* Implement a function that performs basic storage operations.
*
* Requirements:
* - Store, retrieve, and remove items
* - Handle missing keys gracefully
*/
function basicStorageOps() {
// 1. Store your name in localStorage with key 'myName'
// 2. Store your age in localStorage with key 'myAge'
// 3. Retrieve and log both values
// 4. Check if 'nonexistent' key exists, log result
// 5. Remove 'myAge' from storage
// 6. Log the total number of items in localStorage
}
/**
* EXERCISE 2: JSON Storage Wrapper
*
* Create functions that automatically handle JSON serialization.
*/
function setJSON(key, value) {
// Your code here
}
function getJSON(key, defaultValue = null) {
// Your code here
}
// Test with:
// setJSON('user', { name: 'John', age: 30 });
// console.log(getJSON('user'));
// console.log(getJSON('missing', { default: true }));
/**
* EXERCISE 3: Storage with Prefix
*
* Create a storage wrapper that prefixes all keys.
*/
function createPrefixedStorage(prefix) {
// Return an object with set, get, remove, clear methods
// All keys should be prefixed with the given prefix
}
// Test:
// const appStorage = createPrefixedStorage('myApp_');
// appStorage.set('user', 'John');
// console.log(appStorage.get('user')); // 'John'
// console.log(localStorage.getItem('myApp_user')); // 'John'
/**
* EXERCISE 4: Storage with Expiration
*
* Implement storage that automatically expires items.
*/
const expiringStorage = {
set(key, value, ttlSeconds) {
// Store value with expiration timestamp
},
get(key) {
// Return value if not expired, null otherwise
// Remove expired items
},
getRemainingTime(key) {
// Return remaining time in seconds, or 0 if expired
},
};
// Test:
// expiringStorage.set('token', 'abc123', 60); // expires in 60 seconds
// console.log(expiringStorage.get('token')); // 'abc123'
// console.log(expiringStorage.getRemainingTime('token')); // ~60
/**
* EXERCISE 5: Shopping Cart Storage
*
* Implement a shopping cart that persists to localStorage.
*/
class ShoppingCart {
constructor(storageKey = 'cart') {
this.storageKey = storageKey;
}
addItem(item) {
// Add item to cart (item has id, name, price, quantity)
// If item already exists, increase quantity
}
removeItem(itemId) {
// Remove item from cart
}
updateQuantity(itemId, quantity) {
// Update item quantity
// Remove if quantity is 0 or less
}
getItems() {
// Return all items in cart
}
getTotal() {
// Return total price of all items
}
clear() {
// Clear the cart
}
getItemCount() {
// Return total number of items (sum of quantities)
}
}
/**
* EXERCISE 6: Recently Viewed Items
*
* Track recently viewed items with a maximum limit.
*/
class RecentlyViewed {
constructor(maxItems = 10) {
this.maxItems = maxItems;
this.storageKey = 'recentlyViewed';
}
add(item) {
// Add item to recently viewed
// Remove duplicates (same id)
// Keep only maxItems, removing oldest
}
getAll() {
// Return all recently viewed items
}
clear() {
// Clear recently viewed
}
}
/**
* EXERCISE 7: Form Auto-Save
*
* Implement auto-saving for form data.
*/
class FormAutoSave {
constructor(formId, storageType = 'session') {
this.formId = formId;
this.storageKey = `formAutoSave_${formId}`;
this.storage = storageType === 'session' ? sessionStorage : localStorage;
}
save(formData) {
// Save form data with timestamp
}
load() {
// Load saved form data
}
hasSavedData() {
// Check if there's saved data
}
clear() {
// Clear saved data
}
getLastSaveTime() {
// Return when data was last saved
}
}
/**
* EXERCISE 8: Storage Statistics
*
* Calculate storage usage statistics.
*/
function getStorageStats(storage = localStorage) {
// Return object with:
// - totalItems: number of items
// - totalSize: size in bytes
// - items: array of { key, size } sorted by size descending
// - largestItem: key of largest item
// - averageSize: average item size
}
/**
* EXERCISE 9: Storage Migration
*
* Implement storage versioning and migration.
*/
class VersionedStorage {
constructor(key, currentVersion) {
this.key = key;
this.currentVersion = currentVersion;
this.migrations = new Map();
}
addMigration(fromVersion, toVersion, migrateFn) {
// Register a migration function
}
get() {
// Get data, running necessary migrations
}
set(data) {
// Save data with current version
}
// Private: run migrations to bring data up to current version
_migrate(storedData) {}
}
// Example usage:
// const storage = new VersionedStorage('userData', 3);
// storage.addMigration(1, 2, (data) => ({ ...data, email: '' }));
// storage.addMigration(2, 3, (data) => ({ ...data, preferences: {} }));
/**
* EXERCISE 10: Encrypted Storage
*
* Implement basic obfuscation for stored data.
* Note: This is NOT secure encryption, just obfuscation!
*/
const obfuscatedStorage = {
// Simple XOR-based obfuscation (NOT SECURE!)
_encode(str, key) {
// Implement simple encoding
},
_decode(str, key) {
// Implement decoding
},
set(key, value, secret) {
// Store obfuscated value
},
get(key, secret) {
// Retrieve and decode value
},
};
/**
* EXERCISE 11: Storage Sync Manager
*
* Manage state synchronization across tabs.
*/
class StorageSyncManager {
constructor(namespace) {
this.namespace = namespace;
this.handlers = new Map();
this._setupListener();
}
_setupListener() {
// Set up storage event listener
}
setState(key, value) {
// Set state and notify other tabs
}
getState(key) {
// Get current state
}
onStateChange(key, handler) {
// Register handler for state changes
}
broadcast(eventType, data) {
// Broadcast event to all tabs
}
}
/**
* EXERCISE 12: LRU Cache with Storage
*
* Implement a Least Recently Used cache backed by storage.
*/
class LRUStorageCache {
constructor(maxSize, storageKey = 'lruCache') {
this.maxSize = maxSize;
this.storageKey = storageKey;
}
get(key) {
// Get item and update access time
}
set(key, value) {
// Set item, evicting least recently used if at capacity
}
has(key) {
// Check if key exists
}
delete(key) {
// Remove item
}
clear() {
// Clear cache
}
// Private: evict least recently used item
_evict() {}
}
/**
* EXERCISE 13: Storage Quota Manager
*
* Handle storage quota limits gracefully.
*/
class QuotaManager {
constructor(warningThreshold = 0.8) {
this.warningThreshold = warningThreshold;
}
getUsage() {
// Return { used, total, percentage }
}
isNearLimit() {
// Check if usage is above warning threshold
}
safeSet(key, value, onQuotaExceeded) {
// Try to set, call callback if quota exceeded
}
makeRoom(bytesNeeded) {
// Remove items to free up space
// Return true if successful
}
cleanup(olderThanMs) {
// Remove items older than specified time
}
}
/**
* EXERCISE 14: Undo/Redo with Storage
*
* Implement undo/redo functionality using storage.
*/
class UndoRedoStorage {
constructor(key, maxHistory = 50) {
this.key = key;
this.maxHistory = maxHistory;
this.historyKey = `${key}_history`;
this.positionKey = `${key}_position`;
}
get() {
// Get current value
}
set(value) {
// Set value and add to history
}
undo() {
// Undo last change, return new value
}
redo() {
// Redo last undone change, return new value
}
canUndo() {
// Check if undo is available
}
canRedo() {
// Check if redo is available
}
clearHistory() {
// Clear undo/redo history
}
}
/**
* EXERCISE 15: Storage-backed State Machine
*
* Implement a state machine that persists state.
*/
class PersistentStateMachine {
constructor(config, storageKey) {
// config: { initial, states: { stateName: { on: { EVENT: 'nextState' } } } }
this.config = config;
this.storageKey = storageKey;
}
getState() {
// Get current state (from storage or initial)
}
send(event) {
// Process event and transition to next state
// Return new state
}
can(event) {
// Check if event is valid for current state
}
reset() {
// Reset to initial state
}
}
// Example config:
// {
// initial: 'idle',
// states: {
// idle: { on: { START: 'running' } },
// running: { on: { PAUSE: 'paused', STOP: 'idle' } },
// paused: { on: { RESUME: 'running', STOP: 'idle' } }
// }
// }
// ============================================
// SOLUTIONS (Hidden - Scroll to reveal)
// ============================================
/*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
* SOLUTIONS BELOW
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
// SOLUTION 1: Basic Storage Operations
function basicStorageOpsSolution() {
// 1. Store name
localStorage.setItem('myName', 'John');
// 2. Store age
localStorage.setItem('myAge', '30');
// 3. Retrieve and log
console.log('Name:', localStorage.getItem('myName'));
console.log('Age:', localStorage.getItem('myAge'));
// 4. Check nonexistent
const exists = localStorage.getItem('nonexistent') !== null;
console.log('nonexistent exists:', exists);
// 5. Remove age
localStorage.removeItem('myAge');
// 6. Log count
console.log('Total items:', localStorage.length);
// Cleanup
localStorage.removeItem('myName');
}
// SOLUTION 2: JSON Storage Wrapper
function setJSONSolution(key, value) {
localStorage.setItem(key, JSON.stringify(value));
}
function getJSONSolution(key, defaultValue = null) {
const item = localStorage.getItem(key);
if (item === null) return defaultValue;
try {
return JSON.parse(item);
} catch {
return defaultValue;
}
}
// SOLUTION 3: Storage with Prefix
function createPrefixedStorageSolution(prefix) {
return {
set(key, value) {
localStorage.setItem(prefix + key, JSON.stringify(value));
},
get(key, defaultValue = null) {
const item = localStorage.getItem(prefix + key);
if (item === null) return defaultValue;
try {
return JSON.parse(item);
} catch {
return item;
}
},
remove(key) {
localStorage.removeItem(prefix + key);
},
clear() {
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith(prefix)) {
keysToRemove.push(key);
}
}
keysToRemove.forEach((key) => localStorage.removeItem(key));
},
};
}
// SOLUTION 4: Storage with Expiration
const expiringStorageSolution = {
set(key, value, ttlSeconds) {
const item = {
value: value,
expiry: Date.now() + ttlSeconds * 1000,
};
localStorage.setItem(key, JSON.stringify(item));
},
get(key) {
const itemStr = localStorage.getItem(key);
if (!itemStr) return null;
try {
const item = JSON.parse(itemStr);
if (Date.now() > item.expiry) {
localStorage.removeItem(key);
return null;
}
return item.value;
} catch {
return null;
}
},
getRemainingTime(key) {
const itemStr = localStorage.getItem(key);
if (!itemStr) return 0;
try {
const item = JSON.parse(itemStr);
const remaining = Math.max(0, item.expiry - Date.now());
return Math.round(remaining / 1000);
} catch {
return 0;
}
},
};
// SOLUTION 5: Shopping Cart Storage
class ShoppingCartSolution {
constructor(storageKey = 'cart') {
this.storageKey = storageKey;
}
_load() {
const data = localStorage.getItem(this.storageKey);
return data ? JSON.parse(data) : [];
}
_save(items) {
localStorage.setItem(this.storageKey, JSON.stringify(items));
}
addItem(item) {
const items = this._load();
const existingIndex = items.findIndex((i) => i.id === item.id);
if (existingIndex >= 0) {
items[existingIndex].quantity += item.quantity || 1;
} else {
items.push({ ...item, quantity: item.quantity || 1 });
}
this._save(items);
return items;
}
removeItem(itemId) {
const items = this._load().filter((i) => i.id !== itemId);
this._save(items);
return items;
}
updateQuantity(itemId, quantity) {
const items = this._load();
const index = items.findIndex((i) => i.id === itemId);
if (index >= 0) {
if (quantity <= 0) {
items.splice(index, 1);
} else {
items[index].quantity = quantity;
}
this._save(items);
}
return items;
}
getItems() {
return this._load();
}
getTotal() {
return this._load().reduce((sum, item) => {
return sum + item.price * item.quantity;
}, 0);
}
clear() {
this._save([]);
}
getItemCount() {
return this._load().reduce((sum, item) => sum + item.quantity, 0);
}
}
// SOLUTION 6: Recently Viewed Items
class RecentlyViewedSolution {
constructor(maxItems = 10) {
this.maxItems = maxItems;
this.storageKey = 'recentlyViewed';
}
_load() {
const data = localStorage.getItem(this.storageKey);
return data ? JSON.parse(data) : [];
}
_save(items) {
localStorage.setItem(this.storageKey, JSON.stringify(items));
}
add(item) {
let items = this._load();
// Remove existing with same id
items = items.filter((i) => i.id !== item.id);
// Add to beginning
items.unshift({
...item,
viewedAt: Date.now(),
});
// Keep only maxItems
items = items.slice(0, this.maxItems);
this._save(items);
return items;
}
getAll() {
return this._load();
}
clear() {
localStorage.removeItem(this.storageKey);
}
}
// SOLUTION 7: Form Auto-Save
class FormAutoSaveSolution {
constructor(formId, storageType = 'session') {
this.formId = formId;
this.storageKey = `formAutoSave_${formId}`;
this.storage = storageType === 'session' ? sessionStorage : localStorage;
}
save(formData) {
const data = {
formData: formData,
savedAt: Date.now(),
};
this.storage.setItem(this.storageKey, JSON.stringify(data));
}
load() {
const saved = this.storage.getItem(this.storageKey);
if (!saved) return null;
try {
const data = JSON.parse(saved);
return data.formData;
} catch {
return null;
}
}
hasSavedData() {
return this.storage.getItem(this.storageKey) !== null;
}
clear() {
this.storage.removeItem(this.storageKey);
}
getLastSaveTime() {
const saved = this.storage.getItem(this.storageKey);
if (!saved) return null;
try {
const data = JSON.parse(saved);
return new Date(data.savedAt);
} catch {
return null;
}
}
}
// SOLUTION 8: Storage Statistics
function getStorageStatsSolution(storage = localStorage) {
const items = [];
let totalSize = 0;
for (let i = 0; i < storage.length; i++) {
const key = storage.key(i);
const value = storage.getItem(key);
const size = (key.length + value.length) * 2; // UTF-16
items.push({ key, size });
totalSize += size;
}
// Sort by size descending
items.sort((a, b) => b.size - a.size);
return {
totalItems: storage.length,
totalSize: totalSize,
items: items,
largestItem: items.length > 0 ? items[0].key : null,
averageSize:
storage.length > 0 ? Math.round(totalSize / storage.length) : 0,
};
}
// SOLUTION 9: Storage Migration
class VersionedStorageSolution {
constructor(key, currentVersion) {
this.key = key;
this.currentVersion = currentVersion;
this.migrations = new Map();
}
addMigration(fromVersion, toVersion, migrateFn) {
this.migrations.set(`${fromVersion}->${toVersion}`, {
from: fromVersion,
to: toVersion,
migrate: migrateFn,
});
}
get() {
const stored = localStorage.getItem(this.key);
if (!stored) return null;
try {
const parsed = JSON.parse(stored);
if (parsed.version !== this.currentVersion) {
return this._migrate(parsed);
}
return parsed.data;
} catch {
return null;
}
}
set(data) {
const wrapper = {
version: this.currentVersion,
data: data,
};
localStorage.setItem(this.key, JSON.stringify(wrapper));
}
_migrate(storedData) {
let { version, data } = storedData;
while (version < this.currentVersion) {
const nextVersion = version + 1;
const migration = this.migrations.get(`${version}->${nextVersion}`);
if (!migration) {
console.warn(`No migration from ${version} to ${nextVersion}`);
break;
}
data = migration.migrate(data);
version = nextVersion;
}
// Save migrated data
this.set(data);
return data;
}
}
// SOLUTION 10: Encrypted Storage (Obfuscation)
const obfuscatedStorageSolution = {
_encode(str, key) {
let result = '';
for (let i = 0; i < str.length; i++) {
const charCode = str.charCodeAt(i) ^ key.charCodeAt(i % key.length);
result += String.fromCharCode(charCode);
}
return btoa(result);
},
_decode(str, key) {
const decoded = atob(str);
let result = '';
for (let i = 0; i < decoded.length; i++) {
const charCode = decoded.charCodeAt(i) ^ key.charCodeAt(i % key.length);
result += String.fromCharCode(charCode);
}
return result;
},
set(key, value, secret) {
const encoded = this._encode(JSON.stringify(value), secret);
localStorage.setItem(key, encoded);
},
get(key, secret) {
const encoded = localStorage.getItem(key);
if (!encoded) return null;
try {
const decoded = this._decode(encoded, secret);
return JSON.parse(decoded);
} catch {
return null;
}
},
};
// SOLUTION 11: Storage Sync Manager
class StorageSyncManagerSolution {
constructor(namespace) {
this.namespace = namespace;
this.handlers = new Map();
this._setupListener();
}
_setupListener() {
window.addEventListener('storage', (e) => {
if (e.key && e.key.startsWith(`${this.namespace}_`)) {
const key = e.key.substring(this.namespace.length + 1);
if (this.handlers.has(key)) {
const newValue = e.newValue ? JSON.parse(e.newValue) : null;
const oldValue = e.oldValue ? JSON.parse(e.oldValue) : null;
this.handlers.get(key).forEach((handler) => {
handler({ key, newValue, oldValue });
});
}
}
});
}
setState(key, value) {
localStorage.setItem(`${this.namespace}_${key}`, JSON.stringify(value));
}
getState(key) {
const stored = localStorage.getItem(`${this.namespace}_${key}`);
return stored ? JSON.parse(stored) : null;
}
onStateChange(key, handler) {
if (!this.handlers.has(key)) {
this.handlers.set(key, []);
}
this.handlers.get(key).push(handler);
}
broadcast(eventType, data) {
const message = {
type: eventType,
data: data,
timestamp: Date.now(),
};
localStorage.setItem(
`${this.namespace}_broadcast`,
JSON.stringify(message)
);
setTimeout(() => {
localStorage.removeItem(`${this.namespace}_broadcast`);
}, 100);
}
}
// SOLUTION 12: LRU Cache with Storage
class LRUStorageCacheSolution {
constructor(maxSize, storageKey = 'lruCache') {
this.maxSize = maxSize;
this.storageKey = storageKey;
}
_load() {
const data = localStorage.getItem(this.storageKey);
return data ? JSON.parse(data) : { entries: {}, order: [] };
}
_save(cache) {
localStorage.setItem(this.storageKey, JSON.stringify(cache));
}
get(key) {
const cache = this._load();
if (!(key in cache.entries)) return null;
// Move to end (most recently used)
cache.order = cache.order.filter((k) => k !== key);
cache.order.push(key);
this._save(cache);
return cache.entries[key];
}
set(key, value) {
const cache = this._load();
// If key exists, remove from order
if (key in cache.entries) {
cache.order = cache.order.filter((k) => k !== key);
}
// Evict if at capacity
while (cache.order.length >= this.maxSize) {
this._evict(cache);
}
cache.entries[key] = value;
cache.order.push(key);
this._save(cache);
}
has(key) {
const cache = this._load();
return key in cache.entries;
}
delete(key) {
const cache = this._load();
if (key in cache.entries) {
delete cache.entries[key];
cache.order = cache.order.filter((k) => k !== key);
this._save(cache);
return true;
}
return false;
}
clear() {
this._save({ entries: {}, order: [] });
}
_evict(cache) {
if (cache.order.length === 0) return;
const oldest = cache.order.shift();
delete cache.entries[oldest];
}
}
// SOLUTION 13: Storage Quota Manager
class QuotaManagerSolution {
constructor(warningThreshold = 0.8) {
this.warningThreshold = warningThreshold;
this.estimatedLimit = 5 * 1024 * 1024; // 5MB estimate
}
getUsage() {
let used = 0;
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
used += (key.length + localStorage.getItem(key).length) * 2;
}
return {
used: used,
total: this.estimatedLimit,
percentage: ((used / this.estimatedLimit) * 100).toFixed(2),
};
}
isNearLimit() {
const usage = this.getUsage();
return usage.used / usage.total > this.warningThreshold;
}
safeSet(key, value, onQuotaExceeded) {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (e) {
if (e.name === 'QuotaExceededError') {
if (onQuotaExceeded) {
onQuotaExceeded(e);
}
return false;
}
throw e;
}
}
makeRoom(bytesNeeded) {
// Get items with metadata
const items = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
try {
const value = localStorage.getItem(key);
const parsed = JSON.parse(value);
items.push({
key,
size: (key.length + value.length) * 2,
timestamp: parsed.timestamp || 0,
});
} catch {
items.push({ key, size: 0, timestamp: 0 });
}
}
// Sort by timestamp (oldest first)
items.sort((a, b) => a.timestamp - b.timestamp);
let freedBytes = 0;
for (const item of items) {
if (freedBytes >= bytesNeeded) break;
localStorage.removeItem(item.key);
freedBytes += item.size;
}
return freedBytes >= bytesNeeded;
}
cleanup(olderThanMs) {
const cutoff = Date.now() - olderThanMs;
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
try {
const value = JSON.parse(localStorage.getItem(key));
if (value.timestamp && value.timestamp < cutoff) {
keysToRemove.push(key);
}
} catch {
// Skip non-JSON items
}
}
keysToRemove.forEach((key) => localStorage.removeItem(key));
return keysToRemove.length;
}
}
// SOLUTION 14: Undo/Redo with Storage
class UndoRedoStorageSolution {
constructor(key, maxHistory = 50) {
this.key = key;
this.maxHistory = maxHistory;
this.historyKey = `${key}_history`;
this.positionKey = `${key}_position`;
}
_getHistory() {
const data = localStorage.getItem(this.historyKey);
return data ? JSON.parse(data) : [];
}
_setHistory(history) {
localStorage.setItem(this.historyKey, JSON.stringify(history));
}
_getPosition() {
const pos = localStorage.getItem(this.positionKey);
return pos ? parseInt(pos, 10) : -1;
}
_setPosition(pos) {
localStorage.setItem(this.positionKey, pos.toString());
}
get() {
const history = this._getHistory();
const position = this._getPosition();
return position >= 0 && position < history.length
? history[position]
: null;
}
set(value) {
let history = this._getHistory();
let position = this._getPosition();
// Remove any redo entries
history = history.slice(0, position + 1);
// Add new entry
history.push(value);
// Trim to max history
if (history.length > this.maxHistory) {
history = history.slice(history.length - this.maxHistory);
}
this._setHistory(history);
this._setPosition(history.length - 1);
}
undo() {
const position = this._getPosition();
if (position > 0) {
this._setPosition(position - 1);
return this.get();
}
return null;
}
redo() {
const history = this._getHistory();
const position = this._getPosition();
if (position < history.length - 1) {
this._setPosition(position + 1);
return this.get();
}
return null;
}
canUndo() {
return this._getPosition() > 0;
}
canRedo() {
return this._getPosition() < this._getHistory().length - 1;
}
clearHistory() {
localStorage.removeItem(this.historyKey);
localStorage.removeItem(this.positionKey);
}
}
// SOLUTION 15: Storage-backed State Machine
class PersistentStateMachineSolution {
constructor(config, storageKey) {
this.config = config;
this.storageKey = storageKey;
}
getState() {
const saved = localStorage.getItem(this.storageKey);
return saved || this.config.initial;
}
send(event) {
const currentState = this.getState();
const stateConfig = this.config.states[currentState];
if (!stateConfig || !stateConfig.on || !stateConfig.on[event]) {
console.warn(`Invalid event "${event}" for state "${currentState}"`);
return currentState;
}
const nextState = stateConfig.on[event];
localStorage.setItem(this.storageKey, nextState);
return nextState;
}
can(event) {
const currentState = this.getState();
const stateConfig = this.config.states[currentState];
return !!(stateConfig && stateConfig.on && stateConfig.on[event]);
}
reset() {
localStorage.removeItem(this.storageKey);
return this.config.initial;
}
}
console.log('localStorage and sessionStorage exercises loaded!');
console.log(
'Complete each exercise and check against solutions at the bottom.'
);