
<feed xmlns:media="http://search.yahoo.com/mrss/" xmlns="http://www.w3.org/2005/Atom">
    <id>https://jcalderita.com/es/blog/feed.xml</id>
    <title>Jorge Calderita's Blog</title>
    <author>
        <name>Jorge Calderita</name>
    </author>
    <link href="https://jcalderita.com" rel="self"></link>
    <updated>2026-04-17T19:07:36Z</updated>
    <entry>
        <id>https://jcalderita.com/es/blog/astro-to-saga/</id>
        <title>Astro to Saga</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Por qué migré mi portfolio de Astro a Saga, un generador de sitios estáticos en Swift, y cómo eliminé Node de mi stack para siempre.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;Mi Problema 🤔&lt;/h2&gt;
&lt;p&gt;Mi portfolio funcionaba sobre &lt;span class="high"&gt;Astro&lt;/span&gt; con &lt;span class="high"&gt;Bun&lt;/span&gt;. Todo iba bien. Rápido, cómodo, sin quejas técnicas.&lt;/p&gt;
&lt;p&gt;Pero había algo que no encajaba. Cada vez que abría el proyecto me encontraba con un &lt;span class="high"&gt;package.json&lt;/span&gt;, un &lt;span class="high"&gt;tailwind.config&lt;/span&gt;, un &lt;span class="high"&gt;astro.config&lt;/span&gt;, y una carpeta &lt;span class="high"&gt;node_modules&lt;/span&gt; con cientos de dependencias que ni sabía que existían. Esa sensación de perder el control sobre lo que hay dentro de tu propio proyecto me incomoda. Me gusta saber qué ejecuta mi código y por qué está ahí. Ya había eliminado Tailwind en una &lt;a href="/es/blog/tailwind-to-css/"&gt;migración previa a vanilla CSS&lt;/a&gt;, pero el resto del ecosistema JavaScript seguía ahí.&lt;/p&gt;
&lt;p&gt;Y al final, soy desarrollador Swift. Mi día a día es Swift. Y sin embargo, para generar mi propio portfolio dependía de un ecosistema completamente ajeno. Si alguien entraba en mi repositorio, no veía a un desarrollador Swift. Veía un proyecto JavaScript más.&lt;/p&gt;
&lt;p&gt;La pregunta era sencilla: si confío en Swift para todo lo demás, ¿por qué no confío en Swift para esto?&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Solución 🧩&lt;/h2&gt;
&lt;p&gt;Me paré a pensar qué necesitaba realmente. Mi portfolio no es una aplicación web. No tiene estado ni interactividad compleja. Es un conjunto de páginas HTML estáticas generadas a partir de Markdown. No necesito React, ni Vue, ni hidratación. Necesito algo que lea Markdown, lo transforme en HTML y lo escriba en disco.&lt;/p&gt;
&lt;p&gt;Encontré &lt;a href="https://github.com/loopwerk/Saga"&gt;Saga&lt;/a&gt;, un generador de sitios estáticos en Swift. Deliberadamente minimalista: lee archivos, aplica transformaciones, escribe HTML. Lo que no incluye, lo decides tú. Además incluye hot reload para desarrollo, algo que no esperaba encontrar en un proyecto tan pequeño. Esa filosofía me convenció más que cualquier feature list.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ventajas&lt;/th&gt;
&lt;th&gt;Desventajas&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Todo el stack en Swift — un solo lenguaje, sin cambio de contexto&lt;/td&gt;
&lt;td&gt;Ecosistema pequeño — si algo no existe, lo construyes tú&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTML verificado por el compilador gracias al DSL tipado&lt;/td&gt;
&lt;td&gt;Curva de aprendizaje con la sintaxis del DSL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pipeline de imágenes nativo sin dependencias externas&lt;/td&gt;
&lt;td&gt;Pipeline de imágenes solo funciona en macOS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node eliminado del entorno local&lt;/td&gt;
&lt;td&gt;Comunidad y documentación limitadas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Control total sobre cada dependencia del proyecto&lt;/td&gt;
&lt;td&gt;Más trabajo inicial para funcionalidades que en otros frameworks vienen de serie&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;La reflexión de fondo&lt;/h3&gt;
&lt;p&gt;La decisión no fue técnica. Fue sobre coherencia. Quiero que si alguien visita mi repositorio, vea Swift. No digo que &lt;span class="high"&gt;Astro&lt;/span&gt; sea malo — es excelente. Pero mi portfolio es mi tarjeta de presentación, y esa tarjeta tiene que hablar de mí.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Resultado 🎯&lt;/h2&gt;
&lt;p&gt;El sitio que lees ahora mismo está generado con &lt;span class="high"&gt;Saga&lt;/span&gt;, compilado con Swift, y desplegado sobre &lt;span class="high"&gt;Cloudflare Workers&lt;/span&gt; sin que Node haya intervenido en mi máquina.&lt;/p&gt;
&lt;p&gt;Lo que me sorprendió no fue el resultado técnico. Fue la sensación de coherencia. Abrir mi portfolio y ver que todo, desde la primera línea hasta el último deploy, es Swift me produce una tranquilidad difícil de explicar.&lt;/p&gt;
&lt;p&gt;Si eres desarrollador Swift y tu portfolio corre sobre Node, no digo que debas cambiar. Digo que merece la pena preguntarse por qué. Al final, la herramienta que eliges para presentarte dice algo sobre ti. Yo elegí la que me representa.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link href="https://jcalderita.com/es/blog/astro-to-saga/" rel="alternate"></link>
        <media:content url="https://jcalderita.com/static/blog/AstroToSaga.webp" medium="image"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/only-dns/</id>
        <title>OnlyDNS</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Cómo desactivar el proxy de Cloudflare y pasar a modo Solo DNS resolvió en 5 minutos el bloqueo de La Liga que arrastraba desde hacía meses.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;Mi Problema 🤔&lt;/h2&gt;
&lt;p&gt;En el &lt;a href="/es/blog/la-liga-thanks/"&gt;artículo anterior&lt;/a&gt; hablé de cómo &lt;span class="high"&gt;La Liga&lt;/span&gt; bloqueaba mi web durante los partidos. Gente intentando visitar mi portfolio y mi blog se encontraba con que la página no cargaba, simplemente porque mi dominio pasaba por la infraestructura de &lt;span class="high"&gt;Cloudflare&lt;/span&gt;, y &lt;span class="high"&gt;La Liga&lt;/span&gt; bloqueaba rangos enteros de IPs de &lt;span class="high"&gt;Cloudflare&lt;/span&gt; para frenar las retransmisiones ilegales.&lt;/p&gt;
&lt;p&gt;Lo sabía. Lo documenté. Y me quedé ahí.&lt;/p&gt;
&lt;p&gt;Durante meses no hice nada para solucionarlo. No por pereza ni por pensar que fuese complicado — sino por enfado. Mi web funcionaba perfectamente, era &lt;span class="high"&gt;La Liga&lt;/span&gt; la que aplicaba bloqueos indiscriminados y la dejaba inaccesible. No era culpa mía. Así que me planté: no tenía por qué ser yo quien moviese ficha.&lt;/p&gt;
&lt;p&gt;Así que no hice nada. Durante meses.&lt;/p&gt;
&lt;p&gt;Lo que pasa es que tenía el proxy de &lt;span class="high"&gt;Cloudflare&lt;/span&gt; activo — la famosa nube naranja. Mi web está alojada en &lt;span class="high"&gt;Cloudflare Pages&lt;/span&gt;, así que todo está dentro del ecosistema de &lt;span class="high"&gt;Cloudflare&lt;/span&gt;. Pero con el proxy activado, el tráfico pasa por la capa de CDN/proxy de &lt;span class="high"&gt;Cloudflare&lt;/span&gt;, que usa unos rangos de IPs compartidas. Y esos rangos son precisamente los que &lt;span class="high"&gt;La Liga&lt;/span&gt; bloqueaba.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Solución 🧩&lt;/h2&gt;
&lt;p&gt;La solución es desactivar el proxy de &lt;span class="high"&gt;Cloudflare&lt;/span&gt; y pasar al modo &lt;span class="high"&gt;Solo DNS&lt;/span&gt; — la nube gris.&lt;/p&gt;
&lt;p&gt;Con la nube naranja activa, &lt;span class="high"&gt;Cloudflare&lt;/span&gt; es un “centro comercial”: todos los visitantes entran por la puerta de su proxy/CDN, que usa rangos de IPs compartidas con miles de sitios. Son esas IPs las que &lt;span class="high"&gt;La Liga&lt;/span&gt; bloquea en masa para frenar las retransmisiones ilegales — y mi web queda como daño colateral.&lt;/p&gt;
&lt;p&gt;Con la nube gris, &lt;span class="high"&gt;Cloudflare&lt;/span&gt; pasa a ser solo un “director de tráfico”: resuelve el DNS y apunta directamente a &lt;span class="high"&gt;Cloudflare Pages&lt;/span&gt;. Mi web sigue alojada en &lt;span class="high"&gt;Cloudflare&lt;/span&gt;, pero el tráfico llega a través de los rangos de IPs de &lt;span class="high"&gt;Pages&lt;/span&gt; — que son distintos a los del proxy y que &lt;span class="high"&gt;La Liga&lt;/span&gt; no tiene en su lista de bloqueo.&lt;/p&gt;
&lt;p&gt;El proceso en el panel de &lt;span class="high"&gt;Cloudflare&lt;/span&gt; es este:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Entrar en el panel de &lt;span class="high"&gt;Cloudflare&lt;/span&gt; y navegar a &lt;strong&gt;DNS &amp;gt; Registros (Records)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Buscar los registros que apuntan a la web — normalmente un registro &lt;strong&gt;A&lt;/strong&gt; o &lt;strong&gt;CNAME&lt;/strong&gt; con el nombre del dominio.&lt;/li&gt;
&lt;li&gt;En la columna &lt;strong&gt;Estado del proxy&lt;/strong&gt;, hacer clic en el interruptor con la nube naranja para convertirla en una nube gris (&lt;strong&gt;Solo DNS&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Guardar los cambios.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;El cambio propaga en minutos. A partir de ese momento, el tráfico llega por una ruta distinta dentro de &lt;span class="high"&gt;Cloudflare&lt;/span&gt; — una que no está en el punto de mira de los bloqueos de &lt;span class="high"&gt;La Liga&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;Como mi web es estática y sigue alojada en &lt;span class="high"&gt;Cloudflare Pages&lt;/span&gt;, no noto prácticamente ninguna diferencia de velocidad. Pierdo algunas funciones de la capa proxy — como el firewall a nivel de CDN o la caché en sus edge nodes — pero para una web estática de portfolio esas ventajas son marginales comparadas con el beneficio de que la web funcione para todo el mundo durante los partidos.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Resultado 🎯&lt;/h2&gt;
&lt;p&gt;Mi web ya carga durante los partidos de &lt;span class="high"&gt;La Liga&lt;/span&gt;. Cualquier persona que intente visitar mi portfolio o blog mientras suena el himno de la Champions no se va a encontrar con una pantalla en blanco.&lt;/p&gt;
&lt;p&gt;Cinco minutos. Un clic. Meses de problema resuelto.&lt;/p&gt;
&lt;p&gt;La lección que me llevo no es técnica — es pragmática. Tuviese razón o no, mi problema o lo solucionaba yo o no lo iba a solucionar nadie. Y creo que tengo razón: mi web no tiene por qué ser daño colateral de los bloqueos de &lt;span class="high"&gt;La Liga&lt;/span&gt;. Gracias, &lt;span class="high"&gt;La Liga&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link href="https://jcalderita.com/es/blog/only-dns/" rel="alternate"></link>
        <media:content url="https://jcalderita.com/static/blog/OnlyDNS.webp" medium="image"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/my-first-macro/</id>
        <title>My First Macro</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Macro para generar automáticamente el ID, los timestamps y el espacio de nombres en cada modelo Fluent.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;Mi Problema 🤔&lt;/h2&gt;
&lt;p&gt;En mi proyecto tengo decenas de modelos &lt;span class="high"&gt;Fluent&lt;/span&gt;, y hay un bloque de código que se me repite en absolutamente todos ellos:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;public static let space: String? = &amp;quot;sales&amp;quot;

@ID() public var id: UUID?
public init() {}

@Timestamp(.createdAt, on: .create) public var createdAt: Date?
@Timestamp(.updatedAt, on: .update) public var updatedAt: Date?
@Timestamp(.deletedAt, on: .delete) public var deletedAt: Date?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Siempre el mismo bloque. En cada modelo. Sin excepción.&lt;/p&gt;
&lt;p&gt;El espacio de nombres cambia, pero la estructura es idéntica. Con cada modelo nuevo, copio y pego, ajusto el espacio y espero no olvidarme de nada. Con 30 modelos, ese boilerplate se convierte en ruido que me dificulta leer lo que realmente importa: la lógica del modelo.&lt;/p&gt;
&lt;p&gt;Mi solución en Swift para este tipo de problemas es una &lt;span class="high"&gt;macro&lt;/span&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Solución 🧩&lt;/h2&gt;
&lt;p&gt;Una macro de Swift puede generar miembros nuevos en una clase o struct en tiempo de compilación. Exactamente lo que necesito. El resultado que busco es escribir esto:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;@FluentModel(.sales)
public final class ProductModel: Model {
    public static let schema = &amp;quot;products&amp;quot;

    @Field(.name) public var name: String
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Y que el compilador inyecte automáticamente &lt;span class="high"&gt;space&lt;/span&gt;, &lt;span class="high"&gt;id&lt;/span&gt;, &lt;span class="high"&gt;init()&lt;/span&gt; y los tres &lt;span class="high"&gt;timestamps&lt;/span&gt;. Sin escribirlos. Sin mantenerlos.&lt;/p&gt;
&lt;h3&gt;Estructura del paquete&lt;/h3&gt;
&lt;p&gt;Las macros en Swift requieren dos targets separados: la &lt;strong&gt;interfaz pública&lt;/strong&gt; (lo que consumo como desarrollador) y el &lt;strong&gt;plugin&lt;/strong&gt; (la implementación que ejecuta el compilador). En mi caso, ambos viven en el mismo paquete &lt;span class="high"&gt;Macros&lt;/span&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span class="high"&gt;Macros&lt;/span&gt; — declara la macro y el enum &lt;span class="high"&gt;DatabaseSpace&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;MacrosPlugin&lt;/span&gt; — implementa la expansión con &lt;span class="high"&gt;SwiftSyntax&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;DatabaseSpace — los espacios como tipos&lt;/h3&gt;
&lt;p&gt;Antes de definir la macro, necesito modelar los espacios de nombres disponibles. En lugar de usar strings sueltos, decidí que el enum &lt;span class="high"&gt;DatabaseSpace&lt;/span&gt; los haga exhaustivos y seguros en tiempo de compilación:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;public enum DatabaseSpace: String, CaseIterable, Sendable {
    case sales, warehouse
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Esto permite escribir &lt;span class="high"&gt;@FluentModel(.sales)&lt;/span&gt; en lugar de &lt;span class="high"&gt;@FluentModel(“sales”)&lt;/span&gt;. Si el espacio no existe, el compilador lo dice antes de ejecutar nada.&lt;/p&gt;
&lt;h3&gt;La declaración del macro&lt;/h3&gt;
&lt;p&gt;La interfaz pública de la macro es sorprendentemente compacta:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;@attached(member, names: named(space), named(id), named(init), named(createdAt), named(updatedAt), named(deletedAt))
public macro FluentModel(_ space: DatabaseSpace? = nil) = #externalMacro(
    module: &amp;quot;MacrosPlugin&amp;quot;,
    type: &amp;quot;FluentModelMacro&amp;quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El atributo &lt;span class="high"&gt;@attached(member, names:)&lt;/span&gt; le dice al compilador dos cosas: que esta macro añade miembros a la declaración donde se aplica, y cuáles son los nombres exactos que va a generar. Declarar los nombres es obligatorio — Swift los necesita para resolver el árbol de símbolos antes de expandir la macro.&lt;/p&gt;
&lt;h3&gt;La implementación — FluentModelMacro&lt;/h3&gt;
&lt;p&gt;La implementación conforma el protocolo &lt;span class="high"&gt;MemberMacro&lt;/span&gt; y devuelve un array de &lt;span class="high"&gt;DeclSyntax&lt;/span&gt; — fragmentos de código Swift que el compilador inserta en el modelo:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;import SwiftSyntax
import SwiftSyntaxMacros

public struct FluentModelMacro: MemberMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingMembersOf declaration: some DeclGroupSyntax,
        conformingTo protocols: [TypeSyntax],
        in context: some MacroExpansionContext
    ) throws -&amp;gt; [DeclSyntax] {
        [
            spaceDecl(from: node),
            &amp;quot;@ID() public var id: UUID?&amp;quot;,
            &amp;quot;public init() {}&amp;quot;,
            &amp;quot;@Timestamp(.createdAt, on: .create) public var createdAt: Date?&amp;quot;,
            &amp;quot;@Timestamp(.updatedAt, on: .update) public var updatedAt: Date?&amp;quot;,
            &amp;quot;@Timestamp(.deletedAt, on: .delete) public var deletedAt: Date?&amp;quot;,
        ]
    }

    private static func spaceDecl(from node: AttributeSyntax) -&amp;gt; DeclSyntax {
        let value = node.arguments?.as(LabeledExprListSyntax.self)?
            .first?.expression.as(MemberAccessExprSyntax.self)
            .map { &amp;quot;\&amp;quot;\($0.declName.baseName.text)\&amp;quot;&amp;quot; } ?? &amp;quot;nil&amp;quot;
        return &amp;quot;public static let space: String? = \(raw: value)&amp;quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El método &lt;span class="high"&gt;expansion&lt;/span&gt; devuelve directamente el array de declaraciones, sin variables intermedias. Cada elemento es código Swift literal que el compilador inyecta tal cual en el modelo.&lt;/p&gt;
&lt;p&gt;La clave está en &lt;span class="high"&gt;spaceDecl&lt;/span&gt;, que encapsula toda la lógica de extracción y generación en un solo método. Navega el árbol sintáctico del atributo con &lt;span class="high"&gt;SwiftSyntax&lt;/span&gt; usando optional chaining: accede a los argumentos como &lt;span class="high"&gt;LabeledExprListSyntax&lt;/span&gt;, toma la primera expresión, la castea a &lt;span class="high"&gt;MemberAccessExprSyntax&lt;/span&gt; (porque el argumento es un caso de enum como &lt;span class="high"&gt;.sales&lt;/span&gt;) y con &lt;span class="high"&gt;.map&lt;/span&gt; lo convierte en un string entrecomillado. Si cualquier paso de la cadena falla, el operador &lt;span class="high"&gt;??&lt;/span&gt; devuelve &lt;span class="high"&gt;“nil”&lt;/span&gt;. Finalmente, &lt;span class="high"&gt;\(raw:)&lt;/span&gt; interpola el valor directamente en el &lt;span class="high"&gt;DeclSyntax&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;Por último, necesito un &lt;span class="high"&gt;CompilerPlugin&lt;/span&gt; que registre la macro — es el punto de entrada que el compilador carga para saber qué macros están disponibles:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;import SwiftCompilerPlugin
import SwiftSyntaxMacros

@main
struct MacrosPlugin: CompilerPlugin {
    let providingMacros: [Macro.Type] = [
        FluentModelMacro.self,
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Resultado 🎯&lt;/h2&gt;
&lt;p&gt;Con la macro instalada, cada modelo queda limpio y sin ruido:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;@FluentModel(.sales)
public final class ProductModel: Model {
    public static let schema = &amp;quot;products&amp;quot;

    @Field(.name) public var name: String
    @OptionalField(.description) public var description: String?
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El compilador expande &lt;span class="high"&gt;@FluentModel(.sales)&lt;/span&gt; y genera automáticamente:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;public static let space: String? = &amp;quot;sales&amp;quot;
@ID() public var id: UUID?
public init() {}
@Timestamp(.createdAt, on: .create) public var createdAt: Date?
@Timestamp(.updatedAt, on: .update) public var updatedAt: Date?
@Timestamp(.deletedAt, on: .delete) public var deletedAt: Date?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Y si un modelo no pertenece a ningún espacio de nombres — como las vistas — simplemente se omite el argumento:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;@FluentModel()
public final class OrderSummaryModel: Model {
    public static let schema = &amp;quot;order_summaries&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;En ese caso, &lt;span class="high"&gt;space&lt;/span&gt; se genera como &lt;span class="high"&gt;nil&lt;/span&gt; y Fluent ignora el prefijo de esquema.&lt;/p&gt;
&lt;p&gt;Los beneficios en números: &lt;strong&gt;32 modelos&lt;/strong&gt; con la macro aplicada. &lt;strong&gt;6 líneas eliminadas&lt;/strong&gt; por modelo. Más de &lt;strong&gt;190 líneas de boilerplate&lt;/strong&gt; que ya no existen en el repositorio y que nunca más habrá que mantener.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link href="https://jcalderita.com/es/blog/my-first-macro/" rel="alternate"></link>
        <media:content url="https://jcalderita.com/static/blog/MyFirstMacro.webp" medium="image"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/db-admin-vs-developer/</id>
        <title>DB Admin vs Dev</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Separar la administración de base de datos del desarrollo no es una opinión: es una responsabilidad.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;El problema 🤔&lt;/h2&gt;
&lt;p&gt;En muchos proyectos he visto cómo el código de la aplicación termina haciendo cosas que no le corresponden: crear roles, configurar parámetros del servidor de base de datos, gestionar permisos a nivel de instancia. Todo mezclado en el mismo sitio, como si fuera una sola responsabilidad.&lt;/p&gt;
&lt;p&gt;Pero no lo es. La base de datos tiene &lt;strong&gt;dos mundos distintos&lt;/strong&gt;: la &lt;span class="high"&gt;administración&lt;/span&gt; 🛡️ y el &lt;span class="high"&gt;desarrollo&lt;/span&gt; 💻.&lt;/p&gt;
&lt;p&gt;Mezclarlos es uno de los errores más comunes, y también uno de los más costosos 💸. Un desarrollador que configura la instancia desde su código está cruzando una frontera que no le corresponde. Y un administrador que intenta decidir qué tablas o esquemas usa la aplicación está haciendo lo mismo desde el otro lado.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;La idea 🧩&lt;/h2&gt;
&lt;p&gt;La separación es conceptualmente limpia: &lt;strong&gt;la administración pertenece al servidor, el desarrollo pertenece al código&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;El territorio del administrador 🛡️&lt;/h3&gt;
&lt;p&gt;El administrador gestiona la &lt;strong&gt;instancia&lt;/strong&gt; de la base de datos. Su trabajo incluye:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;🔧 &lt;strong&gt;Configuración del servidor&lt;/strong&gt; — parámetros de rendimiento, extensiones a nivel de instancia, ajustes que requieren reinicio&lt;/li&gt;
&lt;li&gt;👤 &lt;strong&gt;Roles y credenciales&lt;/strong&gt; — crear usuarios, asignar contraseñas seguras, definir quién puede conectarse&lt;/li&gt;
&lt;li&gt;🗄️ &lt;strong&gt;Bases de datos&lt;/strong&gt; — crearlas, establecer timeouts, revocar accesos por defecto&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Todo esto es infraestructura 🔐. Son decisiones que se toman una vez, las ejecuta alguien con permisos de superusuario, y no tienen nada que ver con la lógica de negocio. Lo ideal es que vivan como scripts SQL versionados en el repositorio, pero separados del código de la aplicación.&lt;/p&gt;
&lt;h3&gt;El territorio del desarrollador 💻&lt;/h3&gt;
&lt;p&gt;El desarrollador gestiona la &lt;strong&gt;estructura de datos de la aplicación&lt;/strong&gt;: esquemas, tablas, índices, vistas, y todo lo que necesita existir para que la aplicación funcione.&lt;/p&gt;
&lt;p&gt;Esta responsabilidad se expresa a través de &lt;span class="high"&gt;migraciones&lt;/span&gt; ✨ — piezas de código versionadas, reversibles y revisables como cualquier otro cambio. Da igual si usas Swift, Python, Go o TypeScript: el principio es el mismo. La estructura de datos de tu aplicación debe poder reproducirse automáticamente en cualquier entorno 🔄.&lt;/p&gt;
&lt;h3&gt;La zona gris: ¿admin o desarrollo? 🤷&lt;/h3&gt;
&lt;p&gt;Hay casos que parecen ambiguos. Los roles de base de datos son un buen ejemplo. El &lt;strong&gt;rol en sí&lt;/strong&gt; — con su contraseña y permisos de conexión — es un concepto de infraestructura. Lo crea el administrador 🛡️.&lt;/p&gt;
&lt;p&gt;Pero los &lt;strong&gt;permisos sobre tablas concretas&lt;/strong&gt; — qué puede leer, qué puede escribir, sobre qué esquema — eso depende del desarrollador 💻. La tabla tiene que existir antes de que pueda tener permisos, así que esos permisos van en las migraciones, no en scripts de infraestructura.&lt;/p&gt;
&lt;p&gt;La regla que yo aplico es sencilla: si el objeto existe &lt;strong&gt;antes&lt;/strong&gt; que la aplicación, es administración. Si su existencia &lt;strong&gt;depende&lt;/strong&gt; de que la aplicación lo cree, es desarrollo ✅.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;El resultado 🎯&lt;/h2&gt;
&lt;p&gt;Cuando aplicas esta separación, el proceso de puesta en marcha queda claro y reproducible:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;🛡️ El administrador prepara la instancia → servidor configurado, roles creados, base de datos lista&lt;/li&gt;
&lt;li&gt;💻 El desarrollador ejecuta las migraciones → esquemas, tablas y permisos de aplicación aplicados automáticamente&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Cada capa tiene su herramienta. El administrador no toca el código de la aplicación. El desarrollador no necesita credenciales de superusuario 🔒. Nadie pisa el territorio del otro.&lt;/p&gt;
&lt;p&gt;Esta separación también facilita el trabajo en equipo 🤝: los scripts de administración los ejecuta quien tiene acceso al servidor, una sola vez. Las migraciones las ejecuta cualquier desarrollador del equipo en su entorno local, tantas veces como necesite.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;La base de datos no es solo tuya como desarrollador. Pero la estructura de datos de tu aplicación, sí.&lt;/strong&gt; 🏗️&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link href="https://jcalderita.com/es/blog/db-admin-vs-developer/" rel="alternate"></link>
        <media:content url="https://jcalderita.com/static/blog/DBAdminVSDeveloper.webp" medium="image"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/migrate-spaces/</id>
        <title>Migrate Spaces</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Cómo crear espacios de nombres en PostgreSQL con migraciones de FluentKit en Vapor, reemplazando la configuración manual por código versionado y reversible.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;🧩 Problema&lt;/h2&gt;
&lt;p&gt;En el artículo anterior sobre &lt;a href="/es/blog/schema-space/"&gt;Esquemas y Espacios&lt;/a&gt; vimos cómo asignar un espacio de nombres a un modelo usando la propiedad &lt;span class="high"&gt;space&lt;/span&gt;. Muchos me preguntasteis lo mismo: &lt;strong&gt;¿cómo se crean esos espacios en la base de datos?&lt;/strong&gt; 🤔&lt;/p&gt;
&lt;p&gt;La respuesta más habitual era hacerlo a mano, ejecutando un &lt;span class="high"&gt;CREATE SCHEMA&lt;/span&gt; en la consola de PostgreSQL antes de lanzar las migraciones. Funciona, pero rompe algo fundamental: si la base de datos no existe o el entorno es nuevo, &lt;strong&gt;el proceso falla&lt;/strong&gt; antes de llegar a las migraciones reales 💥.&lt;/p&gt;
&lt;p&gt;El otro problema es que administrar espacios de nombres a mano no escala. En cuanto tienes varios entornos (desarrollo, staging, producción) o un equipo, mantener esa sincronía manual se convierte en una fuente de errores silenciosos 😬.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;💡 Solución&lt;/h2&gt;
&lt;p&gt;La solución es convertir la creación de espacios de nombres en una &lt;span class="high"&gt;migración&lt;/span&gt; más. De esta forma, se ejecuta automáticamente en el orden correcto, es reversible y está versionada junto al resto del código ✅.&lt;/p&gt;
&lt;p&gt;La clave está en combinar &lt;span class="high"&gt;FluentKit&lt;/span&gt; con &lt;span class="high"&gt;SQLKit&lt;/span&gt;. FluentKit gestiona el ciclo de vida de las migraciones, pero para ejecutar SQL arbitrario necesitamos acceder al driver subyacente mediante el protocolo &lt;span class="high"&gt;SQLDatabase&lt;/span&gt;. Para mantener el código organizado, encapsulamos los espacios de nombres en un enum con una acción que determina si se crean o se eliminan:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;import FluentKit
import SQLKit

enum SRSchema: String, CaseIterable {
    case account, ai, device, location, sport, task, view

    enum Action {
        case create, drop
    }

    static func execute(_ action: Action, on db: Database) async throws {
        let sql = db as! any SQLDatabase
        let template: (String) -&amp;gt; String =
            switch action {
            case .create: { &amp;quot;CREATE SCHEMA IF NOT EXISTS \($0)&amp;quot; }
            case .drop: { &amp;quot;DROP SCHEMA IF EXISTS \($0) RESTRICT&amp;quot; }
            }
        for schema in allCases {
            try await sql.raw(&amp;quot;\(unsafeRaw: template(schema.rawValue))&amp;quot;).run()
        }
    }
}

struct SchemasMigration: AsyncMigration {
    func prepare(on db: Database) async throws {
        try await SRSchema.execute(.create, on: db)
    }

    func revert(on db: Database) async throws {
        try await SRSchema.execute(.drop, on: db)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Algunos detalles importantes 📋:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SRSchema&lt;/strong&gt; como &lt;strong&gt;CaseIterable&lt;/strong&gt;: al iterar sobre &lt;span class="high"&gt;allCases&lt;/span&gt;, nos aseguramos de que todos los espacios definidos se crean sin tener que listarlos manualmente en la migración. Añadir un nuevo espacio solo requiere agregar un caso al enum 🔄.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Action&lt;/strong&gt;: el enum interno modela las dos operaciones posibles (&lt;span class="high"&gt;create&lt;/span&gt; y &lt;span class="high"&gt;drop&lt;/span&gt;), lo que permite reutilizar el mismo método &lt;span class="high"&gt;execute&lt;/span&gt; tanto en el &lt;span class="high"&gt;prepare&lt;/span&gt; como en el &lt;span class="high"&gt;revert&lt;/span&gt; de la migración ♻️.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Switch expression&lt;/strong&gt;: se usa una &lt;em&gt;switch expression&lt;/em&gt; de Swift para seleccionar el template SQL según la acción. Cada rama devuelve un closure que genera la sentencia correspondiente, manteniendo la lógica compacta y legible 🧹.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CREATE SCHEMA IF NOT EXISTS&lt;/strong&gt;: hace que la migración sea idempotente. Si el esquema ya existe, no falla, simplemente continúa 🛡️.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DROP SCHEMA … RESTRICT&lt;/strong&gt;: en el &lt;span class="high"&gt;revert&lt;/span&gt;, el modificador &lt;span class="high"&gt;RESTRICT&lt;/span&gt; impide eliminar un esquema que contenga tablas. Es una red de seguridad que evita pérdidas accidentales de datos al revertir 🔒.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;unsafeRaw&lt;/strong&gt;: se usa para interpolar el nombre del schema directamente en el SQL. Es seguro aquí porque el valor proviene de un enum propio, nunca de input externo ⚠️.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;📊 Resultado&lt;/h2&gt;
&lt;p&gt;Esta migración debe ser la &lt;strong&gt;primera&lt;/strong&gt; en registrarse ☝️. Antes de crear cualquier tabla, los espacios de nombres tienen que existir. En el runner de migraciones, el orden queda así:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;fluent.migrations.add(
    SchemasMigration()
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Al ejecutar las migraciones en un entorno nuevo, el proceso completo es automático ⚡:&lt;/p&gt;
&lt;p&gt;Y si en algún momento necesitas añadir un nuevo dominio, solo añades el caso al enum &lt;span class="high"&gt;SRSchema&lt;/span&gt; y la próxima vez que se ejecuten las migraciones, el esquema aparece. &lt;strong&gt;Sin tocar la base de datos a mano, sin documentación extra que mantener&lt;/strong&gt; 🎯.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link href="https://jcalderita.com/es/blog/migrate-spaces/" rel="alternate"></link>
        <media:content url="https://jcalderita.com/static/blog/MigrateSpaces.webp" medium="image"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/tailwind-to-css/</id>
        <title>Tailwind to CSS</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Por qué migré mi portfolio de Tailwind CSS a vanilla CSS y qué ventajas obtuve en rendimiento, peso y control total del código.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;🎨 ¿Por Qué Tocar Lo Que Funciona?&lt;/h2&gt;
&lt;p&gt;Mi portfolio llevaba meses funcionando perfectamente con &lt;span class="high"&gt;Tailwind CSS&lt;/span&gt;. Todo iba bien. Los estilos estaban en su sitio, los componentes se veían geniales y la web cargaba rápido.&lt;/p&gt;
&lt;p&gt;Entonces, ¿por qué cambiar?&lt;/p&gt;
&lt;p&gt;Porque &lt;strong&gt;funcionar bien&lt;/strong&gt; y &lt;strong&gt;funcionar de la mejor manera posible&lt;/strong&gt; son cosas distintas. Y cuando te paras a analizar qué hay debajo del capó, a veces descubres que llevas una capa que &lt;span class="high"&gt;no necesitas&lt;/span&gt; 🧅.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🤔 El Problema con Tailwind (en mi caso)&lt;/h2&gt;
&lt;p&gt;No me malinterpretéis: &lt;span class="high"&gt;Tailwind CSS&lt;/span&gt; es una herramienta &lt;strong&gt;brutal&lt;/strong&gt;. Lo he usado en proyectos profesionales y lo seguiré usando donde tenga sentido. Pero en un portfolio personal con &lt;span class="high"&gt;Astro&lt;/span&gt;, empecé a notar ciertas cosas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dependencia innecesaria&lt;/strong&gt;: 3 paquetes extra (&lt;span class="high"&gt;tailwindcss&lt;/span&gt;, &lt;span class="high"&gt;@tailwindcss/vite&lt;/span&gt;, &lt;span class="high"&gt;@tailwindcss/typography&lt;/span&gt;) para un proyecto que no los necesitaba realmente 📦&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Capa de abstracción&lt;/strong&gt;: Tailwind genera CSS a partir de clases de utilidad. Es una capa entre lo que escribes y lo que el navegador interpreta. En un proyecto pequeño, esa capa &lt;strong&gt;suma sin aportar&lt;/strong&gt; 🧱&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Peso extra&lt;/strong&gt;: El CSS generado incluía utilidades que no siempre aprovechaba al máximo. ~20KB de más que el usuario descargaba sin necesidad 📊&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Menor control&lt;/strong&gt;: Cuando quieres algo muy específico, acabas luchando contra el framework en vez de escribir directamente lo que necesitas ⚔️&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;💡 La Decisión: Vanilla CSS con Design Tokens&lt;/h2&gt;
&lt;p&gt;La idea era sencilla: &lt;strong&gt;eliminar Tailwind&lt;/strong&gt; y sustituirlo por &lt;span class="high"&gt;CSS vanilla&lt;/span&gt; con un sistema de &lt;strong&gt;design tokens&lt;/strong&gt; usando &lt;span class="high"&gt;custom properties&lt;/span&gt; de CSS.&lt;/p&gt;
&lt;p&gt;¿Qué son los design tokens? Son variables CSS que definen tu sistema de diseño:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-css"&gt;:root {
  --color-primary: oklch(0.55 0.2 260);
  --color-gray-100: oklch(0.97 0 0);
  --color-gray-900: oklch(0.21 0.006 285.75);

  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-xl: 1.25rem;

  --spacing: 0.25rem;
  --radius-lg: 0.5rem;

  --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
  --transition-colors: color 0.15s, background-color 0.15s, border-color 0.15s;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Con esto tienes &lt;strong&gt;consistencia&lt;/strong&gt; en todo el proyecto sin depender de ningún framework. Solo CSS puro que el navegador entiende &lt;span class="high"&gt;directamente&lt;/span&gt; 🎯.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🔧 La Migración&lt;/h2&gt;
&lt;p&gt;El proceso fue migrar &lt;strong&gt;38 archivos&lt;/strong&gt; en un solo commit. Cada componente &lt;span class="high"&gt;Astro&lt;/span&gt; pasó de usar clases de Tailwind a tener su propio bloque &lt;span class="high"&gt;&amp;lt;style&amp;gt;&lt;/span&gt; con estilos scoped:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Antes (Tailwind):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-astro"&gt;&amp;lt;header class=&amp;quot;flex items-center justify-between px-6 py-4 bg-white dark:bg-gray-900&amp;quot;&amp;gt;
  &amp;lt;nav class=&amp;quot;flex gap-4&amp;quot;&amp;gt;
    &amp;lt;a class=&amp;quot;text-sm font-medium text-gray-700 hover:text-blue-500&amp;quot;&amp;gt;Blog&amp;lt;/a&amp;gt;
  &amp;lt;/nav&amp;gt;
&amp;lt;/header&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Después (Vanilla CSS):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-astro"&gt;&amp;lt;header class=&amp;quot;header&amp;quot;&amp;gt;
  &amp;lt;nav class=&amp;quot;nav&amp;quot;&amp;gt;
    &amp;lt;a class=&amp;quot;nav-link&amp;quot;&amp;gt;Blog&amp;lt;/a&amp;gt;
  &amp;lt;/nav&amp;gt;
&amp;lt;/header&amp;gt;

&amp;lt;style&amp;gt;
  .header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: calc(var(--spacing) * 4) calc(var(--spacing) * 6);
    background-color: var(--color-white);
  }

  :global(.dark) .header {
    background-color: var(--color-gray-900);
  }

  .nav {
    display: flex;
    gap: calc(var(--spacing) * 4);
  }

  .nav-link {
    font-size: var(--text-sm);
    font-weight: 500;
    color: var(--color-gray-700);
    transition: var(--transition-colors);
  }

  .nav-link:hover {
    color: var(--color-blue-500);
  }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;¿Más líneas? Sí. ¿Más claro y mantenible? &lt;strong&gt;Absolutamente&lt;/strong&gt; ✅.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;📚 Lo Que Aprendí por el Camino&lt;/h2&gt;
&lt;p&gt;La migración no fue solo “quitar Tailwind y poner CSS”. Hubo trampas interesantes que vale la pena compartir:&lt;/p&gt;
&lt;h3&gt;Estilos scoped y &lt;span class="high"&gt;&amp;lt;slot&amp;gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;En &lt;span class="high"&gt;Astro&lt;/span&gt;, los estilos dentro de &lt;span class="high"&gt;&amp;lt;style&amp;gt;&lt;/span&gt; son &lt;strong&gt;scoped&lt;/strong&gt; por defecto. Esto significa que cada componente recibe un atributo único (&lt;span class="high"&gt;data-astro-cid-*&lt;/span&gt;) y los estilos solo afectan a ese componente.&lt;/p&gt;
&lt;p&gt;El problema: el contenido que llega por &lt;span class="high"&gt;&amp;lt;slot&amp;gt;&lt;/span&gt; &lt;strong&gt;no recibe&lt;/strong&gt; ese atributo. Si un componente padre intenta estilizar lo que viene por slot, los estilos no se aplican 😱.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;La solución&lt;/strong&gt;: usar &lt;span class="high"&gt;:global()&lt;/span&gt; para los selectores que afectan a contenido slotted:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-css"&gt;.container :global(a) {
  color: var(--color-primary);
  text-decoration: underline;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;span class="high"&gt;opacity&lt;/span&gt; vs transparencia de fondo&lt;/h3&gt;
&lt;p&gt;Con Tailwind usaba clases como &lt;span class="high"&gt;bg-opacity-70&lt;/span&gt;. Al migrar, el primer instinto fue usar la propiedad &lt;span class="high"&gt;opacity&lt;/span&gt;. &lt;strong&gt;Error&lt;/strong&gt;: &lt;span class="high"&gt;opacity&lt;/span&gt; afecta al elemento entero, &lt;strong&gt;incluidos sus hijos&lt;/strong&gt; 👶.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;La solución correcta&lt;/strong&gt;: &lt;span class="high"&gt;color-mix()&lt;/span&gt; para transparencia solo en el fondo:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-css"&gt;.modal-overlay {
  /* MAL: afecta a todo */
  opacity: 0.7;

  /* BIEN: solo el fondo es transparente */
  background-color: color-mix(in oklab, var(--color-gray-900) 70%, transparent);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;📊 Los Resultados&lt;/h2&gt;
&lt;p&gt;Los números hablan por sí solos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;~20KB menos&lt;/strong&gt; de CSS entregado al navegador 📉&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;3 dependencias eliminadas&lt;/strong&gt; del &lt;code&gt;package.json&lt;/code&gt; 🗑️&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;0 capas de abstracción&lt;/strong&gt; entre tu código y el navegador 🎯&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build más rápido&lt;/strong&gt; al no necesitar el procesamiento de Tailwind ⚡&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Control total&lt;/strong&gt; sobre cada línea de CSS que se genera 🎛️&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;El &lt;span class="high"&gt;package.json&lt;/span&gt; pasó de tener &lt;span class="high"&gt;tailwindcss&lt;/span&gt;, &lt;span class="high"&gt;@tailwindcss/vite&lt;/span&gt; y &lt;span class="high"&gt;@tailwindcss/typography&lt;/span&gt; a &lt;strong&gt;no tener ninguna dependencia de estilos&lt;/strong&gt;. Solo CSS puro.&lt;/p&gt;
&lt;p&gt;Y lo mejor: se eliminó el archivo &lt;span class="high"&gt;tailwind.config.mjs&lt;/span&gt; por completo. &lt;span class="high"&gt;Una configuración menos&lt;/span&gt; que mantener 🧹.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🏗️ La Arquitectura Final&lt;/h2&gt;
&lt;p&gt;El sistema de estilos quedó organizado en 4 archivos CSS:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Archivo&lt;/th&gt;
&lt;th&gt;Responsabilidad&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span class="high"&gt;design-tokens.css&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;Colores, tipografía, espaciado, sombras, transiciones&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span class="high"&gt;base-reset.css&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;Reset CSS mínimo y utilidades como &lt;span class="high"&gt;.sr-only&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span class="high"&gt;prose.css&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;Tipografía para contenido del blog con soporte dark mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span class="high"&gt;layout.css&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;Clases de layout globales (&lt;span class="high"&gt;.bodyLayout&lt;/span&gt;, &lt;span class="high"&gt;.mainLayout&lt;/span&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Todo importado desde un único &lt;span class="high"&gt;global.css&lt;/span&gt;. Limpio, predecible y sin magia negra 🧙‍♂️.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;💭 Reflexión Final&lt;/h2&gt;
&lt;p&gt;Migrar de &lt;span class="high"&gt;Tailwind CSS&lt;/span&gt; a &lt;span class="high"&gt;vanilla CSS&lt;/span&gt; no es para todos ni para todos los proyectos. En equipos grandes o proyectos con muchos desarrolladores, Tailwind sigue siendo una &lt;strong&gt;opción fantástica&lt;/strong&gt; por su consistencia y velocidad de desarrollo.&lt;/p&gt;
&lt;p&gt;Pero en un proyecto personal como un portfolio con &lt;span class="high"&gt;Astro&lt;/span&gt;, donde el rendimiento importa y el control total es un lujo que puedes permitirte, &lt;strong&gt;quitarte esa capa de abstracción&lt;/strong&gt; es liberador 🕊️. De hecho, acabé llevando esta filosofía aún más lejos y migré todo el portfolio de Astro a Swift — cuento la historia completa en &lt;a href="/es/blog/astro-to-saga/"&gt;Astro to Saga&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Es como pintar un cuadro: puedes usar plantillas y herramientas que te guíen, o puedes coger el pincel y crear &lt;strong&gt;exactamente&lt;/strong&gt; lo que tienes en mente. Ambas opciones son válidas. Pero cuando el lienzo es tuyo, &lt;span class="high"&gt;pintar a mano tiene su encanto&lt;/span&gt; 🎨.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link href="https://jcalderita.com/es/blog/tailwind-to-css/" rel="alternate"></link>
        <media:content url="https://jcalderita.com/static/blog/TailwindToCSS.webp" medium="image"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/cupertino-mcp/</id>
        <title>Cupertino MCP</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Descubre Cupertino MCP, la herramienta que integra toda la documentación de Apple directamente en tu IA para desarrollo iOS/macOS.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;🍎 El Problema de la Documentación de Apple&lt;/h2&gt;
&lt;p&gt;Si desarrollas para &lt;strong&gt;iOS, macOS o cualquier plataforma de Apple&lt;/strong&gt;, conoces el drill perfecto:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Estás programando tranquilamente 💻&lt;/li&gt;
&lt;li&gt;Necesitas consultar &lt;strong&gt;cómo funciona algo en SwiftUI&lt;/strong&gt; 🤔&lt;/li&gt;
&lt;li&gt;Abres Safari / tu navegador favorito 🌐&lt;/li&gt;
&lt;li&gt;Buscas en Google / DuckDuckGo 🔍&lt;/li&gt;
&lt;li&gt;Entras a la documentación oficial de Apple 📚&lt;/li&gt;
&lt;li&gt;Lees, entiendes, vuelves a tu IDE 🔄&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Repites el proceso 47 veces al día&lt;/strong&gt; 😵&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;O peor aún: &lt;strong&gt;le preguntas a tu IA favorita&lt;/strong&gt; y te da información &lt;span class="high"&gt;desactualizada&lt;/span&gt; o directamente &lt;span class="high"&gt;alucinada&lt;/span&gt; porque su conocimiento no está actualizado con las últimas versiones de Swift o SwiftUI 🤖❌.&lt;/p&gt;
&lt;p&gt;¿Y si te dijera que hay una forma mejor?&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;💡 Cupertino MCP: La Solución&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/mihaelamj/cupertino"&gt;Cupertino MCP&lt;/a&gt;&lt;/strong&gt; es una herramienta que &lt;span class="high"&gt;indexa localmente toda la documentación de Apple&lt;/span&gt; y la pone disponible para tu IA mediante el &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Traducido a cristiano: es como darle a tu IA (Claude, en mi caso) un &lt;strong&gt;acceso directo y verificado&lt;/strong&gt; a toda la documentación oficial de Apple 🎯.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;¿Cuánta documentación estamos hablando?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;📄 &lt;strong&gt;302,424+ páginas de documentación&lt;/strong&gt; oficial&lt;br /&gt;&lt;br /&gt;
🧰 &lt;strong&gt;307 frameworks&lt;/strong&gt; indexados&lt;br /&gt;&lt;br /&gt;
📦 &lt;strong&gt;9,699 paquetes Swift&lt;/strong&gt; catalogados&lt;br /&gt;&lt;br /&gt;
💾 &lt;strong&gt;606 proyectos de código de ejemplo&lt;/strong&gt; de Apple&lt;br /&gt;&lt;br /&gt;
📱 &lt;strong&gt;Human Interface Guidelines&lt;/strong&gt; completas&lt;br /&gt;&lt;br /&gt;
📖 &lt;strong&gt;Swift Evolution proposals&lt;/strong&gt; (~400 propuestas)&lt;br /&gt;&lt;br /&gt;
🏛️ &lt;strong&gt;Apple Archive guides&lt;/strong&gt; (documentación legacy pero valiosa)&lt;/p&gt;
&lt;p&gt;Todo esto, &lt;strong&gt;disponible offline&lt;/strong&gt;, sin necesidad de internet, y &lt;span class="high"&gt;sin riesgo de alucinaciones&lt;/span&gt; de la IA 🛡️.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🎯 Precisión Absoluta&lt;/h2&gt;
&lt;p&gt;Cuando Claude me responde sobre APIs de Apple, &lt;strong&gt;ya no adivina&lt;/strong&gt;. Busca en la documentación oficial real y me da información &lt;span class="high"&gt;100% verificada&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;Nada de: &lt;em&gt;“Creo que en SwiftUI 6 se usa así…”&lt;/em&gt; ❌&lt;/p&gt;
&lt;p&gt;Ahora es: &lt;em&gt;“Según la documentación oficial de SwiftUI 6…”&lt;/em&gt; ✅&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;⚡ Velocidad de Desarrollo&lt;/h2&gt;
&lt;p&gt;Antes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Pregunta → Espera respuesta → Duda → Abrir Safari →
Buscar en Google → Leer docs → Volver al IDE → Implementar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ahora:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Pregunta → Respuesta con documentación oficial → Implementar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span class="high"&gt;Ahorro brutal de tiempo&lt;/span&gt; en cada consulta 🏎️.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;📊 Búsqueda Súper Potente&lt;/h2&gt;
&lt;p&gt;Cupertino usa &lt;strong&gt;SQLite FTS5 con ranking BM25&lt;/strong&gt;. En cristiano: búsquedas &lt;span class="high"&gt;ultra-rápidas&lt;/span&gt; (menos de 100ms) con resultados relevantes ordenados por importancia.&lt;/p&gt;
&lt;p&gt;Puedes filtrar por:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Framework específico 🧰&lt;/li&gt;
&lt;li&gt;Versión de plataforma (iOS 17, macOS 14, etc.) 📱&lt;/li&gt;
&lt;li&gt;Tipo de documentación (API, ejemplos, guías) 📚&lt;/li&gt;
&lt;li&gt;Búsqueda en código de ejemplo 💻&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;🧠 Aprendizaje Contextualizado&lt;/h2&gt;
&lt;p&gt;No solo obtengo &lt;strong&gt;la API correcta&lt;/strong&gt;, también obtengo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ejemplos de código reales de Apple 📝&lt;/li&gt;
&lt;li&gt;Mejores prácticas documentadas 👍&lt;/li&gt;
&lt;li&gt;Patrones de diseño oficiales 🎨&lt;/li&gt;
&lt;li&gt;Alternativas y deprecaciones ⚠️&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Es como tener &lt;strong&gt;un mentor de Apple dentro de Claude&lt;/strong&gt; 🍎🤝🤖.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;💭 Reflexión Final&lt;/h2&gt;
&lt;p&gt;Cupertino MCP es un &lt;strong&gt;ejemplo perfecto&lt;/strong&gt; de cómo las herramientas de IA se vuelven &lt;strong&gt;realmente útiles&lt;/strong&gt; cuando tienen acceso a &lt;span class="high"&gt;información verificada y actualizada&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;No se trata de darle más poder a la IA para que “adivine mejor”. Se trata de &lt;span class="high"&gt;conectarla con fuentes fiables&lt;/span&gt; para que te dé respuestas precisas.&lt;/p&gt;
&lt;p&gt;Si desarrollas para ecosistemas de Apple, Cupertino MCP es una &lt;strong&gt;inversión de 5 minutos&lt;/strong&gt; (instalación) que te ahorrará &lt;strong&gt;horas&lt;/strong&gt; cada semana. Y si usas Claude Code como yo, la experiencia se vuelve &lt;strong&gt;aún mejor&lt;/strong&gt; 💎.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;¿Lo recomiendo?&lt;/strong&gt; Absolutamente. Es &lt;strong&gt;obligatorio&lt;/strong&gt; si desarrollas con Swift, SwiftUI o cualquier framework de Apple 🍎.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;¿Es para todo el mundo?&lt;/strong&gt; Solo si desarrollas para plataformas Apple. Si trabajas con otras tecnologías, busca herramientas similares (seguro hay equivalentes para Android, web, etc.) 🔍.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link href="https://jcalderita.com/es/blog/cupertino-mcp/" rel="alternate"></link>
        <media:content url="https://jcalderita.com/static/blog/CupertinoMCP.webp" medium="image"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/which-ai/</id>
        <title>Which AI</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Cómo lidiar con la fatiga de herramientas de IA como desarrollador: por qué perseguir cada nuevo modelo es imposible y cómo elegir sin caer en el FOMO.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;🎭 Déjà Vu: Los Frameworks Web 2.0&lt;/h2&gt;
&lt;p&gt;¿Recuerdan aquella época dorada donde cada semana aparecía un &lt;span class="high"&gt;nuevo framework web&lt;/span&gt;?&lt;/p&gt;
&lt;p&gt;Uno más &lt;strong&gt;moderno&lt;/strong&gt;, más &lt;strong&gt;rápido&lt;/strong&gt;, más &lt;strong&gt;potente&lt;/strong&gt;, más &lt;strong&gt;bonito&lt;/strong&gt;… Uno que venía a &lt;span class="high"&gt;“matar” a todos los anteriores&lt;/span&gt; y que iba a convertirse en el &lt;span class="high"&gt;único framework que necesitarías usar jamás&lt;/span&gt; 💀.&lt;/p&gt;
&lt;p&gt;Spoiler: ninguno mató a nadie, y ahora tenemos más frameworks que nunca 😂.&lt;/p&gt;
&lt;p&gt;Pues bien, &lt;span class="high"&gt;bienvenidos a la era de las IAs&lt;/span&gt;: el mismo concepto, pero con esteroides 🚀.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;📱 La Fatiga de las Notificaciones&lt;/h2&gt;
&lt;p&gt;Imagina la escena: estás tranquilamente &lt;strong&gt;programando con tu IA de confianza&lt;/strong&gt;, flujo de trabajo perfecto, productividad al máximo… y de repente:&lt;/p&gt;
&lt;p&gt;📢 &lt;strong&gt;Notificación:&lt;/strong&gt; &lt;em&gt;”¡Nueva IA revolucionaria lanzada!”&lt;/em&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;strong&gt;Video:&lt;/strong&gt; &lt;em&gt;“Cómo usarla en 10 minutos”&lt;/em&gt;&lt;br /&gt;&lt;br /&gt;
📝 &lt;strong&gt;Tutorial:&lt;/strong&gt; &lt;em&gt;“Integrala en tu workflow HOY”&lt;/em&gt;&lt;br /&gt;&lt;br /&gt;
💡 &lt;strong&gt;Thread:&lt;/strong&gt; &lt;em&gt;“Por qué deberías cambiarte ahora mismo”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Y aquí viene lo &lt;strong&gt;más divertido&lt;/strong&gt;: el creador de contenido que hace 1 hora tenía un video explicando &lt;strong&gt;“Por qué la antigua IA es el futuro”&lt;/strong&gt;, ahora tiene otro video sobre &lt;strong&gt;“Por qué la nueva IA es mejor que todo”&lt;/strong&gt; 🎪.&lt;/p&gt;
&lt;p&gt;No los culpo, eh. Es su trabajo mantenerse actualizados y crear contenido relevante. Pero como consumidor… &lt;strong&gt;es agotador&lt;/strong&gt; 😅.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🏃‍♂️ La Carrera Imposible&lt;/h2&gt;
&lt;p&gt;Si ya es &lt;strong&gt;complicado seguir las actualizaciones diarias&lt;/strong&gt; de la IA que estás usando (y créeme, son &lt;strong&gt;muchísimas&lt;/strong&gt; actualizaciones), imagínate ponerte a:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Probar cada nueva IA que sale ✅&lt;/li&gt;
&lt;li&gt;Aprender su interfaz y peculiaridades 📚&lt;/li&gt;
&lt;li&gt;Integrarla en tu workflow 🔧&lt;/li&gt;
&lt;li&gt;Compararla con las que ya usas ⚖️&lt;/li&gt;
&lt;li&gt;Decidir si vale la pena el cambio 🤔&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span class="high"&gt;Resultado:&lt;/span&gt; Misión imposible 🎯.&lt;/p&gt;
&lt;p&gt;Y no me malinterpreten: &lt;strong&gt;me encanta tener opciones&lt;/strong&gt;. De hecho, es &lt;strong&gt;maravilloso&lt;/strong&gt; que exista tanta competencia e innovación. Más opciones = más mejor 👍🏻.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🚫 Mi Línea Roja&lt;/h2&gt;
&lt;p&gt;Ahora bien, hay algo en lo que soy &lt;strong&gt;tajante&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;span class="high"&gt;Jamás usaré una IA que requiera permisos totales desde el principio&lt;/span&gt; 🔒.&lt;/p&gt;
&lt;p&gt;Nada de &lt;em&gt;“Dame acceso completo a tu máquina y confía en mí”&lt;/em&gt;. &lt;strong&gt;Eso no va conmigo&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Mi filosofía es clara:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;📋 &lt;strong&gt;Dime qué vas a hacer&lt;/strong&gt; (o planifícalo)&lt;/li&gt;
&lt;li&gt;👀 &lt;strong&gt;Yo reviso los cambios&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Yo apruebo&lt;/strong&gt; (o no apruebo)&lt;/li&gt;
&lt;li&gt;🚀 &lt;strong&gt;Entonces procedes&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;No al libre albedrío de la IA con permisos de administrador. &lt;span class="high"&gt;Control total&lt;/span&gt; es mi mantra, como ya conté en mi artículo sobre &lt;a href="/es/blog/claude-code/"&gt;Claude Code&lt;/a&gt; 💪.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🌐 Ya No Es Solo un Modelo&lt;/h2&gt;
&lt;p&gt;Y aquí está lo &lt;strong&gt;realmente fascinante&lt;/strong&gt;: la IA ya no es simplemente &lt;em&gt;“un modelo”&lt;/em&gt; o &lt;em&gt;“una app”&lt;/em&gt; que instalas y usas.&lt;/p&gt;
&lt;p&gt;Ahora tenemos &lt;strong&gt;ecosistemas completos&lt;/strong&gt; 🏗️:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Modelos base + fine-tuned ⚙️&lt;/li&gt;
&lt;li&gt;Integraciones con IDEs 💻&lt;/li&gt;
&lt;li&gt;Plugins y extensiones 🔌&lt;/li&gt;
&lt;li&gt;APIs y SDKs 📡&lt;/li&gt;
&lt;li&gt;Plataformas de orquestación 🎼&lt;/li&gt;
&lt;li&gt;Herramientas especializadas 🛠️&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Es &lt;strong&gt;increíble&lt;/strong&gt; la velocidad a la que todo esto está evolucionando. Lo que era ciencia ficción hace 2 años, ahora es tu &lt;span class="high"&gt;asistente de código diario&lt;/span&gt; 🤖.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🎢 Viviendo una Época Increíble&lt;/h2&gt;
&lt;p&gt;Al final del día, creo que estamos viviendo algo &lt;span class="high"&gt;histórico&lt;/span&gt; 🎊:&lt;/p&gt;
&lt;p&gt;🥊 &lt;strong&gt;Batalla de los IDEs&lt;/strong&gt;: VSCode vs. Cursor vs. WindSurf vs. Zed vs…&lt;br /&gt;&lt;br /&gt;
⚔️ &lt;strong&gt;Batalla de los Frameworks&lt;/strong&gt;: React vs. Vue vs. Svelte vs. Solid vs…&lt;br /&gt;&lt;br /&gt;
🤖 &lt;strong&gt;Batalla de las IAs&lt;/strong&gt;: Claude vs. ChatGPT vs. Gemini vs. Copilot vs…&lt;/p&gt;
&lt;p&gt;Es &lt;strong&gt;agotador&lt;/strong&gt; intentar seguirlo todo, lo admito. A mí personalmente me resulta &lt;span class="high"&gt;imposible&lt;/span&gt; 🙃.&lt;/p&gt;
&lt;p&gt;Pero también es &lt;strong&gt;emocionante&lt;/strong&gt;. Cada día hay algo nuevo que aprender, algo que probar, algo que te hace replantear cómo trabajas.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🎯 Reflexión Final&lt;/h2&gt;
&lt;p&gt;La moraleja de todo esto es simple:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No necesitas usar todo&lt;/strong&gt;. De verdad, no lo necesitas 🙅‍♂️.&lt;/p&gt;
&lt;p&gt;Encuentra &lt;strong&gt;las herramientas que funcionen para ti&lt;/strong&gt;, aprende a usarlas bien, mantente actualizado con &lt;strong&gt;sus&lt;/strong&gt; novedades, y está bien que de vez en cuando explores alternativas.&lt;/p&gt;
&lt;p&gt;Pero no te vuelvas loco intentando estar en &lt;strong&gt;todas las olas a la vez&lt;/strong&gt;. Elige tu tabla de surf 🏄‍♂️, aprende a usarla bien, y disfruta del viaje.&lt;/p&gt;
&lt;p&gt;Y si un día decides cambiar de tabla, pues adelante. Pero que sea una &lt;strong&gt;decisión consciente&lt;/strong&gt;, no una reacción al &lt;span class="high"&gt;FOMO&lt;/span&gt; (&lt;em&gt;Fear Of Missing Out&lt;/em&gt;) creado por 47 notificaciones simultáneas 😂.&lt;/p&gt;
&lt;p&gt;&lt;span class="high"&gt;Feliz coding&lt;/span&gt; (con la IA que prefieras) 🚀&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link href="https://jcalderita.com/es/blog/which-ai/" rel="alternate"></link>
        <media:content url="https://jcalderita.com/static/blog/WhichAI.webp" medium="image"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/conferences-25/</id>
        <title>Conferences 25</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Lista curada de las mejores conferencias de iOS, Swift y Apple en 2025 con fechas, ubicaciones y enlaces para ver todas las charlas gratis.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;iOSConfSG&lt;/h2&gt;
&lt;p&gt;📅 15-17 de enero&lt;br /&gt;&lt;br /&gt;
📍 Singapur, 🇸🇬&lt;br /&gt;&lt;br /&gt;
🔗 &lt;a href="https://2025.iosconf.sg/"&gt;iosconf&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;a href="https://youtube.com/watch?v=N1H9lvHwQxc&amp;amp;list=PLED4k3CZkY9RBltAgj-o9xSFOMOhBdmXm"&gt;youtube&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;AppDevCon&lt;/h2&gt;
&lt;p&gt;📅 18–21 de marzo&lt;br /&gt;&lt;br /&gt;
📍 Ámsterdam, 🇳🇱&lt;br /&gt;&lt;br /&gt;
🔗 &lt;a href="https://appdevcon.nl"&gt;appdevcon&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;a href="https://appdevcon.nl/videos/"&gt;videos&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Swift Heroes&lt;/h2&gt;
&lt;p&gt;📅 8–9 de abril&lt;br /&gt;&lt;br /&gt;
📍 Turín, 🇮🇹&lt;br /&gt;&lt;br /&gt;
🔗 &lt;a href="https://swiftheroes.com"&gt;swiftheroes&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;a href="https://youtube.com/watch?v=cVNtQK34xdw&amp;amp;list=PLfCiO1zYKkARXhFxrqv3WR6b6JEp4LaAN"&gt;youtube&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;try! Swift Tokyo&lt;/h2&gt;
&lt;p&gt;📅 9–11 de abril&lt;br /&gt;&lt;br /&gt;
📍 Tokio, 🇯🇵&lt;br /&gt;&lt;br /&gt;
🔗 &lt;a href="https://tryswift.jp/en"&gt;tryswift&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;a href="https://youtube.com/watch?v=qY09lmDo7GU&amp;amp;list=PLCl5NM4qD3u_Azg7gKw5CK_DqSLeb4QMY"&gt;youtube&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Deep Dish Swift&lt;/h2&gt;
&lt;p&gt;📅 27–29 de abril&lt;br /&gt;&lt;br /&gt;
📍 Chicago, 🇺🇸&lt;br /&gt;&lt;br /&gt;
🔗 &lt;a href="https://deepdishswift.com"&gt;deepdishswift&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;a href="https://youtube.com/watch?v=eN6NJR67HL4&amp;amp;list=PLGLg44jP5U-4l1OERkMTPrzgG7nQkGGYy"&gt;youtube&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;iOSKonf&lt;/h2&gt;
&lt;p&gt;📅 13–15 de mayo&lt;br /&gt;&lt;br /&gt;
📍 Skopje, 🇲🇰&lt;br /&gt;&lt;br /&gt;
🔗 &lt;a href="https://ioskonf.mk"&gt;ioskonf&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;a href="https://youtube.com/watch?v=AIvCc1v5F4g&amp;amp;list=PLVKQDFwOy1XZXhFOdWfXKdjCz4r_LBeto"&gt;youtube&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Swift Craft&lt;/h2&gt;
&lt;p&gt;📅 19–21 de mayo&lt;br /&gt;&lt;br /&gt;
📍 Kent, 🇬🇧&lt;br /&gt;&lt;br /&gt;
🔗 &lt;a href="https://swiftcraft.uk"&gt;swiftcraft&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;a href="https://youtube.com/watch?v=XR4XrVvHUQo&amp;amp;list=PLugrLwuQvERolaa2XA0dl4UK9Z8XEwiAA"&gt;youtube&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;WWDC&lt;/h2&gt;
&lt;p&gt;📅 9–13 de junio&lt;br /&gt;&lt;br /&gt;
📍 Cupertino, 🇺🇸&lt;br /&gt;&lt;br /&gt;
🔗 &lt;a href="https://developer.apple.com/wwdc25"&gt;apple&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;a href="https://youtube.com/watch?v=IrGYUq1mklk&amp;amp;list=PLjODKV8YBFHZKEn1wsUCL1n-q7tzysEBM"&gt;youtube&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;NSSpain&lt;/h2&gt;
&lt;p&gt;📅 17-19 septiembre&lt;br /&gt;&lt;br /&gt;
📍 Logroño, 🇪🇸&lt;br /&gt;&lt;br /&gt;
🔗 &lt;a href="https://nsspain.com"&gt;nsspain&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;a href="https://youtube.com/watch?v=NjW7lxtZwIM&amp;amp;list=PLztE34GS_piKKQ6y1dkkuhW76jLBHm3NV"&gt;youtube&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Swift Connection&lt;/h2&gt;
&lt;p&gt;📅 6–7 de octubre&lt;br /&gt;&lt;br /&gt;
📍 París, 🇫🇷&lt;br /&gt;&lt;br /&gt;
🔗 &lt;a href="https://swiftconnection.io"&gt;swiftconnection&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;a href="https://youtube.com/watch?v=N05WEfGPp3M&amp;amp;list=PLZsRQnRG-mlIkHsjeax_cRq6kAclNrWBF"&gt;youtube&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;SwiftLeeds&lt;/h2&gt;
&lt;p&gt;📅 7–8 de octubre&lt;br /&gt;&lt;br /&gt;
📍 Leeds, 🇬🇧&lt;br /&gt;&lt;br /&gt;
🔗 &lt;a href="https://swiftleeds.co.uk"&gt;swiftleeds&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;a href="https://youtube.com/watch?v=iNiQHGKULQw&amp;amp;list=PL-wmxEeX64YTpDbpfszWMV76oZZO3wxZH"&gt;youtube&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Pragma Conference&lt;/h2&gt;
&lt;p&gt;📅 30–31 de octubre&lt;br /&gt;&lt;br /&gt;
📍 Bolonia, 🇮🇹&lt;br /&gt;&lt;br /&gt;
🔗 &lt;a href="https://pragmaconference.com"&gt;pragmaconference&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;a href="https://youtube.com/watch?v=7o6Fkj3buxM&amp;amp;list=PLAVm70iJlMuvTihK1OzK9S4Vzw_KO71b0"&gt;youtube&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Do iOS&lt;/h2&gt;
&lt;p&gt;📅 12-13 de noviembre&lt;br /&gt;&lt;br /&gt;
📍 Amsterdam, 🇳🇱&lt;br /&gt;&lt;br /&gt;
🔗 &lt;a href="https://do-ios.com"&gt;do-ios&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;a href="https://youtube.com/watch?v=q-Y44dh6sTo&amp;amp;list=PLJEA4wsA0WeSP_NB1LcqcA7tBjhcg1u4U"&gt;youtube&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link href="https://jcalderita.com/es/blog/conferences-25/" rel="alternate"></link>
        <media:content url="https://jcalderita.com/static/blog/Conferences25.webp" medium="image"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/async-concurrent-map/</id>
        <title>Async Concurrent Map</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Como combinar procesamiento concurrente con chunking para optimizar el uso de recursos en operaciones asíncronas masivas.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;En aplicaciones backend con &lt;span class="high"&gt;Vapor&lt;/span&gt;, cuando procesamos &lt;strong&gt;grandes volúmenes de datos&lt;/strong&gt; de forma concurrente, nos enfrentamos a un dilema de optimización:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Procesamiento secuencial&lt;/strong&gt; con &lt;span class="high"&gt;asyncMap&lt;/span&gt;: garantiza control sobre los recursos, pero es &lt;strong&gt;lento&lt;/strong&gt; al procesar elementos uno a uno.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Procesamiento totalmente concurrente&lt;/strong&gt; con &lt;span class="high"&gt;concurrentMap&lt;/span&gt;: maximiza la velocidad, pero puede &lt;strong&gt;saturar recursos&lt;/strong&gt; al lanzar miles de tareas simultáneas.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Por ejemplo, al procesar 10,000 registros con llamadas a APIs externas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span class="high"&gt;asyncMap&lt;/span&gt;: 10,000 llamadas secuenciales → muy lento pero controlado.&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;concurrentMap&lt;/span&gt;: 10,000 llamadas simultáneas → muy rápido pero puede agotar conexiones/memoria.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Necesitamos una solución que &lt;strong&gt;combine ambos enfoques&lt;/strong&gt;: dividir el trabajo en grupos manejables y procesar cada grupo de forma concurrente, equilibrando velocidad y uso de recursos.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;Extendemos &lt;span class="high"&gt;Collection&lt;/span&gt; con una función que combina &lt;strong&gt;chunking&lt;/strong&gt; (división en grupos) y &lt;strong&gt;procesamiento concurrente&lt;/strong&gt;, permitiendo configurar el tamaño de los grupos y timeout por chunk.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;extension Collection where Element: Sendable {
    func asyncConcurrentMap&amp;lt;T: Sendable&amp;gt;(
        chunkSize: Int? = nil,
        timeout: Double? = nil,
        _ transform: @escaping @Sendable (Element) async throws -&amp;gt; T
    ) async throws -&amp;gt; [T] {
        guard let chunkSize else {
            return try await concurrentMap(transform)
        }

        return try await chunks(ofCount: chunkSize)
            .asyncMap(timeout: timeout) {
                try await $0.concurrentMap(transform)
            }.flatMap { $0 }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Puntos clave:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Si no se especifica &lt;span class="high"&gt;chunkSize&lt;/span&gt;, usa &lt;span class="high"&gt;concurrentMap&lt;/span&gt; puro (procesamiento totalmente concurrente).&lt;/li&gt;
&lt;li&gt;Si se especifica &lt;span class="high"&gt;chunkSize&lt;/span&gt;, divide la colección en grupos con &lt;span class="high"&gt;chunks(ofCount:)&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;Procesa los &lt;strong&gt;chunks secuencialmente&lt;/strong&gt; con &lt;span class="high"&gt;asyncMap&lt;/span&gt; (con timeout opcional).&lt;/li&gt;
&lt;li&gt;Dentro de cada chunk, procesa los elementos &lt;strong&gt;concurrentemente&lt;/strong&gt; con &lt;span class="high"&gt;concurrentMap&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;Aplana los resultados con &lt;span class="high"&gt;flatMap&lt;/span&gt; para devolver un array unificado.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;// Procesar 10,000 registros en chunks de 100
// 100 tareas concurrentes a la vez, 100 veces
let results = try await records.asyncConcurrentMap(
    chunkSize: 100,
    timeout: 30.0
) { record in
    try await apiClient.process(record)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Beneficios de esta aproximación:&lt;/p&gt;
&lt;p&gt;⚡ &lt;strong&gt;Balance perfecto&lt;/strong&gt;: combina la velocidad del procesamiento concurrente con el control del procesamiento secuencial por chunks.&lt;br /&gt;&lt;br /&gt;
🎯 &lt;strong&gt;Control de recursos&lt;/strong&gt;: limita la cantidad de tareas simultáneas al tamaño del chunk, evitando saturación.&lt;br /&gt;&lt;br /&gt;
⏱️ &lt;strong&gt;Timeout por chunk&lt;/strong&gt;: detecta y maneja chunks problemáticos sin bloquear todo el procesamiento.&lt;br /&gt;&lt;br /&gt;
🔧 &lt;strong&gt;Flexibilidad total&lt;/strong&gt;: usa chunking cuando lo necesites, o procesamiento concurrente puro cuando no.&lt;br /&gt;&lt;br /&gt;
📊 &lt;strong&gt;Escalabilidad&lt;/strong&gt;: permite procesar millones de registros ajustando el tamaño del chunk según los recursos disponibles.&lt;/p&gt;
&lt;p&gt;Si necesitas ejecución paralela pura, lo cubrí en &lt;a href="/es/blog/concurrent-map/"&gt;Concurrent Map&lt;/a&gt;. Y si tus chunks secuenciales necesitan rate limiting entre ellos, añadí esa capacidad en &lt;a href="/es/blog/async-map-timeout/"&gt;Async Map Timeout&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Esta solución es la &lt;strong&gt;evolución natural&lt;/strong&gt; de &lt;span class="high"&gt;asyncMap&lt;/span&gt; y &lt;span class="high"&gt;concurrentMap&lt;/span&gt;, combinando lo mejor de ambos mundos para optimizar el procesamiento de datos masivos en aplicaciones backend.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link href="https://jcalderita.com/es/blog/async-concurrent-map/" rel="alternate"></link>
        <media:content url="https://jcalderita.com/static/blog/AsyncConcurrentMap.webp" medium="image"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/async-map-timeout/</id>
        <title>Async Map Timeout</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Actualización de asyncMap para añadir control de rate limiting mediante timeouts entre operaciones.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;En el &lt;a href="/es/blog/async-map/"&gt;post sobre AsyncMap&lt;/a&gt; vimos cómo procesar colecciones de forma &lt;strong&gt;secuencial asíncrona&lt;/strong&gt;. Sin embargo, al trabajar con &lt;strong&gt;APIs externas&lt;/strong&gt; que implementan rate limiting, nos enfrentamos a un problema crítico:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;// Procesar 1000 URLs secuencialmente
let results = try await urls.asyncMap { url in
    try await apiClient.fetch(url)  // ⚠️ 1000 llamadas sin pausa
}
// Error 429: Too Many Requests
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Realizar llamadas consecutivas sin pausas puede:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Exceder límites de rate&lt;/strong&gt;: APIs rechazan con &lt;span class="high"&gt;429 Too Many Requests&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Saturar servicios externos&lt;/strong&gt;: sobrecarga de conexiones simultáneas.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Desperdiciar recursos&lt;/strong&gt;: forzar reintentos consume más tiempo y ancho de banda.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bloqueos temporales&lt;/strong&gt;: algunas APIs bloquean la IP tras múltiples infracciones.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Necesitamos una forma de &lt;strong&gt;controlar el ritmo&lt;/strong&gt; de las operaciones secuenciales, añadiendo pausas intencionales entre cada procesamiento.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;Actualizamos &lt;span class="high"&gt;asyncMap&lt;/span&gt; añadiendo un parámetro opcional &lt;span class="high"&gt;timeout&lt;/span&gt; que introduce una pausa configurable después de procesar cada elemento.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;extension Sequence {
    func asyncMap&amp;lt;T&amp;gt;(
        timeout: Double? = nil,
        _ transform: (Element) async throws -&amp;gt; T
    ) async throws -&amp;gt; [T] {
        var results = [T]()
        results.reserveCapacity(underestimatedCount)
        for element in self {
            try await results.append(transform(element))
            if let timeout {
                try await Task.sleep(for: .seconds(timeout))
            }
        }
        return results
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Cambios clave respecto a la versión original&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;✨ Parámetro &lt;span class="high"&gt;timeout: Double? = nil&lt;/span&gt; opcional y retrocompatible.&lt;br /&gt;&lt;br /&gt;
⏱️ Si se especifica timeout, añade &lt;span class="high"&gt;Task.sleep(for: .seconds(timeout))&lt;/span&gt; después de cada elemento.&lt;br /&gt;&lt;br /&gt;
🔄 Mantiene el comportamiento original cuando no se especifica timeout (sin pausas).&lt;br /&gt;&lt;br /&gt;
📊 Permite ajustar dinámicamente el rate limiting según los límites de cada API.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;// Rate limiting: 1 llamada por segundo
let results = try await urls.asyncMap(timeout: 1.0) {
    try await apiClient.fetch($0)
}

// Rate limiting agresivo: 1 llamada cada 5 segundos
let results = try await endpoints.asyncMap(timeout: 5.0) {
    try await scraper.parse($0)
}

// Sin timeout: comportamiento original (máxima velocidad)
let results = try await localFiles.asyncMap {
    try await processFile($0)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Beneficios de esta actualización:&lt;/p&gt;
&lt;p&gt;⏱️ &lt;strong&gt;Rate limiting configurable&lt;/strong&gt;: controla el ritmo de llamadas según los límites de cada API.&lt;br /&gt;&lt;br /&gt;
🛡️ &lt;strong&gt;Prevención de bloqueos&lt;/strong&gt;: evita errores &lt;span class="high"&gt;429&lt;/span&gt; y suspensiones temporales de IP.&lt;br /&gt;&lt;br /&gt;
🔄 &lt;strong&gt;Retrocompatibilidad total&lt;/strong&gt;: sin timeout funciona exactamente igual que antes.&lt;br /&gt;&lt;br /&gt;
🎯 &lt;strong&gt;Flexibilidad por caso de uso&lt;/strong&gt;: ajusta el timeout según la tolerancia del servicio externo.&lt;br /&gt;&lt;br /&gt;
📊 &lt;strong&gt;Procesamiento predecible&lt;/strong&gt;: calcula fácilmente el tiempo total (n elementos × timeout).&lt;/p&gt;
&lt;p&gt;Esta actualización convierte &lt;span class="high"&gt;asyncMap&lt;/span&gt; en una herramienta &lt;strong&gt;completa para procesamiento secuencial controlado&lt;/strong&gt;, ideal para integración con APIs que imponen límites de tasa y necesitan un flujo de peticiones regulado.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link href="https://jcalderita.com/es/blog/async-map-timeout/" rel="alternate"></link>
        <media:content url="https://jcalderita.com/static/blog/AsyncMapTimeout.webp" medium="image"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/copy-to/</id>
        <title>Copy To</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Exporta grandes conjuntos de datos de PostgreSQL a CSV en Vapor usando el comando nativo COPY TO, evitando serialización manual y reduciendo consumo de memoria.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;En aplicaciones backend con &lt;span class="high"&gt;Vapor&lt;/span&gt;, a menudo necesitamos &lt;strong&gt;exportar grandes volúmenes de datos&lt;/strong&gt; desde la base de datos a archivos para diferentes propósitos: backups, análisis offline, integración con sistemas externos, o auditorías de datos.&lt;/p&gt;
&lt;p&gt;Usar consultas tradicionales con &lt;span class="high"&gt;Fluent&lt;/span&gt; para luego serializar los resultados manualmente presenta varios inconvenientes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Alto consumo de memoria&lt;/strong&gt;: cargar miles de registros en memoria para procesarlos uno a uno.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Procesamiento lento&lt;/strong&gt;: serialización manual a &lt;span class="high"&gt;CSV&lt;/span&gt; requiere iterar y formatear cada registro.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Falta de optimización&lt;/strong&gt;: no aprovecha las capacidades nativas de exportación del motor de base de datos.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Complejidad innecesaria&lt;/strong&gt;: gestión manual de formatos, escapado de caracteres y manejo de valores nulos.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Para escenarios de exportación masiva, necesitamos una estrategia que aproveche las capacidades nativas de &lt;span class="high"&gt;PostgreSQL&lt;/span&gt; para generar archivos de forma eficiente.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;Extendemos &lt;span class="high"&gt;Database&lt;/span&gt; con una función que ejecuta el comando &lt;span class="high"&gt;COPY … TO&lt;/span&gt; de &lt;span class="high"&gt;PostgreSQL&lt;/span&gt;, permitiendo exportar datos directamente desde el schema de la tabla a archivos &lt;span class="high"&gt;CSV&lt;/span&gt; en el sistema de archivos.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;extension Database {
    func exportCSV(
        _ model: any Model.Type,
        file: PathEnum)
        async throws {
        let query = SQLQueryString(
            &amp;quot;&amp;quot;&amp;quot;
            COPY \&amp;quot;\(unsafeRaw: model.space ?? &amp;quot;public&amp;quot;)\&amp;quot;.
            \&amp;quot;\(unsafeRaw: model.schema)\&amp;quot;
            TO '\(unsafeRaw: file.rawValue)'
            WITH (FORMAT csv, HEADER true, DELIMITER ',',
            QUOTE '&amp;quot;', ESCAPE '&amp;quot;', NULL '')
            &amp;quot;&amp;quot;&amp;quot;
        )

        try await self.sqlDatabase
            .raw(query).run()
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Puntos clave:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Usa &lt;span class="high"&gt;COPY … TO&lt;/span&gt; de &lt;span class="high"&gt;PostgreSQL&lt;/span&gt;, el método más eficiente para exportación masiva.&lt;/li&gt;
&lt;li&gt;El parámetro &lt;span class="high"&gt;model.space&lt;/span&gt; soporta schemas personalizados (por defecto &lt;span class="high"&gt;“public”&lt;/span&gt;).&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;model.schema&lt;/span&gt; obtiene automáticamente el nombre de la tabla del modelo &lt;span class="high"&gt;Fluent&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;Configuración &lt;span class="high"&gt;CSV&lt;/span&gt; estándar: headers incluidos, delimitadores y manejo correcto de valores nulos.&lt;/li&gt;
&lt;li&gt;Utiliza &lt;span class="high"&gt;unsafeRaw&lt;/span&gt; para interpolación directa en la query SQL.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;func exportRegions() async throws {
    try await db.exportCSV(
        LocationRegionModel.self, 
        file: file
    )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Beneficios de esta aproximación:&lt;/p&gt;
&lt;p&gt;🚀 &lt;strong&gt;Performance óptimo&lt;/strong&gt;: &lt;span class="high"&gt;COPY … TO&lt;/span&gt; es mucho más rápido que serialización manual.&lt;br /&gt;&lt;br /&gt;
💾 &lt;strong&gt;Eficiencia de recursos&lt;/strong&gt;: &lt;span class="high"&gt;PostgreSQL&lt;/span&gt; escribe directamente al archivo sin cargar datos en memoria de la aplicación.&lt;br /&gt;&lt;br /&gt;
📦 &lt;strong&gt;Formato consistente&lt;/strong&gt;: el motor de base de datos garantiza un &lt;span class="high"&gt;CSV&lt;/span&gt; válido con escapado correcto.&lt;br /&gt;&lt;br /&gt;
🔧 &lt;strong&gt;Integración nativa&lt;/strong&gt;: aprovecha capacidades optimizadas del motor &lt;span class="high"&gt;PostgreSQL&lt;/span&gt;.&lt;br /&gt;&lt;br /&gt;
📊 &lt;strong&gt;Escalabilidad&lt;/strong&gt;: permite exportar millones de registros sin impacto en el rendimiento de la aplicación.&lt;/p&gt;
&lt;p&gt;Esta solución es el complemento perfecto para &lt;span class="high"&gt;importCSV&lt;/span&gt;, formando un par de funciones que permite &lt;strong&gt;movimiento bidireccional de datos&lt;/strong&gt; entre &lt;span class="high"&gt;PostgreSQL&lt;/span&gt; y el sistema de archivos de forma eficiente y confiable. Si aún no has visto la parte de importación, explico cómo construirla usando &lt;code&gt;COPY FROM&lt;/code&gt; de PostgreSQL en &lt;a href="/es/blog/copy-from/"&gt;Copy From&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link href="https://jcalderita.com/es/blog/copy-to/" rel="alternate"></link>
        <media:content medium="image" url="https://jcalderita.com/static/blog/CopyTo.webp"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/copy-from/</id>
        <title>Copy From</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Como hacer bulk insert con Vapor para acelerar tus inserciones de datos a base de datos.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;En aplicaciones backend con &lt;span class="high"&gt;Vapor&lt;/span&gt;, cuando necesitamos insertar &lt;strong&gt;grandes volúmenes de datos&lt;/strong&gt; en la base de datos (migraciones iniciales, importación de catálogos, carga masiva desde APIs externas), usar &lt;span class="high"&gt;.save()&lt;/span&gt; en un bucle genera &lt;strong&gt;múltiples transacciones individuales&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Esto resulta en:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Alta latencia&lt;/strong&gt;: cada insert abre/cierra conexión y transaction overhead.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pobre throughput&lt;/strong&gt;: no aprovecha las capacidades de inserción masiva del motor de base de datos.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Riesgo de timeout&lt;/strong&gt;: operaciones lentas que pueden fallar en entornos con límites de tiempo.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Para escenarios de importación masiva (miles o millones de registros), necesitamos una estrategia de &lt;strong&gt;bulk insert&lt;/strong&gt; que aproveche las capacidades nativas de &lt;span class="high"&gt;PostgreSQL&lt;/span&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;Extendemos &lt;span class="high"&gt;Database&lt;/span&gt; con una función que ejecuta el comando &lt;span class="high"&gt;COPY&lt;/span&gt; de &lt;span class="high"&gt;PostgreSQL&lt;/span&gt;, permitiendo importar archivos &lt;span class="high"&gt;CSV&lt;/span&gt; directamente desde el sistema de archivos al schema de la tabla.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;extension Database {
    func importCSV(
        _ model: any Model.Type,
        file: PathEnum
        ) async throws {
        let query = SQLQueryString(
            &amp;quot;&amp;quot;&amp;quot;
            COPY \&amp;quot;\(unsafeRaw: model.space ?? &amp;quot;public&amp;quot;)\&amp;quot;.
            \&amp;quot;\(unsafeRaw: model.schema)\&amp;quot;
            FROM '\(unsafeRaw: file.rawValue)'
            WITH (FORMAT csv, HEADER true, DELIMITER ',',
            QUOTE '&amp;quot;', ESCAPE '&amp;quot;', NULL '')
            &amp;quot;&amp;quot;&amp;quot;
        )

        try await self.sqlDatabase
            .raw(query).run()
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Puntos clave:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Usa &lt;span class="high"&gt;COPY&lt;/span&gt; de &lt;span class="high"&gt;PostgreSQL&lt;/span&gt;, el método más rápido para bulk insert desde archivos.&lt;/li&gt;
&lt;li&gt;El parámetro &lt;span class="high"&gt;model.space&lt;/span&gt; soporta schemas personalizados (por defecto &lt;span class="high"&gt;“public”&lt;/span&gt;).&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;model.schema&lt;/span&gt; obtiene automáticamente el nombre de la tabla del modelo &lt;span class="high"&gt;Fluent&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;Configuración &lt;span class="high"&gt;CSV&lt;/span&gt; estándar: headers, delimitadores y manejo de valores nulos.&lt;/li&gt;
&lt;li&gt;Utiliza &lt;span class="high"&gt;unsafeRaw&lt;/span&gt; para interpolación directa en la query SQL.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;private func importRegions() async throws {
    try await db.importCSV(
        RegionModel.self,
        file: .LocationFile(&amp;quot;regions&amp;quot;, .csv)
    )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Beneficios de esta aproximación:&lt;/p&gt;
&lt;p&gt;🚀 &lt;strong&gt;Performance extremo&lt;/strong&gt;: &lt;span class="high"&gt;COPY&lt;/span&gt; es hasta &lt;strong&gt;10-100x más rápido&lt;/strong&gt; que inserts individuales.&lt;br /&gt;&lt;br /&gt;
📦 &lt;strong&gt;Transacción atómica&lt;/strong&gt;: toda la importación ocurre en una sola operación, garantizando consistencia.&lt;br /&gt;&lt;br /&gt;
💾 &lt;strong&gt;Eficiencia de recursos&lt;/strong&gt;: minimiza el uso de memoria y conexiones de base de datos.&lt;br /&gt;&lt;br /&gt;
🔧 &lt;strong&gt;Integración nativa&lt;/strong&gt;: aprovecha capacidades optimizadas del motor &lt;span class="high"&gt;PostgreSQL&lt;/span&gt;.&lt;br /&gt;&lt;br /&gt;
📊 &lt;strong&gt;Escalabilidad&lt;/strong&gt;: permite importar millones de registros sin degradación significativa.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Notas&lt;/h2&gt;
&lt;p&gt;Esta implementación utiliza &lt;span class="high"&gt;COPY … FROM&lt;/span&gt; con archivos del sistema. Actualmente existe una &lt;strong&gt;issue abierta en Vapor&lt;/strong&gt; para implementar soporte nativo de &lt;span class="high"&gt;COPY … FROM STDIN&lt;/span&gt;, que permitiría realizar bulk inserts directamente desde memoria sin necesidad de archivos intermedios.&lt;/p&gt;
&lt;p&gt;Estoy vigilando esta issue de manera activa para integrar esta funcionalidad cuando esté disponible, lo que proporcionará una API aún más flexible y eficiente para operaciones de importación masiva.&lt;/p&gt;
&lt;p&gt;Si también necesitas la operación inversa — exportar datos de PostgreSQL a archivos CSV — lo cubrí en &lt;a href="/es/blog/copy-to/"&gt;Copy To&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link href="https://jcalderita.com/es/blog/copy-from/" rel="alternate"></link>
        <media:content medium="image" url="https://jcalderita.com/static/blog/CopyFrom.webp"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/swift-package/</id>
        <title>Swift Package</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Guía completa sobre los comandos de limpieza de Swift Package Manager: clean, reset, purge-cache y cuándo usar cada uno para resolver problemas de dependencias.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;El dilema de las dependencias rotas&lt;/h2&gt;
&lt;p&gt;Cuando trabajas con &lt;strong&gt;Swift Package Manager (SPM)&lt;/strong&gt;, eventualmente te encontrarás con un error de compilación que parece no tener sentido. Has intentado compilar varias veces, has reiniciado Xcode, pero el error persiste. La solución muchas veces está en &lt;strong&gt;limpiar correctamente las cachés y artefactos&lt;/strong&gt; de SPM, pero ¿qué comando usar?&lt;/p&gt;
&lt;p&gt;Existen múltiples formas de “limpiar” en SPM, cada una con un propósito específico. Usar el comando equivocado puede no resolver tu problema o, peor aún, forzarte a descargar gigabytes de dependencias nuevamente.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Los cuatro caminos para limpiar&lt;/h2&gt;
&lt;p&gt;SPM ofrece tres comandos oficiales más una alternativa manual. Cada uno afecta diferentes partes del sistema:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Comando&lt;/th&gt;
&lt;th&gt;Elimina .build local&lt;/th&gt;
&lt;th&gt;Elimina Package.resolved&lt;/th&gt;
&lt;th&gt;Elimina caché global&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span class="high"&gt;swift package clean&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;❌ (solo binarios compilados)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span class="high"&gt;swift package reset&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;✅ (completo)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span class="high"&gt;swift package purge-cache&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span class="high"&gt;rm -rf .build&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;✅ (completo)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Swift package clean&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;swift package clean
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Qué hace:&lt;/strong&gt;&lt;br /&gt;
Elimina únicamente los &lt;strong&gt;binarios compilados finales&lt;/strong&gt; dentro de la carpeta &lt;span class="high"&gt;.build&lt;/span&gt;, pero &lt;strong&gt;mantiene dependencias descargadas y archivos intermedios&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cuándo usarlo:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Después de cambiar configuraciones de compilación&lt;/li&gt;
&lt;li&gt;Para forzar una recompilación completa sin re-descargar dependencias&lt;/li&gt;
&lt;li&gt;Cuando los binarios están corruptos pero las fuentes están bien&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 No descarga dependencias nuevamente ni resuelve problemas de caché de paquetes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Swift package reset&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;swift package reset
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Qué hace:&lt;/strong&gt;&lt;br /&gt;
Elimina &lt;strong&gt;completamente&lt;/strong&gt; la carpeta &lt;span class="high"&gt;.build&lt;/span&gt; (incluyendo dependencias descargadas) y el archivo &lt;span class="high"&gt;Package.resolved&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cuándo usarlo:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cuando hay conflictos en las versiones de dependencias&lt;/li&gt;
&lt;li&gt;Después de cambios importantes en &lt;span class="high"&gt;Package.swift&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Para resolver errores de “dependencia no encontrada”&lt;/li&gt;
&lt;li&gt;Cuando &lt;span class="high"&gt;Package.resolved&lt;/span&gt; está desactualizado o corrupto&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ La próxima compilación descargará y resolverá todas las dependencias desde cero. Esto puede tardar varios minutos dependiendo de la cantidad de paquetes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Swift package purge-cache&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;swift package purge-cache
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;💡 Disponible desde Swift 5.7 en adelante.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Qué hace:&lt;/strong&gt;&lt;br /&gt;
Elimina la &lt;strong&gt;caché global de paquetes&lt;/strong&gt; ubicada en:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/Library/Caches/org.swift.swiftpm/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Esta caché contiene repositorios clonados y artefactos binarios compartidos entre todos tus proyectos.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cuándo usarlo:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cuando múltiples proyectos tienen el mismo problema&lt;/li&gt;
&lt;li&gt;Después de actualizar Xcode o herramientas de Swift&lt;/li&gt;
&lt;li&gt;Para liberar espacio en disco (puede ocupar varios GB)&lt;/li&gt;
&lt;li&gt;Cuando sospechas que la caché global está corrupta&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ Todos tus proyectos tendrán que re-descargar dependencias comunes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;rm -rf .build&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;rm -rf .build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Qué hace:&lt;/strong&gt;&lt;br /&gt;
Elimina manualmente la carpeta &lt;span class="high"&gt;.build&lt;/span&gt; completa, similar a &lt;span class="high"&gt;reset&lt;/span&gt; pero &lt;strong&gt;sin tocar&lt;/strong&gt; &lt;span class="high"&gt;Package.resolved&lt;/span&gt;. Es menos agresivo que reset porque no re-resuelve dependencias.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cuándo usarlo:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Para limpiar artefactos de compilación manteniendo la resolución de versiones&lt;/li&gt;
&lt;li&gt;En scripts de CI/CD donde quieres control total&lt;/li&gt;
&lt;li&gt;Cuando &lt;span class="high"&gt;swift package reset&lt;/span&gt; no está disponible&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 Preserva &lt;span class="high"&gt;Package.resolved&lt;/span&gt;, lo que significa que las versiones de dependencias no se re-resolverán.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Estrategia de resolución de problemas&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Nivel 1: Limpieza ligera&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;swift package clean
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 Prueba primero lo menos invasivo. Resuelve el 30% de los problemas.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nivel 2: Reset completo del proyecto&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;swift package reset
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 Si el nivel 1 falla. Resuelve el 60% de los problemas restantes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nivel 3: Purgar caché global&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;swift package purge-cache
swift package reset
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 Para problemas persistentes o que afectan múltiples proyectos.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nivel 4: Nuclear&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;rm -rf .build
rm Package.resolved
swift package purge-cache
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 Última opción. Borrón y cuenta nueva total.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Casos de uso reales&lt;/h2&gt;
&lt;h3&gt;Escenario 1: Error después de actualizar Xcode&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;swift package purge-cache
swift package reset
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 Las herramientas de compilación cambiaron y la caché puede tener artefactos incompatibles.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Escenario 2: Conflicto de versiones de dependencias&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;swift package reset
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 Necesitas re-resolver todas las dependencias con las nuevas restricciones.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Escenario 3: Espacio en disco lleno&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;swift package purge-cache
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 La caché global puede crecer hasta varios GB sin que lo notes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Escenario 4: CI/CD builds&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;rm -rf .build
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 En entornos de integración continua, quieres builds limpios pero reproducibles con &lt;code&gt;Package.resolved&lt;/code&gt; versionado.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Entendiendo los componentes&lt;/h2&gt;
&lt;h3&gt;.build (local)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Artefactos de compilación del proyecto actual&lt;/li&gt;
&lt;li&gt;Dependencias descargadas específicas del proyecto&lt;/li&gt;
&lt;li&gt;Archivos intermedios de compilación&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Package.resolved&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;“Lockfile” que fija versiones exactas de dependencias&lt;/li&gt;
&lt;li&gt;Garantiza builds reproducibles&lt;/li&gt;
&lt;li&gt;Debe versionarse en Git para proyectos compartidos&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Caché global&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Compartida entre todos tus proyectos Swift&lt;/li&gt;
&lt;li&gt;Contiene clones de repositorios de dependencias&lt;/li&gt;
&lt;li&gt;Artefactos binarios pre-compilados&lt;/li&gt;
&lt;li&gt;Puede alcanzar varios GB con el tiempo&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Mejores prácticas&lt;/h2&gt;
&lt;p&gt;✅ &lt;strong&gt;Versiona&lt;/strong&gt; &lt;span class="high"&gt;Package.resolved&lt;/span&gt; en Git para garantizar que todo el equipo use las mismas versiones&lt;br /&gt;&lt;br /&gt;
✅ &lt;strong&gt;Usa&lt;/strong&gt; &lt;span class="high"&gt;reset&lt;/span&gt; &lt;strong&gt;después de cambios en Package.swift&lt;/strong&gt; para asegurar una resolución limpia&lt;br /&gt;&lt;br /&gt;
✅ &lt;strong&gt;Purga la caché periódicamente&lt;/strong&gt; si trabajas con muchos proyectos&lt;br /&gt;&lt;br /&gt;
✅ &lt;strong&gt;En CI/CD, mantén&lt;/strong&gt; &lt;span class="high"&gt;Package.resolved&lt;/span&gt; pero elimina &lt;span class="high"&gt;.build&lt;/span&gt; para builds limpios&lt;br /&gt;&lt;br /&gt;
❌ &lt;strong&gt;No ignores&lt;/strong&gt; &lt;span class="high"&gt;.build&lt;/span&gt; &lt;strong&gt;en .gitignore&lt;/strong&gt; - ya está ignorado por defecto&lt;br /&gt;&lt;br /&gt;
❌ &lt;strong&gt;No borres&lt;/strong&gt; &lt;span class="high"&gt;Package.resolved&lt;/span&gt; a menos que realmente necesites re-resolver versiones&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Conclusión&lt;/h2&gt;
&lt;p&gt;Entender la diferencia entre estos comandos te ahorra tiempo y frustración. No todos los problemas de compilación necesitan una limpieza nuclear:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span class="high"&gt;clean&lt;/span&gt; → Solo binarios compilados&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;reset&lt;/span&gt; → Proyecto completo + Package.resolved&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;purge-cache&lt;/span&gt; → Caché global compartida&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;rm -rf&lt;/span&gt; → Control manual quirúrgico&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;La próxima vez que SPM te muestre un error extraño, ya sabes exactamente qué herramienta usar.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link href="https://jcalderita.com/es/blog/swift-package/" rel="alternate"></link>
        <media:content url="https://jcalderita.com/static/blog/SwiftPackage.webp" medium="image"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/sub-process/</id>
        <title>Subprocess</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Cambiando el uso de process por Subprocess, la nueva libreria Subprocess es un paquete multiplataforma para lanzar procesos en Swift.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;Al trabajar con procesos externos en aplicaciones &lt;span class="high"&gt;Swift&lt;/span&gt;, la clase tradicional &lt;span class="high"&gt;Process&lt;/span&gt; presenta varias limitaciones importantes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Gestión manual de recursos&lt;/strong&gt;: Requiere configuración explícita de URLs ejecutables, argumentos y manejo de salidas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Falta de soporte async/await&lt;/strong&gt;: Utiliza métodos síncronos que bloquean el hilo de ejecución&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Manejo complejo de errores&lt;/strong&gt;: Dificulta la captura y procesamiento de errores del proceso hijo&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configuración verbosa&lt;/strong&gt;: Cada ejecución requiere múltiples líneas de configuración repetitiva&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;En el código anterior, necesitaba ejecutar &lt;span class="high"&gt;Ghostscript&lt;/span&gt; para convertir archivos PDF a imágenes PNG, pero la implementación con &lt;span class="high"&gt;Process&lt;/span&gt; resulta extensa y poco elegante.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;func _PDFToImages(
    _ fileName: PathEnum
) async throws {
    let process = Process()

    process.executableURL = URL(
        fileURLWithPath: &amp;quot;/opt/homebrew/bin/gs&amp;quot;
    )

    process.arguments = [
        &amp;quot;-dNOPAUSE&amp;quot;, &amp;quot;-dBATCH&amp;quot;,
        &amp;quot;-dQUIET&amp;quot;, &amp;quot;-sDEVICE=png16m&amp;quot;,
        &amp;quot;-r300&amp;quot;,
        &amp;quot;-sOutputFile=\(fileName.rawValue)-%d.png&amp;quot;,
        fileName.rawValue.appending(&amp;quot;.pdf&amp;quot;),
    ]

    try process.run()
    process.waitUntilExit()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;La nueva librería &lt;span class="high"&gt;Subprocess&lt;/span&gt; de Apple proporciona una API moderna y robusta para la ejecución de procesos en &lt;span class="high"&gt;Swift&lt;/span&gt;. Esta librería &lt;strong&gt;multiplataforma&lt;/strong&gt; ofrece soporte nativo para &lt;span class="high"&gt;async/await&lt;/span&gt; y gestión automática de recursos.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Características principales:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;API nativa async/await&lt;/strong&gt; para operaciones no bloqueantes.&lt;br /&gt;&lt;br /&gt;
✅ &lt;strong&gt;Gestión automática de recursos&lt;/strong&gt; y limpieza de procesos.&lt;br /&gt;&lt;br /&gt;
✅ &lt;strong&gt;Control granular de salidas&lt;/strong&gt; (stdout, stderr).&lt;br /&gt;&lt;br /&gt;
✅ &lt;strong&gt;Verificación de estado de terminación&lt;/strong&gt; integrada.&lt;br /&gt;&lt;br /&gt;
✅ &lt;strong&gt;Sintaxis concisa y expresiva.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;La implementación modernizada utiliza la función &lt;span class="high"&gt;run&lt;/span&gt; de &lt;span class="high"&gt;Subprocess&lt;/span&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;func _PDFToImages(
    _ fileName: PathEnum
) async throws {
    let result = try await run(
        .path(.init(&amp;quot;/opt/homebrew/bin/gs&amp;quot;)),
        arguments: [
            &amp;quot;-dNOPAUSE&amp;quot;, &amp;quot;-dBATCH&amp;quot;,
            &amp;quot;-dQUIET&amp;quot;, &amp;quot;-sDEVICE=png16m&amp;quot;,
            &amp;quot;-r300&amp;quot;,
            &amp;quot;-sOutputFile=\(fileName.rawValue)-%d.png&amp;quot;,
            fileName.rawValue.appending(&amp;quot;.pdf&amp;quot;),
        ],
        output: .discarded,
        error: .string(limit: .max)
    )

    try guardAndLogError(
        result.terminationStatus == .exited(0),
        message: result.standardError,
        status: .internalServerError
    )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Parámetros clave:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span class="high"&gt;.path&lt;/span&gt;: Especifica el ejecutable de forma directa&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;output: .discarded&lt;/span&gt;: Descarta la salida estándar ya que no necesitamos procesarla&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;error: .string(limit: .max)&lt;/span&gt;: Captura errores como string para logging&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;result.terminationStatus&lt;/span&gt;: Verifica que el proceso terminó exitosamente&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;p&gt;La migración a &lt;span class="high"&gt;Subprocess&lt;/span&gt; transforma el código de gestión de procesos en una solución &lt;strong&gt;más limpia, segura y eficiente&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Beneficios obtenidos:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;🚀 &lt;strong&gt;Rendimiento mejorado&lt;/strong&gt; con operaciones asíncronas reales.&lt;br /&gt;&lt;br /&gt;
🔒 &lt;strong&gt;Mejor manejo de errores&lt;/strong&gt; con captura integrada de stderr.&lt;br /&gt;&lt;br /&gt;
📝 &lt;strong&gt;Código más legible&lt;/strong&gt; con menos configuración manual.&lt;br /&gt;&lt;br /&gt;
⚡ &lt;strong&gt;Integración perfecta&lt;/strong&gt; con el ecosistema moderno de Swift concurrency.&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;La nueva implementación no solo reduce la complejidad sino que también &lt;strong&gt;mejora la robustez&lt;/strong&gt; del sistema al proporcionar mejor visibilidad de errores y un manejo más elegante de la ejecución asíncrona en aplicaciones &lt;span class="high"&gt;Vapor&lt;/span&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Nota sobre DYLD_LIBRARY_PATH&lt;/h2&gt;
&lt;p&gt;En versiones anteriores de &lt;span class="high"&gt;Swift&lt;/span&gt;, existía un problema conocido con la variable de entorno &lt;span class="high"&gt;DYLD_LIBRARY_PATH&lt;/span&gt; al ejecutar procesos externos. Debido a las restricciones de &lt;strong&gt;System Integrity Protection (SIP)&lt;/strong&gt; de macOS, esta variable era eliminada automáticamente al lanzar subprocesos, lo que causaba errores de “Library not loaded” en ciertos casos.&lt;/p&gt;
&lt;p&gt;La solución temporal requería configurar manualmente las rutas de las librerías usando &lt;span class="high"&gt;install_name_tool&lt;/span&gt; con &lt;span class="high"&gt;@rpath&lt;/span&gt;, o bien establecer la variable &lt;span class="high"&gt;DYLD_LIBRARY_PATH&lt;/span&gt; en su lugar.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;¡Buenas noticias!&lt;/strong&gt; 🎉 Este problema ha sido resuelto en las versiones más recientes de &lt;span class="high"&gt;Swift&lt;/span&gt;. La librería &lt;span class="high"&gt;Subprocess&lt;/span&gt; maneja correctamente las variables de entorno del sistema, incluida &lt;span class="high"&gt;DYLD_LIBRARY_PATH&lt;/span&gt;, sin necesidad de configuraciones adicionales.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link href="https://jcalderita.com/es/blog/sub-process/" rel="alternate"></link>
        <media:content medium="image" url="https://jcalderita.com/static/blog/Subprocess.webp"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/json-snakecase/</id>
        <title>Json Snake Case</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Conveniencias para codificar/decodificar JSON en snake_case sin boilerplate, válidas para cliente iOS y servidor Vapor.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;Cuando integramos APIs REST, es muy común que las claves JSON vengan en &lt;span class="high"&gt;snake_case&lt;/span&gt; (por ejemplo, &lt;strong&gt;first_name&lt;/strong&gt;) mientras que en Swift modelamos las propiedades en &lt;span class="high"&gt;camelCase&lt;/span&gt; (&lt;strong&gt;firstName&lt;/strong&gt;).&lt;br /&gt;
Si no configuramos nada, nos toca escribir &lt;span class="high"&gt;CodingKeys&lt;/span&gt; a mano en cada modelo o aceptar errores de decodificación.&lt;/p&gt;
&lt;p&gt;Buscamos una forma &lt;strong&gt;centralizada&lt;/strong&gt; y &lt;strong&gt;reutilizable&lt;/strong&gt; de:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Decodificar JSON sin &lt;span class="high"&gt;CodingKeys&lt;/span&gt; manuales.&lt;/li&gt;
&lt;li&gt;Codificar nuestros modelos al enviar datos.&lt;/li&gt;
&lt;li&gt;Mantener el mismo comportamiento tanto en apps iOS/macOS (URLSession) como en &lt;strong&gt;Vapor&lt;/strong&gt; (req/res content).&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;Creamos extensiones de &lt;span class="high"&gt;JSONDecoder&lt;/span&gt; y &lt;span class="high"&gt;JSONEncoder&lt;/span&gt; que exponen constructores de conveniencia y atajos estáticos &lt;span class="high"&gt;snakeCase&lt;/span&gt;.&lt;br /&gt;
Ventajas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cero boilerplate&lt;/strong&gt; en los modelos: evita &lt;span class="high"&gt;CodingKeys&lt;/span&gt; repetitivas.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nombre autoexplicativo&lt;/strong&gt; al usarlas: &lt;span class="high"&gt;JSONDecoder.snakeCase&lt;/span&gt; / &lt;span class="high"&gt;JSONEncoder.snakeCase&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consistencia&lt;/strong&gt; en todo el proyecto (cliente y servidor).&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;extension JSONDecoder {
    convenience init(keyDecodingStrategy: KeyDecodingStrategy) {
        self.init()
        self.keyDecodingStrategy = keyDecodingStrategy
    }

    static var snakeCase: JSONDecoder {
        .init(keyDecodingStrategy: .convertFromSnakeCase)
    }
}

extension JSONEncoder {
    convenience init(keyEncodingStrategy: KeyEncodingStrategy) {
        self.init()
        self.keyEncodingStrategy = keyEncodingStrategy
    }

    static var snakeCase: JSONEncoder {
        .init(keyEncodingStrategy: .convertToSnakeCase)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Nota: si un modelo necesita un nombre de clave específico, puedes seguir usando &lt;span class="high"&gt;CodingKeys&lt;/span&gt; localmente; la estrategia &lt;span class="high"&gt;snake_case&lt;/span&gt; actuará como valor por defecto.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Ejemplos de uso&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;// iOS / macOS: leer datos
let data: Data = ...
let user = try JSONDecoder.snakeCase
    .decode(UserDTO.self, from: data)

// iOS / macOS: enviar datos
let body = CreateUserDTO(firstName: &amp;quot;Ada&amp;quot;, lastName: &amp;quot;Lovelace&amp;quot;)
request.httpBody = try JSONEncoder.snakeCase.encode(body)

// Vapor
let input = try req.content
    .decode(CreateUserDTO.self, using: JSONDecoder.snakeCase)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Con estos atajos obtenemos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Menos errores&lt;/strong&gt; y mayor &lt;strong&gt;legibilidad&lt;/strong&gt;: las propiedades permanecen en camelCase idiomático Swift.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interoperabilidad&lt;/strong&gt; inmediata con APIs legacy en snake_case.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configuración única&lt;/strong&gt; y reutilizable en todo el proyecto (tests incluidos).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Esto estandariza cómo serializamos/parseamos JSON sin sacrificar claridad ni control fino cuando hace falta.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link href="https://jcalderita.com/es/blog/json-snakecase/" rel="alternate"></link>
        <media:content url="https://jcalderita.com/static/blog/JsonSnakeCase.webp" medium="image"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/array-dictionary/</id>
        <title>Array to dictionary</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Convierte un Array de elementos Identifiable (ID opcional UUID?) en un diccionario [UUID: Element], ignorando IDs nulos y manteniendo acceso O(1).</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;Al trabajar con listas de modelos (por ejemplo, resultados, eventos o usuarios), a menudo necesitamos acceso &lt;span class="high"&gt;O(1)&lt;/span&gt; por identificador para búsquedas, merges o deduplicación.&lt;/p&gt;
&lt;p&gt;Sin embargo, en muchos dominios el &lt;strong&gt;id&lt;/strong&gt; puede ser opcional (&lt;span class="high"&gt;UUID?&lt;/span&gt;) hasta que el backend lo asigne. Si construimos un diccionario directamente, aparecen dos fricciones:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Hay que &lt;strong&gt;filtrar los elementos sin ID&lt;/strong&gt; para evitar entradas inválidas.&lt;/li&gt;
&lt;li&gt;Debemos &lt;strong&gt;garantizar unicidad&lt;/strong&gt; de las claves o el constructor de &lt;span class="high"&gt;Dictionary(uniqueKeysWithValues:)&lt;/span&gt; fallará en tiempo de ejecución si hay duplicados.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;Extendemos &lt;span class="high"&gt;Array&lt;/span&gt; (cuando sus elementos son &lt;span class="high"&gt;Identifiable&lt;/span&gt; con &lt;span class="high"&gt;ID == UUID?&lt;/span&gt;) para exponer &lt;span class="high"&gt;toDictionary()&lt;/span&gt;.&lt;br /&gt;
La función usa &lt;span class="high"&gt;compactMap&lt;/span&gt; para &lt;strong&gt;descartar elementos sin ID&lt;/strong&gt; y construye el diccionario con &lt;span class="high"&gt;Dictionary(uniqueKeysWithValues:)&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Precondición&lt;/strong&gt;: los IDs existentes deben ser &lt;strong&gt;únicos&lt;/strong&gt; en la colección. Si esperas colisiones, considera una variante con &lt;span class="high"&gt;Dictionary(_, uniquingKeysWith:)&lt;/span&gt; para resolver duplicados.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;extension Array where Element: Identifiable, Element.ID == UUID? {
    func toDictionary() -&amp;gt; [UUID: Element] {
        Dictionary(uniqueKeysWithValues: self.compactMap {
            guard let id = $0.id else { return nil }
            return (id, $0) }
        )
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Los elementos con &lt;span class="high"&gt;id == nil&lt;/span&gt; &lt;strong&gt;no&lt;/strong&gt; se incluyen.&lt;/li&gt;
&lt;li&gt;El acceso por clave es &lt;span class="high"&gt;O(1)&lt;/span&gt; y simplifica merges/joins en memoria.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;p&gt;Un helper pequeño y claro para pasar de un array a un mapa por ID:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Más rápido de consultar&lt;/strong&gt; &lt;span class="high"&gt;O(1)&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Más seguro&lt;/strong&gt;: evita insertar elementos sin identificador.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Más expresivo&lt;/strong&gt; y reutilizable en servicios y view models.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link href="https://jcalderita.com/es/blog/array-dictionary/" rel="alternate"></link>
        <media:content url="https://jcalderita.com/static/blog/ToDictionary.webp" medium="image"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/schedule-queues/</id>
        <title>Schedule Queues</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Programa un mismo ScheduledJob varias veces por hora con una API expresiva: cada N minutos, sin repetir configuración ni cron strings.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;Cuando usamos &lt;span class="high"&gt;Vapor Queues&lt;/span&gt; para planificar trabajos, la API típica es:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;app.queues
    .schedule(MyJob()).hourly().at(0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Esto planifica cada hora. Si queremos ejecutarlo &lt;strong&gt;cada N minutos&lt;/strong&gt; (p. ej., cada 5, 10 o 15), tenemos que registrar manualmente varios &lt;span class="high"&gt;.at(…)&lt;/span&gt;, lo que acaba en &lt;strong&gt;código repetitivo&lt;/strong&gt;, propenso a errores (minutos duplicados/olvidados) y poco legible.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;Extraemos un helper sobre &lt;span class="high"&gt;Application.Queues&lt;/span&gt; que registra el mismo &lt;span class="high"&gt;ScheduledJob&lt;/span&gt; &lt;strong&gt;múltiples veces dentro de la hora&lt;/strong&gt; usando un &lt;span class="high"&gt;stride&lt;/span&gt; con paso &lt;span class="high"&gt;minutes&lt;/span&gt;. Así, con una sola llamada expresamos: &lt;em&gt;“ejecútalo cada N minutos”&lt;/em&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;extension Application.Queues {
    func scheduleEvery(
        _ job: ScheduledJob, 
        minutes: Int
    ) {
        for minuteOffset in stride(
            from: 0, 
            to: 60, 
            by: minutes
        ) {
            schedule(job).hourly()
                .at(.init(integerLiteral: minuteOffset))
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Puntos clave:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Usa &lt;span class="high"&gt;stride(from: 0, to: 60, by: minutes)&lt;/span&gt; para generar los offsets de minuto dentro de la hora.&lt;/li&gt;
&lt;li&gt;Por cada offset se registra &lt;span class="high"&gt;hourly().at(…)&lt;/span&gt;, evitando duplicar lógica.&lt;/li&gt;
&lt;li&gt;Si &lt;span class="high"&gt;minutes&lt;/span&gt; &lt;strong&gt;no divide 60&lt;/strong&gt;, la última ejecución será el mayor múltiplo &amp;lt; 60 (p. ej., &lt;span class="high"&gt;minutes = 7&lt;/span&gt; ⇒ 0, 7, 14, …, 56).&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;p&gt;Ejemplo de uso:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;func configure(_ app: Application) throws {
    app.queues.scheduleEvery(MyJob(), minutes: 5)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Una API &lt;strong&gt;concisa y autoexpresiva&lt;/strong&gt; para tareas periódicas cortas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Menos boilerplate&lt;/strong&gt;: una sola llamada registra todos los disparos.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Menos errores&lt;/strong&gt;: sin listas manuales de &lt;span class="high"&gt;.at(…)&lt;/span&gt; ni minutos duplicados.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Legible y testeable&lt;/strong&gt;: el patrón es evidente y fácil de verificar.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link href="https://jcalderita.com/es/blog/schedule-queues/" rel="alternate"></link>
        <media:content url="https://jcalderita.com/static/blog/ScheduleQueues.webp" medium="image"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/task-list/</id>
        <title>Async Task List</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Implementación de un sistema robusto para la gestión de listas de tareas asíncronas con persistencia de estado en base de datos utilizando Swift y Vapor.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;En sistemas distribuidos con microservicios, un proceso complejo puede generar dificultades de control y fiabilidad. Una solución es &lt;strong&gt;dividirlo&lt;/strong&gt; en tareas &lt;strong&gt;atómicas&lt;/strong&gt;, lo que permite un control más granular del &lt;strong&gt;flujo&lt;/strong&gt;, mejorar la &lt;strong&gt;observabilidad&lt;/strong&gt;, asegurar la &lt;strong&gt;idempotencia&lt;/strong&gt; y facilitar la &lt;strong&gt;recuperación&lt;/strong&gt; ante fallos.&lt;/p&gt;
&lt;p&gt;El reto es diseñar un patrón que permita una &lt;strong&gt;degradación controlada&lt;/strong&gt;, de modo que el fallo de una tarea no afecte al flujo completo, garantizando &lt;strong&gt;resiliencia&lt;/strong&gt; y &lt;strong&gt;tolerancia a fallos&lt;/strong&gt; en entornos de producción.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;Crear un &lt;span class="high"&gt;task executor&lt;/span&gt; que orquesta tareas asíncronas.&lt;br /&gt;
La función &lt;span class="high"&gt;execute&lt;/span&gt; envuelve cada tarea, gestiona errores con &lt;span class="high"&gt;do-catch&lt;/span&gt; como &lt;span class="high"&gt;circuit breaker&lt;/span&gt;, actualiza el estado en caso de &lt;strong&gt;éxito&lt;/strong&gt;, registra y persiste los fallos, y asegura una &lt;strong&gt;transacción atómica&lt;/strong&gt; para mantener la consistencia de datos.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;func execute(
    status: StatusEnum,
    process: ProcessModel,
    _ work: () async throws -&amp;gt; ProcessModel
) async throws {
    do {
        let job = try await work()
        process.setStatus(job.status)
    } catch {
        process.setError(type: status, message: &amp;quot;\(error)&amp;quot;)
    }
    try await repo.updateProcess(process)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;p&gt;Esta implementación ofrece una &lt;strong&gt;abstracción de alto nivel&lt;/strong&gt; que permite una &lt;strong&gt;orquestación precisa&lt;/strong&gt; de procesos asíncronos complejos con garantías de &lt;strong&gt;atomicidad&lt;/strong&gt; y &lt;strong&gt;durabilidad&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;🎯 &lt;strong&gt;Separation of Concerns:&lt;/strong&gt; cada tarea aísla su contexto de ejecución y su gestión de errores.&lt;br /&gt;&lt;br /&gt;
🔄 &lt;strong&gt;Workflow Orchestration:&lt;/strong&gt; posibilita encadenar tareas mediante el pipeline pattern.&lt;br /&gt;&lt;br /&gt;
📊 &lt;strong&gt;Observability:&lt;/strong&gt; genera un audit trail completo para depuración y monitorización.&lt;br /&gt;&lt;br /&gt;
⚡ &lt;strong&gt;Performance:&lt;/strong&gt; mantiene alta concurrencia sin comprometer la consistencia de datos.&lt;br /&gt;&lt;br /&gt;
🛡️ &lt;strong&gt;Resilience:&lt;/strong&gt; incorpora fail-fast y recovery patterns automáticos.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;try await execute(status: .loadImages, process: process) {
    // Your implementation
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link rel="alternate" href="https://jcalderita.com/es/blog/task-list/"></link>
        <media:content medium="image" url="https://jcalderita.com/static/blog/TaskList.webp"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/multi-step/</id>
        <title>Multi-Step Process</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Patrón en Swift para orquestar procesos complejos de varios pasos con control de estado y recuperación ante fallos.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;En sistemas distribuidos y procesos de backend, es común que una operación compleja requiera ejecutar &lt;strong&gt;múltiples pasos secuenciales&lt;/strong&gt; con dependencias entre sí: creación de ficheros, subida, generación de respuestas, validación, limpieza, etc.&lt;br /&gt;
Controlar este flujo de estados es crítico para:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Garantizar que cada paso se ejecute en el &lt;strong&gt;orden correcto&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Permitir la &lt;strong&gt;recuperación&lt;/strong&gt; en caso de error o reinicio del sistema.&lt;/li&gt;
&lt;li&gt;Mantener la &lt;strong&gt;consistencia de datos&lt;/strong&gt; incluso si el proceso se interrumpe.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sin una estrategia clara, el código puede volverse frágil, difícil de escalar y propenso a errores.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;Se implementa una función de &lt;strong&gt;gestión de estado por etapas&lt;/strong&gt; que controla el avance de un &lt;span class="high"&gt;ProcessModel&lt;/span&gt; a través de su ciclo de vida.&lt;br /&gt;
La idea es &lt;strong&gt;encapsular la lógica de transición de estado&lt;/strong&gt; en una única función que evalúa el estado actual y ejecuta la acción correspondiente, hasta que el proceso alcanza su estado final.&lt;/p&gt;
&lt;p&gt;El patrón se apoya en un bucle &lt;span class="high"&gt;repeat-while&lt;/span&gt; que vuelve a evaluar el estado tras cada operación, garantizando que las transiciones ocurran de forma &lt;strong&gt;determinista&lt;/strong&gt; y &lt;strong&gt;resiliente&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;func checkProcess(
    _ process: ProcessModel
) async throws {
    var process = process
    var status = process.status

    repeat {
        status = process.status
        process = try await checkStatus(process)
    } while status != process.status
}

func checkStatus(
    _ process: ProcessModel
) async throws -&amp;gt; ProcessModel {
    switch process.status {
        case .filesCreated: 
            try await _uploadFiles(process)
        case .filesUploaded: 
            try await _createResponses(process)
        case .responsesCreated, .responsesReasoning: 
            try await _checkResponses(process)
        case .responsesCompleted: 
            try await _deleteFiles(process)
        case .filesDeleted: 
            try await _deleteResponses(process)
        case .responsesDeleted: 
            try await _finishResponses(process)
        case .responsesFinished: 
            try await _deleteProcess(process)
        default: process
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Claves de la implementación:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Centralización de estados&lt;/strong&gt;: un único punto de control define todas las transiciones.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reevaluación continua&lt;/strong&gt;: el bucle &lt;span class="high"&gt;repeat-while&lt;/span&gt; permite avanzar automáticamente mientras haya cambios de estado.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Aislamiento de operaciones&lt;/strong&gt;: cada caso del &lt;span class="high"&gt;switch&lt;/span&gt; delega en funciones especializadas (&lt;span class="high"&gt;_uploadFiles&lt;/span&gt;, &lt;span class="high"&gt;_createResponses&lt;/span&gt;, etc.), manteniendo el código limpio y testeable.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;p&gt;Este patrón de &lt;strong&gt;gestión de procesos multi‑etapa&lt;/strong&gt; aporta:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Resiliencia&lt;/strong&gt;: cada transición es atómica y se puede reintentar si ocurre un fallo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Escalabilidad&lt;/strong&gt;: agregar nuevos pasos solo requiere añadir un nuevo caso al &lt;span class="high"&gt;switch&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Claridad&lt;/strong&gt;: el flujo completo del proceso se entiende con una sola lectura.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ejemplo de aplicación: pipelines de procesamiento de datos, flujos de publicación de contenido, o cualquier proceso de larga duración que requiera &lt;strong&gt;control preciso de cada etapa&lt;/strong&gt; sin comprometer la integridad de los datos.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link href="https://jcalderita.com/es/blog/multi-step/" rel="alternate"></link>
        <media:content url="https://jcalderita.com/static/blog/MultiStep.webp" medium="image"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/concurrent-map/</id>
        <title>Concurrent Map</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Crea una extensión concurrentMap sobre Sequence con concurrencia estructurada de Swift para ejecutar transformaciones async en paralelo de forma segura y eficiente.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;Cuando procesamos colecciones en Swift, el método &lt;span class="high"&gt;map&lt;/span&gt; ejecuta las transformaciones de forma secuencial.&lt;br /&gt;
En operaciones intensivas o que implican I/O —como peticiones HTTP, lectura de archivos o consultas de base de datos— esto puede ser un cuello de botella.&lt;br /&gt;
El objetivo es &lt;strong&gt;aprovechar la concurrencia&lt;/strong&gt; para ejecutar múltiples transformaciones en paralelo, garantizando seguridad con &lt;span class="high"&gt;Sendable&lt;/span&gt; y manejo de errores.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;Se crea una extensión de &lt;span class="high"&gt;Sequence&lt;/span&gt; que añade &lt;span class="high"&gt;concurrentMap&lt;/span&gt;.&lt;br /&gt;
Internamente usa &lt;span class="high"&gt;withThrowingTaskGroup&lt;/span&gt;, lo que permite:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lanzar una tarea por cada elemento de la secuencia.&lt;/li&gt;
&lt;li&gt;Ejecutar todas las transformaciones en paralelo, respetando el modelo de concurrencia estructurada de Swift.&lt;/li&gt;
&lt;li&gt;Propagar automáticamente el primer error que se produzca, cancelando las tareas restantes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;El uso de &lt;span class="high"&gt;@Sendable&lt;/span&gt; asegura que tanto los elementos como el resultado sean seguros en entornos concurrentes.&lt;br /&gt;
De esta manera, cualquier operación asíncrona puede beneficiarse de la &lt;strong&gt;ejecución en paralelo&lt;/strong&gt; sin sacrificar la claridad del código.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;extension Sequence where Element: Sendable {
    func concurrentMap&amp;lt;T: Sendable&amp;gt;(
        _ transform: @escaping @Sendable (Element) async throws -&amp;gt; T
    ) async throws -&amp;gt; [T] {
        try await withThrowingTaskGroup(of: T.self) { group in
            for element in self {
                group.addTask {
                    try await transform(element)
                }
            }
            return try await group.reduce(into: []) { 
                $0.append($1) 
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;p&gt;Con &lt;span class="high"&gt;concurrentMap&lt;/span&gt; se consigue una &lt;strong&gt;ganancia de rendimiento significativa&lt;/strong&gt; en operaciones asíncronas que pueden ejecutarse en paralelo.&lt;br /&gt;
El patrón respeta las reglas de &lt;span class="high"&gt;concurrencia estructurada&lt;/span&gt; de Swift, evita &lt;em&gt;data races&lt;/em&gt; y mantiene el mismo estilo declarativo que &lt;span class="high"&gt;map&lt;/span&gt;, por lo que su adopción en proyectos existentes es sencilla.&lt;/p&gt;
&lt;p&gt;Ejemplo de uso:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;let urls: [URL] = [...]
let contents = try await urls.concurrentMap {
    try await fetchContent(from: $0)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Este enfoque es ideal para peticiones HTTP en lotes, procesamiento de imágenes, lectura masiva de datos o cualquier escenario que requiera &lt;strong&gt;máximo paralelismo seguro&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Si necesitas &lt;strong&gt;orden garantizado&lt;/strong&gt; o limitar el consumo de recursos a una tarea a la vez, lo cubrí en &lt;a href="/es/blog/async-map/"&gt;Async Map&lt;/a&gt;. Y si quieres combinar ambas estrategias — procesamiento concurrente dentro de chunks controlados — lo construí en &lt;a href="/es/blog/async-concurrent-map/"&gt;Async Concurrent Map&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link rel="alternate" href="https://jcalderita.com/es/blog/concurrent-map/"></link>
        <media:content medium="image" url="https://jcalderita.com/static/blog/ConcurrentMap.webp"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/async-map/</id>
        <title>Async Map</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Crea una extensión asyncMap secuencial sobre Sequence en Swift para transformar colecciones con async/await preservando el orden y limitando recursos.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;Cuando procesamos colecciones con transformaciones asíncronas, el método &lt;span class="high"&gt;map&lt;/span&gt; no sirve porque sus closures no pueden ser &lt;span class="high"&gt;async&lt;/span&gt;.&lt;br /&gt;
Una solución ingenua sería usar un bucle &lt;span class="high"&gt;for&lt;/span&gt; con &lt;span class="high"&gt;await&lt;/span&gt;, pero se pierde legibilidad y un manejo de errores homogéneo.&lt;br /&gt;
El objetivo es disponer de un &lt;span class="high"&gt;map&lt;/span&gt; &lt;strong&gt;secuencial&lt;/strong&gt; que acepte funciones &lt;span class="high"&gt;async throws&lt;/span&gt;, preserve el orden de los elementos y simplifique el flujo de trabajo.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;Se crea una extensión de &lt;span class="high"&gt;Sequence&lt;/span&gt; que añade &lt;span class="high"&gt;asyncMap&lt;/span&gt;.&lt;br /&gt;
Su ejecución es &lt;strong&gt;secuencial&lt;/strong&gt; dentro de la misma tarea, lo que permite:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mantener el orden determinista de los resultados.&lt;/li&gt;
&lt;li&gt;Limitar el consumo de recursos ejecutando solo una operación a la vez.&lt;/li&gt;
&lt;li&gt;Propagar de manera uniforme el primer error que se produzca, deteniendo el proceso.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;extension Sequence {
    @inlinable
    func asyncMap&amp;lt;T&amp;gt;(
        _ transform: @escaping @Sendable (Element) async throws -&amp;gt; T
    ) async throws -&amp;gt; [T] {
        var results: [T] = []
        results.reserveCapacity(underestimatedCount)
        for element in self {
            let value = try await transform(element)
            results.append(value)
        }
        return results
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;p&gt;Con &lt;span class="high"&gt;asyncMap&lt;/span&gt; se obtiene un flujo claro y seguro para transformaciones asíncronas &lt;strong&gt;sin paralelismo&lt;/strong&gt;.&lt;br /&gt;
Es especialmente útil cuando:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Necesitamos orden garantizado.&lt;/li&gt;
&lt;li&gt;Debemos limitar el consumo de recursos (una tarea en vuelo).&lt;/li&gt;
&lt;li&gt;Existe dependencia entre las operaciones.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Si necesitas &lt;strong&gt;ejecución en paralelo&lt;/strong&gt; en lugar de secuencial, exploré ese enfoque en &lt;a href="/es/blog/concurrent-map/"&gt;Concurrent Map&lt;/a&gt;. Y si tus llamadas secuenciales alcanzan límites de tasa, añadí un mecanismo de timeout en &lt;a href="/es/blog/async-map-timeout/"&gt;Async Map Timeout&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Ejemplo de uso:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;let urls: [URL] = [...]
let contents = try await urls.asyncMap {
    try await fetchContent(from: $0)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link rel="alternate" href="https://jcalderita.com/es/blog/async-map/"></link>
        <media:content medium="image" url="https://jcalderita.com/static/blog/AsyncMap.webp"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/guard-log/</id>
        <title>Guard and LogError</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Crea una función genérica en Swift que combina guard-let, logging de errores y lanzamiento de excepciones en una sola línea reutilizable para backends Vapor.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;En múltiples secciones de mi código necesito ejecutar tres operaciones cada vez que manejo un &lt;span class="high"&gt;Optional&lt;/span&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Desempaquetar&lt;/strong&gt; el contenido del &lt;span class="high"&gt;Optional&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Registrar un error&lt;/strong&gt; en el sistema de logs si el valor es &lt;span class="high"&gt;nil&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lanzar una excepción&lt;/strong&gt; cuando el valor no existe&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Este patrón se repite frecuentemente, generando código duplicado y reduciendo la mantenibilidad.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;La estrategia consiste en &lt;strong&gt;encapsular las tres operaciones&lt;/strong&gt; en funciones reutilizables que trabajen de forma coordinada.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Función auxiliar:&lt;/strong&gt;&lt;/em&gt; &lt;span class="high"&gt;logError&lt;/span&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;func logError(
    _ message: String, 
    status: HTTPResponseStatus
) throws -&amp;gt; Never {
    self.logMessage(message, level: .error)
    throw Abort(status, reason: message)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Esta función ejecuta el &lt;strong&gt;registro del error&lt;/strong&gt; en el sistema de logs y posteriormente &lt;strong&gt;termina la ejecución&lt;/strong&gt; lanzando una excepción &lt;span class="high"&gt;Abort&lt;/span&gt;. El tipo de retorno &lt;span class="high"&gt;Never&lt;/span&gt; es fundamental, ya que indica al compilador que esta función &lt;strong&gt;nunca retorna normalmente&lt;/strong&gt;, garantizando que la ejecución se interrumpa completamente.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Función principal:&lt;/strong&gt;&lt;/em&gt; &lt;span class="high"&gt;guardAndLogError&lt;/span&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;func guardAndLogError&amp;lt;T&amp;gt;(
    _ optional: T?,
    message: String,
    status: HTTPResponseStatus = .noContent
) throws -&amp;gt; T {
    guard let optional else {
        try logError(message, status: status)
    }
    return optional
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Esta &lt;strong&gt;función genérica&lt;/strong&gt; implementa el patrón completo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Utiliza &lt;span class="high"&gt;guard let&lt;/span&gt; para &lt;strong&gt;desempaquetar el Optional&lt;/strong&gt; de forma segura&lt;/li&gt;
&lt;li&gt;Si el valor es &lt;span class="high"&gt;nil&lt;/span&gt;, invoca &lt;span class="high"&gt;logError()&lt;/span&gt; para registrar el fallo y terminar la ejecución&lt;/li&gt;
&lt;li&gt;Si contiene un valor, lo &lt;strong&gt;retorna exitosamente&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;La &lt;strong&gt;genericidad&lt;/strong&gt; &lt;span class="high"&gt;T&lt;/span&gt; permite usar esta función con cualquier tipo de dato opcional.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;p&gt;Con esta implementación, &lt;strong&gt;una sola línea de código&lt;/strong&gt; ejecuta las tres operaciones requeridas: desempaquetado seguro, logging de errores y manejo de excepciones.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;let fileName = try guardAndLogError(
    fileName, 
    message: &amp;quot;Valor fileName no encontrado&amp;quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Beneficios&lt;/h2&gt;
&lt;p&gt;✅ &lt;strong&gt;Reducción de código duplicado&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;
✅ &lt;strong&gt;Manejo consistente de errores&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;
✅ &lt;strong&gt;Logs centralizados y estructurados&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;
✅ &lt;strong&gt;Reutilización mediante genericidad&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Extensibilidad&lt;/h2&gt;
&lt;p&gt;Este patrón puede &lt;strong&gt;extenderse&lt;/strong&gt; para casos de uso más específicos:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;// Para validaciones booleanas
func guardAndLogError(
    _ condition: Bool, 
    message: String
) throws { ... }

// Para arrays
func guardAndLogError&amp;lt;T&amp;gt;(
    _ optionals: T?..., 
    message: String
) throws -&amp;gt; [T] { ... }

// Para tuplas
func guardAndLogError&amp;lt;T, U&amp;gt;(
    _ first: T?, 
    _ second: U?
    , message: String
) throws -&amp;gt; (T, U) { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link rel="alternate" href="https://jcalderita.com/es/blog/guard-log/"></link>
        <media:content medium="image" url="https://jcalderita.com/static/blog/GuardLog.webp"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/schema-space/</id>
        <title>Esquemas y Espacios</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Aprende a usar la propiedad space en los modelos Fluent de Vapor para organizar tablas en namespaces, y evita un sutil bug en la declaración de tipo opcional.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;Esquema y Espacio de Nombres&lt;/h2&gt;
&lt;p&gt;Al definir un modelo de datos en Vapor, es posible especificar el &lt;span class="high"&gt;schema&lt;/span&gt;, que corresponde al nombre de la tabla en la base de datos. Sin embargo, para mantener una arquitectura organizada, es recomendable agrupar las tablas en diferentes espacios de nombres según criterios funcionales, en lugar de concentrarlas todas en el esquema predeterminado. Esta práctica facilita la separación lógica por dominios o módulos de la aplicación.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Implementación&lt;/h2&gt;
&lt;p&gt;Para asignar una tabla a un espacio de nombres específico, se debe sobrescribir la propiedad estática &lt;span class="high"&gt;space&lt;/span&gt; en el modelo. Esta propiedad permite definir el namespace donde residirá la tabla, proporcionando una organización más granular de la estructura de base de datos.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;public final class LocationCityModel: Model {
    public static let schema = &amp;quot;cities&amp;quot;
    public static let space: String? = &amp;quot;location&amp;quot;

    @ID() public var id: UUID?
    @Field(.name) public var name: String

    public init() { }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Consideración&lt;/h2&gt;
&lt;p&gt;Es fundamental declarar la propiedad &lt;span class="high"&gt;space&lt;/span&gt; como tipo &lt;span class="high"&gt;String?&lt;/span&gt; &lt;strong&gt;(opcional)&lt;/strong&gt;. Si se declara como &lt;span class="high"&gt;String&lt;/span&gt; &lt;strong&gt;(no opcional)&lt;/strong&gt;, no se sobrescribirá la propiedad heredada del protocolo &lt;span class="high"&gt;Model&lt;/span&gt;, sino que se creará una nueva propiedad con el mismo nombre. Esto ocasionará que el framework ignore la configuración del espacio de nombres, manteniendo las tablas en el esquema predeterminado sin indicación de error aparente.&lt;/p&gt;
&lt;p&gt;Si te preguntas cómo crear estos espacios de nombres automáticamente en la base de datos, explico cómo convertirlo en una migración en &lt;a href="/es/blog/migrate-spaces/"&gt;Migrate Spaces&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link rel="alternate" href="https://jcalderita.com/es/blog/schema-space/"></link>
        <media:content medium="image" url="https://jcalderita.com/static/blog/SchemaSpace.webp"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/partial-index/</id>
        <title>Índices Parciales</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Extiende SQLCreateIndexBuilder de Vapor para crear índices parciales con cláusulas WHERE sobre columnas NULL, evitando SQL crudo y manteniendo type safety.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;Índices Parciales&lt;/h2&gt;
&lt;p&gt;Un &lt;strong&gt;índice parcial&lt;/strong&gt; es un índice que se crea únicamente sobre un subconjunto de filas de una tabla, definido mediante una condición &lt;span class="high"&gt;WHERE&lt;/span&gt; específica. En lugar de indexar todas las filas, el índice solo incluye aquellas que cumplen ciertos criterios, lo que puede mejorar el rendimiento y reducir el espacio utilizado.&lt;/p&gt;
&lt;p&gt;En mi caso particular, necesitaba indexar campos en función de si sus valores eran &lt;span class="high"&gt;NULL&lt;/span&gt; o &lt;span class="high"&gt;NOT NULL&lt;/span&gt;. Por ejemplo, los siguientes índices parciales en SQL:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;CREATE INDEX index_field_null 
ON table(field) 
WHERE field IS NULL;

CREATE INDEX index_field_not_null 
ON table(field) 
WHERE field IS NOT NULL;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;También es posible crear índices parciales que involucren múltiples columnas:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;CREATE INDEX index_field1_null_field2_null 
ON table(field1, field2) 
WHERE field1 IS NULL AND field2 IS NULL;

CREATE INDEX index_field1_not_null_field2_not_null 
ON table(field1, field2) 
WHERE field1 IS NOT NULL AND field2 IS NOT NULL;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;No todas las bases de datos admiten índices parciales, en mi caso particular estoy usando &lt;span class="high"&gt;PostgreSQL&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;Por defecto, Vapor no ofrece soporte directo para la creación de índices parciales, ya que la generación estándar de índices no contempla la inclusión de una cláusula &lt;span class="high"&gt;WHERE&lt;/span&gt; en la definición del índice.&lt;/p&gt;
&lt;p&gt;El siguiente fragmento de código crea un índice normal, ya sea sobre uno o varios campos, pero no permite especificar una condición para un índice parcial:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;try await db.sqlDatabase
    .create($0.key)
    .on(table)
    .colums($0.colums)
    .run()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Este código funciona para crear índices simples, pero carece de la capacidad para agregar un predicado &lt;span class="high"&gt;WHERE&lt;/span&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Este fragmento ha sido adaptado por mí; el método &lt;span class="high"&gt;.create&lt;/span&gt; del constructor original expone más opciones de configuración, pero en este ejemplo muestro una implementación personalizada y simplificada que uso actualmente.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Alternativa&lt;/h2&gt;
&lt;p&gt;La forma más directa de crear índices parciales es ejecutar sentencias SQL en crudo &lt;span class="high"&gt;raw SQL&lt;/span&gt;, tal como se mostró en los ejemplos iniciales. Esto implica construir un método para ejecutar en crudo las sentencias &lt;span class="high"&gt;CREATE INDEX&lt;/span&gt; con la cláusula &lt;span class="high"&gt;WHERE&lt;/span&gt; correspondiente.&lt;/p&gt;
&lt;p&gt;Aunque efectivo, este enfoque pierde la ventaja de la abstracción y seguridad que ofrece Vapor al construir migraciones y esquemas de base de datos mediante código Swift.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;El builder &lt;span class="high"&gt;SQLCreateIndexBuilder&lt;/span&gt; admite un predicado opcional &lt;span class="high"&gt;predicate&lt;/span&gt;. Si este no es &lt;span class="high"&gt;nil&lt;/span&gt;, agrega una cláusula &lt;span class="high"&gt;WHERE&lt;/span&gt; al índice.&lt;/p&gt;
&lt;p&gt;Por ello, extendí este builder para incluir un método &lt;span class="high"&gt;where&lt;/span&gt; que acepta una lista de columnas y un tipo de índice parcial (por ejemplo, &lt;span class="high"&gt;null&lt;/span&gt; o &lt;span class="high"&gt;not null&lt;/span&gt;). Este método construye la expresión lógica adecuada para el predicado y la asigna al índice.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;extension SQLCreateIndexBuilder {
    @discardableResult
    func `where`(
        _ columns: [FieldKey],
        _ partialIndex: SQLPartialIndexEnum?
    ) -&amp;gt; Self {
        guard let partialIndex else {
            return self
        }
        let op: SQLBinaryOperator = partialIndex == .null ? .is : .isNot

        let conditions = columns.map {
            self.where($0, op)
        }

        let combined: SQLBinaryExpression = conditions.dropFirst()
            .reduce(conditions[0]) { .init($0, .and, $1) }

        return self.where(combined)
    }

    private func `where`(
        _ column: FieldKey,
        _ binary: SQLBinaryOperator
    ) -&amp;gt; SQLBinaryExpression {
        .init(
            left: SQLIdentifier(column.description),
            op: binary,
            right: SQLLiteral.null
        )
    }

    private func `where`(_ expression: SQLExpression) -&amp;gt; Self {
        self.createIndex.predicate = expression
        return self
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Este código permite invocar el método &lt;span class="high"&gt;where&lt;/span&gt; para uno o varios campos, especificando si se desea un índice parcial para valores &lt;span class="high"&gt;NULL&lt;/span&gt; o &lt;span class="high"&gt;NOT NULL&lt;/span&gt;. Si no se proporciona un valor para &lt;span class="high"&gt;partialIndex&lt;/span&gt;, se devuelve el índice sin predicado, comportándose como un índice normal.&lt;/p&gt;
&lt;p&gt;La lógica consiste en crear una lista de expresiones binarias &lt;span class="high"&gt;SQLBinaryExpression&lt;/span&gt; para cada columna, combinándolas con el operador lógico &lt;span class="high"&gt;AND&lt;/span&gt; y asignando el resultado como predicado del índice.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;p&gt;Con esta extensión, ahora puedo crear índices parciales de forma sencilla en Vapor, agregando únicamente una línea al código original:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;try await db.sqlDatabase
    .create($0.key)
    .on(table)
    .colums($0.colums)
    .where($0.colums, $0.partial)
    .run()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Donde &lt;span class="high"&gt;$0.partial&lt;/span&gt; es un valor opcional que indica el tipo de índice parcial deseado &lt;span class="high"&gt;null&lt;/span&gt; o &lt;span class="high"&gt;not null&lt;/span&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Notas finales&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Este enfoque está diseñado específicamente para índices parciales basados en la presencia o ausencia de valores &lt;span class="high"&gt;NULL&lt;/span&gt; en columnas. Sin embargo, la implementación puede adaptarse para soportar otras condiciones y casos de uso más complejos, simplemente modificando la construcción del predicado.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;La solución ofrece una forma limpia y reutilizable de crear índices parciales dentro del ecosistema Vapor, manteniendo la coherencia y seguridad del código Swift.&lt;/p&gt;
&lt;p&gt;Si además quieres organizar tus tablas en espacios de nombres usando la propiedad &lt;code&gt;space&lt;/code&gt; de Fluent, lo cubrí en &lt;a href="/es/blog/schema-space/"&gt;Esquemas y Espacios&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link href="https://jcalderita.com/es/blog/partial-index/" rel="alternate"></link>
        <media:content medium="image" url="https://jcalderita.com/static/blog/PartialIndex.webp"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/my-first-data-race/</id>
        <title>Mi primera condición de carrera</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Un artículo técnico que explica cómo una refactorización para concurrencia en Swift llevó a una condición de carrera, y cómo resolverla usando patrones de actores.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;De refactor a condición de carrera en Swift&lt;/h2&gt;
&lt;p&gt;Durante un proceso de refactorización para convertir una funcionalidad previamente secuencial en una concurrente, me encontré con un problema clásico: garantizar la unicidad de los registros cuando múltiples tareas intentan crear eventos al mismo tiempo. Aunque el objetivo era mejorar el rendimiento procesando miles de eventos en paralelo usando Swift, la transición expuso un desafío típico de concurrencia: evitar duplicados y mantener la integridad de los datos bajo acceso simultáneo.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Actor con array&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;private actor EventsActor {
    private var events: [SportEventModel] = []

    func getOrCreate(
        name: String, 
        cityId: UUID, 
        build: () async throws -&amp;gt; EventModel
    ) async throws -&amp;gt; EventModel {
        if let event = events.first(
            where: { 
                $0.normalizedName == name 
                &amp;amp;&amp;amp; $0.$city.id == cityId 
            }
        ) { return event }

        let event = try await build()
        events.append(event)
        return event
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Ventajas:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Simplicidad.&lt;/li&gt;
&lt;li&gt;Seguridad frente a condiciones de carrera.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Desventaja:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Búsqueda ineficiente en grandes volúmenes &lt;span class="high"&gt;O(n)&lt;/span&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Actor con diccionario&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;private actor EventsActor {
    private var events: [String: EventModel] = [:]

    func getOrCreate(
        name: String, 
        cityId: UUID, 
        build: () async throws -&amp;gt; EventModel
    ) async throws -&amp;gt; EventModel {
        let key = &amp;quot;\(name)\(cityId.uuidString)&amp;quot;
        if let event = events[key] { 
            return event 
        }
        let event = try await build()
        events[key] = event
        return event
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Ventajas:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Búsqueda e inserción rápida &lt;span class="high"&gt;O(1)&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;Ideal para grandes volúmenes de datos.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Bug:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Condición de carrera lógica&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Condición de carrera lógica&lt;/h2&gt;
&lt;p&gt;Cuando múltiples tareas concurrentes intentan crear el mismo evento, todas pueden comprobar que no existe y proceder a crearlo al mismo tiempo.&lt;br /&gt;
Solo una de las instancias resultantes se almacena, mientras que el resto se convierten en “huérfanas”, lo que provoca inconsistencias y referencias rotas en otras estructuras de datos.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Por ejemplo: dos tareas concurrentes comprueban si un evento existe. Ambas ven que no, ambas lo crean, pero solo una sobrevive en el diccionario; la otra referencia ahora está perdida.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;Para evitar esto, es necesario serializar no solo el acceso sino también la creación por clave:&lt;br /&gt;
Si ya hay una creación en curso para esa clave, las tareas concurrentes deben esperar el resultado de la primera, asegurando que todas compartan exactamente el mismo recurso.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;private actor EventsActor {
    private var events: [String: EventModel] = [:]
    private var builds: [String: Task&amp;lt;EventModel, Error&amp;gt;] = [:]
    
    func getOrCreate(
        name: String, 
        cityId: UUID, 
        build: @Sendable @escaping () async throws -&amp;gt; EventModel
    ) async throws -&amp;gt; EventModel {
        let key = &amp;quot;\(name)\(cityId.uuidString)&amp;quot;
        if let event = events[key] { 
            return event 
        }

        if let building = builds[key] { 
            return try await building.value 
        }
        
        let buildTask = Task { 
            try await build() 
        }
        builds[key] = buildTask
        
        let event = try await buildTask.value
        events[key] = event
        builds.removeValue(forKey: key)
        return event
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Lecciones aprendidas&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Un actor por sí solo no previene condiciones de carrera lógicas del tipo “check-then-act”.&lt;/li&gt;
&lt;li&gt;En escenarios concurrentes, serializar la construcción del recurso por clave es fundamental para mantener la integridad de los datos.&lt;/li&gt;
&lt;li&gt;Es esencial probar bajo carga y escenarios concurrentes, no solo en modo secuencial.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link rel="alternate" href="https://jcalderita.com/es/blog/my-first-data-race/"></link>
        <media:content medium="image" url="https://jcalderita.com/static/blog/MyFirstDataRace.webp"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/timestamps-migrations/</id>
        <title>Timestamps en Migraciones</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Cómo evitar repetir createdAt, updatedAt y deletedAt en las migraciones de base de datos usando extensiones.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;Timestamps&lt;/h2&gt;
&lt;p&gt;Si eres como yo, cuando diseñas un modelo de base de datos quieres que todas las tablas incluyan siempre tres campos clave:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span class="high"&gt;createdAt&lt;/span&gt;: indica cuándo se creó el registro.&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;updatedAt&lt;/span&gt;: refleja la última vez que se actualizó.&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;deletedAt&lt;/span&gt;: marca cuándo se eliminó de forma lógica (sin borrar físicamente el registro).&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;Como se puede observar, en cada migración tenemos que escribir manualmente los tres campos de timestamp. Esto no solo es repetitivo, sino que también es propenso a errores si olvidamos algún campo o escribimos mal el tipo de dato.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;try await db.schema(model)
    .id()
    .field(.name, .string, .required)
    .field(.createdAt, .datetime, .required)
    .field(.updatedAt, .datetime, .required)
    .field(.deletedAt, .datetime)
    .create()
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;La solución es crear una extensión de &lt;span class="high"&gt;SchemaBuilder&lt;/span&gt; que agregue un método &lt;span class="high"&gt;timestamps()&lt;/span&gt;. Esta extensión encapsula la lógica repetitiva en un solo lugar, haciendo el código más limpio y mantenible.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;extension SchemaBuilder {
    func timestamps() -&amp;gt; Self {
        self.field(.createdAt, .datetime, .required)
            .field(.updatedAt, .datetime, .required)
            .field(.deletedAt, .datetime)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;p&gt;Ahora nuestras migraciones son mucho más limpias y legibles. Llamando a &lt;span class="high"&gt;.timestamps()&lt;/span&gt; agregamos los tres campos necesarios. Esto reduce la posibilidad de errores y hace que el código sea más fácil de mantener.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;try await db.schema(model)
    .id()
    .field(.name, .string, .required)
    .timestamps()
    .create()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link href="https://jcalderita.com/es/blog/timestamps-migrations/" rel="alternate"></link>
        <media:content medium="image" url="https://jcalderita.com/static/blog/Timestamps.webp"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/cloud-server/</id>
        <title>☁️ Servidor en la nube</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Servidor en la nube, algo necesario cuando trabajas con backend. Descubre las mejores opciones gratuitas y por qué necesitas uno.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;☁️ Cloud Server&lt;/h2&gt;
&lt;p&gt;Cuando trabajas con backend, a no ser que estés trabajando en una red local y no vaya a salir nada de lo que hagas hacia afuera, vas a necesitar sí o sí un servidor en la nube y cuanto antes mejor 🚀.&lt;/p&gt;
&lt;p&gt;Trabajar en local está muy bien, pero tener un servidor en la nube es un paso hacia adelante, y es mejor empezar cuanto antes para tener un servidor de desarrollo en la nube.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🔧 Arquitecturas de CPU&lt;/h2&gt;
&lt;p&gt;Conforme la tecnología avanza, cada vez hay más opciones tanto en proveedores como en tecnología de procesadores 💻:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;AMD&lt;/strong&gt; 🔴:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Excelente relación precio/rendimiento&lt;/li&gt;
&lt;li&gt;✅ Arquitectura x64 madura y estable&lt;/li&gt;
&lt;li&gt;✅ Amplia compatibilidad con software&lt;/li&gt;
&lt;li&gt;✅ Ideal para aplicaciones de alto rendimiento&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Intel&lt;/strong&gt; 🔵:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Líder histórico en el mercado de servidores&lt;/li&gt;
&lt;li&gt;✅ Excelente soporte empresarial&lt;/li&gt;
&lt;li&gt;✅ Optimizaciones específicas para aplicaciones legacy&lt;/li&gt;
&lt;li&gt;✅ Rendimiento consistente y predecible&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ARM&lt;/strong&gt; 🍃:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ El último en llegar pero con gran potencial&lt;/li&gt;
&lt;li&gt;✅ Eficiencia energética superior&lt;/li&gt;
&lt;li&gt;✅ Mejor precio por núcleo&lt;/li&gt;
&lt;li&gt;✅ Ideal para microservicios y contenedores&lt;/li&gt;
&lt;li&gt;✅ Menor consumo = menor costo operativo&lt;/li&gt;
&lt;li&gt;⚠️ Algunas limitaciones de compatibilidad con software legacy&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;💰 Free Tier - Oracle Cloud&lt;/h2&gt;
&lt;p&gt;Yo he optado por Oracle Cloud y el free tier que tiene, el cual te ofrece un servidor ARM, ya que para mí es la opción que mejor se adapta a mis necesidades 🎯.&lt;/p&gt;
&lt;p&gt;🖥️ VM.Standard.A1.Flex (Ampere ARM)&lt;br /&gt;&lt;br /&gt;
⏱️ Hasta &lt;strong&gt;3.000 OCPU-horas&lt;/strong&gt; al mes&lt;br /&gt;&lt;br /&gt;
🧠 Hasta &lt;strong&gt;18.000 GB-horas de memoria&lt;/strong&gt; al mes&lt;br /&gt;&lt;br /&gt;
🚀 1 servidor con &lt;strong&gt;4 OCPUs + 24 GB RAM&lt;/strong&gt; funcionando 24/7&lt;br /&gt;&lt;br /&gt;
🔄 O varias instancias más pequeñas (ejemplo: 4 × 1 OCPU + 6 GB RAM)&lt;br /&gt;&lt;br /&gt;
💾 Hasta &lt;strong&gt;200 GB combinados&lt;/strong&gt; entre volúmenes de arranque y bloques&lt;br /&gt;&lt;br /&gt;
♾️ &lt;strong&gt;Gratuito indefinidamente&lt;/strong&gt;, siempre dentro de los límites del plan&lt;br /&gt;&lt;br /&gt;
⚠️ Oracle puede &lt;strong&gt;reclamar instancias inactivas&lt;/strong&gt; (sin uso significativo en 7 días)&lt;br /&gt;&lt;br /&gt;
🌍 Puede haber &lt;strong&gt;restricciones regionales&lt;/strong&gt; por falta de capacidad&lt;br /&gt;&lt;br /&gt;
💸 &lt;strong&gt;100% gratis&lt;/strong&gt; dentro del plan Always Free&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link rel="alternate" href="https://jcalderita.com/es/blog/cloud-server/"></link>
        <media:content medium="image" url="https://jcalderita.com/static/blog/CloudServer.webp"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/claude-code/</id>
        <title>Claude Code</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Descubre cómo Claude Code se ha convertido en mi asistente de desarrollo favorito y las reglas que aplico para maximizar su potencial.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;🤔 ¿Qué es Claude Code para mí?&lt;/h2&gt;
&lt;p&gt;Para alejarme de lo que habla todo el mundo 😂, voy a contaros sobre &lt;strong&gt;inteligencia artificial&lt;/strong&gt; desde mi experiencia personal.&lt;/p&gt;
&lt;p&gt;He integrado &lt;strong&gt;Claude Code&lt;/strong&gt; en mis proyectos, pero para mí es mucho más que una herramienta: es mi &lt;strong&gt;asistente digital&lt;/strong&gt; 👥, un miembro más de mi equipo de desarrollo, una extensión de mis capacidades o, como bien representa la imagen, &lt;strong&gt;mi sombra tecnológica&lt;/strong&gt; 🌑.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;No me sustituye ni creo que sustituya a ningún desarrollador en estos momentos. Claude Code es tan bueno como lo seas tú o como sepas utilizarlo 💡.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;📋 Mis Reglas de Oro&lt;/h2&gt;
&lt;p&gt;Tengo &lt;strong&gt;cuatro reglas fundamentales&lt;/strong&gt; que aplico religiosamente con Claude Code:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;🔍 &lt;strong&gt;Control Total de Cambios&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Yo reviso todo&lt;/strong&gt; antes de implementar cualquier cambio&lt;/li&gt;
&lt;li&gt;No tiene permisos para modificar archivos sin mi aprobación&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flujo:&lt;/strong&gt; Me proporciona el plan → Reviso los cambios → Los entiendo → Los apruebo (o no)&lt;/li&gt;
&lt;li&gt;Esta regla es &lt;strong&gt;súper importante&lt;/strong&gt; para mí. Darle permisos ciegos no va con mi filosofía 🚫&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start="2"&gt;
&lt;li&gt;📝 &lt;strong&gt;Claude.md como Biblia del Proyecto&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Trabajo intensamente el archivo &lt;span class="high"&gt;CLAUDE.md&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Defino qué es el proyecto y establezco &lt;strong&gt;guidelines claras&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Es mi manera de “entrenar” a Claude sobre mi estilo y preferencias&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resultado:&lt;/strong&gt; Respuestas más alineadas con mi visión 🎯&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start="3"&gt;
&lt;li&gt;⚙️ &lt;strong&gt;Configuración Inteligente&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Configuro sus settings con &lt;strong&gt;rutas de documentación oficial&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Cuando hago preguntas sobre tecnologías específicas, utiliza fuentes oficiales&lt;/li&gt;
&lt;li&gt;Esto garantiza respuestas más precisas y actualizadas 📚&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start="4"&gt;
&lt;li&gt;🚀 &lt;strong&gt;Trabajo Colaborativo, No Dependencia&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nunca&lt;/strong&gt; parto de un IDE vacío&lt;/li&gt;
&lt;li&gt;Siempre trabajo sobre una base de código que ya he desarrollado&lt;/li&gt;
&lt;li&gt;Mi flujo: Empiezo a crear → Me atascos → Pregunto a Claude&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pregunta recurrente:&lt;/strong&gt; &lt;em&gt;”¿Hay una manera más limpia y eficiente de hacer este código?”&lt;/em&gt; 🤝&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;💡 Imaginación sin límite&lt;/h2&gt;
&lt;p&gt;Y aquí viene lo interesante: &lt;strong&gt;Claude Code no es solo para programar&lt;/strong&gt; 🎨. Puedes usarlo para lo que se te ocurra. Solo hazte esta pregunta:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;¿Cómo puedo hacer que esta idea funcione con Claude Code?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Una de las formas más geniales en las que lo uso &lt;strong&gt;fuera de programación&lt;/strong&gt; es con mi consumo de contenido técnico:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Consumo muchísimo contenido de programación:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;🎓 Cursos online&lt;br /&gt;&lt;br /&gt;
💊 Píldoras de conocimiento&lt;br /&gt;&lt;br /&gt;
🎤 Conferencias y talks&lt;br /&gt;&lt;br /&gt;
🗣️ Debates técnicos&lt;br /&gt;&lt;br /&gt;
🎧 Podcasts especializados&lt;br /&gt;&lt;br /&gt;
🛠️ Talleres prácticos&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;El problema típico:&lt;/strong&gt; Estás programando y recuerdas haber visto algo útil en un vídeo, pero… ¿cuál era? ¿En qué minuto? 😅&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mi solución con Claude:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Le pregunto: &lt;em&gt;”¿Dónde se habla de esto?”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Me devuelve:
&lt;ul&gt;
&lt;li&gt;📹 El vídeo o vídeos específicos&lt;/li&gt;
&lt;li&gt;⏰ El timestamp exacto donde se menciona&lt;/li&gt;
&lt;li&gt;📄 Un pequeño resumen del contenido&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Resultado:&lt;/strong&gt; ¡&lt;strong&gt;Maravilloso!&lt;/strong&gt; ✨ Acceso instantáneo a todo mi conocimiento consumido.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🎯 Reflexión Final&lt;/h2&gt;
&lt;p&gt;Claude Code se ha convertido en mi &lt;strong&gt;sombra tecnológica&lt;/strong&gt;. No es magia, no hace el trabajo por ti, pero si sabes usarlo correctamente, puede &lt;strong&gt;potenciar enormemente&lt;/strong&gt; tu productividad y creatividad.&lt;/p&gt;
&lt;p&gt;La clave está en establecer límites claros, mantener el control y usarlo como lo que realmente es: &lt;strong&gt;un asistente excepcional&lt;/strong&gt; que amplifica tus capacidades, no que las reemplaza 🚀.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link rel="alternate" href="https://jcalderita.com/es/blog/claude-code/"></link>
        <media:content medium="image" url="https://jcalderita.com/static/blog/ClaudeCode.webp"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/love-vapor/</id>
        <title>Amo Vapor</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Descubre por qué Vapor se convirtió en mi framework favorito de Swift en servidor en el Bootcamp, desde gRPC hasta PassKeys y tareas async.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;❤️ Swift del lado servidor&lt;/h2&gt;
&lt;p&gt;Cuando cursé el &lt;em&gt;Swift Full Stack Bootcamp&lt;/em&gt; en &lt;a href="https://acoding.academy"&gt;Apple Coding Academy&lt;/a&gt;, uno de los módulos que se nos impartió fue &lt;em&gt;Swift del lado servidor&lt;/em&gt; a través del framework &lt;a href="https://vapor.codes"&gt;Vapor&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Nada más comenzar con la parte teórica, mi corazón empezó a palpitar con fuerza y simplemente, a mitad de aquella primera clase, ya estaba completamente enamorado.&lt;/p&gt;
&lt;p&gt;Así que, si me preguntan &lt;strong&gt;“¿Cuál fue tu módulo favorito del Bootcamp?”&lt;/strong&gt;, sin lugar a dudas diría que este.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;👍🏻 ¿Por qué me gusta?&lt;/h2&gt;
&lt;p&gt;Siempre he pensado que la parte de &lt;strong&gt;backend&lt;/strong&gt; es el &lt;strong&gt;cerebro&lt;/strong&gt; de cualquier aplicación o web.&lt;br /&gt;
Es ahí donde está &lt;strong&gt;toda la chicha&lt;/strong&gt;: donde se procesa la información (normalmente desde una base de datos) y se prepara para que el usuario final la reciba de la forma más &lt;strong&gt;sencilla y transparente posible&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Que quede claro: &lt;strong&gt;no menosprecio el frontend&lt;/strong&gt;.&lt;br /&gt;
De hecho, me parece admirable la gente que crea diseños increíbles: auténticos héroes para mí, porque eso es algo que personalmente me resulta imposible… aunque con &lt;strong&gt;SwiftUI&lt;/strong&gt; me entiendo bastante bien.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🧸 Jugando con Vapor&lt;/h2&gt;
&lt;p&gt;En próximas entradas del blog iré profundizando mucho más, pero ya puedo contar que me he dedicado a &lt;strong&gt;jugar mucho con Vapor&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;📦 Paquete individual para configurar distintos servicios&lt;br /&gt;&lt;br /&gt;
🌐 Servidor &lt;strong&gt;gRPC&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;
🗂 Modelo de datos con &lt;strong&gt;vistas&lt;/strong&gt; (incluidas &lt;em&gt;materialized views&lt;/em&gt;), índices, permisos por aplicación y esquemas&lt;br /&gt;&lt;br /&gt;
🧮 Enumerados con migraciones automáticas&lt;br /&gt;&lt;br /&gt;
🔑 &lt;strong&gt;PassKey&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;
📊 &lt;em&gt;Bulk inserts&lt;/em&gt; para poblar grandes volúmenes de datos&lt;br /&gt;&lt;br /&gt;
⏱ Tareas programadas con asincronía&lt;br /&gt;&lt;br /&gt;
… y mucho más 🚀&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A lo largo de distintas entradas del blog contaré un poco de todo.&lt;br /&gt;
Eso sí: aunque Vapor y Swift del lado servidor me guste mucho, &lt;strong&gt;no todo será sobre backend&lt;/strong&gt;… habrá espacio para muchos otros temas que me entusiasman.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link rel="alternate" href="https://jcalderita.com/es/blog/love-vapor/"></link>
        <media:content medium="image" url="https://jcalderita.com/static/blog/LoveVapor.webp"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/la-liga-thanks/</id>
        <title>¡Gracias, La Liga!</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Cómo La Liga bloquea mi web durante los partidos por medidas antipiratería que afectan a sitios legítimos en Cloudflare.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;Mi web bloqueada en partidos de La Liga&lt;/h2&gt;
&lt;p&gt;Si estás intentando visitar mi web durante un partido de La Liga y no carga, no te lo estás imaginando: en realidad, está siendo bloqueada.&lt;/p&gt;
&lt;p&gt;En España, &lt;strong&gt;La Liga aplica medidas antipiratería muy agresivas&lt;/strong&gt;, y una de ellas consiste en &lt;strong&gt;bloquear el acceso a sitios web alojados en la red de Cloudflare&lt;/strong&gt; durante los partidos en directo. Desafortunadamente, esto significa que si tu web está alojada en Cloudflare —como la mía— puedes verte afectado aunque no estés emitiendo ni distribuyendo ningún contenido ilegal.&lt;/p&gt;
&lt;p&gt;Cloudflare protege y acelera millones de sitios web en todo el mundo, pero debido a la política de La Liga, &lt;strong&gt;rangos enteros de IP o nodos CDN pueden ser incluidos en listas negras&lt;/strong&gt;, lo cual afecta a &lt;strong&gt;innumerables webs legítimas&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;¿Por qué sucede esto?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;La Liga utiliza sistemas automatizados y bloqueos a nivel DNS para restringir el acceso a lo que considera fuentes potenciales de piratería.&lt;/li&gt;
&lt;li&gt;Cloudflare es utilizado por todo tipo de sitios, incluidos aquellos que sí albergan retransmisiones ilegales —por lo que sus IPs se marcan.&lt;/li&gt;
&lt;li&gt;Como resultado, muchas &lt;strong&gt;webs inocentes se bloquean&lt;/strong&gt; durante los partidos, simplemente por compartir infraestructura.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;¿Qué puedes hacer?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Si estás en España y no puedes acceder a mi portfolio durante un partido, vuelve a intentarlo más tarde.&lt;/li&gt;
&lt;li&gt;Si te pica la curiosidad, existen herramientas online para comprobar si un dominio está siendo bloqueado.&lt;/li&gt;
&lt;li&gt;O puedes usar una VPN para evitar temporalmente la restricción (solo para visitar mi portfolio, claro 😅).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Es un ejemplo frustrante de cómo una aplicación excesiva de medidas digitales puede dañar la web abierta —incluso portfolios pequeños como el mío.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;”¡Gracias, La Liga!”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Actualización:&lt;/strong&gt; Finalmente encontré una solución sencilla para este problema. Si quieres saber cómo lo resolví en 5 minutos, lee &lt;a href="/es/blog/only-dns/"&gt;Only DNS&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link rel="alternate" href="https://jcalderita.com/es/blog/la-liga-thanks/"></link>
        <media:content medium="image" url="https://jcalderita.com/static/blog/LaLiga.webp"></media:content>
    </entry>
    <entry>
        <id>https://jcalderita.com/es/blog/my-first-article/</id>
        <title>Mi primer artículo</title>
        <updated>2026-04-17T19:06:06Z</updated>
        <summary>Actualización de mi sitio web personal construido con Astro y Tailwind, donde compartiré artículos y experiencias como desarrollador Swift en el ecosistema Apple.</summary>
        <content type="html">&lt;hr /&gt;
&lt;h2&gt;🚀 ¡Gran actualización en mi sitio web!&lt;/h2&gt;
&lt;p&gt;Hoy lanzo una &lt;strong&gt;importante actualización&lt;/strong&gt; de mi sitio web personal, que ahora está construido con &lt;a href="https://astro.build/"&gt;Astro&lt;/a&gt; y &lt;a href="https://tailwindcss.com/"&gt;Tailwind&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;He añadido la posibilidad de &lt;strong&gt;escribir artículos&lt;/strong&gt; — ¡y este es el primero!&lt;br /&gt;
A partir de ahora, compartiré mis experiencias, descubrimientos y desafíos diarios como &lt;strong&gt;desarrollador Swift&lt;/strong&gt;.&lt;br /&gt;
También puede que comparta algo de mi vida personal.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚡️ &lt;strong&gt;Sin calendario fijo:&lt;/strong&gt;&lt;br /&gt;
Publicaré siempre que tenga algo relevante que compartir — ya sean problemas, soluciones, curiosidades o nuevas tecnologías que esté explorando.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Con esta nueva versión, me puedo centrar únicamente en escribir artículos en &lt;strong&gt;Markdown&lt;/strong&gt;; todo lo demás está completamente automatizado.&lt;/p&gt;
&lt;p&gt;Si te interesa &lt;strong&gt;Swift, iOS, visionOS, Vapor, desarrollo full-stack con Swift y Web&lt;/strong&gt;, o simplemente quieres acompañarme en este camino — ¡bienvenido!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
        <link rel="alternate" href="https://jcalderita.com/es/blog/my-first-article/"></link>
        <media:content medium="image" url="https://jcalderita.com/static/blog/MyFirstArticle.webp"></media:content>
    </entry>
</feed>