Desarrollo 10 min de lectura 1964 palabras

CookieStore API: El Futuro Asíncrono de la Gestión de Cookies en JavaScript

EN
CookieStore API: El Futuro Asíncrono de la Gestión de Cookies en JavaScript

CookieStore API: El Futuro Asíncrono de la Gestión de Cookies en JavaScript

Durante décadas, los desarrolladores web hemos dependido de la antigua y limitada interfaz document.cookie para manejar cookies en el navegador. Esta API síncrona, con su sintaxis peculiar de cadenas de texto, ha sido fuente de frustración y errores. Pero eso está cambiando con la llegada de CookieStore API, una interfaz moderna y asíncrona que promete revolucionar cómo interactuamos with cookies.

El Problema con document.cookie

Antes de sumergirnos en CookieStore, recordemos los dolores de cabeza que nos ha causado document.cookie:

Sintaxis Arcaica y Propensa a Errores

// La forma "tradicional" - compleja y error-prone
document.cookie = "username=john_doe; expires=Thu, 18 Dec 2025 12:00:00 UTC; path=/; SameSite=Strict; Secure";

// Leer cookies requiere parsing manual
function getCookie(name) {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) return parts.pop().split(';').shift();
}

Limitaciones Fundamentales

  • Interfaz síncrona: Bloquea el hilo principal
  • Solo disponible en contexto de ventana: No funciona en Service Workers
  • Parsing manual: Requiere código personalizado para leer valores
  • Sin eventos nativos: Imposible escuchar cambios en cookies
  • Propenso a errores: Sintaxis de cadena compleja

Presentando CookieStore API

CookieStore API es la respuesta moderna a estos problemas, ofreciendo:

  • Interfaz asíncrona basada en Promises
  • Disponible en Service Workers
  • Métodos simples y claros
  • Eventos de cambio nativos
  • Mejor manejo de errores
  • Tipado de opciones

Disponibilidad y Compatibilidad

⚠️ Importante: CookieStore API está disponible solo en contextos seguros (HTTPS) y su soporte en navegadores aún es limitado. Actualmente es compatible con:

  • Chrome/Edge 87+
  • Opera 73+
  • Safari (experimental)
  • Firefox (en desarrollo)

Métodos Principales de CookieStore

1. cookieStore.set() - Establecer Cookies

// Sintaxis simple: nombre y valor
await cookieStore.set('username', 'john_doe');

// Sintaxis completa con opciones
await cookieStore.set({
    name: 'sessionToken',
    value: 'abc123xyz789',
    expires: Date.now() + (24 * 60 * 60 * 1000), // 24 horas
    domain: 'example.com',
    path: '/',
    sameSite: 'strict',
    secure: true,
    partitioned: true // Para cookies de terceros
});

// Con manejo de errores
try {
    await cookieStore.set('preferences', JSON.stringify(userPrefs));
    console.log('Cookie establecida exitosamente');
} catch (error) {
    console.error('Error al establecer cookie:', error);
}
// Obtener cookie por nombre
const userCookie = await cookieStore.get('username');
console.log(userCookie);
// Output: { name: 'username', value: 'john_doe', domain: 'example.com', ... }

// Obtener con opciones específicas
const sessionCookie = await cookieStore.get({
    name: 'sessionToken',
    path: '/admin'
});

// Verificar si existe
if (userCookie) {
    console.log(`Usuario: ${userCookie.value}`);
} else {
    console.log('Usuario no identificado');
}

3. cookieStore.getAll() - Obtener Múltiples Cookies

// Obtener todas las cookies
const allCookies = await cookieStore.getAll();
console.log(`Encontradas ${allCookies.length} cookies`);

// Filtrar por opciones
const sessionCookies = await cookieStore.getAll({
    path: '/session'
});

// Procesar cookies
allCookies.forEach(cookie => {
    console.log(`${cookie.name}: ${cookie.value}`);
});

4. cookieStore.delete() - Eliminar Cookies

// Eliminar por nombre
await cookieStore.delete('username');

// Eliminar con opciones específicas
await cookieStore.delete({
    name: 'sessionToken',
    path: '/admin'
});

// Con confirmación
try {
    await cookieStore.delete('tempData');
    console.log('Cookie eliminada exitosamente');
} catch (error) {
    console.error('Error al eliminar cookie:', error);
}

Eventos de Cambio - La Revolución

Una de las características más potentes de CookieStore API es la capacidad de escuchar cambios en cookies:

// Escuchar cambios en cookies
cookieStore.addEventListener('change', (event) => {
    console.log('Cambio en cookies detectado:', event);
    
    // Cookies que cambiaron
    event.changed.forEach(cookie => {
        console.log(`Cookie modificada: ${cookie.name} = ${cookie.value}`);
    });
    
    // Cookies que fueron eliminadas
    event.deleted.forEach(cookie => {
        console.log(`Cookie eliminada: ${cookie.name}`);
    });
});

// Ejemplo práctico: Sincronizar estado de usuario
cookieStore.addEventListener('change', (event) => {
    const authChange = event.changed.find(c => c.name === 'authToken') || 
                      event.deleted.find(c => c.name === 'authToken');
    
    if (authChange) {
        // Actualizar UI basado en cambio de autenticación
        updateAuthUI();
    }
});

Casos de Uso Prácticos

1. Gestión de Sesiones Avanzada

class SessionManager {
    async createSession(userId, permissions) {
        const sessionData = {
            userId,
            permissions,
            createdAt: Date.now(),
            lastActive: Date.now()
        };
        
        await cookieStore.set({
            name: 'session',
            value: JSON.stringify(sessionData),
            expires: Date.now() + (8 * 60 * 60 * 1000), // 8 horas
            sameSite: 'strict',
            secure: true,
            httpOnly: false // Para acceso desde JS
        });
    }
    
    async getSession() {
        const sessionCookie = await cookieStore.get('session');
        return sessionCookie ? JSON.parse(sessionCookie.value) : null;
    }
    
    async updateLastActive() {
        const session = await this.getSession();
        if (session) {
            session.lastActive = Date.now();
            await cookieStore.set('session', JSON.stringify(session));
        }
    }
    
    async destroySession() {
        await cookieStore.delete('session');
    }
}

2. Sistema de Preferencias de Usuario

class UserPreferences {
    constructor() {
        this.preferences = {};
        this.loadPreferences();
        this.setupChangeListener();
    }
    
    async loadPreferences() {
        const prefCookie = await cookieStore.get('userPrefs');
        this.preferences = prefCookie ? 
            JSON.parse(prefCookie.value) : 
            this.getDefaultPreferences();
    }
    
    async updatePreference(key, value) {
        this.preferences[key] = value;
        
        await cookieStore.set({
            name: 'userPrefs',
            value: JSON.stringify(this.preferences),
            expires: Date.now() + (365 * 24 * 60 * 60 * 1000), // 1 año
            sameSite: 'lax'
        });
        
        this.applyPreference(key, value);
    }
    
    setupChangeListener() {
        cookieStore.addEventListener('change', (event) => {
            const prefsChange = event.changed.find(c => c.name === 'userPrefs');
            if (prefsChange) {
                this.preferences = JSON.parse(prefsChange.value);
                this.applyAllPreferences();
            }
        });
    }
    
    applyPreference(key, value) {
        switch(key) {
            case 'theme':
                document.documentElement.setAttribute('data-theme', value);
                break;
            case 'language':
                this.updateLanguage(value);
                break;
            case 'fontSize':
                document.documentElement.style.fontSize = `${value}px`;
                break;
        }
    }
    
    getDefaultPreferences() {
        return {
            theme: 'auto',
            language: 'es',
            fontSize: 16,
            notifications: true
        };
    }
}

3. Tracking y Analytics Avanzado

class AdvancedAnalytics {
    constructor() {
        this.initializeTracking();
    }
    
    async initializeTracking() {
        // Obtener o crear ID de visitante único
        let visitorId = await cookieStore.get('visitorId');
        if (!visitorId) {
            visitorId = this.generateVisitorId();
            await cookieStore.set({
                name: 'visitorId',
                value: visitorId,
                expires: Date.now() + (2 * 365 * 24 * 60 * 60 * 1000), // 2 años
                sameSite: 'lax'
            });
        }
        
        // Tracking de sesión
        await this.updateSessionTracking();
        
        // Configurar eventos
        this.setupTrackingEvents();
    }
    
    async updateSessionTracking() {
        const sessionData = {
            startTime: Date.now(),
            pageViews: 1,
            events: []
        };
        
        await cookieStore.set({
            name: 'currentSession',
            value: JSON.stringify(sessionData),
            // Cookie de sesión - se elimina al cerrar navegador
            sameSite: 'lax'
        });
    }
    
    async trackEvent(eventName, eventData = {}) {
        const sessionCookie = await cookieStore.get('currentSession');
        if (sessionCookie) {
            const session = JSON.parse(sessionCookie.value);
            session.events.push({
                name: eventName,
                data: eventData,
                timestamp: Date.now()
            });
            
            await cookieStore.set('currentSession', JSON.stringify(session));
        }
    }
    
    setupTrackingEvents() {
        // Escuchar cambios en cookies de tracking
        cookieStore.addEventListener('change', (event) => {
            event.deleted.forEach(cookie => {
                if (cookie.name === 'currentSession') {
                    this.sendSessionData();
                }
            });
        });
    }
    
    generateVisitorId() {
        return 'visitor_' + Math.random().toString(36).substr(2, 9) + Date.now();
    }
}

4. Sistema de Consentimiento GDPR

class ConsentManager {
    constructor() {
        this.consentTypes = ['necessary', 'analytics', 'marketing', 'personalization'];
        this.initializeConsent();
    }
    
    async initializeConsent() {
        const consent = await cookieStore.get('cookieConsent');
        
        if (!consent) {
            this.showConsentBanner();
        } else {
            this.applyConsent(JSON.parse(consent.value));
        }
        
        this.setupConsentChangeListener();
    }
    
    async updateConsent(consentData) {
        await cookieStore.set({
            name: 'cookieConsent',
            value: JSON.stringify({
                ...consentData,
                timestamp: Date.now(),
                version: '1.0'
            }),
            expires: Date.now() + (365 * 24 * 60 * 60 * 1000), // 1 año
            sameSite: 'strict',
            secure: true
        });
        
        this.applyConsent(consentData);
        this.cleanupNonConsentedCookies(consentData);
    }
    
    async cleanupNonConsentedCookies(consent) {
        const allCookies = await cookieStore.getAll();
        
        for (const cookie of allCookies) {
            const cookieType = this.getCookieType(cookie.name);
            
            if (cookieType && !consent[cookieType]) {
                await cookieStore.delete(cookie.name);
            }
        }
    }
    
    setupConsentChangeListener() {
        cookieStore.addEventListener('change', (event) => {
            const consentChange = event.changed.find(c => c.name === 'cookieConsent');
            if (consentChange) {
                const newConsent = JSON.parse(consentChange.value);
                this.applyConsent(newConsent);
            }
        });
    }
    
    getCookieType(cookieName) {
        const cookieTypeMap = {
            'analytics_': 'analytics',
            'marketing_': 'marketing',
            'personalization_': 'personalization',
            // Cookies necesarias siempre permitidas
            'session': null,
            'csrf_token': null
        };
        
        for (const [prefix, type] of Object.entries(cookieTypeMap)) {
            if (cookieName.startsWith(prefix)) {
                return type;
            }
        }
        
        return 'analytics'; // Por defecto
    }
}

Uso en Service Workers

Una de las ventajas más significativas de CookieStore API es su disponibilidad en Service Workers:

// En un Service Worker
self.addEventListener('activate', async (event) => {
    // Limpiar cookies obsoletas al activar SW
    const oldCookies = await cookieStore.getAll({
        name: 'temp_'
    });
    
    for (const cookie of oldCookies) {
        await cookieStore.delete(cookie.name);
    }
});

// Sincronizar datos offline
self.addEventListener('sync', async (event) => {
    if (event.tag === 'sync-user-data') {
        const userCookie = await cookieStore.get('userData');
        if (userCookie) {
            const userData = JSON.parse(userCookie.value);
            await syncUserDataToServer(userData);
        }
    }
});

// Interceptar requests y añadir cookies automáticamente
self.addEventListener('fetch', async (event) => {
    if (event.request.url.includes('/api/')) {
        const authCookie = await cookieStore.get('authToken');
        
        if (authCookie) {
            const modifiedRequest = new Request(event.request, {
                headers: {
                    ...event.request.headers,
                    'Authorization': `Bearer ${authCookie.value}`
                }
            });
            
            event.respondWith(fetch(modifiedRequest));
        }
    }
});

Mejores Prácticas

1. Manejo de Errores Robusto

async function safeCookieOperation(operation) {
    try {
        return await operation();
    } catch (error) {
        if (error.name === 'NotAllowedError') {
            console.warn('Cookies bloqueadas por el usuario');
            return null;
        } else if (error.name === 'TypeError') {
            console.error('Error de tipo en operación de cookie:', error);
            return null;
        } else {
            console.error('Error inesperado:', error);
            throw error;
        }
    }
}

// Uso
const userData = await safeCookieOperation(() => 
    cookieStore.get('userData')
);

2. Validación de Datos

class SecureCookieManager {
    async setSecureCookie(name, value, options = {}) {
        // Validar entrada
        if (!this.isValidCookieName(name)) {
            throw new Error('Nombre de cookie inválido');
        }
        
        if (!this.isValidCookieValue(value)) {
            throw new Error('Valor de cookie inválido');
        }
        
        // Configuración segura por defecto
        const secureOptions = {
            secure: true,
            sameSite: 'strict',
            ...options
        };
        
        await cookieStore.set(name, value, secureOptions);
    }
    
    isValidCookieName(name) {
        return /^[a-zA-Z0-9_-]+$/.test(name) && name.length <= 50;
    }
    
    isValidCookieValue(value) {
        return typeof value === 'string' && value.length <= 4096;
    }
}

3. Performance y Caching

class CachedCookieStore {
    constructor() {
        this.cache = new Map();
        this.setupCacheInvalidation();
    }
    
    async get(name) {
        if (this.cache.has(name)) {
            const cached = this.cache.get(name);
            if (cached.expires > Date.now()) {
                return cached.value;
            }
        }
        
        const cookie = await cookieStore.get(name);
        if (cookie) {
            this.cache.set(name, {
                value: cookie,
                expires: Date.now() + 30000 // Cache 30 segundos
            });
        }
        
        return cookie;
    }
    
    setupCacheInvalidation() {
        cookieStore.addEventListener('change', (event) => {
            // Invalidar cache para cookies modificadas
            event.changed.forEach(cookie => {
                this.cache.delete(cookie.name);
            });
            
            event.deleted.forEach(cookie => {
                this.cache.delete(cookie.name);
            });
        });
    }
}

Migración desde document.cookie

Estrategia de Migración Gradual

class CookieAdapter {
    constructor() {
        this.supportsCookieStore = 'cookieStore' in window;
    }
    
    async get(name) {
        if (this.supportsCookieStore) {
            return await cookieStore.get(name);
        } else {
            // Fallback a document.cookie
            return this.getLegacyCookie(name);
        }
    }
    
    async set(name, value, options = {}) {
        if (this.supportsCookieStore) {
            await cookieStore.set(name, value, options);
        } else {
            this.setLegacyCookie(name, value, options);
        }
    }
    
    async delete(name) {
        if (this.supportsCookieStore) {
            await cookieStore.delete(name);
        } else {
            this.deleteLegacyCookie(name);
        }
    }
    
    // Métodos legacy para compatibilidad
    getLegacyCookie(name) {
        const value = `; ${document.cookie}`;
        const parts = value.split(`; ${name}=`);
        if (parts.length === 2) {
            const cookieValue = parts.pop().split(';').shift();
            return { name, value: cookieValue };
        }
        return null;
    }
    
    setLegacyCookie(name, value, options) {
        let cookieString = `${name}=${value}`;
        
        if (options.expires) {
            cookieString += `; expires=${new Date(options.expires).toUTCString()}`;
        }
        if (options.path) {
            cookieString += `; path=${options.path}`;
        }
        if (options.secure) {
            cookieString += `; secure`;
        }
        if (options.sameSite) {
            cookieString += `; samesite=${options.sameSite}`;
        }
        
        document.cookie = cookieString;
    }
    
    deleteLegacyCookie(name) {
        document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
    }
}

Consideraciones de Seguridad

1. Configuración Segura por Defecto

const SECURE_COOKIE_DEFAULTS = {
    secure: true,        // Solo HTTPS
    sameSite: 'strict',  // Protección CSRF
    httpOnly: false,     // Acceso desde JS (ajustar según necesidad)
    partitioned: true    // Para cookies de terceros
};

async function setSecureCookie(name, value, customOptions = {}) {
    const options = { ...SECURE_COOKIE_DEFAULTS, ...customOptions };
    await cookieStore.set(name, value, options);
}

2. Validación y Sanitización

class SecureDataHandler {
    static sanitizeValue(value) {
        // Remover caracteres peligrosos
        return value.replace(/[<>\"'&]/g, '');
    }
    
    static encryptSensitiveData(data, key) {
        // Implementar encriptación para datos sensibles
        // Usar Web Crypto API para encriptación real
        return btoa(JSON.stringify(data));
    }
    
    static async setEncryptedCookie(name, sensitiveData, key) {
        const encrypted = this.encryptSensitiveData(sensitiveData, key);
        await cookieStore.set(name, encrypted, SECURE_COOKIE_DEFAULTS);
    }
}

El Futuro de CookieStore API

La evolución continúa con nuevas funcionalidades propuestas:

  • Soporte para cookies HTTPOnly desde JavaScript en contextos seguros
  • Integración mejorada con Web Crypto API para encriptación nativa
  • Mejor control granular de políticas de cookies
  • APIs de observación más avanzadas para patrones complejos

Conclusión: ¿Deberías Adoptar CookieStore API?

Ventajas claras:

  • Interfaz moderna y asíncrona
  • Mejor developer experience
  • Funcionalidad en Service Workers
  • Eventos de cambio nativos
  • Menos código boilerplate

Consideraciones:

  • Soporte limitado en navegadores
  • Solo disponible en HTTPS
  • Requiere fallbacks para compatibilidad completa

Recomendación: Comienza a experimentar con CookieStore API en proyectos nuevos que puedan tolerar soporte limitado de navegadores, pero mantén un sistema de fallback robusto hasta que la adopción sea más amplia.

La gestión de cookies está evolucionando, y CookieStore API representa el futuro de esta funcionalidad fundamental. Es momento de comenzar a prepararse para esta transición.


Enlaces útiles:

Tags: #JavaScript #CookieStore #WebAPI #Cookies #ModernWeb #ServiceWorkers #WebDevelopment