@nodefony/http 10.0.0-alpha.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 (155) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +77 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
  6. package/dist/index.js +108 -0
  7. package/dist/nodefony/command/assetsPublishCommand.js +102 -0
  8. package/dist/nodefony/command/certificatesCommand.js +47 -0
  9. package/dist/nodefony/command/networkCommand.js +27 -0
  10. package/dist/nodefony/command/proxyGenerateCommand.js +66 -0
  11. package/dist/nodefony/config/config.js +335 -0
  12. package/dist/nodefony/config/defineModuleConfig.js +93 -0
  13. package/dist/nodefony/interfaces/IContext.js +1 -0
  14. package/dist/nodefony/interfaces/ICookie.js +1 -0
  15. package/dist/nodefony/interfaces/IErrorRenderer.js +1 -0
  16. package/dist/nodefony/interfaces/IHttpConfig.js +1 -0
  17. package/dist/nodefony/interfaces/IHttpKernel.js +1 -0
  18. package/dist/nodefony/interfaces/IRequest.js +1 -0
  19. package/dist/nodefony/interfaces/IRequestLogger.js +1 -0
  20. package/dist/nodefony/interfaces/IResponse.js +1 -0
  21. package/dist/nodefony/interfaces/ISession.js +1 -0
  22. package/dist/nodefony/interfaces/IUpload.js +1 -0
  23. package/dist/nodefony/interfaces/index.js +1 -0
  24. package/dist/nodefony/service/HttpAdminApi.js +376 -0
  25. package/dist/nodefony/service/ProfilerAdminApi.js +73 -0
  26. package/dist/nodefony/service/audit-logger.js +159 -0
  27. package/dist/nodefony/service/certificates.js +545 -0
  28. package/dist/nodefony/service/error-renderer.js +320 -0
  29. package/dist/nodefony/service/http-kernel.js +948 -0
  30. package/dist/nodefony/service/pretty-request-logger.js +72 -0
  31. package/dist/nodefony/service/request-logger.js +54 -0
  32. package/dist/nodefony/service/servers/clientError.js +20 -0
  33. package/dist/nodefony/service/servers/server-http.js +135 -0
  34. package/dist/nodefony/service/servers/server-https.js +204 -0
  35. package/dist/nodefony/service/servers/server-static.js +192 -0
  36. package/dist/nodefony/service/servers/server-websocket-secure.js +104 -0
  37. package/dist/nodefony/service/servers/server-websocket.js +104 -0
  38. package/dist/nodefony/service/servers/serverShutdown.js +31 -0
  39. package/dist/nodefony/service/servers/wsHeartbeat.js +64 -0
  40. package/dist/nodefony/service/sessions/sessions-service.js +580 -0
  41. package/dist/nodefony/service/trace.js +72 -0
  42. package/dist/nodefony/service/upload/upload-service.js +171 -0
  43. package/dist/nodefony/src/assets/collectAssets.js +34 -0
  44. package/dist/nodefony/src/assets/prebuiltUi.js +125 -0
  45. package/dist/nodefony/src/context/Context.js +415 -0
  46. package/dist/nodefony/src/context/domainMatcher.js +88 -0
  47. package/dist/nodefony/src/context/forwarded.js +185 -0
  48. package/dist/nodefony/src/context/http/HttpContext.js +309 -0
  49. package/dist/nodefony/src/context/http/Request.js +543 -0
  50. package/dist/nodefony/src/context/http/Response.js +368 -0
  51. package/dist/nodefony/src/context/http/parser.js +188 -0
  52. package/dist/nodefony/src/context/http/urlFastPath.js +103 -0
  53. package/dist/nodefony/src/context/http2/Request.js +29 -0
  54. package/dist/nodefony/src/context/http2/Response.js +97 -0
  55. package/dist/nodefony/src/context/metaData.js +47 -0
  56. package/dist/nodefony/src/context/requestId.js +41 -0
  57. package/dist/nodefony/src/context/trustProxy.js +167 -0
  58. package/dist/nodefony/src/context/websocket/Response.js +181 -0
  59. package/dist/nodefony/src/context/websocket/WebsocketContext.js +389 -0
  60. package/dist/nodefony/src/context/websocket/wsBackpressure.js +56 -0
  61. package/dist/nodefony/src/context/websocket/wsLogContent.js +68 -0
  62. package/dist/nodefony/src/cookies/cookie.js +258 -0
  63. package/dist/nodefony/src/errors/httpError.js +69 -0
  64. package/dist/nodefony/src/profiler/FrameProfile.js +95 -0
  65. package/dist/nodefony/src/profiler/Profiler.js +139 -0
  66. package/dist/nodefony/src/proxy/generateProxyConfig.js +157 -0
  67. package/dist/nodefony/src/rateLimit/IRateLimitStore.js +1 -0
  68. package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +146 -0
  69. package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +64 -0
  70. package/dist/nodefony/src/rateLimit/rateLimitFilters.js +20 -0
  71. package/dist/nodefony/src/servers/portBinder.js +114 -0
  72. package/dist/nodefony/src/session/session.js +390 -0
  73. package/dist/nodefony/src/session/storage/MemorySessionStorage.js +185 -0
  74. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +137 -0
  75. package/dist/nodefony/src/session/storage/sessionFilters.js +83 -0
  76. package/dist/nodefony/src/session/storage/sessionSort.js +53 -0
  77. package/dist/types/index.d.ts +83 -0
  78. package/dist/types/nodefony/command/assetsPublishCommand.d.ts +23 -0
  79. package/dist/types/nodefony/command/certificatesCommand.d.ts +17 -0
  80. package/dist/types/nodefony/command/networkCommand.d.ts +8 -0
  81. package/dist/types/nodefony/command/proxyGenerateCommand.d.ts +19 -0
  82. package/dist/types/nodefony/config/config.d.ts +197 -0
  83. package/dist/types/nodefony/config/defineModuleConfig.d.ts +39 -0
  84. package/dist/types/nodefony/interfaces/IContext.d.ts +138 -0
  85. package/dist/types/nodefony/interfaces/ICookie.d.ts +47 -0
  86. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +55 -0
  87. package/dist/types/nodefony/interfaces/IHttpConfig.d.ts +12 -0
  88. package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -0
  89. package/dist/types/nodefony/interfaces/IRequest.d.ts +35 -0
  90. package/dist/types/nodefony/interfaces/IRequestLogger.d.ts +31 -0
  91. package/dist/types/nodefony/interfaces/IResponse.d.ts +39 -0
  92. package/dist/types/nodefony/interfaces/ISession.d.ts +283 -0
  93. package/dist/types/nodefony/interfaces/IUpload.d.ts +66 -0
  94. package/dist/types/nodefony/interfaces/index.d.ts +7 -0
  95. package/dist/types/nodefony/service/HttpAdminApi.d.ts +18 -0
  96. package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +23 -0
  97. package/dist/types/nodefony/service/audit-logger.d.ts +143 -0
  98. package/dist/types/nodefony/service/certificates.d.ts +246 -0
  99. package/dist/types/nodefony/service/error-renderer.d.ts +74 -0
  100. package/dist/types/nodefony/service/http-kernel.d.ts +377 -0
  101. package/dist/types/nodefony/service/pretty-request-logger.d.ts +25 -0
  102. package/dist/types/nodefony/service/request-logger.d.ts +18 -0
  103. package/dist/types/nodefony/service/servers/clientError.d.ts +14 -0
  104. package/dist/types/nodefony/service/servers/server-http.d.ts +42 -0
  105. package/dist/types/nodefony/service/servers/server-https.d.ts +41 -0
  106. package/dist/types/nodefony/service/servers/server-static.d.ts +62 -0
  107. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +29 -0
  108. package/dist/types/nodefony/service/servers/server-websocket.d.ts +29 -0
  109. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +27 -0
  110. package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +46 -0
  111. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +218 -0
  112. package/dist/types/nodefony/service/trace.d.ts +39 -0
  113. package/dist/types/nodefony/service/upload/upload-service.d.ts +61 -0
  114. package/dist/types/nodefony/src/assets/collectAssets.d.ts +35 -0
  115. package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +99 -0
  116. package/dist/types/nodefony/src/context/Context.d.ts +195 -0
  117. package/dist/types/nodefony/src/context/domainMatcher.d.ts +67 -0
  118. package/dist/types/nodefony/src/context/forwarded.d.ts +95 -0
  119. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +85 -0
  120. package/dist/types/nodefony/src/context/http/Request.d.ts +203 -0
  121. package/dist/types/nodefony/src/context/http/Response.d.ts +68 -0
  122. package/dist/types/nodefony/src/context/http/parser.d.ts +65 -0
  123. package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +52 -0
  124. package/dist/types/nodefony/src/context/http2/Request.d.ts +14 -0
  125. package/dist/types/nodefony/src/context/http2/Response.d.ts +20 -0
  126. package/dist/types/nodefony/src/context/metaData.d.ts +58 -0
  127. package/dist/types/nodefony/src/context/requestId.d.ts +28 -0
  128. package/dist/types/nodefony/src/context/trustProxy.d.ts +77 -0
  129. package/dist/types/nodefony/src/context/websocket/Response.d.ts +53 -0
  130. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +125 -0
  131. package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +73 -0
  132. package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +37 -0
  133. package/dist/types/nodefony/src/cookies/cookie.d.ts +88 -0
  134. package/dist/types/nodefony/src/errors/httpError.d.ts +15 -0
  135. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +110 -0
  136. package/dist/types/nodefony/src/profiler/Profiler.d.ts +192 -0
  137. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +76 -0
  138. package/dist/types/nodefony/src/rateLimit/IRateLimitStore.d.ts +98 -0
  139. package/dist/types/nodefony/src/rateLimit/MemoryRateLimitStore.d.ts +40 -0
  140. package/dist/types/nodefony/src/rateLimit/WsConnectionCounter.d.ts +37 -0
  141. package/dist/types/nodefony/src/rateLimit/rateLimitFilters.d.ts +18 -0
  142. package/dist/types/nodefony/src/servers/portBinder.d.ts +102 -0
  143. package/dist/types/nodefony/src/session/session.d.ts +171 -0
  144. package/dist/types/nodefony/src/session/storage/MemorySessionStorage.d.ts +77 -0
  145. package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +81 -0
  146. package/dist/types/nodefony/src/session/storage/sessionFilters.d.ts +102 -0
  147. package/dist/types/nodefony/src/session/storage/sessionSort.d.ts +45 -0
  148. package/docs/cookies.md +365 -0
  149. package/docs/index.md +163 -0
  150. package/docs/observabilite.md +460 -0
  151. package/docs/rate-limit.md +372 -0
  152. package/docs/servers.md +935 -0
  153. package/docs/session.md +768 -0
  154. package/docs/upload.md +460 -0
  155. package/package.json +101 -0
@@ -0,0 +1,104 @@
1
+ import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js";
2
+ import __decorate from "../../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
3
+ import http_kernel_default from "../http-kernel.js";
4
+ import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js";
5
+ import { startHeartbeat, trackPong } from "./wsHeartbeat.js";
6
+ import { Module, Service, inject } from "nodefony";
7
+ import Ws, { WebSocketServer } from "ws";
8
+ //#region nodefony/service/servers/server-websocket-secure.ts
9
+ let WebsocketSecure = class WebsocketSecure extends Service {
10
+ httpKernel;
11
+ module;
12
+ ready = false;
13
+ server = null;
14
+ port;
15
+ domain;
16
+ protocol = "1.1";
17
+ family = null;
18
+ scheme = "wss";
19
+ address = null;
20
+ type = "websocket-secure";
21
+ infos = null;
22
+ /** Timer keep-alive (UN par serveur). `null` si désactivé ou pas démarré. */
23
+ heartbeatTimer = null;
24
+ constructor(module, httpKernel) {
25
+ super("server-websocket-secure", module.container, module.notificationsCenter, module.options.websocketSecure);
26
+ this.httpKernel = httpKernel;
27
+ this.module = module;
28
+ this.port = this.setPort();
29
+ this.domain = this.kernel?.domain;
30
+ this.ready = false;
31
+ }
32
+ setPort() {
33
+ if (this.kernel?.options.servers?.https) return this.kernel?.options.servers?.https?.port || 0;
34
+ return 0;
35
+ }
36
+ async createServer(serverHttps) {
37
+ return new Promise((resolve, reject) => {
38
+ try {
39
+ this.infos = serverHttps.server.address();
40
+ if (this.infos) {
41
+ this.port = this.infos.port;
42
+ this.address = this.infos.address;
43
+ this.family = this.infos.family;
44
+ this.protocol = serverHttps.protocol;
45
+ }
46
+ this.server = new WebSocketServer({
47
+ ...this.options ?? {},
48
+ server: serverHttps.server,
49
+ clientTracking: true
50
+ });
51
+ this.server.on("connection", this.onConnection.bind(this));
52
+ this.heartbeatTimer = startHeartbeat(this.server, this.options);
53
+ this.kernel?.prependOnceListener("onTerminate", this.terminate.bind(this));
54
+ if (this.server) this.ready = true;
55
+ this.module.fire("onServersReady", this.type, this);
56
+ return resolve(this.server);
57
+ } catch (e) {
58
+ this.log(e, "ERROR");
59
+ return reject(e);
60
+ }
61
+ });
62
+ }
63
+ onConnection(ws, req) {
64
+ trackPong(ws);
65
+ this.httpKernel.onWebsocketRequest(ws, req, this.type).catch(() => {
66
+ process.nextTick(() => {});
67
+ });
68
+ }
69
+ terminate() {
70
+ return new Promise((resolve, reject) => {
71
+ if (this.heartbeatTimer) {
72
+ clearInterval(this.heartbeatTimer);
73
+ this.heartbeatTimer = null;
74
+ }
75
+ if (this.server && this.ready) {
76
+ const shutdownMsg = JSON.stringify({ nodefony: { state: "shutDown" } });
77
+ this.server.clients.forEach((client) => {
78
+ if (client.readyState === Ws.OPEN) {
79
+ client.send(shutdownMsg);
80
+ client.close(1001, "Server shutting down");
81
+ }
82
+ });
83
+ setTimeout(() => {
84
+ try {
85
+ this.server?.close();
86
+ this.log(` SHUTDOWN WEBSOCKET Server is listening on DOMAIN : ${this.domain} PORT : ${this.port}`, "INFO");
87
+ return resolve(true);
88
+ } catch (e) {
89
+ return reject(e);
90
+ }
91
+ }, 300);
92
+ return;
93
+ }
94
+ return resolve(true);
95
+ });
96
+ }
97
+ showBanner() {
98
+ if (this.infos) this.log(`Server Listen on ${this.scheme}://${this.infos.address}:${this.infos.port} Family: ${this.infos.family} Protocol : ${this.protocol}`);
99
+ }
100
+ };
101
+ WebsocketSecure = __decorate([__decorateParam(1, inject("HttpKernel")), __decorateMetadata("design:paramtypes", [typeof Module === "undefined" ? Object : Module, typeof http_kernel_default === "undefined" ? Object : http_kernel_default])], WebsocketSecure);
102
+ var server_websocket_secure_default = WebsocketSecure;
103
+ //#endregion
104
+ export { server_websocket_secure_default as default };
@@ -0,0 +1,104 @@
1
+ import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js";
2
+ import __decorate from "../../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
3
+ import http_kernel_default from "../http-kernel.js";
4
+ import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js";
5
+ import { startHeartbeat, trackPong } from "./wsHeartbeat.js";
6
+ import { Module, Service, inject } from "nodefony";
7
+ import Ws, { WebSocketServer } from "ws";
8
+ //#region nodefony/service/servers/server-websocket.ts
9
+ let Websocket = class Websocket extends Service {
10
+ httpKernel;
11
+ module;
12
+ ready = false;
13
+ server = null;
14
+ port;
15
+ domain;
16
+ protocol = "1.1";
17
+ family = null;
18
+ scheme = "ws";
19
+ address = null;
20
+ type = "websocket";
21
+ infos = null;
22
+ /** Timer keep-alive (UN par serveur). `null` si désactivé ou pas démarré. */
23
+ heartbeatTimer = null;
24
+ constructor(module, httpKernel) {
25
+ super("server-websocket", module.container, module.notificationsCenter, module.options.websocket);
26
+ this.httpKernel = httpKernel;
27
+ this.module = module;
28
+ this.port = this.setPort();
29
+ this.domain = this.kernel?.domain;
30
+ this.ready = false;
31
+ }
32
+ setPort() {
33
+ if (this.kernel?.options.servers?.http) return this.kernel?.options.servers?.http?.port || 0;
34
+ return 0;
35
+ }
36
+ async createServer(serverHttp) {
37
+ return new Promise((resolve, reject) => {
38
+ try {
39
+ this.infos = serverHttp.server.address();
40
+ if (this.infos) {
41
+ this.port = this.infos.port;
42
+ this.address = this.infos.address;
43
+ this.family = this.infos.family;
44
+ this.protocol = serverHttp.protocol;
45
+ }
46
+ this.server = new WebSocketServer({
47
+ ...this.options ?? {},
48
+ server: serverHttp.server,
49
+ clientTracking: true
50
+ });
51
+ this.server.on("connection", this.onConnection.bind(this));
52
+ this.heartbeatTimer = startHeartbeat(this.server, this.options);
53
+ this.kernel?.prependOnceListener("onTerminate", this.terminate.bind(this));
54
+ if (this.server) this.ready = true;
55
+ this.module.fire("onServersReady", this.type, this);
56
+ return resolve(this.server);
57
+ } catch (e) {
58
+ this.log(e, "ERROR");
59
+ return reject(e);
60
+ }
61
+ });
62
+ }
63
+ onConnection(ws, req) {
64
+ trackPong(ws);
65
+ this.httpKernel.onWebsocketRequest(ws, req, this.type).catch(() => {
66
+ process.nextTick(() => {});
67
+ });
68
+ }
69
+ terminate() {
70
+ return new Promise((resolve, reject) => {
71
+ if (this.heartbeatTimer) {
72
+ clearInterval(this.heartbeatTimer);
73
+ this.heartbeatTimer = null;
74
+ }
75
+ if (this.server && this.ready) {
76
+ const shutdownMsg = JSON.stringify({ nodefony: { state: "shutDown" } });
77
+ this.server.clients.forEach((client) => {
78
+ if (client.readyState === Ws.OPEN) {
79
+ client.send(shutdownMsg);
80
+ client.close(1001, "Server shutting down");
81
+ }
82
+ });
83
+ setTimeout(() => {
84
+ try {
85
+ this.server?.close();
86
+ this.log(` SHUTDOWN WEBSOCKET Server is listening on DOMAIN : ${this.domain} PORT : ${this.port}`, "INFO");
87
+ return resolve(true);
88
+ } catch (e) {
89
+ return reject(e);
90
+ }
91
+ }, 300);
92
+ return;
93
+ }
94
+ return resolve(true);
95
+ });
96
+ }
97
+ showBanner() {
98
+ if (this.infos) this.log(`Server Listen on ${this.scheme}://${this.infos.address}:${this.infos.port} Family: ${this.infos.family} Protocol : ${this.protocol}`);
99
+ }
100
+ };
101
+ Websocket = __decorate([__decorateParam(1, inject("HttpKernel")), __decorateMetadata("design:paramtypes", [typeof Module === "undefined" ? Object : Module, typeof http_kernel_default === "undefined" ? Object : http_kernel_default])], Websocket);
102
+ var server_websocket_default = Websocket;
103
+ //#endregion
104
+ export { server_websocket_default as default };
@@ -0,0 +1,31 @@
1
+ import { createHttpTerminator } from "http-terminator";
2
+ //#region nodefony/service/servers/serverShutdown.ts
3
+ /**
4
+ * Fabrique le terminator de drain graceful d'un serveur HTTP/HTTPS/HTTP2.
5
+ *
6
+ * Au `terminate()` (shutdown SIGTERM/docker stop) : les requêtes in-flight se
7
+ * terminent (header `connection: close` injecté sur les réponses en cours), les
8
+ * sockets idle sont fermées immédiatement, et tout ce qui reste après
9
+ * `shutdownTimeout` ms est détruit de force. Le terminator appelle lui-même
10
+ * `server.close()` — ne pas le rappeler derrière.
11
+ *
12
+ * ⚠️ Les sockets WebSocket upgradées (sans réponse HTTP en cours) sont détruites
13
+ * SANS frame Close par le terminator → les serveurs WS doivent fermer leurs
14
+ * clients (close 1001) AVANT ce drain. Garanti par l'ordre des listeners
15
+ * `onTerminate` : WS/WSS s'attachent en `prependOnceListener`, les serveurs
16
+ * HTTP en `once`.
17
+ *
18
+ * @param server - serveur Node à drainer (créé, pas forcément listening)
19
+ * @param shutdownTimeout - délai de drain en ms avant destruction forcée
20
+ * @returns le terminator à invoquer au shutdown
21
+ */
22
+ function createDrainTerminator(server, shutdownTimeout) {
23
+ return createHttpTerminator({
24
+ server,
25
+ gracefulTerminationTimeout: shutdownTimeout ?? 5e3
26
+ });
27
+ }
28
+ /** Drain par défaut (ms) — même valeur que le défaut Zod `servers.*.shutdownTimeout`. */
29
+ const DEFAULT_SHUTDOWN_TIMEOUT = 5e3;
30
+ //#endregion
31
+ export { DEFAULT_SHUTDOWN_TIMEOUT, createDrainTerminator };
@@ -0,0 +1,64 @@
1
+ import Ws from "ws";
2
+ //#region nodefony/service/servers/wsHeartbeat.ts
3
+ /**
4
+ * Arme le suivi keep-alive d'UNE connexion : initialise l'horodatage de vie et
5
+ * attache l'unique listener `pong` qui le rafraîchit.
6
+ *
7
+ * Le listener `pong` vit et meurt avec la socket (l'émetteur EST la socket `ws`,
8
+ * détruite au `close`/`terminate`) → aucun `removeListener` explicite à prévoir.
9
+ * Un seul listener par connexion, c'est le pair-event obligatoire du ping.
10
+ *
11
+ * @param ws - la socket fraîchement connectée
12
+ */
13
+ const trackPong = (ws) => {
14
+ const sock = ws;
15
+ sock._nfLastPong = performance.now();
16
+ sock._nfPingedAt = 0;
17
+ ws.on("pong", () => {
18
+ ws._nfLastPong = performance.now();
19
+ });
20
+ };
21
+ /**
22
+ * Démarre le heartbeat keep-alive du serveur : détecte les connexions zombies
23
+ * (half-open — TCP encore ouvert mais le pair a disparu sans frame Close) et les
24
+ * `terminate()` pour libérer slot mémoire + descripteur de fichier.
25
+ *
26
+ * Sémantique (fidèle à l'ancienne lib `websocket`) : un ping est émis tous les
27
+ * `keepaliveInterval` ms ; si aucun `pong` n'arrive dans les `keepaliveGracePeriod`
28
+ * ms qui suivent ce ping, la socket est détruite. Temps de réclamation borné par
29
+ * `keepaliveInterval + keepaliveGracePeriod` (+ une granularité de tick).
30
+ *
31
+ * PERF (hot path WS) : **UN seul `setInterval` par serveur** — jamais un timer par
32
+ * connexion. Chaque tick ne lit/écrit que des `number` sur la socket → 0 allocation
33
+ * sur le chemin nominal. `unref()` pour ne pas retenir le process à l'arrêt. Le timer
34
+ * DOIT être `clearInterval` au shutdown (cf appelant).
35
+ *
36
+ * @param server - le `WebSocketServer` dont on surveille `clients`
37
+ * @param options - knobs keep-alive (depuis la config Zod du module)
38
+ * @returns le timer à nettoyer au shutdown, ou `null` si le keep-alive est désactivé
39
+ */
40
+ const startHeartbeat = (server, options) => {
41
+ const interval = options.keepaliveInterval ?? 0;
42
+ if (interval <= 0) return null;
43
+ const grace = options.keepaliveGracePeriod ?? 0;
44
+ const timer = setInterval(() => {
45
+ const now = performance.now();
46
+ for (const client of server.clients) {
47
+ if (client.readyState !== Ws.OPEN) continue;
48
+ const sock = client;
49
+ if (sock._nfLastPong === void 0) sock._nfLastPong = now;
50
+ if ((sock._nfPingedAt ?? 0) > sock._nfLastPong) {
51
+ if (now - sock._nfPingedAt > grace) client.terminate();
52
+ continue;
53
+ }
54
+ if (now - sock._nfLastPong >= interval) {
55
+ sock._nfPingedAt = now;
56
+ client.ping();
57
+ }
58
+ }
59
+ }, Math.max(250, grace > 0 ? Math.min(interval, grace) : interval));
60
+ timer.unref?.();
61
+ return timer;
62
+ };
63
+ //#endregion
64
+ export { startHeartbeat, trackPong };