@rvoh/psychic 3.4.1 → 3.4.2

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.
@@ -138,11 +138,37 @@ export default class PsychicServer {
138
138
  await this.stop();
139
139
  process.exit();
140
140
  }
141
- async stop({ bypassClosingDbConnections = false } = {}) {
141
+ async stop({ bypassClosingDbConnections = false, gracefulShutdownTimeoutMillis = 10_000, } = {}) {
142
142
  for (const hook of PsychicApp.getOrFail().specialHooks.serverShutdown) {
143
143
  await hook(this);
144
144
  }
145
- this.httpServer?.close();
145
+ if (this.httpServer) {
146
+ const httpServer = this.httpServer;
147
+ await new Promise(resolve => {
148
+ // Bounded grace period: if still-active requests haven't finished by
149
+ // the timeout, force the remaining sockets so shutdown can never hang
150
+ // forever (the original bug). `closeIdle/AllConnections` are Node
151
+ // >= 18.2; on older runtimes the optional calls no-op and prior
152
+ // (potentially hanging) behavior is retained rather than throwing.
153
+ const forceTimer = setTimeout(() => httpServer.closeAllConnections?.(), gracefulShutdownTimeoutMillis);
154
+ forceTimer.unref?.();
155
+ // `close` stops accepting new connections and fires its callback only
156
+ // once every existing connection has ended.
157
+ httpServer.close(() => {
158
+ clearTimeout(forceTimer);
159
+ resolve();
160
+ });
161
+ // Immediately drop *idle* keep-alive sockets. These (browsers, fetch
162
+ // agents, reverse proxies sitting between requests) are what kept
163
+ // `close()`'s callback from ever firing — and, transitively, a leased
164
+ // DB pool client from being released — causing shutdown to hang (a
165
+ // SIGTERM drain that never completes; a feature-spec `afterAll` that
166
+ // blocks for the full hook timeout). Dropping only *idle* sockets
167
+ // does NOT abort in-flight requests, so a normal graceful shutdown
168
+ // still lets active handlers finish within the grace period above.
169
+ httpServer.closeIdleConnections?.();
170
+ });
171
+ }
146
172
  if (!bypassClosingDbConnections) {
147
173
  await closeAllDbConnections();
148
174
  }
@@ -138,11 +138,37 @@ export default class PsychicServer {
138
138
  await this.stop();
139
139
  process.exit();
140
140
  }
141
- async stop({ bypassClosingDbConnections = false } = {}) {
141
+ async stop({ bypassClosingDbConnections = false, gracefulShutdownTimeoutMillis = 10_000, } = {}) {
142
142
  for (const hook of PsychicApp.getOrFail().specialHooks.serverShutdown) {
143
143
  await hook(this);
144
144
  }
145
- this.httpServer?.close();
145
+ if (this.httpServer) {
146
+ const httpServer = this.httpServer;
147
+ await new Promise(resolve => {
148
+ // Bounded grace period: if still-active requests haven't finished by
149
+ // the timeout, force the remaining sockets so shutdown can never hang
150
+ // forever (the original bug). `closeIdle/AllConnections` are Node
151
+ // >= 18.2; on older runtimes the optional calls no-op and prior
152
+ // (potentially hanging) behavior is retained rather than throwing.
153
+ const forceTimer = setTimeout(() => httpServer.closeAllConnections?.(), gracefulShutdownTimeoutMillis);
154
+ forceTimer.unref?.();
155
+ // `close` stops accepting new connections and fires its callback only
156
+ // once every existing connection has ended.
157
+ httpServer.close(() => {
158
+ clearTimeout(forceTimer);
159
+ resolve();
160
+ });
161
+ // Immediately drop *idle* keep-alive sockets. These (browsers, fetch
162
+ // agents, reverse proxies sitting between requests) are what kept
163
+ // `close()`'s callback from ever firing — and, transitively, a leased
164
+ // DB pool client from being released — causing shutdown to hang (a
165
+ // SIGTERM drain that never completes; a feature-spec `afterAll` that
166
+ // blocks for the full hook timeout). Dropping only *idle* sockets
167
+ // does NOT abort in-flight requests, so a normal graceful shutdown
168
+ // still lets active handlers finish within the grace period above.
169
+ httpServer.closeIdleConnections?.();
170
+ });
171
+ }
146
172
  if (!bypassClosingDbConnections) {
147
173
  await closeAllDbConnections();
148
174
  }
@@ -17,8 +17,9 @@ export default class PsychicServer {
17
17
  attach(id: string, obj: any): void;
18
18
  $attached: Record<string, any>;
19
19
  private shutdownAndExit;
20
- stop({ bypassClosingDbConnections }?: {
20
+ stop({ bypassClosingDbConnections, gracefulShutdownTimeoutMillis, }?: {
21
21
  bypassClosingDbConnections?: boolean;
22
+ gracefulShutdownTimeoutMillis?: number;
22
23
  }): Promise<void>;
23
24
  serveForRequestSpecs(block: () => void | Promise<void>): Promise<boolean>;
24
25
  buildApp(): void;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "type": "module",
3
3
  "name": "@rvoh/psychic",
4
4
  "description": "Typescript web framework",
5
- "version": "3.4.1",
5
+ "version": "3.4.2",
6
6
  "author": "RVOHealth",
7
7
  "repository": {
8
8
  "type": "git",