BeeSave — Documentazione Tecnica
Persistenza DTO su storage del browser: envelope, status, namespace e migrazione
📁 Panoramica
BeeSave non è più un wrapper nudo su localStorage.setItem. Ogni record è un envelope { __bee, v, t, d }: così chiave assente, JSON corrotto e primo avvio non sono la stessa cosa. La facade statica BeeSave delega a uno store di default; se ti servono due giochi (o i test) isolati, BeeSave.create(options) restituisce un BeeSaveStore indipendente.
La responsabilità resta serializzare un DTO JSON-piatto. Quello che ancora non fa: cifratura, sync tra tab, IndexedDB, compressione, stima della quota residua, debounce. Il motore non la chiama: sei tu a decidere quando salvare. Il barrel la re-esporta; il game loop no.
📁 Envelope: non è più il JSON nudo
In storage non trovi più "1200" o {"level":3}. Trovi un oggetto marchiato:
__bee è il marchio (costante interna 1). v è la versione dello schema dello store, non un campo tuo. t è Date.now() al momento del save. d è il DTO. Se il JSON in disco non ha __bee === 1 e una proprietà d, viene trattato come legacy: il valore parsato intero è il payload, version vale 0, legacy: true. I save della classe vecchia restano leggibili.
La chiave reale è prefix + (namespace ? namespace + ':' : '') + key. Default: BeeSave:highScore. Con namespace honeyrun: BeeSave:honeyrun:highScore. Non c’è sanitizzazione: se passi già il prefisso, lo duplichi.
📁 Result e status: save/read non lanciano più (di solito)
Ogni scrittura e ogni read torna un oggetto result, anche in errore. ok è true solo se status === 'ok'. Gli ultimi esiti stanno su lastWrite / lastRead dello store (anche sulla facade: BeeSave.store.lastRead).
| Campo | Tipo | Significato |
|---|---|---|
ok |
boolean |
true solo per BEE_SAVE_STATUS.OK. |
status |
string |
Uno dei valori congelati in BEE_SAVE_STATUS. |
key / storageKey |
string |
Chiave logica vs chiave reale in storage. |
value |
any |
DTO se ok; undefined negli altri status. |
version / savedAt |
number |
Schema envelope e timestamp. Zero se assente/corrotto. |
legacy |
boolean |
true se il JSON non era un envelope BeeSave. |
message |
string |
Motivo umano (chiave vuota, path DTO, errore nativo). |
Status possibili: ok, missing, corrupt, unavailable, rejected, quota. Non sono numeri: confronta con BEE_SAVE_STATUS.CORRUPT, non con stringhe sparse se puoi evitarlo.
📁 save: validazione DTO, try/catch, quota
save non è più “stringify e spera”. Ordine reale:
1. Chiave vuota → rejected, niente I/O.
2. assertDto cammina l’albero. Rifiuta undefined, funzioni, symbol, bigint, cicli, istanze di classe (prototipo diverso da Object/Array). Date è ammessa in validazione, ma JSON.stringify la serializza come stringa ISO: al load torna una stringa, non un Date. Messaggio tipo $.player is a live BeePlayer — serialize a plain DTO.
3. Backend assente → unavailable (solo se hai spento il fallback memoria).
4. JSON.stringify in try/catch → fallimento → rejected.
5. setItem in try/catch → QuotaExceededError / NS_ERROR_DOM_QUOTA_REACHED / code 22 o 1014 → status quota; qualsiasi altro throw → unavailable. L’eccezione non sale al chiamante.
📁 read vs load: missing e corrupt non sono più identici
read(key) è l’API onesta. load(key, defaultValue) è lo zucchero: se lo status è ok restituisce value (anche se hai salvato null); in tutti gli altri status restituisce defaultValue. Quindi load maschera ancora corruzione, quota di lettura, storage morto e chiave vuota. Se ti serve distinguere il primo avvio da un save marcio, usa read.
getItem === null → missing. Parse che lancia → status corrupt. Con onCorrupt: 'remove' prova a cancellare (se anche removeItem fallisce lo status resta comunque corrupt). Default 'keep': il byte marcio resta, has è true, exists è false.
📁 exists, has, null, undefined
Questo era il groviglio della classe vecchia. Ora i due booleani non coincidono.
exists(key) è read(key).status === 'ok': record valido e leggibile. Un JSON rotto non conta. Un null salvato di proposito sì: stringify di null è legale, exists è true, load restituisce null (il valore), non il default.
has(key) è presenza fisica: getItem !== null, anche se il payload è spazzatura. Non parse-a.
save(key, undefined) viene rifiutato da assertDto (rejected, messaggio $ is undefined — JSON cannot store it; use null). Non finisce più una stringa "undefined" in disco.
Breaking rispetto alla v vecchia: exists non è più getItem !== null. Codice che usava exists per dire “c’è qualcosa in disco” deve passare a has.
📁 prefix, namespace, stessa origin
Stessa origin = stesso disco. Il prefix BeeSave: ti isola dalle chiavi di analytics, non da un altro gioco BeeEngine sullo stesso dominio. Per quello esiste namespace: due store con namespace diversi non si pestano le chiavi e clearAll cancella solo il prefisso corrente (storageKey(''), quindi BeeSave: oppure BeeSave:honeyrun:).
Chiama configure una volta, all’avvio, prima di qualsiasi load/save. Cambiare prefix o namespace a metà sessione rende irraggiungibili i record vecchi: load vede missing, clearAll non li tocca. Non è sandbox di sicurezza: chi apre DevTools vede tutte le chiavi dell’origine.
BeeSave.prefix è un getter/setter sullo store di default. Il setter con stringa vuota/falsy ripristina 'BeeSave:'. configure({ prefix: '' }) invece accetta la stringa vuota: attenzione, le chiavi diventano nude.
📁 Versioning e migrate
Lo store ha version (default 1). Ogni save scrive quel numero nell’envelope. In read, se hai passato migrate e version < this.version, chiama migrate(value, fromVersion, toVersion, key). Se la callback lancia, il record è corrupt. Se resaveOnMigrate è true, dopo una migrazione riuscita riscrive col nuovo schema (un save interno: se quello fallisce, read torna comunque ok col valore migrato in RAM, ma il disco può restare vecchio).
I record legacy (senza envelope) hanno versione 0: la migrate parte, se l’hai registrata. Non c’è migrate “verso il basso”. Non c’è checksum. La migrate è roba tua: mappa campi, non magia.
📁 localStorage assente, privato, fallback memoria
All’avvio lo store prova localStorage con un probe setItem/removeItem. Se manca, se lancia (Safari privato, WebView, SSR), il disco nativo è null. Con fallback: 'memory' (default) usa una Map in RAM che imita l’interfaccia Storage: il gioco gira, i save non sopravvivono al reload.
persistent è true solo se il disco nativo c’è. available è true se c’è un backend (disco o memoria). Con fallback diverso da 'memory' e disco morto, save/read tornano unavailable senza lanciare.
Puoi iniettare uno storage finto: BeeSave.create({ storage: mock }) o configure({ storage }). Utile nei test. storage: null è iniezione esplicita: niente probe, resti sul fallback.
📁 Slot, remove, clearAll
slotKey(i) produce slot:0, slot:1, … (Number(index) || 0: NaN e undefined diventano slot 0). saveSlot / readSlot / loadSlot sono alias su quella chiave. Non c’è un indice massimo: tre slot o trenta, sei tu.
remove(key) torna boolean: false se chiave vuota, backend assente, o removeItem lancia. Non throw-a.
clearAll non chiama localStorage.clear(). Due passate (accumula, poi cancella) solo sulle chiavi che startsWith il root corrente. Ritorna il numero di chiavi rimosse, non void. Ogni key(i) / removeItem è in try: una chiave ostile non ferma il giro. Se cambi namespace, le chiavi dell’altro namespace restano.
📁 Esempio d’uso con BeeEngine
Configura una volta. Carica con read se ti interessa lo status; salva un DTO, non l’istanza BeePlayer. Il motore non serializza entità.
📁 Note per chi integra — cosa è chiuso e cosa no
Non serializzare entità vive. Ora te lo dice in faccia (rejected), invece di scrivere {} o lanciare un TypeError non catturato. Estrai un DTO piatto.
load resta comodo e opaco. Se usi solo load, un parse rotto sembra ancora un primo avvio. Lo strumento per non farti fregare è read (o lastRead subito dopo).
Fallback memoria non è persistenza. In privato o senza localStorage, save torna ok lo stesso. Controlla BeeSave.store.persistent se devi avvisare il giocatore.
Nessun debounce. save ogni frame è legale e sbagliato: stringify + I/O sul main thread. Salva su eventi (pausa, game over, setting).
Nessuna cifratura. Application → Local Storage è editabile. I high score non sono anti-cheat. Il namespace non è un confine di sicurezza.
Due store, due dischi logici. BeeSave.create({ namespace: 'editor' }) non condivide le chiavi con la facade statica, a meno che prefix+namespace coincidano. La facade e BeeSave.store sono lo stesso oggetto.
📁 Riepilogo API
I metodi statici di BeeSave sono alias dello store di default. available, persistent, lastRead, lastWrite, storageKey e slotKey vivono sullo store: BeeSave.store oppure un BeeSaveStore creato da te.
| Membro | Ritorna | Scopo |
|---|---|---|
BEE_SAVE_DEFAULTS |
oggetto frozen | prefix 'BeeSave:', namespace '', version 1, onCorrupt 'keep', fallback 'memory', resaveOnMigrate false. |
BEE_SAVE_STATUS |
oggetto frozen | OK MISSING CORRUPT UNAVAILABLE REJECTED QUOTA. |
new BeeSaveStore(options) |
BeeSaveStore |
Store isolato. Options: defaults + storage + migrate. |
BeeSave.create(options) |
BeeSaveStore |
Factory: new BeeSaveStore(options). |
BeeSave.configure(options) |
BeeSave |
Riconfigura lo store di default. Da chiamare all’avvio. |
BeeSave.store |
BeeSaveStore |
Istanza di default usata da tutti i metodi statici. |
BeeSave.prefix |
string |
Getter/setter sullo store di default. Falsy nel setter → default. |
available / persistent |
boolean |
C’è un backend / il backend è il disco nativo, non la Map. |
lastRead / lastWrite |
result o null |
Ultimo esito di read / save. |
storageKey(key) |
string |
prefix + namespace: + key. |
save(key, value) |
result | Valida DTO, scrive envelope. Catch su stringify e setItem. Non lancia. |
read(key) |
result | Status onesto: ok / missing / corrupt / … |
load(key, defaultValue = null) |
any |
Valore se ok (incluso null salvato). Altrimenti il default. |
exists(key) |
boolean |
true solo se read è ok. Il JSON rotto non conta. |
has(key) |
boolean |
Chiave presente in disco, anche corrotta. |
remove(key) |
boolean |
Rimuove la chiave prefissata. false se non può. |
clearAll() |
number |
Due passate sul prefix+namespace corrente. Non è clear(). |
saveSlot / readSlot / loadSlot |
come save/read/load | Chiave slot:N. N da Number(index) || 0. |
