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

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

1964 palabras

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

Comentarios

Artículos relacionados

6 min

1247 palabras

Idempotencia en Laravel: Cómo Evitar Duplicados en tus APIs con Elegancia

En el desarrollo de APIs modernas, uno de los desafíos más críticos es garantizar que las operaciones no se ejecuten múltiples veces de forma accidental. Imagina un usuario que realiza un pago y, por problemas de conectividad, hace clic varias veces en el botón “Pagar”. Sin las medidas adecuadas, podrías procesar múltiples pagos por la misma transacción. Aquí es donde entra en juego la idempotencia.

7 min

1279 palabras

¿Estás cansado de ver imports como import Logger from "../../../utils/logger" en tus proyectos de Node.js? Si desarrollas aplicaciones con estructuras de carpetas complejas, seguramente te has encontrado con el laberinto de puntos y barras que pueden llegar a ser los imports relativos. Afortunadamente, TypeScript ofrece una solución elegante: los Path Aliases.

En esta guía completa aprenderás a configurar path aliases en proyectos Node.js con TypeScript, eliminando para siempre esos imports confusos y mejorando significativamente la legibilidad y mantenibilidad de tu código.

8 min

1650 palabras

¿Te has encontrado alguna vez duplicando utilidades, tipos de datos o componentes entre diferentes aplicaciones? Si trabajas en múltiples proyectos que necesitan compartir código común, seguramente has enfrentado el dilema de cómo gestionar este código compartido de manera eficiente.

Recientemente, mientras exploraba diferentes enfoques para compartir código entre aplicaciones, me topé con una solución elegante y simple que muchos desarrolladores pasan por alto: los symlinks de Yarn. Esta técnica puede ser la respuesta perfecta si buscas una alternativa liviana a las configuraciones complejas de monorepos.

14 min

2873 palabras

La Filament v4 Beta ha llegado oficialmente, y es sin duda la actualización más ambiciosa y completa en la historia de este framework. Después de explorar en detalle todas las nuevas características, puedo afirmar que esta versión representa un salto cuántico en términos de rendimiento, facilidad de uso y capacidades de desarrollo.

En este análisis exhaustivo, vamos a explorar cada una de las nuevas características de Filament v4, explicando no solo qué es nuevo, sino también cómo estas mejoras pueden transformar tu flujo de trabajo y las posibilidades de tus aplicaciones.

4 min

804 palabras

El equipo de Filament ha anunciado emocionantes detalles sobre el próximo lanzamiento de Filament v4 Beta, y sin duda es la versión más esperada hasta la fecha. Filament v4 es el lanzamiento más grande y repleto de características que Filament haya tenido nunca, superando incluso a la masiva v3 que requirió más de 100 versiones menores.

Las características más destacadas de Filament v4

Recursos Anidados (Nested Resources)

Una de las solicitudes más longevas de la comunidad finalmente se hace realidad. Los recursos anidados permiten operar sobre un recurso de Filament dentro del contexto de un recurso padre.

5 min

1044 palabras

Durante mucho tiempo, TypeScript ha carecido de una biblioteca estándar robusta. Mientras que otros lenguajes como Rust, Go o Python ofrecen herramientas estándar para el manejo de errores, concurrencia y efectos secundarios, los desarrolladores de TypeScript hemos tenido que recurrir a múltiples bibliotecas especializadas. Effect TS está cambiando esto al ofrecer una solución unificada y potente para el desarrollo de aplicaciones TypeScript modernas.

¿Qué es Effect TS?

Effect es una poderosa biblioteca de TypeScript diseñada para ayudar a los desarrolladores a crear fácilmente programas complejos, síncronos y asíncronos. Inspirada en ZIO de Scala, Effect trae los principios de la programación funcional a TypeScript de una manera práctica y accesible.