Skip to content

Storage and Saved State

Arnel Robles edited this page Oct 1, 2026 · 1 revision

Storage and Saved State

DataService is the persistence layer under RewardSystem. It serializes to JSON and writes through a storage object you can replace.

import { DataService } from 'dopaminejs';

const data = new DataService({
    storage: window.sessionStorage,  // default: localStorage, or memory if that is unavailable
    prefix: 'myapp_'                 // default: 'dopamine_'
});

The player is stored under <prefix>player, so dopamine_player by default.

Methods

Method Returns Notes
save(key, data) Promise<boolean> false on failure. The error is logged, not thrown.
load(key, defaultValue = null) Promise<value> The default on a missing key or a parse error.
clear(key) Promise<void> Removes the key.

Custom storage

Any object with getItem, setItem and removeItem works. The methods may return values or promises.

const serverStorage = {
    async getItem(key) {
        const response = await fetch(`/api/progress/${key}`);
        return response.ok ? response.text() : null;
    },
    async setItem(key, value) {
        await fetch(`/api/progress/${key}`, { method: 'PUT', body: value });
    },
    async removeItem(key) {
        await fetch(`/api/progress/${key}`, { method: 'DELETE' });
    }
};

const rewards = new RewardSystem(new DataService({ storage: serverStorage }));

With promise storage, writes go out one at a time in the order they were made, so an older value cannot land after a newer one. load waits for any write still in flight.

SoundManager reads its mute flag in the constructor, so it needs synchronous storage.

When localStorage is missing

import { resolveStorage, createMemoryStorage } from 'dopaminejs';
  • resolveStorage() returns localStorage if it exists and a test write succeeds. Otherwise it returns a memory store. This covers servers, workers, Safari private browsing and storage-blocked iframes.
  • createMemoryStorage() returns a store backed by a Map. Nothing survives a reload. Useful in tests.

DataService calls resolveStorage() when you pass no storage.

Saved state is not trusted

The player lives in the browser, and the player can edit it. On load, RewardSystem checks every field:

  • xp that is not a finite number of 0 or more becomes 0
  • level is at least what the XP earns. A higher stored level is kept.
  • streak counts must be integers of 1 or more, and lastPlayDate must look like YYYY-MM-DD
  • achievements and stats must be plain records, and keys such as __proto__ are dropped
  • stat values that are not finite numbers are dropped, except totalPlays and highScore, which become 0
  • a save from an older version gets the fields added since

This stops a bad save from crashing the app or reaching Object.prototype. It does not make the numbers honest. If a number matters, compute it on your server. See Security.

Failed writes

A full or blocked storage does not throw. RewardSystem emits save_failed and keeps the state in memory.

rewards.on('save_failed', ({ key }) => showBanner('Progress is not being saved'));

Resetting progress

await data.clear('player');
await rewards.init();   // loads a fresh default player

Clone this wiki locally