BeeEngine JAVASCRIPT 2D ENGINE
Documentazione – BeeEngine
v2.5.0

BeeSave — Documentazione Tecnica

Persistenza DTO su storage del browser: envelope, status, namespace e migrazione

BeeEngine Mascot

📁 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.

import { BeeSave, BeeSaveStore, BEE_SAVE_STATUS } from ‘beeengine’; BeeSave.configure({ namespace: ‘honeyrun’, version: 2 }); const written = BeeSave.save(‘progress’, { level: 3 }); written.ok; // true se è andata a disco (o in memoria) written.status; // ‘ok’ | ‘rejected’ | ‘quota’ | ‘unavailable’

📁 Envelope: non è più il JSON nudo

In storage non trovi più "1200" o {"level":3}. Trovi un oggetto marchiato:

{ “__bee”: 1, “v”: 1, “t”: 1736000000000, “d”: { “level”: 3 } }

__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 vuotarejected, 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 assenteunavailable (solo se hai spento il fallback memoria).

4. JSON.stringify in try/catch → fallimento → rejected.

5. setItem in try/catchQuotaExceededError / NS_ERROR_DOM_QUOTA_REACHED / code 22 o 1014 → status quota; qualsiasi altro throw → unavailable. L’eccezione non sale al chiamante.

const r = BeeSave.save(‘progress’, payload); if (!r.ok) { if (r.status === BEE_SAVE_STATUS.QUOTA) { // disco pieno; il valore precedente di quella chiave resta } // UI: r.message } BeeSave.save(‘hero’, player); // rejected: $.hero is a live BeePlayer — serialize a plain DTO

📁 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.

const rec = BeeSave.read(‘progress’); if (rec.status === BEE_SAVE_STATUS.MISSING) { // vero primo avvio } else if (rec.status === BEE_SAVE_STATUS.CORRUPT) { // JSON.parse fallito, o migrate che ha lanciato // onCorrupt default ‘keep’: il record resta; ‘remove’ lo cancella } else if (rec.ok) { applica(rec.value); } // comodo, ma missing e corrupt cadono entrambi sul default const data = BeeSave.load(‘progress’, { level: 1 });

getItem === nullmissing. 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.

BeeSave.save(‘slot’, null); BeeSave.exists(‘slot’); // true BeeSave.load(‘slot’, { ok: true }); // null, non { ok: true } BeeSave.has(‘rotto’); // true se la chiave c’è, anche corrotta BeeSave.exists(‘rotto’); // false se read non è ok

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:).

BeeSave.configure({ prefix: ‘BeeSave:’, namespace: ‘honeyrun’ }); BeeSave.store.storageKey(‘slot0’); // → “BeeSave:honeyrun:slot0”

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.

BeeSave.configure({ version: 2, resaveOnMigrate: true, migrate(data, from, to, key) { if (from < 1) data = { level: data.level ?? 1 }; if (from < 2) data.muted = data.muted === true; return data; } });

📁 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à.

import { BeeSave, BeePlayer, BEE_SAVE_STATUS } from ‘beeengine’; BeeSave.configure({ namespace: ‘honeyrun’, version: 1 }); const DEFAULTS = { level: 1, highScore: 0, muted: false }; function onEnter() { const rec = BeeSave.readSlot(0); const data = rec.ok ? rec.value : { DEFAULTS }; if (rec.status === BEE_SAVE_STATUS.CORRUPT) { // UI: save illeggibile, si parte da DEFAULTS } const player = new BeePlayer(80, 400); return { player, data }; } function onGameOver(score, data) { const r = BeeSave.saveSlot(0, { level: data.level, highScore: Math.max(data.highScore, score), muted: data.muted }); if (!r.ok) { // UI: “impossibile salvare” — r.status / r.message } }

📁 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.