@jk2908/solas 0.13.0 → 0.13.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.2
4
+
5
+ ### Changed
6
+
7
+ - The Node adapter's response-stream loop and close handlers use declarations and `while (true)` for readability. No behaviour change.
8
+
9
+ ## 0.13.1
10
+
11
+ ### Fixes
12
+
13
+ - The Node adapter now aborts `request.signal` when the client disconnects, so streaming endpoints such as SSE can stop their producers.
14
+ - The Node adapter flushes response headers before streaming the body, so a delayed first chunk no longer leaves the client waiting with no response.
15
+ - The Node adapter cancels the response stream when the client disconnects, instead of continuing to write into a dead socket.
16
+
3
17
  ## 0.13.0
4
18
 
5
19
  ### Features
@@ -1,4 +1,3 @@
1
- import { once } from 'node:events';
2
1
  import http from 'node:http';
3
2
  import path from 'node:path';
4
3
  import { pathToFileURL } from 'node:url';
@@ -38,26 +37,59 @@ function nodeToWeb(req) {
38
37
  */
39
38
  async function webToNode(stream, res) {
40
39
  const reader = stream.getReader();
40
+ let closed = false;
41
+ // a client that goes away leaves nothing to write to; cancel the source so
42
+ // its producer stops instead of buffering into a dead socket
43
+ function onClose() {
44
+ closed = true;
45
+ void reader.cancel().catch(() => { });
46
+ }
47
+ res.once('close', onClose);
41
48
  try {
42
49
  while (true) {
50
+ if (closed)
51
+ break;
43
52
  const { done, value } = await reader.read();
44
53
  if (done)
45
54
  break;
46
- // `write` returns false when the socket buffer is full. That is not an
47
- // error: wait for the `'drain'` event before writing more, otherwise a
48
- // fast producer and a slow client would buffer the whole body in memory
49
- if (!res.write(value))
50
- await once(res, 'drain');
55
+ if (closed || res.destroyed || res.writableEnded)
56
+ break;
57
+ try {
58
+ // `write` returns false when the socket buffer is full. That is not an
59
+ // error: wait for the `'drain'` event before writing more, otherwise a
60
+ // fast producer and a slow client would buffer the whole body in memory
61
+ if (!res.write(value)) {
62
+ await new Promise(resolve => {
63
+ function settle() {
64
+ res.off('drain', settle);
65
+ res.off('close', settle);
66
+ resolve();
67
+ }
68
+ res.on('drain', settle);
69
+ res.on('close', settle);
70
+ });
71
+ }
72
+ }
73
+ catch {
74
+ // the socket died between the checks above
75
+ break;
76
+ }
51
77
  }
52
78
  }
53
79
  finally {
54
- reader.releaseLock();
80
+ res.off('close', onClose);
81
+ try {
82
+ reader.releaseLock();
83
+ }
84
+ catch {
85
+ // a pending read can outlive a cancel by a tick
86
+ }
55
87
  }
56
88
  }
57
89
  /**
58
90
  * Convert a Node request into a fetch `Request`, streaming the body when present.
59
91
  */
60
- function toRequest(req) {
92
+ function toRequest(req, signal) {
61
93
  const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
62
94
  const headers = new Headers();
63
95
  for (const [key, value] of Object.entries(req.headers)) {
@@ -77,6 +109,7 @@ function toRequest(req) {
77
109
  headers,
78
110
  body: hasBody ? nodeToWeb(req) : undefined,
79
111
  duplex: 'half',
112
+ signal,
80
113
  };
81
114
  return new Request(url, init);
82
115
  }
@@ -99,8 +132,12 @@ async function sendResponse(res, response) {
99
132
  res.end();
100
133
  return;
101
134
  }
135
+ // send headers now so a delayed first chunk (SSE, slow streams) does not
136
+ // leave the client waiting with no response
137
+ res.flushHeaders();
102
138
  await webToNode(response.body, res);
103
- res.end();
139
+ if (!res.destroyed)
140
+ res.end();
104
141
  }
105
142
  /**
106
143
  * Start a production HTTP server for a Node (or Bun) deployment.
@@ -121,16 +158,31 @@ async function adapter(options = {}) {
121
158
  const host = options.host ?? '0.0.0.0';
122
159
  const handler = await loadHandler(entry);
123
160
  const server = http.createServer(async (req, res) => {
161
+ const abort = new AbortController();
162
+ // `close` before the response has finished means the client went away;
163
+ // surface that through `req.signal` so long-lived work can stop
164
+ function onClose() {
165
+ if (!res.writableEnded)
166
+ abort.abort(new Error('client disconnected'));
167
+ }
168
+ res.once('close', onClose);
124
169
  try {
125
- const response = await handler.fetch(toRequest(req));
170
+ const response = await handler.fetch(toRequest(req, abort.signal));
126
171
  await sendResponse(res, response);
127
172
  }
128
173
  catch (err) {
129
174
  console.error('[solas] request failed', err);
175
+ if (res.headersSent) {
176
+ res.destroy();
177
+ return;
178
+ }
130
179
  res.statusCode = 500;
131
180
  res.setHeader('content-type', 'text/plain; charset=utf-8');
132
181
  res.end('Internal Server Error');
133
182
  }
183
+ finally {
184
+ res.off('close', onClose);
185
+ }
134
186
  });
135
187
  await new Promise(resolve => server.listen(port, host, resolve));
136
188
  options.onListen?.({ port, host });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jk2908/solas",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "description": "A Vite + React meta-framework exploring streaming, Server Components, and partial prerendering. Designed for simplicity and lightness",
5
5
  "keywords": [
6
6
  "framework",