@jsenv/server 15.5.3 → 16.0.1

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 (55) hide show
  1. package/README.md +79 -86
  2. package/dist/html/4xx.html +196 -0
  3. package/dist/html/500.html +17 -0
  4. package/dist/html/route_inspector.html +2435 -0
  5. package/dist/js/formidable.js +17 -0
  6. package/dist/js/vendors.js +2001 -0
  7. package/dist/js/ws.js +6863 -0
  8. package/dist/jsenv_server.js +8829 -0
  9. package/package.json +19 -9
  10. package/src/content_negotiation/pick_content_version.js +63 -0
  11. package/src/{fetch_filesystem.js → filesystem/fetch_filesystem.js} +57 -72
  12. package/src/{serve_directory.js → filesystem/serve_directory.js} +1 -2
  13. package/src/interfacing_with_node/from_node_request.js +455 -0
  14. package/src/interfacing_with_node/get_observable_value_type.js +43 -0
  15. package/src/interfacing_with_node/observable.js +91 -75
  16. package/src/interfacing_with_node/observable_from.js +34 -0
  17. package/src/interfacing_with_node/observable_from_file_handle.js +30 -0
  18. package/src/interfacing_with_node/observable_from_node_stream.js +44 -46
  19. package/src/interfacing_with_node/observable_from_node_web_readable_stream.js +32 -0
  20. package/src/interfacing_with_node/observable_from_promise.js +13 -0
  21. package/src/interfacing_with_node/write_node_response.js +80 -35
  22. package/src/internal/colorizeResponseStatus.js +2 -0
  23. package/src/internal/headers_composition.js +42 -15
  24. package/src/internal/multiple-header.js +27 -12
  25. package/src/internal/object_composition.js +2 -2
  26. package/src/internal/response_composition.js +16 -3
  27. package/src/main.js +15 -11
  28. package/src/progressive_response.js +38 -0
  29. package/src/replace_placeholder_in_html.js +12 -0
  30. package/src/request_body_handling.js +22 -0
  31. package/src/router/router.js +878 -0
  32. package/src/server_events.js +431 -0
  33. package/src/service_controller.js +79 -73
  34. package/src/services/autoreload_on_server_restart/client/autoreload_on_server_restart.js +125 -0
  35. package/src/services/autoreload_on_server_restart/jsenv_service_autoreload_on_server_restart.js +45 -0
  36. package/src/services/cors/jsenv_service_cors.js +10 -18
  37. package/src/services/default_body_4xx_5xx/client/4xx.html +65 -0
  38. package/src/services/default_body_4xx_5xx/jsenv_service_default_body_4xx_5xx.js +82 -0
  39. package/src/services/error_handler/client/500.html +17 -0
  40. package/src/services/error_handler/jsenv_service_error_handler.js +47 -28
  41. package/src/services/internal_client_files/jsenv_service_internal_client_files.js +34 -0
  42. package/src/services/open_file/jsenv_service_open_file.js +37 -0
  43. package/src/services/route_inspector/client/code_block.js +215 -0
  44. package/src/services/route_inspector/client/route_inspector.html +1363 -0
  45. package/src/services/route_inspector/jsenv_service_route_inspector.js +39 -0
  46. package/src/start_server.js +575 -607
  47. package/src/web_socket_response.js +53 -0
  48. package/src/from_fetch_response.js +0 -15
  49. package/src/interfacing_with_node/body.js +0 -81
  50. package/src/interfacing_with_node/request_factory.js +0 -125
  51. package/src/readRequestBody.js +0 -32
  52. package/src/server_timing/timing_measure.js +0 -26
  53. package/src/service_composition/path_to_regexp.js +0 -436
  54. package/src/service_composition/routing.js +0 -31
  55. package/src/sse/sse_room.js +0 -313
@@ -1,3 +1,5 @@
1
+ // https://wicg.github.io/observable/#core-infrastructure
2
+
1
3
  if ("observable" in Symbol === false) {
2
4
  Symbol.observable = Symbol.for("observable");
3
5
  }
@@ -9,40 +11,73 @@ export const createObservable = (producer) => {
9
11
 
10
12
  const observable = {
11
13
  [Symbol.observable]: () => observable,
12
- subscribe: ({
13
- next = () => {},
14
- error = (value) => {
15
- throw value;
14
+ subscribe: (
15
+ {
16
+ next = () => {},
17
+ error = (value) => {
18
+ throw value;
19
+ },
20
+ complete = () => {},
16
21
  },
17
- complete = () => {},
18
- }) => {
22
+ { signal = new AbortController().signal } = {},
23
+ ) => {
19
24
  let cleanup = () => {};
20
25
  const subscription = {
21
- closed: false,
26
+ active: true,
27
+ signal,
22
28
  unsubscribe: () => {
23
29
  subscription.closed = true;
24
30
  cleanup();
25
31
  },
26
32
  };
27
33
 
34
+ const teardownCallbackList = [];
35
+ const close = () => {
36
+ subscription.active = false;
37
+ let i = teardownCallbackList.length;
38
+ while (i--) {
39
+ teardownCallbackList[i]();
40
+ }
41
+ teardownCallbackList.length = 0;
42
+ };
43
+
44
+ signal.addEventListener("abort", () => {
45
+ close();
46
+ });
47
+
28
48
  const producerReturnValue = producer({
29
49
  next: (value) => {
30
- if (subscription.closed) return;
50
+ if (!subscription.active) {
51
+ return;
52
+ }
31
53
  next(value);
32
54
  },
33
55
  error: (value) => {
34
- if (subscription.closed) return;
56
+ if (!subscription.active) {
57
+ return;
58
+ }
35
59
  error(value);
60
+ close();
36
61
  },
37
62
  complete: () => {
38
- if (subscription.closed) return;
63
+ if (!subscription.active) {
64
+ return;
65
+ }
39
66
  complete();
67
+ close();
68
+ },
69
+ addTeardown: (teardownCallback) => {
70
+ if (!subscription.active) {
71
+ teardownCallback();
72
+ return;
73
+ }
74
+ teardownCallbackList.push(teardownCallback);
40
75
  },
41
76
  });
42
77
  if (typeof producerReturnValue === "function") {
43
78
  cleanup = producerReturnValue;
44
79
  }
45
- return subscription;
80
+ return undefined;
46
81
  },
47
82
  };
48
83
 
@@ -61,47 +96,27 @@ export const isObservable = (value) => {
61
96
  return false;
62
97
  };
63
98
 
64
- export const observableFromValue = (value) => {
65
- if (isObservable(value)) {
66
- return value;
67
- }
68
-
69
- return createObservable(({ next, complete }) => {
70
- next(value);
71
- const timer = setTimeout(() => {
72
- complete();
73
- });
74
- return () => {
75
- clearTimeout(timer);
76
- };
77
- });
78
- };
79
-
80
99
  export const createCompositeProducer = ({ cleanup = () => {} } = {}) => {
81
- const observables = new Set();
82
- const observers = new Set();
83
-
100
+ const observableSet = new Set();
101
+ const observerSet = new Set();
84
102
  const addObservable = (observable) => {
85
- if (observables.has(observable)) {
103
+ if (observableSet.has(observable)) {
86
104
  return false;
87
105
  }
88
-
89
- observables.add(observable);
90
- observers.forEach((observer) => {
106
+ observableSet.add(observable);
107
+ for (const observer of observerSet) {
91
108
  observer.observe(observable);
92
- });
109
+ }
93
110
  return true;
94
111
  };
95
-
96
112
  const removeObservable = (observable) => {
97
- if (!observables.has(observable)) {
113
+ if (!observableSet.has(observable)) {
98
114
  return false;
99
115
  }
100
-
101
- observables.delete(observable);
102
- observers.forEach((observer) => {
116
+ observableSet.delete(observable);
117
+ for (const observer of observerSet) {
103
118
  observer.unobserve(observable);
104
- });
119
+ }
105
120
  return true;
106
121
  };
107
122
 
@@ -109,63 +124,64 @@ export const createCompositeProducer = ({ cleanup = () => {} } = {}) => {
109
124
  next = () => {},
110
125
  complete = () => {},
111
126
  error = () => {},
127
+ addTeardown = () => {},
112
128
  }) => {
113
129
  let completeCount = 0;
114
-
115
130
  const checkComplete = () => {
116
- if (completeCount === observables.size) {
131
+ if (completeCount === observableSet.size) {
117
132
  complete();
118
133
  }
119
134
  };
135
+ addTeardown(() => {
136
+ for (const [, abort] of abortMap) {
137
+ abort();
138
+ }
139
+ abortMap.clear();
140
+ cleanup();
141
+ });
120
142
 
121
- const subscriptions = new Map();
143
+ const abortMap = new Map();
122
144
  const observe = (observable) => {
123
- const subscription = observable.subscribe({
124
- next: (value) => {
125
- next(value);
126
- },
127
- error: (value) => {
128
- error(value);
129
- },
130
- complete: () => {
131
- subscriptions.delete(observable);
132
- completeCount++;
133
- checkComplete();
145
+ const abortController = new AbortController();
146
+ observable.subscribe(
147
+ {
148
+ next: (value) => {
149
+ next(value);
150
+ },
151
+ error: (value) => {
152
+ error(value);
153
+ },
154
+ complete: () => {
155
+ abortMap.delete(observable);
156
+ completeCount++;
157
+ checkComplete();
158
+ },
134
159
  },
160
+ { signal: abortController.signal },
161
+ );
162
+ abortMap.set(observable, () => {
163
+ abortController.abort();
135
164
  });
136
- subscriptions.set(observable, subscription);
137
165
  };
138
166
  const unobserve = (observable) => {
139
- const subscription = subscriptions.get(observable);
140
- if (!subscription) {
167
+ const abort = abortMap.get(observable);
168
+ if (!abort) {
141
169
  return;
142
170
  }
143
-
144
- subscription.unsubscribe();
145
- subscriptions.delete(observable);
171
+ abortMap.delete(observable);
172
+ abort();
146
173
  checkComplete();
147
174
  };
148
175
  const observer = {
149
176
  observe,
150
177
  unobserve,
151
178
  };
152
- observers.add(observer);
153
- observables.forEach((observable) => {
179
+ observerSet.add(observer);
180
+ for (const observable of observableSet) {
154
181
  observe(observable);
155
- });
156
-
157
- return () => {
158
- observers.delete(observer);
159
- subscriptions.forEach((subscription) => {
160
- subscription.unsubscribe();
161
- });
162
- subscriptions.clear();
163
- cleanup();
164
- };
182
+ }
165
183
  };
166
-
167
184
  producer.addObservable = addObservable;
168
185
  producer.removeObservable = removeObservable;
169
-
170
186
  return producer;
171
187
  };
@@ -0,0 +1,34 @@
1
+ import { getObservableValueType } from "./get_observable_value_type.js";
2
+ import { createObservable } from "./observable.js";
3
+ import { observableFromFileHandle } from "./observable_from_file_handle.js";
4
+ import { observableFromNodeStream } from "./observable_from_node_stream.js";
5
+ import { observableFromNodeWebReadableStream } from "./observable_from_node_web_readable_stream.js";
6
+ import { observableFromPromise } from "./observable_from_promise.js";
7
+
8
+ export const observableFromValue = (value) => {
9
+ const observableValueType = getObservableValueType(value);
10
+ if (observableValueType === "observable") {
11
+ return value;
12
+ }
13
+ if (observableValueType === "promise") {
14
+ return observableFromPromise(value);
15
+ }
16
+ if (observableValueType === "file_handle") {
17
+ return observableFromFileHandle(value);
18
+ }
19
+ if (observableValueType === "node_stream") {
20
+ return observableFromNodeStream(value);
21
+ }
22
+ if (observableValueType === "node_web_stream") {
23
+ return observableFromNodeWebReadableStream(value);
24
+ }
25
+ return createObservable(({ next, complete, addTeardown }) => {
26
+ next(value);
27
+ const timer = setTimeout(() => {
28
+ complete();
29
+ });
30
+ addTeardown(() => {
31
+ clearTimeout(timer);
32
+ });
33
+ });
34
+ };
@@ -0,0 +1,30 @@
1
+ import { createReadStream } from "node:fs";
2
+ import { observableFromNodeStream } from "./observable_from_node_stream.js";
3
+
4
+ export const isFileHandle = (value) => {
5
+ return value && value.constructor && value.constructor.name === "FileHandle";
6
+ };
7
+
8
+ export const observableFromFileHandle = (fileHandle) => {
9
+ return observableFromNodeStream(fileHandleToReadableStream(fileHandle));
10
+ };
11
+
12
+ export const fileHandleToReadableStream = (fileHandle) => {
13
+ const fileReadableStream =
14
+ typeof fileHandle.createReadStream === "function"
15
+ ? fileHandle.createReadStream()
16
+ : createReadStream(
17
+ "/toto", // is it ok to pass a fake path like this?
18
+ {
19
+ fd: fileHandle.fd,
20
+ emitClose: true,
21
+ // autoClose: true
22
+ },
23
+ );
24
+ // I suppose it's required only when doing fs.createReadStream()
25
+ // and not fileHandle.createReadStream()
26
+ // fileReadableStream.on("end", () => {
27
+ // fileHandle.close()
28
+ // })
29
+ return fileReadableStream;
30
+ };
@@ -4,58 +4,56 @@ import { createObservable } from "./observable.js";
4
4
 
5
5
  export const observableFromNodeStream = (
6
6
  nodeStream,
7
- {
8
- readableStreamLifetime = 120_000, // 2s
9
- } = {},
7
+ { readableLifetime } = {},
10
8
  ) => {
11
- const observable = createObservable(({ next, error, complete }) => {
12
- if (nodeStream.isPaused()) {
13
- nodeStream.resume();
14
- } else if (nodeStream.complete) {
15
- complete();
16
- return null;
17
- }
18
- const cleanup = () => {
19
- nodeStream.removeListener("data", next);
20
- nodeStream.removeListener("error", error);
21
- nodeStream.removeListener("end", complete);
22
- nodeStream.removeListener("close", cleanup);
23
- nodeStream.destroy();
24
- };
25
- // should we do nodeStream.resume() in case the stream was paused ?
26
- nodeStream.once("error", error);
27
- nodeStream.on("data", (data) => {
28
- next(data);
29
- });
30
- nodeStream.once("close", () => {
31
- cleanup();
32
- });
33
- nodeStream.once("end", () => {
34
- complete();
35
- });
36
- return cleanup;
37
- });
9
+ const observable = createObservable(
10
+ ({ next, error, complete, addTeardown }) => {
11
+ const errorEventCallback = (e) => {
12
+ error(e);
13
+ };
14
+ const dataEventCallback = (data) => {
15
+ next(data);
16
+ };
17
+ const closeEventCallback = () => {
18
+ complete();
19
+ };
20
+ const endEventCallback = () => {
21
+ complete();
22
+ };
23
+ nodeStream.once("error", errorEventCallback);
24
+ nodeStream.on("data", dataEventCallback);
25
+ nodeStream.once("end", endEventCallback);
26
+ nodeStream.once("close", closeEventCallback); // not sure it's required
27
+ addTeardown(() => {
28
+ nodeStream.removeListener("error", errorEventCallback);
29
+ nodeStream.removeListener("data", dataEventCallback);
30
+ nodeStream.removeListener("end", endEventCallback);
31
+ nodeStream.removeListener("close", closeEventCallback); // not sure it's required
32
+ });
33
+ if (nodeStream.isPaused()) {
34
+ nodeStream.resume();
35
+ } else if (nodeStream.complete) {
36
+ complete();
37
+ }
38
+ },
39
+ );
38
40
 
39
- if (nodeStream instanceof Readable) {
40
- // safe measure, ensure the readable stream gets
41
- // used in the next ${readableStreamLifetimeInSeconds} otherwise destroys it
41
+ if (readableLifetime && nodeStream instanceof Readable) {
42
42
  const timeout = setTimeout(() => {
43
- process.emitWarning(
44
- `Readable stream not used after ${
45
- readableStreamLifetime / 1000
46
- } seconds. It will be destroyed to release resources`,
47
- {
48
- CODE: "READABLE_STREAM_TIMEOUT",
49
- // url is for http client request
50
- detail: `path: ${nodeStream.path}, fd: ${nodeStream.fd}, url: ${nodeStream.url}`,
51
- },
52
- );
53
- nodeStream.destroy();
54
- }, readableStreamLifetime);
55
- observable.timeout = timeout;
43
+ // disabled for now
44
+ // process.emitWarning(
45
+ // `Readable stream not used after ${readableLifetime / 1000} seconds.`,
46
+ // {
47
+ // CODE: "READABLE_STREAM_TIMEOUT",
48
+ // // url is for http client request
49
+ // detail: `path: ${nodeStream.path}, fd: ${nodeStream.fd}, url: ${nodeStream.url}`,
50
+ // },
51
+ // );
52
+ }, readableLifetime).unref();
56
53
  onceReadableStreamUsedOrClosed(nodeStream, () => {
57
54
  clearTimeout(timeout);
58
55
  });
56
+ observable.timeout = timeout;
59
57
  }
60
58
 
61
59
  return observable;
@@ -0,0 +1,32 @@
1
+ // https://nodejs.org/api/webstreams.html#readablestreamgetreaderoptions
2
+ // https://nodejs.org/api/webstreams.html
3
+ // we can read as text using TextDecoder, see https://developer.mozilla.org/fr/docs/Web/API/Fetch_API/Using_Fetch#traiter_un_fichier_texte_ligne_%C3%A0_ligne
4
+ import { createObservable } from "./observable.js";
5
+
6
+ export const observableFromNodeWebReadableStream = (nodeWebReadableStream) => {
7
+ const observable = createObservable(
8
+ ({ next, error, complete, addTeardown }) => {
9
+ const reader = nodeWebReadableStream.getReader();
10
+
11
+ const readNext = async () => {
12
+ try {
13
+ const { done, value } = await reader.read();
14
+ if (done) {
15
+ complete();
16
+ return;
17
+ }
18
+ next(value);
19
+ readNext();
20
+ } catch (e) {
21
+ error(e);
22
+ }
23
+ };
24
+ readNext();
25
+ addTeardown(() => {
26
+ reader.cancel();
27
+ });
28
+ },
29
+ );
30
+
31
+ return observable;
32
+ };
@@ -0,0 +1,13 @@
1
+ import { createObservable } from "./observable.js";
2
+
3
+ export const observableFromPromise = (promise) => {
4
+ return createObservable(async ({ next, error, complete }) => {
5
+ try {
6
+ const value = await promise;
7
+ next(value);
8
+ complete();
9
+ } catch (e) {
10
+ error(e);
11
+ }
12
+ });
13
+ };
@@ -1,19 +1,46 @@
1
1
  import { raceCallbacks } from "@jsenv/abort";
2
2
  import http from "node:http";
3
3
  import { Http2ServerResponse } from "node:http2";
4
-
5
- import { normalizeBodyMethods } from "./body.js";
4
+ import { Socket } from "node:net";
5
+ import { getObservableValueType } from "./get_observable_value_type.js";
6
+ import { observableFromValue } from "./observable_from.js";
6
7
 
7
8
  export const writeNodeResponse = async (
8
9
  responseStream,
9
10
  { status, statusText, headers, body, bodyEncoding },
10
11
  { signal, ignoreBody, onAbort, onError, onHeadersSent, onEnd } = {},
11
12
  ) => {
12
- body = await body;
13
- const bodyMethods = normalizeBodyMethods(body);
13
+ const isNetSocket = responseStream instanceof Socket;
14
+ if (
15
+ body &&
16
+ body.isObservableBody &&
17
+ headers["connection"] === undefined &&
18
+ headers["content-length"] === undefined
19
+ ) {
20
+ headers["transfer-encoding"] = "chunked";
21
+ }
22
+ // if (body && headers["content-length"] === undefined) {
23
+ // headers["transfer-encoding"] = "chunked";
24
+ // }
25
+
26
+ const bodyObservableType = getObservableValueType(body);
27
+ const destroyBody = () => {
28
+ if (bodyObservableType === "file_handle") {
29
+ body.close();
30
+ return;
31
+ }
32
+ if (bodyObservableType === "node_stream") {
33
+ body.destroy();
34
+ return;
35
+ }
36
+ if (bodyObservableType === "node_web_stream") {
37
+ body.cancel();
38
+ return;
39
+ }
40
+ };
14
41
 
15
42
  if (signal.aborted) {
16
- bodyMethods.destroy();
43
+ destroyBody();
17
44
  responseStream.destroy();
18
45
  onAbort();
19
46
  return;
@@ -34,43 +61,50 @@ export const writeNodeResponse = async (
34
61
 
35
62
  if (ignoreBody) {
36
63
  onEnd();
37
- bodyMethods.destroy();
64
+ destroyBody();
38
65
  responseStream.end();
39
66
  return;
40
67
  }
41
68
 
42
- if (bodyEncoding) {
69
+ if (bodyEncoding && !isNetSocket) {
43
70
  responseStream.setEncoding(bodyEncoding);
44
71
  }
45
72
 
46
73
  await new Promise((resolve) => {
47
- const observable = bodyMethods.asObservable();
48
- const subscription = observable.subscribe({
49
- next: (data) => {
50
- try {
51
- responseStream.write(data);
52
- } catch (e) {
53
- // Something inside Node.js sometimes puts stream
54
- // in a state where .write() throw despites nodeResponse.destroyed
55
- // being undefined and "close" event not being emitted.
56
- // I have tested if we are the one calling destroy
57
- // (I have commented every .destroy() call)
58
- // but issue still occurs
59
- // For the record it's "hard" to reproduce but can be by running
60
- // a lot of tests against a browser in the context of @jsenv/core testing
61
- if (e.code === "ERR_HTTP2_INVALID_STREAM") {
62
- return;
74
+ const observable = observableFromValue(body);
75
+ const abortController = new AbortController();
76
+ signal.addEventListener("abort", () => {
77
+ abortController.abort();
78
+ });
79
+ observable.subscribe(
80
+ {
81
+ next: (data) => {
82
+ try {
83
+ responseStream.write(data);
84
+ } catch (e) {
85
+ // Something inside Node.js sometimes puts stream
86
+ // in a state where .write() throw despites nodeResponse.destroyed
87
+ // being undefined and "close" event not being emitted.
88
+ // I have tested if we are the one calling destroy
89
+ // (I have commented every .destroy() call)
90
+ // but issue still occurs
91
+ // For the record it's "hard" to reproduce but can be by running
92
+ // a lot of tests against a browser in the context of @jsenv/core testing
93
+ if (e.code === "ERR_HTTP2_INVALID_STREAM") {
94
+ return;
95
+ }
96
+ responseStream.emit("error", e);
63
97
  }
64
- responseStream.emit("error", e);
65
- }
66
- },
67
- error: (value) => {
68
- responseStream.emit("error", value);
69
- },
70
- complete: () => {
71
- responseStream.end();
98
+ },
99
+ error: (value) => {
100
+ responseStream.emit("error", value);
101
+ },
102
+ complete: () => {
103
+ responseStream.end();
104
+ },
72
105
  },
73
- });
106
+ { signal: abortController.signal },
107
+ );
74
108
 
75
109
  raceCallbacks(
76
110
  {
@@ -102,13 +136,13 @@ export const writeNodeResponse = async (
102
136
  (winner) => {
103
137
  const raceEffects = {
104
138
  abort: () => {
105
- subscription.unsubscribe();
139
+ abortController.abort();
106
140
  responseStream.destroy();
107
141
  onAbort();
108
142
  resolve();
109
143
  },
110
144
  error: (error) => {
111
- subscription.unsubscribe();
145
+ abortController.abort();
112
146
  responseStream.destroy();
113
147
  onError(error);
114
148
  resolve();
@@ -119,7 +153,7 @@ export const writeNodeResponse = async (
119
153
  // it may happen in case of server sent event
120
154
  // where body is kept open to write to client
121
155
  // and the browser is reloaded or closed for instance
122
- subscription.unsubscribe();
156
+ abortController.abort();
123
157
  responseStream.destroy();
124
158
  onAbort();
125
159
  resolve();
@@ -139,6 +173,7 @@ const writeHead = (
139
173
  responseStream,
140
174
  { status, statusText, headers, onHeadersSent },
141
175
  ) => {
176
+ const responseIsNetSocket = responseStream instanceof Socket;
142
177
  const responseIsHttp2ServerResponse =
143
178
  responseStream instanceof Http2ServerResponse;
144
179
  const responseIsServerHttp2Stream =
@@ -172,6 +207,16 @@ const writeHead = (
172
207
  onHeadersSent({ nodeHeaders, status, statusText });
173
208
  return;
174
209
  }
210
+ if (responseIsNetSocket) {
211
+ const headersString = Object.keys(nodeHeaders)
212
+ .map((h) => `${h}: ${nodeHeaders[h]}`)
213
+ .join("\r\n");
214
+ responseStream.write(
215
+ `HTTP/1.1 ${status} ${statusText}\r\n${headersString}\r\n\r\n`,
216
+ );
217
+ onHeadersSent({ nodeHeaders, status, statusText });
218
+ return;
219
+ }
175
220
 
176
221
  try {
177
222
  responseStream.writeHead(status, statusText, nodeHeaders);
@@ -9,6 +9,8 @@ const magenta = "\x1b[35m";
9
9
  const cyan = "\x1b[36m";
10
10
  // const white = "\x1b[37m"
11
11
 
12
+ export const websocketSuffixColorized = `{ upgrade: ${magenta}websocket${close} }`;
13
+
12
14
  export const colorizeResponseStatus = (status) => {
13
15
  const statusType = statusToType(status);
14
16
  if (statusType === "information") return `${cyan}${status}${close}`;