GenHTTP Lambda

Como funciona

Escreves um snippet de C#. Aquilo que ele devolve fica alojado num endereço público, com HTTPS, em poucos segundos. Aqui está tudo, pela ordem em que o vais encontrar.

O que é uma lambda

Uma lambda é um snippet que devolve um handler do GenHTTP. A plataforma compila-o, carrega-o e monta o que ele devolveu no teu próprio endereço. Não há projeto, nem ficheiro de build, nem instruções using. Todos os módulos do GenHTTP já vêm importados.

return Content.From(Resource.FromString("hello"));

Isto é uma lambda completa. Publicada em /lambda/your-key/, responde a todos os pedidos com a palavra hello.

O snippet é feito de instruções, não de uma classe. A última coisa que faz é devolver algo que consiga servir pedidos: um handler, ou um builder de um handler.

A tua primeira lambda

  1. 1
    Clica em Criar a minha lambda. Recebes um endereço público e uma chave de edição. A chave é a única forma de voltares a entrar, por isso guarda-a. Ninguém a consegue recuperar por ti.
  2. 2
    Vais parar ao painel de controlo, com um pequeno serviço REST já escrito como primeira versão. É só um ponto de partida.
  3. 3
    Dá a chave de edição a um agente e diz-lhe o que deve criar: ele escreve novas versões via MCP. Ou abre Código e escreve-o tu: Verificar compila sem guardar nada e diz-te o que o compilador acha, com ficheiro e linha.
  4. 4
    Clica em Fazer deploy. Agora está online. Antes disso não há nada acessível, e cada novo deploy prolonga o tempo que fica online.

O painel de controlo

O link de edição abre um painel de controlo em vez de uma caixa de texto: aqui, a maior parte do código é escrita por agentes, por isso a primeira coisa no ecrã é como está a tua lambda. A barra lateral tem a lambda (se está online, o endereço e um botão quando há uma versão mais recente à espera de ficar online) e as secções. O que se faz raramente, como mudar o endereço ou eliminá-la, está no menu ⋯ que lá encontras.

Visão geral
O que é a app, se está online, quantos pedidos teve hoje e quantos falharam, a última alteração e quanto espaço ainda sobra.
Documentação
O que é a app, para quem é e porquê, e porque está construída desta forma. Escrita pelos agentes e guardada com cada versão.
Alterar
Diz o que deve ficar diferente e o agente deste servidor trata disso enquanto acompanhas. Experimenta a alteração num rascunho - uma cópia com um endereço próprio - e põe-na online quando funcionar. Desliga Pôr online quando terminar para experimentares tu o rascunho primeiro. Só trabalha na tua app: um pedido que não tenha a ver com ela, ou que sirva para causar dano, é recusado, e ele diz porquê.
Rascunhos
Alterações a ser experimentadas antes de irem online, cada uma num endereço próprio e com dados de teste próprios. Aberto, um rascunho tem o seu próprio código, dados de teste e logs. A secção existe assim que há um rascunho.
Ficheiros
Os ficheiros de uma versão: o código e os assets, o próprio programa. Um cadeado ou um globo indica se o público lhes consegue aceder.
Dados
O que a lambda guarda enquanto corre, partilhado por todas as versões: a base de dados, o workspace e as chaves e palavras-passe, cada um no seu separador. Vê as tabelas e os ficheiros, carrega ficheiros, define chaves e palavras-passe ou liga e desliga um tipo. A vista simples mostra-o assim que a app guarda alguma coisa.
Versões
O que cada versão mudou, o que foi pedido e a diferença para a anterior. Faz deploy ou reverte a partir daqui, ou começa um rascunho a partir de qualquer uma delas.
Deploys
O que esteve online e quando, e o que o pôs offline.
Estatísticas
Pedidos, falhas, tempos de resposta e os caminhos mais pedidos, na última hora ou nas últimas 24 horas.
Logs
Os pedidos, o que a lambda escreveu na consola e o stack trace de tudo o que correu mal, em tempo real.
Código
Para o escrever à mão. Verificar compila, Guardar cria uma versão, Fazer deploy põe-na online. Num rascunho, Guardar mantém-no no rascunho e mostra-o no endereço do rascunho.Ctrl-S guarda; F12 vai para a declaração.
Testes
Como a app é testada automaticamente, com os scripts e os dados de teste para isso. Só na vista completa.

Todas as secções funcionam da mesma forma: o título, um ⓘ que a explica, as ações à direita e, quando há mais do que uma vista, uma fila de separadores por baixo. No código, os separadores são os ficheiros. A vista completa junta as secções em grupos: como as pessoas encontram a app, onde se faz uma alteração, o programa e os seus dados, e como corre.

O tráfego e os logs ficam em memória, para acompanhar e não para guardar: um reinício do servidor põe-nos a zero. As versões e o histórico de deploys ficam guardados.

Explicar o porquê

Uma versão é o código e, se quiseres, duas notas sobre ele: a especificação, o que o utilizador quer e porquê, com as palavras dele sempre que possível, e a alteração, uma linha sobre o que a versão faz. Aparecem ao lado do diff no histórico de versões, por isso o porquê fica junto do quê: para ti e para o próximo agente que ler o histórico antes de mudar alguma coisa.

POST /api/v1/lambdas/{editorKey}/versions
{
  "files": [ { "name": "lambda.cs", "code": "..." } ],
  "specification": "Um livro de visitas que as pessoas podem assinar; as entradas têm de sobreviver a um reinício",
  "change": "Guarda as entradas na base de dados para sobreviverem a um reinício"
}

Os agentes passam os mesmos dois campos a write_code. Em Código, guardar pede-te a alteração. Ambos são opcionais: em vez de ser recusada, uma especificação longa é cortada aos 4000 caracteres, e uma alteração aos 500. Um rascunho tem as suas próprias duas notas, e a versão em que é integrado fica com elas.

Documentação e testes

Cada versão guarda, ao lado do programa, o que está escrito sobre ela: a sua documentação (o que é a app, para quem é e porquê, e porque está construída desta forma) e os seus testes: como verificar automaticamente que funciona, com os scripts e os dados de teste para isso. Os agentes escrevem-nos com uma nova lambda e mantêm-nos atualizados a cada alteração. O próximo agente a alterar a lambda lê-os primeiro, para saber para que serve a app e o que tem de continuar a funcionar, algo que o código, por si só, não diz.

.lambda/docs/product.md
o que é a app, para quem é, o que as pessoas fazem com ela e porquê
.lambda/docs/decisions.md
as decisões técnicas, e porque foram tomadas
.lambda/tests/README.md
como a app é testada automaticamente, e como correr os testes
.lambda/tests/…
os scripts e os dados de teste que os testes usam

São ficheiros da versão como quaisquer outros, na pasta .lambda: o histórico mostra o que uma versão mudou neles, reverter traz de volta a documentação que correspondia a essa versão, e um rascunho tem uma cópia própria, que fica online com ele. Nunca são compilados nem servidos, e contam para o espaço que os assets de uma versão podem ocupar.

No painel de controlo, Documentação mostra as páginas para ler, e Testes como a app é testada e os ficheiros ao lado; a versão escolhe-se tal como para os seus ficheiros. Também é possível editar lá uma página, o que guarda a versão seguinte. A vista simples chama à documentação Sobre e mostra apenas para que serve a app: para a corrigir, diz ao agente.

São escritos na língua que usas com o agente, para quem alterar a app a seguir, seja uma pessoa ou um agente. Não são uma cópia do código: dizem para que serve, e porquê.

Alterar com segurança

Uma versão nunca muda depois de guardada, e é isso que faz com que valha a pena guardar cada uma: qualquer uma pode ser comparada e voltar a ficar online exatamente como era. Para alterar uma lambda que as pessoas usam, começa antes um rascunho.

  1. 1
    Começa-o a partir de qualquer versão em Versões, ou deixa o agente começá-lo. É uma cópia do código, dos assets, da documentação e dos testes dessa versão, e dos dados da lambda.
  2. 2
    Altera-o as vezes que for preciso: em Código, ou pedindo ao agente. A pré-visualização responde num endereço próprio, /features/…/, com dados de teste próprios. Os visitantes da lambda não veem nada disto, e nada do que ele escreve chega aos dados da lambda.
  3. 3
    Pôr online quando estiver bem: passa a ser a próxima versão, com as suas notas, e fica online. O rascunho desaparece com isso: a pré-visualização e os dados de teste.
POST /api/v1/lambdas/{editorKey}/features
{ "name": "Ranking" }

PUT  /api/v1/lambdas/{editorKey}/features/{feature}/files?deploy=true
POST /api/v1/lambdas/{editorKey}/features/{feature}/merge
{ "deploy": true }

Podes trabalhar em vários rascunhos ao mesmo tempo. Só um que esteja atualizado com a versão mais recente pode ficar online, para que nunca desfaça uma versão guardada depois de o rascunho ter começado. Se outro ficou online primeiro, traz as alterações dele - ou pede ao agente que o faça - e marca o rascunho como atualizado. Nada fica online sozinho; é de propósito. A API chama feature a um rascunho, e merge a pô-lo online.

Mais do que um ficheiro

Os tipos não têm de ficar por baixo do código que os usa. Em Código, clica em + ao lado dos ficheiros: o novo ficheiro é compilado ao lado do snippet, no mesmo namespace, por isso não é preciso importar nada. Um nome sem extensão é tratado como C#.

lambda.cs
var shelf = new Shelf();

return Inline.Create()
             .Get(() => shelf.All())
             .Post((Book book) => shelf.Add(book));
Shelf.cs
public sealed class Shelf
{
    private readonly List<Book> _books = [];

    public IEnumerable<Book> All() => _books;

    public Book Add(Book book)
    {
        _books.Add(book);
        return book;
    }
}

public record Book(string Title, string Author);

Servir uma página

Há duas formas de servir uma página, e mais uma para o que as pessoas carregam junto dela.

Uma página, escrita no código

Serve para algo pequeno. A página faz parte do snippet.

var page = Resource.FromString("""
                              <!doctype html>
                              <title>Mine</title>
                              <h1>It works</h1>
                              """)
                   .Type(new ContentType("text/html; charset=utf-8"));

return Content.From(page);

Uma pasta de ficheiros a sério

O que queres para qualquer coisa com uma folha de estilos e um script. Os ficheiros são adicionados da mesma forma que um ficheiro C#, e servidos exatamente como foram escritos. Nada os compila.

return Layout.Create()
             .Add("api", api)
             .Add(Assets.App("site"));

Ficheiros carregados, a partir dos dados

Para o que as pessoas carregam ou a lambda cria (fotografias, documentos), servido ao lado da app. Não para as páginas da própria app: essas pertencem a uma pasta de ficheiros, onde entram nas versões juntamente com o código que precisa delas.

return Layout.Create()
             .Add("api", api)
             .Add("uploads", Workspace.Files("uploads"))
             .Add(Assets.App("site"));

Um front-end, passo a passo

A segunda, em pormenor. Todas as demos servem a página assim, a partir de uma pasta chamada web: abre demo-crud para ver uma. As demos são só de leitura; a chave de edição de cada uma é o próprio nome.

  1. 1
    Em Código, clica em + ao lado dos ficheiros e escreve site/index.html. Um nome com uma barra põe o ficheiro numa pasta; um nome com extensão é tratado como o tipo de ficheiro que indica.
  2. 2
    Adiciona site/app.css e site/app.js da mesma forma. A tua página refere-se a eles pelo nome, como em href="app.css", porque a pasta é a raiz do que é servido e não faz parte do endereço.
  3. 3
    Para o que não é texto, como uma imagem ou um tipo de letra, abre um ficheiro em site e clica no botão de carregar ao lado dos ficheiros: vai parar à mesma pasta. Um PNG não se escreve num editor de texto, por isso é por aí que entra.
  4. 4
    Em lambda.cs, serve a pasta:
    return Layout.Create().Add(Assets.App("site"));
  5. 5
    Clica em Fazer deploy. site/index.html responde em /, site/app.css em /app.css, e qualquer endereço que não corresponda a nenhum ficheiro recebe a página. Assim, um front-end com routing próprio continua a funcionar quando alguém recarrega num deep link.
  6. 6
    Junta-lhe uma API e a página já tem com quem falar:
    var api = Inline.Create().Get("notes", () => notes);
    
    return Layout.Create()
                 .Add("api", api)
                 .Add(Assets.App("site"));

Os dois sítios onde vivem os ficheiros

Uma lambda guarda ficheiros em dois sítios, e o editor mostra-os em separado: Ficheiros tem os ficheiros de uma versão (o programa) e Dados tem o workspace (o que o programa guarda). A diferença está em a quem pertencem. Os ficheiros de uma versão pertencem a essa versão; os dados pertencem à lambda, e todas as versões os partilham.

Numa versãoNos dados
o que guardao código e os assets: o programa, incluindo o front-end, e também a sua documentação e os seus testestudo o que a lambda escreve, ou que alguém carrega
quando mudanunca: uma alteração é uma nova versãono momento em que algo é escrito
um deploypõe online exatamente estes ficheirosnunca lhes toca
revertertraz de volta os ficheiros antigosnão tem efeito: são os mesmos para todas as versões
um rascunhocomeça como uma cópia delestrabalha numa cópia deles
quando desaparecemcom as versões antigas, passado o limitecom a lambda, ou quando os desligas
acedido no código comoAssetsWorkspace

Não podem ser um só sítio. Se fossem, um deploy ou apagava tudo o que a lambda escreveu entretanto, ou nunca se poderia remover nada do que ela traz. Um jogo com um ranking quer a segunda opção; a página que ele serve quer a primeira. Por isso, a página vai na versão, e o ranking nos dados.

Guardar registos

Os registos (entradas, contas, encomendas, votos) pertencem à base de dados: uma base de dados SQLite só da lambda, ligada em Dados. O código abre uma ligação com Database.GetConnection() e lê e escreve nela através do Entity Framework Core, com um contexto próprio que mapeia as tabelas:

// migrations/V1__Create_notes.sql:
//   CREATE TABLE notes (id INTEGER PRIMARY KEY, text TEXT NOT NULL);

using (var connection = Database.GetConnection())
{
    new Evolve(connection) { Locations = [Assets.Root + "migrations"] }.Migrate();
}

return Inline.Create()
             .Get("notes", () =>
             {
                 using var db = new Notes(Database.GetConnection());

                 return db.Entries.OrderBy(n => n.Id).Select(n => n.Text).ToList();
             })
             .Post("notes", (NoteInput input) =>
             {
                 using var db = new Notes(Database.GetConnection());

                 db.Entries.Add(new Note { Text = input.Text });

                 return db.SaveChanges();
             });

record NoteInput(string Text);

class Note
{
    public long Id { get; set; }

    public string Text { get; set; }
}

// maps the table the migration made, on the connection it is handed
class Notes(SqliteConnection connection) : DbContext
{
    public DbSet<Note> Entries => Set<Note>();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlite(connection, contextOwnsConnection: true);

    protected override void OnModelCreating(ModelBuilder model)
        => model.Entity<Note>().ToTable("notes");
}

As tabelas são criadas por migrações: ficheiros SQL que seguem com a versão em migrations/, aplicados por ordem pelo Evolve quando a lambda arranca – cada um uma só vez, por isso uma nova versão só corre o que é novo. Nunca alteres uma migração que já foi aplicada; uma alteração a uma tabela é o ficheiro seguinte.

Como todos os dados, a base de dados é partilhada por todas as versões, os deploys e as reversões não lhe tocam, e um rascunho trabalha sobre uma cópia dela. Em Dados vês as tabelas e o que têm; a vista simples chama-lhes registos. Transferir como projeto .NET leva-a consigo como um simples ficheiro SQLite.

Cria um contexto onde precisares dele e liberta-o quando terminares, e usa-o de forma síncrona: ToList e SaveChanges, e não ToListAsync e SaveChangesAsync. As tabelas são criadas pelas migrações, nunca pelo Entity Framework. A demo demo-crud faz tudo isto.

Guardar ficheiros

Workspace é um diretório privado onde a tua lambda pode ler e escrever: o sítio para ficheiros – fotografias que alguém carrega, um documento que ela cria, um modelo que ela lê. Os registos pertencem à base de dados, e o que se sabe sobre um ficheiro – quem o carregou, e quando – também é um registo.

// uploads go into a folder of their own, served as they are
Workspace.CreateFolder("photos");

return Layout.Create()
             .Add("photos", Workspace.Files("photos"))
             .Add("upload", Inline.Create().Post(async (Stream body) =>
             {
                 using var content = new MemoryStream();

                 await body.CopyToAsync(content);

                 Workspace.WriteBytes($"photos/{Guid.NewGuid():N}.jpg", content.ToArray());
             }));

Há também ReadBytes, WriteBytes, Delete, List, CreateFolder e Tree/Files/App para o servir. Mais nada no sistema de ficheiros é acessível.

Chaves e palavras-passe

Uma chave de API, uma palavra-passe ou um token vai para os segredos, não para o código – onde o teriam cada versão, cada transferência e quem quer que leia o histórico. O código lê um segredo pelo nome:

var weather = new System.Net.Http.HttpClient();

weather.DefaultRequestHeaders.Add("X-Api-Key", Secret.Read("WEATHER_API_KEY"));

return Inline.Create()
             .Get("today", async () => await weather.GetStringAsync("https://weather.example/today"));

Liga os segredos em Dados e define lá o valor. Depois de guardado, nunca mais é mostrado – nem a ti, nem a um agente; só o podes substituir. A lista mostra que nomes o código lê que ainda não têm valor, e a visão geral pede-os. Secret.Exists diz se um segredo está definido, para código que funciona sem ele. Como todos os dados, os segredos são partilhados por todas as versões, e um rascunho trabalha sobre uma cópia.

São guardados cifrados, com uma chave que não está na base de dados. Num projeto transferido, Secret.Read("NAME") lê a variável de ambiente NAME – os valores ficam aqui.

WebSockets

Suportados, e pensados de raiz. A demo demo-game emparelha jogadores e corre todos os jogos no servidor. A forma mais simples são três callbacks:

var room = new ConcurrentDictionary<IReactiveConnection, string>();

var socket = Websocket.Functional()
                      .OnOpen(c => { room[c] = "someone"; return ValueTask.CompletedTask; })
                      .OnMessage(async (c, text) =>
                      {
                          foreach (var other in room.Keys)
                          {
                              await other.WritePayloadAsync(text);
                          }
                      })
                      .OnClose((c, _) => { room.TryRemove(c, out _); return ValueTask.CompletedTask; });

return Layout.Create().Add("chat", socket);

Quando a página só fica à escuta (uma contagem, um feed, um resultado em direto), os server-sent events são mais simples: uma única resposta longa em que o servidor vai escrevendo e que o browser volta a ligar sozinho. A demo demo-live envia assim cada voto a todos os que estão a ver. Seja como for, é o servidor que envia o que mudou. Uma página que pergunta de novo a cada poucos segundos faz um pedido de cada vez, tenha algo mudado ou não, e mesmo assim chega atrasada.

Há uma coisa que apanha toda a gente: um browser não consegue definir headers no handshake de um WebSocket. Passa o que o handler precisa na query, onde ele o lê a partir de connection.Request.Header.Query, ou envia os segredos na primeira mensagem.

O que não podes fazer

O teu código corre num servidor partilhado, por isso parte do C# é recusada antes de compilar: iniciar processos, abrir sockets próprios, carregar assemblies, aceder ao sistema de ficheiros fora do teu workspace, e usar reflection para contornar qualquer uma destas regras. O mesmo vale para esperar por uma task com .Result ou .Wait() em vez de usar await: os pedidos correm numa thread por núcleo, e a task teria de terminar precisamente na thread que está à espera dela.

Todo o resto está lá, incluindo toda a API de módulos do GenHTTP. Se algo for recusado, ficas a saber em que linha e porquê, e não apenas que falhou.

Levar o código contigo

Transferir como projeto .NET, no editor, dá-te tudo de uma vez: uma solução que podes abrir, correr com dotnet run e guardar. Só precisa do pacote GenHTTP e traz um Dockerfile para a compilares e correres como contentor.

O teu snippet passa a ser o Project.cs, e o Program.cs serve o que ele devolve. Os teus outros ficheiros vêm exatamente como os escreveste. Workspace e Assets passam a ser duas pastas ao lado do programa, com os mesmos métodos, à parte numa pasta Platform, por isso não tens de mudar nada no teu código. Secret lê aí as variáveis de ambiente com o mesmo nome; os valores ficam cá. A documentação e os testes vêm também, em docs e tests. Database abre database/database.db, que a transferência traz com os registos que a tua app guardou.

Convém saber antes de começares: o que escreves é teu e sai daqui inteiro. Correr nesta máquina não o prende a ela.

Publicar o código

Se o que criaste pode ajudar outras pessoas, publica o código: abre Código aberto no painel de controlo, escolhe uma licença (a MIT, a não ser que queiras outra) e liga a publicação. O código ganha uma página própria entre as apps de código aberto, onde qualquer pessoa o pode ler, dar-lhe uma estrela e transferir qualquer versão como o mesmo projeto que Transferir como projeto .NET te dá, com a licença ao lado.

Todas as versões são publicadas, incluindo as anteriores, com a documentação, os testes e a alteração que cada uma fez. O que a app guarda nunca é publicado (os registos, os ficheiros que guardou, os valores das chaves e palavras-passe), nem o que pediste pelas tuas próprias palavras, nem quem usa a app. Desliga a publicação e a página desaparece; as estrelas ficam guardadas para quando a voltares a ligar.

Tudo o que está no código fica público, incluindo as versões anteriores. Uma chave ou uma palavra-passe fica junto das chaves e palavras-passe em Dados, nunca no código, publicado ou não.

Deixar um agente fazer o trabalho

Há um endpoint MCP em /mcp. Liga-lhe um agente e ele pode fazer tudo o que o editor faz: ler o guia, ler uma demo inteira, escrever ficheiros, compilá-los e fazer deploy. Por baixo, é a mesma API.

O agente explica o porquê à medida que avança (write_code recebe a especificação e a alteração) e pode ver o que publicou: read_logs responde com os pedidos recentes da lambda, o que ela escreveu na consola e o stack trace de tudo o que lançou. É assim que um agente descobre que o código funciona, em vez de o assumir. Tu vês o mesmo no painel de controlo. Escreve a documentação e os testes à medida que avança, lê-os antes de alterar o que quer que seja, e corre os testes no endereço de um rascunho antes de o pôr online. Uma página feita para ser encontrada recebe um título, uma descrição, um ícone e uma pré-visualização para quando alguém partilha o link. No fim das páginas que cria, acrescenta uma pequena linha a dizer que foram feitas com o GenHTTP Lambda; diz-lhe se preferires que não a ponha, e ele retira-a.

Saber mais →