ecfr-navigator
Version:
A lightweight, reusable Vue 3 component with Pinia integration for navigating hierarchical eCFR-style content in existing Vue applications.
553 lines (485 loc) • 14.4 kB
JavaScript
import { ref, reactive, computed, watch } from 'vue';
/**
* Advanced storage engine for form data with persistence, versioning, and validation
*/
export function useStorageEngine(options = {}) {
const {
storageKey = 'form_data',
storageType = 'localStorage', // 'localStorage', 'sessionStorage', 'memory'
autoSave = true,
autoSaveDelay = 1000,
enableVersioning = true,
maxVersions = 10,
enableEncryption = false,
compressionEnabled = true
} = options;
// Internal state
const storage = reactive({
data: {},
metadata: {
version: 1,
created: new Date().toISOString(),
lastModified: new Date().toISOString(),
checksum: null,
encrypted: false,
compressed: false
},
versions: [],
isDirty: false,
isLoading: false,
lastSaved: null,
errors: []
});
const watchers = new Map();
const validators = new Map();
let autoSaveTimeout = null;
/**
* Get storage adapter based on type
*/
const getStorageAdapter = () => {
switch (storageType) {
case 'localStorage':
return {
getItem: (key) => localStorage.getItem(key),
setItem: (key, value) => localStorage.setItem(key, value),
removeItem: (key) => localStorage.removeItem(key)
};
case 'sessionStorage':
return {
getItem: (key) => sessionStorage.getItem(key),
setItem: (key, value) => sessionStorage.setItem(key, value),
removeItem: (key) => sessionStorage.removeItem(key)
};
case 'memory':
default:
const memoryStorage = new Map();
return {
getItem: (key) => memoryStorage.get(key) || null,
setItem: (key, value) => memoryStorage.set(key, value),
removeItem: (key) => memoryStorage.delete(key)
};
}
};
const storageAdapter = getStorageAdapter();
/**
* Generate checksum for data integrity
*/
const generateChecksum = (data) => {
const str = JSON.stringify(data);
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32-bit integer
}
return hash.toString(16);
};
/**
* Simple compression for storage efficiency
*/
const compress = (data) => {
if (!compressionEnabled) return data;
// Simple LZ-style compression simulation
const str = JSON.stringify(data);
return btoa(str); // Base64 encoding as compression simulation
};
const decompress = (compressedData) => {
if (!compressionEnabled) return compressedData;
try {
const str = atob(compressedData);
return JSON.parse(str);
} catch {
return compressedData;
}
};
/**
* Simple encryption for sensitive data
*/
const encrypt = (data) => {
if (!enableEncryption) return data;
// Simple XOR encryption simulation
const str = JSON.stringify(data);
const key = 'secret_key_12345';
let encrypted = '';
for (let i = 0; i < str.length; i++) {
encrypted += String.fromCharCode(str.charCodeAt(i) ^ key.charCodeAt(i % key.length));
}
return btoa(encrypted);
};
const decrypt = (encryptedData) => {
if (!enableEncryption) return encryptedData;
try {
const encrypted = atob(encryptedData);
const key = 'secret_key_12345';
let decrypted = '';
for (let i = 0; i < encrypted.length; i++) {
decrypted += String.fromCharCode(encrypted.charCodeAt(i) ^ key.charCodeAt(i % key.length));
}
return JSON.parse(decrypted);
} catch {
return encryptedData;
}
};
/**
* Load data from storage
*/
const load = async () => {
storage.isLoading = true;
storage.errors = [];
try {
const stored = storageAdapter.getItem(storageKey);
if (!stored) {
storage.isLoading = false;
return false;
}
let parsedData = JSON.parse(stored);
// Handle encryption
if (parsedData.metadata?.encrypted) {
parsedData.data = decrypt(parsedData.data);
}
// Handle compression
if (parsedData.metadata?.compressed) {
parsedData.data = decompress(parsedData.data);
}
// Verify checksum
if (parsedData.metadata?.checksum) {
const currentChecksum = generateChecksum(parsedData.data);
if (currentChecksum !== parsedData.metadata.checksum) {
storage.errors.push('Data integrity check failed');
storage.isLoading = false;
return false;
}
}
Object.assign(storage, parsedData);
storage.isDirty = false;
storage.isLoading = false;
return true;
} catch (error) {
storage.errors.push(`Load error: ${error.message}`);
storage.isLoading = false;
return false;
}
};
/**
* Save data to storage
*/
const save = async () => {
try {
// Create version if versioning enabled
if (enableVersioning && storage.data && Object.keys(storage.data).length > 0) {
const version = {
version: storage.metadata.version,
data: JSON.parse(JSON.stringify(storage.data)),
timestamp: new Date().toISOString()
};
storage.versions.unshift(version);
if (storage.versions.length > maxVersions) {
storage.versions = storage.versions.slice(0, maxVersions);
}
}
// Update metadata
storage.metadata.version += 1;
storage.metadata.lastModified = new Date().toISOString();
storage.metadata.checksum = generateChecksum(storage.data);
storage.metadata.encrypted = enableEncryption;
storage.metadata.compressed = compressionEnabled;
// Prepare data for storage
let dataToStore = storage.data;
if (enableEncryption) {
dataToStore = encrypt(dataToStore);
}
if (compressionEnabled) {
dataToStore = compress(dataToStore);
}
const payload = {
data: dataToStore,
metadata: storage.metadata,
versions: storage.versions
};
storageAdapter.setItem(storageKey, JSON.stringify(payload));
storage.isDirty = false;
storage.lastSaved = new Date();
storage.errors = [];
return true;
} catch (error) {
storage.errors.push(`Save error: ${error.message}`);
return false;
}
};
/**
* Auto-save functionality
*/
const triggerAutoSave = () => {
if (!autoSave) return;
if (autoSaveTimeout) {
clearTimeout(autoSaveTimeout);
}
autoSaveTimeout = setTimeout(() => {
save();
}, autoSaveDelay);
};
/**
* Set a property value with dot notation support
*/
const setProperty = (path, value) => {
const keys = path.split('.');
let current = storage.data;
// Create nested structure if it doesn't exist
for (let i = 0; i < keys.length - 1; i++) {
if (!(keys[i] in current) || typeof current[keys[i]] !== 'object') {
current[keys[i]] = {};
}
current = current[keys[i]];
}
const lastKey = keys[keys.length - 1];
const oldValue = current[lastKey];
current[lastKey] = value;
storage.isDirty = true;
triggerAutoSave();
// Trigger property watchers
if (watchers.has(path)) {
watchers.get(path).forEach(callback => {
callback(value, oldValue, path);
});
}
// Run validators
if (validators.has(path)) {
const validator = validators.get(path);
const result = validator(value, storage.data);
if (result !== true) {
storage.errors.push(`Validation error for ${path}: ${result}`);
}
}
return value;
};
/**
* Get a property value with dot notation support
*/
const getProperty = (path, defaultValue = undefined) => {
const keys = path.split('.');
let current = storage.data;
for (const key of keys) {
if (current === null || current === undefined || !(key in current)) {
return defaultValue;
}
current = current[key];
}
return current;
};
/**
* Check if a property exists
*/
const hasProperty = (path) => {
const keys = path.split('.');
let current = storage.data;
for (const key of keys) {
if (current === null || current === undefined || !(key in current)) {
return false;
}
current = current[key];
}
return true;
};
/**
* Delete a property
*/
const deleteProperty = (path) => {
const keys = path.split('.');
let current = storage.data;
for (let i = 0; i < keys.length - 1; i++) {
if (!(keys[i] in current)) {
return false;
}
current = current[keys[i]];
}
const lastKey = keys[keys.length - 1];
if (lastKey in current) {
delete current[lastKey];
storage.isDirty = true;
triggerAutoSave();
return true;
}
return false;
};
/**
* Watch property changes
*/
const watchProperty = (path, callback) => {
if (!watchers.has(path)) {
watchers.set(path, new Set());
}
watchers.get(path).add(callback);
// Return unwatch function
return () => {
const pathWatchers = watchers.get(path);
if (pathWatchers) {
pathWatchers.delete(callback);
if (pathWatchers.size === 0) {
watchers.delete(path);
}
}
};
};
/**
* Add validator for a property
*/
const addValidator = (path, validator) => {
validators.set(path, validator);
};
/**
* Remove validator for a property
*/
const removeValidator = (path) => {
validators.delete(path);
};
/**
* Validate all properties
*/
const validateAll = () => {
storage.errors = [];
const results = {};
for (const [path, validator] of validators) {
const value = getProperty(path);
const result = validator(value, storage.data);
if (result !== true) {
storage.errors.push(`Validation error for ${path}: ${result}`);
results[path] = result;
}
}
return Object.keys(results).length === 0 ? true : results;
};
/**
* Restore from a specific version
*/
const restoreVersion = (versionIndex) => {
if (versionIndex >= 0 && versionIndex < storage.versions.length) {
const version = storage.versions[versionIndex];
storage.data = JSON.parse(JSON.stringify(version.data));
storage.isDirty = true;
triggerAutoSave();
return true;
}
return false;
};
/**
* Clear all data
*/
const clear = () => {
storage.data = {};
storage.metadata = {
version: 1,
created: new Date().toISOString(),
lastModified: new Date().toISOString(),
checksum: null,
encrypted: false,
compressed: false
};
storage.versions = [];
storage.isDirty = false;
storage.errors = [];
storageAdapter.removeItem(storageKey);
};
/**
* Export data in various formats
*/
const exportData = (format = 'json') => {
switch (format) {
case 'json':
return JSON.stringify(storage.data, null, 2);
case 'csv':
// Simple CSV export for flat data
const flatData = flattenObject(storage.data);
const headers = Object.keys(flatData).join(',');
const values = Object.values(flatData).map(v => `"${v}"`).join(',');
return `${headers}\n${values}`;
default:
return storage.data;
}
};
/**
* Import data from various formats
*/
const importData = (data, format = 'json') => {
try {
let parsedData;
switch (format) {
case 'json':
parsedData = typeof data === 'string' ? JSON.parse(data) : data;
break;
default:
parsedData = data;
}
storage.data = parsedData;
storage.isDirty = true;
triggerAutoSave();
return true;
} catch (error) {
storage.errors.push(`Import error: ${error.message}`);
return false;
}
};
/**
* Utility function to flatten nested objects
*/
const flattenObject = (obj, prefix = '') => {
const flattened = {};
for (const key in obj) {
const newKey = prefix ? `${prefix}.${key}` : key;
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
Object.assign(flattened, flattenObject(obj[key], newKey));
} else {
flattened[newKey] = obj[key];
}
}
return flattened;
};
// Computed properties
const isValid = computed(() => storage.errors.length === 0);
const dataSize = computed(() => JSON.stringify(storage.data).length);
const hasUnsavedChanges = computed(() => storage.isDirty);
// Initialize
load();
return {
// State
storage: readonly(storage),
isValid,
dataSize,
hasUnsavedChanges,
// Core operations
load,
save,
clear,
// Property operations
setProperty,
getProperty,
hasProperty,
deleteProperty,
// Watching and validation
watchProperty,
addValidator,
removeValidator,
validateAll,
// Versioning
restoreVersion,
// Import/Export
exportData,
importData,
// Utility
triggerAutoSave
};
}
/**
* Create a readonly proxy to prevent direct mutations
*/
function readonly(obj) {
return new Proxy(obj, {
set() {
console.warn('Direct mutation of storage state is not allowed. Use setProperty() instead.');
return false;
},
deleteProperty() {
console.warn('Direct deletion of storage properties is not allowed. Use deleteProperty() instead.');
return false;
}
});
}