@lakutata/entrypoint 3.0.0-beta.2 → 3.0.0-beta.4

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.
@@ -123,7 +123,8 @@ export type ExpressHTTPEntrypointOptions = {
123
123
  *
124
124
  * When the client disconnects before the response, the action's abort signal fires and nothing is written. When the
125
125
  * entrypoint stops, the server stops accepting connections, the event streams end, and the requests in progress are
126
- * given up to one second after they settle to send their responses before the remaining connections are closed.
126
+ * given up to one second after they settle to send their responses before the remaining connections are closed — the
127
+ * connections taken over by an upgrade (the WebSockets of the application) too, which would hold the stop forever.
127
128
  * @param options The adapter options; by default it listens on port 80 of all the interfaces with a new express
128
129
  * application.
129
130
  * @returns The adapter, to give to the `http` option of `buildEntrypoints`.
@@ -11,6 +11,7 @@ const HTTPContext_js_1 = require("../../context/HTTPContext.js");
11
11
  const BuiltinChannels_js_1 = require("../../channel/BuiltinChannels.js");
12
12
  const Result_js_1 = require("../../lib/Result.js");
13
13
  const RouteNotFoundException_js_1 = require("../../exceptions/RouteNotFoundException.js");
14
+ const Connections_js_1 = require("../../lib/Connections.js");
14
15
  const Responders_js_1 = require("./Responders.js");
15
16
  /**
16
17
  * The first segments of the routes
@@ -56,7 +57,8 @@ function eventFrame(data, event, id) {
56
57
  *
57
58
  * When the client disconnects before the response, the action's abort signal fires and nothing is written. When the
58
59
  * entrypoint stops, the server stops accepting connections, the event streams end, and the requests in progress are
59
- * given up to one second after they settle to send their responses before the remaining connections are closed.
60
+ * given up to one second after they settle to send their responses before the remaining connections are closed — the
61
+ * connections taken over by an upgrade (the WebSockets of the application) too, which would hold the stop forever.
60
62
  * @param options The adapter options; by default it listens on port 80 of all the interfaces with a new express
61
63
  * application.
62
64
  * @returns The adapter, to give to the `http` option of `buildEntrypoints`.
@@ -107,6 +109,7 @@ function expressHTTPEntrypoint(options = {}) {
107
109
  const input = options.input ?? ((request) => ({ ...request.query, ...(request.body && typeof request.body === 'object' ? request.body : {}) }));
108
110
  const streams = new Set();
109
111
  let server;
112
+ let destroyConnections;
110
113
  let closed;
111
114
  let stopping = false;
112
115
  //The responses of the actions not sent yet
@@ -245,6 +248,7 @@ function expressHTTPEntrypoint(options = {}) {
245
248
  start: async (runtime) => {
246
249
  const app = options.app ?? (0, express_1.default)();
247
250
  server = (0, node_http_1.createServer)(options.server ?? {}, app);
251
+ destroyConnections = (0, Connections_js_1.trackConnections)(server);
248
252
  const routes = new Map();
249
253
  for (const action of runtime.actions)
250
254
  routes.set(action.address.route, (routes.get(action.address.route) ?? new Set()).add(action.address.method));
@@ -294,6 +298,8 @@ function expressHTTPEntrypoint(options = {}) {
294
298
  })]);
295
299
  clearTimeout(timer);
296
300
  server?.closeAllConnections();
301
+ //The connections taken over by an upgrade (a WebSocket) are not closed by closeAllConnections() and hold the close of the server
302
+ destroyConnections?.();
297
303
  await closed;
298
304
  await options.stop?.();
299
305
  }
@@ -109,7 +109,8 @@ export type FastifyHTTPEntrypointOptions = {
109
109
  * When the client disconnects before the response, the action's abort signal fires and nothing is written. When the
110
110
  * entrypoint stops, fastify stops accepting connections and answers the new requests with 503, the event streams end,
111
111
  * and the requests in progress are given up to one second after they settle to send their responses before the
112
- * remaining connections are closed.
112
+ * remaining connections are closed — the connections taken over by an upgrade (the WebSockets of the application) too,
113
+ * which would hold the stop forever.
113
114
  * @param options The adapter options; by default it listens on port 80 with a new fastify instance.
114
115
  * @returns The adapter, to give to the `http` option of `buildEntrypoints`.
115
116
  * @example
@@ -11,6 +11,7 @@ const HTTPContext_js_1 = require("../../context/HTTPContext.js");
11
11
  const BuiltinChannels_js_1 = require("../../channel/BuiltinChannels.js");
12
12
  const Result_js_1 = require("../../lib/Result.js");
13
13
  const RouteNotFoundException_js_1 = require("../../exceptions/RouteNotFoundException.js");
14
+ const Connections_js_1 = require("../../lib/Connections.js");
14
15
  const Responders_js_1 = require("./Responders.js");
15
16
  /**
16
17
  * The content types of the common file extensions
@@ -85,7 +86,8 @@ function isFastifyInstance(value) {
85
86
  * When the client disconnects before the response, the action's abort signal fires and nothing is written. When the
86
87
  * entrypoint stops, fastify stops accepting connections and answers the new requests with 503, the event streams end,
87
88
  * and the requests in progress are given up to one second after they settle to send their responses before the
88
- * remaining connections are closed.
89
+ * remaining connections are closed — the connections taken over by an upgrade (the WebSockets of the application) too,
90
+ * which would hold the stop forever.
89
91
  * @param options The adapter options; by default it listens on port 80 with a new fastify instance.
90
92
  * @returns The adapter, to give to the `http` option of `buildEntrypoints`.
91
93
  * @example
@@ -137,6 +139,7 @@ function fastifyHTTPEntrypoint(options = {}) {
137
139
  }));
138
140
  const streams = new Set();
139
141
  let app;
142
+ let destroyConnections;
140
143
  let closed;
141
144
  let stopping = false;
142
145
  //The responses not sent yet
@@ -266,6 +269,7 @@ function fastifyHTTPEntrypoint(options = {}) {
266
269
  start: async (runtime) => {
267
270
  const fastify = isFastifyInstance(options.fastify) ? options.fastify : (0, fastify_1.default)(options.fastify ?? {});
268
271
  app = fastify;
272
+ destroyConnections = (0, Connections_js_1.trackConnections)(fastify.server);
269
273
  const routes = new Map();
270
274
  for (const action of runtime.actions)
271
275
  routes.set(action.address.route, (routes.get(action.address.route) ?? new Set()).add(String(action.address.method).toUpperCase()));
@@ -332,6 +336,8 @@ function fastifyHTTPEntrypoint(options = {}) {
332
336
  })]);
333
337
  clearTimeout(timer);
334
338
  app?.server.closeAllConnections();
339
+ //The connections taken over by an upgrade (a WebSocket) are not closed by closeAllConnections() and hold the close of the server
340
+ destroyConnections?.();
335
341
  await closed;
336
342
  await options.stop?.();
337
343
  }
@@ -77,7 +77,8 @@ export declare function httpRequestOfContext(context: ServiceContext): IncomingM
77
77
  *
78
78
  * When a client disconnects before its response, the action's abort signal fires. When the entrypoint stops, the new
79
79
  * requests are answered with an `EntrypointStoppedException` error; once the requests in progress end, the event
80
- * streams are closed and the server shuts down.
80
+ * streams are closed and the server shuts down, closing its remaining connections (the ones taken over by an upgrade of
81
+ * the application too).
81
82
  * @param options The adapter options; by default it listens on port 3000 of all the interfaces, on the path `/`.
82
83
  * @returns The adapter, to give to the `service` option of `buildEntrypoints`.
83
84
  * @example
@@ -8,6 +8,7 @@ const ServiceContext_js_1 = require("../../context/ServiceContext.js");
8
8
  const BuiltinChannels_js_1 = require("../../channel/BuiltinChannels.js");
9
9
  const ServiceResponseCodec_js_1 = require("../../lib/ServiceResponseCodec.js");
10
10
  const EntrypointStoppedException_js_1 = require("../../exceptions/EntrypointStoppedException.js");
11
+ const Connections_js_1 = require("../../lib/Connections.js");
11
12
  /**
12
13
  * Get the HTTP request carrying a service action's request, when {@link httpServiceEntrypoint} serves it (to read its
13
14
  * headers, its remote address…).
@@ -92,7 +93,8 @@ async function readBody(request, limit) {
92
93
  *
93
94
  * When a client disconnects before its response, the action's abort signal fires. When the entrypoint stops, the new
94
95
  * requests are answered with an `EntrypointStoppedException` error; once the requests in progress end, the event
95
- * streams are closed and the server shuts down.
96
+ * streams are closed and the server shuts down, closing its remaining connections (the ones taken over by an upgrade of
97
+ * the application too).
96
98
  * @param options The adapter options; by default it listens on port 3000 of all the interfaces, on the path `/`.
97
99
  * @returns The adapter, to give to the `service` option of `buildEntrypoints`.
98
100
  * @example
@@ -135,6 +137,7 @@ function httpServiceEntrypoint(options = {}) {
135
137
  const eventsPath = `${base}/events`;
136
138
  const limit = options.limit ?? 10 * 1024 * 1024;
137
139
  let server;
140
+ let destroyConnections;
138
141
  let stopping = false;
139
142
  let keepAlive;
140
143
  //The event streams of each client (a client may listen several times)
@@ -222,6 +225,7 @@ function httpServiceEntrypoint(options = {}) {
222
225
  //The event streams kept open by the proxies
223
226
  keepAlive = setInterval(() => clients.forEach((streams) => streams.forEach((stream) => stream.write(': keep-alive\n\n'))), 15000);
224
227
  keepAlive.unref();
228
+ destroyConnections = (0, Connections_js_1.trackConnections)(server);
225
229
  await new Promise((resolve, reject) => {
226
230
  server.once('error', reject).listen(options.port ?? 3000, options.host, () => {
227
231
  server.off('error', reject);
@@ -242,6 +246,8 @@ function httpServiceEntrypoint(options = {}) {
242
246
  return;
243
247
  const closing = new Promise((resolve) => void server.close(() => resolve()));
244
248
  server.closeAllConnections();
249
+ //The connections taken over by an upgrade (a WebSocket) are not closed by closeAllConnections() and hold the close of the server
250
+ destroyConnections?.();
245
251
  await closing;
246
252
  },
247
253
  notify: (clientId, event, args) => {
@@ -103,7 +103,8 @@ export type JSONRPCServiceEntrypointOptions = JSONRPCMappingOptions & {
103
103
  *
104
104
  * When an HTTP client disconnects before its response, the action's abort signal fires. When the entrypoint stops,
105
105
  * stdio stops reading, the new requests are answered with `STOPPING`, and once the requests in progress are answered
106
- * the WebSocket connections are closed (code 1001) and the server shuts down.
106
+ * the WebSocket connections are closed (code 1001; the ones not answering within a second are terminated) and the
107
+ * server shuts down, closing its remaining connections (the ones taken over by an upgrade of the application too).
107
108
  * @param options The adapter options; by default it serves HTTP on port 3000 of all the interfaces, on the path `/`.
108
109
  * @returns The adapter, to give to the `service` option of `buildEntrypoints`.
109
110
  * @example
@@ -39,6 +39,7 @@ const node_http_1 = require("node:http");
39
39
  const BuiltinChannels_js_1 = require("../../channel/BuiltinChannels.js");
40
40
  const JSONRPCProtocol_js_1 = require("./JSONRPCProtocol.js");
41
41
  const ApplicationOf_js_1 = require("../../lib/ApplicationOf.js");
42
+ const Connections_js_1 = require("../../lib/Connections.js");
42
43
  /**
43
44
  * The headers of the responses of HTTP: the tools of the browsers may call the adapter
44
45
  */
@@ -173,7 +174,8 @@ function readMessages(input, framing, limit, receive, ended, oversized) {
173
174
  *
174
175
  * When an HTTP client disconnects before its response, the action's abort signal fires. When the entrypoint stops,
175
176
  * stdio stops reading, the new requests are answered with `STOPPING`, and once the requests in progress are answered
176
- * the WebSocket connections are closed (code 1001) and the server shuts down.
177
+ * the WebSocket connections are closed (code 1001; the ones not answering within a second are terminated) and the
178
+ * server shuts down, closing its remaining connections (the ones taken over by an upgrade of the application too).
177
179
  * @param options The adapter options; by default it serves HTTP on port 3000 of all the interfaces, on the path `/`.
178
180
  * @returns The adapter, to give to the `service` option of `buildEntrypoints`.
179
181
  * @example
@@ -228,6 +230,7 @@ function jsonRPCServiceEntrypoint(options = {}) {
228
230
  const path = (options.path ?? '/').replace(/(.)\/+$/, '$1');
229
231
  let stopping = false;
230
232
  let server;
233
+ let destroyConnections;
231
234
  let webSocketServer;
232
235
  let stopReading;
233
236
  let heartbeat;
@@ -263,6 +266,7 @@ function jsonRPCServiceEntrypoint(options = {}) {
263
266
  return handled;
264
267
  };
265
268
  const listen = async (target) => {
269
+ destroyConnections = (0, Connections_js_1.trackConnections)(target);
266
270
  await new Promise((resolve, reject) => {
267
271
  target.once('error', reject).listen(options.port ?? 3000, options.host, () => {
268
272
  target.off('error', reject);
@@ -375,13 +379,25 @@ function jsonRPCServiceEntrypoint(options = {}) {
375
379
  await Promise.allSettled([...inProgress]);
376
380
  clients.clear();
377
381
  if (webSocketServer) {
378
- webSocketServer.clients.forEach((socket) => socket.close(1001, 'The service stops'));
379
- await new Promise((resolve) => webSocketServer.close(() => resolve()));
382
+ //The WebSockets are given a second to answer their close, then the ones not answering are terminated (the
383
+ //close of the server waits for them all)
384
+ const sockets = webSocketServer.clients;
385
+ const closedSockets = new Promise((resolve) => webSocketServer.close(() => resolve()));
386
+ sockets.forEach((socket) => socket.close(1001, 'The service stops'));
387
+ let timer;
388
+ await Promise.race([closedSockets, new Promise((resolve) => {
389
+ timer = setTimeout(resolve, 1000);
390
+ })]);
391
+ clearTimeout(timer);
392
+ sockets.forEach((socket) => socket.terminate());
393
+ await closedSockets;
380
394
  }
381
395
  if (!server)
382
396
  return;
383
397
  const closing = new Promise((resolve) => void server.close(() => resolve()));
384
398
  server.closeAllConnections();
399
+ //The WebSockets not answering their close, and the connections taken over by an upgrade of the application, hold the close of the server
400
+ destroyConnections?.();
385
401
  await closing;
386
402
  },
387
403
  notify: (clientId, event, args) => {
@@ -0,0 +1,10 @@
1
+ import type { Server } from 'node:net';
2
+ /**
3
+ * Track the connections of a server, to destroy the ones its `closeAllConnections()` leaves open: the connections taken
4
+ * over by an upgrade (a WebSocket: socket.io, ws, the hot reload of a development server) or by a CONNECT request,
5
+ * which hold the close of the server forever. Unlike a listener of 'upgrade', it does not change how the server answers
6
+ * the requests (without one, an upgrade request is answered as any request).
7
+ * @param server The server whose connections are tracked, before it listens.
8
+ * @returns A function destroying the connections still open.
9
+ */
10
+ export declare function trackConnections(server: Server): () => void;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.trackConnections = trackConnections;
4
+ /**
5
+ * Track the connections of a server, to destroy the ones its `closeAllConnections()` leaves open: the connections taken
6
+ * over by an upgrade (a WebSocket: socket.io, ws, the hot reload of a development server) or by a CONNECT request,
7
+ * which hold the close of the server forever. Unlike a listener of 'upgrade', it does not change how the server answers
8
+ * the requests (without one, an upgrade request is answered as any request).
9
+ * @param server The server whose connections are tracked, before it listens.
10
+ * @returns A function destroying the connections still open.
11
+ */
12
+ function trackConnections(server) {
13
+ const sockets = new Set();
14
+ server.on('connection', (socket) => {
15
+ sockets.add(socket);
16
+ socket.once('close', () => sockets.delete(socket));
17
+ });
18
+ return () => sockets.forEach((socket) => void socket.destroy());
19
+ }
@@ -5,6 +5,7 @@ import { HTTPContext } from '../../context/HTTPContext.js';
5
5
  import { HTTPChannel } from '../../channel/BuiltinChannels.js';
6
6
  import { EventsResult, FileResult, StatusResult } from '../../lib/Result.js';
7
7
  import { RouteNotFoundException } from '../../exceptions/RouteNotFoundException.js';
8
+ import { trackConnections } from '../../lib/Connections.js';
8
9
  import { JSONResponder } from './Responders.js';
9
10
  /**
10
11
  * The first segments of the routes
@@ -50,7 +51,8 @@ function eventFrame(data, event, id) {
50
51
  *
51
52
  * When the client disconnects before the response, the action's abort signal fires and nothing is written. When the
52
53
  * entrypoint stops, the server stops accepting connections, the event streams end, and the requests in progress are
53
- * given up to one second after they settle to send their responses before the remaining connections are closed.
54
+ * given up to one second after they settle to send their responses before the remaining connections are closed — the
55
+ * connections taken over by an upgrade (the WebSockets of the application) too, which would hold the stop forever.
54
56
  * @param options The adapter options; by default it listens on port 80 of all the interfaces with a new express
55
57
  * application.
56
58
  * @returns The adapter, to give to the `http` option of `buildEntrypoints`.
@@ -101,6 +103,7 @@ export function expressHTTPEntrypoint(options = {}) {
101
103
  const input = options.input ?? ((request) => ({ ...request.query, ...(request.body && typeof request.body === 'object' ? request.body : {}) }));
102
104
  const streams = new Set();
103
105
  let server;
106
+ let destroyConnections;
104
107
  let closed;
105
108
  let stopping = false;
106
109
  //The responses of the actions not sent yet
@@ -239,6 +242,7 @@ export function expressHTTPEntrypoint(options = {}) {
239
242
  start: async (runtime) => {
240
243
  const app = options.app ?? express();
241
244
  server = createServer(options.server ?? {}, app);
245
+ destroyConnections = trackConnections(server);
242
246
  const routes = new Map();
243
247
  for (const action of runtime.actions)
244
248
  routes.set(action.address.route, (routes.get(action.address.route) ?? new Set()).add(action.address.method));
@@ -288,6 +292,8 @@ export function expressHTTPEntrypoint(options = {}) {
288
292
  })]);
289
293
  clearTimeout(timer);
290
294
  server?.closeAllConnections();
295
+ //The connections taken over by an upgrade (a WebSocket) are not closed by closeAllConnections() and hold the close of the server
296
+ destroyConnections?.();
291
297
  await closed;
292
298
  await options.stop?.();
293
299
  }
@@ -5,6 +5,7 @@ import { HTTPContext } from '../../context/HTTPContext.js';
5
5
  import { HTTPChannel } from '../../channel/BuiltinChannels.js';
6
6
  import { EventsResult, FileResult, StatusResult } from '../../lib/Result.js';
7
7
  import { RouteNotFoundException } from '../../exceptions/RouteNotFoundException.js';
8
+ import { trackConnections } from '../../lib/Connections.js';
8
9
  import { FastifyJSONResponder } from './Responders.js';
9
10
  /**
10
11
  * The content types of the common file extensions
@@ -79,7 +80,8 @@ function isFastifyInstance(value) {
79
80
  * When the client disconnects before the response, the action's abort signal fires and nothing is written. When the
80
81
  * entrypoint stops, fastify stops accepting connections and answers the new requests with 503, the event streams end,
81
82
  * and the requests in progress are given up to one second after they settle to send their responses before the
82
- * remaining connections are closed.
83
+ * remaining connections are closed — the connections taken over by an upgrade (the WebSockets of the application) too,
84
+ * which would hold the stop forever.
83
85
  * @param options The adapter options; by default it listens on port 80 with a new fastify instance.
84
86
  * @returns The adapter, to give to the `http` option of `buildEntrypoints`.
85
87
  * @example
@@ -131,6 +133,7 @@ export function fastifyHTTPEntrypoint(options = {}) {
131
133
  }));
132
134
  const streams = new Set();
133
135
  let app;
136
+ let destroyConnections;
134
137
  let closed;
135
138
  let stopping = false;
136
139
  //The responses not sent yet
@@ -260,6 +263,7 @@ export function fastifyHTTPEntrypoint(options = {}) {
260
263
  start: async (runtime) => {
261
264
  const fastify = isFastifyInstance(options.fastify) ? options.fastify : Fastify(options.fastify ?? {});
262
265
  app = fastify;
266
+ destroyConnections = trackConnections(fastify.server);
263
267
  const routes = new Map();
264
268
  for (const action of runtime.actions)
265
269
  routes.set(action.address.route, (routes.get(action.address.route) ?? new Set()).add(String(action.address.method).toUpperCase()));
@@ -326,6 +330,8 @@ export function fastifyHTTPEntrypoint(options = {}) {
326
330
  })]);
327
331
  clearTimeout(timer);
328
332
  app?.server.closeAllConnections();
333
+ //The connections taken over by an upgrade (a WebSocket) are not closed by closeAllConnections() and hold the close of the server
334
+ destroyConnections?.();
329
335
  await closed;
330
336
  await options.stop?.();
331
337
  }
@@ -4,6 +4,7 @@ import { ServiceContext } from '../../context/ServiceContext.js';
4
4
  import { ServiceChannel } from '../../channel/BuiltinChannels.js';
5
5
  import { ServiceResponseCodec } from '../../lib/ServiceResponseCodec.js';
6
6
  import { EntrypointStoppedException } from '../../exceptions/EntrypointStoppedException.js';
7
+ import { trackConnections } from '../../lib/Connections.js';
7
8
  /**
8
9
  * Get the HTTP request carrying a service action's request, when {@link httpServiceEntrypoint} serves it (to read its
9
10
  * headers, its remote address…).
@@ -88,7 +89,8 @@ async function readBody(request, limit) {
88
89
  *
89
90
  * When a client disconnects before its response, the action's abort signal fires. When the entrypoint stops, the new
90
91
  * requests are answered with an `EntrypointStoppedException` error; once the requests in progress end, the event
91
- * streams are closed and the server shuts down.
92
+ * streams are closed and the server shuts down, closing its remaining connections (the ones taken over by an upgrade of
93
+ * the application too).
92
94
  * @param options The adapter options; by default it listens on port 3000 of all the interfaces, on the path `/`.
93
95
  * @returns The adapter, to give to the `service` option of `buildEntrypoints`.
94
96
  * @example
@@ -131,6 +133,7 @@ export function httpServiceEntrypoint(options = {}) {
131
133
  const eventsPath = `${base}/events`;
132
134
  const limit = options.limit ?? 10 * 1024 * 1024;
133
135
  let server;
136
+ let destroyConnections;
134
137
  let stopping = false;
135
138
  let keepAlive;
136
139
  //The event streams of each client (a client may listen several times)
@@ -218,6 +221,7 @@ export function httpServiceEntrypoint(options = {}) {
218
221
  //The event streams kept open by the proxies
219
222
  keepAlive = setInterval(() => clients.forEach((streams) => streams.forEach((stream) => stream.write(': keep-alive\n\n'))), 15000);
220
223
  keepAlive.unref();
224
+ destroyConnections = trackConnections(server);
221
225
  await new Promise((resolve, reject) => {
222
226
  server.once('error', reject).listen(options.port ?? 3000, options.host, () => {
223
227
  server.off('error', reject);
@@ -238,6 +242,8 @@ export function httpServiceEntrypoint(options = {}) {
238
242
  return;
239
243
  const closing = new Promise((resolve) => void server.close(() => resolve()));
240
244
  server.closeAllConnections();
245
+ //The connections taken over by an upgrade (a WebSocket) are not closed by closeAllConnections() and hold the close of the server
246
+ destroyConnections?.();
241
247
  await closing;
242
248
  },
243
249
  notify: (clientId, event, args) => {
@@ -3,6 +3,7 @@ import { createServer } from 'node:http';
3
3
  import { ServiceChannel } from '../../channel/BuiltinChannels.js';
4
4
  import { JSONRPCErrorCode, JSONRPCProtocol } from './JSONRPCProtocol.js';
5
5
  import { applicationOf } from '../../lib/ApplicationOf.js';
6
+ import { trackConnections } from '../../lib/Connections.js';
6
7
  /**
7
8
  * The headers of the responses of HTTP: the tools of the browsers may call the adapter
8
9
  */
@@ -137,7 +138,8 @@ function readMessages(input, framing, limit, receive, ended, oversized) {
137
138
  *
138
139
  * When an HTTP client disconnects before its response, the action's abort signal fires. When the entrypoint stops,
139
140
  * stdio stops reading, the new requests are answered with `STOPPING`, and once the requests in progress are answered
140
- * the WebSocket connections are closed (code 1001) and the server shuts down.
141
+ * the WebSocket connections are closed (code 1001; the ones not answering within a second are terminated) and the
142
+ * server shuts down, closing its remaining connections (the ones taken over by an upgrade of the application too).
141
143
  * @param options The adapter options; by default it serves HTTP on port 3000 of all the interfaces, on the path `/`.
142
144
  * @returns The adapter, to give to the `service` option of `buildEntrypoints`.
143
145
  * @example
@@ -192,6 +194,7 @@ export function jsonRPCServiceEntrypoint(options = {}) {
192
194
  const path = (options.path ?? '/').replace(/(.)\/+$/, '$1');
193
195
  let stopping = false;
194
196
  let server;
197
+ let destroyConnections;
195
198
  let webSocketServer;
196
199
  let stopReading;
197
200
  let heartbeat;
@@ -227,6 +230,7 @@ export function jsonRPCServiceEntrypoint(options = {}) {
227
230
  return handled;
228
231
  };
229
232
  const listen = async (target) => {
233
+ destroyConnections = trackConnections(target);
230
234
  await new Promise((resolve, reject) => {
231
235
  target.once('error', reject).listen(options.port ?? 3000, options.host, () => {
232
236
  target.off('error', reject);
@@ -339,13 +343,25 @@ export function jsonRPCServiceEntrypoint(options = {}) {
339
343
  await Promise.allSettled([...inProgress]);
340
344
  clients.clear();
341
345
  if (webSocketServer) {
342
- webSocketServer.clients.forEach((socket) => socket.close(1001, 'The service stops'));
343
- await new Promise((resolve) => webSocketServer.close(() => resolve()));
346
+ //The WebSockets are given a second to answer their close, then the ones not answering are terminated (the
347
+ //close of the server waits for them all)
348
+ const sockets = webSocketServer.clients;
349
+ const closedSockets = new Promise((resolve) => webSocketServer.close(() => resolve()));
350
+ sockets.forEach((socket) => socket.close(1001, 'The service stops'));
351
+ let timer;
352
+ await Promise.race([closedSockets, new Promise((resolve) => {
353
+ timer = setTimeout(resolve, 1000);
354
+ })]);
355
+ clearTimeout(timer);
356
+ sockets.forEach((socket) => socket.terminate());
357
+ await closedSockets;
344
358
  }
345
359
  if (!server)
346
360
  return;
347
361
  const closing = new Promise((resolve) => void server.close(() => resolve()));
348
362
  server.closeAllConnections();
363
+ //The WebSockets not answering their close, and the connections taken over by an upgrade of the application, hold the close of the server
364
+ destroyConnections?.();
349
365
  await closing;
350
366
  },
351
367
  notify: (clientId, event, args) => {
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Track the connections of a server, to destroy the ones its `closeAllConnections()` leaves open: the connections taken
3
+ * over by an upgrade (a WebSocket: socket.io, ws, the hot reload of a development server) or by a CONNECT request,
4
+ * which hold the close of the server forever. Unlike a listener of 'upgrade', it does not change how the server answers
5
+ * the requests (without one, an upgrade request is answered as any request).
6
+ * @param server The server whose connections are tracked, before it listens.
7
+ * @returns A function destroying the connections still open.
8
+ */
9
+ export function trackConnections(server) {
10
+ const sockets = new Set();
11
+ server.on('connection', (socket) => {
12
+ sockets.add(socket);
13
+ socket.once('close', () => sockets.delete(socket));
14
+ });
15
+ return () => sockets.forEach((socket) => void socket.destroy());
16
+ }
@@ -123,7 +123,8 @@ export type ExpressHTTPEntrypointOptions = {
123
123
  *
124
124
  * When the client disconnects before the response, the action's abort signal fires and nothing is written. When the
125
125
  * entrypoint stops, the server stops accepting connections, the event streams end, and the requests in progress are
126
- * given up to one second after they settle to send their responses before the remaining connections are closed.
126
+ * given up to one second after they settle to send their responses before the remaining connections are closed — the
127
+ * connections taken over by an upgrade (the WebSockets of the application) too, which would hold the stop forever.
127
128
  * @param options The adapter options; by default it listens on port 80 of all the interfaces with a new express
128
129
  * application.
129
130
  * @returns The adapter, to give to the `http` option of `buildEntrypoints`.
@@ -109,7 +109,8 @@ export type FastifyHTTPEntrypointOptions = {
109
109
  * When the client disconnects before the response, the action's abort signal fires and nothing is written. When the
110
110
  * entrypoint stops, fastify stops accepting connections and answers the new requests with 503, the event streams end,
111
111
  * and the requests in progress are given up to one second after they settle to send their responses before the
112
- * remaining connections are closed.
112
+ * remaining connections are closed — the connections taken over by an upgrade (the WebSockets of the application) too,
113
+ * which would hold the stop forever.
113
114
  * @param options The adapter options; by default it listens on port 80 with a new fastify instance.
114
115
  * @returns The adapter, to give to the `http` option of `buildEntrypoints`.
115
116
  * @example
@@ -77,7 +77,8 @@ export declare function httpRequestOfContext(context: ServiceContext): IncomingM
77
77
  *
78
78
  * When a client disconnects before its response, the action's abort signal fires. When the entrypoint stops, the new
79
79
  * requests are answered with an `EntrypointStoppedException` error; once the requests in progress end, the event
80
- * streams are closed and the server shuts down.
80
+ * streams are closed and the server shuts down, closing its remaining connections (the ones taken over by an upgrade of
81
+ * the application too).
81
82
  * @param options The adapter options; by default it listens on port 3000 of all the interfaces, on the path `/`.
82
83
  * @returns The adapter, to give to the `service` option of `buildEntrypoints`.
83
84
  * @example
@@ -103,7 +103,8 @@ export type JSONRPCServiceEntrypointOptions = JSONRPCMappingOptions & {
103
103
  *
104
104
  * When an HTTP client disconnects before its response, the action's abort signal fires. When the entrypoint stops,
105
105
  * stdio stops reading, the new requests are answered with `STOPPING`, and once the requests in progress are answered
106
- * the WebSocket connections are closed (code 1001) and the server shuts down.
106
+ * the WebSocket connections are closed (code 1001; the ones not answering within a second are terminated) and the
107
+ * server shuts down, closing its remaining connections (the ones taken over by an upgrade of the application too).
107
108
  * @param options The adapter options; by default it serves HTTP on port 3000 of all the interfaces, on the path `/`.
108
109
  * @returns The adapter, to give to the `service` option of `buildEntrypoints`.
109
110
  * @example
@@ -0,0 +1,10 @@
1
+ import type { Server } from 'node:net';
2
+ /**
3
+ * Track the connections of a server, to destroy the ones its `closeAllConnections()` leaves open: the connections taken
4
+ * over by an upgrade (a WebSocket: socket.io, ws, the hot reload of a development server) or by a CONNECT request,
5
+ * which hold the close of the server forever. Unlike a listener of 'upgrade', it does not change how the server answers
6
+ * the requests (without one, an upgrade request is answered as any request).
7
+ * @param server The server whose connections are tracked, before it listens.
8
+ * @returns A function destroying the connections still open.
9
+ */
10
+ export declare function trackConnections(server: Server): () => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lakutata/entrypoint",
3
- "version": "3.0.0-beta.2",
3
+ "version": "3.0.0-beta.4",
4
4
  "description": "The entrypoint component of the lakutata framework: controllers and the dispatch of the CLI, HTTP and service actions.",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/exports/Entrypoint.js",
@@ -110,8 +110,8 @@
110
110
  },
111
111
  "peerDependencies": {
112
112
  "@inquirer/prompts": "^7.0.0 || ^8.0.0",
113
- "@lakutata/core": "3.0.0-beta.2",
114
- "@lakutata/utils": "3.0.0-beta.2",
113
+ "@lakutata/core": "3.0.0-beta.4",
114
+ "@lakutata/utils": "3.0.0-beta.4",
115
115
  "commander": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0",
116
116
  "express": "^4.21.0 || ^5.0.0",
117
117
  "fastify": "^5.0.0",