# history

source:
  import subprocess, time, json

  NATS_CONTAINER = "nats_demo"
  NATS_PORT = 4222
  NATS_URL = f"nats://127.0.0.1:{NATS_PORT}"

  def _docker(*args, check=True):
      return subprocess.run(["docker", *args], capture_output=True, text=True, check=check)

  def ensure_nats(name=NATS_CONTAINER, port=NATS_PORT):
      """Arranca un broker NATS en Docker de forma idempotente. Devuelve el estado."""
      # ¿Existe el contenedor (corriendo o parado)?
      out = _docker("ps", "-a", "--filter", f"name=^{name}$", "--format", "{{.State}}", check=False).stdout.strip()
      if out == "running":
          state = "already-running"
      elif out in ("exited", "created", "paused"):
          _docker("start", name)
          state = "started"
      else:
          _docker("run", "-d", "--name", name, "-p", f"{port}:4222", "-p", "8222:8222",
                  "nats:latest", "-js", "-m", "8222")
          state = "created"
      # Esperar a que el puerto acepte conexiones
      time.sleep(1.0)
      return state

  state = ensure_nats()
  print(f"Broker NATS: {state} en {NATS_URL}")
  # El endpoint de monitorización (puerto 8222) expone métricas del servidor en JSON
  import urllib.request
  varz = json.loads(urllib.request.urlopen("http://127.0.0.1:8222/varz", timeout=3).read())
  print(f"nats-server version: {varz['version']} | jetstream activo: {'jetstream' in varz and bool(varz['jetstream'])}")

source_raw:
  import subprocess, time, json

  NATS_CONTAINER = "nats_demo"
  NATS_PORT = 4222
  NATS_URL = f"nats://127.0.0.1:{NATS_PORT}"

  def _docker(*args, check=True):
      return subprocess.run(["docker", *args], capture_output=True, text=True, check=check)

  def ensure_nats(name=NATS_CONTAINER, port=NATS_PORT):
      """Arranca un broker NATS en Docker de forma idempotente. Devuelve el estado."""
      # ¿Existe el contenedor (corriendo o parado)?
      out = _docker("ps", "-a", "--filter", f"name=^{name}$", "--format", "{{.State}}", check=False).stdout.strip()
      if out == "running":
          state = "already-running"
      elif out in ("exited", "created", "paused"):
          _docker("start", name)
          state = "started"
      else:
          _docker("run", "-d", "--name", name, "-p", f"{port}:4222", "-p", "8222:8222",
                  "nats:latest", "-js", "-m", "8222")
          state = "created"
      # Esperar a que el puerto acepte conexiones
      time.sleep(1.0)
      return state

  state = ensure_nats()
  print(f"Broker NATS: {state} en {NATS_URL}")
  # El endpoint de monitorización (puerto 8222) expone métricas del servidor en JSON
  import urllib.request
  varz = json.loads(urllib.request.urlopen("http://127.0.0.1:8222/varz", timeout=3).read())
  print(f"nats-server version: {varz['version']} | jetstream activo: {'jetstream' in varz and bool(varz['jetstream'])}")


source:
  import asyncio
  import nats

  # Conexión persistente del notebook (simula el cliente de un proceso de larga vida)
  nc = await nats.connect(NATS_URL, name="notebook-01")
  info = nc._server_info  # metadata que el broker envía en el handshake
  print("Conectado.")
  print(f"  server_id  : {info['server_id']}")
  print(f"  max_payload: {info['max_payload']/1024/1024:.0f} MiB por mensaje")
  print(f"  client_id  : {info['client_id']}")

source_raw:
  import asyncio
  import nats

  # Conexión persistente del notebook (simula el cliente de un proceso de larga vida)
  nc = await nats.connect(NATS_URL, name="notebook-01")
  info = nc._server_info  # metadata que el broker envía en el handshake
  print("Conectado.")
  print(f"  server_id  : {info['server_id']}")
  print(f"  max_payload: {info['max_payload']/1024/1024:.0f} MiB por mensaje")
  print(f"  client_id  : {info['client_id']}")


source:
  # El subscriber declara interés ANTES de que se publique (core = at-most-once)
  sub = await nc.subscribe("saludos")

  # El publisher envía. No sabe quién escucha; solo conoce el subject.
  await nc.publish("saludos", b"hola desde el publisher")
  await nc.flush()  # fuerza el envío al broker antes de seguir

  # El subscriber recoge el mensaje
  msg = await sub.next_msg(timeout=2)
  print(f"subject recibido : {msg.subject}")
  print(f"payload          : {msg.data.decode()}")

  await sub.unsubscribe()

source_raw:
  # El subscriber declara interés ANTES de que se publique (core = at-most-once)
  sub = await nc.subscribe("saludos")

  # El publisher envía. No sabe quién escucha; solo conoce el subject.
  await nc.publish("saludos", b"hola desde el publisher")
  await nc.flush()  # fuerza el envío al broker antes de seguir

  # El subscriber recoge el mensaje
  msg = await sub.next_msg(timeout=2)
  print(f"subject recibido : {msg.subject}")
  print(f"payload          : {msg.data.decode()}")

  await sub.unsubscribe()


source:
  recibidos = []  # registro de quién recibió qué

  def make_handler(nombre):
      async def handler(msg):
          recibidos.append({"subscriber": nombre, "subject": msg.subject, "data": msg.data.decode()})
      return handler

  # Tres subscribers independientes al MISMO subject
  subs = []
  for nombre in ["sub-A", "sub-B", "sub-C"]:
      s = await nc.subscribe("noticias", cb=make_handler(nombre))
      subs.append(s)

  # Un único publish...
  await nc.publish("noticias", b"titular: NATS entrega a todos")
  await nc.flush()
  await asyncio.sleep(0.3)  # dar tiempo a los callbacks

  for r in recibidos:
      print(f"{r['subscriber']} <- [{r['subject']}] {r['data']}")
  print(f"\nUn publish -> {len(recibidos)} entregas (fan-out)")

  for s in subs:
      await s.unsubscribe()

source_raw:
  recibidos = []  # registro de quién recibió qué

  def make_handler(nombre):
      async def handler(msg):
          recibidos.append({"subscriber": nombre, "subject": msg.subject, "data": msg.data.decode()})
      return handler

  # Tres subscribers independientes al MISMO subject
  subs = []
  for nombre in ["sub-A", "sub-B", "sub-C"]:
      s = await nc.subscribe("noticias", cb=make_handler(nombre))
      subs.append(s)

  # Un único publish...
  await nc.publish("noticias", b"titular: NATS entrega a todos")
  await nc.flush()
  await asyncio.sleep(0.3)  # dar tiempo a los callbacks

  for r in recibidos:
      print(f"{r['subscriber']} <- [{r['subject']}] {r['data']}")
  print(f"\nUn publish -> {len(recibidos)} entregas (fan-out)")

  for s in subs:
      await s.unsubscribe()


source:
  wild = []

  async def on_star(msg):
      wild.append({"patron": "sensor.*.temp", "subject": msg.subject, "data": msg.data.decode()})

  async def on_gt(msg):
      wild.append({"patron": "sensor.>", "subject": msg.subject, "data": msg.data.decode()})

  s_star = await nc.subscribe("sensor.*.temp", cb=on_star)
  s_gt   = await nc.subscribe("sensor.>",      cb=on_gt)

  # Publicamos en varios subjects concretos
  publicados = {
      "sensor.cocina.temp":          "21.5",
      "sensor.salon.temp":           "22.1",
      "sensor.salon.humedad":        "48",
      "sensor.garaje.puerta.estado": "abierta",
  }
  for subj, val in publicados.items():
      await nc.publish(subj, val.encode())
  await nc.flush()
  await asyncio.sleep(0.3)

  print("Mensajes publicados:", list(publicados))
  print()
  for w in wild:
      print(f"[{w['patron']:14}] casó {w['subject']}")

  await s_star.unsubscribe(); await s_gt.unsubscribe()

source_raw:
  wild = []

  async def on_star(msg):
      wild.append({"patron": "sensor.*.temp", "subject": msg.subject, "data": msg.data.decode()})

  async def on_gt(msg):
      wild.append({"patron": "sensor.>", "subject": msg.subject, "data": msg.data.decode()})

  s_star = await nc.subscribe("sensor.*.temp", cb=on_star)
  s_gt   = await nc.subscribe("sensor.>",      cb=on_gt)

  # Publicamos en varios subjects concretos
  publicados = {
      "sensor.cocina.temp":          "21.5",
      "sensor.salon.temp":           "22.1",
      "sensor.salon.humedad":        "48",
      "sensor.garaje.puerta.estado": "abierta",
  }
  for subj, val in publicados.items():
      await nc.publish(subj, val.encode())
  await nc.flush()
  await asyncio.sleep(0.3)

  print("Mensajes publicados:", list(publicados))
  print()
  for w in wild:
      print(f"[{w['patron']:14}] casó {w['subject']}")

  await s_star.unsubscribe(); await s_gt.unsubscribe()


source:
  import pandas as pd
  import matplotlib.pyplot as plt
  import numpy as np

  patrones = ["sensor.*.temp", "sensor.>"]
  subjects = list(publicados)

  # Construir matriz de coincidencias a partir de lo que recibió cada suscripción
  M = np.zeros((len(patrones), len(subjects)), dtype=int)
  for w in wild:
      i = patrones.index(w["patron"])
      j = subjects.index(w["subject"])
      M[i, j] = 1

  fig, ax = plt.subplots(figsize=(9, 2.6))
  ax.imshow(M, cmap="Greens", vmin=0, vmax=1, aspect="auto")
  ax.set_xticks(range(len(subjects))); ax.set_xticklabels(subjects, rotation=25, ha="right", fontsize=9)
  ax.set_yticks(range(len(patrones))); ax.set_yticklabels(patrones, fontsize=10)
  for i in range(len(patrones)):
      for j in range(len(subjects)):
          ax.text(j, i, "OK" if M[i, j] else "-", ha="center", va="center",
                  color="white" if M[i, j] else "#999", fontsize=12)
  ax.set_title("Coincidencia de wildcards (OK = el subscriber recibió el mensaje)")
  plt.tight_layout(); plt.show()

  pd.DataFrame(M, index=patrones, columns=subjects)

source_raw:
  import pandas as pd
  import matplotlib.pyplot as plt
  import numpy as np

  patrones = ["sensor.*.temp", "sensor.>"]
  subjects = list(publicados)

  # Construir matriz de coincidencias a partir de lo que recibió cada suscripción
  M = np.zeros((len(patrones), len(subjects)), dtype=int)
  for w in wild:
      i = patrones.index(w["patron"])
      j = subjects.index(w["subject"])
      M[i, j] = 1

  fig, ax = plt.subplots(figsize=(9, 2.6))
  ax.imshow(M, cmap="Greens", vmin=0, vmax=1, aspect="auto")
  ax.set_xticks(range(len(subjects))); ax.set_xticklabels(subjects, rotation=25, ha="right", fontsize=9)
  ax.set_yticks(range(len(patrones))); ax.set_yticklabels(patrones, fontsize=10)
  for i in range(len(patrones)):
      for j in range(len(subjects)):
          ax.text(j, i, "OK" if M[i, j] else "-", ha="center", va="center",
                  color="white" if M[i, j] else "#999", fontsize=12)
  ax.set_title("Coincidencia de wildcards (OK = el subscriber recibió el mensaje)")
  plt.tight_layout(); plt.show()

  pd.DataFrame(M, index=patrones, columns=subjects)


source:
  import subprocess, time, asyncio
  import nats

  NATS_CONTAINER = "nats_demo"
  NATS_PORT = 4222
  NATS_URL = f"nats://127.0.0.1:{NATS_PORT}"

  def _docker(*args, check=True):
      return subprocess.run(["docker", *args], capture_output=True, text=True, check=check)

  def ensure_nats(name=NATS_CONTAINER, port=NATS_PORT):
      out = _docker("ps", "-a", "--filter", f"name=^{name}$", "--format", "{{.State}}", check=False).stdout.strip()
      if out == "running":
          state = "already-running"
      elif out in ("exited", "created", "paused"):
          _docker("start", name); state = "started"
      else:
          _docker("run", "-d", "--name", name, "-p", f"{port}:4222", "-p", "8222:8222",
                  "nats:latest", "-js", "-m", "8222"); state = "created"
      time.sleep(1.0)
      return state

  print("Broker:", ensure_nats())
  nc = await nats.connect(NATS_URL, name="notebook-02")
  print("Conectado, client_id:", nc._server_info["client_id"])

source_raw:
  import subprocess, time, asyncio
  import nats

  NATS_CONTAINER = "nats_demo"
  NATS_PORT = 4222
  NATS_URL = f"nats://127.0.0.1:{NATS_PORT}"

  def _docker(*args, check=True):
      return subprocess.run(["docker", *args], capture_output=True, text=True, check=check)

  def ensure_nats(name=NATS_CONTAINER, port=NATS_PORT):
      out = _docker("ps", "-a", "--filter", f"name=^{name}$", "--format", "{{.State}}", check=False).stdout.strip()
      if out == "running":
          state = "already-running"
      elif out in ("exited", "created", "paused"):
          _docker("start", name); state = "started"
      else:
          _docker("run", "-d", "--name", name, "-p", f"{port}:4222", "-p", "8222:8222",
                  "nats:latest", "-js", "-m", "8222"); state = "created"
      time.sleep(1.0)
      return state

  print("Broker:", ensure_nats())
  nc = await nats.connect(NATS_URL, name="notebook-02")
  print("Conectado, client_id:", nc._server_info["client_id"])


source:
  from collections import Counter

  trabajo = Counter()      # cuántas tareas procesó cada worker
  orden = []               # traza temporal (worker, tarea)

  def make_worker(nombre):
      async def worker(msg):
          tarea = msg.data.decode()
          trabajo[nombre] += 1
          orden.append((nombre, tarea))
      return worker

  # 3 workers, MISMO subject, MISMO queue group -> NATS reparte
  workers = []
  for nombre in ["worker-1", "worker-2", "worker-3"]:
      s = await nc.subscribe("tareas", queue="procesadores", cb=make_worker(nombre))
      workers.append(s)

  # Publicar 12 tareas
  for i in range(12):
      await nc.publish("tareas", f"tarea-{i:02d}".encode())
  await nc.flush()
  await asyncio.sleep(0.5)

  print("Reparto de carga (cada tarea fue a UN solo worker):")
  for w, n in sorted(trabajo.items()):
      print(f"  {w}: {n} tareas")
  print(f"  TOTAL procesado: {sum(trabajo.values())} de 12 tareas")

  for s in workers:
      await s.unsubscribe()

source_raw:
  from collections import Counter

  trabajo = Counter()      # cuántas tareas procesó cada worker
  orden = []               # traza temporal (worker, tarea)

  def make_worker(nombre):
      async def worker(msg):
          tarea = msg.data.decode()
          trabajo[nombre] += 1
          orden.append((nombre, tarea))
      return worker

  # 3 workers, MISMO subject, MISMO queue group -> NATS reparte
  workers = []
  for nombre in ["worker-1", "worker-2", "worker-3"]:
      s = await nc.subscribe("tareas", queue="procesadores", cb=make_worker(nombre))
      workers.append(s)

  # Publicar 12 tareas
  for i in range(12):
      await nc.publish("tareas", f"tarea-{i:02d}".encode())
  await nc.flush()
  await asyncio.sleep(0.5)

  print("Reparto de carga (cada tarea fue a UN solo worker):")
  for w, n in sorted(trabajo.items()):
      print(f"  {w}: {n} tareas")
  print(f"  TOTAL procesado: {sum(trabajo.values())} de 12 tareas")

  for s in workers:
      await s.unsubscribe()


source:
  import matplotlib.pyplot as plt

  nombres = [f"worker-{i}" for i in (1, 2, 3)]
  valores = [trabajo.get(n, 0) for n in nombres]

  fig, ax = plt.subplots(figsize=(7, 3))
  barras = ax.bar(nombres, valores, color=["#2563eb", "#16a34a", "#db2777"])
  ax.bar_label(barras, padding=3)
  ax.set_ylabel("tareas procesadas")
  ax.set_title(f"Queue group 'procesadores' — 12 tareas repartidas entre {len(nombres)} workers")
  ax.set_ylim(0, max(valores) + 2)
  plt.tight_layout(); plt.show()

source_raw:
  import matplotlib.pyplot as plt

  nombres = [f"worker-{i}" for i in (1, 2, 3)]
  valores = [trabajo.get(n, 0) for n in nombres]

  fig, ax = plt.subplots(figsize=(7, 3))
  barras = ax.bar(nombres, valores, color=["#2563eb", "#16a34a", "#db2777"])
  ax.bar_label(barras, padding=3)
  ax.set_ylabel("tareas procesadas")
  ax.set_title(f"Queue group 'procesadores' — 12 tareas repartidas entre {len(nombres)} workers")
  ax.set_ylim(0, max(valores) + 2)
  plt.tight_layout(); plt.show()


source:
  # Servicio: convierte el texto recibido a mayúsculas y responde
  async def servicio_mayusculas(msg):
      respuesta = msg.data.decode().upper()
      await msg.respond(respuesta.encode())

  sub_svc = await nc.subscribe("servicio.mayusculas", cb=servicio_mayusculas)

  # Cliente: pide y espera respuesta (con timeout)
  peticiones = ["hola mundo", "nats request reply", "desacople total"]
  for texto in peticiones:
      resp = await nc.request("servicio.mayusculas", texto.encode(), timeout=1.0)
      print(f"  peticion : {texto!r}")
      print(f"  respuesta: {resp.data.decode()!r}  (vino por el inbox {resp.subject})")
      print()

  # Timeout cuando nadie responde a un subject
  try:
      await nc.request("servicio.inexistente", b"hay alguien?", timeout=0.5)
  except asyncio.TimeoutError:
      print("servicio.inexistente -> TimeoutError: nadie respondió (comportamiento esperado)")

  await sub_svc.unsubscribe()

source_raw:
  # Servicio: convierte el texto recibido a mayúsculas y responde
  async def servicio_mayusculas(msg):
      respuesta = msg.data.decode().upper()
      await msg.respond(respuesta.encode())

  sub_svc = await nc.subscribe("servicio.mayusculas", cb=servicio_mayusculas)

  # Cliente: pide y espera respuesta (con timeout)
  peticiones = ["hola mundo", "nats request reply", "desacople total"]
  for texto in peticiones:
      resp = await nc.request("servicio.mayusculas", texto.encode(), timeout=1.0)
      print(f"  peticion : {texto!r}")
      print(f"  respuesta: {resp.data.decode()!r}  (vino por el inbox {resp.subject})")
      print()

  # Timeout cuando nadie responde a un subject
  try:
      await nc.request("servicio.inexistente", b"hay alguien?", timeout=0.5)
  except asyncio.TimeoutError:
      print("servicio.inexistente -> TimeoutError: nadie respondió (comportamiento esperado)")

  await sub_svc.unsubscribe()


source:
  js = nc.jetstream()

  # Crear (o actualizar) un stream que persiste todo lo de 'eventos.>'
  # Lo recreamos limpio para que la demo sea determinista
  try:
      await js.delete_stream("EVENTOS")
  except Exception:
      pass
  stream = await js.add_stream(name="EVENTOS", subjects=["eventos.>"])
  print(f"Stream creado: {stream.config.name}  subjects={stream.config.subjects}  storage={stream.config.storage}")

  # Publicar 5 eventos SIN que haya ningún consumidor escuchando todavía
  for i in range(5):
      ack = await js.publish("eventos.click", f"evento #{i}".encode())
      print(f"  publicado eventos.click -> stream='{ack.stream}' seq={ack.seq}")

  info = await js.find_stream_name_by_subject("eventos.click")
  estado = (await js.stream_info("EVENTOS")).state
  print(f"\nMensajes retenidos en el stream: {estado.messages} (siguen ahí aunque nadie los haya leído)")

source_raw:
  js = nc.jetstream()

  # Crear (o actualizar) un stream que persiste todo lo de 'eventos.>'
  # Lo recreamos limpio para que la demo sea determinista
  try:
      await js.delete_stream("EVENTOS")
  except Exception:
      pass
  stream = await js.add_stream(name="EVENTOS", subjects=["eventos.>"])
  print(f"Stream creado: {stream.config.name}  subjects={stream.config.subjects}  storage={stream.config.storage}")

  # Publicar 5 eventos SIN que haya ningún consumidor escuchando todavía
  for i in range(5):
      ack = await js.publish("eventos.click", f"evento #{i}".encode())
      print(f"  publicado eventos.click -> stream='{ack.stream}' seq={ack.seq}")

  info = await js.find_stream_name_by_subject("eventos.click")
  estado = (await js.stream_info("EVENTOS")).state
  print(f"\nMensajes retenidos en el stream: {estado.messages} (siguen ahí aunque nadie los haya leído)")


source:
  # Pull consumer durable: pedimos los mensajes bajo demanda
  psub = await js.pull_subscribe("eventos.>", durable="lector-eventos")

  mensajes = await psub.fetch(5, timeout=2)
  print(f"Recuperados {len(mensajes)} mensajes del stream (replay):")
  for m in mensajes:
      print(f"  seq={m.metadata.sequence.stream}  subject={m.subject}  data={m.data.decode()!r}")
      await m.ack()  # confirmamos el procesado; el durable avanza su cursor

  # Tras el ack, un segundo fetch no devuelve nada nuevo (cursor avanzado)
  try:
      extra = await psub.fetch(1, timeout=1)
  except (asyncio.TimeoutError, Exception):
      extra = []
  print(f"\nSegundo fetch tras ack: {len(extra)} mensajes (el durable recuerda que ya los leyó)")

source_raw:
  # Pull consumer durable: pedimos los mensajes bajo demanda
  psub = await js.pull_subscribe("eventos.>", durable="lector-eventos")

  mensajes = await psub.fetch(5, timeout=2)
  print(f"Recuperados {len(mensajes)} mensajes del stream (replay):")
  for m in mensajes:
      print(f"  seq={m.metadata.sequence.stream}  subject={m.subject}  data={m.data.decode()!r}")
      await m.ack()  # confirmamos el procesado; el durable avanza su cursor

  # Tras el ack, un segundo fetch no devuelve nada nuevo (cursor avanzado)
  try:
      extra = await psub.fetch(1, timeout=1)
  except (asyncio.TimeoutError, Exception):
      extra = []
  print(f"\nSegundo fetch tras ack: {len(extra)} mensajes (el durable recuerda que ya los leyó)")


