@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.
- package/README.md +79 -86
- package/dist/html/4xx.html +196 -0
- package/dist/html/500.html +17 -0
- package/dist/html/route_inspector.html +2435 -0
- package/dist/js/formidable.js +17 -0
- package/dist/js/vendors.js +2001 -0
- package/dist/js/ws.js +6863 -0
- package/dist/jsenv_server.js +8829 -0
- package/package.json +19 -9
- package/src/content_negotiation/pick_content_version.js +63 -0
- package/src/{fetch_filesystem.js → filesystem/fetch_filesystem.js} +57 -72
- package/src/{serve_directory.js → filesystem/serve_directory.js} +1 -2
- package/src/interfacing_with_node/from_node_request.js +455 -0
- package/src/interfacing_with_node/get_observable_value_type.js +43 -0
- package/src/interfacing_with_node/observable.js +91 -75
- package/src/interfacing_with_node/observable_from.js +34 -0
- package/src/interfacing_with_node/observable_from_file_handle.js +30 -0
- package/src/interfacing_with_node/observable_from_node_stream.js +44 -46
- package/src/interfacing_with_node/observable_from_node_web_readable_stream.js +32 -0
- package/src/interfacing_with_node/observable_from_promise.js +13 -0
- package/src/interfacing_with_node/write_node_response.js +80 -35
- package/src/internal/colorizeResponseStatus.js +2 -0
- package/src/internal/headers_composition.js +42 -15
- package/src/internal/multiple-header.js +27 -12
- package/src/internal/object_composition.js +2 -2
- package/src/internal/response_composition.js +16 -3
- package/src/main.js +15 -11
- package/src/progressive_response.js +38 -0
- package/src/replace_placeholder_in_html.js +12 -0
- package/src/request_body_handling.js +22 -0
- package/src/router/router.js +878 -0
- package/src/server_events.js +431 -0
- package/src/service_controller.js +79 -73
- package/src/services/autoreload_on_server_restart/client/autoreload_on_server_restart.js +125 -0
- package/src/services/autoreload_on_server_restart/jsenv_service_autoreload_on_server_restart.js +45 -0
- package/src/services/cors/jsenv_service_cors.js +10 -18
- package/src/services/default_body_4xx_5xx/client/4xx.html +65 -0
- package/src/services/default_body_4xx_5xx/jsenv_service_default_body_4xx_5xx.js +82 -0
- package/src/services/error_handler/client/500.html +17 -0
- package/src/services/error_handler/jsenv_service_error_handler.js +47 -28
- package/src/services/internal_client_files/jsenv_service_internal_client_files.js +34 -0
- package/src/services/open_file/jsenv_service_open_file.js +37 -0
- package/src/services/route_inspector/client/code_block.js +215 -0
- package/src/services/route_inspector/client/route_inspector.html +1363 -0
- package/src/services/route_inspector/jsenv_service_route_inspector.js +39 -0
- package/src/start_server.js +575 -607
- package/src/web_socket_response.js +53 -0
- package/src/from_fetch_response.js +0 -15
- package/src/interfacing_with_node/body.js +0 -81
- package/src/interfacing_with_node/request_factory.js +0 -125
- package/src/readRequestBody.js +0 -32
- package/src/server_timing/timing_measure.js +0 -26
- package/src/service_composition/path_to_regexp.js +0 -436
- package/src/service_composition/routing.js +0 -31
- 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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
subscribe: (
|
|
15
|
+
{
|
|
16
|
+
next = () => {},
|
|
17
|
+
error = (value) => {
|
|
18
|
+
throw value;
|
|
19
|
+
},
|
|
20
|
+
complete = () => {},
|
|
16
21
|
},
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
{ signal = new AbortController().signal } = {},
|
|
23
|
+
) => {
|
|
19
24
|
let cleanup = () => {};
|
|
20
25
|
const subscription = {
|
|
21
|
-
|
|
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.
|
|
50
|
+
if (!subscription.active) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
31
53
|
next(value);
|
|
32
54
|
},
|
|
33
55
|
error: (value) => {
|
|
34
|
-
if (subscription.
|
|
56
|
+
if (!subscription.active) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
35
59
|
error(value);
|
|
60
|
+
close();
|
|
36
61
|
},
|
|
37
62
|
complete: () => {
|
|
38
|
-
if (subscription.
|
|
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
|
|
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
|
|
82
|
-
const
|
|
83
|
-
|
|
100
|
+
const observableSet = new Set();
|
|
101
|
+
const observerSet = new Set();
|
|
84
102
|
const addObservable = (observable) => {
|
|
85
|
-
if (
|
|
103
|
+
if (observableSet.has(observable)) {
|
|
86
104
|
return false;
|
|
87
105
|
}
|
|
88
|
-
|
|
89
|
-
|
|
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 (!
|
|
113
|
+
if (!observableSet.has(observable)) {
|
|
98
114
|
return false;
|
|
99
115
|
}
|
|
100
|
-
|
|
101
|
-
|
|
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 ===
|
|
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
|
|
143
|
+
const abortMap = new Map();
|
|
122
144
|
const observe = (observable) => {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
140
|
-
if (!
|
|
167
|
+
const abort = abortMap.get(observable);
|
|
168
|
+
if (!abort) {
|
|
141
169
|
return;
|
|
142
170
|
}
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
153
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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 {
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
139
|
+
abortController.abort();
|
|
106
140
|
responseStream.destroy();
|
|
107
141
|
onAbort();
|
|
108
142
|
resolve();
|
|
109
143
|
},
|
|
110
144
|
error: (error) => {
|
|
111
|
-
|
|
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
|
-
|
|
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}`;
|