@rvoh/psychic 3.4.1 → 3.5.0

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.
@@ -11,6 +11,11 @@ export default class PackageManager {
11
11
  switch (this.packageManager) {
12
12
  case 'npm':
13
13
  return { command: 'npm', args: ['install', '--save-dev', ...list] };
14
+ case 'bun':
15
+ return { command: 'bun', args: ['add', '--dev', ...list] };
16
+ case 'deno':
17
+ // Deno needs an explicit registry prefix to add npm packages.
18
+ return { command: 'deno', args: ['add', '--dev', ...denoSpecifiers(list)] };
14
19
  default:
15
20
  return { command: this.packageManager, args: ['add', '-D', ...list] };
16
21
  }
@@ -19,6 +24,10 @@ export default class PackageManager {
19
24
  switch (this.packageManager) {
20
25
  case 'npm':
21
26
  return { command: 'npm', args: ['install', ...list] };
27
+ case 'bun':
28
+ return { command: 'bun', args: ['add', ...list] };
29
+ case 'deno':
30
+ return { command: 'deno', args: ['add', ...denoSpecifiers(list)] };
22
31
  default:
23
32
  return { command: this.packageManager, args: ['add', ...list] };
24
33
  }
@@ -32,6 +41,14 @@ export default class PackageManager {
32
41
  command: 'npm',
33
42
  args: args.length ? ['run', cmd, '--', ...args] : ['run', cmd],
34
43
  };
44
+ case 'bun':
45
+ // `bun <script>` is treated as file execution; `bun run` is required to
46
+ // resolve a package.json script. Bun forwards trailing args directly.
47
+ return { command: 'bun', args: ['run', cmd, ...args] };
48
+ case 'deno':
49
+ // Deno has no `<pm> <script>` shorthand; `deno task` resolves a
50
+ // package.json script (or deno.json task) and forwards trailing args.
51
+ return { command: 'deno', args: ['task', cmd, ...args] };
35
52
  default:
36
53
  return { command: this.packageManager, args: [cmd, ...args] };
37
54
  }
@@ -42,8 +59,23 @@ export default class PackageManager {
42
59
  return { command: 'npm', args: ['exec', '--', cmd, ...args] };
43
60
  case 'yarn':
44
61
  return { command: 'yarn', args: [cmd, ...args] };
62
+ case 'bun':
63
+ return { command: 'bunx', args: [cmd, ...args] };
64
+ case 'deno':
65
+ // `deno run` against an npm: specifier is Deno's equivalent of npx;
66
+ // -A grants the permissions the executed tool needs.
67
+ return { command: 'deno', args: ['run', '-A', denoSpecifier(cmd), ...args] };
45
68
  default:
46
69
  return { command: this.packageManager, args: ['exec', cmd, ...args] };
47
70
  }
48
71
  }
49
72
  }
73
+ // Deno requires npm/jsr packages to carry an explicit registry prefix
74
+ // (`npm:lodash`); bare names are treated as local/JSR specifiers. Leave any
75
+ // already-prefixed specifier untouched.
76
+ function denoSpecifier(pkg) {
77
+ return /^(npm|jsr):/.test(pkg) ? pkg : `npm:${pkg}`;
78
+ }
79
+ function denoSpecifiers(list) {
80
+ return list.map(denoSpecifier);
81
+ }
@@ -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
  }
@@ -11,6 +11,11 @@ export default class PackageManager {
11
11
  switch (this.packageManager) {
12
12
  case 'npm':
13
13
  return { command: 'npm', args: ['install', '--save-dev', ...list] };
14
+ case 'bun':
15
+ return { command: 'bun', args: ['add', '--dev', ...list] };
16
+ case 'deno':
17
+ // Deno needs an explicit registry prefix to add npm packages.
18
+ return { command: 'deno', args: ['add', '--dev', ...denoSpecifiers(list)] };
14
19
  default:
15
20
  return { command: this.packageManager, args: ['add', '-D', ...list] };
16
21
  }
@@ -19,6 +24,10 @@ export default class PackageManager {
19
24
  switch (this.packageManager) {
20
25
  case 'npm':
21
26
  return { command: 'npm', args: ['install', ...list] };
27
+ case 'bun':
28
+ return { command: 'bun', args: ['add', ...list] };
29
+ case 'deno':
30
+ return { command: 'deno', args: ['add', ...denoSpecifiers(list)] };
22
31
  default:
23
32
  return { command: this.packageManager, args: ['add', ...list] };
24
33
  }
@@ -32,6 +41,14 @@ export default class PackageManager {
32
41
  command: 'npm',
33
42
  args: args.length ? ['run', cmd, '--', ...args] : ['run', cmd],
34
43
  };
44
+ case 'bun':
45
+ // `bun <script>` is treated as file execution; `bun run` is required to
46
+ // resolve a package.json script. Bun forwards trailing args directly.
47
+ return { command: 'bun', args: ['run', cmd, ...args] };
48
+ case 'deno':
49
+ // Deno has no `<pm> <script>` shorthand; `deno task` resolves a
50
+ // package.json script (or deno.json task) and forwards trailing args.
51
+ return { command: 'deno', args: ['task', cmd, ...args] };
35
52
  default:
36
53
  return { command: this.packageManager, args: [cmd, ...args] };
37
54
  }
@@ -42,8 +59,23 @@ export default class PackageManager {
42
59
  return { command: 'npm', args: ['exec', '--', cmd, ...args] };
43
60
  case 'yarn':
44
61
  return { command: 'yarn', args: [cmd, ...args] };
62
+ case 'bun':
63
+ return { command: 'bunx', args: [cmd, ...args] };
64
+ case 'deno':
65
+ // `deno run` against an npm: specifier is Deno's equivalent of npx;
66
+ // -A grants the permissions the executed tool needs.
67
+ return { command: 'deno', args: ['run', '-A', denoSpecifier(cmd), ...args] };
45
68
  default:
46
69
  return { command: this.packageManager, args: ['exec', cmd, ...args] };
47
70
  }
48
71
  }
49
72
  }
73
+ // Deno requires npm/jsr packages to carry an explicit registry prefix
74
+ // (`npm:lodash`); bare names are treated as local/JSR specifiers. Leave any
75
+ // already-prefixed specifier untouched.
76
+ function denoSpecifier(pkg) {
77
+ return /^(npm|jsr):/.test(pkg) ? pkg : `npm:${pkg}`;
78
+ }
79
+ function denoSpecifiers(list) {
80
+ return list.map(denoSpecifier);
81
+ }
@@ -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
  }
@@ -2,11 +2,13 @@ export interface PackageManagerCommand {
2
2
  command: string;
3
3
  args: string[];
4
4
  }
5
+ type SupportedPackageManager = 'pnpm' | 'yarn' | 'npm' | 'bun' | 'deno';
5
6
  export default class PackageManager {
6
- static get packageManager(): "pnpm" | "yarn" | "npm";
7
+ static get packageManager(): SupportedPackageManager;
7
8
  static add(dependencyOrDependencies: string | string[], { dev }?: {
8
9
  dev?: boolean;
9
10
  }): PackageManagerCommand;
10
11
  static run(cmd: string, args?: string[]): PackageManagerCommand;
11
12
  static exec(cmd: string, args?: string[]): PackageManagerCommand;
12
13
  }
14
+ export {};
@@ -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.5.0",
6
6
  "author": "RVOHealth",
7
7
  "repository": {
8
8
  "type": "git",
@@ -138,11 +138,5 @@
138
138
  "vitest": "^4.1.0",
139
139
  "winston": "^3.14.2"
140
140
  },
141
- "packageManager": "pnpm@10.26.0+sha512.3b3f6c725ebe712506c0ab1ad4133cf86b1f4b687effce62a9b38b4d72e3954242e643190fc51fa1642949c735f403debd44f5cb0edd657abe63a8b6a7e1e402",
142
- "pnpm": {
143
- "overrides": {
144
- "diff": ">=8.0.3",
145
- "path-to-regexp": ">=8.4.0"
146
- }
147
- }
141
+ "packageManager": "pnpm@11.1.3+sha512.c85357fe17ca12dd23dd7071822666dfd7e3cb76fe214e3370b5ea2fb34f2a231185509b63e717f3cd0acb38dd3f8d82bcd5e8172400ae678b70ea4fbed0896d"
148
142
  }