Repository navigation
ServerEvents de
🌐 Diese Seite auf: English · Português · Español · Français · Deutsch
Was der Server veröffentlicht, von einer Komponente gehört, während die Person zusieht: ein Preis, der sich bewegt, eine Nachricht in einem Raum, eine Bestellung, die ihren Zustand geändert hat. Die Komponente abonniert ein typisiertes Topic, beliebiger Code auf dem Server veröffentlicht auf dasselbe Topic, und das SDK trägt es über eine Verbindung pro Seite und übergibt der Komponente jede Nutzlast als den C#-Typ, der sie war.
Seit 0.2.0-preview.61
public sealed record RoomMessage(string Author, string Text, long Sequence);
public static class Topics
{
public static ServerTopic<RoomMessage> Room(string id) => new($"room:{id}");
}Ein Topic ist ein Wert. Sein Name ist der eigene der App, ohne auferlegte Konvention, und sein
Typparameter ist der Vertrag: Subscribe übergibt der Komponente ein Action<RoomMessage>,
PublishAsync nimmt ein RoomMessage, also ist eine Nutzlast des falschen Typs ein Kompilierfehler
statt eines undefined im Browser. Deklariere es einmal, im C# der App, und verwende denselben Wert
auf beiden Seiten.
Baue ein Topic dort, wo sein Nutzlasttyp konkret ist, wie oben. Ein Topic, das dort gebaut wird, wo
dieser Typ ein Typparameter ist, in einer Hilfe wie static ServerTopic<T> Topic<T>(string name),
bricht den Build mit EQ2013: JavaScript löscht den Typ dort, und das Topic könnte
nicht wiederbeleben, was es empfängt. Ein Topic, das die Seite vom Server erhält, das Ergebnis einer
Server Action oder der Zustand einer Seite, behält den Typ seiner Nutzlast.
IServerEvents ist eine Fähigkeit, über den Konstruktor genommen wie
INetworkStatus:
public sealed class RoomScreen(IServerEvents? events) : StatefulComponent
{
private readonly List<RoomMessage> _messages = [];
private IDisposable? _room;
private IDisposable? _connection;
private ServerConnectionState _state;
private bool _refused;
protected override void OnMount()
{
_room = events?.Subscribe(Topics.Room("lobby"),
message => SetState(() => _messages.Add(message)),
refusal => SetState(() => _refused = true));
_connection = events?.OnConnectionChanged(connection => SetState(() => _state = connection.State));
}
protected override void OnUnmount()
{
_room?.Dispose();
_connection?.Dispose();
}
public override VisualNode Build(ComponentContext context) => _refused
? Text("This room is not open to you.", TypeRole.BodyM)
: Column(gap: Space.S2, children: [
Text(_state == ServerConnectionState.Connected ? "Live" : "Catching up", TypeRole.Caption),
.. _messages.Select(message => Text(message.Text, TypeRole.BodyM)),
]);
}-
Abonniere in
OnMount, gib inOnUnmountfrei. Zweimal freizugeben ist in Ordnung. Eine Seite, die auf dem Server rendert, abonniert dort nichts: dasIServerEventsdes Servers verbindet sich nie, also rendert dieselbe Komponente, und das des Browsers abonniert, sobald es hydriert. - Eine Verbindung pro Seite. Das erste Abonnement öffnet sie und das letzte schließt sie, und jedes Topic, das die Seite hält, fährt darauf mit, welche Komponente auch abonniert hat.
-
Die Nutzlast kommt wiederbelebt an. Sie wird im Leitungsformat des Ergebnisses einer Server
Action geschrieben und auf dieselbe Weise wiederbelebt, also lesen sich ein Record, ein Enum, ein
decimalund einlongso, wie sie sich in C# lesen, und ebenso das Typargument eines generischen Records (dasValuevonBox<long>kommt alslongan). -
Eine Ablehnung sagt, warum.
onRefusederhält einServerTopicRefusal(Topic, Reason):Forbidden(eine Regel passte und lehnte diese Anfrage ab),Unknown(keine Regel passt auf das Topic),LimitReached(die Seite hält so viele Topics, wie der Server erlaubt) oderFailed(der Server oder das Netz antwortete nicht, viermal gefragt). Das Abonnement empfängt dann nichts. Ohne Handler sagt die Konsole des Browsers, welches Topic und warum.
Connection antwortet, wo sie jetzt steht, als ein ServerConnection(State, LastEventId), und
OnConnectionChanged hört jeden Zustandswechsel: Connecting, Connected, Reconnecting,
Disconnected.
Wenn die Verbindung abbricht (ein Netz, das blinkte, ein Server, der neu startete, ein Deployment),
wird sie von selbst wieder geöffnet und jedes lebende Abonnement wird neu gebunden. Connected wird
gemeldet, sobald sie gebunden sind, mit der id des letzten Ereignisses, das die Seite erhalten hat,
sodass eine Seite, die den Server nach dem fragt, was sie verpasst hat, über eine
Server Action, dann fragt, wenn dazwischen nichts mehr verpasst werden kann. Die
Zustellung ist höchstens einmal; die id ist, wie eine Seite aufholt.
Eine Bindung, für die der Server nicht antworten kann, wird erneut gefragt, bevor die Seite es erfährt: ein Stream, der eben geendet hat, ist dem Server einen Augenblick unbekannt, bevor der Browser ihn enden sieht, und ein Proxy antwortet für eine Instanz, die ein Deployment wegnimmt. Die Seite fragt nach einer halben Sekunde, einer Sekunde und zwei Sekunden erneut, auf welcher Verbindung sie dann auch ist, und erst ein vierter Fehlschlag in Folge lehnt das Topic ab. Eine Freigabe, die unbeantwortet blieb, wird auf dieselbe Weise erneut gefragt, und beim letzten Fehlschlag öffnet die Seite ihren Stream erneut, was alles freigibt, was der alte hielt. Jede Anfrage bekommt zehn Sekunden, ihre Antwort eingeschlossen, und endet sofort, wenn ihr Stream abbricht. Eine Bindung, die unbeantwortet blieb, hat das Topic möglicherweise doch gebunden, also gibt die Seite auch dieses frei, wenn sie es loslässt.
public sealed class RoomService(IServerEventPublisher events)
{
public ValueTask PostAsync(string room, RoomMessage message, CancellationToken cancellationToken) =>
events.PublishAsync(Topics.Room(room), message,
new ServerEventPublishOptions { Id = message.Sequence.ToString(CultureInfo.InvariantCulture) },
cancellationToken);
}Beliebiger Code auf dem Server veröffentlicht: eine Server Action, ein gehosteter Dienst, der Handler
eines Webhooks. Die id ist optional und reist als die id des Ereignisses (LastEventId auf der
Seite); sie darf keinen Zeilenumbruch enthalten. Eine Nutzlast, die größer als die Grenze ist, wirft
dort, wo sie veröffentlicht wird, und benennt das Topic und die Einstellung.
Eine Klasse im Projekt der App, die nur auf dem Server läuft, einschließlich eines gehosteten
Dienstes, der veröffentlicht, wird mit [ServerOnly] markiert, oder der Compiler versucht, sie für
den Browser zu bauen, und sagt es (EQ2004).
Nichts wird gehört, bis die App sagt, wer es hören darf:
builder.Services.AddUI(options => options
.ScanAssembly(typeof(Program).Assembly)
.UseServerEvents(events => events
.Topic("prices", rule => rule.AllowAnonymous())
.Topic("room:{roomId}", rule => rule.RequireAuthorization("RoomMember"))
.Topic("user:{userId}", rule => rule.Authorize(topic =>
topic.Values["userId"] == topic.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier)))));-
Vorlagen sind Routen. Ein Topic wird gegen jede Vorlage mit der Routensyntax von ASP.NET Core
abgeglichen, und die Vorlage, deren wörtlicher Text mehr vom Topic festlegt, entscheidet:
room:{roomId}undroom:{roomId}:participant:{participantId}passen beide aufroom:a:participant:7, und die zweite entscheidet. Vorlagen, die gleich viel festlegen, entscheiden alle, und jede muss das Abonnement erlauben. Der Standardwert eines Parameters füllt ein Topic, das ihn auslässt:room/{roomId=lobby}passt aufroom. Ein Topic, auf das keine Vorlage passt, wird alsUnknownabgelehnt: standardmäßig geschlossen. -
RequireAuthorization()ohne Richtlinie verlangt die Standardrichtlinie der App, einen angemeldeten Benutzer, sofern die App sie nicht geändert hat. Mit Richtlinien werden sie so ausgewertet, wie die Autorisierungs-Middleware von ASP.NET Core die eines Endpunkts auswertet: zusammengefasst, ihre Authentifizierungsschemata zuerst authentifiziert, sodass eine auf ein Bearer-Token beschränkte Richtlinie nur vom Benutzer dieses Tokens erfüllt wird, nie von dem des Cookies. Das Topic ist ihre Ressource: einAuthorizationHandler<TRequirement, ServerTopicContext>liest die Werte der Vorlage (topic.Values["roomId"]), den Namen des Topics, die id der Verbindung und die Anfrage. -
Authorize(…)nimmt einen Delegaten, synchron oder asynchron, für eine Prüfung, für die eine Richtlinie schwer wäre. -
Beim Start scheitert eine Regel, die nichts sagt, und ebenso ein eingeschränkter Parameter
(
{id:int}): eine Einschränkung würde im Stillen ein Topic ablehnen, das die App selbst geschrieben hat. - Die Ereignis-Endpunkte lassen anonyme Anfragen zu, sodass die Rückfall-Autorisierungsrichtlinie einer App nicht die Verbindung selbst ablehnt: die Regel jedes Topics entscheidet, und eine Verbindung trägt nichts, bis ein Topic an sie gebunden ist.
-
UseServerEventsdarf mehr als einmal aufgerufen werden. Eine Bibliothek und die App dürfen jede ihre Topics deklarieren: jeder Aufruf addiert sich zu einer Konfiguration, die von einem Satz von Endpunkten bedient wird. Ein Topic, das beide deklarieren, wird von beiden geregelt, wobei jede Regel es erlaubt oder nicht, also zieht die App ein Topic an, das eine Bibliothek deklariert hat, und nichts lockert es. -
Eine App, die
UseServerEventsnie aufruft, sagt es ihren Seiten. Ein Abonnement dort wird sofort alsUnknownabgelehnt, und die Konsole des Browsers benenntUseServerEvents, statt dass die Seite einen Stream öffnet, den nichts beantwortet.
IServerEventHandler hört eine Verbindung sich öffnen und schließen und ein Topic gebunden und
freigegeben werden, jedes mit der Anfrage zur Hand und den Werten der Vorlage. Präsenz, Belegung und
Audit werden darauf gebaut:
public sealed class RoomPresence(IRoomRegistry rooms) : IServerEventHandler
{
public ValueTask OnSubscribedAsync(ServerTopicContext topic) =>
rooms.JoinAsync(topic.Values["roomId"]!, topic.ConnectionId);
public ValueTask OnReleasedAsync(ServerTopicContext topic) =>
rooms.LeaveAsync(topic.Values["roomId"]!, topic.ConnectionId);
}
// …
.UseServerEvents(events => events
.Topic("room:{roomId}", rule => rule.RequireAuthorization())
.AddHandler<RoomPresence>())Ein Topic, das die Seite freigibt, und jedes Topic einer Verbindung, die endet, wird als freigegeben gehört. Ein Handler, der wirft, wird protokolliert, und die anderen hören es dennoch. Die Handler einer Verbindung hören ihre Übergänge in Reihenfolge: ein Beitritt, auf den noch gewartet wird, wenn der Stream schließt, wird vor der Freigabe dieses Topics und der Trennung gehört, also hält ein Handler, der nie zurückkehrt, das Schließen seiner Verbindung auf.
IServerEventBackplane trägt jedes veröffentlichte Ereignis zu jeder Instanz der App; die Vorgabe
hält sie im Prozess, was alles ist, was eine Instanz braucht. Eine App, die mehrere betreibt,
registriert ihre eigene, über Redis-Pub/Sub oder einen Servicebus, mit UseBackplane<T>(): sie
veröffentlicht ein ServerEventEnvelope (das Topic, die schon geschriebene Nutzlast, die id) und
übergibt der Instanz jedes, das sie empfängt.
Eine Seite bindet ihre Topics mit Anfragen, die ihre Verbindung benennen, also müssen sie hinter
einem Lastverteiler die Instanz erreichen, die ihren Stream hält: Sitzungsaffinität, wie SignalR
sie verlangt. Eine Bindung, die weiterhin eine andere Instanz erreicht, wird nach vier Versuchen als
Failed gemeldet, und die Konsole des Browsers benennt die Ursache.
Aus EQuantic:ServerEvents in der appsettings.json gebunden, oder mit Configure(…) am Builder
gesetzt:
| Schlüssel | Vorgabe | Was er begrenzt |
|---|---|---|
HeartbeatInterval |
00:00:15 |
Wie lange eine Verbindung still bleibt, bevor ein Herzschlag Proxys davon abhält, sie zu schließen |
MaxTopicsPerConnection |
64 |
Die Topics, die eine Seite gleichzeitig halten darf |
MaxPayloadBytes |
65536 |
Die Größe einer Nutzlast, geprüft dort, wo sie veröffentlicht wird |
MaxQueuedEventsPerConnection |
256 |
Die Ereignisse, die auf eine langsam lesende Seite warten; darüber schließt die Verbindung, und die Seite verbindet sich erneut |
{
"EQuantic": {
"ServerEvents": {
"MaxTopicsPerConnection": 16
}
}
}Ein Wert, mit dem die Verbindungen nicht laufen können, ein Herzschlag von null oder weniger oder eine Grenze unter eins, hält die App vom Starten ab, und der Fehler benennt den Schlüssel und den Wert, den er bekam.
-
GET /_equantic/eventsist ein Server-Sent-Events-Stream: sein erstes Ereignis übergibt der Seite die id ihrer Verbindung, jede Nutzlast ist einmessage-Ereignis, das{ "topic", "payload" }trägt, und eine Kommentarzeile ist der Herzschlag. Er wird nie zwischengespeichert, nie komprimiert, und bittet einen Reverse-Proxy, ihn nicht zu puffern (X-Accel-Buffering: no). -
POST /_equantic/events/{connection}/subscribeund…/release, mit{ "topic": "…" }, binden und geben ein Topic frei:204, wenn es getan ist,403mit dem Grund,404für eine Verbindung, die der Server nicht kennt. Nichts, was eine Seite sendet, veröffentlicht; der Weg von der Seite zum Server ist eine Server Action. - Ihr Körper muss JSON sein (
415sonst) von höchstens 8 KB (413). Ein Topic wird wie die Anfrage autorisiert, die es bindet, Cookies eingeschlossen, also würde ein Körper, den eine andere Seite senden könnte, der eines Formulars oder einesno-cors-Fetch, dieser Seite erlauben, die Topics ihrer Besucher an einen Stream zu binden, den sie geöffnet hat; ein JSON-Körper von einem anderen Ursprung braucht den Preflight des Browsers, den die CORS-Richtlinie der App ablehnt. - Über HTTP/1.1 öffnet ein Browser sechs Verbindungen pro Ursprung, und jeder Tab hält einen Stream. HTTP/2, Kestrels Vorgabe über TLS, trägt sie alle auf einer.
- Wenn die App stoppt, endet jeder Stream, und jede Seite verbindet sich erneut: zu dieser Instanz, sobald sie zurück ist, oder zu einer anderen in einem rollenden Deployment.
- Der Client ist Teil der Laufzeit, die die App schon ausliefert. Kein Skript kommt von irgendwo anders.
Eine Server Action, die streamt (IAsyncEnumerable<T>), Präsenz als eigene Funktion, die
Wiedergabe von der id des letzten Ereignisses an, ein WebSocket-Transport, ein Redis-Backplane-Paket,
und die Realisierung auf Photon. Siehe die Roadmap.
🏁 Hier starten
🏗️ Architektur
- Architekturüberblick
- Write-once-Komponenten
- Deklarative Oberfläche
- Paketarchitektur
- Komponenten
- Styling
- Lokalisierung
- Analytics und GTM
📱 Write-once
- Photon-Engine
- Design System
- Fähigkeiten
- Speicher
- Formulare
- Code-Editor
- Markdown
- Mermaid
- E-Mail-Rendering
⚙️ Kompilierung
⚡ Runtime
🔌 Server
🎨 Ökosystem
🚀 Entwicklung