sync efactura-generator -> 0.9-beta-14
Mirror sincronizat cu repo canonic /workspace/efactura-generator. CLAUDE.md: documentat workflow sync + exclude config.json din rsync deploy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
185
efactura-generator/js/storage.js
Normal file
185
efactura-generator/js/storage.js
Normal file
@@ -0,0 +1,185 @@
|
||||
// js/storage.js
|
||||
//
|
||||
// Helpers de stocare pentru efactura-generator (PR-PROFIL / A12+A13).
|
||||
//
|
||||
// Reguli:
|
||||
// 1. Toate cheile localStorage/sessionStorage încep cu "efactura." —
|
||||
// enforced la setter; getJSON acceptă orice cheie pentru compatibilitate
|
||||
// retroactivă, dar setJSON/cacheSet aruncă dacă prefixul lipsește.
|
||||
// 2. Quota errors localStorage → toast vizibil "spațiu local plin".
|
||||
// 3. Cheile convenționale: efactura.{tip}.v1
|
||||
// Ex: efactura.profil.v1, efactura.catalog.v1, efactura.session.v1
|
||||
//
|
||||
// Exports:
|
||||
// getJSON(key, default) → valoare parsată sau default
|
||||
// setJSON(key, value) → salvează; toast error dacă QuotaExceeded
|
||||
// cacheGet(key) → sessionStorage (ephemer, null dacă absent)
|
||||
// cacheSet(key, value) → sessionStorage (silențios dacă eșuează)
|
||||
// openCatalog() → Promise<IDBDatabase> pentru catalog produse (A13)
|
||||
|
||||
const KEY_PREFIX = 'efactura.';
|
||||
|
||||
/**
|
||||
* Validează că cheia respectă prefixul obligatoriu.
|
||||
* @param {string} key
|
||||
*/
|
||||
function _enforcePrefix(key) {
|
||||
if (typeof key !== 'string' || !key.startsWith(KEY_PREFIX)) {
|
||||
throw new Error(
|
||||
`storage.js: cheia "${key}" trebuie să înceapă cu "${KEY_PREFIX}". ` +
|
||||
`Convenție: efactura.{tip}.v1`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Afișează un toast (dacă window.showToast e disponibil) sau loghează.
|
||||
* @param {string} msg
|
||||
* @param {string} variant 'error'|'warning'|'info'|'success'
|
||||
*/
|
||||
function _toast(msg, variant = 'error') {
|
||||
if (typeof window !== 'undefined' && typeof window.showToast === 'function') {
|
||||
window.showToast(msg, variant);
|
||||
} else {
|
||||
console.warn('[storage]', msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Citește o valoare JSON din localStorage.
|
||||
* Returnează `defaultValue` dacă cheia lipsește sau JSON e invalid.
|
||||
*
|
||||
* @param {string} key
|
||||
* @param {*} defaultValue
|
||||
* @returns {*}
|
||||
*/
|
||||
export function getJSON(key, defaultValue = null) {
|
||||
_enforcePrefix(key);
|
||||
try {
|
||||
const raw = localStorage.getItem(key);
|
||||
if (raw === null) return defaultValue;
|
||||
return JSON.parse(raw);
|
||||
} catch (_) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrie o valoare JSON în localStorage.
|
||||
* La QuotaExceededError → toast "spațiu local plin".
|
||||
*
|
||||
* @param {string} key
|
||||
* @param {*} value
|
||||
*/
|
||||
export function setJSON(key, value) {
|
||||
_enforcePrefix(key);
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
} catch (err) {
|
||||
// QuotaExceededError: code 22 (Firefox/Chrome), 1014 (Firefox NS), sau name check.
|
||||
const isQuota = err && (
|
||||
err.name === 'QuotaExceededError' ||
|
||||
err.name === 'NS_ERROR_DOM_QUOTA_REACHED' ||
|
||||
err.code === 22 ||
|
||||
err.code === 1014
|
||||
);
|
||||
if (isQuota) {
|
||||
_toast(
|
||||
'Spațiu local plin — datele nu au putut fi salvate.',
|
||||
'error'
|
||||
);
|
||||
} else {
|
||||
_toast(`Eroare la salvare locală: ${err && err.message ? err.message : err}`, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Citește din sessionStorage (cache ephemer, valabil doar pe durata sesiunii).
|
||||
* Returnează null dacă absent sau invalid.
|
||||
*
|
||||
* @param {string} key
|
||||
* @returns {*|null}
|
||||
*/
|
||||
export function cacheGet(key) {
|
||||
_enforcePrefix(key);
|
||||
try {
|
||||
const raw = sessionStorage.getItem(key);
|
||||
if (raw === null) return null;
|
||||
return JSON.parse(raw);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrie în sessionStorage. Erorile sunt ignorate silențios (storage e
|
||||
* ephemer și poate fi blocat de browser în incognito / iframe sandboxed).
|
||||
*
|
||||
* @param {string} key
|
||||
* @param {*} value
|
||||
*/
|
||||
export function cacheSet(key, value) {
|
||||
_enforcePrefix(key);
|
||||
try {
|
||||
sessionStorage.setItem(key, JSON.stringify(value));
|
||||
} catch (_) {
|
||||
// Ignorat: sessionStorage e ephemer, erorile nu sunt critice.
|
||||
}
|
||||
}
|
||||
|
||||
// IndexedDB pentru catalog produse (A13 lazy init).
|
||||
let _catalogDb = null;
|
||||
|
||||
/**
|
||||
* Deschide (sau returnează instanța cached a) bazei de date IndexedDB
|
||||
* `efactura` v1. Crează object store `products` la prima rulare.
|
||||
*
|
||||
* Schema v1 (lock per eng review 14A):
|
||||
* - DB name: `efactura`
|
||||
* - store: `products`, keyPath: `id` (uuid v4 generat de caller)
|
||||
* - indexes: `name`, `sellerItemID`, `cpvCode`
|
||||
*
|
||||
* Dacă IndexedDB lipsește (private browsing), Promise rejectează cu Error
|
||||
* `indexeddb-unavailable` — caller-ul trebuie să degradeze la "feature
|
||||
* disabled" cu toast (NU să crash-eze).
|
||||
*
|
||||
* @returns {Promise<IDBDatabase>}
|
||||
*/
|
||||
export function openCatalog() {
|
||||
if (_catalogDb) return Promise.resolve(_catalogDb);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof indexedDB === 'undefined') {
|
||||
reject(new Error('indexeddb-unavailable'));
|
||||
return;
|
||||
}
|
||||
const req = indexedDB.open('efactura', 1);
|
||||
|
||||
req.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
if (!db.objectStoreNames.contains('products')) {
|
||||
const store = db.createObjectStore('products', { keyPath: 'id' });
|
||||
store.createIndex('name', 'name', { unique: false });
|
||||
store.createIndex('sellerItemID', 'sellerItemID', { unique: false });
|
||||
store.createIndex('cpvCode', 'cpvCode', { unique: false });
|
||||
}
|
||||
};
|
||||
|
||||
req.onsuccess = (event) => {
|
||||
_catalogDb = event.target.result;
|
||||
resolve(_catalogDb);
|
||||
};
|
||||
|
||||
req.onerror = (event) => {
|
||||
reject(event.target.error);
|
||||
};
|
||||
|
||||
req.onblocked = () => {
|
||||
reject(new Error('indexeddb-blocked'));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Export prefix pentru tests / consumeri care vor să verifice convenția.
|
||||
export const STORAGE_PREFIX = KEY_PREFIX;
|
||||
Reference in New Issue
Block a user