logo hsb.horse
← Voltar para o índice do blog

Blog

Cansei de continuar escrevendo o mesmo boilerplate de Web Streams

Por que reuni utilitários recorrentes de ReadableStream<Uint8Array> em @hsblabs/web-stream-extras. Os três padrões que eu repetia sempre, a classe base ByteTransformStream e a criptografia de stream com Web Crypto.

Publicado:

Em algum momento você precisa de um ReadableStream<Uint8Array>. Aí abre um arquivo vazio e volta a escrever a mesma estrutura de controller de sempre. Depois surge a necessidade de coletar os bytes no fim do stream. Mais um utilitário. Em seguida esse resultado precisa ir para algum lugar que espera ArrayBuffer, não Uint8Array. Mais uma conversão.

Nada disso é difícil. Tudo isso é ruído.

Por isso reuni essas peças em @hsblabs/web-stream-extras.


Os três padrões que eu reescrevia sem parar

Criar um ReadableStream a partir de um array de chunks

// before
const stream = new ReadableStream({
start(controller) {
for (const chunk of chunks) controller.enqueue(chunk);
controller.close();
},
});

Coletar um stream em um único Uint8Array

// before
const chunks: Uint8Array[] = [];
const reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
const result = concatU8Arrays(chunks); // isso também eu precisava escrever

Converter entre Uint8Array, ArrayBuffer e string

Aquelas pequenas diferenças que fazem você perder tempo procurando a view certa ou o padrão correto de cópia.


After

import {
readableFromChunks,
readAllBytes,
stringToBinary,
binaryToString,
} from "@hsblabs/web-stream-extras";
const stream = readableFromChunks([
stringToBinary("hello"),
stringToBinary(" world"),
]);
const result = await readAllBytes(stream);
console.log(binaryToString(result)); // "hello world"

Instalação:

Terminal window
npm install @hsblabs/web-stream-extras

Funciona em Node.js ≥22 e navegadores modernos. Sem dependências de runtime.


Montando pipelines de transformação binária

Quando você precisa de uma etapa de transformação customizada, ByteTransformStream fornece uma classe base tipada que permite estender TransformStream sem reimplementar toda a infraestrutura.

import { ByteTransformStream } from "@hsblabs/web-stream-extras";
class UpperCaseStream extends ByteTransformStream {
transform(chunk: Uint8Array) {
// processa os bytes e envia a saída com this.push()
}
}

ByteQueue cuida do buffering interno quando você precisa de leituras parciais atravessando limites de chunks. encodeBase64Url e decodeBase64Url atuam nas bordas entre bytes e texto.


Criptografia de stream

A criptografia fica em um subcaminho separado. Se você não precisar dela, ela não é entregue.

import { encryptStream, decryptStream } from "@hsblabs/web-stream-extras/encryption";
const key = crypto.getRandomValues(new Uint8Array(32));
const plaintext = readableFromChunks([stringToBinary("secret payload")]);
const encrypted = encryptStream(key, plaintext);
const decrypted = decryptStream(key, encrypted);
console.log(binaryToString(await readAllBytes(decrypted))); // "secret payload"

As duas funções recebem e devolvem ReadableStream<Uint8Array>, então se conectam naturalmente ao restante do pipeline. Os componentes internos EncryptionStream e DecryptionStream também são expostos como wrappers TransformStream, caso você precise deles diretamente.

O formato usa AES-GCM com chaves e nonces por registro derivados com HKDF. Ele foi pensado para streams, não como um formato genérico de criptografia de arquivos.

Padrão com chave mestra

Se a aplicação já administra uma chave mestra AES-GCM separada e deseja armazenar chaves por stream junto com metadados criptografados, webCryptoStream encapsula esse padrão.

import { webCryptoStream } from "@hsblabs/web-stream-extras/encryption";
const masterKey = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"],
);
const crypto$ = webCryptoStream(masterKey);
const streamKey = await crypto$.createStreamKey();
const encrypted = await crypto$.encrypt(streamKey, plaintext);
const decrypted = await crypto$.decrypt(streamKey, encrypted);

Se você já tem uma chave bruta de 32 bytes, encryptStream / decryptStream são suficientes.


O que ele não faz

Vale deixar explícito:

  • Sem autenticação — oferece confidencialidade, não verificação de identidade
  • Sem armazenamento de chaves — onde manter a stream key é responsabilidade da aplicação
  • Sem derivação de chave baseada em senha — PBKDF2 / Argon2 precisam entrar por conta própria
  • Sem gestão de usuários — isto é um utilitário low-level de stream, não um framework de auth

npm · GitHub

Se algo estiver faltando, abrir uma issue é o caminho mais rápido.