strata-storage
Version:
Zero-dependency universal storage plugin providing a unified API for all storage operations across web, Android, and iOS platforms
366 lines (365 loc) • 10.2 kB
JavaScript
/**
* Utility functions for Strata Storage
* Zero dependencies - all utilities implemented from scratch
*/
import { ValidationError } from "./errors.js";
/**
* Check if code is running in a browser environment
*/
export function isBrowser() {
return typeof window !== 'undefined' && typeof window.document !== 'undefined';
}
/**
* Check if code is running in Node.js
*/
export function isNode() {
return (typeof process !== 'undefined' &&
process.versions !== undefined &&
process.versions.node !== undefined);
}
/**
* Check if code is running in a Web Worker
*/
export function isWebWorker() {
return (typeof self !== 'undefined' &&
typeof self.importScripts === 'function');
}
/**
* Check if code is running in Capacitor
*/
export function isCapacitor() {
return typeof window?.Capacitor !== 'undefined';
}
/**
* Deep clone an object (no dependencies!)
*/
export function deepClone(obj) {
if (obj === null || typeof obj !== 'object')
return obj;
if (obj instanceof Date)
return new Date(obj.getTime());
if (obj instanceof Array)
return obj.map((item) => deepClone(item));
if (obj instanceof RegExp)
return new RegExp(obj.source, obj.flags);
const clonedObj = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
clonedObj[key] = deepClone(obj[key]);
}
}
return clonedObj;
}
/**
* Deep merge objects
*/
export function deepMerge(target, ...sources) {
if (!sources.length)
return target;
const source = sources.shift();
if (isObject(target) && isObject(source)) {
for (const key in source) {
if (isObject(source[key])) {
if (!target[key])
Object.assign(target, { [key]: {} });
deepMerge(target[key], source[key]);
}
else {
Object.assign(target, { [key]: source[key] });
}
}
}
return deepMerge(target, ...sources);
}
/**
* Check if value is a plain object
*/
export function isObject(item) {
return item !== null && typeof item === 'object' && item.constructor === Object;
}
/**
* Generate a unique ID
*/
export function generateId() {
const timestamp = Date.now().toString(36);
const randomPart = Math.random().toString(36).substring(2, 9);
return `${timestamp}-${randomPart}`;
}
/**
* Simple glob pattern matching
*/
export function matchGlob(pattern, str) {
const regexPattern = pattern
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
.replace(/\*/g, '.*')
.replace(/\?/g, '.');
return new RegExp(`^${regexPattern}$`).test(str);
}
/**
* Format bytes to human readable
*/
export function formatBytes(bytes, decimals = 2) {
if (bytes === 0)
return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
}
/**
* Parse size string to bytes
*/
export function parseSize(size) {
if (typeof size === 'number')
return size;
const units = {
b: 1,
byte: 1,
bytes: 1,
kb: 1024,
mb: 1024 * 1024,
gb: 1024 * 1024 * 1024,
tb: 1024 * 1024 * 1024 * 1024,
};
const match = size.toLowerCase().match(/^(\d+(?:\.\d+)?)\s*([a-z]+)?$/);
if (!match) {
throw new ValidationError('Invalid size format', {
provided: size,
expected: 'Number with unit (e.g., 5MB, 1GB)',
});
}
const [, num, unit = 'b'] = match;
const multiplier = units[unit] || 1;
return parseFloat(num) * multiplier;
}
/**
* Debounce function
*/
export function debounce(func, wait) {
let timeout = null;
return function executedFunction(...args) {
const later = () => {
timeout = null;
func(...args);
};
if (timeout)
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
/**
* Throttle function
*/
export function throttle(func, limit) {
let inThrottle = false;
return function executedFunction(...args) {
if (!inThrottle) {
func(...args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
/**
* Promise with timeout
*/
export function withTimeout(promise, timeoutMs, errorMessage = 'Operation timed out') {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(errorMessage));
}, timeoutMs);
promise
.then(resolve)
.catch(reject)
.finally(() => clearTimeout(timer));
});
}
/**
* Retry with exponential backoff
*/
export async function retry(fn, options = {}) {
const { maxRetries = 3, initialDelay = 100, maxDelay = 10000, factor = 2 } = options;
let lastError;
let delay = initialDelay;
for (let i = 0; i <= maxRetries; i++) {
try {
return await fn();
}
catch (error) {
lastError = error;
if (i === maxRetries)
break;
await sleep(delay);
delay = Math.min(delay * factor, maxDelay);
}
}
throw lastError;
}
/**
* Sleep for specified milliseconds
*/
export function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Create a deferred promise
*/
export function createDeferred() {
let resolve;
let reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
}
/**
* Serialize value to JSON with support for special types
*/
export function serialize(value) {
return JSON.stringify(value, (_, val) => {
if (val instanceof Date)
return { __type: 'Date', value: val.toISOString() };
if (val instanceof RegExp)
return { __type: 'RegExp', value: val.toString() };
if (val instanceof Map)
return { __type: 'Map', value: Array.from(val.entries()) };
if (val instanceof Set)
return { __type: 'Set', value: Array.from(val) };
if (typeof val === 'bigint')
return { __type: 'BigInt', value: val.toString() };
return val;
});
}
/**
* Deserialize JSON with support for special types
*/
export function deserialize(json) {
return JSON.parse(json, (_, val) => {
if (val && typeof val === 'object' && '__type' in val) {
switch (val.__type) {
case 'Date':
return new Date(val.value);
case 'RegExp': {
const match = val.value.match(/^\/(.*)\/([gimuy]*)$/);
return match ? new RegExp(match[1], match[2]) : new RegExp(val.value);
}
case 'Map':
return new Map(val.value);
case 'Set':
return new Set(val.value);
case 'BigInt':
return BigInt(val.value);
}
}
return val;
});
}
/**
* Calculate object size in bytes (rough estimate)
*/
export function getObjectSize(obj) {
const seen = new WeakSet();
function calculateSize(item) {
if (item === null || item === undefined)
return 0;
const type = typeof item;
switch (type) {
case 'boolean':
return 4;
case 'number':
return 8;
case 'string':
return item.length * 2; // UTF-16
case 'bigint':
return item.toString().length;
case 'object':
if (seen.has(item))
return 0;
seen.add(item);
if (item instanceof Date)
return 8;
if (item instanceof RegExp)
return item.toString().length * 2;
if (Array.isArray(item)) {
return item.reduce((sum, val) => sum + calculateSize(val), 0);
}
return Object.entries(item).reduce((sum, [key, val]) => sum + key.length * 2 + calculateSize(val), 0);
default:
return 0;
}
}
return calculateSize(obj);
}
/**
* Create a simple event emitter
*/
export class EventEmitter {
events = new Map();
on(event, handler) {
if (!this.events.has(event)) {
this.events.set(event, new Set());
}
this.events.get(event).add(handler);
}
off(event, handler) {
this.events.get(event)?.delete(handler);
}
emit(event, ...args) {
this.events.get(event)?.forEach((handler) => {
try {
handler(...args);
}
catch (error) {
console.error(`Error in event handler for ${event}:`, error);
}
});
}
once(event, handler) {
const onceHandler = (...args) => {
handler(...args);
this.off(event, onceHandler);
};
this.on(event, onceHandler);
}
removeAllListeners(event) {
if (event) {
this.events.delete(event);
}
else {
this.events.clear();
}
}
}
/**
* Check if a value is a valid storage key
*/
export function isValidKey(key) {
return typeof key === 'string' && key.length > 0;
}
/**
* Check if a value can be stored
*/
export function isValidValue(value) {
// Allow all values except undefined
return value !== undefined;
}
/**
* Serialize a value for storage
*/
export function serializeValue(value) {
return serialize(value);
}
/**
* Deserialize a value from storage
*/
export function deserializeValue(value) {
return deserialize(value);
}
export function createError(message, code, details) {
const error = new Error(message);
error.code = code;
error.details = details;
return error;
}