strata-storage
Version:
Zero-dependency universal storage plugin providing a unified API for all storage operations across web, Android, and iOS platforms
206 lines (205 loc) • 6.95 kB
JavaScript
/**
* Compression Feature - Pure JavaScript LZ-string implementation
* Zero-dependency compression/decompression for storage values
*/
import { CompressionError } from "../utils/errors.js";
/**
* Compression manager using pure JavaScript LZ-string algorithm
*/
export class CompressionManager {
config;
constructor(config = {}) {
this.config = {
algorithm: config.algorithm || 'lz',
threshold: config.threshold || 1024, // 1KB default threshold
level: config.level || 6,
};
}
/**
* Compress data using LZ-string algorithm
*/
async compress(data) {
try {
const jsonStr = JSON.stringify(data);
const originalSize = new Blob([jsonStr]).size;
// Skip compression if below threshold
if (originalSize < this.config.threshold) {
return data;
}
// Compress using LZ algorithm
const compressed = this.lzCompress(jsonStr);
const compressedSize = new Blob([compressed]).size;
// Only use compression if it reduces size
if (compressedSize >= originalSize) {
return data;
}
return {
data: compressed,
algorithm: this.config.algorithm,
originalSize,
compressedSize,
};
}
catch (error) {
throw new CompressionError(`Compression failed: ${error}`);
}
}
/**
* Decompress data
*/
async decompress(compressedData) {
try {
if (!this.isCompressedData(compressedData)) {
return compressedData;
}
const decompressed = this.lzDecompress(compressedData.data);
return JSON.parse(decompressed);
}
catch (error) {
throw new CompressionError(`Decompression failed: ${error}`);
}
}
/**
* Check if data is compressed
*/
isCompressedData(data) {
return (typeof data === 'object' &&
data !== null &&
'data' in data &&
'algorithm' in data &&
'originalSize' in data &&
'compressedSize' in data);
}
/**
* LZ-string compression implementation
*/
lzCompress(uncompressed) {
if (uncompressed == null)
return '';
const context = new Map();
const out = [];
let currentChar;
let phrase = uncompressed.charAt(0);
let code = 256;
for (let i = 1; i < uncompressed.length; i++) {
currentChar = uncompressed.charAt(i);
if (context.has(phrase + currentChar)) {
phrase += currentChar;
}
else {
if (phrase.length > 1) {
out.push(String.fromCharCode(context.get(phrase)));
}
else {
out.push(phrase);
}
context.set(phrase + currentChar, code);
code++;
phrase = currentChar;
}
}
if (phrase.length > 1) {
out.push(String.fromCharCode(context.get(phrase)));
}
else {
out.push(phrase);
}
return this.encodeToBase64(out.join(''));
}
/**
* LZ-string decompression implementation
*/
lzDecompress(compressed) {
if (compressed == null)
return '';
if (compressed === '')
return '';
const decoded = this.decodeFromBase64(compressed);
const dictionary = new Map();
let currentChar = decoded.charAt(0);
let oldPhrase = currentChar;
const out = [currentChar];
let code = 256;
let phrase;
for (let i = 1; i < decoded.length; i++) {
const currentCode = decoded.charCodeAt(i);
if (currentCode < 256) {
phrase = decoded.charAt(i);
}
else {
phrase = dictionary.get(currentCode) || oldPhrase + currentChar;
}
out.push(phrase);
currentChar = phrase.charAt(0);
dictionary.set(code, oldPhrase + currentChar);
code++;
oldPhrase = phrase;
}
return out.join('');
}
/**
* Base64 encoding for compressed data
*/
encodeToBase64(input) {
if (typeof btoa !== 'undefined') {
// Browser environment
return btoa(unescape(encodeURIComponent(input)));
}
// Pure JS implementation for Node.js or other environments
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
let result = '';
let i = 0;
while (i < input.length) {
const a = input.charCodeAt(i++);
const b = i < input.length ? input.charCodeAt(i++) : 0;
const c = i < input.length ? input.charCodeAt(i++) : 0;
const bitmap = (a << 16) | (b << 8) | c;
result += chars.charAt((bitmap >> 18) & 63);
result += chars.charAt((bitmap >> 12) & 63);
result += i - 2 < input.length ? chars.charAt((bitmap >> 6) & 63) : '=';
result += i - 1 < input.length ? chars.charAt(bitmap & 63) : '=';
}
return result;
}
/**
* Base64 decoding for compressed data
*/
decodeFromBase64(input) {
if (typeof atob !== 'undefined') {
// Browser environment
return decodeURIComponent(escape(atob(input)));
}
// Pure JS implementation
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
let result = '';
let i = 0;
input = input.replace(/[^A-Za-z0-9+/]/g, '');
while (i < input.length) {
const encoded1 = chars.indexOf(input.charAt(i++));
const encoded2 = chars.indexOf(input.charAt(i++));
const encoded3 = chars.indexOf(input.charAt(i++));
const encoded4 = chars.indexOf(input.charAt(i++));
const bitmap = (encoded1 << 18) | (encoded2 << 12) | (encoded3 << 6) | encoded4;
result += String.fromCharCode((bitmap >> 16) & 255);
if (encoded3 !== 64)
result += String.fromCharCode((bitmap >> 8) & 255);
if (encoded4 !== 64)
result += String.fromCharCode(bitmap & 255);
}
return result;
}
/**
* Get compression ratio
*/
getCompressionRatio(compressedData) {
return compressedData.compressedSize / compressedData.originalSize;
}
/**
* Get savings percentage
*/
getSavingsPercentage(compressedData) {
return (((compressedData.originalSize - compressedData.compressedSize) /
compressedData.originalSize) *
100);
}
}