IR
irwinrodriguez.dev
Volver a documentación

Funciones avanzadas

Lo que hay por encima de servir un JSON: eventos en tiempo real, webhooks salientes firmados, el endpoint de estado, el modo servicio, el despliegue en la instalación y el banco de pruebas.

Server-Sent Events (SSE)

SSE deja que el servidor empuje eventos al cliente por una conexión HTTP que se queda abierta. El contrato tiene tres piezas, y las tres se ven desde el controlador: la ruta tiene que terminar en /events y estar declarada, el cliente tiene que mandar Accept: text/event-stream o recibe un 406, y el backend declara qué eventos soporta con res.events(). El framing, el retry:, el evento connected, los pings y la reposición por Last-Event-Id los pone el middleware.

Habilitar en JSON:

"middleware": { "sse": { "enabled": true } }

En el controlador:

SSE: declarar y emitir los eventos de pedidos
* La ruta TIENE que terminar en /events. Aqui no se escribe el
* framing a mano ni se hace un bucle: eso ocuparia uno de los pocos
* hilos VFP del pool durante todo el rato. Lo unico que hace este
* endpoint es DECLARAR que eventos existen.
PROCEDURE pSuscripcion(req, res) HELP "GET: pedidos/events public"
    res.events("pedido.creado")
    res.events("pedido.enviado")
    RETURN THIS.Ok(.NULL.)
ENDPROC

* Y se emite desde cualquier otro endpoint, cuando pasa algo.
PROCEDURE pCrearPedido(req, res) HELP "POST: pedidos"
    LOCAL lcId, loRes

    lcId = THIS.CrearPedido(req.json)

    res.emit("pedido.creado", '{"id":"' + lcId + '"}')

    loRes    = THIS.newObject("id")
    loRes.id = lcId
    RETURN THIS.Created(loRes, "/pedidos/" + lcId)
ENDPROC

Cliente JavaScript:

// EventSource manda Accept: text/event-stream por su cuenta.
// Sin esa cabecera, el middleware responde 406.
const es = new EventSource('/pedidos/events?events=pedido.creado');

// El middleware manda primero un evento 'connected' que dice que
// eventos hay, y despues los tuyos, cada uno con su id.
es.addEventListener('pedido.creado', (e) => {
  console.log(JSON.parse(e.data).id);
});

// Si se corta, el navegador reconecta solo mandando Last-Event-Id,
// y el servidor repone lo que tenga en cache: la entrega es
// at-least-once, asi que deduplica por e.lastEventId.
es.onerror = () => es.close();
Pedir un evento que el backend no declara te deja suscrito a NADA No a todo. El comodin solo es el valor por defecto cuando no mandas ?events= en absoluto.

Sistema de Webhooks

FoxServer implementa Standard Webhooks en las dos direcciones, y está auditado y con test. Lo importante del modelo: los salientes no se disparan desde el código, se declaran en el manifiesto, y al servir ese endpoint el cuerpo de la respuesta sale firmado hacia los suscriptores activos.

  • Se declaran con foxserver webhook-add-out, y se ajustan con webhook-set --retries --timeout --backoff
  • Firma HMAC-SHA256 en base64 sobre {id}.{timestamp}.{body}, con prefijo v1,
  • El secreto puede ser whsec_<base64> (el formato de svix, Clerk o Resend) y entonces la clave son los bytes decodificados
  • retries: N son N intentos en total, no N reintentos extra. Y un 404 del suscriptor corta los reintentos de inmediato
  • Lo que se agota acaba en <host>\queue\webhooks\dead-letters\
  • Todos los intentos comparten webhook-id, para que el receptor deduplique, y cada uno se refirma con su propio timestamp
  • Se prueba sin salir de la consola con foxserver webhook-test

Declarar uno saliente:

REM Se declara con la CLI, no editando el manifiesto a mano.
foxserver webhook-add-out --project C:\proyectos\tienda ^
    --route pedidos --method POST ^
    --url https://socio.com/webhooks --secret whsec_...

REM Y se ajusta despues:
foxserver webhook-set --project C:\proyectos\tienda ^
    --route pedidos --retries 3 --timeout 30 --backoff 5,15,30

REM Para probarlo sin salir de la consola:
foxserver webhook-test --project C:\proyectos\tienda --route pedidos

Y ya está: el endpoint se escribe como cualquier otro. Lo que devuelva es lo que se manda.

pCrearPedido: lo que devuelve sale firmado
* Ni una linea de webhook aqui dentro. Lo que devuelve este endpoint
* es exactamente lo que sale firmado hacia los suscriptores.
PROCEDURE pCrearPedido(req, res) HELP "POST: pedidos"
    LOCAL lcId, loRes

    lcId = THIS.CrearPedido(req.json)

    loRes          = THIS.newObject("orderId,status")
    loRes.orderId  = lcId
    loRes.status   = "created"
    RETURN THIS.Created(loRes, "/pedidos/" + lcId)
ENDPROC

Lo que le llega al suscriptor:

POST https://socio.com/webhooks
webhook-id: 550e8400-e29b-41d4-a716-446655440000
webhook-timestamp: 1789476225
webhook-signature: v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=

{"orderId":"ORD-001","status":"created"}

El webhook-timestamp es epoch en segundos, y la firma es v1, más el base64 del HMAC-SHA256 sobre {id}.{timestamp}.{body}. Si tu controlador llega a ejecutarse con un webhook ENTRANTE, es que el middleware ya comprobó la firma y la ventana anti-repetición de cinco minutos: el webhook ya es auténtico. Ojo con una cosa: un proveedor con firma propia, como Stripe o GitHub, no se declara como entrante, porque el middleware lo rechazaría por no ser Standard Webhooks. Esa ruta se sirve como un endpoint normal y la verificas tú.

Recargar sin reiniciar

No hay recarga automática, y conviene decirlo claro porque la palabra «hot reload» sugiere otra cosa: FoxServer no vigila el disco. Lo que hay es un interruptor, y basta: después de un foxserver build, un POST al endpoint de estado suelta los objetos COM del servidor, y la siguiente petición los crea limpios, ya con tu DLL nueva.

El ciclo completo:

  1. Editas y guardas tu .prg.
  2. Recompilas con foxserver build (o desde FoxForge) y se regenera la MTDLL del proyecto. Ojo: hace falta consola elevada, porque BUILD MTDLL registra la coclase al terminar.
  3. POST <endpoint-de-estado>/reload con la cabecera X-Status-Pin.
  4. El servidor suelta sus referencias COM. La siguiente petición crea los controladores otra vez, ya con el código nuevo.

La llamada:

curl -X POST http://127.0.0.1:8099/status/reload ^
     -H "X-Status-Pin: <tu PIN>"

El endpoint de estado

Se declara en el manifiesto y pide un PIN: sin la cabecera X-Status-Pin, o con un PIN que no es, responde 401; si el servidor no tiene PIN configurado, 403. Cualquier verbo que no sea GET sobre esa ruta da 404, no 405.

"server": {
  "checkstatusenabled": true,
  "endpoint": "status",
  "statuspin": "<cifrado con middleware-set>"
}
curl http://127.0.0.1:8099/status -H "X-Status-Pin: <tu PIN>"

Y lo que devuelve:

{
  "uptime": {
    "raw": "...",
    "formatted": "5d 3h 22m"
  },
  "memory": {
    "raw": 257425408,
    "formatted": "245 MB"
  },
  "connections": {
    "current": 7
  },
  "config": {
    "host": "127.0.0.1",
    "port": 8099,
    "middleware": {
      "auth": true,
      "logging": true
    }
  },
  "license": {
    "kind": "Runtime",
    "holder": "Tu Empresa, S.L.",
    "seats": 1,
    "isMaintenanceActive": true,
    "limitedByMaintenance": false
  },
  "metrics": {
    "successRate": 99.97,
    "rejectedRequests": 0,
    "rejectionRate": 0
  }
}
license.kind es lo que decide quién te puede llamar Developer o Runtime. Con Developer, el servidor responde 403 a toda petición que no venga de 127.0.0.1. Es lo primero que hay que mirar cuando desde otra máquina todo da 403.

Modo Servicio de Windows

FoxServer puede instalarse como un Servicio de Windows para arranque automático y ejecución en segundo plano sin ventana de consola.

Comandos:

# Install as service
bin\FoxServer.Host.exe --install-service

# Uninstall service
bin\FoxServer.Host.exe --uninstall-service

# Manage via Windows Services
services.msc
En modo servicio los logs van a los ficheros de siempre. El Visor de Eventos de Windows NO los recibe: allí solo se escribe una entrada si falla la escritura del propio log. El servicio lo da de alta el instalador; a mano, bin\FoxServer.Host.exe --install-service desde una consola elevada.

Desplegar en la instalación

foxserver serve levanta el host apuntando a la carpeta build\ de tu proyecto: está bien para probar, pero ese servidor no lo ve verify.cmd ni lo levanta el servicio de Windows, porque los dos miran config\servers\ de la raíz de la instalación. Cuando quieras que forme parte de ella:

foxserver deploy --project C:\proyectos\almacen
Desplegado Almacen en la instalacion:
  C:\Program Files\FoxServer\config\servers\ALMACEN.json
  clave: 127.0.0.1:8099:
Antes de escribir comprueba que no haya otro manifiesto con la misma clave host:puerto:prefijo. Si lo hay, sale con 11 y dice cuál es — y hace bien: el host arrancaría uno y omitiría el otro con un aviso que sólo se ve en el log, que es de los fallos más desagradables de diagnosticar. --force lo salta.

Reconstruir el entorno de tu aplicación

Tu lógica de negocio necesita lo que monta el main.prg de tu ERP — la configuración, la conexión, las tablas maestras —, y un controlador nace sin nada de eso. No hay que reescribir la lógica: hay que reconstruir el entorno una vez. Para eso está el lifecycle.

MiErpLifecycle: montar el entorno del ERP al arrancar
define class MiErpLifecycle as ApplicationLifecycle olepublic

    procedure OnApplicationStarting
        local loCfg
        * Lo que hace el main.prg de tu aplicacion: SET PATH, abrir las
        * maestras, leer la configuracion, conectar.
        loCfg = createobject("Empty")
        addproperty(loCfg, "empresa", "SbSoftware")
        this.SetResource("config", loCfg)
    endproc

    procedure OnApplicationStopping
        * Cerrar lo que se abrio.
    endproc

enddefine
"lifecycle": { "enabled": true, "classname": "MiErpLifecycleClass" }
olepublic no es opcional. Sin él la clase no llega a la type library de la MTDLL y el motor no puede activarla: verás "Cannot create ApplicationLifecycle instance" en el log. El classname del manifiesto lo rellena el hook al compilar, con su sufijo, así que no hay que escribirlo a mano.

Depurar sin compilar — FoxServerBench

La herramienta que más tiempo ahorra: ejecuta tu controlador sin MTDLL, sin servidor y sin http.sys, con el depurador abierto sobre tu propio fuente. Pones SET STEP ON en tu procedure, lanzas el banco, y paras donde quieras.

FoxServerBench: ejecutar un endpoint sin servidor
loBench = createobject("FoxServerBench")
loBench.SetProject("c:\misproyectos\ventas")
loBench.OnDeclare = "MiCarga"              && tu rutina: SET PROCEDURE, abrir tablas...
loBench.SetResource("config", loFalsa)     && simula lo que deja el lifecycle

loBench.Request("GET", "articulos/42")
loRes = loBench.Run()

? loRes.StatusCode
? loRes.Body
Tres cosas antes de acusar al banco de mentir. JSONFox.fxp tiene que estar COMPILADO en el build\ de tu proyecto: sin él, un POST con cuerpo JSON llega con req.json a null y nadie te avisa. El API no se encadena: Request(...).Run() no funciona, cada llamada va suelta. Y es un banco, no un emulador: no reproduce middlewares, ni autenticación, ni CORS, ni concurrencia. Un verde aquí demuestra que tu lógica es servible, no que el endpoint autentique bien.

Siguiente: Referencia API →