1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
|
<!doctype html><html lang=en-us><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link href="data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAL69vf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAv76+/8LBwQkAAAAAAAAAAAAAAAC+vb3/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAL+9vf/Bv78JAAAAAAAAAAAAAAAAu7q6/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC7ubr/vr29CAAAAAAAAAAAy8nJAZ6foP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAnqGj/6GipAoAAAAAHLjU/xcXHf/BwsL/I8XY/yPK3v8XGiD/IbjL/yPF2f8XGiD/Fxkf/yLF2f8gnK3/Fxog/62ztv8fwNf/FRcd/x271v8mz93/GRsi/xkXHf8p097/GiIp/xobIv8p0t3/KdPe/xocIv8fYmr/KNPe/xoZH/8aHCL/J87c/xy81/8VFxz/IsPZ/8zS0/8XGiD/Ir/R/yPH2/8XGiD/Fxkf/yPH2/8dd4T/GBog/yPJ3f8jyNr/uru9/xcUGv8cudb/EhITDKi5vRKlvMP/RUpOERwcHRAdOj4QHTk8EBwdHRAdNTgQHTo/EBwcHRAcHB0QSGduEKW4vf+koqQfHzg+EBqz0ewSFRv7EyMr/xq51vsTERb7ExUb+xq41fsau9j7ExUb+xiPp/sZudb7ExUb+xMVG/sZuNX/GKvI/BIUGfMdvdn/IrfL/xcaIP8n1eb/J9Dh/xkcIf8ZGR7/J8/f/xxCSv8ZGyH/J9Dg/ybQ4P8ZHCL/FSQs/yPK3/8UExj/GE1b/ybS5P8ZGB7/Ghwj/ynW5P8p2Ob/Ghwi/yWrtv8p1eH/Ghwi/xocIv8p1uT/J8XT/xkcIv8m1un/Hb7d/xUYH/8hzOr/HtHu/xcaIf8XGB//I8vi/xgxOv8XGSD/I8rg/yPK4P8XGiD/GUFL/yPP6f8SERj/Fhkh/x3A4f8AAAAAJ2f9/ydr//8mZPH/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlYu38J2v//ydo/f8AAAAAAAAAAAd8/fkFqf//Iob8sAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMY39awWr//8FfP3/AAAAAAAAAAAFm/7/SfD//wR+/f8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOB/f9B7v//BaX+/wAAAAAAAAAAQ878SAyZ/v9n1v4KAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADu9v8DDJb+/z3N/XgAAAAA3/sAAN/7AADf+wAA3/sAAAAAAAAAAAAAAAAAAN/7AAAAAAAAAAAAAAAAAAAAAAAAj/EAAI/5AACP8QAA3/sAAA==" rel=icon type=image/x-icon><title>Simple Server-Sent Events based PubSub Server</title><meta name=description content="Before we continue ."><link rel=alternate type=application/rss+xml title="Mitja Felicijan's posts" href=https://mitjafelicijan.com/index.xml><link rel=alternate type=application/rss+xml title="Mitja Felicijan's notes" href=https://mitjafelicijan.com/notes.xml><style>body{padding:1rem;max-width:760px;background:#fff;font-family:sans-serif;line-height:1.35rem;font-size:16px;margin:0 auto}hr{margin-block-start:1.5rem}h1,h2,h3{line-height:initial}h1{font-size:xx-large}footer{margin-block-start:2rem}cap{text-transform:capitalize}table{max-width:100%;width:100%;border-collapse:separate;border-spacing:2px;border:1px solid #000;border-left:1px solid #999;border-top:1px solid #999}blockquote{font-style:italic}table thead{background:#eee}ul.list li{padding:.2em 0}ul{line-height:1.4em}td,th{border:1px solid #000;padding:4px;border-right:1px solid #999;border-bottom:1px solid #999;text-align:left}pre{text-wrap:nowrap;overflow-x:auto;padding:0 1em;border:1px solid #dcdcdc}code{padding:0 3px;font-size:14px;border:0}pre code{line-height:1.3em}pre,code,pre *,code *{font-family:monospace}figure{margin-inline-start:0;margin-inline-end:0}figcaption{text-align:center}figcaption p{margin:.3em 0 0}img,video,audio{max-width:100%}header{display:flex;flex-direction:row;gap:3rem}nav{display:flex;gap:.75rem}nav.main{flex-grow:1}.pstatus-orange{background:gold}.pstatus-green{background:#9acd32}.pstatus-red{background:#cd5c5c}@media only screen and (max-width:600px){body{padding:15px}header{flex-direction:column;gap:1rem}a{word-wrap:break-word}}</style><header><nav class=main itemscope itemtype=http://schema.org/SiteNavigationElement role=toolbar><a href=/>Home</a>
<a href=https://files.mitjafelicijan.com/ target=_blank>Files</a>
<a href=/mitjafelicijan.pgp.pub.txt target=_blank>PGP</a>
<a href=/curriculum-vitae.html>CV</a>
<a href=/index.xml target=_blank>RSS</a></nav></header><main role=main><article itemtype=http://schema.org/Article><h1 itemtype=headline>Simple Server-Sent Events based PubSub Server</h1><p><cap>post</cap>, Mar 22, 2020 on <a href=https://mitjafelicijan.com>Mitja Felicijan's blog</a><div><h2 id=before-we-continue->Before we continue ...</h2><p>Publisher Subscriber model is nothing new and there are many amazing solutions
out there, so writing a new one would be a waste of time if other solutions
wouldn't have quite complex install procedures and weren't so hard to maintain.
But to be fair, comparing this simple server with something like
<a href=https://kafka.apache.org/>Kafka</a> or <a href=https://www.rabbitmq.com/>RabbitMQ</a> is
laughable at the least. Those solutions are enterprise grade and have many
mechanisms there to ensure messages aren't lost and much more. Regardless of
these drawbacks, this method has been tested on a large website and worked until
now without any problems. So now, that we got that cleared up, let's continue.<p><em><strong>Wiki definition:</strong> Publish/subscribe messaging, or pub/sub messaging, is a
form of asynchronous service-to-service communication used in serverless and
microservices architectures. In a pub/sub model, any message published to a
topic is immediately received by all the subscribers to the topic.</em><h2 id=general-goals>General goals</h2><ul><li>provide a simple server that relays messages to all the connected clients,<li>messages can be posted on specific topics,<li>messages get sent via <a href=https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events>Server-Sent
Events</a>
to all the subscribers.</ul><h2 id=how-exactly-does-the-pubsub-model-work>How exactly does the pub/sub model work?</h2><p>The easiest way to explain this is with diagram bellow. Basic function is
simple. We have subscribers that receive messages, and we have publishers that
create and post messages. Similar model is also well know pattern that works on
a premise of consumers and producers, and they take similar roles.<figure><img src=/posts/simple-pubsub-server/pubsub-overview.png alt="How PubSub works"></figure><p><strong>These are some naive characteristics we want to achieve:</strong><ul><li>producer is publishing messages to subscribe topic,<li>consumer is receiving messages from subscribed topic,<li>servers is also known as Broker,<li>broker does not store messages or tracks success,<li>broker uses
<a href=https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics)>FIFO</a> method
for delivering messages,<li>if consumer wants to receive messages from a topic, producer and consumer
topics must match,<li>consumer can subscribe to multiple topics,<li>producer can publish to multiple topics,<li>each message has a messageId.</ul><p><strong>Known drawbacks:</strong><ul><li>messages will not be stored in a persistent queue or unreceived messages like
<a href=https://en.wikipedia.org/wiki/Dead_letter_queue>DeadLetterQueue</a> so old
messages could be lost on server restart,<li><a href=https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events>Server-Sent
Events</a>
opens a long-running connection between the client and the server so make sure
if your setup is load balanced that the load balancer in this case can have
long opened connection,<li>no system moderation due to the dynamic nature of creating queues.</ul><h2 id=server-sent-events>Server-Sent Events</h2><p>Read more about it on <a href=https://html.spec.whatwg.org/multipage/server-sent-events.html>official specification
page</a>.<h3 id=current-browser-support>Current browser support</h3><figure><img src=/posts/simple-pubsub-server/caniuse.png alt="Browser support"></figure><p>Check
<a href="https://caniuse.com/#feat=eventsource">https://caniuse.com/#feat=eventsource</a>
for latest information about browser support.<h3 id=known-issues>Known issues</h3><ul><li>Firefox 52 and below do not support EventSource in web/shared workers<li>In Firefox prior to version 36 server-sent events do not reconnect
automatically in case of a connection interrupt (bug)<li>Reportedly, CORS in EventSource is currently supported in Firefox 10+, Opera
12+, Chrome 26+, Safari 7.0+.<li>Antivirus software may block the event streaming data chunks.</ul><p>Source: <a href="https://caniuse.com/#feat=eventsource">https://caniuse.com/#feat=eventsource</a><h3 id=message-format>Message format</h3><p>The simplest message that can be sent is only with data attribute:<pre tabindex=0 style=background-color:#fff><code><span style=display:flex><span>data: this is a simple message
</span></span><span style=display:flex><span><blank line>
</span></span></code></pre><p>You can send message IDs to be used if the connection is dropped:<pre tabindex=0 style=background-color:#fff><code><span style=display:flex><span>id: 33
</span></span><span style=display:flex><span>data: this is line one
</span></span><span style=display:flex><span>data: this is line two
</span></span><span style=display:flex><span><blank line>
</span></span></code></pre><p>And you can specify your own event types (the above messages will all trigger
the message event):<pre tabindex=0 style=background-color:#fff><code><span style=display:flex><span>id: 36
</span></span><span style=display:flex><span>event: price
</span></span><span style=display:flex><span>data: 103.34
</span></span><span style=display:flex><span><blank line>
</span></span></code></pre><h3 id=server-requirements>Server requirements</h3><p>The important thing is how you send headers and which headers are sent by the
server that triggers browser to threat response as a EventStream.<p>Headers responsible for this are:<pre tabindex=0 style=background-color:#fff><code><span style=display:flex><span>Content-Type: text/event-stream
</span></span><span style=display:flex><span>Cache-Control: no-cache
</span></span><span style=display:flex><span>Connection: keep-alive
</span></span></code></pre><h3 id=debugging-with-google-chrome>Debugging with Google Chrome</h3><p>Google Chrome provides build-in debugging and exploration tool for <a href=https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events>Server-Sent
Events</a>
which is quite nice and available from Developer Tools under Network tab.<blockquote><p>You can debug only client side events that get received and not the server
ones. For debugging server events add <code>console.log</code> to <code>server.js</code> code and
print out events.</blockquote><figure><img src=/posts/simple-pubsub-server/chrome-debugging.png alt="Google Chrome Developer Tools EventStream"></figure><h2 id=server-implementation>Server implementation</h2><p>For the sake of this example we will use <a href=https://nodejs.org/en/>Node.js</a> with
<a href=https://expressjs.com>Express</a> as our router since this is the easiest way to
get started and we will use already written SSE library for node
<a href=https://www.npmjs.com/package/sse-pubsub>sse-pubsub</a> so we don't reinvent the
wheel.<pre tabindex=0 style=background-color:#fff><code><span style=display:flex><span>npm init --yes
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span>npm install express
</span></span><span style=display:flex><span>npm install body-parser
</span></span><span style=display:flex><span>npm install sse-pubsub
</span></span></code></pre><p>Basic implementation of a server (<code>server.js</code>):<pre tabindex=0 style=background-color:#fff><code><span style=display:flex><span><span style=color:#00f>const</span> express = require(<span style=color:#a31515>'express'</span>);
</span></span><span style=display:flex><span><span style=color:#00f>const</span> bodyParser = require(<span style=color:#a31515>'body-parser'</span>);
</span></span><span style=display:flex><span><span style=color:#00f>const</span> SSETopic = require(<span style=color:#a31515>'sse-pubsub'</span>);
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span><span style=color:#00f>const</span> app = express();
</span></span><span style=display:flex><span><span style=color:#00f>const</span> port = process.env.PORT || 4000;
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span><span style=color:green>// topics container
</span></span></span><span style=display:flex><span><span style=color:green></span><span style=color:#00f>const</span> sseTopics = {};
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span>app.use(bodyParser.json());
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span><span style=color:green>// open for all cors
</span></span></span><span style=display:flex><span><span style=color:green></span>app.all(<span style=color:#a31515>'*'</span>, (req, res, next) => {
</span></span><span style=display:flex><span> res.header(<span style=color:#a31515>'Access-Control-Allow-Origin'</span>, <span style=color:#a31515>'*'</span>);
</span></span><span style=display:flex><span> res.header(<span style=color:#a31515>'Access-Control-Allow-Headers'</span>, <span style=color:#a31515>'X-Requested-With, Content-Type'</span>);
</span></span><span style=display:flex><span> next();
</span></span><span style=display:flex><span>});
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span><span style=color:green>// preflight request error fix
</span></span></span><span style=display:flex><span><span style=color:green></span>app.options(<span style=color:#a31515>'*'</span>, <span style=color:#00f>async</span> (req, res) => {
</span></span><span style=display:flex><span> res.header(<span style=color:#a31515>'Access-Control-Allow-Origin'</span>, <span style=color:#a31515>'*'</span>);
</span></span><span style=display:flex><span> res.header(<span style=color:#a31515>'Access-Control-Allow-Headers'</span>, <span style=color:#a31515>'X-Requested-With, Content-Type'</span>);
</span></span><span style=display:flex><span> res.send(<span style=color:#a31515>'OK'</span>);
</span></span><span style=display:flex><span>});
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span><span style=color:green>// serve the event streams
</span></span></span><span style=display:flex><span><span style=color:green></span>app.get(<span style=color:#a31515>'/stream/:topic'</span>, <span style=color:#00f>async</span> (req, res, next) => {
</span></span><span style=display:flex><span> <span style=color:#00f>const</span> topic = req.params.topic;
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> <span style=color:#00f>if</span> (!(topic <span style=color:#00f>in</span> sseTopics)) {
</span></span><span style=display:flex><span> sseTopics[topic] = <span style=color:#00f>new</span> SSETopic({
</span></span><span style=display:flex><span> pingInterval: 0,
</span></span><span style=display:flex><span> maxStreamDuration: 15000,
</span></span><span style=display:flex><span> });
</span></span><span style=display:flex><span> }
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> <span style=color:green>// subscribing client to topic
</span></span></span><span style=display:flex><span><span style=color:green></span> sseTopics[topic].subscribe(req, res);
</span></span><span style=display:flex><span>});
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span><span style=color:green>// accepts new messages into topic
</span></span></span><span style=display:flex><span><span style=color:green></span>app.post(<span style=color:#a31515>'/publish'</span>, <span style=color:#00f>async</span> (req, res) => {
</span></span><span style=display:flex><span> <span style=color:#00f>let</span> body = req.body;
</span></span><span style=display:flex><span> <span style=color:#00f>let</span> status = 200;
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> console.log(<span style=color:#a31515>'Incoming message:'</span>, req.body);
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> <span style=color:#00f>if</span> (
</span></span><span style=display:flex><span> body.hasOwnProperty(<span style=color:#a31515>'topic'</span>) &&
</span></span><span style=display:flex><span> body.hasOwnProperty(<span style=color:#a31515>'event'</span>) &&
</span></span><span style=display:flex><span> body.hasOwnProperty(<span style=color:#a31515>'message'</span>)
</span></span><span style=display:flex><span> ) {
</span></span><span style=display:flex><span> <span style=color:#00f>const</span> topic = req.body.topic;
</span></span><span style=display:flex><span> <span style=color:#00f>const</span> event = req.body.event;
</span></span><span style=display:flex><span> <span style=color:#00f>const</span> message = req.body.message;
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> <span style=color:#00f>if</span> (topic <span style=color:#00f>in</span> sseTopics) {
</span></span><span style=display:flex><span> <span style=color:green>// sends message to all the subscribers
</span></span></span><span style=display:flex><span><span style=color:green></span> sseTopics[topic].publish(message, event);
</span></span><span style=display:flex><span> }
</span></span><span style=display:flex><span> } <span style=color:#00f>else</span> {
</span></span><span style=display:flex><span> status = 400;
</span></span><span style=display:flex><span> }
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> res.status(status).send({
</span></span><span style=display:flex><span> status,
</span></span><span style=display:flex><span> });
</span></span><span style=display:flex><span>});
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span><span style=color:green>// returns JSON object of all opened topics
</span></span></span><span style=display:flex><span><span style=color:green></span>app.get(<span style=color:#a31515>'/status'</span>, <span style=color:#00f>async</span> (req, res) => {
</span></span><span style=display:flex><span> res.send(sseTopics);
</span></span><span style=display:flex><span>});
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span><span style=color:green>// health-check endpoint
</span></span></span><span style=display:flex><span><span style=color:green></span>app.get(<span style=color:#a31515>'/'</span>, <span style=color:#00f>async</span> (req, res) => {
</span></span><span style=display:flex><span> res.send(<span style=color:#a31515>'OK'</span>);
</span></span><span style=display:flex><span>});
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span><span style=color:green>// return a 404 if no routes match
</span></span></span><span style=display:flex><span><span style=color:green></span>app.use((req, res, next) => {
</span></span><span style=display:flex><span> res.set(<span style=color:#a31515>'Cache-Control'</span>, <span style=color:#a31515>'private, no-store'</span>);
</span></span><span style=display:flex><span> res.status(404).end(<span style=color:#a31515>'Not found'</span>);
</span></span><span style=display:flex><span>});
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span><span style=color:green>// starts the server
</span></span></span><span style=display:flex><span><span style=color:green></span>app.listen(port, () => {
</span></span><span style=display:flex><span> console.log(<span style=color:#a31515>`PubSub server running on http://localhost:</span><span style=color:#a31515>${</span>port<span style=color:#a31515>}</span><span style=color:#a31515>`</span>);
</span></span><span style=display:flex><span>});
</span></span></code></pre><h3 id=our-custom-message-format>Our custom message format</h3><p>Each message posted on a server must be in a specific format that out server
accepts. Having structure like this allows us to have multiple separated type of
events on each topic.<p>With this we can separate streams and only receive events that belong to the
topic.<p>One example would be, that we have index page and we want to receive messages
about new upvotes or new subscribers but we don't want to follow events for
other pages. This reduces clutter and overall network. And structure is much
nicer and maintanable.<pre tabindex=0 style=background-color:#fff><code><span style=display:flex><span>{
</span></span><span style=display:flex><span> "topic": <span style=color:#a31515>"sample-topic"</span>,
</span></span><span style=display:flex><span> "event": <span style=color:#a31515>"sample-event"</span>,
</span></span><span style=display:flex><span> "message": { "name": <span style=color:#a31515>"John"</span> }
</span></span><span style=display:flex><span>}
</span></span></code></pre><h2 id=publisher-and-subscriber-clients>Publisher and subscriber clients</h2><h3 id=publisher-and-subscriber-in-action>Publisher and subscriber in action</h3><p><video src=/posts/simple-pubsub-server/clients.m4v controls></video><p>You can download <a href=../simple-pubsub-server/sse-pubsub-server.zip>the code</a> and
follow along.<h3 id=publisher>Publisher</h3><p>As talked about above publisher is the one that send messages to the
broker/server. Message inside the payload can be whatever you want (string,
object, array). I would however personally avoid send large chunks of data like
blobs and such.<pre tabindex=0 style=background-color:#fff><code><span style=display:flex><span><span style=color:#00f><!DOCTYPE html></span>
</span></span><span style=display:flex><span><html lang=<span style=color:#a31515>"en"</span>>
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> <head>
</span></span><span style=display:flex><span> <meta charset=<span style=color:#a31515>"UTF-8"</span>>
</span></span><span style=display:flex><span> <meta name=<span style=color:#a31515>"viewport"</span> content=<span style=color:#a31515>"width=device-width, initial-scale=1.0"</span>>
</span></span><span style=display:flex><span> <title>Publisher</title>
</span></span><span style=display:flex><span> </head>
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> <body>
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> <h1>Publisher</h1>
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> <fieldset>
</span></span><span style=display:flex><span> <p>
</span></span><span style=display:flex><span> <label>Server:</label>
</span></span><span style=display:flex><span> <input type=<span style=color:#a31515>"text"</span> id=<span style=color:#a31515>"server"</span> value=<span style=color:#a31515>"http://localhost:4000"</span>>
</span></span><span style=display:flex><span> </p>
</span></span><span style=display:flex><span> <p>
</span></span><span style=display:flex><span> <label>Topic:</label>
</span></span><span style=display:flex><span> <input type=<span style=color:#a31515>"text"</span> id=<span style=color:#a31515>"topic"</span> value=<span style=color:#a31515>"sample-topic"</span>>
</span></span><span style=display:flex><span> </p>
</span></span><span style=display:flex><span> <p>
</span></span><span style=display:flex><span> <label>Event:</label>
</span></span><span style=display:flex><span> <input type=<span style=color:#a31515>"text"</span> id=<span style=color:#a31515>"event"</span> value=<span style=color:#a31515>"sample-event"</span>>
</span></span><span style=display:flex><span> </p>
</span></span><span style=display:flex><span> <p>
</span></span><span style=display:flex><span> <label>Message:</label>
</span></span><span style=display:flex><span> <input type=<span style=color:#a31515>"text"</span> id=<span style=color:#a31515>"message"</span> value=<span style=color:#a31515>'{"name": "John"}'</span>>
</span></span><span style=display:flex><span> </p>
</span></span><span style=display:flex><span> <p>
</span></span><span style=display:flex><span> <button type=<span style=color:#a31515>"button"</span> id=<span style=color:#a31515>"button"</span>>Publish message to topic</button>
</span></span><span style=display:flex><span> </p>
</span></span><span style=display:flex><span> </fieldset>
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> <script>
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> <span style=color:#00f>const</span> button = document.querySelector(<span style=color:#a31515>'#button'</span>);
</span></span><span style=display:flex><span> <span style=color:#00f>const</span> server = document.querySelector(<span style=color:#a31515>'#server'</span>);
</span></span><span style=display:flex><span> <span style=color:#00f>const</span> topic = document.querySelector(<span style=color:#a31515>'#topic'</span>);
</span></span><span style=display:flex><span> <span style=color:#00f>const</span> event = document.querySelector(<span style=color:#a31515>'#event'</span>);
</span></span><span style=display:flex><span> <span style=color:#00f>const</span> message = document.querySelector(<span style=color:#a31515>'#message'</span>);
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> button.addEventListener(<span style=color:#a31515>'click'</span>, <span style=color:#00f>async</span> (evt) => {
</span></span><span style=display:flex><span> <span style=color:#00f>const</span> req = <span style=color:#00f>await</span> fetch(<span style=color:#a31515>`</span><span style=color:#a31515>${</span>server.value<span style=color:#a31515>}</span><span style=color:#a31515>/publish`</span>, {
</span></span><span style=display:flex><span> method: <span style=color:#a31515>'post'</span>,
</span></span><span style=display:flex><span> headers: {
</span></span><span style=display:flex><span> <span style=color:#a31515>'Accept'</span>: <span style=color:#a31515>'application/json'</span>,
</span></span><span style=display:flex><span> <span style=color:#a31515>'Content-Type'</span>: <span style=color:#a31515>'application/json'</span>,
</span></span><span style=display:flex><span> },
</span></span><span style=display:flex><span> body: JSON.stringify({
</span></span><span style=display:flex><span> topic: topic.value,
</span></span><span style=display:flex><span> event: event.value,
</span></span><span style=display:flex><span> message: JSON.parse(message.value),
</span></span><span style=display:flex><span> }),
</span></span><span style=display:flex><span> });
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> <span style=color:#00f>const</span> res = <span style=color:#00f>await</span> req.json();
</span></span><span style=display:flex><span> console.log(res);
</span></span><span style=display:flex><span> });
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> </script>
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> </body>
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span></html>
</span></span></code></pre><h3 id=subscriber>Subscriber</h3><p>Subscriber is responsible for receiving new messages that come from server via
publisher. The code bellow is very rudimentary but works and follows the
implementation guidelines for EventSource.<p>You can use either Developer Tools Console to see incoming messages or you can
defer to Debugging with Google Chrome section above to see all EventStream
messages.<blockquote><p>Don't be alarmed if the subscriber gets disconnected from the server every so
often. The code we have here resets connection every 15s but it automatically
get reconnected and fetches all messages up to last received message id. This
setting can be adjusted in <code>server.js</code> file; search for the
<code>maxStreamDuration</code> variable.</blockquote><pre tabindex=0 style=background-color:#fff><code><span style=display:flex><span><span style=color:#00f><!DOCTYPE html></span>
</span></span><span style=display:flex><span><html lang=<span style=color:#a31515>"en"</span>>
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> <head>
</span></span><span style=display:flex><span> <meta charset=<span style=color:#a31515>"UTF-8"</span>>
</span></span><span style=display:flex><span> <meta name=<span style=color:#a31515>"viewport"</span> content=<span style=color:#a31515>"width=device-width, initial-scale=1.0"</span>>
</span></span><span style=display:flex><span> <title>Subscriber</title>
</span></span><span style=display:flex><span> <link rel=<span style=color:#a31515>"stylesheet"</span> href=<span style=color:#a31515>"style.css"</span>>
</span></span><span style=display:flex><span> </head>
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> <body>
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> <h1>Subscriber</h1>
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> <fieldset>
</span></span><span style=display:flex><span> <p>
</span></span><span style=display:flex><span> <label>Server:</label>
</span></span><span style=display:flex><span> <input type=<span style=color:#a31515>"text"</span> id=<span style=color:#a31515>"server"</span> value=<span style=color:#a31515>"http://localhost:4000"</span>>
</span></span><span style=display:flex><span> </p>
</span></span><span style=display:flex><span> <p>
</span></span><span style=display:flex><span> <label>Topic:</label>
</span></span><span style=display:flex><span> <input type=<span style=color:#a31515>"text"</span> id=<span style=color:#a31515>"topic"</span> value=<span style=color:#a31515>"sample-topic"</span>>
</span></span><span style=display:flex><span> </p>
</span></span><span style=display:flex><span> <p>
</span></span><span style=display:flex><span> <label>Event:</label>
</span></span><span style=display:flex><span> <input type=<span style=color:#a31515>"text"</span> id=<span style=color:#a31515>"event"</span> value=<span style=color:#a31515>"sample-event"</span>>
</span></span><span style=display:flex><span> </p>
</span></span><span style=display:flex><span> <p>
</span></span><span style=display:flex><span> <button type=<span style=color:#a31515>"button"</span> id=<span style=color:#a31515>"button"</span>>Subscribe to topic</button>
</span></span><span style=display:flex><span> </p>
</span></span><span style=display:flex><span> </fieldset>
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> <script>
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> <span style=color:#00f>const</span> button = document.querySelector(<span style=color:#a31515>'#button'</span>);
</span></span><span style=display:flex><span> <span style=color:#00f>const</span> server = document.querySelector(<span style=color:#a31515>'#server'</span>);
</span></span><span style=display:flex><span> <span style=color:#00f>const</span> topic = document.querySelector(<span style=color:#a31515>'#topic'</span>);
</span></span><span style=display:flex><span> <span style=color:#00f>const</span> event = document.querySelector(<span style=color:#a31515>'#event'</span>);
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> button.addEventListener(<span style=color:#a31515>'click'</span>, <span style=color:#00f>async</span> (evt) => {
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> <span style=color:#00f>let</span> es = <span style=color:#00f>new</span> EventSource(<span style=color:#a31515>`</span><span style=color:#a31515>${</span>server.value<span style=color:#a31515>}</span><span style=color:#a31515>/stream/</span><span style=color:#a31515>${</span>topic.value<span style=color:#a31515>}</span><span style=color:#a31515>`</span>);
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> es.addEventListener(event.value, <span style=color:#00f>function</span> (evt) {
</span></span><span style=display:flex><span> console.log(<span style=color:#a31515>`incoming message`</span>, JSON.parse(evt.data));
</span></span><span style=display:flex><span> });
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> es.addEventListener(<span style=color:#a31515>'open'</span>, <span style=color:#00f>function</span> (evt) {
</span></span><span style=display:flex><span> console.log(<span style=color:#a31515>'connected'</span>, evt);
</span></span><span style=display:flex><span> });
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> es.addEventListener(<span style=color:#a31515>'error'</span>, <span style=color:#00f>function</span> (evt) {
</span></span><span style=display:flex><span> console.log(<span style=color:#a31515>'error'</span>, evt);
</span></span><span style=display:flex><span> });
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> });
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> </script>
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> </body>
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span></html>
</span></span></code></pre><h2 id=reading-further>Reading further</h2><ul><li><a href=https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events>Using server-sent events</a><li><a href=https://www.smashingmagazine.com/2018/02/sse-websockets-data-flow-http2/>Using SSE Instead Of WebSockets For Unidirectional Data Flow Over HTTP/2</a><li><a href=https://apifriends.com/api-streaming/server-sent-events/>What is Server-Sent Events?</a><li><a href=https://tools.ietf.org/id/draft-xie-bidirectional-messaging-01.html>An HTTP/2 extension for bidirectional messaging communication</a><li><a href=https://developers.google.com/web/fundamentals/performance/http2>Introduction to HTTP/2</a><li><a href=https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API>The WebSocket API (WebSockets)</a></ul></div></article></main><section><hr><h2>Posts from blogs I follow around the net</h2><ul><li><a href=https://chotrin.org/writing/2023-10-20.html target=_blank rel=noopener>OpenBSD upgrade and fall things.</a><div>Been AFK for a bit. It's autumn and I upgraded this server to OpenBSD 7.4! — <a href=https://chotrin.org>chötrin's wiki.</a><li><a href=https://mirzapandzo.com/next-image-url-parameter-is-valid-but-upstream-response-is-invalid target=_blank rel=noopener>Next/Image "url" parameter is valid but upstream response is invalid</a><div>Getting "url" parameter is valid but upstream response is invalid error with Next/Image on WSL2 — <a href=https://mirzapandzo.com/>Mirza Pandzo's Blog</a><li><a href=https://drewdevault.com/2023/10/13/Going-off-script.html target=_blank rel=noopener>Going off-script</a><div>There is a phenomenon in society which I find quite bizarre. Upon our entry to
this mortal coil, we are endowed with self-awareness, agency, and free will.
Each of th… — <a href=https://drewdevault.com>Drew DeVault's blog</a><li><a href=https://solar.lowtechmagazine.com/2023/10/workshop-in-rotterdam-how-to-build-a-bike-generator/ target=_blank rel=noopener>Workshop in Rotterdam: How to Build a Bike Generator</a><div>Afbeelding: Low-tech Magazine workshop in Rotterdam, the Netherlands. Poster: Marie Verdeil. Image: Sara Vercauteren
The workshop takes place on behalf of the “Hou… — <a href=https://solar.lowtechmagazine.com/posts/>LOW←TECH MAGAZINE English</a><li><a href="http://offbeatpursuit.com:80/blog/?id=24" target=_blank rel=noopener>Printf debugging</a><div>tags:
plan9
There’s no shame in that. Yes, there is documentation, code to be
read, and debuggers to be used. But sometimes you just need to “see”
what is happening.
So… — <a href=http://offbeatpursuit.com:80/blog/>WLOG - blog</a><li><a href=https://neil.computer/notes/chart-of-accounts-for-startups-and-saas-companies/ target=_blank rel=noopener>Chart of Accounts for Startups and SaaS Companies</a><div>Accounting is fundamental to starting a business. You need to have a basic understanding of accounting principles and essential bookkeeping. I had to learn it. Ther… — <a href=https://neil.computer/>Neil Panchal</a><li><a href=https://journal.valeriansaliou.name/deploy-a-nomad-cluster-on-alpine-linux-with-vultr/ target=_blank rel=noopener>Deploy a Nomad Cluster on Alpine Linux with Vultr</a><div>After spending countless hours trying to understand how to deploy my apps on Kubernetes for the first time to host Mirage, an AI API service that I run, I ended up … — <a href=https://journal.valeriansaliou.name/>Valerian Saliou</a><li><a href=https://jcs.org/2023/10/17/wikipedia target=_blank rel=noopener>Wikipedia Reader 1.0 Released</a><div>Wikipedia Reader
1.0 has been released:
wikipedia-1.0.sit
(StuffIt 3 archive, includes
source code
and THINK C 5 project file)
SHA256: 360e12d064f6579695f1e627ce34cb2f0… — <a href=https://jcs.org/>joshua stein</a></ul><p><a href=https://git.sr.ht/~sircmpwn/openring>Generated with openring.</a></section><footer><hr><p><big><strong>Want to comment or have something to add?</strong></big><p>You can write me an email
at <a href=mailto:m@mitjafelicijan.com>m@mitjafelicijan.com</a> or
catch up with me <a href=https://telegram.me/mitjafelicijan target=_blank>on Telegram</a>.<hr><p>This website does not track you. Content is made available under
the <a href=https://creativecommons.org/licenses/by/4.0/ target=_blank rel=noreferrer>CC BY 4.0 license</a> unless specified
otherwise. Blog is also available as <a href=/index.xml target=_blank>RSS feed</a>.</footer><script>
window.va = window.va || function () { (window.vaq = window.vaq || []).push(arguments); };
</script><script defer src=/_vercel/insights/script.js></script>
|