node-turbo 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.c8rc.json +5 -0
  2. package/.esdoc.json +83 -0
  3. package/.eslintrc.json +10 -0
  4. package/.mocharc.json +7 -0
  5. package/LICENSE +21 -0
  6. package/README.md +620 -0
  7. package/docs/API.md +857 -0
  8. package/lib/errors/index.js +36 -0
  9. package/lib/express/express-turbo-stream.js +41 -0
  10. package/lib/express/index.js +4 -0
  11. package/lib/express/turbocharge-express.js +108 -0
  12. package/lib/index.js +8 -0
  13. package/lib/koa/index.js +4 -0
  14. package/lib/koa/koa-turbo-stream.js +44 -0
  15. package/lib/koa/turbocharge-koa.js +122 -0
  16. package/lib/request-helpers.js +53 -0
  17. package/lib/sse/index.js +3 -0
  18. package/lib/sse/sse-turbo-stream.js +137 -0
  19. package/lib/turbo-element.js +71 -0
  20. package/lib/turbo-frame.js +79 -0
  21. package/lib/turbo-readable.js +125 -0
  22. package/lib/turbo-stream-element.js +67 -0
  23. package/lib/turbo-stream.js +350 -0
  24. package/lib/ws/index.js +4 -0
  25. package/lib/ws/ws-turbo-stream.js +112 -0
  26. package/package.json +75 -0
  27. package/test/hooks.js +46 -0
  28. package/test/integration/express.test.js +137 -0
  29. package/test/integration/koa.test.js +125 -0
  30. package/test/integration/sse.test.js +80 -0
  31. package/test/integration/ws.test.js +155 -0
  32. package/test/package.test.js +68 -0
  33. package/test/unit/core/request-helpers.test.js +97 -0
  34. package/test/unit/core/turbo-element.test.js +15 -0
  35. package/test/unit/core/turbo-frame.test.js +63 -0
  36. package/test/unit/core/turbo-readable.test.js +93 -0
  37. package/test/unit/core/turbo-stream-element.test.js +76 -0
  38. package/test/unit/core/turbo-stream.test.js +308 -0
  39. package/test/unit/express/express-turbo-stream.test.js +39 -0
  40. package/test/unit/express/turbocharge-express.test.js +123 -0
  41. package/test/unit/koa/koa-turbo-stream.test.js +56 -0
  42. package/test/unit/koa/turbocharge-koa.test.js +141 -0
  43. package/test/unit/sse/sse-turbo-stream.test.js +109 -0
  44. package/test/unit/ws/ws-turbo-stream.test.js +46 -0
package/README.md ADDED
@@ -0,0 +1,620 @@
1
+ # node-turbo
2
+
3
+ A library for Node.js to assist with the server side of [37signals](https://37signals.com)' [Hotwire Turbo](https://turbo.hotwired.dev) framework. It provides classes and functions for Web servers and also convenience functions for the frameworks [Koa](https://koajs.com) and [Express](https://expressjs.com) as well as for [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) and [SSE](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events).
4
+
5
+ This documentation assumes that you are familiar with Turbo and its [handbook](https://turbo.hotwired.dev/handbook/introduction).
6
+
7
+ ## Table of Contents
8
+ - [Installation](#installation)
9
+ - [Compatibility](#compatibility)
10
+ - [Browser](#browser)
11
+ - [ES Module](#es-module)
12
+ - [Tested](#tested)
13
+ - [API docs](#api-docs)
14
+ - [Usage](#usage)
15
+ - [Basics\/Standalone](#basicsstandalone)
16
+ - [Turbo Stream](#turbo-stream)
17
+ - [Target multiple elements](#target-multiple-elements)
18
+ - [Custom actions](#custom-actions)
19
+ - [Using the Node.js streams API](#using-the-nodejs-streams-api)
20
+ - [Turbo Frame](#turbo-frame)
21
+ - [Request helper functions](#request-helper-functions)
22
+ - [isTurboStreamRequest(request)](#isturbostreamrequestrequest)
23
+ - [isTurboFrameRequest(request)](#isturboframerequestrequest)
24
+ - [getTurboFrameId(request)](#getturboframeidrequest)
25
+ - [Koa](#koa)
26
+ - [Express](#express)
27
+ - [WebSocket](#websocket)
28
+ - [SSE](#sse)
29
+ - [Using http.Server](#using-httpserver)
30
+ - [Using Koa](#using-koa)
31
+ - [Using Express](#using-express)
32
+ - [License](#license)
33
+
34
+ ## Installation
35
+ ```console
36
+ npm install node-turbo
37
+ ```
38
+
39
+ ## Compatibility
40
+
41
+ ### Browser
42
+ This module has been built for Node.js only and does not work in the browser (nor is it needed there).
43
+
44
+ ### ES Module
45
+ node-turbo as been written as an ECMAScript module and all examples will use ES module syntax. If you want to use node-turbo within a CommonJS application, use dynamic [`import()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import) instead of `require()`.
46
+
47
+ ### Tested
48
+ node-turbo has been tested with:
49
+
50
+ | Name | Version(s) |
51
+ | :--- | :--- |
52
+ | [Node.js](https://nodejs.org/) | 16.6 — 18.16.0 |
53
+ | [Hotwired Turbo](https://turbo.hotwired.dev/) | 7.3.0 — 8.0.0-beta.2 |
54
+ | [Koa](https://koajs.com/) | 2.14.2 |
55
+ | [Express](https://expressjs.com/) | 4.18.2 |
56
+ | [ws](https://github.com/websockets/ws) | 8.15.1 |
57
+
58
+ ## API docs
59
+ See [`/docs/API.md`](./docs/API.md) for a documentation of all node-turbo classes and functions.
60
+
61
+ ## Usage
62
+
63
+ ### Basics/Standalone
64
+
65
+ #### Turbo Stream
66
+ ```javascript
67
+ import { TurboStream } from 'node-turbo';
68
+
69
+ const ts = new TurboStream({
70
+ action: 'append',
71
+ target: 'target-id'
72
+ },
73
+ '<p>My content</p>');
74
+
75
+ const html = ts.render();
76
+ ```
77
+
78
+ This will render the following HTML fragment:
79
+
80
+ ```html
81
+ <turbo-stream action="append" target="target-id">
82
+ <template>
83
+ <p>My content</p>
84
+ </template>
85
+ </turbo-stream>
86
+ ```
87
+
88
+ For all [supported actions](https://turbo.hotwired.dev/handbook/streams#stream-messages-and-actions), there are chainable shortcut functions:
89
+
90
+ ```javascript
91
+ import { TurboStream } from 'node-turbo';
92
+
93
+ const ts = new TurboStream()
94
+ .append('target-id', '<p>My content</p>')
95
+ .replace('target-id-2', '<p>New content</p>')
96
+ .remove('target-id-3');
97
+ const html = ts.render();
98
+ ```
99
+
100
+ Result:
101
+
102
+ ```html
103
+ <turbo-stream action="append" target="target-id">
104
+ <template>
105
+ <p>My content</p>
106
+ </template>
107
+ </turbo-stream>
108
+ <turbo-stream action="replace" target="target-id-2">
109
+ <template>
110
+ <p>New content</p>
111
+ </template>
112
+ </turbo-stream>
113
+ <turbo-stream action="remove" target="target-id-3">
114
+ <!-- <template> and content are omitted -->
115
+ </turbo-stream>
116
+ ```
117
+
118
+ ##### Target multiple elements
119
+ If you want to [target multiple elements](https://turbo.hotwired.dev/handbook/streams#actions-with-multiple-targets), you can use the `[action]All()` function:
120
+
121
+ ```javascript
122
+ import { TurboStream } from 'node-turbo';
123
+
124
+ let ts = new TurboStream()
125
+ .appendAll('.my-targets', '<p>My content</p>');
126
+ ```
127
+
128
+ Result:
129
+
130
+ ```html
131
+ <turbo-stream action="append" targets=".my-targets">
132
+ <template>
133
+ <p>My content</p>
134
+ </template>
135
+ </turbo-stream>
136
+ ```
137
+
138
+ ##### Custom actions
139
+ If you want to use [custom actions](https://turbo.hotwired.dev/handbook/streams#custom-actions), you can use the `custom()`/`customAll()` functions:
140
+ ```javascript
141
+ import { TurboStream } from 'node-turbo';
142
+
143
+ let ts = new TurboStream()
144
+ .custom('custom-action', 'target-id', '<p>My content</p>');
145
+ ```
146
+
147
+ ##### Using the Node.js streams API
148
+ If you want to use the [Node.js streams API](https://nodejs.org/docs/latest/api/stream.html) with Turbo Streams, you can
149
+ create a Readable stream instance which reads Turbo Stream messages.
150
+
151
+ ```javascript
152
+ import { TurboStream } from 'node-turbo';
153
+
154
+ const ts = new TurboStream();
155
+ const readable = ts.createReadableStream();
156
+
157
+ readable.pipe(process.stdout)
158
+ ```
159
+
160
+ See [Koa](#koa), [SSE](#sse) or [WebSocket](#websocket) for further examples.
161
+
162
+ #### Turbo Frame
163
+ ```javascript
164
+ import { TurboFrame } from 'node-turbo';
165
+
166
+ const tf = new TurboFrame('my-id', '<p>content</p>');
167
+ const html = tframe.render();
168
+ ```
169
+
170
+ This will render the following HTML fragment:
171
+
172
+ ```html
173
+ <turbo-frame id="my-id">
174
+ <p>My content</p>
175
+ </turbo-stream>
176
+ ```
177
+
178
+ #### Request Helper Functions
179
+ node-turbo also provides the following helper functions. You can use these to adapt the behaviour of your server to the differend kind of requests.
180
+
181
+ ##### isTurboStreamRequest(request)
182
+ ```javascript
183
+ import { isTurboStreamRequest } from 'node-turbo';
184
+
185
+ const isTsReq = isTurboStreamRequest(req);
186
+ ```
187
+ Checks if the request is a Turbo Stream request by looking if the HTTP header `Accept` includes the MIME type `text/vnd.turbo-stream.html`. Expects an object like an http.ClientRequest instance but doesn not make any hard checks. Returns `true` or `false`.
188
+
189
+ ##### isTurboFrameRequest(request)
190
+ ```javascript
191
+ import { isTurboFrameRequest } from 'node-turbo';
192
+
193
+ const isTfReq = isTurboFrameRequest(req);
194
+ ```
195
+ Checks if the request is a Turbo Stream request by looking for the HTTP header `turbo-frame`, which holds the ID of the Turbo Frame that made the request. Expects an object like an `http.ClientRequest` instance but does not make any hard checks. Returns `true` or `false`.
196
+
197
+ ##### getTurboFrameId(request)
198
+ ```javascript
199
+ import { getTurboFrameId } from 'node-turbo';
200
+
201
+ const tfId = getTurboFrameId(req);
202
+ ```
203
+ Returns the content of the HTTP header `turbo-frame`, which holds the ID of the Turbo Frame which made the request. Expects an object like an `http.ClientRequest` instance but doesn not make any hard checks.
204
+
205
+ ### Koa
206
+ You can add convencience functions to your Koa application by calling `turbochargeKoa(app)`. This adds the following functions to Koa's `context`:
207
+
208
+ - `turboStream()`
209
+ Returns a chainable Turbo Stream instance which directly writes to `ctx.body` whenever an element is added. Also sets the correct `Content-Type` header.
210
+ - `turboFrame()`
211
+ Returns a Turbo Frame instance which directly writes to `ctx.body`.
212
+ - `isTurboStreamRequest()`
213
+ Checks if the request is a Turbo Stream request by looking for the MIME type in the `accept` headers.
214
+ Returns `true`/`false`.
215
+ - `isTurboFrameRequest()`
216
+ Checks if the request is a Turbo Frame request by looking for the `turbo-frame` header.
217
+ Returns `true`/`false`.
218
+ - `getTurboFrameId()`
219
+ Returns the contents of the `turbo-frame` header.
220
+ - `sseTurboStream()`
221
+ *Experimental*. Configures Koa to keep the connection open and use a stream to pipe to `ctx.res`.
222
+ See [SSE using Koa](#using-koa) for further examples.
223
+
224
+
225
+ ```javascript
226
+ import Koa from 'koa';
227
+ import { turbochargeKoa } from 'node-turbo/koa';
228
+
229
+ const app = new Koa();
230
+ turbochargeKoa(app);
231
+
232
+ app.use(async (ctx, next) => {
233
+ if (ctx.path !== '/turbo-frame') {
234
+ return await next();
235
+ }
236
+
237
+ if (ctx.isTurboFrameRequest()) {
238
+ // Automatically retrieves the Turbo Frame ID from the header
239
+ // and uses it for the response.
240
+ ctx.turboFrame('<p>New content</p>');
241
+ //You can set it manually with:
242
+ // ctx.turboFrame('turbo-frame-id', <p>New content</p>');
243
+ }
244
+ else {
245
+ ctx.redirect('/path/to/other/page');
246
+ }
247
+ });
248
+
249
+ app.use(async (ctx, next) => {
250
+ if (ctx.path !== '/turbo-stream') {
251
+ return await next();
252
+ }
253
+
254
+ if (ctx.isTurboStreamRequest()) {
255
+ ctx.turboStream()
256
+ .append('target-id', '<p>New content</p>');
257
+ }
258
+ else {
259
+ ctx.redirect('/path/to/other/page');
260
+ }
261
+ });
262
+
263
+ app.use(async (ctx, next) => {
264
+ if (ctx.path !== '/sse') {
265
+ return await next();
266
+ }
267
+
268
+ const ssets = ctx.sseTurboStream();
269
+
270
+ // These get automatically piped to ctx.res in SSE format.
271
+ ssets
272
+ .append('target-id', '<p>My content</p>')
273
+ .updateAll('.targets', '<p>My other content</p>');
274
+ });
275
+
276
+ app.listen(8080);
277
+ ```
278
+
279
+ ### Express
280
+ You can add convencience functions to your Express application by calling `turbochargeExpress(app)`. This adds the following functions to Express' `request` object:
281
+
282
+ - `isTurboStreamRequest()`
283
+ Checks if the request is a Turbo Stream request by looking for the MIME type in the `accept` headers.
284
+ Returns `true`/`false`.
285
+ - `isTurboFrameRequest()`
286
+ Checks if the request is a Turbo Frame request by looking for the `turbo-frame` header.
287
+ Returns `true`/`false`.
288
+ - `getTurboFrameId()`
289
+ Returns the contents of the `turbo-frame` header.
290
+
291
+ Also adds the following functions to Express' `response` object:
292
+
293
+ - `turboStream()`
294
+ Returns a chainable Turbo Stream instance which introduces the function `send()` which sends the rendered Turbo Stream message as HTTP response with the correct MIME type.
295
+ - `turboFrame(id, content)`
296
+ Returns a Turbo Frame instance which directly sends the rendered Turbo Frame message as HTTP response.
297
+ - `turboFrame(content)`
298
+ If you omit the `id` attribute, it is automatically added by using the ID from the `turbo-frame` header.
299
+ - `sseTurboStream()`
300
+ *Experimental*. Configures Express to keep the connection open and use a stream to pipe to `res`.
301
+ See [SSE using Express](#using-express) for further examples.
302
+
303
+ ```javascript
304
+ import express from 'express';
305
+ import { turbochargeExpress } from 'node-turbo/express';
306
+
307
+ const app = express();
308
+ turbochargeExpress(app);
309
+
310
+ app.get('/', (req, res) => {
311
+ if (req.isTurboFrameRequest()) {
312
+ res.turboFrame('<p>My content</p>');
313
+ }
314
+ else if (req.isTurboStreamRequest()) {
315
+ res.turboStream()
316
+ .append('target-id', '<p>My content</p>')
317
+ .remove('taget-id-2')
318
+ .send();
319
+ }
320
+ else {
321
+ res.status(501).end();
322
+ }
323
+ });
324
+
325
+ app.listen(8080, () => {
326
+ // ...
327
+ });
328
+ ```
329
+
330
+ ### WebSocket
331
+
332
+ We're using the library [ws](https://github.com/websockets/ws) for our examples.
333
+
334
+ ```javascript
335
+ import { WsTurboStream } from 'node-turbo/ws';
336
+ import { WebSocketServer } from 'ws';
337
+
338
+ const wss = new WebSocketServer({ port: 8080 });
339
+
340
+ wss.on('connection', webSocket => {
341
+ // The Turbo Stream messages get sent immediately.
342
+ WsTurboStream
343
+ .use(webSocket)
344
+ .append('id1', 'c1')
345
+ .update('id2', 'c2');
346
+ });
347
+ ```
348
+
349
+ You can also use the Node.js streams API by utilizing ws' `createWebSocketStream()` function.
350
+
351
+ ```javascript
352
+ import { TurboStream } from 'node-turbo';
353
+ import { WebSocketServer, createWebSocketStream } from 'ws';
354
+
355
+ const wss = new WebSocketServer({ port: 8080 });
356
+
357
+ wss.on('connection', function connection(ws) {
358
+ const ts = new TurboStream();
359
+ const readable = ts.createReadableStream();
360
+ const wsStream = createWebSocketStream(ws, { encoding: 'utf8' });
361
+ readable.pipe(wsStream);
362
+
363
+ ts
364
+ .append('target-id', '<p>My content</p>')
365
+ .update('target-id-2', '<p>Updated content</p>')
366
+ .remove('target-id-2');
367
+ });
368
+ ```
369
+
370
+ ### SSE
371
+
372
+ #### Using http.Server
373
+
374
+ ```javascript
375
+ import http from 'node:http';
376
+ import { SseTurboStream } from 'node-turbo/sse';
377
+
378
+ const config = {};
379
+ config.port = 8080;
380
+ config.baseUrl = `http://localhost:${config.port}`;
381
+ config.sseUrl = `${config.baseUrl}/sse`;
382
+
383
+ const httpServer = http.createServer((req, res) => {
384
+
385
+ // SSE endpoint
386
+ if (req.url === '/sse') {
387
+
388
+ res.writeHead(200, {
389
+ 'Content-Type': 'text/event-stream',
390
+ 'Cache-Control': 'no-cache',
391
+ ...(req.httpVersionMajor === 1 && { 'Connection': 'keep-alive' })
392
+ });
393
+
394
+ // Turbo listens to nameless events and 'message' events.
395
+ const ssets = new SseTurboStream('message');
396
+
397
+ // Timeout is only here for us to have time to observe.
398
+ setTimeout(() => {
399
+ ssets.append('stream1', '<p>My content</p>')
400
+ .append('stream2', '<p>My content 2</p>')
401
+ .append('stream3', '<p>\n<span>My multiline content 3</span>\n</p>');
402
+
403
+ res.write(ssets.flush());
404
+ }, 1000);
405
+
406
+ // You can also use the streams API.
407
+ setTimeout(() => {
408
+ const stream = ssets.createReadableStream();
409
+ stream.pipe(res);
410
+ ssets.prependAll('.stream', '<p>Prepend!</p>');
411
+ }, 2000);
412
+
413
+ return;
414
+ }
415
+
416
+ // Client
417
+ res.end(`<!DOCTYPE html>
418
+ <html>
419
+ <head>
420
+ <meta charset="utf-8">
421
+ <title>SSE Test</title>
422
+ <style>
423
+ .b {
424
+ border: 1px dashed #cccc;
425
+ margin-bottom: 10px;
426
+ padding: 10px;
427
+ }
428
+ </style>
429
+ <script type="module" src="https://unpkg.com/@hotwired/turbo@8.0.0-beta.2/dist/turbo.es2017-esm.js"></script>
430
+ <script>
431
+ var eventSource = new EventSource('/sse');
432
+ eventSource.onmessage = function(event) {
433
+ document.getElementById('log').innerText += event.data + '\\n\\n';
434
+ };
435
+ </script>
436
+ </head>
437
+ <body>
438
+ <turbo-stream-source src="${ config.sseUrl }">
439
+ <h1>SSE Test</h1>
440
+ <h2>Control</h2>
441
+ <pre class="b" id="log"></pre>
442
+ <h2>stream1</h2>
443
+ <div class="b stream" id="stream1"></div>
444
+ <h2>stream2</h2>
445
+ <div class="b stream" id="stream2"></div>
446
+ <h2>stream3</h2>
447
+ <div class="b stream" id="stream3"></div>
448
+ </body>
449
+ </html>`);
450
+ });
451
+
452
+ httpServer.listen(config.port);
453
+
454
+ httpServer.on('error', (err) => {
455
+ console.log(err);
456
+ process.exit(1);
457
+ });
458
+
459
+ httpServer.on('listening', () => {
460
+ console.log(`HTTP server listening on port ${config.port}…`);
461
+ });
462
+ ```
463
+
464
+ #### Using Koa
465
+ ```javascript
466
+ import Koa from 'koa';
467
+ import { turbochargeKoa } from 'node-turbo/koa';
468
+
469
+ // Config
470
+ const config = {};
471
+ config.port = 8080;
472
+ config.baseUrl = `http://localhost:${config.port}`;
473
+ config.sseUrl = `${config.baseUrl}/sse`;
474
+
475
+ // Koa
476
+ const app = new Koa();
477
+
478
+ // Augment Koa with convenience functions.
479
+ turbochargeKoa(app);
480
+
481
+ app.use(async (ctx, next) => {
482
+ if (ctx.path !== '/sse') {
483
+ return await next();
484
+ }
485
+
486
+ // Use convenience function to configure Koa.
487
+ // Returns SseTurboStream instance which directly streams to res.
488
+ const ssets = ctx.sseTurboStream();
489
+
490
+ // Timeout is only here for us to have time to observe.
491
+ setTimeout(() => {
492
+ ssets
493
+ .append('stream1', '<p>My content <strong>1</strong></p>')
494
+ .append('stream2', '<p>My content <strong>2</strong></p>')
495
+ .append('stream3', '<p>My content <strong>3</strong></p>');
496
+ }, 1000);
497
+
498
+ setTimeout(() => {
499
+ ssets.prependAll('.stream', '<p>Prepend all</p>');
500
+ }, 2000);
501
+ });
502
+
503
+ app.use(async (ctx, next) => {
504
+ ctx.body = `<!DOCTYPE html>
505
+ <html>
506
+ <head>
507
+ <meta charset="utf-8">
508
+ <title>SSE Test</title>
509
+ <style>
510
+ .b {
511
+ border: 1px dashed #cccc;
512
+ margin-bottom: 10px;
513
+ padding: 10px;
514
+ }
515
+ </style>
516
+ <script type="module" src="https://unpkg.com/@hotwired/turbo@8.0.0-beta.2/dist/turbo.es2017-esm.js"></script>
517
+ <script>
518
+ var eventSource = new EventSource('/sse');
519
+ eventSource.onmessage = function(event) {
520
+ document.getElementById('log').innerText += event.data + '\\n\\n';
521
+ };
522
+ </script>
523
+ </head>
524
+ <body>
525
+ <turbo-stream-source src="${ config.sseUrl }">
526
+ <h1>SSE Test</h1>
527
+ <h2>Control</h2>
528
+ <pre class="b" id="log"></pre>
529
+ <h2>stream1</h2>
530
+ <div class="b stream" id="stream1"></div>
531
+ <h2>stream2</h2>
532
+ <div class="b stream" id="stream2"></div>
533
+ <h2>stream3</h2>
534
+ <div class="b stream" id="stream3"></div>
535
+ </body>
536
+ </html>`;
537
+ });
538
+
539
+ app.listen(config.port);
540
+ ```
541
+
542
+ #### Using Express
543
+ ```javascript
544
+ import express from 'express';
545
+ import { turbochargeExpress } from 'node-turbo/express';
546
+
547
+ // Config
548
+ const config = {};
549
+ config.port = 8080;
550
+ config.baseUrl = `http://localhost:${config.port}`;
551
+ config.sseUrl = `${config.baseUrl}/sse`;
552
+
553
+ // Express
554
+ const app = express();
555
+
556
+ // Augment Express with convenience functions.
557
+ turbochargeExpress(app);
558
+
559
+ // SSE endpoint
560
+ app.get('/sse', async (req, res) => {
561
+
562
+ // Use convenience function to configure Express.
563
+ // Returns SseTurboStream instance which directly streams to res.
564
+ const ssets = res.sseTurboStream();
565
+
566
+ // Timeout is only here for us to have time to observe.
567
+ setTimeout(() => {
568
+ ssets
569
+ .append('stream1', '<p>My content <strong>1</strong></p>')
570
+ .append('stream2', '<p>My content <strong>2</strong></p>')
571
+ .append('stream3', '<p>My content <strong>3</strong></p>');
572
+ }, 1000);
573
+
574
+ setTimeout(() => {
575
+ ssets.prependAll('.stream', '<p>Prepend all</p>');
576
+ }, 2000);
577
+ });
578
+
579
+ // Client
580
+ app.get('/', async (req, res) => {
581
+ res.send(`<!DOCTYPE html>
582
+ <html>
583
+ <head>
584
+ <meta charset="utf-8">
585
+ <title>SSE Test</title>
586
+ <style>
587
+ .b {
588
+ border: 1px dashed #cccc;
589
+ margin-bottom: 10px;
590
+ padding: 10px;
591
+ }
592
+ </style>
593
+ <script type="module" src="https://unpkg.com/@hotwired/turbo@8.0.0-beta.2/dist/turbo.es2017-esm.js"></script>
594
+ <script>
595
+ var eventSource = new EventSource('/sse');
596
+ eventSource.onmessage = function(event) {
597
+ document.getElementById('log').innerText += event.data + '\\n\\n';
598
+ };
599
+ </script>
600
+ </head>
601
+ <body>
602
+ <turbo-stream-source src="${ config.sseUrl }">
603
+ <h1>SSE Test</h1>
604
+ <h2>Control</h2>
605
+ <pre class="b" id="log"></pre>
606
+ <h2>stream1</h2>
607
+ <div class="b stream" id="stream1"></div>
608
+ <h2>stream2</h2>
609
+ <div class="b stream" id="stream2"></div>
610
+ <h2>stream3</h2>
611
+ <div class="b stream" id="stream3"></div>
612
+ </body>
613
+ </html>`)
614
+ });
615
+
616
+ app.listen(config.port);
617
+ ```
618
+ ## License
619
+
620
+ node-turbo is © 2024 Walter Krivanek <walter@vividvisions.com> and released under the [MIT license](https://mit-license.org).