The TypeScript ServerEventClient
is an idiomatic port of ServiceStack's
C# Server Events Client in native TypeScript providing a productive
client to consume ServiceStack's real-time Server Events that can be used in both
TypeScript Web and node.js server applications.
Install​
ServerEventClient
is available in the
@servicestack/client npm package
which is preconfigured in all ServiceStackVS TypeScript VS.NET Templates
npm based projects can install it with:
$ npm install @servicestack/client
To configure Server Sent Events on the client create a new instance of ServerEventsClient
with the
baseUrl and the channels you want to connect to, e.g:
const channels = ["home"];
const client = new ServerEventsClient("/", channels, {
handlers: {
onConnect: (sub:ServerEventConnect) => { // Successful SSE connection
console.log("You've connected! welcome " + sub.displayName);
},
onJoin: (msg:ServerEventJoin) => { // User has joined subscribed channel
console.log("Welcome, " + msg.displayName);
},
onLeave: (msg:ServerEventLeave) => { // User has left subscribed channel
console.log(msg.displayName + " has left the building");
},
onUpdate: (msg:ServerEventUpdate) => { // User channel subscription was changed
console.log(msg.displayName + " channels subscription were updated");
},
onMessage: (msg:ServerEventMessage) => {} // Invoked for each other message
//... Register custom handlers
announce: (text:string) => {} // Handle messages with simple argument
chat: (chatMsg:ChatMessage) => {} // Handle messages with complex type argument
CustomMessage: (msg:CustomMessage) => {} // Handle complex types with default selector
},
receivers: {
//... Register any receivers
tv: {
watch: function (id) { // Handle 'tv.watch {url}' messages
var el = document.querySelector("#tv");
if (id.indexOf('youtu.be') >= 0) {
var v = splitOnLast(id, '/')[1];
el.innerHTML = templates.youtube.replace("{id}", v);
} else {
el.innerHTML = templates.generic.replace("{id}", id);
}
el.style.display = 'block';
},
off: function () { // Handle 'tv.off' messages
var el = document.querySelector("#tv");
el.style.display = 'none';
el.innerHTML = '';
}
}
},
onException: (e:Error) => {}, // Invoked on each Error
onReconnect: (e:Error) => {} // Invoked after each auto-reconnect
})
.addListener("theEvent",(e:ServerEventMessage) => {}) // Add listener for pub/sub event trigger
.start(); // Start listening for Server Events!
If hosted from same ServiceStack Instance, the relative
/
url can be used instead of the absolutebaseUrl
Message Events​
ServiceStack Server Events has 4 built-in events sent during a subscriptions life-cycle:
- onConnect - sent when successfully connected, includes the subscriptions private
subscriptionId
as well as heartbeat and unregister urls that's used to automatically setup periodic heartbeats - onJoin - sent when a new user joins the channel
- onLeave - sent when a user leaves the channel
- onUpdate - sent when a user's channels subscription was updated
The onJoin/onLeave/onUpdate events can be turned off with
ServerEventsFeature.NotifyChannelOfSubscriptions=false
.
All other messages can be handled with the catch-all:
- onMessage - fired when any other message is sent
Server Event Client Events​
Other top-level events the ServerEventClient
fires that can be handled include:
- onException - Invoked on each error the client receives
- onReconnect - Invoked after each time the client had to auto-reconnect
- onTick - Invoked after any event or state change
Selectors​
A selector is a string that identifies what should handle the message, it's used by the client to route the message to different handlers. The client bindings supports 4 different handlers out of the box:
Global Event Handlers​
The easiest way to handle a custom event is to define a handler, e.g:
const client = new ServerEventsClient("/", channels, {
handlers: {
paint: (color:string) => {
this.style.background = color;
},
chat: (chatMsg:ChatMessage, e:ServerEventMessage) => {
}
}
}).start();
The selector to invoke a global event handler is:
cmd.{handler} {message}
Which can be sent in ServiceStack with:
ServerEvents.NotifyChannel(channel, "cmd.paint", "green");
ServerEvents.NotifyChannel(channel, "cmd.chat", new ChatMessage { ... });
Where {handler}
is the name of the handler you want to invoke, e.g cmd.paint
. When invoked from a server event the message (deserialized from JSON) is the first argument, the Server Sent DOM Event is the 2nd argument and this
by default is assigned to document.body
.
function paint(msg /* JSON object msg */, e /*ServerEventMessage*/){
this // HTML Element or document.body
this.style.background = "green";
}
Handling Messages with the Default Selector​
All IServerEvents
Notify API's includes overloads for sending messages without a selector that by convention will take the format cmd.{TypeName}
.
As they're prefixed with cmd.*
these events can be handled with a handler based on Message type name, e.g:
const client = new ServerEventsClient("/", channels, {
handlers: {
CustomType: (msg:CustomType) => { ... },
SetterType: (msg:SetterType, e:ServerEventMessage) => { ... }
}
}).start();
Which will be called when messages are sent without a selector, e.g:
public class MyServices : Service
{
public IServerEvents ServerEvents { get; set; }
public void Any(Request request)
{
ServerEvents.NotifyChannel("home", new CustomType { ... });
ServerEvents.NotifyChannel("home", new SetterType { ... });
}
}
Postfix CSS selector​
All server event handler options also support a postfix CSS selector for specifying what each handler should be bound to with a $
followed by the CSS selector, e.g:
cmd.{handler}${cssSelector}
A concrete example for calling the above API would be:
cmd.paintGreen$#btnPaint
Which will bind this
to the #btnSubmit
HTML Element.
INFO
Spaces in CSS selectors need to be encoded with %20
Modifying CSS​
As it's a popular use-case Server Events also has native support for modifying CSS properties with:
css.{propertyName}${cssSelector} {propertyValue}
Where the message is the property value, which roughly translates to:
document.querySelectorAll({cssSelector}).style[{propertyName}] = {propertyValue}
When no CSS selector is specified it falls back to document.body
by default.
css.background #eceff1
Some other examples include:
css.background$#top #673ab7 // $('#top').css('background','#673ab7')
css.font$li bold 12px verdana // $('li').css('font','bold 12px verdana')
css.visibility$a,img hidden // $('a,img').css('visibility','#673ab7')
css.visibility$a%20img hidden // $('a img').css('visibility','hidden')
Receivers​
In programming languages based on message-passing like Smalltalk and Objective-C invoking a method is done by sending a message to a receiver. This is conceptually equivalent to invoking a method on an instance in C# where both these statements are roughly equivalent:
// Objective-C
[receiver method:argument]
// C#
receiver.method(argument)
Support for receivers is available in the following format:
{receiver}.{target} {msg}
Registering Receivers​
Registering a receiver can be done with a map of the object instance and the name you want it to be exported as. E.g. The window
and document
global objects can be setup to receive messages with:
const client = new ServerEventsClient("/", channels, {
receivers: {
"window": window,
"document": document
}
}).start();
Alternatively you can add receivers at runtime with:
const client = new ServerEventsClient("/", channels)
.registerNamedReceiver("window", window);
.registerNamedReceiver("document", document)
.start();
Once registered you can set any property or call any method on a receiver with:
document.title New Window Title
window.location http://google.com
Where if {target}
was a function it will be invoked with the message, otherwise its property will be set.
By default when no {cssSelector}
is defined, this
is bound to the receiver instance.
Example of setting up a custom receiver:
const client = new ServerEventsClient("/", channels, {
receivers: {
tv: {
watch: function (id) {
var el = document.querySelector("#tv");
if (id.indexOf('youtu.be') >= 0) {
var v = splitOnLast(id, '/')[1];
el.innerHTML = templates.youtube.replace("{id}", v);
} else {
el.innerHTML = templates.generic.replace("{id}", id);
}
el.style.display = 'block';
},
off: function () {
var el = document.querySelector("#tv");
el.style.display = 'none';
el.innerHTML = '';
}
}
}
}).start();
This registers a custom tv
receiver that can now be called with:
tv.watch http://youtu.be/518XP8prwZo
tv.watch https://servicestack.net/img/logo-220.png
tv.off
Receiver Constructors​
In addition to registering an object instance as a receiver, you can also specify a class
(aka constructor Function) instead, e.g:
class CssReceiver {
backgroundImage(url:string) {
document.body.style.backgroundImage = url;
}
}
const client = new ServerEventsClient("/", channels, {
receivers: {
css: CssReceiver
}
}).start();
Which will invoke backgroundImage
method off a new instance of the CssReceiver
class that's triggered with:
css.background-image url(https://bit.ly/1yIJOBH)
and can be sent to all subscriptions on the home channel in ServiceStack with:
ServerEvents.NotifyChannel("home", "css.background-image", "url(https://bit.ly/1yIJOBH)");
This works the same with Global Receivers, in addition Receivers can extend ServerEventReceiver
:
class ServerEventReceiver implements IReceiver {
public client: ServerEventsClient;
public request: ServerEventMessage;
noSuchMethod(selector: string, message:any) {}
}
To access additional built-in functionality where it will allow receivers to access the ServerEventsClient
client dependency, the ServerEventMessage
that was received, it also lets you handle any unhandled messages
sent by implementing noSuchMethod()
, e.g:
class JavaScriptReceiver extends ServerEventReceiver {
chatMessage(msg: ChatMessage) {
let request = new LogMessage();
request.channel = msg.channel;
request.message = msg.message;
this.client.serviceClient.post(request)
.then(r => {
console.log(`logged ${msg.message} sent by ${msg.fromName} to ${msg.channel}`);
});
}
announce(message:string) {
alert(message);
}
toggle() {
let el = document.querySelector(this.request.cssSelector);
el.style.display = el.style.display != "none" ? "none" : "block";
}
noSuchMethod(selector:string, message:ServerEventMessage) {
console.log(`Unhandled ${selector} was sent message: `, message);
}
}
client.registerReceiver(JavaScriptReceiver); //register Global Receiver
These can triggered with:
ServerEvents.NotifyChannel(channel, new ChatMessage { ... });
ServerEvents.NotifyChannel(channel, "cmd.announce", "Hello, World!");
ServerEvents.NotifyChannel(channel, "cmd.toggle$#sidebar");
ServerEvents.NotifyChannel(channel, "cmd.UnknownSelector", new Message { ... });
Dependency Resolvers​
You can control the lifetime of the receivers by injecting a custom resolver which will let you reuse the same
Receiver instance by using a SingletonInstanceResolver
, e.g:
const client = new ServerEventsClient("/", channels)
.setResolver(new SingletonInstanceResolver())
.registerReceiver(JavaScriptReceiver)
.start();
Which is implemented with:
export class SingletonInstanceResolver implements IResolver {
tryResolve(ctor:ObjectConstructor): any {
return (ctor as any).instance
|| ((ctor as any).instance = new ctor());
}
}
Un Registering a Receiver​
As receivers are maintained in a simple map, they can be disabled at anytime with:
client.unregisterReceiver("window");
and re-enabled with:
client.registerNamedReceiver("window", window);
Whilst Named Receivers are used to handle messages sent to a specific namespaced selector, the client also supports registering a Global Receiver for handling messages sent with the special cmd.*
selector.
Event Triggers​
Triggers enable a pub/sub event model where multiple listeners can subscribe and be notified of an event.
Registering an event handler can be done at anytime using the addListener()
API, e.g:
const handler = (e:ServerEventMessage) => {
console.log(`received event ${e.target} with arg: ${JSON.parse(e.json)}`);
}
const client = new ServerEventsClient("/", channels)
.addListener("customEvent", handler)
.start();
//Register another listener to 'customEvent' event
client.addListener("customEvent", e => { ... });
The selector to trigger this custom event is:
trigger.customEvent arg
trigger.customEvent {json}
Which can be sent in ServiceStack with a simple or complex type argument, e.g:
ServerEvents.NotifyChannel(channel, "trigger.customEvent", "arg");
ServerEvents.NotifyChannel(channel, "trigger.customEvent", new ChatMessage { ... });
Removing Listeners​
Use removeListener()
to stop listening for an event, e.g:
//Remove first event listener
client.removeListener("customEvent", handler);
Channel Subscriber APIs​
You can use any of the APIs below to update an active Subscriptions Channels:
client.subscribeToChannels("chan3","chan4");
client.unsubscribeFromChannels("chan1","chan2");
//Alternatively subscribe/unsubscribe to channels in the same request with:
var request = new UpdateEventSubscriber();
request.subscribeChannels = ["chan3","chan4"];
request.unsubscribeChannels = ["chan1","chan2"];
client.updateSubscriber(request);
Get Channel Subscribers​
Once connected, you can get a list of channel subscribers the ServerEventsClient
is currently connected
to with:
client.getChannelSubscribers()
.then(users => users.forEach(x =>
console.log(`#${x.userId} @${x.displayName} ${x.profileUrl} ${x.channels}`)));
Accessing ServiceClient​
Alternatively you can access the channel subscribers using the built-in JsonServiceClient
, e.g:
let request = new GetEventSubscribers();
request.channels = ["chan1","chan2"];
client.serviceClient.get(request)
.then(r => ...);
Which you can also use to call your own Services:
client.serviceClient.post(new MyRequest())
.then(r => ...)
Authentication via JWT​
As the TypeScript ServerEventsClient
needs to use the browsers native EventSource class to establish the SSE connection it's not able to customize
the HTTP Request Headers in other clients but as the client shares the same cookies with the browser you can use a JWT Token Cookie either
by Requesting to use a JWT Token Cookie at Authentication or by setting the Token
Cookie on the client, CORS permitting, e.g:
document.cookie = "ss-tok={Token}";
Alternatively you can enable authenticating via JWT on the QueryString:
Send JWTs in HTTP Params​
The JWT Auth Provider can opt-in to accept JWT's via the Query String or HTML POST FormData with:
new JwtAuthProvider {
AllowInQueryString = true,
AllowInFormData = true
}
This is useful for situations where it's not possible to attach the JWT in the HTTP Request Headers or ss-tok
Cookie.
For example if you wanted to authenticate via JWT to a real-time Server Events stream from a token retrieved from a remote auth server (i.e. so the JWT Cookie isn't already configured with the SSE server) you can call the /session-to-token API to convert the JWT Bearer Token into a JWT Cookie which will configure it with that domain so the subsequent HTTP Requests to the SSE event stream contains the JWT cookie and establishes an authenticated session:
var client = new JsonServiceClient(BaseUrl);
client.setBearerToken(JWT);
await client.post(new ConvertSessionToToken());
var sseClient = new ServerEventsClient(BaseUrl, ["*"], {
handlers: {
onConnect: e => {
console.log(e.isAuthenticated /*true*/, e.userId, e.displayName);
}
}
}).start();
Unfortunately this wont work in node.exe
Server Apps (or in integration tests) which doesn't support a central location for configuring domain cookies. One solution that works everywhere is to add the JWT to the ?ss-tok
query string that's used to connect to the /event-stream
URL, e.g:
var sseClient = new ServerEventsClient(BaseUrl, ["*"], {
resolveStreamUrl: url => appendQueryString(url, { "ss-tok": JWT }),
handlers: {
onConnect: e => {
console.log(e.isAuthenticated /*true*/, e.userId, e.displayName);
}
}
}).start();
Integration Test Examples​
More examples of the ServerEventClient
can be found in the TypeScript ServerEventClient Test Suite.
TypeScript ServerEvents Examples​
Web, Node.js and React Native ServerEvents Apps​
Using TypeScript ServerEvents Client to create real-time Web, node.js server and React Native Mobile Apps:
Gistlyn​
Gistlyn is a C# Gist IDE for creating, running and sharing stand-alone, executable C# snippets.
Live Demo: http://gistlyn.com
React Chat​
React Chat is a port of ServiceStack Chat ES5, jQuery Server Events demo into a TypeScript, React and Redux App:
Networked Time Traveller Shape Creator​
A network-enhanced version of the stand-alone Time Traveller Shape Creator that allows users to connect to and watch other users using the App in real-time similar to how users can use Remote Desktop to watch another computer's screen:
Live demo: http://redux.servicestack.net
Chat​
Feature-rich Single Page Chat App, showcasing Server Events support in 170 lines of JavaScript!
React Chat Desktop​
Built with React Desktop Apps VS.NET template and packaged into a native Desktop App for Windows and OSX - showcasing synchronized real-time control of multiple Windows Apps:
Downloads for Windows, OSX, Linux and Web