Armazenamento de alto desempenho para seu app: a API Storage Foundation

A plataforma da Web oferece cada vez mais aos desenvolvedores as ferramentas necessárias para criar aplicativos de alto desempenho ajustados para a Web. Mais notavelmente, o WebAssembly (Wasm) abriu as portas para aplicativos da Web rápidos e poderosos, enquanto tecnologias como o Emscripten permitem que os desenvolvedores reutilizem códigos testados na Web. Para aproveitar esse potencial, os desenvolvedores precisam ter o mesmo poder e flexibilidade em relação ao armazenamento.

É aí que entra a API Storage Foundation. A API Storage Foundation é uma nova API de armazenamento rápida e não opinativa que desbloqueia novos casos de uso muito solicitados para a Web, como implementar bancos de dados com bom desempenho e gerenciar arquivos temporários grandes. Com essa nova interface, os desenvolvedores podem "trazer o próprio armazenamento" para a Web, reduzindo a lacuna de recursos entre o código específico da Web e da plataforma.

A API Storage Foundation foi projetada para se parecer com um sistema de arquivos muito básico, oferecendo flexibilidade aos desenvolvedores com primitivos genéricos, simples e de alto desempenho em que eles podem criar componentes de nível superior. Os aplicativos podem aproveitar a melhor ferramenta para suas necessidades, encontrando o equilíbrio certo entre usabilidade, desempenho e confiabilidade.

Por que a Web precisa de outra API de armazenamento?

A plataforma da Web oferece várias opções de armazenamento para desenvolvedores, cada uma criada com casos de uso específicos em mente.

  • Algumas dessas opções claramente não se sobrepõem a essa proposta, porque elas permitem que apenas quantidades muito pequenas de dados sejam armazenadas, como cookies ou a API Web Storage que consiste nos mecanismos sessionStorage e localStorage.
  • Outras opções já foram descontinuadas por vários motivos, como a API File and Directory Entries ou a WebSQL.
  • A API File System Access tem uma superfície de API semelhante, mas seu uso é para interagir com o sistema de arquivos do cliente e fornecer acesso a dados que podem estar fora da propriedade da origem ou até mesmo do navegador. Esse foco diferente vem com considerações de segurança mais rígidas e custos de performance mais altos.
  • A API IndexedDB pode ser usada como um back-end para alguns dos casos de uso da API Storage Foundation. Por exemplo, o Emscripten inclui IDBFS, um sistema de arquivos persistente baseado em IndexedDB. No entanto, como o IndexedDB é fundamentalmente um repositório de chave-valor, ele tem limitações de desempenho significativas. Além disso, o acesso direto a subseções de um arquivo é ainda mais difícil e lento no IndexedDB.
  • Por fim, a interface CacheStorage tem ampla compatibilidade e é ajustada para armazenar dados de grande porte, como recursos de aplicativos da Web, mas os valores são imutáveis.

A API Storage Foundation é uma tentativa de fechar todas as lacunas das opções de armazenamento anteriores, permitindo o armazenamento de arquivos grandes mutáveis de alto desempenho definidos na origem do aplicativo.

Casos de uso sugeridos para a API Storage Foundation

Exemplos de sites que podem usar essa API incluem:

  • Apps de produtividade ou criatividade que operam com grandes quantidades de dados de vídeo, áudio ou imagem. Esses apps podem descarregar segmentos no disco em vez de mantê-los na memória.
  • Apps que dependem de um sistema de arquivos persistente acessível pelo Wasm e que precisam de mais desempenho do que o IDBFS pode garantir.

O que é a API Storage Foundation?

A API tem duas partes principais:

  • Chamadas do sistema de arquivos, que oferecem funcionalidade básica para interagir com arquivos e caminhos de arquivos.
  • Gerenciadores de arquivos, que fornecem acesso de leitura e gravação a um arquivo existente.

Chamadas do sistema de arquivos

A API Storage Foundation apresenta um novo objeto, storageFoundation, que reside no objeto window e que inclui várias funções:

  • storageFoundation.open(name): abre o arquivo com o nome informado, se existir, e cria um novo arquivo. Retorna uma promessa que é resolvida com o arquivo aberto.
  • storageFoundation.delete(name): remove o arquivo com o nome fornecido. Retorna uma promessa que é resolvida quando o arquivo é excluído.
  • storageFoundation.rename(oldName, newName): renomeia atomicamente o arquivo do nome antigo para o novo. Retorna uma promessa que é resolvida quando o arquivo é renomeado.
  • storageFoundation.getAll(): retorna uma promessa que é resolvida com uma matriz de todos os nomes de arquivo existentes.
  • storageFoundation.requestCapacity(requestedCapacity): solicita a nova capacidade (em bytes) para uso pelo contexto de execução atual. Retorna uma promessa resolvida com a quantidade restante de capacidade disponível.
  • storageFoundation.releaseCapacity(toBeReleasedCapacity): libera o número especificado de bytes do contexto de execução atual e retorna uma promessa que é resolvida com a capacidade restante.
  • storageFoundation.getRemainingCapacity(): retorna uma promessa que é resolvida com a capacidade disponível para o contexto de execução atual.

Gerenciadores de arquivos

O trabalho com arquivos acontece por meio das seguintes funções:

  • NativeIOFile.close(): fecha um arquivo e retorna uma promessa que é resolvida quando a operação é concluída.
  • NativeIOFile.flush(): sincroniza (ou seja, limpa) o estado em memória de um arquivo com o dispositivo de armazenamento e retorna uma promessa que é resolvida quando a operação é concluída.
  • NativeIOFile.getLength(): retorna uma promessa que é resolvida com o comprimento do arquivo em bytes.
  • NativeIOFile.setLength(length): define o comprimento do arquivo em bytes e retorna uma promessa que é resolvida quando a operação é concluída. Se o novo comprimento for menor que o atual, os bytes serão removidos a partir do final do arquivo. Caso contrário, o arquivo será estendido com bytes de valor zero.
  • NativeIOFile.read(buffer, offset): lê o conteúdo do arquivo no deslocamento especificado usando um buffer que é o resultado da transferência do buffer especificado, que é deixado desconectado. Retorna um NativeIOReadResult com o buffer transferido e o número de bytes que foram lidos.

    Um NativeIOReadResult é um objeto que consiste em duas entradas:

    • buffer: um ArrayBufferView, que é o resultado da transferência do buffer transmitido para read(). Ele tem o mesmo tipo e comprimento do buffer de origem.
    • readBytes: o número de bytes que foram lidos com sucesso em buffer. Esse valor pode ser menor que o tamanho do buffer, se ocorrer um erro ou se o intervalo de leitura se estender além do final do arquivo. Ele é definido como zero se o intervalo de leitura estiver além do final do arquivo.
  • NativeIOFile.write(buffer, offset): grava o conteúdo do buffer especificado no arquivo no deslocamento especificado. O buffer é transferido antes que qualquer dado seja gravado e, portanto, é deixado desconectado. Retorna um NativeIOWriteResult com o buffer transferido e o número de bytes que foram gravados. O arquivo será estendido se o intervalo de gravação exceder o comprimento.

    Um NativeIOWriteResult é um objeto que consiste em duas entradas:

    • buffer: um ArrayBufferView que é o resultado da transferência do buffer transmitido para write(). É do mesmo tipo e comprimento do buffer de origem.
    • writtenBytes: o número de bytes gravados em buffer. Esse valor pode ser menor que o tamanho do buffer se ocorrer um erro.

Exemplos completos

Para deixar os conceitos apresentados acima mais claros, confira dois exemplos completos que explicam os diferentes estágios do ciclo de vida dos arquivos do Storage Foundation.

Abertura, escrita, leitura e fechamento

// Open a file (creating it if needed).
const file = await storageFoundation.open('test_file');
try {
  // Request 100 bytes of capacity for this context.
  await storageFoundation.requestCapacity(100);

  const writeBuffer = new Uint8Array([64, 65, 66]);
  // Write the buffer at offset 0. After this operation, `result.buffer`
  // contains the transferred buffer and `result.writtenBytes` is 3,
  // the number of bytes written. `writeBuffer` is left detached.
  let result = await file.write(writeBuffer, 0);

  const readBuffer = new Uint8Array(3);
  // Read at offset 1. `result.buffer` contains the transferred buffer,
  // `result.readBytes` is 2, the number of bytes read. `readBuffer` is left
  // detached.
  result = await file.read(readBuffer, 1);
  // `Uint8Array(3) [65, 66, 0]`
  console.log(result.buffer);
} finally {
  file.close();
}

Abrir, listar e excluir

// Open three different files (creating them if needed).
await storageFoundation.open('sunrise');
await storageFoundation.open('noon');
await storageFoundation.open('sunset');
// List all existing files.
// `["sunset", "sunrise", "noon"]`
await storageFoundation.getAll();
// Delete one of the three files.
await storageFoundation.delete('noon');
// List all remaining existing files.
// `["sunrise", "noon"]`
await storageFoundation.getAll();

Demonstração

Teste a demonstração da API Storage Foundation na incorporação abaixo. Crie, renomeie, grave e leia arquivos, além de conferir a capacidade disponível que você pediu para atualizar conforme faz mudanças. Confira o código-fonte da demonstração no Glitch.

Segurança e permissões

A equipe do Chromium projetou e implementou a API Storage Foundation usando os princípios básicos definidos em Como controlar o acesso a recursos poderosos da plataforma da Web, incluindo controle do usuário, transparência e ergonomia.

Seguindo o mesmo padrão de outras APIs de armazenamento modernas na Web, o acesso à API Storage Foundation é limitado à origem, o que significa que uma origem só pode acessar dados criados por ela mesma. Ele também é limitado a contextos seguros.

Controle do usuário

A cota de armazenamento será usada para distribuir o acesso ao espaço em disco e evitar abusos. A memória que você quer ocupar precisa ser solicitada primeiro. Assim como outras APIs de armazenamento, os usuários podem limpar o espaço ocupado pela API Storage Foundation no navegador.

Links úteis

Agradecimentos

A API Storage Foundation foi especificada e implementada por Emanuel Krivoy e Richard Stotz. Este artigo foi revisado por Pete LePage e Joe Medley.

Imagem principal de Markus Spiske no Unsplash.