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
// beforeconst stream = new ReadableStream({ start(controller) { for (const chunk of chunks) controller.enqueue(chunk); controller.close(); },});Coletar um stream em um único Uint8Array
// beforeconst 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 escreverConverter 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:
npm install @hsblabs/web-stream-extrasFunciona 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
Se algo estiver faltando, abrir uma issue é o caminho mais rápido.
hsb.horse