Skip to content

Salve e carregue o estado do jogo com IndexedDB

IndexedDB é a melhor opção para armazenar dados persistentes de jogos no navegador. Ele lida com grandes volumes de dados, funciona offline e não bloqueia a thread principal.

1) Por que usar IndexedDB em vez de localStorage?

RecursolocalStorageIndexedDB
Limite de armazenamento~5-10 MB50+ MB (geralmente GB)
Tipos de dadosApenas stringsObjetos, blobs, arrays
AssíncronoNão (bloqueia)Sim
Consultas indexadasNãoSim

Para jogos, IndexedDB quase sempre é a escolha certa.

2) Abrindo um banco de dados

js
function openGameDB() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open('MyGame', 1)
    
    request.onerror = () => reject(request.error)
    request.onsuccess = () => resolve(request.result)
    
    request.onupgradeneeded = (event) => {
      const db = event.target.result
      
      // Cria os armazenamentos
      if (!db.objectStoreNames.contains('saves')) {
        db.createObjectStore('saves', { keyPath: 'slot' })
      }
      if (!db.objectStoreNames.contains('settings')) {
        db.createObjectStore('settings', { keyPath: 'key' })
      }
    }
  })
}

3) Salvando o estado do jogo

js
async function saveGame(slot, gameState) {
  const db = await openGameDB()
  
  return new Promise((resolve, reject) => {
    const tx = db.transaction('saves', 'readwrite')
    const store = tx.objectStore('saves')
    
    const saveData = {
      slot,
      state: gameState,
      timestamp: Date.now(),
    }
    
    const request = store.put(saveData)
    request.onsuccess = () => resolve()
    request.onerror = () => reject(request.error)
  })
}

4) Carregando o estado do jogo

js
async function loadGame(slot) {
  const db = await openGameDB()
  
  return new Promise((resolve, reject) => {
    const tx = db.transaction('saves', 'readonly')
    const store = tx.objectStore('saves')
    
    const request = store.get(slot)
    request.onsuccess = () => resolve(request.result?.state || null)
    request.onerror = () => reject(request.error)
  })
}

5) Listando todos os saves

js
async function listSaves() {
  const db = await openGameDB()
  
  return new Promise((resolve, reject) => {
    const tx = db.transaction('saves', 'readonly')
    const store = tx.objectStore('saves')
    
    const request = store.getAll()
    request.onsuccess = () => resolve(request.result)
    request.onerror = () => reject(request.error)
  })
}

6) Excluindo um save

js
async function deleteSave(slot) {
  const db = await openGameDB()
  
  return new Promise((resolve, reject) => {
    const tx = db.transaction('saves', 'readwrite')
    const store = tx.objectStore('saves')
    
    const request = store.delete(slot)
    request.onsuccess = () => resolve()
    request.onerror = () => reject(request.error)
  })
}

7) Uma classe GameStorage completa

js
class GameStorage {
  constructor(dbName = 'GameData', version = 1) {
    this.dbName = dbName
    this.version = version
    this.db = null
  }
  
  async init() {
    this.db = await this.openDB()
  }
  
  openDB() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.version)
      request.onerror = () => reject(request.error)
      request.onsuccess = () => resolve(request.result)
      request.onupgradeneeded = (e) => {
        const db = e.target.result
        if (!db.objectStoreNames.contains('saves')) {
          db.createObjectStore('saves', { keyPath: 'slot' })
        }
        if (!db.objectStoreNames.contains('settings')) {
          db.createObjectStore('settings', { keyPath: 'key' })
        }
        if (!db.objectStoreNames.contains('assets')) {
          db.createObjectStore('assets', { keyPath: 'url' })
        }
      }
    })
  }
  
  async save(slot, data) {
    const tx = this.db.transaction('saves', 'readwrite')
    tx.objectStore('saves').put({ slot, data, timestamp: Date.now() })
    return tx.complete
  }
  
  async load(slot) {
    const tx = this.db.transaction('saves', 'readonly')
    const result = await this.promisify(tx.objectStore('saves').get(slot))
    return result?.data || null
  }
  
  async setSetting(key, value) {
    const tx = this.db.transaction('settings', 'readwrite')
    tx.objectStore('settings').put({ key, value })
  }
  
  async getSetting(key, defaultValue = null) {
    const tx = this.db.transaction('settings', 'readonly')
    const result = await this.promisify(tx.objectStore('settings').get(key))
    return result?.value ?? defaultValue
  }
  
  promisify(request) {
    return new Promise((resolve, reject) => {
      request.onsuccess = () => resolve(request.result)
      request.onerror = () => reject(request.error)
    })
  }
}

8) Armazenando dados binários (texturas, áudio)

IndexedDB lida com Blobs e ArrayBuffers:

js
async function cacheAsset(url, blob) {
  const tx = db.transaction('assets', 'readwrite')
  tx.objectStore('assets').put({ url, blob, cached: Date.now() })
}

async function getCachedAsset(url) {
  const tx = db.transaction('assets', 'readonly')
  const result = await promisify(tx.objectStore('assets').get(url))
  return result?.blob || null
}

9) Padrão de salvamento automático

js
class AutoSave {
  constructor(storage, interval = 60000) {
    this.storage = storage
    this.interval = interval
    this.timer = null
    this.dirty = false
  }
  
  markDirty() {
    this.dirty = true
  }
  
  start(getState) {
    this.timer = setInterval(async () => {
      if (this.dirty) {
        await this.storage.save('autosave', getState())
        this.dirty = false
        console.log('Salvo automaticamente')
      }
    }, this.interval)
  }
  
  stop() {
    clearInterval(this.timer)
  }
}

10) Tratamento de erros e alternativas

js
async function safeLoad(slot, defaultState) {
  try {
    const saved = await loadGame(slot)
    if (saved) {
      // Valida/migra saves antigos, se necessário
      return migrateSave(saved)
    }
  } catch (err) {
    console.warn('Falha ao carregar o save:', err)
  }
  return defaultState
}

function migrateSave(save) {
  // Lida com formatos antigos de save
  if (!save.version) {
    save.version = 1
    save.settings = save.settings || {}
  }
  return save
}

11) Faça os saves sobreviverem à remoção

Não há garantia de que os dados do IndexedDB permanecerão armazenados. Por padrão, uma origem usa armazenamento de "melhor esforço", e o navegador pode removê-lo quando o disco fica cheio ou, no Safari/WebKit, após um período sem interação do usuário com seu site. No caso de saves de jogos, esses são justamente os dados que você não quer perder.

Solicite armazenamento persistente para que o navegador não apague seus dados sem uma ação explícita do usuário:

js
async function makeStoragePersistent() {
  if (navigator.storage && navigator.storage.persist) {
    const persisted = await navigator.storage.persist()
    console.log(persisted ? 'Os saves estão protegidos contra remoção' : 'Os saves podem ser removidos sob pressão de armazenamento')
    return persisted
  }
  return false
}

Os navegadores decidem se concedem essa permissão com base em sinais de engajamento — quanto o usuário interage com seu site e se ele está instalado como PWA —, portanto, não presuma que a solicitação sempre terá sucesso. Você também pode verificar quanto espaço está disponível antes de gravar saves grandes ou assets em cache:

js
async function checkStorage() {
  if (navigator.storage && navigator.storage.estimate) {
    const { usage, quota } = await navigator.storage.estimate()
    console.log(`Usando ${usage} de ${quota} bytes`)
  }
}

Ambas as APIs exigem um contexto seguro (HTTPS ou localhost).

Conteúdo relacionado

Recursos externos