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:
* 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)
ENDPROCCliente 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();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 conwebhook-set --retries --timeout --backoff - Firma HMAC-SHA256 en base64 sobre
{id}.{timestamp}.{body}, con prefijov1, - El secreto puede ser
whsec_<base64>(el formato de svix, Clerk o Resend) y entonces la clave son los bytes decodificados retries: Nson 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 pedidosY ya está: el endpoint se escribe como cualquier otro. Lo que devuelva es lo que se manda.
* 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)
ENDPROCLo 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:
- Editas y guardas tu
.prg. - Recompilas con
foxserver build(o desde FoxForge) y se regenera la MTDLL del proyecto. Ojo: hace falta consola elevada, porqueBUILD MTDLLregistra la coclase al terminar. POST <endpoint-de-estado>/reloadcon la cabeceraX-Status-Pin.- 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
}
}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.mscDesplegar 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\almacenDesplegado Almacen en la instalacion:
C:\Program Files\FoxServer\config\servers\ALMACEN.json
clave: 127.0.0.1:8099: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.
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" }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.
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.BodySiguiente: Referencia API →