M

Myreli

Rascunhos abertos ao mundo, geralmente sobre código e às vezes sobre a vida.

[talks] Introdução Rápida a Criptografia Assimétrica

Versão textual da tech talk de introdução à criptografia assimétrica.

Conceitos

Wikipedia

Ambiente

  • Node.js >= 14 <= 18
  • TypeScript
  • Jest

Instruções podem ser encontradas aqui.

Demonstração

// src/AsymmetricCrypto.test.ts

import { AsymmetricCrypto } from "./AsymmetricCrypto"

/**
 * Async-Await (ou callbacks)
 * Node.js Event Loop
 */

it("should enable secure message exchange between Ana and Bob", async () => {
    const asymmetricCrypto = new AsymmetricCrypto()
    const anaKeys = await asymmetricCrypto.generateKeyPair()
    const bobKeys = await asymmetricCrypto.generateKeyPair()

    /**
     * Aqui ocorreria a Exchange de Keys 
     * (Ana envia a publica para o Bob e vice-versa)
     */

    // Ana envia mensagem para Bob
    const cryptedMessage = asymmetricCrypto.encrypt("Hello", bobKeys.publicKey)
    expect(cryptedMessage.toString()).not.toEqual("Hello")

    // Bob recebe mensagem de Ana
    const message = asymmetricCrypto.decrypt(cryptedMessage, bobKeys.privateKey)
    expect(message.toString()).toEqual("Hello")

    // Bob responde mensagem para Ana
    const cryptedReply = asymmetricCrypto.encrypt("Hello, Ana", anaKeys.publicKey)
    expect(cryptedReply.toString()).not.toEqual("Hello, Ana")

    // Ana recebe a resposta de Bob
    const reply = asymmetricCrypto.decrypt(cryptedReply, anaKeys.privateKey)
    expect(reply.toString()).toEqual("Hello, Ana")
})

Implementação

// src/AsymmetricCrypto.ts 

import { generateKeyPair as cryptoGenerateKeyPair, publicEncrypt, privateDecrypt } from "node:crypto"
import { promisify } from 'node:util'

export type KeyPair = {
    privateKey: string 
    publicKey: string
}

export class AsymmetricCrypto {

    public async generateKeyPair(): Promise<KeyPair> {
        const { privateKey, publicKey } = await promisify(cryptoGenerateKeyPair)('rsa', {
            modulusLength: 4096,
            publicKeyEncoding: {
              type: 'spki',
              format: 'pem'
            },
            privateKeyEncoding: {
              type: 'pkcs8',
              format: 'pem'
            }
          })

        return {
            privateKey,
            publicKey
        }
    }

    public encrypt(message: string, recipientKey: string): Buffer {
        return publicEncrypt(recipientKey, Buffer.from(message))
    }

    public decrypt(encryptedMessage: Buffer, privateKey: string): Buffer {
        return privateDecrypt(privateKey, encryptedMessage)
    }
}

Menções

[Resumo] RabbitMQ in Depth: An in-depth tour of message properties

Esse é resumo do capítulo 3 do RabbitMQ in Depth com foco em quais são as propriedades disponíveis e como impactam as mensagens.

As propriedades da mensagem ficam nos headers da mensagem (Basic.Properties).

Propriedades

content-type

Define o tipo de conteúdo do corpo da mensagem

content-encoding

Define se o conteúdo está codificado ou comprimido de alguma forma.

Por exemplo aqui podemos definir que o corpo das mensaagens estará comprimido utilizando GZIP, e assim os consumidores conseguem de-comprimir.

message_id

Identifica unicamente uma mensagem

correlation_id

Identifica a resposta a uma mensagem

timestamp

Define quando a mensagem foi criada

expiration

Define quando a mensagem deixa de ter validade

delivery-mode

O RabbitMQ utiliza para decidir quando escrever no disco e quando manter em memória

app-id

Define a aplicação que originou a mensagem

user-id

Define o usuário que originou a mensagem

type

Permite a definição de um contrato entre consumidor e produtor

reply-to

Define o roteamento das mensagens quando utilizando o padrão de resposta

headers

É um conjunto de chave-valor, também utilizado pelo RabbitMQ para roteamento

KOTLIN IDIOMÁTICO: Valor padrão ou inicialização tardia?

Em alguns casos não conseguimos injetar as propriedades via construtor, como em testes, e tendemos a aplicar um valor padrão, por exemplo:

var cobaia: Cobaia = null
// ... várias coisas até a cobaia estar disponível
cobaia = Myreli()

Isso é muito comum com testes, porque em vez de inicializar no construtor, inicializamos em uma etapa de *setup *para nos aproveitarmos da injeção de dependências com mocks e outros recursos. Por exemplo o código abaixo não é um bom teste:

class CobaiaSpec {
    val cobaia: Cobaia = Myreli() // 😿

    @Test fun test() {
        cobaia.executarExperimentosMalucos()
    }
}

Então precisamos inicializar no setup dos testes:

class CobaiaSpec {
    var cobaia: Cobaia = null // 😿

    @SetUp fun setUp() {
        cobaia = Myreli() // 😺
    }

    @Test fun test() {
        cobaia?.executarExperimentosMalucos() // 😿
    }
} 

Mas dar um valor padrão para uma variável simplesmente porque precisamos dela em diferentes sub contextos sempre trás um aspecto estranho. O kotlin resolve esse problema com a inicialização tardia:

class CobaiaSpec {
    lateinit var cobaia: Cobaia // 😻

    @SetUp fun setUp() {
        cobaia = Myreli() // 😻
    }

    @Test fun test() {
        cobaia.executarExperimentosMalucos()
    }
} 

Referências

[Gestão de Incidentes] Postmortem: aprendendo com os próprios erros

RASCUNHO

O que é e como aplicar postmortem.

  • Registro por escrito de um incidente, o impacto, a resolução e as causas raízes

Diretiva Primária

"Independentemente do que descobrimos, nós entendemos e acreditamos de verdade que todos fizeram o melhor trabalho que podiam, dado o que se sabia na época, suas habilidades e aptidões, os recursos disponíveis e a situação em questão."

Premissas

  • Focar em uma reunião produtiva e construtiva, sem apontar dedos ou culpados

  • O objetivo é compartilhar conhecimento e melhoria contínua

Sugestões

  • Fazer uma publicação assíncrona (newsletter) mensal resumindo os postmortems

  • Registrar o postmortem na ferramenta de gerenciamento, junto com os incidentes

Modelo

# [#1234] Postmortem: Falha na geração automática do relatório XYZ

**Síntese:** 
Lorem ipsum dolor sit amet, consectetur adipiscing elit.

- Atualizado em: 2022-03-16
- Autores: @Myreli, @Alguem, @Pessoa
- Anexos: [Dashboard](http://link.to/dashboard), [Relatos](http://link.to/relatos)

## Impacto
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.

## Causa Raíz
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut 
labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris 
nisi ut aliquip ex ea commodo consequat.

## Resolução
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, 
totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae 
dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, 
sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.

## Detecção
Consectetur adipiscing elit.

## Aprendizados
**O que foi bem**
- Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit
- Quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt

**O que precisa melhorar**
- At vero eos et accusamus et iusto odio dignissimos ducimus
- Nam libero tempore, cum soluta nobis est eligendi optio cumque

## Planos de Ação
- [Sed ut perspiciatis unde omnis](https://link.to/#4567)
- [Nemo enim ipsam](https://link.to/#8901)

## Linha do Tempo
- 2022-03-14 14:30 `REPORTADO`: Incidente reportado pelo usuário XYZ 
- 2022-03-14 16:30 Investigando causa do problema (atualização automática 2h)
- 2022-03-14 17:03 `IDENTIFICADO`: Componente problemático identificado
- 2022-03-14 18:20 `MITIGADO`: Correção temporária liberada
- 2022-03-14 20:40 `REPRODUZIDO`: Problema reproduzido em ambiente de teste
- 2022-03-14 22:40 Investigando soluções
- 2022-03-15 09:00 `RESOLVIDO`: Solução definitiva liberada

Referências:

Uma breve comparação entre await e then para lidar com operações assíncronas

Em construção!

Existe uma conversa frequente nos fóruns de Node.js acerca de como lidar com funções assíncronas. Ainda que async e then nos fornecem a mesma funcionalidade para lidar com código assíncrono em JavaScript, ambas são distintas em seu funcionamento e efeitos colaterais.


Essa não é uma introdução as promises ou programação assíncrona, apenas devaneios sobre formas de lidar com o resultado dessas operações. Aqui estão excelentes materiais para aprender sobre recursos de programação assíncrona no JavaScript:


Trabalhando com Node.js (JavaScript) você provavelmente já se deparou com esses dois tipos de código:

  1. Requisição a API construída pelo time do then

fetch("https://emojihub.herokuapp.com/api/random")
    .then(response => response.json())
    .then(data => console.info(data))
  1. Requisição a API construída pelo time do async/await

const response = await fetch("https://emojihub.herokuapp.com/api/random")

const data = await response.json()

console.info(data)

Ambas resultam na mesma coisa: um emoji retornado randomicamente pela API e exibido no console. Mas cada API tem seus objetivos e seus respectivos casos de uso, e a semântica de cada um é diferente.

Defendo que não existe bala de prata e cada recurso tem sua razão de ser, então esse rascunho não serve para dizer qual é melhor, mas sim comparar ambos e ajudar na escolha, além de compartilhar minha preferência do ponto de vista da legibilidade.

História

Para que o próximo tópico faça sentido, vou começar traduzindo cada uma das implementações:

  1. Utilizando then, estamos essencialmente:

'BUSCAR O EMOJI NA API'
    ENTÃO 'TRANSFORMAR A RESPOSTA EM JSON'
    ENTÃO 'IMPRIMIR OS DADOS'
  1. Utilizando await, estamos essencialmente:

RESPOSTA = (AGUARDE) 'BUSCAR O EMOJI NA API'

DADOS = (AGUARDE) 'TRANSFORMAR A RESPOSTA EM JSON'

'IMPRIMIR OS DADOS'

A implementação sozinha pode parecer pouca diferença, mas no segundo caso o código lê mais natural, muito semelhante ao síncrono. Enquanto o primeiro depende de bastante compreensão do https://subscription.packtpub.com/book/web-development/9781783287314/1/ch01lvl1sec10/the-callback-pattern e corre o risco do http://callbackhell.com/, caso seja mal implementado.

Para além da naturalidade, cada formato tem objetivo diferente e internalidades diferentes. Apesar de parecer apenas um açúcar sintático, await implica em outras diferenças também.

E esse é um motivo histórico, em linha do tempo:

  1. Node.js foi criado profundamente atrelado ao Padrão Callback, que permitia a utilização de código assíncrono

  2. Foram criadas as promises e as novas APIs de then/catch/finally que permitiram minimizar o callback hell

  3. Finalmente foram criadas as funções assíncronas, que trouxeram o async/await e permitiram o código mais legível e natural (https://developers.google.com/web/fundamentals/primers/async-functions)

Apesar de não serem sempre recursos concorrentes e terem seus próprios casos de uso, a nova sintaxe é uma evolução levando em conta diversos dos problemas anteriores. Por isso, para alguns casos ela claramente será mais compreensível, porque surgiu em um contexto diferente — de resolver problemas anteriores.

O mesmo vale para as novas APIs de manipulação de coleções — map, filter,reduce — nenhum deles substitui o bom e velho for ou ainda o while, apenas resolvem problemas específicos.

Compreensibilidade

Escreverei pouco sobre este tópico porque o código fala mais do que mil palavras. E porque existem materiais melhores escritos sobre isso (como esse da Google https://developers.google.com/web/fundamentals/primers/async-functions ou esse da MDN https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Promises).

Naturalmente (ao menos na nossa região) fazemos a leitura de cima para baixo e da esquerda para a direita. E é isso que o código expressa:

console.log("#1");

await something();

console.log("#2");

console.log("#3");

Já utilizando then, não podemos garantir que o código a seguir será executado nessa ordem, mas sim que vai acontecer na ordem que for mais performática:

console.log("#1");

something().then(() => { 
    console.log("#3? (or #2)");
})

console.log("#2? (or #3)");

Para garantir a ordem, seria necessário encadear a execução a resolução da Promise, causando aninhamento e possível quebra de semântica:

console.log("#1");

something().then(() => { 
    console.log("#2");
}).finally(() => {
    console.log("#3");
})

Código limpo é código limpo em qualquer lugar e com qualquer padrão, então sem aninhar seus thens. Em qualquer nível, sempre evite o callback hell: https://ibb.co/DkMWQfq.

Performance

Hoje em dia, o async/await é mais rápido que as outras opções, e muito mais rápido que implementações manuais de promise. Isso é graças ao https://v8.dev/blog/fast-async, uma implementação da V8 que se aproveitou dos recursos para evitar o overhead que era causado pela promise extra no await .

De todo modo, sempre priorize realizar em “paralelo” processamentos que não são bloqueantes e dependentes entre si e aguardar por todos de uma vez só. O promise.all é um exemplo de recurso que possibilita isso, e pode ser utilizado em qualquer uma das maneiras. (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all)

Tratamento de Erros

Além de possibilitar o tratamento de erros mais familiar (try/catch), a V8 trabalhou em um recurso poderosíssimo para o tratamento de erros com async/await: Zero-cost async stack traces (https://docs.google.com/document/d/13Sy_kBIJGP0XT34V1CV3nkWya4TwYx9L3Yv45LdGB6Q/edit#heading=h.e6lcalo0cl47). Isso significa que o rastreamento de erro contempla as informações do código síncrono e assíncrono, resolvendo uma dor forte do padrão de callbacks ou do then/catch, que era detectar a origem de erros não capturados quando surgiam de código assíncrono.

Por outro lado, como JavaScript não possui catchs condicionais, o tratamento específico de exceção pode ser mais verboso, enquanto temos o par then/catch que pode especificar por cada operação assíncrona.

Para se aprofundar nas convenções de tratamento de erro, veja:

Afinal, qual forma é a melhor?

Nenhuma. Cada uma pode ter um caso de uso mais ou menos adequado, de acordo com o contexto.

Assim como não devemos encadear vários condicionais (if) também não deveríamos encadear vários resolvedores de promessas (then). E assim como não deveríamos implementar um complexo padrão de projeto para resolver uma validação de sim ou não, não deveríamos criar uma função para utilizar await quando o padrão de callback resolveria.

Por causa do ganho em compreensibilidade e das possibilidades de melhoria em performance que chegaram com a sintaxe async/await, a discussão ainda vem evoluindo para que await possa ser utilizado também em nível de módulos.

As propostas originais mencionadas servem como contexto para o problema do callback hell e as vantagens observadas com a nova sintaxe:

Isso não demonstra que ele seja melhor, mas que foi amplamente adotado e o uso vem crescendo. As pessoas olhariam estranho para um código cheio de then encadeado sendo que temos uma API muito mais limpa para lidar com isso.

E as pessoas também olhariam estranho para uma lista de awaits dentro de uma mesma função porque isso pode estar criando um bloqueio por forçar o comportamento síncrono, principalmente quando dentro de loops. (off-topic: Se você tem trabalhando com várias operações encadeadas, recomendo fortemente o estudo de streams. Especialmente as pipelines https://nodejs.org/api/stream.html#streampipelinesource-transforms-destination-callback)

Lembrando do Fowler:

Any fool can write code that a computer can understand. Good programmers write code that humans can understand.

No fim, o melhor para a aplicação é não bloquear o event loop. A forma como isso será codificado depende muito mais do código e das pessoas que vão ler ele — pessoalmente vejo que a sintaxe async/await costuma atender a maioria dos casos comuns, além de ser compatível com a especificação de async iterators (https://tc39.es/proposal-async-iteration/).

Isso não exime de utilizar callbacks, eles são core do Node (https://nodejs.org/en/knowledge/getting-started/control-flow/what-are-callbacks/), ainda que eles próprios tratem async/await como a alternativa moderna para lidar com assincronismo (https://nodejs.dev/learn/modern-asynchronous-javascript-with-async-and-await).

Além disso, vale mencionar que a V8, o motor que faz a magica acontecer e JS executar no Back-End com Node.js, aconselha (1) a utilização de async/await em vez de promises escritas manualmente pelos ganhos em performance e (2) a utilização das implementações nativas de promise em vez de bibliotecas pelos outros benefícios mencionados anteriormente, na seção de performance.

“Não bloqueie o Event Loop

Afinal, por que tudo isso? (em homenagem ao meu amigo que xingou Node.js dizendo que ler um arquivo em Java na aula de POO foi mais simples que entender o Event Loop)

Quando alguém me questiona porque tantas APIs nativas do Node.js, ou mesmo as bibliotecas mais utilizadas, são assíncronas e se não seria mais simples elas simplesmente serem síncronas como em algumas outras linguagens, minha resposta gira em torno dê: “Não bloqueie o Event Loop”.

Node.js® é descrito em sua própria documentação como “um ambiente de execução JavaScript assíncrono e orientado a eventos” que utiliza um “modelo de I/O não bloqueante”. Na prática isso quer dizer muitas coisas, e a arquitetura do Node.js é uma que vale estudar, mas podemos resumir em “não bloqueie o event loop” porque essa é a estratégia para que ele seja não bloqueante e orientado a eventos por padrão.

O event loop nada mais é do que a thread principal, e devemos mantê-la livre de processos pesados para que ela se mantenha performática e segura. Em vez disso, enviamos tarefas pesadas para outras threads e lidamos com o resultado de forma assíncrona, através de eventos.

Por isso, para garantirmos nunca bloquear a thread principal, que tantas APIs nativas são assíncronas por padrão, e assim deve ser com as SDKs e bibliotecas que utilizamos. Se não é assíncrono por padrão, torne-a. Não bloqueie o Event Loop :)

Alguns recursos da própria documentação para se aprofundar nesse tema e na arquitetura do Node.js:


Conclusão

Não existe bala de prata, como tudo na tecnologia e na vida. Ambos são recursos poderosos, assim como o próprio padrão callback e o ideal é entender os dois para definir o mais adequado ao contexto — afinal foram criados em contextos diferentes para resolver problemas diferentes.

Quando estamos falando de tratamento de erros, legibilidade e desempenho, o async/await performa melhor, mas apenas isso não o torna a solução ideal.

Para se aprofundar nesse tema você pode ler mais nos materiais que eu utilizei como base para escrever:


Gratíssima a você por ler até aqui. Espero que esse conteúdo tenha agregado de alguma forma. 🤗

Se quiser conversar sobre o tema você pode me enviar um e-mail para myreli@duck.com, deixar uma mensagem no Guestbook ou ainda um agradecimento.

Complexidade Cognitiva: quão simples é compreender seu código?

#Refactoring #Complexity #CleanCode


Em construção!

Hoje durante uma reunião de retrospectiva da Sprint estava discutindo com o time maneiras de medir a qualidade do código. Como utilizamos o Sonar, estávamos buscando a conhecida métrica de Complexidade Ciclomática dentre as mapeadas pela própria ferramenta para exemplificar.

Surpreendentemente (para mim) a métrica não existia. Em vez disso, Complexidade Cognitiva era exibida nos resultados da busca. 

Buscando um pouco mais sobre, me deparei com o relatório Complexidade Cognitiva: uma nova maneira de medir compreensibilidade do próprio Sonar, um material que apresenta a nova métrica como uma alternativa à anterior.

Essa resenha¹ busca trazer uma introdução opinativa do tema, bem como resumir o porquê a nova métrica foi criada e que problemas são resolvidos.


Recursos


¹ Diferente de um resumo, que é descritivo e imparcial, resenhas também contém opinião e crítica pessoais.

O estado-da-arte do Bluetooth para comunicação em 2021

Tecnologia sem fio está mais em pauta do que nunca com a migração para trabalho remoto e popularização dos Earbuds TWS (leia-se airpods e similares). No entanto, a tecnologia não avançou o suficiente na qualidade de áudio em geral mas, principalmente, na qualidade de comunicação.

Parafraseando um cliente que avaliou um fone bluetooth de uma marca famosa na Amazon:

Em vez de "plug and play" recebemos "buy and pray".

TL;DR

Esse é um resumo em tópicos, pule-o se não quiser spoilers e preferir ler minhas divagações por completo.

  • Fiquei indignada com Headsets, Bluetooth e áudio horrível nas ligações
  • O problema não é seu fone/computador/sistema operacional
  • Não existe nenhum headset bluetooth puro sem essa limitação atualmente
  • A solução pode estar a caminho com os novos dispositivos BT 5.2 com suporte ao codec LC3
  • Headsets com software e hardware proprietário são uma opção, como Jabra
  • Headsets que utilizam a tecnologia wireless 2.4ghz com receptor próprio são outra opção, como Corsair ou Razer
  • Descobri que gosto de testar headsets

Por que o som dos headsets sem fio bluetooth é tão horrível durante ligações?

Além de dias sombrios, a pandemia trouxe outro problema: com o despreparo das empresas para o trabalho remoto e a comunicação assíncrona, começaram a surgir infinitas reuniões que resultam em horas e horas utilizando fones de ouvido e causando uma dor de cabeça imensa diariamente.

Isso me fez decidir deixar de lado meu velho amigo com fio e buscar alternativas que me dessem mais mobilidade e fossem mais leves para aguentar um dia inteiro de trabalho.

Com tantas pessoas na rua utilizando fones sem fio como acessorios tive a ilusão de que seria super simples.

Não foi.

Então vou tornar pública uma obsessão recente minha: por que, em 2021, ainda não é possível fazer uma chamada de vídeo com um fone sem fio sem parecer o áudio de um rádio dos anos 90?

Jornada

Essa publicação é uma forma de auto-ajuda =D. Comecei a ficar tão obcecada e impressionada com o assunto que, quando percebi, estava compilando uma implementação customizada de Bluetooth para configurar meu ambiente de desenvolvimento. Assim começou essa nota e por isso vou registrar aqui tudo que aprendi após descobrir que fones bluetooth são terríveis para chamadas.

No fim, desisti de resolver o problema definitivamente e decidi aguardar os próximos capítulos, mas descobri um novo hobby: avaliar a qualidade de headsets. Combina exatamente duas coisas que gosto muito - música (a parte teórica e física) e tecnologia. Quem sabe começo a escrever review de uns dispositivos por aí.

Comecei buscando, furiosamente, a origem do problema. Inicialmente achei que poderia ser algo específico com hardware, sistema operacional, versão do Bluetooth, etc. Tudo isso é relevante, mas nenhum deles era o problema real.

Problema

Tecnicamente, qual é o problema?

Quando conectei meus fones e comecei a testar notei que o áudio e os controles funcionavam perfeitamente ouvindo música mas, quando eu mudava para uma ligação, o áudio ficava horrível e o microfone também. Geralmente os controles também pioravam - funcionavam de forma limitada ou não respondiam.

Se você jogar imediatamente esse problema no Google bastante gente menciona que o problema é no Ubuntu/Linux (spoiler: não é).

Quando estamos ouvindo música (ou seja, audio sendo transmitido de forma unidirecional) o perfil Bluetooth utilizado é o A2DP, com qualidade quase HD. Já quando estamos em uma chamada (ou seja, audio sendo recebido e enviado, transmitido de forma bidirecional) o perfil Bluetooth utilizado é o HSP/HFP, com qualidade quase ridícula =D.

Esse é um problema antigo e uma implementação mais antiga ainda, de quando a qualidade de áudio por telefones já era uma coisa mágica, bem distante da realidade atual. Em síntese algumas limitações são responsáveis por esse problema:

  • Só é possível utilizar um perfil de bluetooth ao mesmo tempo. Então precisamos escolher entre A2PD (áudio ótimo mas o microfone não funciona) ou HSP (áudio horrível, mas microfone funciona)
  • Apesar do Bluetooth ter quase 80 canais de transmissão, apenas dois são dedicados para transmissão de áudio. Isso significa que dois são utilizados inteiramente para a transmissão de áudio de música, mas que eles são divididos entre microfone e fone quando estamos em uma chamada
  • Utilizando os codecs padrão, enquanto o A2DP transmite 48 kHz, o HSP transmite 8~16kHz (isso significa que o áudio é, literalmente, três vezes pior)

Afinal, existe uma solução para esse problema? Sinceramente, mais ou menos. Vou explorar melhor no próximo tópico, mas a verdade é que não existe ainda uma solução definitiva, ainda que parece que ela está a caminho.

Solução

A triste verdade é que ainda não existe uma solução definitiva para o problema, mas algumas alternativas são possíveis e parece que a correção está a caminho.

#1 Semi Gambiarra: FastStream Codec e software proprietário

O objetivo do codec FastStream é habilitar comunicação bi-direcional utilizando o perfil A2P2 (ou seja, justamente resolver o problema) e parece que ele sucede relativamente bem nisso.

No entanto a solução é trabalhosa e cara: é necessário encontrar headsets específicos que suportem FastStream e adquirir um adaptador de bluetooth separado para o computador/notebook.

No Brasil existem pouquíssimos ou nenhum à venda, por isso não testei, mas gostaria; a implementação é bem curiosa e interessante.

Além disso existe a opção de software proprietário. Soluções oferecidas pela Jabra, por exemplo, trazem uma implementação própria e panteteada que promete resolver o problema. Fiquei muito inclinada e com muita vontade de experimentar o Jabra Evolve 2 65, mas o orçamento não permite e não existe revenda autorizada para pessoas físicas no Brasil.

#2 Super Gambiarra: Microfone separado

Essa é uma semi solução, mas foi a que acabei utilizando após pesar o custo-benefício. A ideia é adquirir um microfone USB de mesa e aí utilizar um fone sem fio apenas para o áudio (como os podcasters fazem).

Isso garantiu mobilidade mínima para ir beber água ou esticar as pernas sem perder o que está acontecendo, mas gera uma situação engraçada de largar a cafeteira e sair correndo para responder quando alguém chama. Além disso, a qualidade desses microfones não é perfeita para quem divide o escritório: mesmo os do tipo cardioide captam bastante som externo então precisa ficar esperto no push-to-talk ou então deixar as pessoas do outro lado sofrendo com qualquer vibração na mesa e falas de companheiros de casa.

Fora do Brasil também existem opções desse microfone apartado sem fio, que também resolveria o problema e sem atrapalhar a mobilidade. A tecnologia do microfone é excelente, os testes de qualidade de áudio dele são surpreendentes, esse seria um teste interessante e existem reviews bem promissoras, mas não tem no Brasil.

#3 Esperança: Bluetooth 5.2

A especificação da mais recente versão do Bluetooth (5.2) foi lançada recentemente e trás umas novidades muito atrativas e que mudam bastante em relação as versões anteriores.

Vale a pena ler o material completo, mas o que interessa para esse cenário é: MultiStream e o novo codec LC3.

Na teoria essas mudanças vão possibilitar que o a recepção e transmissão de áudio sejam feitas ao mesmo tempo em alta definição, mantendo os benefícios de baixo consumo de energia e com alcance e qualidade ainda maiores e mais estáveis.

Mas bluetooth é a única opção sem fio?

Não!

Existem opções de headsets sem fio que utilizam a tecnologia sem fio 2.4ghz e não sofre com os problemas do Bluetooth. Obviamente que nada é um mar de rosa e ela sofre com vários outros problemas, um deles, sendo o preço bem mais caro e a mobilidade menor já que na maioria das vezes o alcance é menor (30m do Bluetooth contra a média de 12m dos Wireless).

Essa tecnologia é geralmente encontrada em Headsets Gamer, que podem ser uma opção para quem está buscando conforto, bom áudio e bom microfone. Atualmente estou de olho em dois:

Vale ressaltar que se você curte muito ouvir música em alta definição nenhuma tecnologia sem fio ainda chega na mesma definição que fones com fio. A maioria das pessoas não vai notar a diferença, mas algumas pessoas sim.

Referências

LocalStack

Como simular os recursos da AWS localmente com LocalStack

Uma abordagem para desenvolvimento e testes de aplicações cloud em ambiente local: rápida, sem custo e offline.

Neste artigo vamos estudar uma abordagem para o desenvolvimento de aplicações que utilizem AWS, para desenvolvimento e testes em Integração Contínua. Ao fim, queremos pode executar os principais serviços da AWS (com suas APIs oficiais) utilizando Docker.

Isso nos permite um ciclo de desenvolvimento mais rápido e eficiente, além de conseguir incluir os serviços da AWS em nossa pipeline.

Diagram By LocalStack

Requisitos

Não precisa de muita coisa para seguir este tutorial além de pouca familiridade com docker e AWS, mas são necessárias algumas instalações com, no mínimo, as seguintes versões:

Requisitos

As dores de cabeça que surgiram com a computação em nuvem

A computação em nuvem trouxe muitas mudanças e benefícios para o ciclo de desenvolvimento - menos preocupação com infraestrutura e mais preocupação com código. Mas esses benefícios chegaram a um custo para os desenvolvedores: agora não é mais possível ter um ambiente de desenvolvimento completo e isolado.

Enquanto cloud-based development não é popularizado, desenvolvedores de aplicações em nuvem trabalham em um ambiente hibrido - utilizando recursos cloud da AWS, por exemplo, é comum alocar uma infraestrutura completa de "desenvolvimento" para experimentar os serviços e permitir que os devs trabalhem.

Essa abordagem tem diversos pontos negativos, mas vou destacar dois principais: custos e limitações.

Por parte do custo, não existe uma versão gratuita dos serviços cloud para desenvolvimento, toda a utilização de recursos é cobrada e isso inclui testes, desenvolvimento, pipeline, experimentação, POCs e etc. Diferente de trabalhar em um cenário on-premise em que tudo é executado de forma local. Isso significa que testes de integração ficam prejudicados e limitados as bibliotecas de testes que tentam simular aquele ambiente.

Já na parte das limitações - além da latência e configurações de rede no caso de VPCs - muita liberdade do time de desenvolvimento é perdida, visto que eles estão atuando em um ambiente real que tem impacto imediato na conta e nos outros desenvolvedores. Em um time diverso, com pessoas com muita ou pouca experiência em cloud o ambiente cloud se torna uma dor de cabeça para quem utiliza e para quem paga a conta.

  1. Setup
  2. Provisionar/Utilizar recursos de forma transparente com aws-cli
  3. Utilização (desenvolvimento e testes de integração em CI)

LocalStack FTW: trazendo a nuvem para o ambiente de desenvolvimento

LocalStack surgiu para resolver justamente este problema: executando de forma local (Docker), podemos ter todos os serviços da AWS disponíveis e fazer a bagunça que quisermos - offline, sem custos, sem limitações e sem atrapalhar os colegas. Por ser eficiente e portátil permite que o desenvolvimento utilize todo o poder da nuvem em um container.

It works on my machine... Docker Meme By ProgrammerHumor

O conceito é simples: ter um ambiente completamente funcional da AWS sendo executado de forma local, respeitando as APIs oficiais e sendo o mais transparente possível. Além disso, LocalStack simula erros reais da AWS e executa os serviços de forma totalmente desacoplada.

Instalando e Executando LocalStack

Dado todo esse contexto, agora veremos como instalar e executar os serviços AWS de fato com LocalStack. E é bem simples, instala o pacote localstack pelo pip e então executa com o comando start:

Requisitos

Se você digitar docker ps vai ver que temos o container executando a partir da imagem localstack/localstack expondo a porta 4566.

E com isso, temos a AWS executando em nossa máquina.

Para utilizar os recursos, podemos utilizar as próprias ferramentas da AWS para interagir, alterando apenas as configurações de perfil. Eu criei um perfil "localstack", mas pode ser o global, ou qualquer outra nomenclatura.

Configuração

E agora, se você está acostumado com a AWS e digitar algum comando, por exemplo aws lambda list-functions, verá um erro porque não temos credenciais válidas da AWS.

Então, para conseguir executar os serviços, precisamos sobreescrever o endpoint padrão da AWS, utilizando o nosso local:

Execução

Exemplo de provisionamento e utilização de uma fila com SQS

Agora podemos utilizar todos os recursos da AWS pelo CLI ou pelas SDKs. Para o nosso caso de exemplo, vamos criar uma fila de pedidos a serem atendidos.

A primeira etapa é criar e provisionar a fila e, assim como ocorre em um ambiente real da AWS, podemos fazer dessa forma:

Criar fila no SQS

Agora, vamos simular que dois pedidos diferentes foram feitos e vamos enviar um por um para a fila:

[
    {
        "id": "ORDER#0001",
        "customer": "Myreli",
        "items": ["Fries", "Chocolate Shake"]
    },
        {
        "id": "ORDER#0002",
        "customer": "William",
        "items": ["Cheeseburger Combo Meal"]
    }
]

Publicar na fila no SQS

E por último, vamos consumir essas mensagens da fila, assim como faríamos de uma fila na infraestrutura da AWS:

Consumir mensagem da fila no SQS

Isso encerra o exemplo do uso de SQS com LocalStack, utilizando a ferramenta oficial da AWS para tal.

Para expandir o exemplo, o mesmo pode ser feito com uma SDK oficial em Node ou Kotlin, por exemplo. E aí basta configurar quando sobreescrever a URL original - na pipeline de CI e no ambiente de desenvolvimento.

Em Java, por exemplo, a implementação seria bem simples:

// Builder em ambiente Cloud
SqsClient.builder()
    .region(@Region)
    .build();
// Builder em ambiente Local
SqsClient.builder()
    .region(@Region)
    .endpointOverride("http://localhost:4576")
    .build();

Próximos Passos

Isso conclui a introdução ao LocalStack e já permite que seja implantado em projetos reais, mas é só uma ponta da ferramenta. Recomendo explorar o repositório oficial para otimizar o fluxo de trabalho.

Além disso, é um bom exercício para praticar implementar exatamente o mesmo exemplo apresentado aqui com alguma SDK oficial.