node-turbo 1.2.7 → 1.3.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.
- package/LICENSE +1 -1
- package/README.md +15 -15
- package/docs/API.md +20 -17
- package/docs/config.codekit3 +6 -4
- package/lib/express/express-turbo-stream.js +1 -1
- package/lib/express/turbocharge-express.js +3 -2
- package/lib/koa/turbocharge-koa.js +4 -2
- package/lib/sse/sse-turbo-stream.js +44 -25
- package/lib/turbo-readable.js +3 -10
- package/lib/turbo-stream.js +50 -11
- package/lib/ws/ws-turbo-stream.js +1 -0
- package/package.json +12 -12
- package/test/end2end/server-koa.js +4 -3
- package/test/integration/sse.test.js +45 -33
- package/test/integration/ws.test.js +2 -3
- package/test/unit/core/turbo-readable.test.js +8 -11
- package/test/unit/core/turbo-stream.test.js +89 -19
- package/test/unit/express/turbocharge-express.test.js +2 -1
- package/test/unit/koa/turbocharge-koa.test.js +2 -1
- package/test/unit/sse/sse-turbo-stream.test.js +12 -4
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -55,11 +55,11 @@ node-turbo has been tested with 100% code coverage with the following engines/li
|
|
|
55
55
|
|
|
56
56
|
| Name | Version(s) |
|
|
57
57
|
| :--- | :--- |
|
|
58
|
-
| [Hotwire Turbo](https://turbo.hotwired.dev/) | 7.3.0 - 8.0.
|
|
58
|
+
| [Hotwire Turbo](https://turbo.hotwired.dev/) | 7.3.0 - 8.0.23 |
|
|
59
59
|
| [Node.js](https://nodejs.org/) | 16.6 - 22.20.0 |
|
|
60
|
-
| [Koa](https://koajs.com/) | 2.14.2 - 3.
|
|
61
|
-
| [Express](https://expressjs.com/) | 4.18.2 - 5.1
|
|
62
|
-
| [ws](https://github.com/websockets/ws) | 8.15.1 - 8.
|
|
60
|
+
| [Koa](https://koajs.com/) | 2.14.2 - 3.1.2 |
|
|
61
|
+
| [Express](https://expressjs.com/) | 4.18.2 - 5.2.1 |
|
|
62
|
+
| [ws](https://github.com/websockets/ws) | 8.15.1 - 8.19.0 |
|
|
63
63
|
|
|
64
64
|
## API docs
|
|
65
65
|
See [`/docs/API.md`](./docs/API.md) for a documentation of all node-turbo classes and functions.
|
|
@@ -216,15 +216,15 @@ Result:
|
|
|
216
216
|
|
|
217
217
|
##### Using the Node.js streams API
|
|
218
218
|
If you want to use the [Node.js streams API](https://nodejs.org/docs/latest/api/stream.html) with Turbo Streams, you can
|
|
219
|
-
create a
|
|
219
|
+
create a `PassThrough` stream instance which reads Turbo Stream messages.
|
|
220
220
|
|
|
221
221
|
```javascript
|
|
222
222
|
import { TurboStream } from 'node-turbo';
|
|
223
223
|
|
|
224
224
|
const ts = new TurboStream();
|
|
225
|
-
const
|
|
225
|
+
const nStream = ts.createNodeStream();
|
|
226
226
|
|
|
227
|
-
|
|
227
|
+
nStream.pipe(process.stdout);
|
|
228
228
|
|
|
229
229
|
// These elements get piped immediately:
|
|
230
230
|
ts
|
|
@@ -459,9 +459,9 @@ const wss = new WebSocketServer({ port: 8080 });
|
|
|
459
459
|
|
|
460
460
|
wss.on('connection', function connection(ws) {
|
|
461
461
|
const ts = new TurboStream();
|
|
462
|
-
const
|
|
462
|
+
const nStream = ts.createNodeStream();
|
|
463
463
|
const wsStream = createWebSocketStream(ws, { encoding: 'utf8' });
|
|
464
|
-
|
|
464
|
+
nStream.pipe(wsStream);
|
|
465
465
|
|
|
466
466
|
ts
|
|
467
467
|
.append('target-id', '<p>My content</p>')
|
|
@@ -508,8 +508,8 @@ const httpServer = http.createServer((req, res) => {
|
|
|
508
508
|
|
|
509
509
|
// You can also use the streams API.
|
|
510
510
|
setTimeout(() => {
|
|
511
|
-
const
|
|
512
|
-
|
|
511
|
+
const nStream = ssets.createNodeStream();
|
|
512
|
+
nStream.pipe(res);
|
|
513
513
|
ssets.prependAll('.stream', '<p>Prepend!</p>');
|
|
514
514
|
}, 2000);
|
|
515
515
|
|
|
@@ -529,7 +529,7 @@ const httpServer = http.createServer((req, res) => {
|
|
|
529
529
|
padding: 10px;
|
|
530
530
|
}
|
|
531
531
|
</style>
|
|
532
|
-
<script type="module" src="https://unpkg.com/@hotwired/turbo@8.0.
|
|
532
|
+
<script type="module" src="https://unpkg.com/@hotwired/turbo@8.0.23/dist/turbo.es2017-esm.js"></script>
|
|
533
533
|
<script>
|
|
534
534
|
var eventSource = new EventSource('/sse');
|
|
535
535
|
eventSource.onmessage = function(event) {
|
|
@@ -616,7 +616,7 @@ app.use(async (ctx, next) => {
|
|
|
616
616
|
padding: 10px;
|
|
617
617
|
}
|
|
618
618
|
</style>
|
|
619
|
-
<script type="module" src="https://unpkg.com/@hotwired/turbo@8.0.
|
|
619
|
+
<script type="module" src="https://unpkg.com/@hotwired/turbo@8.0.23/dist/turbo.es2017-esm.js"></script>
|
|
620
620
|
<script>
|
|
621
621
|
var eventSource = new EventSource('/sse');
|
|
622
622
|
eventSource.onmessage = function(event) {
|
|
@@ -693,7 +693,7 @@ app.get('/', async (req, res) => {
|
|
|
693
693
|
padding: 10px;
|
|
694
694
|
}
|
|
695
695
|
</style>
|
|
696
|
-
<script type="module" src="https://unpkg.com/@hotwired/turbo@8.0.
|
|
696
|
+
<script type="module" src="https://unpkg.com/@hotwired/turbo@8.0.23/dist/turbo.es2017-esm.js"></script>
|
|
697
697
|
<script>
|
|
698
698
|
var eventSource = new EventSource('/sse');
|
|
699
699
|
eventSource.onmessage = function(event) {
|
|
@@ -720,4 +720,4 @@ app.listen(config.port);
|
|
|
720
720
|
```
|
|
721
721
|
## License
|
|
722
722
|
|
|
723
|
-
node-turbo is © 2024-
|
|
723
|
+
node-turbo is © 2024-2026 Walter Krivanek and released under the [MIT license](https://mit-license.org).
|
package/docs/API.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# node-turbo API documentation
|
|
2
2
|
|
|
3
|
-
Version 1.
|
|
3
|
+
Version 1.3.0
|
|
4
4
|
|
|
5
5
|
## Table of Contents
|
|
6
6
|
|
|
@@ -26,7 +26,7 @@ Version 1.2.7
|
|
|
26
26
|
- [turbostream.flush()](#turbostreamflush)
|
|
27
27
|
- [turbostream.custom(action, target, content)](#turbostreamcustomaction-target-content)
|
|
28
28
|
- [turbostream.customAll(action, targets, content)](#turbostreamcustomallaction-targets-content)
|
|
29
|
-
- [turbostream.
|
|
29
|
+
- [turbostream.createNodeStream(opts, streamOptions)](#turbostreamcreatenodestreamopts-streamoptions)
|
|
30
30
|
- [turbostream.append(targetOrAttributes, content)](#turbostreamappendtargetorattributes-content)
|
|
31
31
|
- [turbostream.appendAll(targetsOrAttributes, content)](#turbostreamappendalltargetsorattributes-content)
|
|
32
32
|
- [turbostream.prepend(targetOrAttributes, content)](#turbostreamprependtargetorattributes-content)
|
|
@@ -54,7 +54,7 @@ Version 1.2.7
|
|
|
54
54
|
- [turboelement.renderAttributesAsHtml()](#turboelementrenderattributesashtml)
|
|
55
55
|
- [turboelement.validate()](#turboelementvalidate)
|
|
56
56
|
- [turboelement.render()](#turboelementrender)
|
|
57
|
-
- [Class: TurboReadable](#class-turboreadable)
|
|
57
|
+
- [Class: TurboReadable (deprecated)](#class-turboreadable)
|
|
58
58
|
- [new TurboReadable(turboStream[, opts])](#new-turboreadableturbostream-opts)
|
|
59
59
|
- [turboreadable._turboStream](#turboreadable_turbostream)
|
|
60
60
|
- [turboreadable._boundPush](#turboreadable_boundpush)
|
|
@@ -92,7 +92,7 @@ Version 1.2.7
|
|
|
92
92
|
- [sseturbostream.eventName](#sseturbostreameventname)
|
|
93
93
|
- [sseturbostream.render()](#sseturbostreamrender)
|
|
94
94
|
- [sseturbostream.renderSseEvent(raw)](#sseturbostreamrendersseeventraw)
|
|
95
|
-
- [sseturbostream.
|
|
95
|
+
- [sseturbostream.createNodeStream()](#sseturbostreamcreatenodestream)
|
|
96
96
|
- [sseturbostream.event(eventName)](#sseturbostreameventeventname)
|
|
97
97
|
- [node-turbo/errors](#node-turboerrors)
|
|
98
98
|
- [Class: ValidationError](#class-validationerror)
|
|
@@ -271,16 +271,16 @@ Adds a Turbo Stream Element with a custom action, targeting multiple DOM element
|
|
|
271
271
|
|
|
272
272
|
Returns: {TurboStream} The instance for chaining.
|
|
273
273
|
|
|
274
|
-
#### turbostream.
|
|
274
|
+
#### turbostream.createNodeStream(opts, streamOptions)
|
|
275
275
|
|
|
276
276
|
- `opts` {Object<String, String>} The options for stream creation.
|
|
277
|
-
- `opts.continuous` {Boolean} If true, a
|
|
278
|
-
If false, a
|
|
277
|
+
- `opts.continuous` {Boolean} If true, a {node:stream~PassThrough} instance is returned.
|
|
278
|
+
If false, a {node:stream~Readable} created from the buffered items is returned.
|
|
279
279
|
- `streamOptions` {Object<String, String>} The options for the readable stream itself.
|
|
280
280
|
|
|
281
|
-
Creates a
|
|
281
|
+
Creates either a {node:stream~Readable} or {node:stream~PassThrough} stream instance.
|
|
282
282
|
|
|
283
|
-
Returns: {stream
|
|
283
|
+
Returns: {node:stream~Readable | {node:stream~PassThrough}} Either a readable stream or a TurboReadable instance.
|
|
284
284
|
|
|
285
285
|
#### turbostream.append(targetOrAttributes, content)
|
|
286
286
|
|
|
@@ -510,6 +510,9 @@ Render function to implement.
|
|
|
510
510
|
|
|
511
511
|
### Class: TurboReadable
|
|
512
512
|
|
|
513
|
+
> [!WARNING]
|
|
514
|
+
> Deprecated since v1.3.0!
|
|
515
|
+
|
|
513
516
|
```javascript
|
|
514
517
|
import { TurboReadable } from 'node-turbo';
|
|
515
518
|
```
|
|
@@ -688,7 +691,7 @@ Adds the element to the buffer, if config.buffer === true.
|
|
|
688
691
|
Fires the event 'element' with the added element.
|
|
689
692
|
> - clear()
|
|
690
693
|
Clears the buffer.
|
|
691
|
-
> -
|
|
694
|
+
> - createNodeStream(opts, streamOptions)
|
|
692
695
|
Creates a readable stream.
|
|
693
696
|
> - custom(action, target, content)
|
|
694
697
|
Adds a Turbo Stream Element with a custom action.
|
|
@@ -752,7 +755,7 @@ Adds the element to the buffer, if config.buffer === true.
|
|
|
752
755
|
Fires the event 'element' with the added element.
|
|
753
756
|
> - clear()
|
|
754
757
|
Clears the buffer.
|
|
755
|
-
> -
|
|
758
|
+
> - createNodeStream(opts, streamOptions)
|
|
756
759
|
Creates a readable stream.
|
|
757
760
|
> - custom(action, target, content)
|
|
758
761
|
Adds a Turbo Stream Element with a custom action.
|
|
@@ -851,7 +854,7 @@ Adds the element to the buffer, if config.buffer === true.
|
|
|
851
854
|
Fires the event 'element' with the added element.
|
|
852
855
|
> - clear()
|
|
853
856
|
Clears the buffer.
|
|
854
|
-
> -
|
|
857
|
+
> - createNodeStream(opts, streamOptions)
|
|
855
858
|
Creates a readable stream.
|
|
856
859
|
> - custom(action, target, content)
|
|
857
860
|
Adds a Turbo Stream Element with a custom action.
|
|
@@ -942,15 +945,15 @@ Returns: {String | null} The rendered SSE or null if there were no elements in t
|
|
|
942
945
|
|
|
943
946
|
- `raw` {String} The raw HTML string.
|
|
944
947
|
|
|
945
|
-
Takes
|
|
948
|
+
Takes an HTML fragment string and converts it to an SSE event message.
|
|
946
949
|
|
|
947
950
|
Returns: {String | null} The converted SSE event message or null if no string has been passed.
|
|
948
951
|
|
|
949
|
-
#### sseturbostream.
|
|
952
|
+
#### sseturbostream.createNodeStream()
|
|
950
953
|
|
|
951
|
-
Creates a {
|
|
954
|
+
Creates a {node:stream~PassThrough} instance, which passes Turbo Stream elements through as SSE messages.
|
|
952
955
|
|
|
953
|
-
Returns: {node:stream
|
|
956
|
+
Returns: {node:stream~PassThrough} The PassThrough stream instance.
|
|
954
957
|
|
|
955
958
|
#### sseturbostream.event(eventName)
|
|
956
959
|
|
|
@@ -1050,4 +1053,4 @@ Gets thrown when invalid attributes are discovered.
|
|
|
1050
1053
|
|
|
1051
1054
|
***
|
|
1052
1055
|
|
|
1053
|
-
node-turbo is © 2024-
|
|
1056
|
+
node-turbo is © 2024-2026 by Walter Krivanek and released under the [MIT license](https://mit-license.org).
|
package/docs/config.codekit3
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"uuidString" : "F605B793-549B-4143-ACBD-2C8AF906877B"
|
|
8
8
|
}
|
|
9
9
|
],
|
|
10
|
-
"creatorBuild" : "
|
|
10
|
+
"creatorBuild" : "34892",
|
|
11
11
|
"files" : {
|
|
12
12
|
"\/API.md" : {
|
|
13
13
|
"cB" : 0,
|
|
@@ -1327,8 +1327,6 @@
|
|
|
1327
1327
|
"buildFolderActive" : 0,
|
|
1328
1328
|
"buildFolderName" : "build",
|
|
1329
1329
|
"cleanBuild" : 1,
|
|
1330
|
-
"cssoForceMediaMerge" : 0,
|
|
1331
|
-
"cssoRestructure" : 1,
|
|
1332
1330
|
"environmentVariableEntries" : [
|
|
1333
1331
|
"NODE_ENV:::production"
|
|
1334
1332
|
],
|
|
@@ -1965,6 +1963,9 @@
|
|
|
1965
1963
|
"active" : 0,
|
|
1966
1964
|
"optionString" : "{'skipBlankLines': false, 'ignoreComments': false}"
|
|
1967
1965
|
},
|
|
1966
|
+
"no-unassigned-vars" : {
|
|
1967
|
+
"active" : 0
|
|
1968
|
+
},
|
|
1968
1969
|
"no-undef" : {
|
|
1969
1970
|
"active" : 1,
|
|
1970
1971
|
"optionString" : "{'typeof': false}"
|
|
@@ -2045,7 +2046,8 @@
|
|
|
2045
2046
|
"active" : 0
|
|
2046
2047
|
},
|
|
2047
2048
|
"no-useless-escape" : {
|
|
2048
|
-
"active" : 1
|
|
2049
|
+
"active" : 1,
|
|
2050
|
+
"optionString" : "{'allowRegexCharacters': []}"
|
|
2049
2051
|
},
|
|
2050
2052
|
"no-useless-rename" : {
|
|
2051
2053
|
"active" : 0,
|
|
@@ -98,8 +98,9 @@ export function turbochargeExpress(expressApp, opts) {
|
|
|
98
98
|
|
|
99
99
|
this.flushHeaders();
|
|
100
100
|
|
|
101
|
-
const ssets = new SseTurboStream();
|
|
102
|
-
const stream = ssets.
|
|
101
|
+
const ssets = new SseTurboStream();
|
|
102
|
+
const stream = ssets.createNodeStream();
|
|
103
|
+
this.req.on('close', () => stream.destroy());
|
|
103
104
|
stream.pipe(this);
|
|
104
105
|
|
|
105
106
|
return ssets;
|
|
@@ -108,9 +108,11 @@ export function turbochargeKoa(koaApp, opts = {}) {
|
|
|
108
108
|
|
|
109
109
|
// this.response.flushHeaders();
|
|
110
110
|
|
|
111
|
-
const
|
|
111
|
+
const
|
|
112
112
|
ssets = new SseTurboStream(),
|
|
113
|
-
readable = ssets.
|
|
113
|
+
readable = ssets.createNodeStream();
|
|
114
|
+
|
|
115
|
+
this.req.on('close', () => readable.destroy());
|
|
114
116
|
|
|
115
117
|
this.status = 200;
|
|
116
118
|
this.body = readable;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
|
|
2
|
-
import { TurboStream
|
|
3
|
-
import {
|
|
2
|
+
import { TurboStream } from '#core';
|
|
3
|
+
import { PassThrough } from 'node:stream';
|
|
4
|
+
import { deprecate } from 'node:util';
|
|
4
5
|
import db from 'debug';
|
|
5
6
|
const debug = db('node-turbo:sse');
|
|
6
7
|
|
|
@@ -70,7 +71,7 @@ export class SseTurboStream extends TurboStream {
|
|
|
70
71
|
|
|
71
72
|
|
|
72
73
|
/**
|
|
73
|
-
* Takes
|
|
74
|
+
* Takes an HTML fragment string and converts it to an SSE event message.
|
|
74
75
|
*
|
|
75
76
|
* @param {String} raw - The raw HTML string.
|
|
76
77
|
* @returns {String|null} The converted SSE event message or null if no string has been passed.
|
|
@@ -92,33 +93,51 @@ export class SseTurboStream extends TurboStream {
|
|
|
92
93
|
|
|
93
94
|
|
|
94
95
|
/**
|
|
95
|
-
* Creates a {
|
|
96
|
-
* SSE
|
|
96
|
+
* Creates a {node:stream.PassThrough} to which the the TurboStream elements
|
|
97
|
+
* are written as SSE event messages
|
|
97
98
|
*
|
|
98
99
|
* @returns {node:stream.Transform} The Transform stream instance.
|
|
99
|
-
* @since 1.
|
|
100
|
+
* @since 1.3.0
|
|
100
101
|
*/
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
}
|
|
102
|
+
createNodeStream() {
|
|
103
|
+
const stream = new PassThrough();
|
|
104
|
+
|
|
105
|
+
// Don't buffer Turbo Stream elements.
|
|
106
|
+
this.updateConfig({ buffer: false });
|
|
107
|
+
|
|
108
|
+
// Instead, immediately write them to the stream.
|
|
109
|
+
const elementListener = (el) => {
|
|
110
|
+
stream.write(this.renderSseEvent(el.render()));
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
this.on('element', elementListener);
|
|
114
|
+
|
|
115
|
+
stream.once('close', () => {
|
|
116
|
+
this.removeListener('element', elementListener)
|
|
118
117
|
});
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
|
|
119
|
+
// If we have previously buffered Turbo Stream elements,
|
|
120
|
+
// write them immediately.
|
|
121
|
+
if (this.length > 0) {
|
|
122
|
+
this.elements.forEach((el) => this.emit('element', el));
|
|
123
|
+
this.clear();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return stream;
|
|
121
127
|
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Creates a {node:stream.PassThrough} to which the the TurboStream elements
|
|
132
|
+
* are written as SSE event messages
|
|
133
|
+
*
|
|
134
|
+
* @returns {node:stream.Transform} The Transform stream instance.
|
|
135
|
+
* @since 1.0.0
|
|
136
|
+
* @deprecated since 1.3.0
|
|
137
|
+
*/
|
|
138
|
+
createReadableStream = deprecate(() => {
|
|
139
|
+
return this.createNodeStream();
|
|
140
|
+
}, 'sseTurboStream.createReadableStream() is deprecated. Use sseTurboStream.createNodeStream() instead.');
|
|
122
141
|
|
|
123
142
|
|
|
124
143
|
/**
|
package/lib/turbo-readable.js
CHANGED
|
@@ -11,6 +11,7 @@ const debug = db('node-turbo:readable');
|
|
|
11
11
|
*
|
|
12
12
|
* @extends {node:stream~Readable}
|
|
13
13
|
* @since 1.0.0
|
|
14
|
+
* @deprecated since 1.3.8
|
|
14
15
|
*/
|
|
15
16
|
export class TurboReadable extends Readable {
|
|
16
17
|
|
|
@@ -101,19 +102,11 @@ export class TurboReadable extends Readable {
|
|
|
101
102
|
* @param {Error} err - The error object, if thrown.
|
|
102
103
|
* @since 1.0.0
|
|
103
104
|
*/
|
|
104
|
-
_destroy(err) {
|
|
105
|
+
_destroy(err, callback) {
|
|
105
106
|
debug('_destroy()', err);
|
|
106
107
|
this._turboStream.removeListener('element', this._boundPush);
|
|
107
108
|
this._turboStream.updateConfig({ buffer: true });
|
|
108
|
-
|
|
109
|
-
// if (typeof callback === 'function') {
|
|
110
|
-
// if (err) {
|
|
111
|
-
// callback(err);
|
|
112
|
-
// }
|
|
113
|
-
// else {
|
|
114
|
-
// callback();
|
|
115
|
-
// }
|
|
116
|
-
// }
|
|
109
|
+
callback(err);
|
|
117
110
|
}
|
|
118
111
|
|
|
119
112
|
|
package/lib/turbo-stream.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
import { EventEmitter } from 'node:events';
|
|
3
|
-
import { TurboStreamElement
|
|
4
|
-
import {
|
|
3
|
+
import { TurboStreamElement } from '#core';
|
|
4
|
+
import { Readable, PassThrough } from 'node:stream';
|
|
5
5
|
import { deprecate } from 'node:util';
|
|
6
6
|
import db from 'debug';
|
|
7
7
|
const debug = db('node-turbo:turbostream');
|
|
@@ -271,28 +271,67 @@ export class TurboStream extends EventEmitter {
|
|
|
271
271
|
|
|
272
272
|
|
|
273
273
|
/**
|
|
274
|
-
* Creates a
|
|
275
|
-
*
|
|
274
|
+
* Creates a {node:stream.PassThrough} stream to which the the TurboStream elements
|
|
275
|
+
* are written.
|
|
276
|
+
*
|
|
276
277
|
* @param {Object<String, String>} opts - The options for stream creation.
|
|
277
|
-
* @param {Boolean} opts.continuous - If true, a
|
|
278
|
-
* If false, a readable stream created from the buffered
|
|
278
|
+
* @param {Boolean} opts.continuous - If true, a {node:stream.PassThrough} is returned that
|
|
279
|
+
* receives elements as they are added. If false, a readable stream created from the buffered
|
|
280
|
+
* items is returned.
|
|
279
281
|
* @param {Object<String, String>} streamOptions - The options for the readable stream itself.
|
|
280
|
-
* @returns {stream.Readable
|
|
282
|
+
* @returns {node:stream.PassThrough|stream.Readable} Either a PassThrough stream or a Readable.from() instance.
|
|
281
283
|
* @since 1.0.0
|
|
282
284
|
*/
|
|
283
|
-
|
|
285
|
+
createNodeStream(opts, streamOptions = {}) {
|
|
284
286
|
opts = Object.assign({}, { continuous: true }, opts);
|
|
285
287
|
debug('createReadableStream()', opts, streamOptions);
|
|
286
288
|
|
|
287
289
|
if (opts.continuous === true) {
|
|
288
|
-
debug(' returns
|
|
289
|
-
|
|
290
|
+
debug(' returns PassThrough stream');
|
|
291
|
+
|
|
292
|
+
const stream = new PassThrough(streamOptions);
|
|
293
|
+
|
|
294
|
+
// Don't buffer Turbo Stream elements.
|
|
295
|
+
this.updateConfig({ buffer: false });
|
|
296
|
+
|
|
297
|
+
// Instead, immediately write them to the stream.
|
|
298
|
+
const elementListener = (el) => {
|
|
299
|
+
stream.write(el.render());
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
this.on('element', elementListener);
|
|
303
|
+
|
|
304
|
+
stream.once('close', () => {
|
|
305
|
+
this.removeListener('element', elementListener);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// If we have previously buffered Turbo Stream elements,
|
|
309
|
+
// write them immediately.
|
|
310
|
+
if (this.length > 0) {
|
|
311
|
+
this.elements.forEach((el) => this.emit('element', el));
|
|
312
|
+
this.clear();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return stream;
|
|
290
316
|
}
|
|
291
317
|
else {
|
|
292
318
|
debug(' returns Readable.from()');
|
|
293
|
-
return Readable.from(this.length > 0 ? this.render().split('\n') : [], streamOptions);
|
|
319
|
+
return Readable.from((this.length > 0) ? this.render().split('\n') : [], streamOptions);
|
|
294
320
|
}
|
|
295
321
|
}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Creates a {node:stream.PassThrough} stream to which the the TurboStream elements
|
|
326
|
+
* are written.
|
|
327
|
+
*
|
|
328
|
+
* @returns {node:stream.Transform} The Transform stream instance.
|
|
329
|
+
* @since 1.0.0
|
|
330
|
+
* @deprecated since 1.3.0
|
|
331
|
+
*/
|
|
332
|
+
createReadableStream = deprecate((opts, streamOptions = {}) => {
|
|
333
|
+
return this.createNodeStream(opts, streamOptions);
|
|
334
|
+
}, 'turboStream.createReadableStream() is deprecated. Use turboStream.createNodeStream() instead.')
|
|
296
335
|
|
|
297
336
|
|
|
298
337
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-turbo",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "A library for Node.js to assist with the server side of 37signals' Hotwire Turbo framework. It provides classes and functions for Web servers and also convenience functions for the frameworks Koa and Express as well as for WebSocket and SSE.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node",
|
|
@@ -64,23 +64,23 @@
|
|
|
64
64
|
"negotiator": "^1.0.0"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
|
-
"@hotwired/turbo": "^8.0.
|
|
68
|
-
"@koa/bodyparser": "^6.
|
|
69
|
-
"@playwright/test": "^1.
|
|
70
|
-
"c8": "^
|
|
71
|
-
"chai": "^6.2.
|
|
67
|
+
"@hotwired/turbo": "^8.0.23",
|
|
68
|
+
"@koa/bodyparser": "^6.1.0",
|
|
69
|
+
"@playwright/test": "^1.58.2",
|
|
70
|
+
"c8": "^11.0.0",
|
|
71
|
+
"chai": "^6.2.2",
|
|
72
72
|
"chai-spies": "^1.1.0",
|
|
73
73
|
"colorette": "^2.0.20",
|
|
74
|
-
"eventsource": "^4.
|
|
75
|
-
"express": "^5.1
|
|
74
|
+
"eventsource": "^4.1.0",
|
|
75
|
+
"express": "^5.2.1",
|
|
76
76
|
"git-repo-info": "^2.1.1",
|
|
77
|
-
"koa": "^3.
|
|
77
|
+
"koa": "^3.1.2",
|
|
78
78
|
"koa-route": "^4.0.1",
|
|
79
79
|
"koa-static": "^5.0.0",
|
|
80
|
-
"mocha": "^11.7.
|
|
80
|
+
"mocha": "^11.7.5",
|
|
81
81
|
"node-mocks-http": "^1.17.2",
|
|
82
82
|
"nunjucks": "^3.2.4",
|
|
83
|
-
"supertest": "^7.
|
|
84
|
-
"ws": "^8.
|
|
83
|
+
"supertest": "^7.2.2",
|
|
84
|
+
"ws": "^8.19.0"
|
|
85
85
|
}
|
|
86
86
|
}
|
|
@@ -4,13 +4,14 @@ import route from 'koa-route';
|
|
|
4
4
|
import serve from 'koa-static';
|
|
5
5
|
import bodyParser from '@koa/bodyparser';
|
|
6
6
|
import { turbochargeKoa } from '#koa';
|
|
7
|
-
import { SseTurboStream } from '#sse';
|
|
8
7
|
|
|
9
8
|
const app = new Koa();
|
|
10
9
|
let ssets;
|
|
11
10
|
|
|
12
|
-
app.on('error', (err
|
|
13
|
-
|
|
11
|
+
app.on('error', (err) => {
|
|
12
|
+
if (err?.code !== 'ERR_STREAM_PREMATURE_CLOSE') {
|
|
13
|
+
console.error('Error in Koa:', err);
|
|
14
|
+
}
|
|
14
15
|
});
|
|
15
16
|
|
|
16
17
|
// Add node-turbo functions to Koa context.
|
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
|
|
2
2
|
import { expect } from '../chai.js';
|
|
3
|
-
import request from 'supertest';
|
|
4
3
|
import Koa from 'koa';
|
|
5
4
|
import { EventSource } from 'eventsource';
|
|
6
|
-
import { TurboStream } from '#core';
|
|
7
5
|
import { SseTurboStream } from '#sse';
|
|
8
|
-
import { PassThrough } from 'node:stream';
|
|
9
6
|
|
|
7
|
+
|
|
10
8
|
const port = 8888;
|
|
11
9
|
|
|
10
|
+
|
|
12
11
|
describe('SSE integration', function() {
|
|
13
12
|
|
|
14
13
|
before(function() {
|
|
14
|
+
this.readable = null;
|
|
15
|
+
this.eventSource = null;
|
|
16
|
+
this.server = null;
|
|
15
17
|
this.app = new Koa();
|
|
16
18
|
this.sseTurboStream = new SseTurboStream();
|
|
17
|
-
|
|
19
|
+
|
|
18
20
|
this.app.on('error', (err, ctx) => {
|
|
19
|
-
|
|
21
|
+
// ERR_STREAM_PREMATURE_CLOSE is expected when the SSE client disconnects.
|
|
22
|
+
// Also @see: {@link https://github.com/koajs/koa/pull/612|Koa GitHub Issue 612}
|
|
23
|
+
if (err?.code !== 'ERR_STREAM_PREMATURE_CLOSE') {
|
|
24
|
+
console.error(err);
|
|
25
|
+
}
|
|
20
26
|
});
|
|
21
27
|
|
|
22
28
|
this.app.use(async (ctx, next) => {
|
|
@@ -29,52 +35,58 @@ describe('SSE integration', function() {
|
|
|
29
35
|
ctx.req.socket.setKeepAlive(true);
|
|
30
36
|
|
|
31
37
|
ctx.set({
|
|
38
|
+
'Content-Type': SseTurboStream.MIME_TYPE,
|
|
32
39
|
'Cache-Control': 'no-cache',
|
|
33
|
-
'Connection': 'keep-alive'
|
|
40
|
+
'Connection': 'keep-alive'
|
|
34
41
|
});
|
|
35
42
|
|
|
36
|
-
this.readable = this.sseTurboStream.
|
|
43
|
+
this.readable = this.sseTurboStream.createNodeStream();
|
|
44
|
+
ctx.req.on('close', () => this.readable.destroy());
|
|
37
45
|
|
|
38
|
-
ctx.type = SseTurboStream.MIME_TYPE;
|
|
39
46
|
ctx.status = 200;
|
|
40
47
|
ctx.body = this.readable;
|
|
41
48
|
});
|
|
42
|
-
|
|
49
|
+
|
|
43
50
|
this.server = this.app.listen(port);
|
|
44
51
|
});
|
|
45
52
|
|
|
46
53
|
|
|
47
54
|
after(function() {
|
|
48
|
-
this.
|
|
49
|
-
this.eventSource.close();
|
|
55
|
+
this.server.closeAllConnections();
|
|
50
56
|
this.server.close();
|
|
51
57
|
});
|
|
52
58
|
|
|
53
59
|
|
|
54
|
-
it('Turbo Stream messages get sent to EventSource as SSE messages', function(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
this.eventSource = new EventSource(`http://localhost:${port}/sse`);
|
|
58
|
-
|
|
59
|
-
this.eventSource.addEventListener('error', e => {
|
|
60
|
-
expect.fail('An error occurred while attempting to connect');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
this.eventSource.addEventListener('message', e => {
|
|
60
|
+
it('Turbo Stream messages get sent to EventSource as SSE messages', function() {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const messages = [];
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
expect(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
this.eventSource = new EventSource(`http://localhost:${port}/sse`);
|
|
65
|
+
|
|
66
|
+
this.eventSource.addEventListener('error', (e) => {
|
|
67
|
+
expect.fail(e.message);
|
|
68
|
+
reject(e);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
this.eventSource.addEventListener('message', (e) => {
|
|
72
|
+
messages.push(e);
|
|
73
|
+
|
|
74
|
+
if (messages.length === 2) {
|
|
75
|
+
expect(messages[0].type).to.equal('message');
|
|
76
|
+
expect(messages[0].data).to.equal('<turbo-stream action="append" target="t1"><template>c1</template></turbo-stream>');
|
|
77
|
+
expect(messages[1].type).to.equal('message');
|
|
78
|
+
expect(messages[1].data).to.equal('<turbo-stream action="replace" target="t2"><template>c2</template></turbo-stream>');
|
|
79
|
+
|
|
80
|
+
this.eventSource.close();
|
|
81
|
+
resolve();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Send Turbo Stream messages.
|
|
86
|
+
this.sseTurboStream
|
|
87
|
+
.append('t1', 'c1')
|
|
88
|
+
.replace('t2', 'c2');
|
|
72
89
|
});
|
|
73
|
-
|
|
74
|
-
// Send Turbo Stream messages.
|
|
75
|
-
this.sseTurboStream.append('t1', 'c1');
|
|
76
|
-
this.sseTurboStream.replace('t2', 'c2');
|
|
77
90
|
});
|
|
78
91
|
|
|
79
|
-
|
|
80
92
|
});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
|
|
2
2
|
import { expect } from '../chai.js';
|
|
3
|
-
import request from 'supertest';
|
|
4
3
|
import { TurboStream } from '#core';
|
|
5
4
|
import { WsTurboStream } from '#ws';
|
|
6
5
|
import { WebSocketServer, WebSocket, createWebSocketStream } from 'ws';
|
|
@@ -113,7 +112,7 @@ describe('WebSocket integration', function() {
|
|
|
113
112
|
const messages = [];
|
|
114
113
|
|
|
115
114
|
// We're expecting one message with two elements.
|
|
116
|
-
ws.on('message', (data
|
|
115
|
+
ws.on('message', (data) => {
|
|
117
116
|
messages.push(data.toString());
|
|
118
117
|
if (messages.length === 2) {
|
|
119
118
|
expect(messages[0]).to.equal('<turbo-stream action="append" target="t1"><template>c1</template></turbo-stream>');
|
|
@@ -127,7 +126,7 @@ describe('WebSocket integration', function() {
|
|
|
127
126
|
ws.once('open', () => {
|
|
128
127
|
this.wss.clients.forEach(ws => {
|
|
129
128
|
const ts = new TurboStream();
|
|
130
|
-
const readable = ts.
|
|
129
|
+
const readable = ts.createNodeStream();
|
|
131
130
|
|
|
132
131
|
readable.on('error', err => {
|
|
133
132
|
return reject(err);
|
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
|
|
2
2
|
import { expect, spy } from '../../chai.js';
|
|
3
|
-
import { TurboStream,
|
|
4
|
-
import { Readable } from 'node:stream';
|
|
3
|
+
import { TurboStream, TurboStreamElement, TurboReadable } from '#core';
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
action: 'a',
|
|
8
|
-
target: 't'
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
describe('TurboReadable', function() {
|
|
5
|
+
describe('TurboReadable (deprecated)', function() {
|
|
12
6
|
|
|
13
7
|
before(function() {
|
|
14
8
|
this.ts = new TurboStream()
|
|
15
9
|
this.readable = new TurboReadable(this.ts);
|
|
16
10
|
});
|
|
17
11
|
|
|
12
|
+
after(function() {
|
|
13
|
+
this.readable.destroy();
|
|
14
|
+
});
|
|
18
15
|
|
|
19
16
|
beforeEach(function() {
|
|
20
17
|
spy.on(this.readable, 'push');
|
|
@@ -68,11 +65,11 @@ describe('TurboReadable', function() {
|
|
|
68
65
|
});
|
|
69
66
|
|
|
70
67
|
|
|
71
|
-
it('_destroy() gets called when
|
|
72
|
-
const readable =
|
|
68
|
+
it('_destroy() gets called when stream is destroyed', function() {
|
|
69
|
+
const readable = new TurboReadable(new TurboStream());
|
|
70
|
+
|
|
73
71
|
spy.on(readable, '_destroy');
|
|
74
72
|
readable.destroy();
|
|
75
|
-
|
|
76
73
|
expect(readable._destroy).to.have.been.called();
|
|
77
74
|
spy.restore(readable, '_destroy');
|
|
78
75
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
import { expect } from '../../chai.js';
|
|
3
|
-
import { TurboStream, TurboElement, TurboStreamElement
|
|
4
|
-
import { Readable } from 'node:stream';
|
|
3
|
+
import { TurboStream, TurboElement, TurboStreamElement } from '#core';
|
|
4
|
+
import { Readable, PassThrough } from 'node:stream';
|
|
5
5
|
|
|
6
6
|
const attr = {
|
|
7
7
|
action: 'a',
|
|
@@ -379,30 +379,100 @@ describe('TurboStream', function() {
|
|
|
379
379
|
expect(ts.elements[0].attributes.action).to.equal('morph');
|
|
380
380
|
});
|
|
381
381
|
|
|
382
|
+
it ('createReadableStream() warns about deprecation.', function() {
|
|
383
|
+
return new Promise(resolve => {
|
|
384
|
+
process.once('warning', err => {
|
|
385
|
+
expect(err).to.be.an('error');
|
|
386
|
+
expect(err.message).to.include('createReadableStream() is deprecated');
|
|
387
|
+
resolve();
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const ts = new TurboStream();
|
|
391
|
+
ts.createReadableStream();
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
382
395
|
});
|
|
383
396
|
});
|
|
384
397
|
|
|
385
398
|
|
|
386
|
-
describe('
|
|
399
|
+
describe('createNodeStream(opts)', function() {
|
|
387
400
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const readable = ts.createReadableStream({ continuous: false });
|
|
397
|
-
expect(readable).not.to.be.an.instanceof(TurboReadable);
|
|
398
|
-
expect(readable).to.be.an.instanceof(Readable);
|
|
401
|
+
describe('opts.continuous = true', function() {
|
|
402
|
+
|
|
403
|
+
it('returns PassThrough stream', function() {
|
|
404
|
+
const ts = new TurboStream();
|
|
405
|
+
const readable = ts.createNodeStream();
|
|
406
|
+
expect(readable).to.be.an.instanceof(PassThrough);
|
|
407
|
+
});
|
|
408
|
+
|
|
399
409
|
});
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
describe('opts.continuous = false', function() {
|
|
413
|
+
|
|
414
|
+
it('returns new Readable()', function() {
|
|
415
|
+
const ts = new TurboStream();
|
|
416
|
+
const readable = ts.createNodeStream({ continuous: false });
|
|
417
|
+
expect(readable).not.to.be.an.instanceof(PassThrough);
|
|
418
|
+
expect(readable).to.be.an.instanceof(Readable);
|
|
419
|
+
readable.destroy();
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('emits event \'element\' for existing Turbo Stream elements', function() {
|
|
423
|
+
return new Promise(resolve => {
|
|
424
|
+
const ts = new TurboStream().append('t', 'c');
|
|
400
425
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
426
|
+
ts.on('element', (el) => {
|
|
427
|
+
expect(el).to.be.an.instanceof(TurboStreamElement);
|
|
428
|
+
|
|
429
|
+
resolve();
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
const readable = ts.createNodeStream();
|
|
433
|
+
readable.destroy();
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('Readable stream includes previously added elements (rendered)', function() {
|
|
438
|
+
const ts = new TurboStream()
|
|
439
|
+
.append('t', 'c')
|
|
440
|
+
.append('t2', 'c2');
|
|
441
|
+
|
|
442
|
+
const readable = ts.createNodeStream({ continuous: false });
|
|
443
|
+
readable.setEncoding('utf8');
|
|
444
|
+
readable.pause();
|
|
445
|
+
|
|
446
|
+
let
|
|
447
|
+
el = '',
|
|
448
|
+
str = '';
|
|
449
|
+
|
|
450
|
+
while ((el = readable.read()) !== null) {
|
|
451
|
+
str += el;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
readable.destroy();
|
|
455
|
+
expect(str).to.equal('<turbo-stream action="append" target="t"><template>c</template></turbo-stream><turbo-stream action="append" target="t2"><template>c2</template></turbo-stream>');
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('returns new Readable(), even if empty', function() {
|
|
459
|
+
const ts = new TurboStream();
|
|
460
|
+
const readable = ts.createNodeStream({ continuous: false });
|
|
461
|
+
expect(readable).not.to.be.an.instanceof(PassThrough);
|
|
462
|
+
expect(readable).to.be.an.instanceof(Readable);
|
|
463
|
+
readable.destroy();
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('Readable stream is empty, if TurboStream is empty', function() {
|
|
467
|
+
const ts = new TurboStream();
|
|
468
|
+
|
|
469
|
+
const readable = ts.createNodeStream({ continuous: false });
|
|
470
|
+
readable.setEncoding('utf8');
|
|
471
|
+
readable.pause();
|
|
472
|
+
expect(readable.read()).to.be.null;
|
|
473
|
+
readable.destroy();
|
|
474
|
+
});
|
|
475
|
+
|
|
406
476
|
});
|
|
407
477
|
|
|
408
478
|
});
|
|
@@ -60,18 +60,26 @@ describe('SseTurboStream', function() {
|
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
|
|
63
|
-
it('
|
|
63
|
+
it('createNodeStream() returns Transform', function() {
|
|
64
64
|
const sseSt = new SseTurboStream();
|
|
65
|
-
const readable = sseSt.
|
|
65
|
+
const readable = sseSt.createNodeStream();
|
|
66
66
|
|
|
67
67
|
expect(readable).to.be.an.instanceof(Transform);
|
|
68
68
|
});
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
it('createReadableStream() (deprecated) returns Transform', function() {
|
|
72
|
+
const sseSt = new SseTurboStream();
|
|
73
|
+
|
|
74
|
+
const readable = sseSt.createReadableStream();
|
|
75
|
+
expect(readable).to.be.an.instanceof(Transform);
|
|
76
|
+
});
|
|
69
77
|
|
|
70
78
|
|
|
71
79
|
it('SSE message gets written to stream', function() {
|
|
72
80
|
return new Promise((resolve, reject) => {
|
|
73
81
|
const sseSt = new SseTurboStream();
|
|
74
|
-
const readable = sseSt.
|
|
82
|
+
const readable = sseSt.createNodeStream();
|
|
75
83
|
|
|
76
84
|
readable.on('data', chunk => {
|
|
77
85
|
expect(chunk.toString()).to.equal('data: <turbo-stream action="append" target="t"><template>c</template></turbo-stream>\n\n');
|
|
@@ -91,7 +99,7 @@ describe('SseTurboStream', function() {
|
|
|
91
99
|
it('SSE message with event name gets written to stream', function() {
|
|
92
100
|
return new Promise((resolve, reject) => {
|
|
93
101
|
const sseSt = new SseTurboStream('my-event');
|
|
94
|
-
const readable = sseSt.
|
|
102
|
+
const readable = sseSt.createNodeStream();
|
|
95
103
|
|
|
96
104
|
readable.on('data', chunk => {
|
|
97
105
|
expect(chunk.toString()).to.equal('event: my-event\ndata: <turbo-stream action="append" target="t"><template>c</template></turbo-stream>\n\n');
|