@travetto/runtime 7.1.1 → 7.1.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.
package/README.md CHANGED
@@ -108,11 +108,6 @@ interface EnvData {
108
108
  * @default 2s
109
109
  */
110
110
  TRV_SHUTDOWN_WAIT: TimeSpan | number;
111
- /**
112
- * The time to wait for stdout to drain during shutdown
113
- * @default 0s
114
- */
115
- TRV_SHUTDOWN_STDOUT_WAIT: TimeSpan | number;
116
111
  /**
117
112
  * The desired runtime module
118
113
  */
@@ -353,7 +348,7 @@ As a registered shutdown handler, you can do.
353
348
  import { ShutdownManager } from '@travetto/runtime';
354
349
 
355
350
  export function registerShutdownHandler() {
356
- ShutdownManager.onGracefulShutdown(async () => {
351
+ ShutdownManager.signal.addEventListener('abort', () => {
357
352
  // Do important work, the framework will wait until all async
358
353
  // operations are completed before finishing shutdown
359
354
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/runtime",
3
- "version": "7.1.1",
3
+ "version": "7.1.2",
4
4
  "type": "module",
5
5
  "description": "Runtime for travetto applications.",
6
6
  "keywords": [
@@ -26,12 +26,12 @@
26
26
  "directory": "module/runtime"
27
27
  },
28
28
  "dependencies": {
29
- "@travetto/manifest": "^7.1.0",
29
+ "@travetto/manifest": "^7.1.1",
30
30
  "@types/debug": "^4.1.12",
31
31
  "debug": "^4.4.3"
32
32
  },
33
33
  "peerDependencies": {
34
- "@travetto/transformer": "^7.1.0"
34
+ "@travetto/transformer": "^7.1.1"
35
35
  },
36
36
  "peerDependenciesMeta": {
37
37
  "@travetto/transformer": {
package/src/shutdown.ts CHANGED
@@ -1,99 +1,110 @@
1
+ import type { ChildProcess } from 'node:child_process';
2
+
1
3
  import { Env } from './env.ts';
2
4
  import { Util } from './util.ts';
3
5
  import { TimeUtil } from './time.ts';
4
6
 
7
+ const MAPPING = [['restart', 200], ['error', 1], ['quit', 0]] as const;
8
+ export type ShutdownReason = typeof MAPPING[number][0];
9
+
10
+ const REASON_TO_CODE = new Map<ShutdownReason, number>(MAPPING);
11
+ const CODE_TO_REASON = new Map<number, ShutdownReason>(MAPPING.map(([k, v]) => [v, k]));
12
+
5
13
  type Handler = (event: Event) => unknown;
14
+ type ShutdownSignal = 'SIGINT' | 'SIGTERM' | 'SIGUSR2' | string | undefined;
15
+ type ShutdownEvent = { signal?: ShutdownSignal, reason?: ShutdownReason | number, exit?: boolean };
16
+
17
+ const isShutdownEvent = (event: unknown): event is ShutdownEvent =>
18
+ typeof event === 'object' && event !== null && 'type' in event && event.type === 'shutdown';
19
+
20
+ const wrapped = async (handler: Handler): Promise<void> => {
21
+ try {
22
+ await handler(new Event('abort'));
23
+ } catch (err) {
24
+ console.error('Error during shutdown handler', err);
25
+ }
26
+ };
6
27
 
7
28
  /**
8
29
  * Shutdown manager, allowing for listening for graceful shutdowns
9
30
  */
10
31
  export class ShutdownManager {
11
-
12
32
  static #shouldIgnoreInterrupt = false;
13
- static #registered = new Map<Handler, Handler>();
14
- static #pending: Function[] = [];
33
+ static #registered = new Set<Handler>();
15
34
  static #controller = new AbortController();
16
- static #startedAt: number = 0;
17
- static #addListener = this.#controller.signal.addEventListener.bind(this.#controller.signal);
18
- static #removeListener = this.#controller.signal.removeEventListener.bind(this.#controller.signal);
19
35
 
20
36
  static {
21
- this.#controller.signal.addEventListener = (type: 'abort', listener: Handler): void => this.onGracefulShutdown(listener);
22
- this.#controller.signal.removeEventListener = (type: 'abort', listener: Handler): void => {
23
- this.#removeListener(type, this.#registered.get(listener) ?? listener);
24
- };
37
+ this.#controller.signal.addEventListener = (_: 'abort', listener: Handler): void => { this.#registered.add(listener); };
38
+ this.#controller.signal.removeEventListener = (_: 'abort', listener: Handler): void => { this.#registered.delete(listener); };
39
+ process
40
+ .on('message', event => { isShutdownEvent(event) && this.shutdown(event); })
41
+ .on('SIGINT', () => this.shutdown({ signal: 'SIGINT' }))
42
+ .on('SIGUSR2', () => this.shutdown({ signal: 'SIGUSR2' }))
43
+ .on('SIGTERM', () => this.shutdown({ signal: 'SIGTERM' }));
25
44
  }
26
45
 
27
46
  static get signal(): AbortSignal {
28
47
  return this.#controller.signal;
29
48
  }
30
49
 
31
- static disableInterrupt(): void {
50
+ /** Disable SIGINT interrupt handling */
51
+ static disableInterrupt(): typeof ShutdownManager {
32
52
  this.#shouldIgnoreInterrupt = true;
53
+ return this;
33
54
  }
34
55
 
35
- static onGracefulShutdown(listener: Handler): void {
36
- if (!this.#registered.size) {
37
- process
38
- .on('SIGINT', () => this.shutdown('SIGINT')) // Ensure we get a newline on ctrl-c
39
- .on('SIGUSR2', () => this.shutdown('SIGUSR2'))
40
- .on('SIGTERM', () => this.shutdown('SIGTERM'));
41
- }
56
+ /** Convert exit code to a reason string */
57
+ static reasonForExitCode(code: number): ShutdownReason {
58
+ return CODE_TO_REASON.get(code) ?? 'error';
59
+ }
42
60
 
43
- const wrappedListener: Handler = event => { this.#pending.push(() => listener(event)); };
44
- this.#registered.set(listener, wrappedListener);
45
- return this.#addListener('abort', wrappedListener);
61
+ /** Trigger a watch signal signal to a subprocess */
62
+ static async shutdownChild(subprocess: ChildProcess, config?: ShutdownEvent): Promise<void> {
63
+ subprocess?.send!({ type: 'shutdown', ...config });
46
64
  }
47
65
 
48
66
  /**
49
67
  * Shutdown the application gracefully
50
68
  */
51
- static async shutdown(source?: string, code?: number): Promise<void> {
52
- if (this.#shouldIgnoreInterrupt && source === 'SIGINT') {
69
+ static async shutdown(event?: ShutdownEvent): Promise<void> {
70
+ if ((event?.signal === 'SIGINT' && this.#shouldIgnoreInterrupt) || this.#controller.signal.aborted) {
53
71
  return;
54
72
  }
55
- if (this.#controller.signal.aborted) {
56
- if (this.#startedAt && (Date.now() - this.#startedAt) > 500) {
57
- console.warn('Shutdown already in progress, exiting immediately', { source });
58
- process.exit(0); // Quit immediately
59
- } else {
60
- return;
61
- }
62
- }
63
73
 
64
- if (process.stdout.isTTY && source === 'SIGINT') {
74
+ process // Allow shutdown if anything is still listening
75
+ .removeAllListeners('message')
76
+ .removeAllListeners('SIGINT')
77
+ .removeAllListeners('SIGTERM')
78
+ .removeAllListeners('SIGUSR2');
79
+
80
+ if (event?.signal === 'SIGINT' && process.stdout.isTTY) {
65
81
  process.stdout.write('\n');
66
82
  }
67
83
 
68
- process.removeAllListeners('message'); // Allow shutdown if anything is still listening
69
-
70
- const context = source ? [{ source, pid: process.pid }] : [{ pid: process.pid }];
71
- this.#startedAt = Date.now();
72
- this.#controller.abort('Shutdown started');
73
- await Util.queueMacroTask(); // Force the event loop to wait one cycle
74
-
75
- console.debug('Shutdown started', ...context, { pending: this.#pending.length });
84
+ if (event?.reason !== undefined) {
85
+ const { reason } = event;
86
+ process.exitCode = (typeof reason === 'string' ? REASON_TO_CODE.get(reason) : reason);
87
+ }
76
88
 
77
89
  const timeout = TimeUtil.fromValue(Env.TRV_SHUTDOWN_WAIT.value) ?? 2000;
78
- const timeoutTasks = Util.nonBlockingTimeout(timeout).then(() => this);
79
- const allPendingTasks = Promise.all(this.#pending.map(fn => Promise.resolve(fn()).catch(err => {
80
- console.error('Error during shutdown task', err, ...context);
81
- })));
90
+ const context = { ...event, pid: process.pid, timeout, pending: this.#registered.size };
82
91
 
83
- const winner = await Promise.race([timeoutTasks, allPendingTasks]);
92
+ this.#controller.abort('Shutdown started');
93
+ console.debug('Shutdown started', context);
84
94
 
85
- process.exitCode = code ?? process.exitCode ?? 0;
95
+ const winner = await Promise.race([
96
+ Util.nonBlockingTimeout(timeout).then(() => this),
97
+ Promise.all([...this.#registered].map(wrapped))
98
+ ]);
86
99
 
87
100
  if (winner !== this) {
88
- console.debug('Shutdown completed', ...context);
101
+ console.debug('Shutdown completed', context);
89
102
  } else {
90
- console.warn('Shutdown timed out', ...context);
91
- process.exit(1); // Indicate error on timeout
103
+ console.warn('Shutdown timed out', context);
92
104
  }
93
105
 
94
- if (Env.TRV_SHUTDOWN_STDOUT_WAIT.isSet) {
95
- const stdoutDrain = TimeUtil.fromValue(Env.TRV_SHUTDOWN_STDOUT_WAIT.value)!;
96
- await Util.blockingTimeout(stdoutDrain);
106
+ if (event?.exit) {
107
+ process.exit();
97
108
  }
98
109
  }
99
110
  }
package/src/trv.d.ts CHANGED
@@ -31,11 +31,6 @@ declare module "@travetto/runtime" {
31
31
  * @default 2s
32
32
  */
33
33
  TRV_SHUTDOWN_WAIT: TimeSpan | number;
34
- /**
35
- * The time to wait for stdout to drain during shutdown
36
- * @default 0s
37
- */
38
- TRV_SHUTDOWN_STDOUT_WAIT: TimeSpan | number;
39
34
  /**
40
35
  * The desired runtime module
41
36
  */
package/src/watch.ts CHANGED
@@ -1,18 +1,16 @@
1
- import type { ChildProcess } from 'node:child_process';
2
-
3
1
  import type { CompilerEventPayload, CompilerEventType } from '@travetto/compiler';
4
2
 
5
3
  import { AppError } from './error.ts';
6
4
  import { Util } from './util.ts';
7
5
  import { RuntimeIndex } from './manifest-index.ts';
8
- import { ShutdownManager } from './shutdown.ts';
6
+ import { ShutdownManager, type ShutdownReason } from './shutdown.ts';
9
7
  import { castTo } from './types.ts';
10
8
 
11
9
  type RetryRunState = {
12
10
  iteration: number;
13
11
  startTime: number;
14
12
  errorIterations: number;
15
- result?: 'error' | 'restart' | 'stop';
13
+ result?: ShutdownReason;
16
14
  };
17
15
 
18
16
  type RetryRunConfig = {
@@ -27,34 +25,6 @@ type RetryRunConfig = {
27
25
  */
28
26
  export class WatchUtil {
29
27
 
30
- static #RESTART_EXIT_CODE = 200;
31
-
32
- /** Convert exit code to a result type */
33
- static exitCodeToResult(code: number): RetryRunState['result'] {
34
- return code === this.#RESTART_EXIT_CODE ? 'restart' : (code !== null && code > 0) ? 'error' : 'stop';
35
- }
36
-
37
- /** Exit with a restart exit code */
38
- static exitWithRestart(): void {
39
- ShutdownManager.shutdown('SIGTERM', this.#RESTART_EXIT_CODE);
40
- }
41
-
42
- /** Listen for restart signals */
43
- static listenForSignals(): void {
44
- ShutdownManager.disableInterrupt();
45
- process.on('message', event => {
46
- switch (event) {
47
- case 'WATCH_RESTART': this.exitWithRestart(); break;
48
- case 'WATCH_SHUTDOWN': ShutdownManager.shutdown('SIGTERM', 0); break;
49
- }
50
- });
51
- }
52
-
53
- /** Trigger a watch signal signal to a subprocess */
54
- static triggerSignal(subprocess: ChildProcess, signal: 'WATCH_RESTART' | 'WATCH_SHUTDOWN'): void {
55
- subprocess.connected && subprocess.send?.(signal);
56
- }
57
-
58
28
  /** Compute the delay before restarting */
59
29
  static computeRestartDelay(state: RetryRunState, config: RetryRunConfig): number {
60
30
  return state.result === 'error'
@@ -65,7 +35,7 @@ export class WatchUtil {
65
35
  /**
66
36
  * Run with restart capability
67
37
  */
68
- static async runWithRetry(run: (state: RetryRunState & { signal: AbortSignal }) => Promise<RetryRunState['result']>, options?: Partial<RetryRunConfig>): Promise<void> {
38
+ static async runWithRetry(run: (state: RetryRunState & { signal: AbortSignal }) => Promise<ShutdownReason>, options?: Partial<RetryRunConfig>): Promise<void> {
69
39
  let retryExhausted = false;
70
40
 
71
41
  const state: RetryRunState = {
@@ -89,7 +59,7 @@ export class WatchUtil {
89
59
 
90
60
  state.result = await run({ ...state, signal: ShutdownManager.signal }).catch(() => 'error' as const);
91
61
  switch (state.result) {
92
- case 'stop': break outer;
62
+ case 'quit': break outer;
93
63
  case 'error': state.errorIterations += 1; break;
94
64
  case 'restart': {
95
65
  state.startTime = Date.now();
@@ -114,7 +84,12 @@ export class WatchUtil {
114
84
  options?: Partial<RetryRunConfig>,
115
85
  ): Promise<void> {
116
86
  const { CompilerClient } = await import('@travetto/compiler/src/server/client.ts');
117
- const client = new CompilerClient(RuntimeIndex.manifest, console);
87
+ const client = new CompilerClient(RuntimeIndex.manifest, {
88
+ debug: (...args: unknown[]): void => console.debug(...args),
89
+ info: (...args: unknown[]): void => console.info(...args),
90
+ warn: (...args: unknown[]): void => console.warn(...args),
91
+ error: (...args: unknown[]): void => console.error(...args),
92
+ });
118
93
 
119
94
  return this.runWithRetry(async ({ signal }) => {
120
95
  await client.waitForState(['compile-end', 'watch-start'], undefined, signal);