Skip to content

Commit 7fc9eb1

Browse files
committed
1st draft of README
1 parent 34c1d4b commit 7fc9eb1

File tree

1 file changed

+374
-2
lines changed

1 file changed

+374
-2
lines changed

README.md

Lines changed: 374 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,374 @@
1-
# typescript-server-events
2-
Showcase of different TypeScript Apps utilizing Server Events
1+
# TypeScript Service Client and Server Events Apps
2+
3+
This project contains a number of self-contained TypeScript and JavaScript projects showcasing the different JavaScript runtime environments that can leverage
4+
[servicestack-client](https://github.com/ServiceStack/servicestack-client)
5+
to enable typed end-to-end API calls using the generic `JsonServiceClient` and the generated
6+
[TypeScript Add ServiceStack Reference](http://docs.servicestack.net/typescript-add-servicestack-reference) DTOs as well as easily handling real-time notifications using
7+
[TypeScript ServerEventsClient](http://docs.servicestack.net/typescript-server-events-client)
8+
with minimal effort.
9+
10+
The [servicestack-client](https://github.com/ServiceStack/servicestack-client) npm
11+
package contains an isomorphic library that can be used in either JavaScript or TypeScript Single Page Web Apps, node.js server projects as well as React Native Mobile Apps. It closely follows the design of the
12+
[C#/.NET JsonServiceClient](http://docs.servicestack.net/csharp-client) and C#
13+
[ServerEventsClient](http://docs.servicestack.net/csharp-server-events-client)
14+
in idiomatic TypeScript to maximize **knowledge sharing** and minimize native **porting efforts** between the different languages [Add ServiceStack Reference supports](http://docs.servicestack.net/add-servicestack-reference#supported-languages).
15+
16+
The examples in this project below explore the simplicity, type benefits and value provided by the
17+
`JsonServiceClient` and `ServerEventsClient` which enables 100% code sharing of client logic across
18+
JavaScript's most popular environments.
19+
20+
## Web App
21+
22+
The [Web Example App](https://github.com/ServiceStackApps/typescript-server-events/tree/master/web)
23+
was built with in [>100 lines of application code](https://github.com/ServiceStackApps/typescript-server-events/blob/master/web/src/app.ts)
24+
and uses no external runtime library dependencies other than
25+
[servicestack-client](https://github.com/ServiceStack/servicestack-client) for its functional Web App
26+
that can connect to any ServerEvents-enabled ServiceStack instance (with CORS) to keep a real-time log of all
27+
commands sent to the subscribed channel with a synchronized Live list of other Users that are also currently subscribed to the channel.
28+
29+
The Web App is spread across the 4 files below with all functionality maintained in **app.ts**:
30+
31+
- [app.ts](https://github.com/ServiceStackApps/typescript-server-events/blob/master/web/src/app.ts) - Entire App Logic
32+
- [dtos.ts](https://github.com/ServiceStackApps/typescript-server-events/blob/master/web/src/dtos.ts) - Server generated DTOs from [chat.servicestack.net/types/typescript](http://chat.servicestack.net/types/typescript)
33+
- [index.html](https://github.com/ServiceStackApps/typescript-server-events/blob/master/web/index.html) - Static HTML page
34+
- [default.css](https://github.com/ServiceStackApps/typescript-server-events/blob/master/web/default.css) - Static default.css styles
35+
36+
![](https://raw.githubusercontent.com/ServiceStack/Assets/master/img/livedemos/typescript-serverevents/web.png)
37+
38+
### Web Server Events Configuration
39+
40+
The heart of the App that's driving all its functionality is the Server Events subscription contained
41+
in these few lines below:
42+
43+
```ts
44+
const startListening = () => {
45+
BASEURL = $("#baseUrl").value;
46+
CHANNEL = $("#channel").value;
47+
if (client != null)
48+
client.stop();
49+
50+
console.log(`Connecting to ${BASEURL} on channel ${CHANNEL}`);
51+
client = new ServerEventsClient(BASEURL, [CHANNEL], {
52+
handlers: {
53+
onConnect: (e:ServerEventConnect) => {
54+
refresh(sub = e);
55+
},
56+
onJoin: refresh,
57+
onLeave: refresh,
58+
onUpdate: refresh,
59+
onMessage: (e:ServerEventMessage) => {
60+
addMessage(e);
61+
refreshMessages();
62+
}
63+
},
64+
onException: e => {
65+
addMessageHtml(`<div class="error">${e.message || e}</div>`);
66+
}
67+
}).start();
68+
}
69+
```
70+
71+
### Handler implementations
72+
73+
Essentially declarative configuration hooking up different Server Events to the `refresh` handlers
74+
below which adds the command message to the channels `MESSAGES` list, updates the UI then refreshes the
75+
`users` list by calling the built-in `client.getChannelSubscribers()`:
76+
77+
```ts
78+
const $ = sel => document.querySelector(sel);
79+
const $msgs = $("#messages > div") as HTMLDivElement;
80+
const $users = $("#users > div") as HTMLDivElement;
81+
82+
const refresh = (e:ServerEventMessage) => {
83+
addMessage(e);
84+
refreshMessages();
85+
refreshUsers();
86+
};
87+
88+
const refreshUsers = async () => {
89+
var users = await client.getChannelSubscribers();
90+
users.sort((x,y) => y.userId.localeCompare(x.userId));
91+
var usersMap = {};
92+
var userIds = Object.keys(usersMap);
93+
var html = users.map(x =>
94+
`<div class="${x.userId == sub.userId ? 'me' : ''}">
95+
<img src="${x.profileUrl}" /><b>@${x.displayName}</b><i>#${x.userId}</i><br/>
96+
</div>`);
97+
$users.innerHTML = html.join('');
98+
};
99+
100+
const addMessage = (x:ServerEventMessage) => addMessageHtml(
101+
`<div><b>${x.selector}</b>
102+
<span class="json" title=${x.json}>${x.json}</span>
103+
</div>`);
104+
const addMessageHtml = (html) => (MESSAGES[CHANNEL] || (MESSAGES[CHANNEL]=[])).push(html);
105+
const refreshMessages = () => $msgs.innerHTML= (MESSAGES[CHANNEL]||[]).reverse().join('');
106+
```
107+
108+
### Changing Server Subscription
109+
110+
To change the server and channel we want to connect to we just need to `startListening()` again
111+
when the **change** button is clicked:
112+
113+
```csharp
114+
$("#btnChange").onclick = startListening;
115+
```
116+
117+
Which will close the previous subscription and start a new one at the new server and channel.
118+
You can test connecting to another server by connecting to the .NET Core version of Chat at [chat.netcore.io](http://chat.netcore.io).
119+
120+
### Calling Typed Web Services
121+
122+
The Web App also sends messages
123+
124+
Download the TypeScript DTOs from the [chat.servicestack.net](http://chat.servicestack.net) at
125+
[/types/typescript](http://docs.servicestack.net/add-servicestack-reference#language-paths)
126+
127+
curl http://chat.servicestack.net/types/typescript > dtos.ts
128+
129+
Then once their downloaded we can reference the Request DTO's of the Services we want to call with:
130+
131+
```ts
132+
import { PostChatToChannel, PostRawToChannel } from "./dtos";
133+
```
134+
135+
Which just like all
136+
[ServiceStack Reference languages](http://docs.servicestack.net/add-servicestack-reference#supported-languages)
137+
we can populate and send with a generic `JsonServiceClient`, an instance of which is also pre-configured with the same `{baseUrl}` available at `client.serviceClient`, e.g:
138+
139+
```ts
140+
const sendChat = () => {
141+
let request = new PostChatToChannel();
142+
request.from = sub.id;
143+
request.channel = CHANNEL;
144+
request.selector = "cmd.chat";
145+
request.message = $("#txtChat").value;
146+
client.serviceClient.post(request);
147+
};
148+
```
149+
150+
All that's left is sending the chat message which we can do by pressing the **chat** button or hitting enter:
151+
152+
```ts
153+
$("#btnSendChat").onclick = sendChat;
154+
$("#txtChat").onkeydown = e => e.keyCode == 13 ? sendChat() : null;
155+
```
156+
157+
### Running Web App
158+
159+
To see it in action we just need to launch a static Web Server in the `/web` directory, e.g:
160+
161+
cd web
162+
http-server
163+
164+
Which will launch a HTTP Server at `http://localhost:8080/` which you can play with in your browser.
165+
166+
### Making changes to Web App
167+
168+
The vibrant ecosystem surrounding npm makes it the best place to develop Single Page Apps with world class
169+
tools like [Babel](https://babeljs.io/) which you can run in a command-line with:
170+
171+
npm run watch
172+
173+
That will launch a background watcher to monitor your source files for changes and **on save** automatically
174+
pipe them through the TypeScript compiler and bundle your app in `/dist/bundle.js` which
175+
is the only .js source file our app needs to reference and reload with **F5** to see any changes.
176+
177+
## node.js Server App
178+
179+
The [/node](https://github.com/ServiceStackApps/typescript-server-events/tree/master/node) server.js app
180+
has exactly the same functionality as the Web App except instead of using **servicestack-client** to connect
181+
to [chat.servicestack.net](http://chat.servicestack.net) Server Events stream on the client, all
182+
connections are made in node.js and only the server state is sent to the client to render its UI.
183+
184+
As the functionality of the app remains the same we're able to reuse the existing DTOs, .html and .css:
185+
186+
- [dtos.ts](https://github.com/ServiceStackApps/typescript-server-events/blob/master/node/src/dtos.ts)
187+
- [index.html](https://github.com/ServiceStackApps/typescript-server-events/blob/master/node/index.html)
188+
- [default.css](https://github.com/ServiceStackApps/typescript-server-events/blob/master/node/default.css)
189+
190+
![](https://raw.githubusercontent.com/ServiceStack/Assets/master/img/livedemos/typescript-serverevents/node.png)
191+
192+
The difference is in the App's logic which is now split into 2 with the node.js `server.ts` now containing
193+
most of the App's functionality whilst the `app.ts` relegated to periodically updating the UI with the
194+
node.js server state:
195+
196+
- [server.ts](https://github.com/ServiceStackApps/typescript-server-events/blob/master/node/server.ts) - maintain all client and server events connection to [chat.servicestack.net](http://chat.servicestack.net)
197+
- [app.ts](https://github.com/ServiceStackApps/typescript-server-events/blob/master/node/src/app.ts) - periodically render node.js state to HTML UI
198+
199+
As our goal is to maintain the minimal dependencies in each App, the implementation of `server.ts` is
200+
written against a bare-bones node `http.createServer()` without utilizing a server web framework which
201+
makes the implementation more verbose but also easier to understand as we're not relying on any hidden
202+
functionality enabled in a server web framework.
203+
204+
### Enable Server Events
205+
206+
A change we need to make given our App is now running in node.js instead of in a browser is to import the
207+
[eventsource](https://www.npmjs.com/package/eventsource) pure JavaScript polyfill to provide an `EventSource` implementation in node.js which we can make available in the ``global`` scope in TypeScript with:
208+
209+
```ts
210+
declare var global:any;
211+
global.EventSource = require('eventsource');
212+
```
213+
214+
### Node.js Server Events Configuration
215+
216+
Whilst the environment is different the Server Events configuration remains largely the same, but instead of
217+
retrieving the connection info from Text boxes in a Web Page, it's instead retrieved from the queryString
218+
passed when the client App calls our `/listen` handler, e.g:
219+
220+
```ts
221+
"/listen": (req,res) => {
222+
const qs = url.parse(req.url, true).query;
223+
if (client) {
224+
client.stop();
225+
client = null;
226+
}
227+
BASEURL = qs["baseUrl"];
228+
CHANNEL = qs["channel"];
229+
console.log(`Connecting to ${BASEURL} #${CHANNEL}...`);
230+
client = new ServerEventsClient(BASEURL, [CHANNEL], {
231+
handlers: {
232+
onConnect: (e:ServerEventConnect) => {
233+
refresh(sub = e);
234+
},
235+
onJoin: refresh,
236+
onLeave: refresh,
237+
onUpdate: refresh,
238+
onMessage: (e:ServerEventMessage) => {
239+
addMessage(e);
240+
}
241+
},
242+
onException: e => {
243+
addMessageHtml(`<div class="error">${e.message || e}</div>`);
244+
}
245+
}).start();
246+
res.end();
247+
},
248+
```
249+
250+
### Node.js Handler implementation
251+
252+
The handler implementations are more or less the same as the Web App albeit a bit simpler as it just needs
253+
to capture the Server Event messages and not concern itself with updating the UI:
254+
255+
```ts
256+
var MESSAGES = [];
257+
var USERS = [];
258+
259+
const refresh = (e:ServerEventMessage) => {
260+
addMessage(e);
261+
refreshUsers();
262+
};
263+
const refreshUsers = async () => {
264+
var users = await client.getChannelSubscribers();
265+
users.sort((x,y) => y.userId.localeCompare(x.userId));
266+
267+
var usersMap = {};
268+
var userIds = Object.keys(usersMap);
269+
USERS = users.map(x => ({
270+
profileUrl: x.profileUrl,
271+
displayName: x.displayName,
272+
userId: x.userId
273+
}));
274+
};
275+
const addMessage = (x:ServerEventMessage) =>
276+
addMessageHtml(`<div><b>${x.selector}</b> <span class="json" title=${x.json}>${x.json}</span></div>`);
277+
const addMessageHtml = (html:string) =>
278+
(MESSAGES[CHANNEL] || (MESSAGES[CHANNEL] = [])).push(html);
279+
```
280+
281+
### Syncing the UI with Server state in node.js
282+
283+
Syncing and rendering the UI is now the primary job of our clients `app.ts` which just polls the servers
284+
`/state` route every 100ms and injects it into the HTML UI:
285+
286+
```ts
287+
const syncState = () => {
288+
client.get<any>("/state").then(state => {
289+
var html = state.users.map(x =>
290+
`<div class="${x.userId == state.sub.userId ? 'me' : ''}">
291+
<img src="${x.profileUrl}" /><b>@${x.displayName}</b><i>#${x.userId}</i><br/>
292+
</div>`);
293+
$users.innerHTML = html.join('');
294+
$msgs.innerHTML = state.messages.reverse().join('');
295+
});
296+
};
297+
298+
setInterval(syncState, 100);
299+
```
300+
301+
The `/state` handler being just dumping the state and collections to JSON:
302+
303+
```ts
304+
"/state": (req, res) => {
305+
var state = {
306+
baseUrl: BASEURL,
307+
channel: CHANNEL,
308+
sub,
309+
messages: (MESSAGES[CHANNEL] || ['<div class="error">NOT CONNECTED</div>']),
310+
users: USERS
311+
};
312+
res.writeHead(200, { "Content-Type": "application/json" });
313+
res.end(JSON.stringify(state));
314+
},
315+
```
316+
317+
### Calling Typed Web Services in node.js
318+
319+
As we can expect making Typed API calls in node.js is the same as in a browser except the user data
320+
comes from a queryString instead of a HTML UI:
321+
322+
```ts
323+
"/chat": (req,res) => {
324+
const qs = url.parse(req.url, true).query;
325+
let request = new PostChatToChannel();
326+
request.from = sub.id;
327+
request.channel = CHANNEL;
328+
request.selector = "cmd.chat";
329+
request.message = qs["message"];
330+
client.serviceClient.post(request);
331+
res.end();
332+
},
333+
```
334+
335+
Back in client `app.ts` land our event handlers are exactly the same, the difference is in `sendChat()`
336+
where instead of making the API call itself it tells the node `server.ts` to do it by calling the `/chat`
337+
handler:
338+
339+
```ts
340+
const sendChat = () => client.get("/chat", { message: $("#txtChat").value });
341+
342+
$("#btnSendChat").onclick = sendChat;
343+
$("#txtChat").onkeydown = e => e.keyCode == 13 ? sendChat() : null;
344+
```
345+
346+
### Running node server.ts
347+
348+
To run our node app we need to launch the compiled `server.js` App with:
349+
350+
cd node
351+
node server.js
352+
353+
Which also launches our HTTP Server at `http://localhost:8080/`.
354+
355+
### Making changes to Web App
356+
357+
Since there's now a client and server component, we still need to run **Babel** to monitor our source files
358+
for changes and regenerate client `/dist/bundle.js`:
359+
360+
npm run watch
361+
362+
But if we've a change to `server.ts` we need to compile it by running:
363+
364+
tsc
365+
366+
Then we can re-run our server to see our changes:
367+
368+
node server.js
369+
370+
## React Native App
371+
372+
![](https://raw.githubusercontent.com/ServiceStack/Assets/master/img/livedemos/typescript-serverevents/react-native.png)
373+
374+

0 commit comments

Comments
 (0)