wandres.dev
DURABLE OBJECTS · el modelo

Acceder al objeto: el stub, el RPC y dónde vive

Tienes el namespace y sabes acuñar identidades. Falta la mecánica de hablarle al objeto: con el id obtienes un stub, un proxy local hacia una instancia que puede vivir a un océano de distancia, y llamas sus métodos por RPC como si fueran de un objeto de tu mismo programa. La vía antigua es fetch, todavía útil para WebSockets. Y por fin la pregunta que todas las lecciones pospusieron: un Durable Object vive en una sola ubicación física, y esa colocación es un factor de diseño real. Vemos el stub, el RPC frente a fetch, dónde se instancia el objeto y cómo influir con location hints.

⏱ 18 min

Ya nombras objetos y entiendes por qué son únicos y seguros. Ahora, cómo se les habla. La respuesta corta es de una elegancia engañosa: obtienes un stub y llamas sus métodos con await, como si el objeto viviera en tu mismo proceso. La respuesta larga esconde una verdad que hemos ido aplazando lección tras lección —un Durable Object vive en un único lugar del planeta, y todas las llamadas convergen allí—. El stub disimula la distancia, pero no la abole: conocerla es lo que separa a quien invoca un objeto de quien diseña dónde debe posarse su gravedad.

🎯 Al terminar esta lección sabrás
  • Obtener el stub de un objeto con get(id) desde el namespace del binding.
  • Invocar los métodos públicos del objeto por RPC, y conocer la vía alternativa de fetch.
  • Comprender que el stub es un proxy hacia un objeto remoto que vive en una sola ubicación.
  • Razonar dónde se instancia físicamente un objeto y cómo influir con location hints.

Del binding al stub

El camino tiene dos peldaños que ya conoces: del namespace sacas un id, y del id sacas el stub.

const id = env.CONTADOR.idFromName("global");
const stub = env.CONTADOR.get(id);

El stub es un proxy del lado del cliente: un asa local que representa a un objeto remoto que puede estar a miles de kilómetros. Tener el stub no instancia nada pesado en tu Worker ni reserva memoria en tu isolate; es una referencia, ligera y barata. El objeto real se crea —o se despierta de su hibernación— de forma perezosa, en la primera llamada que le hagas, no al obtener el stub.

Esta pereza importa: puedes obtener stubs a discreción, guardarlos, pasarlos, sin coste. El gasto llega cuando de verdad hablas con el objeto, y solo entonces la plataforma lo materializa donde le corresponde.

Y como el stub es solo una referencia, puedes reutilizarlo para varias llamadas seguidas dentro de la misma invocación, o volver a pedirlo cuando lo necesites: no hay conexión que amortizar ni pool que gestionar. El objeto pesado vive al otro lado; tú solo sostienes un asa ligera hacia él.

Llamar sus métodos: RPC o fetch

Con un Durable Object moderno —el que extiende DurableObject— sus métodos públicos son invocables por RPC directamente, como si fueran locales. Le pones un await delante y ya está:

export default {
  async fetch(request, env): Promise<Response> {
    const id = env.CONTADOR.idFromName("global");
    const stub = env.CONTADOR.get(id);
    const total = await stub.incrementar(); // RPC transparente
    return Response.json({ total });
  },
} satisfies ExportedHandler<Env>;

El RPC es transparente: los argumentos y el valor de retorno viajan serializados, y el runtime enruta la llamada hasta donde viva el objeto, sin que tú construyas una petición ni parsees una respuesta. Lees el código como una llamada a un método porque ese es, exactamente, el modelo mental correcto: un objeto con métodos, que resulta estar lejos.

La vía antigua es fetch. El objeto implementa fetch(request) y tú lo invocas con stub.fetch(request), entregando un Request y recibiendo un Response. Es más ceremoniosa —empaquetas y desempaquetas a mano— pero sigue siendo la puerta obligada para lo que es intrínsecamente una petición HTTP: sobre todo, abrir un WebSocket contra el objeto, que se negocia con un upgrade sobre fetch.

ℹ️
RPC para lógica, fetch para el protocolo HTTP

La regla es simple. Si quieres invocar una operación de tu objeto —incrementar, entrar en la sala, pedir un turno—, usa RPC: es tipado, directo y se lee como código normal. Si lo que llega es una petición HTTP que quieres reenviar entera, o necesitas la semántica de HTTP como el upgrade a WebSocket, usa fetch. RPC es la vía por defecto de 2026 para la lógica; fetch es la puerta cuando el protocolo mismo es HTTP.

🪶

stub perezoso

Obtener el stub no materializa nada: es una referencia barata. El objeto se crea o se despierta en la primera llamada.

📞

RPC transparente

Invocas métodos con await como si fueran locales. Argumentos y retorno viajan serializados sin que construyas peticiones.

🌐

fetch para HTTP

La vía cuando lo que hay es una petición HTTP: reenviar un Request entero o negociar el upgrade a WebSocket.

📍

Un domicilio

El objeto vive en un solo centro de datos. Toda llamada, desde donde sea, se enruta hasta ese único hogar.

Dónde vive físicamente

Aquí está el hecho que todas las lecciones anteriores dieron por sabido sin decirlo: un Durable Object es un único punto en el espacio. Cuando se accede a él por primera vez, la plataforma lo instancia en un centro de datos —por defecto, cerca de quien lo invoca primero, o guiado por una pista de ubicación— y allí se queda. Desde ese momento, cada llamada, venga de donde venga, se enruta a ese hogar. El objeto no está en todas partes como el Worker: está en un sitio.

// una pista de ubicacion al obtener el stub
const id = env.SALA.idFromName("sala:eu-42");
const stub = env.SALA.get(id, { locationHint: "weur" });
flowchart LR
U[usuario en Lima] --> W[Worker cercano en Lima]
W --> DO[Durable Object en Frankfurt]
DO --> ST[storage coubicado]
style DO fill:#f9e2af,color:#11111b
style ST fill:#a6e3a1,color:#11111b

Esto reintroduce, ahora concreta, la tensión del primer nivel de la guía: el cómputo del Worker está cerca del usuario, pero esta pieza de estado está en un lugar. Hay una asimetría de latencia real: un usuario en Lima habla con un Worker en Lima, pero si el objeto vive en Fráncfort, ese salto lo paga. La buena noticia es que el objeto y su almacenamiento están coubicados, así que el objeto conversa con sus propios datos a velocidad de memoria; el coste de red es el salto del Worker al objeto, pagado una vez por llamada externa, no una vez por cada lectura de estado.

La consecuencia de diseño es que la colocación importa. Un location hint te deja empujar el objeto hacia la región donde viven los usuarios o los sistemas que más lo usan, para que ese único salto sea lo más corto posible. No hay una respuesta universal: coloca cada objeto cerca de su centro de gravedad, allí donde converge la mayoría de quienes lo necesitan.

Y ese centro de gravedad no siempre es el usuario. Si dos objetos conversan mucho entre sí —el de una partida y el de cada jugador—, o si un objeto escribe sin cesar a un almacén regional, acercarlo a esos interlocutores puede pesar más que acercarlo a las personas. La gravedad apunta a donde está el diálogo más intenso, sean usuarios, otros objetos o datos.

El stub disuelve la distancia en la sintaxis, pero no en la física

Hay una larga y honrosa tradición en la informática de hacer que lo remoto parezca local: las llamadas a procedimiento remoto de los años ochenta, CORBA, los objetos distribuidos de Java, todos persiguieron el mismo sueño de que invocar algo al otro lado de la red se leyera igual que invocar algo en tu propia memoria. Y todos tropezaron con la misma piedra, bautizada en las falacias del cómputo distribuido: la red no es fiable, no tiene latencia cero, y fingir que sí acaba pasando factura. El RPC de los Durable Objects revive ese sueño con una diferencia crucial, que es que no miente sobre lo que oculta. await stub.incrementar() se lee como una llamada local, y esa transparencia es un regalo enorme para la ergonomía —compones actores distribuidos como si compusieras objetos de un mismo programa—; pero el await está ahí, honesto, marcando el punto donde el tiempo y la distancia entran en la ecuación. El objeto vive en un lugar del mundo, y la sintaxis limpia no cambia esa física. Interiorizar las dos verdades a la vez es lo que separa a quien usa Durable Objects de quien los diseña. La primera verdad, liberadora: puedes tratar un actor remoto como un objeto y dejar que la plataforma se ocupe del enrutado, el despertar y la serialización. La segunda, sobria: ese objeto tiene un domicilio, y cada llamada externa viaja hasta él, de modo que dónde poses su gravedad —cerca de qué usuarios, de qué datos, de qué otros objetos— es una decisión de rendimiento que ninguna abstracción tomará por ti. El maestro no elige entre las dos lecturas; sostiene ambas. Escribe el código como si la distancia no existiera y coloca el objeto como si fuera lo único que existe.

⚔️ Habla con un objeto y colócalo
  1. Desde un Worker, obtén el stub de un contador con idFromName e invócalo por RPC con await stub.incrementar(). Devuelve el total en la respuesta.
  2. Añade al mismo objeto un método fetch y ejerce las dos vías: RPC para la lógica y stub.fetch(request) para reenviar una petición entera. Contrasta cuánta ceremonia pide cada una.
  3. Crea el objeto con un locationHint hacia una región concreta y razona, según dónde estén tus usuarios, si esa colocación acorta o alarga el salto del Worker al objeto.
  4. Explica por qué obtener el stub es barato y perezoso, y en qué momento exacto la plataforma materializa el objeto.
  5. Argumenta la frase “el stub disuelve la distancia en la sintaxis pero no en la física” con un ejemplo de latencia de tu propio despliegue.