Erweiterte Funktionen
Was über dem Ausliefern eines JSON liegt: Echtzeitereignisse, signierte ausgehende Webhooks, der Status-Endpunkt, der Dienstmodus, die Verteilung in die Installation und der Prüfstand.
Server-Sent Events (SSE)
SSE lässt den Server Ereignisse über eine offen bleibende HTTP-Verbindung an den Client schieben. Der Vertrag hat drei Teile, und alle drei sind vom Controller aus sichtbar: die Route muss auf /events enden und deklariert sein, der Client muss Accept: text/event-stream senden oder bekommt 406, und das Backend erklärt mit res.events(), welche Ereignisse es unterstützt. Framing, retry:, das Ereignis connected, die Pings und das Nachliefern über Last-Event-Id übernimmt die Middleware.
In JSON aktivieren:
"middleware": { "sse": { "enabled": true } }Im Controller:
* 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)
ENDPROCJavaScript-Client:
// 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();Webhook-System
FoxServer setzt Standard Webhooks in beide Richtungen um, geprüft und mit Tests. Das Wesentliche am Modell: ausgehende Webhooks werden nicht aus dem Code ausgelöst, sondern im Manifest deklariert, und beim Bedienen dieses Endpunkts geht der Rumpf der Antwort signiert an die aktiven Abonnenten.
- Deklariert mit
foxserver webhook-add-out, eingestellt mitwebhook-set --retries --timeout --backoff - HMAC-SHA256-Signatur in base64 über
{id}.{timestamp}.{body}, mit Präfixv1, - Das Geheimnis darf
whsec_<base64>sein (das Format von svix, Clerk oder Resend); dann sind die dekodierten Bytes der Schlüssel retries: Nbedeutet N Versuche insgesamt, nicht N zusätzliche. Und ein 404 des Abonnenten bricht die Wiederholungen sofort ab- Was aufgebraucht ist, landet in
<host>\queue\webhooks\dead-letters\ - Alle Versuche teilen dieselbe
webhook-id, damit der Empfänger entdoppeln kann, und jeder wird mit eigenem Zeitstempel neu signiert - Getestet wird ohne die Konsole zu verlassen, mit
foxserver webhook-test
Einen ausgehenden deklarieren:
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 pedidosUnd das war es: der Endpunkt wird wie jeder andere geschrieben. Was er zurückgibt, wird gesendet.
* 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)
ENDPROCWas beim Abonnenten ankommt:
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"}Der webhook-timestamp ist Epoch in Sekunden, und die Signatur ist v1, plus das base64 des HMAC-SHA256 über {id}.{timestamp}.{body}. Läuft Ihr Controller bei einem EINGEHENDEN Webhook überhaupt an, hat die Middleware Signatur und das Fünf-Minuten-Fenster gegen Wiedereinspielung schon geprüft: der Webhook ist bereits echt. Eine Sache ist zu beachten: ein Anbieter mit eigener Signatur, etwa Stripe oder GitHub, wird nicht als eingehend deklariert, denn die Middleware würde ihn ablehnen, weil er keine Standard Webhooks sind. Diese Route wird als normaler Endpunkt bedient, und Sie prüfen sie selbst.
Neu laden ohne Neustart
Es gibt kein automatisches Neuladen, und das gehört deutlich gesagt, weil das Wort „Hot Reload“ etwas anderes nahelegt: FoxServer überwacht die Festplatte nicht. Es gibt einen Schalter, und der genügt: nach einem foxserver build gibt ein POST an den Status-Endpunkt die COM-Objekte des Servers frei, und die nächste Anfrage erzeugt sie sauber neu, schon mit Ihrer neuen DLL.
Der vollständige Ablauf:
- Sie bearbeiten und speichern Ihre
.prg. - Sie kompilieren mit
foxserver build(oder aus FoxForge) neu, und die MTDLL des Projekts entsteht neu. Achtung: dafür braucht es eine erhöhte Konsole, dennBUILD MTDLLregistriert am Ende die Coclass. POST <Status-Endpunkt>/reloadmit dem HeaderX-Status-Pin.- Der Server gibt seine COM-Referenzen frei. Die nächste Anfrage erzeugt die Controller erneut, nun mit dem neuen Code.
Der Aufruf:
curl -X POST http://127.0.0.1:8099/status/reload ^
-H "X-Status-Pin: <tu PIN>"Der Status-Endpunkt
Er wird im Manifest deklariert und verlangt eine PIN: ohne den Header X-Status-Pin oder mit falscher PIN antwortet er mit 401; hat der Server keine PIN eingerichtet, mit 403. Jedes andere Verb als GET auf dieser Route ergibt 404, nicht 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>"Und was er zurückgibt:
{
"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
}
}Windows-Dienstmodus
FoxServer kann als Windows-Dienst installiert werden für automatischen Start und Hintergrundbetrieb ohne Konsolenfenster.
Befehle:
# Install as service
bin\FoxServer.Host.exe --install-service
# Uninstall service
bin\FoxServer.Host.exe --uninstall-service
# Manage via Windows Services
services.mscIn die Installation ausrollen
foxserver serve startet den Host mit Blick auf den Ordner build\ Ihres Projekts: zum Testen gut, aber verify.cmd sieht diesen Server nicht und der Windows-Dienst startet ihn nicht, denn beide schauen in config\servers\ der Installationswurzel. Wenn er Teil davon werden soll:
foxserver deploy --project C:\proyectos\almacenDesplegado Almacen en la instalacion:
C:\Program Files\FoxServer\config\servers\ALMACEN.json
clave: 127.0.0.1:8099:Die Umgebung Ihrer Anwendung wiederherstellen
Ihre Geschäftslogik braucht das, was die main.prg Ihres ERP aufbaut - Konfiguration, Verbindung, Stammtabellen -, und ein Controller wird ohne all das geboren. Die Logik muss nicht neu geschrieben werden: die Umgebung muss einmal wiederhergestellt werden. Dafür gibt es den 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" }Debuggen ohne Kompilieren - FoxServerBench
Das Werkzeug, das am meisten Zeit spart: es führt Ihren Controller ohne MTDLL, ohne Server und ohne http.sys aus, mit dem Debugger über Ihrer eigenen Quelle. Sie setzen SET STEP ON in Ihre Prozedur, starten die Bank und halten an, wo Sie wollen.
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.BodyWeiter: API-Referenz →