惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
News and Events Feed by Topic
F
Fortinet All Blogs
J
Java Code Geeks
GbyAI
GbyAI
Engineering at Meta
Engineering at Meta
P
Privacy International News Feed
V
Visual Studio Blog
Martin Fowler
Martin Fowler
博客园 - 司徒正美
A
Arctic Wolf
M
MIT News - Artificial intelligence
Blog — PlanetScale
Blog — PlanetScale
U
Unit 42
Scott Helme
Scott Helme
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
P
Proofpoint News Feed
P
Palo Alto Networks Blog
S
Securelist
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
T
Threatpost
S
SegmentFault 最新的问题
AWS News Blog
AWS News Blog
MongoDB | Blog
MongoDB | Blog
Apple Machine Learning Research
Apple Machine Learning Research
K
Kaspersky official blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
T
The Exploit Database - CXSecurity.com
Google DeepMind News
Google DeepMind News
A
About on SuperTechFans
月光博客
月光博客
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
C
CXSECURITY Database RSS Feed - CXSecurity.com
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
NISL@THU
NISL@THU
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Latest news
Latest news
Y
Y Combinator Blog
PCI Perspectives
PCI Perspectives
S
Security Affairs
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
B
Blog RSS Feed
O
OpenAI News
Google Online Security Blog
Google Online Security Blog
The Hacker News
The Hacker News
博客园 - Franky
Attack and Defense Labs
Attack and Defense Labs
Hugging Face - Blog
Hugging Face - Blog
N
Netflix TechBlog - Medium
The GitHub Blog
The GitHub Blog
T
Troy Hunt's Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Chat en tiempo real con Fitz: WebSockets tipados y AsyncAPI auto-generado
Martin Palopoli · 2026-06-17 · via DEV Community

Sumá un dashboard de clicks en vivo al acortador de URLs de la Parte 2 usando @ws("/path"), WsConn<T> y el schema AsyncAPI 3.0 que Fitz genera automáticamente. Misma auth, mismos types, mismo binario.

La promesa

En la Parte 2 construimos un acortador de URLs: endpoints HTTP, ORM Postgres, auth JWT, binario nativo. Real y shippeable, pero cada interacción es request-response. Refrescá la página de stats para ver el click nuevo. Como 1998 otra vez.

Hoy lo hacemos vivo. Cuando alguien clickea un URL corto:

  1. El handler HTTP sigue redirigiendo e incrementando el counter (igual que antes).
  2. Un WebSocket suscrito a /dashboard recibe un mensaje tipado con el evento de click.
  3. El dashboard se actualiza sin polling.

El diff completo contra el código de la Parte 2: ~40 líneas. Sin nuevos installs de librería. Sin paquete websockets, sin server socketio, sin redis para pub/sub. Solo @ws sobre una función.

El modelo de WebSocket tipado

type ClickEvent {
    code: Str,
    target_url: Str,
    timestamp: Str,
}

@authenticated
@ws("/dashboard")
async fn dashboard(conn: WsConn<ClickEvent>, user: User) {
    log.info("dashboard.connected", { user_email: user.email })
    loop {
        let msg = match conn.recv() {
            Ok(m) => m,
            Err(_) => break,   // cliente desconectó
        }
        // Por ahora solo hacemos echo. En un dashboard real ignoraríamos
        // los incoming y solo pusheríamos out — `broadcast` está abajo.
        conn.send(msg)
    }
}

Tres cosas en un decorador:

  • @ws("/dashboard") — registrar un endpoint WebSocket en ese path.
  • WsConn<ClickEvent> — la conexión tipada. Cada frame in o out se marshallea como ClickEvent.
  • @authenticated — la auth corre antes del upgrade WebSocket. Token malo → 401, sin socket abierto, sin recursos de red gastados.

@ws entiende el auth_provider de la Parte 2. La misma función de verificación de JWT gatea el dashboard.

Auto-marshalling, en ambas direcciones

La deserialización del body HTTP de la Parte 1 — JSON type-checked, defaults aplicados, fields faltantes detectados, extras rechazados — también funciona para frames WebSocket.

Cuando el dashboard envía un frame, Fitz serializa el ClickEvent a JSON y lo envía. Cuando un frame llega, Fitz deserializa el JSON en ClickEvent y valida. Si un frame está malformado, el recv() retorna Err.

No escribís ni una sola llamada a json.dumps/json.loads. El compilador lo hizo.

En contraste: el loop típico de WebSocket en Python:

# server típico de FastAPI / websockets
@app.websocket("/dashboard")
async def dashboard(websocket: WebSocket):
    await websocket.accept()
    while True:
        try:
            data = await websocket.receive_text()
            msg = ClickEvent.model_validate_json(data)  # pydantic
        except WebSocketDisconnect:
            break
        except ValidationError as e:
            await websocket.send_text(json.dumps({"error": str(e)}))
            continue
        await websocket.send_text(msg.model_dump_json())

En Fitz:

@ws("/dashboard")
async fn dashboard(conn: WsConn<ClickEvent>) {
    loop {
        let msg = match conn.recv() {
            Ok(m) => m,
            Err(_) => break,
        }
        conn.send(msg)
    }
}

La versión Python tiene la misma lógica pero tenés que deletrearla. La versión Fitz tiene la lógica en el type.

Broadcasteando clicks en vivo

Ahora cableamos el evento de click del redirect HTTP al dashboard. El truco: broadcast sobre un WsConn<T> envía a cada conexión sobre el mismo endpoint, no solo a la que lo llamó.

@get("/{code}")
async fn redirect(db: DbConn, code: Str) -> Result<HttpResponse> {
    let link: Link = match Link.where(fn(l) => l.code == code).first(db).await {
        Ok(l) => l,
        Err(_) => return Err("not found"),
    }
    // Igual que la Parte 2: spawnear un increment background.
    spawn(increment_clicks(db, link.id))
    // Nuevo: broadcastear el click event al dashboard.
    spawn(notify_dashboard(link.code, link.target_url))
    return Ok(redirect_to(link.target_url))
}

@background
async fn notify_dashboard(code: Str, target_url: Str) {
    let event = ClickEvent {
        code: code,
        target_url: target_url,
        timestamp: now_iso(),
    }
    // El runtime mantiene el broadcaster para `/dashboard` accesible
    // desde cualquier lugar vía el handle tipado.
    ws.broadcast("/dashboard", event)
}

El handler de redirect no cambió en shape — sigue retornando el response de redirect. Sumamos un spawn. La fn background notify_dashboard llama ws.broadcast que fanout a cada conexión actualmente suscrita a /dashboard.

El broadcast es fire-and-forget. Si no hay dashboards conectados, el call es no-op. Si hay 50 dashboards conectados, los 50 reciben el evento. El runtime maneja la lista de conexiones activas.

Heartbeat, horneado

Las conexiones WebSocket mueren silenciosamente en producción. Algún proxy decide que 60 segundos de idle es demasiado, droppea la conexión TCP, y tu cliente cree que sigue conectado. Cada librería WebSocket tiene que sumar heartbeats; en Fitz es una flag sobre el server:

@server(43929, ws_heartbeat_secs=30)
fn main() => 0

Cada 30 segundos, el runtime envía un frame Ping sobre cada conexión WebSocket. Si el cliente no responde con Pong, el runtime considera la conexión muerta y la cierra limpia. La mayoría de los proxies, incluyendo Nginx y Cloudflare, aceptan esto como "todavía vivo" y no la droppean.

Seteá ws_heartbeat_secs=0 para desactivar (default es 30).

Este es el tipo de feature que nunca te tomarías el trabajo de agregar en un proyecto chico, después pasarías un domingo debuggeando cuando se rompe en producción. Default on por la misma razón que tcp_keepalive está default on.

AsyncAPI generado automático

OpenAPI describe servicios HTTP. AsyncAPI es su hermano event-driven — mismo modelo de schema, pero para WebSockets, Kafka, MQTT, etc. Fitz genera AsyncAPI 3.0 automático, de la misma forma que genera OpenAPI para HTTP:

curl http://localhost:8080/asyncapi.json

{
  "asyncapi": "3.0.0",
  "info": { "title": "shortener", "version": "0.1.0" },
  "channels": {
    "/dashboard": {
      "messages": {
        "ClickEvent": {
          "payload": {
            "type": "object",
            "properties": {
              "code": { "type": "string" },
              "target_url": { "type": "string" },
              "timestamp": { "type": "string" }
            },
            "required": ["code", "target_url", "timestamp"]
          }
        }
      }
    }
  },
  "operations": {
    "/dashboard.receive": { ... },
    "/dashboard.send": { ... }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": { "type": "http", "scheme": "bearer", "bearerFormat": "JWT" }
    }
  }
}

Este es el API de eventos completo de tu servicio. No conozco otro lenguaje que auto-genere AsyncAPI desde código tipado. El ecosistema de AsyncAPI tiene generadores de clientes (TypeScript, Java, Python), lo que significa: dropeá el schema en studio.asyncapi.com o asyncapi generate, conseguí un cliente tipado para tu front-end.

Si marcás un server con @server(docs=false), ni OpenAPI ni AsyncAPI se exponen. Los defaults están on porque el costo es chico y el valor es grande.

Un cliente browser mínimo

Un cliente vanilla JS de 30 líneas para probar el dashboard:

<!doctype html>
<input id="token" placeholder="pegá JWT token">
<button id="connect">Conectar</button>
<ul id="events"></ul>
<script>
document.getElementById("connect").onclick = () => {
    const token = document.getElementById("token").value
    const ws = new WebSocket("ws://localhost:8080/dashboard", [], {
        headers: { Authorization: `Bearer ${token}` }
    })
    // Nota: los constructores de WebSocket del browser no aceptan headers
    // custom directamente. En producción, pondrías el token en un query
    // param (?token=...) y harías que el auth_provider lo lea de ahí.
    ws.onmessage = (e) => {
        const event = JSON.parse(e.data)
        const li = document.createElement("li")
        li.textContent = `${event.timestamp}${event.code}${event.target_url}`
        document.getElementById("events").prepend(li)
    }
    ws.onerror = (e) => console.error("ws error", e)
    ws.onclose = () => console.log("desconectado")
}
</script>

Poné esto en un archivo, abrí en el browser, pegá un JWT, clickeá el botón connect. Después en otra terminal:

# Conseguí un token (del POST /login de la Parte 2)
TOKEN=$(curl -s localhost:8080/login -X POST -H 'content-type: application/json' \
    -d '{"email":"ada@example.com","password":"secret-ada-123"}' | jq -r .token)

# Creá un URL corto
curl -X POST localhost:8080/shorten -H "Authorization: Bearer $TOKEN" \
    -H 'content-type: application/json' \
    -d '{"target_url":"https://github.com/Thegreekman76/fitz"}'

# Clickeá el URL corto — la lista `events` del browser se actualiza al instante
curl -I localhost:8080/abc123

El <ul> del dashboard se popula con el click event en el momento que el redirect ocurre. El browser tenía abierto un WebSocket; el click event se broadcasteó; el callback JS lo renderizó. En vivo.

Limitaciones y trade-offs

Honestamente:

  • Un tipo por endpoint. WsConn<ClickEvent> significa que cada frame in y out es ClickEvent. Si necesitás que ambas direcciones sean tipos distintos (In = ChatMessage, Out = ServerEvent), el workaround es hacer el tipo una suma (estilo union) — declarar un tipo Event más ancho con fields opcionales. La solución más limpia (WsConn<In, Out> generic de dos tipos) está en el roadmap.
  • Sin rooms / channels adentro de un endpoint. broadcast va a cada conexión sobre el endpoint. Si querés "broadcastear solo a users suscritos al proyecto 42", mantenés un Map<Int, Vec<WsConn>> vos o partís en múltiples endpoints (uno por proyecto — funciona bien para counts bajos).
  • Sin reconnect con replay de estado. Si el dashboard se desconecta, al reconectar ve solo eventos nuevos — no hay "dame los últimos 30 segundos que me perdí". Construir eso necesita un event log (una tabla Postgres poleada, o Redis Streams). Afuera de la capa WebSocket.
  • Frames binarios son soportados vía WsConn<Bytes>, pero no hablé de ellos acá. Útiles para uploads de archivos o audio streaming; el AsyncAPI emite format: binary.

Estos son los gaps honestos. El 90% de los casos de uso de WebSocket (updates en tiempo real, chat, dashboards en vivo, edición colaborativa multi-user para un solo documento) están cubiertos hoy.

Cómo esto compone con el resto de Fitz

Podés mezclar WebSockets con todo lo demás:

  • Auth: @authenticated/@admin/@requires("role") funcionan sobre @ws, evaluados antes del upgrade.
  • Middleware: middlewares corren antes del upgrade para cosas como rate limiting por IP.
  • ORM: el handler WebSocket puede tomar un DbConn y queryear la base de datos.
  • Async: recv/send/broadcast son awaiteables; combinalos con llamadas HTTP o queries Postgres adentro del loop.
  • Cron / spawn: jobs cron pueden ws.broadcast("/topic", event) para pushear updates periódicos.
  • OpenTelemetry: el chequeo de auth antes del upgrade emite un trace span; broadcasts subsecuentes también pueden trackearse.

Mismo lenguaje, mismos types, mismo binario. El WebSocket no es un mundo separado.

Qué necesitarías después para un producto real

El dashboard de arriba alcanza para demo "clicks de URL shortener en tiempo real". Para un producto en producción extenderías con:

  • Filtrado por usuario: solo pushear clicks para URLs que el user creó. Trivial — chequeá user.email == link.user_email antes de broadcastear, o partí en un endpoint por user con el email en el path.
  • Agregación: no enviar cada click; enviar una rate de clicks por código por segundo. Mantené estado en el handler, enviá frames agregados cada 1 segundo.
  • Integración con framework de frontend: tipos TypeScript desde el schema AsyncAPI. Corré asyncapi generate una vez.

Nada de esto cambia la estructura del código Fitz. Son todos "editá el call site del broadcast".

Probalo

# Linux / macOS / WSL
curl -sSf https://thegreekman76.github.io/fitz/install.sh | sh

# Windows (PowerShell)
irm https://thegreekman76.github.io/fitz/install.ps1 | iex

# Reabrí la terminal, después:
git clone https://github.com/Thegreekman76/fitz.git
cd fitz/boilerplates/api-websocket

# Leé el README, corré con docker compose o `fitz dev`
docker compose up

Para VSCode (recomendado — hover sobre WsConn<T>, autocomplete en conn.send/recv/broadcast): bajá el fitz-lang-<plataforma>.vsix desde la página de releases y code --install-extension fitz-lang-<plataforma>.vsix --force. El Language Server viene incluido.

El boilerplate api-websocket es un servidor de chat tipado. Más o menos el mismo shape que el dashboard de arriba — loop { recv; broadcast } con auth.

Para el shortener completo + dashboard en vivo, el boilerplate api-orm-full tiene la cosa entera ensamblada (rutas HTTP + ORM + dashboard WebSocket + cron job + auth JWT) en ~250 líneas total.

Repo: github.com/Thegreekman76/fitz
Boilerplate api-websocket: github.com/Thegreekman76/fitz/tree/main/boilerplates/api-websocket
Boilerplate api-orm-full (la cosa entera): github.com/Thegreekman76/fitz/tree/main/boilerplates/api-orm-full
Docs y curso: thegreekman76.github.io/fitz
Capítulo de la guía sobre WebSockets: thegreekman76.github.io/fitz/guide/#29-websockets
Roadmap: docs/roadmap.md
CHANGELOG: CHANGELOG.md
Issues: github.com/Thegreekman76/fitz/issues

Si construís algo con esto, escribime. Lo quiero ver.

Hasta la próxima.