@jsenv/server 9.1.1 → 9.2.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/server",
3
- "version": "9.1.1",
3
+ "version": "9.2.3",
4
4
  "description": "Write your Node.js server using pure functions",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -0,0 +1,31 @@
1
+ /*
2
+ * See callback_race.md
3
+ */
4
+
5
+ export const raceCallbacks = (raceDescription, winnerCallback) => {
6
+ const cleanCallbacks = []
7
+
8
+ const cleanup = () => {
9
+ const cleanCallbacksCopy = cleanCallbacks.slice()
10
+ cleanCallbacks.length = 0
11
+ cleanCallbacksCopy.forEach((clean) => {
12
+ clean()
13
+ })
14
+ }
15
+
16
+ Object.keys(raceDescription).forEach((candidateName) => {
17
+ const register = raceDescription[candidateName]
18
+ const returnValue = register((data) => {
19
+ cleanup()
20
+ winnerCallback({
21
+ name: candidateName,
22
+ data,
23
+ })
24
+ })
25
+ if (typeof returnValue === "function") {
26
+ cleanCallbacks.push(returnValue)
27
+ }
28
+ })
29
+
30
+ return cleanup
31
+ }
@@ -0,0 +1,38 @@
1
+ Taking different code paths based on different inputs is messy and code oftens forget to clean things behind itself.
2
+
3
+ "raceCallbacks" helps to do both properly with the following API
4
+
5
+ ```js
6
+ import { raceCallbacks } from "somewhere"
7
+
8
+ raceCallbacks(
9
+ {
10
+ timeout: (cb) => {
11
+ const timeout = setTimeout(cb, 1000)
12
+ return () => {
13
+ clearTimeout(timeout)
14
+ }
15
+ },
16
+ error: () => {
17
+ something.on("error", cb)
18
+ return () => {
19
+ return something.removeListener("error", cb)
20
+ }
21
+ },
22
+ end: () => {
23
+ something.on("end", cb)
24
+ return () => {
25
+ return something.removeListener("end", cb)
26
+ }
27
+ },
28
+ },
29
+ (winner) => {
30
+ const raceEffects = {
31
+ timeout: () => console.log("timeout after 1000ms"),
32
+ error: (e) => console.error("error", e),
33
+ end: (value) => console.log("end", value),
34
+ }
35
+ raceEffects[winner.name](winner.value)
36
+ },
37
+ )
38
+ ```
@@ -0,0 +1,58 @@
1
+ export const createCleaner = () => {
2
+ let callbacks = []
3
+
4
+ const addCallback = (callback) => {
5
+ if (typeof callback !== "function") {
6
+ throw new Error(`callback must be a function, got ${callback}`)
7
+ }
8
+
9
+ // don't register twice
10
+ const existingCallback = callbacks.find((callbackCandidate) => {
11
+ return callbackCandidate === callback
12
+ })
13
+ if (existingCallback) {
14
+ if (typeof process.emitWarning === "object") {
15
+ process.emitWarning(`Trying to register same callback twice`, {
16
+ CODE: "CALLBACK_DUPLICATION",
17
+ detail: `It's often the sign that code is executd more than once`,
18
+ })
19
+ } else {
20
+ console.warn(`Trying to add same callback twice`)
21
+ }
22
+ } else {
23
+ callbacks = [...callbacks, callback]
24
+ }
25
+
26
+ return () => {
27
+ callbacks = arrayWithout(callbacks, callback)
28
+ }
29
+ }
30
+
31
+ const clean = async (reason) => {
32
+ const callbacksCopy = callbacks.slice()
33
+ callbacks.length = 0
34
+
35
+ await Promise.all(
36
+ callbacksCopy.map(async (callback) => {
37
+ await callback(reason)
38
+ }),
39
+ )
40
+ }
41
+
42
+ return { addCallback, clean }
43
+ }
44
+
45
+ const arrayWithout = (array, item) => {
46
+ if (array.length === 0) return array
47
+ const arrayWithoutItem = []
48
+ let i = 0
49
+ while (i < array.length) {
50
+ const value = array[i]
51
+ i++
52
+ if (value === item) {
53
+ continue
54
+ }
55
+ arrayWithoutItem.push(value)
56
+ }
57
+ return arrayWithoutItem
58
+ }
package/src/abort/main.js CHANGED
@@ -27,4 +27,7 @@
27
27
  */
28
28
 
29
29
  export { Abort } from "./abort.js"
30
- export { Cleanup } from "./cleanup.js"
30
+ export { createCleaner } from "./cleaner.js"
31
+
32
+ // When these file will become a NPM package, this export should be Node.js only
33
+ export { createOperation } from "./operation.js"
@@ -0,0 +1,38 @@
1
+ import { Abort } from "./abort.js"
2
+ import { raceCallbacks } from "./callback_race.js"
3
+ import { createCleaner } from "./cleaner.js"
4
+
5
+ export const createOperation = ({
6
+ abortSignal = Abort.dormantSignal(),
7
+ cleanOnAbort = false,
8
+ } = {}) => {
9
+ const cleaner = createCleaner()
10
+ const operation = { abortSignal, cleaner }
11
+
12
+ if (cleanOnAbort) {
13
+ raceCallbacks(
14
+ {
15
+ aborted: (cb) => {
16
+ operation.abortSignal.addEventListener("abort", cb)
17
+ return () => {
18
+ operation.abortSignal.removeEventListener("abort", cb)
19
+ }
20
+ },
21
+ cleaned: (cb) => {
22
+ return cleaner.addCallback(cb)
23
+ },
24
+ },
25
+ (winner) => {
26
+ const raceEffects = {
27
+ aborted: () => {
28
+ cleaner.clean()
29
+ },
30
+ cleaned: () => {},
31
+ }
32
+ raceEffects[winner.name]()
33
+ },
34
+ )
35
+ }
36
+
37
+ return operation
38
+ }
@@ -0,0 +1,26 @@
1
+ import { Abort } from "./abort.js"
2
+ import { raceProcessTeardownEvents } from "./process_teardown_events.js"
3
+
4
+ export const abortOperationOnProcessTeardown = (
5
+ operation,
6
+ processTeardownEvents,
7
+ ) => {
8
+ const processSignalAbortController = new AbortController()
9
+ operation.abortSignal = Abort.composeTwoAbortSignals(
10
+ operation.abortSignal,
11
+ processSignalAbortController.signal,
12
+ )
13
+ const cancelProcessTeardownRace = raceProcessTeardownEvents(
14
+ processTeardownEvents,
15
+ () => {
16
+ processSignalAbortController.abort()
17
+ },
18
+ )
19
+ const removeCleanCallback = operation.cleaner.addCallback(() => {
20
+ cancelProcessTeardownRace()
21
+ })
22
+ return () => {
23
+ cancelProcessTeardownRace()
24
+ removeCleanCallback()
25
+ }
26
+ }
@@ -0,0 +1,59 @@
1
+ import { raceCallbacks } from "./callback_race.js"
2
+
3
+ export const raceProcessTeardownEvents = (processTeardownEvents, callback) => {
4
+ return raceCallbacks(
5
+ {
6
+ ...(processTeardownEvents.SIGHUP ? SIGHUP_CALLBACK : {}),
7
+ ...(processTeardownEvents.SIGTERM ? SIGTERM_CALLBACK : {}),
8
+ ...(processTeardownEvents.SIGINT ? SIGINT_CALLBACK : {}),
9
+ ...(processTeardownEvents.beforeExit ? BEFORE_EXIT_CALLBACK : {}),
10
+ ...(processTeardownEvents.exit ? EXIT_CALLBACK : {}),
11
+ },
12
+ callback,
13
+ )
14
+ }
15
+
16
+ const SIGHUP_CALLBACK = {
17
+ SIGHUP: (cb) => {
18
+ process.on("SIGHUP", cb)
19
+ return () => {
20
+ process.removeListener("SIGHUP", cb)
21
+ }
22
+ },
23
+ }
24
+
25
+ const SIGTERM_CALLBACK = {
26
+ SIGTERM: (cb) => {
27
+ process.on("SIGTERM", cb)
28
+ return () => {
29
+ process.removeListener("SIGTERM", cb)
30
+ }
31
+ },
32
+ }
33
+
34
+ const BEFORE_EXIT_CALLBACK = {
35
+ beforeExit: (cb) => {
36
+ process.on("beforeExit", cb)
37
+ return () => {
38
+ process.removeListener("beforeExit", cb)
39
+ }
40
+ },
41
+ }
42
+
43
+ const EXIT_CALLBACK = {
44
+ exit: (cb) => {
45
+ process.on("exit", cb)
46
+ return () => {
47
+ process.removeListener("exit", cb)
48
+ }
49
+ },
50
+ }
51
+
52
+ const SIGINT_CALLBACK = {
53
+ SIGINT: (cb) => {
54
+ process.on("SIGINT", cb)
55
+ return () => {
56
+ process.removeListener("SIGINT", cb)
57
+ }
58
+ },
59
+ }
@@ -1,9 +1,10 @@
1
+ import { raceCallbacks } from "../abort/callback_race.js"
1
2
  import { nodeStreamToObservable } from "./nodeStreamToObservable.js"
2
3
  import { headersFromObject } from "./headersFromObject.js"
3
4
 
4
5
  export const nodeRequestToRequest = (
5
6
  nodeRequest,
6
- { serverCleanup, serverOrigin },
7
+ { serverOperation, serverOrigin },
7
8
  ) => {
8
9
  const { method } = nodeRequest
9
10
  const { url: ressource } = nodeRequest
@@ -24,7 +25,7 @@ export const nodeRequestToRequest = (
24
25
  }
25
26
 
26
27
  return Object.freeze({
27
- abortSignal: createRequestAbortSignal({ nodeRequest, serverCleanup }),
28
+ abortSignal: createRequestAbortSignal({ nodeRequest, serverOperation }),
28
29
  origin: requestOrigin,
29
30
  http2: Boolean(nodeRequest.stream),
30
31
  ressource,
@@ -34,13 +35,49 @@ export const nodeRequestToRequest = (
34
35
  })
35
36
  }
36
37
 
37
- const createRequestAbortSignal = ({ nodeRequest, serverCleanup }) => {
38
+ const createRequestAbortSignal = ({ nodeRequest, serverOperation }) => {
38
39
  const abortController = new AbortController()
39
- nodeRequest.on("abort", () => {
40
- abortController.abort()
41
- })
42
- serverCleanup.addCallback(() => {
43
- abortController.abort()
44
- })
40
+ raceCallbacks(
41
+ {
42
+ server_closing: (cb) => {
43
+ return serverOperation.cleaner.addCallback(cb)
44
+ },
45
+ request_close: (cb) => {
46
+ nodeRequest.on("close", cb)
47
+ return () => {
48
+ nodeRequest.removeListener("close", cb)
49
+ }
50
+ },
51
+ request_error: (cb) => {
52
+ nodeRequest.on("error", cb)
53
+ return () => {
54
+ nodeRequest.removeListener("error", cb)
55
+ }
56
+ },
57
+ request_finish: (cb) => {
58
+ nodeRequest.on("finish", cb)
59
+ return () => {
60
+ nodeRequest.removeListener("finish", cb)
61
+ }
62
+ },
63
+ },
64
+ (winner) => {
65
+ const raceEffects = {
66
+ server_closing: () => {
67
+ abortController.abort()
68
+ },
69
+ request_close: () => {
70
+ if (!nodeRequest.complete) {
71
+ // aborted while reading request body
72
+ abortController.abort()
73
+ }
74
+ },
75
+ // no special effects on thoose, just remove other callbacks
76
+ request_error: () => {},
77
+ request_finish: () => {},
78
+ }
79
+ raceEffects[winner.name]()
80
+ },
81
+ )
45
82
  return abortController.signal
46
83
  }
@@ -21,26 +21,19 @@ export const nodeStreamToObservable = (nodeStream) => {
21
21
  }
22
22
 
23
23
  return createObservable(({ next, error, complete }) => {
24
- // should we do nodeStream.resume() in case the stream was paused ?
25
- nodeStream.once("error", error)
26
- nodeStream.on("data", next)
27
- nodeStream.once("end", complete)
28
-
29
24
  const cleanup = () => {
30
25
  nodeStream.removeListener("data", next)
31
26
  nodeStream.removeListener("error", error)
32
27
  nodeStream.removeListener("end", complete)
33
-
34
- if (typeof nodeStream.abort === "function") {
35
- nodeStream.abort()
36
- } else {
37
- nodeStream.destroy()
38
- }
28
+ nodeStream.removeListener("close", cleanup)
29
+ nodeStream.destroy()
39
30
  }
40
31
 
41
- if (typeof nodeStream.once === "function") {
42
- nodeStream.once("abort", cleanup)
43
- }
32
+ // should we do nodeStream.resume() in case the stream was paused ?
33
+ nodeStream.once("error", error)
34
+ nodeStream.on("data", next)
35
+ nodeStream.once("close", cleanup)
36
+ nodeStream.once("end", complete)
44
37
 
45
38
  return cleanup
46
39
  })
@@ -1,6 +1,6 @@
1
1
  import { Http2ServerResponse } from "node:http2"
2
2
 
3
- import { raceCallbacks } from "./callback_race.js"
3
+ import { raceCallbacks } from "@jsenv/server/src/abort/callback_race.js"
4
4
  import { normalizeResponseBodyMethods } from "./responseBody.js"
5
5
 
6
6
  export const populateNodeResponse = async (
@@ -41,92 +41,95 @@ export const populateNodeResponse = async (
41
41
  responseStream.setEncoding(bodyEncoding)
42
42
  }
43
43
 
44
- const winnerPromise = raceCallbacks({
45
- abort: (cb) => {
46
- abortSignal.addEventListener("abort", cb)
47
- return () => {
48
- abortSignal.removeEventListener("abort", cb)
49
- }
50
- },
51
- error: (cb) => {
52
- responseStream.on("error", cb)
53
- return () => {
54
- responseStream.removeListener("error", cb)
55
- }
56
- },
57
- close: (cb) => {
58
- responseStream.on("close", cb)
59
- return () => {
60
- responseStream.removeListener("close", cb)
61
- }
62
- },
63
- finish: (cb) => {
64
- responseStream.on("finish", cb)
65
- return () => {
66
- responseStream.removeListener("finish", cb)
67
- }
68
- },
69
- })
70
-
71
- const observable = bodyMethods.asObservable()
72
- const subscription = observable.subscribe({
73
- next: (data) => {
74
- try {
75
- responseStream.write(data)
76
- } catch (e) {
77
- // Something inside Node.js sometimes puts stream
78
- // in a state where .write() throw despites nodeResponse.destroyed
79
- // being undefined and "close" event not being emitted.
80
- // I have tested if we are the one calling destroy
81
- // (I have commented every .destroy() call)
82
- // but issue still occurs
83
- // For the record it's "hard" to reproduce but can be by running
84
- // a lot of tests against a browser in the context of @jsenv/core testing
85
- if (e.code === "ERR_HTTP2_INVALID_STREAM") {
86
- return
87
- }
88
- responseStream.emit("error", e)
89
- }
90
- },
91
- error: (value) => {
92
- responseStream.emit("error", value)
93
- },
94
- complete: () => {
95
- responseStream.end()
96
- },
97
- })
98
-
99
- const winner = await winnerPromise
100
44
  await new Promise((resolve) => {
101
- const raceEffects = {
102
- abort: () => {
103
- subscription.unsubscribe()
104
- responseStream.destroy()
105
- onAbort()
106
- resolve()
45
+ raceCallbacks(
46
+ {
47
+ abort: (cb) => {
48
+ abortSignal.addEventListener("abort", cb)
49
+ return () => {
50
+ abortSignal.removeEventListener("abort", cb)
51
+ }
52
+ },
53
+ error: (cb) => {
54
+ responseStream.on("error", cb)
55
+ return () => {
56
+ responseStream.removeListener("error", cb)
57
+ }
58
+ },
59
+ close: (cb) => {
60
+ responseStream.on("close", cb)
61
+ return () => {
62
+ responseStream.removeListener("close", cb)
63
+ }
64
+ },
65
+ finish: (cb) => {
66
+ responseStream.on("finish", cb)
67
+ return () => {
68
+ responseStream.removeListener("finish", cb)
69
+ }
70
+ },
107
71
  },
108
- error: (error) => {
109
- subscription.unsubscribe()
110
- responseStream.destroy()
111
- onError(error)
112
- resolve()
72
+ (winner) => {
73
+ const raceEffects = {
74
+ abort: () => {
75
+ subscription.unsubscribe()
76
+ responseStream.destroy()
77
+ onAbort()
78
+ resolve()
79
+ },
80
+ error: (error) => {
81
+ subscription.unsubscribe()
82
+ responseStream.destroy()
83
+ onError(error)
84
+ resolve()
85
+ },
86
+ close: () => {
87
+ // close body in case nodeResponse is prematurely closed
88
+ // while body is writing
89
+ // it may happen in case of server sent event
90
+ // where body is kept open to write to client
91
+ // and the browser is reloaded or closed for instance
92
+ subscription.unsubscribe()
93
+ responseStream.destroy()
94
+ onAbort()
95
+ resolve()
96
+ },
97
+ finish: () => {
98
+ onEnd()
99
+ resolve()
100
+ },
101
+ }
102
+ raceEffects[winner.name](winner.data)
103
+ },
104
+ )
105
+
106
+ const observable = bodyMethods.asObservable()
107
+ const subscription = observable.subscribe({
108
+ next: (data) => {
109
+ try {
110
+ responseStream.write(data)
111
+ } catch (e) {
112
+ // Something inside Node.js sometimes puts stream
113
+ // in a state where .write() throw despites nodeResponse.destroyed
114
+ // being undefined and "close" event not being emitted.
115
+ // I have tested if we are the one calling destroy
116
+ // (I have commented every .destroy() call)
117
+ // but issue still occurs
118
+ // For the record it's "hard" to reproduce but can be by running
119
+ // a lot of tests against a browser in the context of @jsenv/core testing
120
+ if (e.code === "ERR_HTTP2_INVALID_STREAM") {
121
+ return
122
+ }
123
+ responseStream.emit("error", e)
124
+ }
113
125
  },
114
- close: () => {
115
- // close body in case nodeResponse is prematurely closed
116
- // while body is writing
117
- // it may happen in case of server sent event
118
- // where body is kept open to write to client
119
- // and the browser is reloaded or closed for instance
120
- subscription.unsubscribe()
121
- onEnd()
122
- resolve()
126
+ error: (value) => {
127
+ responseStream.emit("error", value)
123
128
  },
124
- finish: () => {
125
- onEnd()
126
- resolve()
129
+ complete: () => {
130
+ responseStream.end()
127
131
  },
128
- }
129
- raceEffects[winner.name](winner.data)
132
+ })
130
133
  })
131
134
  }
132
135
 
@@ -3,9 +3,9 @@ import cluster from "node:cluster"
3
3
  import { memoize } from "@jsenv/filesystem"
4
4
  import { createDetailedMessage, createLogger } from "@jsenv/logger"
5
5
 
6
- import { Abort, Cleanup } from "./abort/main.js"
6
+ import { createOperation, Abort } from "./abort/main.js"
7
+ import { abortOperationOnProcessTeardown } from "./abort/process_teardown_abort.js"
7
8
 
8
- import { raceCallbacks } from "./internal/callback_race.js"
9
9
  import { createPolyglotServer } from "./internal/server-polyglot.js"
10
10
  import { trackServerPendingConnections } from "./internal/trackServerPendingConnections.js"
11
11
  import { trackServerPendingRequests } from "./internal/trackServerPendingRequests.js"
@@ -27,9 +27,10 @@ import {
27
27
  STOP_REASON_PROCESS_EXIT,
28
28
  STOP_REASON_NOT_SPECIFIED,
29
29
  } from "./stopReasons.js"
30
+ import { raceProcessTeardownEvents } from "./abort/process_teardown_events.js"
30
31
 
31
32
  export const startServer = async ({
32
- abortSignal = Abort.dormantSignal(),
33
+ abortSignal,
33
34
  logLevel,
34
35
  serverName = "server",
35
36
 
@@ -59,8 +60,7 @@ export const startServer = async ({
59
60
  sendServerInternalErrorDetails = false,
60
61
  serverInternalErrorToResponse = jsenvServerInternalErrorToResponse,
61
62
 
62
- startedCallback = () => {},
63
- stoppedCallback = () => {},
63
+ onStop = () => {},
64
64
  nagle = true,
65
65
  } = {}) => {
66
66
  if (protocol !== "http" && protocol !== "https") {
@@ -78,9 +78,23 @@ export const startServer = async ({
78
78
  throw new Error(`http2 needs "https" but protocol is "${protocol}"`)
79
79
  }
80
80
 
81
+ const serverOperation = createOperation({
82
+ abortSignal,
83
+ })
84
+ const processTeardownEvents = {
85
+ SIGHUP: stopOnExit,
86
+ SIGTERM: stopOnExit,
87
+ SIGINT: stopOnSIGINT,
88
+ beforeExit: stopOnExit,
89
+ exit: stopOnExit,
90
+ }
91
+ const cancelAbortOnProcessTeardown = abortOperationOnProcessTeardown(
92
+ serverOperation,
93
+ processTeardownEvents,
94
+ )
81
95
  // we "must" listen abort signal only while server is starting
82
96
  // after this, it's stop() that must be used
83
- Abort.throwIfAborted(abortSignal)
97
+ Abort.throwIfAborted(serverOperation.abortSignal)
84
98
 
85
99
  const logger = createLogger({ logLevel })
86
100
  if (
@@ -113,29 +127,6 @@ export const startServer = async ({
113
127
  throw error
114
128
  }
115
129
 
116
- const serverCleanup = Cleanup.create()
117
-
118
- if (stopOnExit || stopOnSIGINT) {
119
- raceCallbacks({
120
- ...(stopOnExit ? EXIT_CALLBACKS : {}),
121
- ...(stopOnSIGINT ? SIGINT_CALLBACKS : {}),
122
- // to ensure other events are cleaned up when stop is called for other reasons
123
- other_stop_reasons: (cb) => {
124
- return serverCleanup.addCallback(cb)
125
- },
126
- }).then((winner) => {
127
- const effects = {
128
- SIGHUP: () => stop(STOP_REASON_PROCESS_SIGHUP),
129
- SIGTERM: () => stop(STOP_REASON_PROCESS_SIGTERM),
130
- SIGINT: () => stop(STOP_REASON_PROCESS_SIGINT),
131
- beforeExit: () => stop(STOP_REASON_PROCESS_BEFORE_EXIT),
132
- exit: () => stop(STOP_REASON_PROCESS_EXIT),
133
- other_stop_reasons: () => {},
134
- }
135
- effects[winner.name]()
136
- })
137
- }
138
-
139
130
  const nodeServer = await createNodeServer({
140
131
  protocol,
141
132
  redirectHttpToHttps,
@@ -145,7 +136,7 @@ export const startServer = async ({
145
136
  http2,
146
137
  http1Allowed,
147
138
  })
148
- Abort.throwIfAborted(abortSignal)
139
+ Abort.throwIfAborted(serverOperation.abortSignal)
149
140
 
150
141
  // https://nodejs.org/api/net.html#net_server_unref
151
142
  if (!keepProcessAlive) {
@@ -160,16 +151,16 @@ export const startServer = async ({
160
151
  const stop = memoize(async (reason = STOP_REASON_NOT_SPECIFIED) => {
161
152
  status = "stopping"
162
153
  logger.info(`${serverName} stopping server because ${reason}`)
154
+ onStop({ reason })
163
155
 
164
- await serverCleanup.cleanup(reason)
156
+ await serverOperation.cleaner.clean(reason)
165
157
  await stopListening(nodeServer)
166
158
  status = "stopped"
167
- stoppedCallback({ reason })
168
159
  stoppedResolve(reason)
169
160
  })
170
161
 
171
162
  port = await listen({
172
- abortSignal,
163
+ abortSignal: serverOperation.abortSignal,
173
164
  server: nodeServer,
174
165
  port,
175
166
  portHint,
@@ -184,19 +175,19 @@ export const startServer = async ({
184
175
  nodeServer,
185
176
  onError,
186
177
  )
187
- serverCleanup.addCallback(removeConnectionErrorListener)
178
+ serverOperation.cleaner.addCallback(removeConnectionErrorListener)
188
179
 
189
180
  const connectionsTracker = trackServerPendingConnections(nodeServer, {
190
181
  http2,
191
182
  })
192
183
  // opened connection must be shutdown before the close event is emitted
193
- serverCleanup.addCallback(connectionsTracker.stop)
184
+ serverOperation.cleaner.addCallback(connectionsTracker.stop)
194
185
 
195
186
  const pendingRequestsTracker = trackServerPendingRequests(nodeServer, {
196
187
  http2,
197
188
  })
198
189
  // ensure pending requests got a response from the server
199
- serverCleanup.addCallback((reason) => {
190
+ serverOperation.cleaner.addCallback((reason) => {
200
191
  pendingRequestsTracker.stop({
201
192
  status: reason === STOP_REASON_INTERNAL_ERROR ? 500 : 503,
202
193
  reason,
@@ -216,7 +207,7 @@ export const startServer = async ({
216
207
  }
217
208
 
218
209
  const request = nodeRequestToRequest(nodeRequest, {
219
- serverCleanup,
210
+ serverOperation,
220
211
  serverOrigin,
221
212
  })
222
213
 
@@ -356,16 +347,18 @@ export const startServer = async ({
356
347
  parentHttp2Stream: pushStream,
357
348
  })
358
349
 
359
- const responseLength = responseProperties.headers["content-length"] || 0
360
- // should we check on "parentHttp2Stream" or "nodeResponse.stream"?
361
- const { effectiveRecvDataLength, remoteWindowSize } =
362
- parentHttp2Stream.session.state
363
- if (effectiveRecvDataLength + responseLength > remoteWindowSize) {
364
- addRequestLog(requestNode, {
365
- type: "debug",
366
- value: `Aborting stream to prevent exceeding remoteWindowSize`,
367
- })
368
- pushRequestAbortController.abort()
350
+ if (responseProperties !== ABORTED_RESPONSE_PROPERTIES) {
351
+ const responseLength = responseProperties.headers["content-length"] || 0
352
+ // should we check on "parentHttp2Stream" or "nodeResponse.stream"?
353
+ const { effectiveRecvDataLength, remoteWindowSize } =
354
+ parentHttp2Stream.session.state
355
+ if (effectiveRecvDataLength + responseLength > remoteWindowSize) {
356
+ addRequestLog(requestNode, {
357
+ type: "debug",
358
+ value: `Aborting stream to prevent exceeding remoteWindowSize`,
359
+ })
360
+ pushRequestAbortController.abort()
361
+ }
369
362
  }
370
363
 
371
364
  await sendResponse({
@@ -411,7 +404,7 @@ export const startServer = async ({
411
404
  }
412
405
  }
413
406
  })
414
- let { error, responseProperties } = responseFromShortcircuit
407
+ let { aborted, error, responseProperties } = responseFromShortcircuit
415
408
  ? { responseProperties: responseFromShortcircuit }
416
409
  : await generateResponseDescription({
417
410
  requestToResponse,
@@ -429,6 +422,10 @@ export const startServer = async ({
429
422
  },
430
423
  })
431
424
 
425
+ if (aborted) {
426
+ return ABORTED_RESPONSE_PROPERTIES
427
+ }
428
+
432
429
  // internal error, create 500 response
433
430
  if (error) {
434
431
  if (
@@ -580,12 +577,25 @@ export const startServer = async ({
580
577
 
581
578
  const removeRequestListener = listenRequest(nodeServer, requestCallback)
582
579
  // ensure we don't try to handle new requests while server is stopping
583
- serverCleanup.addCallback(removeRequestListener)
580
+ serverOperation.cleaner.addCallback(removeRequestListener)
584
581
 
585
582
  logger.info(
586
583
  `${serverName} started at ${serverOrigin} (${serverOrigins.external})`,
587
584
  )
588
- startedCallback({ origin: serverOrigin })
585
+
586
+ cancelAbortOnProcessTeardown() // it's now handled by stop()
587
+ const cancelProcessTeardownRace = raceProcessTeardownEvents(
588
+ processTeardownEvents,
589
+ (winner) => {
590
+ stop(PROCESS_TEARDOWN_EVENTS_MAP[winner.name])
591
+ },
592
+ )
593
+ serverOperation.cleaner.addCallback(() => {
594
+ cancelProcessTeardownRace()
595
+ })
596
+ nodeServer.once("close", () => {
597
+ cancelProcessTeardownRace()
598
+ })
589
599
 
590
600
  return {
591
601
  getStatus: () => status,
@@ -636,6 +646,11 @@ const generateResponseDescription = async ({
636
646
  responseProperties: returnValue,
637
647
  }
638
648
  } catch (error) {
649
+ if (error.name === "AbortError" && request.abortSignal.aborted) {
650
+ return {
651
+ aborted: true,
652
+ }
653
+ }
639
654
  return {
640
655
  error,
641
656
  }
@@ -695,38 +710,12 @@ const testCanPushStream = (http2Stream) => {
695
710
  }
696
711
  }
697
712
 
698
- const EXIT_CALLBACKS = {
699
- SIGHUP: (cb) => {
700
- process.on("SIGHUP", cb)
701
- return () => {
702
- process.removeListener("SIGHUP", cb)
703
- }
704
- },
705
- SIGTERM: (cb) => {
706
- process.on("SIGTERM", cb)
707
- return () => {
708
- process.removeListener("SIGTERM", cb)
709
- }
710
- },
711
- beforeExit: (cb) => {
712
- process.on("beforeExit", cb)
713
- return () => {
714
- process.removeListener("beforeExit", cb)
715
- }
716
- },
717
- exit: (cb) => {
718
- process.on("exit", cb)
719
- return () => {
720
- process.removeListener("exit", cb)
721
- }
722
- },
723
- }
713
+ const ABORTED_RESPONSE_PROPERTIES = {}
724
714
 
725
- const SIGINT_CALLBACKS = {
726
- SIGINT: (cb) => {
727
- process.on("SIGINT", cb)
728
- return () => {
729
- process.removeListener("SIGINT", cb)
730
- }
731
- },
715
+ const PROCESS_TEARDOWN_EVENTS_MAP = {
716
+ SIGHUP: STOP_REASON_PROCESS_SIGHUP,
717
+ SIGTERM: STOP_REASON_PROCESS_SIGTERM,
718
+ SIGINT: STOP_REASON_PROCESS_SIGINT,
719
+ beforeExit: STOP_REASON_PROCESS_BEFORE_EXIT,
720
+ exit: STOP_REASON_PROCESS_EXIT,
732
721
  }
@@ -1,60 +0,0 @@
1
- export const Cleanup = {
2
- create: () => {
3
- let callbacks = []
4
-
5
- const addCallback = (callback) => {
6
- if (typeof callback !== "function") {
7
- throw new Error(`callback must be a function, got ${callback}`)
8
- }
9
-
10
- // don't register twice
11
- const existingCallback = callbacks.find((callbackCandidate) => {
12
- return callbackCandidate === callback
13
- })
14
- if (existingCallback) {
15
- if (typeof process.emitWarning === "object") {
16
- process.emitWarning(`Trying to register same callback twice`, {
17
- CODE: "CALLBACK_DUPLICATION",
18
- detail: `It's often the sign that code is executd more than once`,
19
- })
20
- } else {
21
- console.warn(`Trying to add same callback twice`)
22
- }
23
- } else {
24
- callbacks = [...callbacks, callback]
25
- }
26
-
27
- return () => {
28
- callbacks = arrayWithout(callbacks, callback)
29
- }
30
- }
31
-
32
- const cleanup = async (reason) => {
33
- const callbacksCopy = callbacks.slice()
34
- callbacks.length = 0
35
-
36
- await Promise.all(
37
- callbacksCopy.map(async (callback) => {
38
- await callback(reason)
39
- }),
40
- )
41
- }
42
-
43
- return { addCallback, cleanup }
44
- },
45
- }
46
-
47
- const arrayWithout = (array, item) => {
48
- if (array.length === 0) return array
49
- const arrayWithoutItem = []
50
- let i = 0
51
- while (i < array.length) {
52
- const value = array[i]
53
- i++
54
- if (value === item) {
55
- continue
56
- }
57
- arrayWithoutItem.push(value)
58
- }
59
- return arrayWithoutItem
60
- }
@@ -1,59 +0,0 @@
1
- /*
2
- * Taking different code paths based on different inputs is messy
3
- * and code oftens forget to clean things behind itself
4
- * "raceCallbacks" helps to do both properly with the following API:
5
- *
6
- * const winnerPromise = raceCallbacks({
7
- * timeout: (cb) => {
8
- * const timeout = setTimeout(cb, 1000)
9
- * return () => {
10
- * clearTimeout(timeout)
11
- * }
12
- * },
13
- * error: () => {
14
- * something.on('error', cb)
15
- * return () => {
16
- * return something.removeListener('error', cb)
17
- * }
18
- * },
19
- * end: () => {
20
- * something.on('end', cb)
21
- * return () => {
22
- * return something.removeListener('end', cb)
23
- * }
24
- * }
25
- * })
26
- *
27
- * // do stuff
28
- *
29
- * const winner = await winnerPromise
30
- * {
31
- * timeout: () => console.log('timeout after 1000ms'),
32
- * error: (e) => console.error('error', e),
33
- * end: (value) => console.log('end', value)
34
- * }[winner.name](winner.value)
35
- *
36
- */
37
-
38
- export const raceCallbacks = (raceDescription) => {
39
- return new Promise((resolve) => {
40
- const cleanupCallbacks = []
41
- Object.keys(raceDescription).forEach((candidateName) => {
42
- const register = raceDescription[candidateName]
43
- const returnValue = register((data) => {
44
- const cleanupCallbacksCopy = cleanupCallbacks.slice()
45
- cleanupCallbacks.length = 0
46
- cleanupCallbacksCopy.forEach((cleanup) => {
47
- cleanup()
48
- })
49
- resolve({
50
- name: candidateName,
51
- data,
52
- })
53
- })
54
- if (typeof returnValue === "function") {
55
- cleanupCallbacks.push(returnValue)
56
- }
57
- })
58
- })
59
- }