@travetto/runtime 7.0.5 → 7.1.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.
- package/README.md +13 -12
- package/package.json +3 -3
- package/src/context.ts +9 -12
- package/src/exec.ts +12 -22
- package/src/shutdown.ts +68 -62
- package/src/time.ts +24 -13
- package/src/trv.d.ts +8 -7
- package/src/watch.ts +16 -18
package/README.md
CHANGED
|
@@ -35,12 +35,12 @@ While running any code within the framework, there are common patterns/goals for
|
|
|
35
35
|
```typescript
|
|
36
36
|
class $Runtime {
|
|
37
37
|
constructor(idx: ManifestIndex, resourceOverrides?: Record<string, string>);
|
|
38
|
-
/**
|
|
39
|
-
get
|
|
38
|
+
/** The role we are running as */
|
|
39
|
+
get role(): Role;
|
|
40
40
|
/** Are we in production mode */
|
|
41
41
|
get production(): boolean;
|
|
42
|
-
/**
|
|
43
|
-
get
|
|
42
|
+
/** Are we in development mode */
|
|
43
|
+
get localDevelopment(): boolean;
|
|
44
44
|
/** Get debug value */
|
|
45
45
|
get debug(): false | string;
|
|
46
46
|
/** Manifest main */
|
|
@@ -87,15 +87,11 @@ interface EnvData {
|
|
|
87
87
|
*/
|
|
88
88
|
NODE_ENV: 'development' | 'production';
|
|
89
89
|
/**
|
|
90
|
-
* Outputs all console.debug messages, defaults to
|
|
90
|
+
* Outputs all console.debug messages, defaults to off
|
|
91
91
|
*/
|
|
92
92
|
DEBUG: boolean | string;
|
|
93
93
|
/**
|
|
94
|
-
*
|
|
95
|
-
*/
|
|
96
|
-
TRV_ENV: string;
|
|
97
|
-
/**
|
|
98
|
-
* Special role to run as, used to access additional files from the manifest during runtime.
|
|
94
|
+
* The role we are running as, allows access to additional files from the manifest during runtime.
|
|
99
95
|
*/
|
|
100
96
|
TRV_ROLE: Role;
|
|
101
97
|
/**
|
|
@@ -112,6 +108,11 @@ interface EnvData {
|
|
|
112
108
|
* @default 2s
|
|
113
109
|
*/
|
|
114
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;
|
|
115
116
|
/**
|
|
116
117
|
* The desired runtime module
|
|
117
118
|
*/
|
|
@@ -214,7 +215,7 @@ export function work() {
|
|
|
214
215
|
```javascript
|
|
215
216
|
import * as Δfunction from "@travetto/runtime/src/function.js";
|
|
216
217
|
import * as Δconsole from "@travetto/runtime/src/console.js";
|
|
217
|
-
|
|
218
|
+
const Δm_1 = ["@travetto/runtime", "doc/transpile.ts"];
|
|
218
219
|
export function work() {
|
|
219
220
|
Δconsole.log({ level: "debug", import: Δm_1, line: 2, scope: "work", args: ['Start Work'] });
|
|
220
221
|
try {
|
|
@@ -282,7 +283,7 @@ tpl`{{age:20}} {{name: 'bob'}}</>;
|
|
|
282
283
|
```
|
|
283
284
|
|
|
284
285
|
## Time Utilities
|
|
285
|
-
[TimeUtil](https://github.com/travetto/travetto/tree/main/module/runtime/src/time.ts#
|
|
286
|
+
[TimeUtil](https://github.com/travetto/travetto/tree/main/module/runtime/src/time.ts#L20) contains general helper methods, created to assist with time-based inputs via environment variables, command line interfaces, and other string-heavy based input.
|
|
286
287
|
|
|
287
288
|
**Code: Time Utilities**
|
|
288
289
|
```typescript
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/runtime",
|
|
3
|
-
"version": "7.0
|
|
3
|
+
"version": "7.1.0",
|
|
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.0
|
|
29
|
+
"@travetto/manifest": "^7.1.0",
|
|
30
30
|
"@types/debug": "^4.1.12",
|
|
31
31
|
"debug": "^4.4.3"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@travetto/transformer": "^7.0
|
|
34
|
+
"@travetto/transformer": "^7.1.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependenciesMeta": {
|
|
37
37
|
"@travetto/transformer": {
|
package/src/context.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { Env } from './env.ts';
|
|
|
7
7
|
import { RuntimeIndex } from './manifest-index.ts';
|
|
8
8
|
import { describeFunction } from './function.ts';
|
|
9
9
|
import { JSONUtil } from './json.ts';
|
|
10
|
+
import type { Role } from './trv';
|
|
10
11
|
|
|
11
12
|
/** Constrained version of {@type ManifestContext} */
|
|
12
13
|
class $Runtime {
|
|
@@ -26,9 +27,10 @@ class $Runtime {
|
|
|
26
27
|
};
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
/**
|
|
30
|
-
get
|
|
31
|
-
|
|
30
|
+
/** The role we are running as */
|
|
31
|
+
get role(): Role {
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
33
|
+
return Env.TRV_ROLE.value as Role ?? 'std';
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
/** Are we in production mode */
|
|
@@ -36,19 +38,14 @@ class $Runtime {
|
|
|
36
38
|
return process.env.NODE_ENV === 'production';
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
/**
|
|
40
|
-
get
|
|
41
|
-
|
|
42
|
-
case 'production': return 'production';
|
|
43
|
-
case 'test': return 'test';
|
|
44
|
-
default: return 'development';
|
|
45
|
-
}
|
|
41
|
+
/** Are we in development mode */
|
|
42
|
+
get localDevelopment(): boolean {
|
|
43
|
+
return !this.production && this.role === 'std';
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
/** Get debug value */
|
|
49
47
|
get debug(): false | string {
|
|
50
|
-
|
|
51
|
-
return (!value && this.production) || Env.DEBUG.isFalse ? false : value;
|
|
48
|
+
return Env.DEBUG.isFalse ? false : (Env.DEBUG.value || false);
|
|
52
49
|
}
|
|
53
50
|
|
|
54
51
|
/** Manifest main */
|
package/src/exec.ts
CHANGED
|
@@ -42,29 +42,19 @@ export class ExecUtil {
|
|
|
42
42
|
/**
|
|
43
43
|
* Defer control to subprocess execution, mainly used for nested execution
|
|
44
44
|
*/
|
|
45
|
-
static async deferToSubprocess(child: ChildProcess
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
static async deferToSubprocess(child: ChildProcess): Promise<ExecutionResult> {
|
|
46
|
+
const messageToChild = (value: unknown): void => { child.send(value!); };
|
|
47
|
+
const messageToParent = (value: unknown): void => { process.send?.(value); };
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
process.on('message', messageToChild);
|
|
51
|
+
child.on('message', messageToParent);
|
|
52
|
+
const result = await this.getResult(child, { catch: true });
|
|
53
|
+
process.exitCode = child.exitCode;
|
|
54
|
+
return result;
|
|
55
|
+
} finally {
|
|
56
|
+
process.off('message', messageToChild);
|
|
49
57
|
}
|
|
50
|
-
|
|
51
|
-
child.on('message', value => process.send?.(value));
|
|
52
|
-
|
|
53
|
-
const interrupt = (): void => { child?.kill('SIGINT'); };
|
|
54
|
-
const toMessage = (value: unknown): void => { child?.send(value!); };
|
|
55
|
-
|
|
56
|
-
// Proxy kill requests
|
|
57
|
-
process.on('message', toMessage);
|
|
58
|
-
|
|
59
|
-
if (relayInterrupt) {
|
|
60
|
-
process.on('SIGINT', interrupt);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const result = await ExecUtil.getResult(child, { catch: true });
|
|
64
|
-
process.exitCode = child.exitCode;
|
|
65
|
-
process.off('message', toMessage);
|
|
66
|
-
process.off('SIGINT', interrupt);
|
|
67
|
-
return result;
|
|
68
58
|
}
|
|
69
59
|
|
|
70
60
|
/**
|
package/src/shutdown.ts
CHANGED
|
@@ -2,93 +2,99 @@ import { Env } from './env.ts';
|
|
|
2
2
|
import { Util } from './util.ts';
|
|
3
3
|
import { TimeUtil } from './time.ts';
|
|
4
4
|
|
|
5
|
+
type Handler = (event: Event) => unknown;
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Shutdown manager, allowing for listening for graceful shutdowns
|
|
7
9
|
*/
|
|
8
10
|
export class ShutdownManager {
|
|
9
11
|
|
|
10
|
-
static #
|
|
11
|
-
static #
|
|
12
|
-
static #pending:
|
|
12
|
+
static #shouldIgnoreInterrupt = false;
|
|
13
|
+
static #registered = new Map<Handler, Handler>();
|
|
14
|
+
static #pending: Function[] = [];
|
|
15
|
+
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);
|
|
13
19
|
|
|
14
|
-
static
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
this.#registered = true;
|
|
19
|
-
const cleanup = (signal: string): void => {
|
|
20
|
-
if (this.#pending && (Date.now() - this.#pending.time) > 500) {
|
|
21
|
-
console.warn('Shutdown already in progress, exiting immediately', { signal });
|
|
22
|
-
process.exit(0); // Quit immediately
|
|
23
|
-
}
|
|
24
|
-
this.gracefulShutdown(signal).then(() => process.exit(0));
|
|
20
|
+
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);
|
|
25
24
|
};
|
|
26
|
-
if (process.stdout.isTTY) {
|
|
27
|
-
process.on('SIGINT', () => process.stdout.write('\n')); // Ensure we get a newline on ctrl-c
|
|
28
|
-
}
|
|
29
|
-
process.on('SIGUSR2', cleanup);
|
|
30
|
-
process.on('SIGTERM', cleanup);
|
|
31
|
-
process.on('SIGINT', cleanup);
|
|
32
25
|
}
|
|
33
26
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
27
|
+
static get signal(): AbortSignal {
|
|
28
|
+
return this.#controller.signal;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static disableInterrupt(): void {
|
|
32
|
+
this.#shouldIgnoreInterrupt = true;
|
|
33
|
+
}
|
|
34
|
+
|
|
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
|
+
}
|
|
42
|
+
|
|
43
|
+
const wrappedListener: Handler = event => { this.#pending.push(() => listener(event)); };
|
|
44
|
+
this.#registered.set(listener, wrappedListener);
|
|
45
|
+
return this.#addListener('abort', wrappedListener);
|
|
48
46
|
}
|
|
49
47
|
|
|
50
48
|
/**
|
|
51
|
-
*
|
|
49
|
+
* Shutdown the application gracefully
|
|
52
50
|
*/
|
|
53
|
-
static async
|
|
54
|
-
if (this.#
|
|
55
|
-
return this.#pending.promise;
|
|
56
|
-
} else if (!this.#handlers.length) {
|
|
51
|
+
static async shutdown(source?: string, code?: number): Promise<void> {
|
|
52
|
+
if (this.#shouldIgnoreInterrupt && source === 'SIGINT') {
|
|
57
53
|
return;
|
|
58
54
|
}
|
|
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
|
+
|
|
64
|
+
if (process.stdout.isTTY && source === 'SIGINT') {
|
|
65
|
+
process.stdout.write('\n');
|
|
66
|
+
}
|
|
59
67
|
|
|
60
|
-
|
|
68
|
+
process.removeAllListeners('message'); // Allow shutdown if anything is still listening
|
|
61
69
|
|
|
70
|
+
const context = source ? [{ source, pid: process.pid }] : [{ pid: process.pid }];
|
|
71
|
+
this.#startedAt = Date.now();
|
|
72
|
+
this.#controller.abort('Shutdown started');
|
|
62
73
|
await Util.queueMacroTask(); // Force the event loop to wait one cycle
|
|
63
74
|
|
|
75
|
+
console.debug('Shutdown started', ...context, { pending: this.#pending.length });
|
|
76
|
+
|
|
64
77
|
const timeout = TimeUtil.fromValue(Env.TRV_SHUTDOWN_WAIT.value) ?? 2000;
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
console.debug('Stopping', { scope });
|
|
70
|
-
}
|
|
71
|
-
try {
|
|
72
|
-
await handler();
|
|
73
|
-
if (scope) {
|
|
74
|
-
console.debug('Stopped', { scope });
|
|
75
|
-
}
|
|
76
|
-
} catch (error) {
|
|
77
|
-
console.error('Error stopping', { error, scope });
|
|
78
|
-
}
|
|
79
|
-
}));
|
|
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
|
+
})));
|
|
80
82
|
|
|
81
|
-
const winner = await Promise.race([
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
]);
|
|
83
|
+
const winner = await Promise.race([timeoutTasks, allPendingTasks]);
|
|
84
|
+
|
|
85
|
+
process.exitCode = code ?? process.exitCode ?? 0;
|
|
85
86
|
|
|
86
87
|
if (winner !== this) {
|
|
87
|
-
console.debug('
|
|
88
|
+
console.debug('Shutdown completed', ...context);
|
|
88
89
|
} else {
|
|
89
|
-
console.
|
|
90
|
+
console.warn('Shutdown timed out', ...context);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (Env.TRV_SHUTDOWN_STDOUT_WAIT.isSet) {
|
|
94
|
+
const stdoutDrain = TimeUtil.fromValue(Env.TRV_SHUTDOWN_STDOUT_WAIT.value)!;
|
|
95
|
+
await Util.blockingTimeout(stdoutDrain);
|
|
90
96
|
}
|
|
91
97
|
|
|
92
|
-
|
|
98
|
+
process.exit();
|
|
93
99
|
}
|
|
94
100
|
}
|
package/src/time.ts
CHANGED
|
@@ -15,6 +15,7 @@ export type TimeSpan = `${number}${keyof typeof TIME_UNITS}`;
|
|
|
15
15
|
export type TimeUnit = keyof typeof TIME_UNITS;
|
|
16
16
|
|
|
17
17
|
const TIME_PATTERN = new RegExp(`^(?<amount>-?[0-9.]+)(?<unit>${Object.keys(TIME_UNITS).join('|')})$`);
|
|
18
|
+
const TIME_LIKE_STRING = /\d{1,30}[a-z]$/i;
|
|
18
19
|
|
|
19
20
|
export class TimeUtil {
|
|
20
21
|
|
|
@@ -66,14 +67,19 @@ export class TimeUtil {
|
|
|
66
67
|
* Resolve time or span to possible time
|
|
67
68
|
*/
|
|
68
69
|
static fromValue(value: Date | number | string | undefined): number | undefined {
|
|
69
|
-
|
|
70
|
-
return value;
|
|
70
|
+
switch (typeof value) {
|
|
71
|
+
case 'number': return Number.isNaN(value) ? undefined : value;
|
|
72
|
+
case 'object': return value.getTime();
|
|
73
|
+
case 'undefined': return undefined;
|
|
74
|
+
case 'string': {
|
|
75
|
+
if (TIME_LIKE_STRING.test(value)) { // Looks like span
|
|
76
|
+
return this.isTimeSpan(value) ? this.asMillis(value) : undefined;
|
|
77
|
+
} else {
|
|
78
|
+
const parsed = parseInt(value, 10);
|
|
79
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
71
82
|
}
|
|
72
|
-
const result = (typeof value === 'string' && /\d{1,30}[a-z]$/i.test(value)) ?
|
|
73
|
-
(this.isTimeSpan(value) ? this.asMillis(value) : undefined) :
|
|
74
|
-
(typeof value === 'string' ? parseInt(value, 10) :
|
|
75
|
-
(value instanceof Date ? value.getTime() : value));
|
|
76
|
-
return Number.isNaN(result) ? undefined : result;
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
/**
|
|
@@ -90,11 +96,16 @@ export class TimeUtil {
|
|
|
90
96
|
* @param time Time in milliseconds
|
|
91
97
|
*/
|
|
92
98
|
static asClock(time: number): string {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
const rawSeconds = Math.trunc(time / 1000);
|
|
100
|
+
const seconds = rawSeconds % 60;
|
|
101
|
+
const minutes = Math.trunc(rawSeconds / 60) % 60;
|
|
102
|
+
const hours = Math.trunc(rawSeconds / 3600);
|
|
103
|
+
if (hours) {
|
|
104
|
+
return `${hours.toString().padStart(2, '0')}h ${minutes.toString().padStart(2, '0')}m`;
|
|
105
|
+
} else if (minutes) {
|
|
106
|
+
return `${minutes.toString().padStart(2, '0')}m ${seconds.toString().padStart(2, '0')}s`;
|
|
107
|
+
} else {
|
|
108
|
+
return `${seconds.toString().padStart(2, '0')}s`;
|
|
109
|
+
}
|
|
99
110
|
}
|
|
100
111
|
}
|
package/src/trv.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ManifestModuleRole } from '@travetto/manifest';
|
|
2
2
|
import { type TimeSpan } from './time.ts';
|
|
3
|
-
type Role = Exclude<ManifestModuleRole, '
|
|
3
|
+
type Role = Exclude<ManifestModuleRole, 'compile'>;
|
|
4
4
|
|
|
5
5
|
declare module "@travetto/runtime" {
|
|
6
6
|
interface EnvData {
|
|
@@ -10,15 +10,11 @@ declare module "@travetto/runtime" {
|
|
|
10
10
|
*/
|
|
11
11
|
NODE_ENV: 'development' | 'production';
|
|
12
12
|
/**
|
|
13
|
-
* Outputs all console.debug messages, defaults to
|
|
13
|
+
* Outputs all console.debug messages, defaults to off
|
|
14
14
|
*/
|
|
15
15
|
DEBUG: boolean | string;
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
*/
|
|
19
|
-
TRV_ENV: string;
|
|
20
|
-
/**
|
|
21
|
-
* Special role to run as, used to access additional files from the manifest during runtime.
|
|
17
|
+
* The role we are running as, allows access to additional files from the manifest during runtime.
|
|
22
18
|
*/
|
|
23
19
|
TRV_ROLE: Role;
|
|
24
20
|
/**
|
|
@@ -35,6 +31,11 @@ declare module "@travetto/runtime" {
|
|
|
35
31
|
* @default 2s
|
|
36
32
|
*/
|
|
37
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;
|
|
38
39
|
/**
|
|
39
40
|
* The desired runtime module
|
|
40
41
|
*/
|
package/src/watch.ts
CHANGED
|
@@ -31,26 +31,28 @@ export class WatchUtil {
|
|
|
31
31
|
|
|
32
32
|
/** Convert exit code to a result type */
|
|
33
33
|
static exitCodeToResult(code: number): RetryRunState['result'] {
|
|
34
|
-
return code === this.#RESTART_EXIT_CODE ? 'restart' : code > 0 ? 'error' : 'stop';
|
|
34
|
+
return code === this.#RESTART_EXIT_CODE ? 'restart' : (code !== null && code > 0) ? 'error' : 'stop';
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/** Exit with a restart exit code */
|
|
38
38
|
static exitWithRestart(): void {
|
|
39
|
-
|
|
39
|
+
ShutdownManager.shutdown('SIGTERM', this.#RESTART_EXIT_CODE);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/** Listen for restart signals */
|
|
43
|
-
static
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
+
});
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
/** Trigger a
|
|
52
|
-
static
|
|
53
|
-
subprocess
|
|
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);
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
/** Compute the delay before restarting */
|
|
@@ -64,8 +66,6 @@ export class WatchUtil {
|
|
|
64
66
|
* Run with restart capability
|
|
65
67
|
*/
|
|
66
68
|
static async runWithRetry(run: (state: RetryRunState & { signal: AbortSignal }) => Promise<RetryRunState['result']>, options?: Partial<RetryRunConfig>): Promise<void> {
|
|
67
|
-
const controller = new AbortController();
|
|
68
|
-
const cleanup = ShutdownManager.onGracefulShutdown(() => controller.abort());
|
|
69
69
|
let retryExhausted = false;
|
|
70
70
|
|
|
71
71
|
const state: RetryRunState = {
|
|
@@ -82,14 +82,14 @@ export class WatchUtil {
|
|
|
82
82
|
};
|
|
83
83
|
|
|
84
84
|
|
|
85
|
-
while (!
|
|
85
|
+
outer: while (!ShutdownManager.signal.aborted && !retryExhausted) {
|
|
86
86
|
if (state.iteration > 0) {
|
|
87
87
|
await config.onRetry(state, config);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
state.result = await run({ ...state, signal:
|
|
90
|
+
state.result = await run({ ...state, signal: ShutdownManager.signal }).catch(() => 'error' as const);
|
|
91
91
|
switch (state.result) {
|
|
92
|
-
case 'stop':
|
|
92
|
+
case 'stop': break outer;
|
|
93
93
|
case 'error': state.errorIterations += 1; break;
|
|
94
94
|
case 'restart': {
|
|
95
95
|
state.startTime = Date.now();
|
|
@@ -104,8 +104,6 @@ export class WatchUtil {
|
|
|
104
104
|
if (retryExhausted) {
|
|
105
105
|
throw new AppError(`Operation failed after ${state.errorIterations} attempts`);
|
|
106
106
|
}
|
|
107
|
-
|
|
108
|
-
cleanup?.();
|
|
109
107
|
}
|
|
110
108
|
|
|
111
109
|
/** Watch compiler events */
|