javascript

exercises

exercises.js
/**
 * ========================================
 * 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.'
);
Exercises - JavaScript Tutorial | DeepML