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

Últimas Entradas

7 min

1379 palabras

CookieStore API: The Async Future of Cookie Management in JavaScript

For decades, web developers have depended on the old and limited document.cookie interface to handle cookies in the browser. This synchronous API, with its peculiar string syntax, has been a source of frustration and errors. But that’s changing with the arrival of CookieStore API, a modern and asynchronous interface that promises to revolutionize how we interact with cookies.

The Problem with document.cookie

Before diving into CookieStore, let’s recall the headaches document.cookie has caused us:

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.

6 min

1149 palabras

Idempotency in Laravel: How to Avoid Duplicates in Your APIs with Elegance

In modern API development, one of the most critical challenges is ensuring that operations don’t execute multiple times accidentally. Imagine a user making a payment and, due to connectivity issues, clicking the “Pay” button multiple times. Without proper measures, you might process multiple payments for the same transaction. This is where idempotency comes into play.

What is Idempotency?

Idempotency is a mathematical concept applied to programming that guarantees that an operation produces the same result regardless of how many times it’s executed. In the context of APIs, it means you can make the same request multiple times without causing additional side effects.

6 min

1231 palabras

Are you tired of seeing imports like import Logger from "../../../utils/logger" in your Node.js projects? If you develop applications with complex folder structures, you’ve surely encountered the labyrinth of dots and slashes that relative imports can become. Fortunately, TypeScript offers an elegant solution: Path Aliases.

In this complete guide, you’ll learn to configure path aliases in Node.js projects with TypeScript, forever eliminating those confusing imports and significantly improving the readability and maintainability of your code.