strata-storage
Version:
Zero-dependency universal storage plugin providing a unified API for all storage operations across web, Android, and iOS platforms
261 lines (260 loc) • 7.96 kB
JavaScript
/**
* Cookie Adapter - Browser cookie implementation
* Provides limited storage with 4KB per cookie limit
*/
import { BaseAdapter } from "../../core/BaseAdapter.js";
import { StorageError, QuotaExceededError } from "../../utils/errors.js";
/**
* Browser cookie adapter
*/
export class CookieAdapter extends BaseAdapter {
name = 'cookies';
capabilities = {
persistent: true,
synchronous: false,
observable: false, // No native change events
transactional: false,
queryable: true,
maxSize: 4096, // 4KB per cookie
binary: false,
encrypted: false,
crossTab: true, // Cookies are shared across tabs
};
prefix;
cookieOptions;
maxCookieSize = 4096; // 4KB limit per cookie
constructor(prefix = 'strata_', options = {}) {
super();
this.prefix = prefix;
this.cookieOptions = {
path: '/',
...options,
};
}
/**
* Check if cookies are available
*/
async isAvailable() {
if (typeof window === 'undefined' || !navigator.cookieEnabled) {
return false;
}
// Test if we can actually set cookies
try {
const testKey = `${this.prefix}test`;
this.setCookie(testKey, 'test', { maxAge: 1 });
const result = this.getCookie(testKey) === 'test';
this.deleteCookie(testKey);
return result;
}
catch {
return false;
}
}
/**
* Initialize the adapter
*/
async initialize(config) {
if (config) {
this.cookieOptions = { ...this.cookieOptions, ...config };
}
this.startTTLCleanup();
}
/**
* Get a value from cookies
*/
async get(key) {
const cookieKey = this.prefix + key;
const value = this.getCookie(cookieKey);
if (!value)
return null;
try {
const decoded = decodeURIComponent(value);
const parsed = JSON.parse(decoded);
// Check TTL
if (this.isExpired(parsed)) {
await this.remove(key);
return null;
}
return parsed;
}
catch (error) {
console.error(`Failed to parse cookie ${key}:`, error);
return null;
}
}
/**
* Set a value in cookies
*/
async set(key, value) {
const cookieKey = this.prefix + key;
const oldValue = await this.get(key);
try {
const serialized = JSON.stringify(value);
const encoded = encodeURIComponent(serialized);
// Check size limit
if (encoded.length > this.maxCookieSize) {
throw new QuotaExceededError(`Cookie size ${encoded.length} exceeds limit ${this.maxCookieSize}`, { key, size: encoded.length });
}
// Set cookie with options
const options = { ...this.cookieOptions };
// Handle TTL
if (value.expires) {
const maxAge = Math.floor((value.expires - Date.now()) / 1000);
if (maxAge > 0) {
options.maxAge = maxAge;
}
}
this.setCookie(cookieKey, encoded, options);
this.emitChange(key, oldValue?.value, value.value, 'local');
}
catch (error) {
if (error instanceof QuotaExceededError) {
throw error;
}
throw new StorageError(`Failed to set cookie ${key}: ${error}`);
}
}
/**
* Remove a value from cookies
*/
async remove(key) {
const oldValue = await this.get(key);
const cookieKey = this.prefix + key;
this.deleteCookie(cookieKey);
if (oldValue) {
this.emitChange(key, oldValue.value, undefined, 'local');
}
}
/**
* Clear cookies
*/
async clear(options) {
if (!options || (!options.pattern && !options.tags && !options.expiredOnly)) {
// Clear all cookies with our prefix
const cookies = this.getAllCookies();
for (const [cookieKey] of cookies) {
if (cookieKey.startsWith(this.prefix)) {
this.deleteCookie(cookieKey);
}
}
this.emitChange('*', undefined, undefined, 'local');
return;
}
// Use base implementation for filtered clear
await super.clear(options);
}
/**
* Get all keys
*/
async keys(pattern) {
const cookies = this.getAllCookies();
const keys = [];
for (const [cookieKey] of cookies) {
if (cookieKey.startsWith(this.prefix)) {
const key = cookieKey.substring(this.prefix.length);
// Check if not expired
const value = await this.get(key);
if (value) {
keys.push(key);
}
}
}
return this.filterKeys(keys, pattern);
}
/**
* Get storage size
*/
async size(detailed) {
const cookies = this.getAllCookies();
let total = 0;
let count = 0;
let keySize = 0;
let valueSize = 0;
for (const [cookieKey, cookieValue] of cookies) {
if (cookieKey.startsWith(this.prefix)) {
count++;
const itemSize = (cookieKey.length + cookieValue.length) * 2; // UTF-16
total += itemSize;
if (detailed) {
keySize += cookieKey.length * 2;
valueSize += cookieValue.length * 2;
}
}
}
const result = { total, count };
if (detailed) {
result.detailed = {
keys: keySize,
values: valueSize,
metadata: 0,
};
}
return result;
}
// Cookie manipulation helpers
/**
* Get a cookie value
*/
getCookie(name) {
if (typeof document === 'undefined')
return null;
const nameEQ = name + '=';
const cookies = document.cookie.split(';');
for (const cookie of cookies) {
const trimmed = cookie.trim();
if (trimmed.indexOf(nameEQ) === 0) {
return trimmed.substring(nameEQ.length);
}
}
return null;
}
/**
* Set a cookie
*/
setCookie(name, value, options = {}) {
if (typeof document === 'undefined')
return;
let cookie = `${name}=${value}`;
if (options.maxAge !== undefined) {
cookie += `; max-age=${options.maxAge}`;
}
if (options.domain) {
cookie += `; domain=${options.domain}`;
}
if (options.path) {
cookie += `; path=${options.path}`;
}
if (options.secure) {
cookie += '; secure';
}
if (options.sameSite) {
cookie += `; samesite=${options.sameSite}`;
}
document.cookie = cookie;
}
/**
* Delete a cookie
*/
deleteCookie(name) {
this.setCookie(name, '', { ...this.cookieOptions, maxAge: 0 });
}
/**
* Get all cookies as key-value pairs
*/
getAllCookies() {
if (typeof document === 'undefined')
return [];
const cookies = [];
const cookieStrings = document.cookie.split(';');
for (const cookie of cookieStrings) {
const trimmed = cookie.trim();
const eqIndex = trimmed.indexOf('=');
if (eqIndex > 0) {
const name = trimmed.substring(0, eqIndex);
const value = trimmed.substring(eqIndex + 1);
cookies.push([name, value]);
}
}
return cookies;
}
}