@travetto/runtime 6.0.0 → 6.0.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.
- package/README.md +1 -1
- package/package.json +4 -4
- package/src/exec.ts +16 -7
- package/src/shutdown.ts +58 -33
- package/src/time.ts +4 -4
- package/src/util.ts +24 -0
- package/src/watch.ts +3 -2
- package/support/transformer.console-log.ts +7 -1
package/README.md
CHANGED
|
@@ -168,7 +168,7 @@ export class EnvProp<T> {
|
|
|
168
168
|
```
|
|
169
169
|
|
|
170
170
|
## Standard Error Support
|
|
171
|
-
While the framework is 100 % compatible with standard `Error` instances, there are cases in which additional functionality is desired. Within the framework we use [AppError](https://github.com/travetto/travetto/tree/main/module/runtime/src/error.ts#L26) (or its derivatives) to represent framework errors. This class is available for use in your own projects. Some of the additional benefits of using this class is enhanced error reporting, as well as better integration with other modules (e.g. the [Web API](https://github.com/travetto/travetto/tree/main/module/web#readme "Declarative
|
|
171
|
+
While the framework is 100 % compatible with standard `Error` instances, there are cases in which additional functionality is desired. Within the framework we use [AppError](https://github.com/travetto/travetto/tree/main/module/runtime/src/error.ts#L26) (or its derivatives) to represent framework errors. This class is available for use in your own projects. Some of the additional benefits of using this class is enhanced error reporting, as well as better integration with other modules (e.g. the [Web API](https://github.com/travetto/travetto/tree/main/module/web#readme "Declarative support for creating Web Applications") module and HTTP status codes).
|
|
172
172
|
|
|
173
173
|
The [AppError](https://github.com/travetto/travetto/tree/main/module/runtime/src/error.ts#L26) takes in a message, and an optional payload and / or error classification. The currently supported error classifications are:
|
|
174
174
|
* `general` - General purpose errors
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/runtime",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.1",
|
|
4
4
|
"description": "Runtime for travetto applications.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"console-manager",
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
"directory": "module/runtime"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@travetto/manifest": "^6.0.
|
|
28
|
+
"@travetto/manifest": "^6.0.1",
|
|
29
29
|
"@types/debug": "^4.1.12",
|
|
30
|
-
"debug": "^4.4.
|
|
30
|
+
"debug": "^4.4.3"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@travetto/transformer": "^6.0.
|
|
33
|
+
"@travetto/transformer": "^6.0.1"
|
|
34
34
|
},
|
|
35
35
|
"peerDependenciesMeta": {
|
|
36
36
|
"@travetto/transformer": {
|
package/src/exec.ts
CHANGED
|
@@ -51,28 +51,37 @@ export class ExecUtil {
|
|
|
51
51
|
*/
|
|
52
52
|
static async withRestart(
|
|
53
53
|
run: () => ChildProcess,
|
|
54
|
-
maxRetriesPerMinute?: number,
|
|
55
|
-
killSignal: 'SIGINT' | 'SIGTERM' = 'SIGINT'
|
|
54
|
+
config?: { maxRetriesPerMinute?: number, relayInterrupt?: boolean }
|
|
56
55
|
): Promise<ExecutionResult> {
|
|
57
|
-
const maxRetries = maxRetriesPerMinute ?? 5;
|
|
56
|
+
const maxRetries = config?.maxRetriesPerMinute ?? 5;
|
|
57
|
+
const relayInterrupt = config?.relayInterrupt ?? false;
|
|
58
|
+
|
|
58
59
|
const restarts: number[] = [];
|
|
59
60
|
|
|
61
|
+
if (!relayInterrupt) {
|
|
62
|
+
process.removeAllListeners('SIGINT'); // Remove any existing listeners
|
|
63
|
+
process.on('SIGINT', () => { }); // Prevents SIGINT from killing parent process, the child will handle
|
|
64
|
+
}
|
|
65
|
+
|
|
60
66
|
for (; ;) {
|
|
61
67
|
const proc = run();
|
|
62
|
-
|
|
63
|
-
const toKill = (): void => { proc.kill(killSignal); };
|
|
68
|
+
const interrupt = (): void => { proc.kill('SIGINT'); };
|
|
64
69
|
const toMessage = (v: unknown): void => { proc.send?.(v!); };
|
|
65
70
|
|
|
66
71
|
// Proxy kill requests
|
|
67
72
|
process.on('message', toMessage);
|
|
68
|
-
|
|
73
|
+
if (relayInterrupt) {
|
|
74
|
+
process.on('SIGINT', interrupt);
|
|
75
|
+
}
|
|
69
76
|
proc.on('message', v => process.send?.(v));
|
|
70
77
|
|
|
71
78
|
const result = await this.getResult(proc, { catch: true });
|
|
72
79
|
if (result.code !== this.RESTART_EXIT_CODE) {
|
|
73
80
|
return result;
|
|
74
81
|
} else {
|
|
75
|
-
|
|
82
|
+
if (relayInterrupt) {
|
|
83
|
+
process.off('SIGINT', interrupt);
|
|
84
|
+
}
|
|
76
85
|
process.off('message', toMessage);
|
|
77
86
|
restarts.unshift(Date.now());
|
|
78
87
|
if (restarts.length === maxRetries) {
|
package/src/shutdown.ts
CHANGED
|
@@ -8,20 +8,37 @@ import { TimeUtil } from './time.ts';
|
|
|
8
8
|
export class ShutdownManager {
|
|
9
9
|
|
|
10
10
|
static #registered = false;
|
|
11
|
-
static #handlers: {
|
|
11
|
+
static #handlers: { scope?: string, handler: () => (void | Promise<void>) }[] = [];
|
|
12
|
+
static #pending: (PromiseWithResolvers<void> & { time: number }) | undefined;
|
|
13
|
+
|
|
14
|
+
static #ensureExitListeners(): void {
|
|
15
|
+
if (this.#registered) {
|
|
16
|
+
return;
|
|
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));
|
|
25
|
+
};
|
|
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
|
+
}
|
|
12
33
|
|
|
13
34
|
/**
|
|
14
35
|
* On Shutdown requested
|
|
15
|
-
* @param
|
|
36
|
+
* @param source The source of the shutdown request, for logging purposes
|
|
16
37
|
* @param handler synchronous or asynchronous handler
|
|
17
38
|
*/
|
|
18
|
-
static onGracefulShutdown(handler: () => (void | Promise<void>),
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const done = (): void => { this.gracefulShutdown(0); };
|
|
22
|
-
process.on('SIGUSR2', done).on('SIGTERM', done).on('SIGINT', done);
|
|
23
|
-
}
|
|
24
|
-
this.#handlers.push({ handler, name: typeof name === 'string' ? name : name?.constructor?.Ⲑid });
|
|
39
|
+
static onGracefulShutdown(handler: () => (void | Promise<void>), scope?: string): () => void {
|
|
40
|
+
this.#ensureExitListeners();
|
|
41
|
+
this.#handlers.push({ handler, scope });
|
|
25
42
|
return () => {
|
|
26
43
|
const idx = this.#handlers.findIndex(x => x.handler === handler);
|
|
27
44
|
if (idx >= 0) {
|
|
@@ -33,37 +50,45 @@ export class ShutdownManager {
|
|
|
33
50
|
/**
|
|
34
51
|
* Wait for graceful shutdown to run and complete
|
|
35
52
|
*/
|
|
36
|
-
static async gracefulShutdown(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
|
|
53
|
+
static async gracefulShutdown(source: string): Promise<void> {
|
|
54
|
+
if (this.#pending) {
|
|
55
|
+
return this.#pending.promise;
|
|
56
|
+
} else if (!this.#handlers.length) {
|
|
57
|
+
return;
|
|
41
58
|
}
|
|
42
59
|
|
|
43
|
-
|
|
44
|
-
console.debug('Graceful shutdown: started');
|
|
60
|
+
this.#pending = { ...Promise.withResolvers<void>(), time: Date.now() };
|
|
45
61
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
62
|
+
await Util.queueMacroTask(); // Force the event loop to wait one cycle
|
|
63
|
+
|
|
64
|
+
const timeout = TimeUtil.fromValue(Env.TRV_SHUTDOWN_WAIT.val) ?? 2000;
|
|
65
|
+
const items = this.#handlers.splice(0, this.#handlers.length);
|
|
66
|
+
console.debug('Graceful shutdown: started', { source, timeout, count: items.length });
|
|
67
|
+
const handlers = Promise.all(items.map(async ({ scope, handler }) => {
|
|
68
|
+
if (scope) {
|
|
69
|
+
console.debug('Stopping', { scope });
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
await handler();
|
|
73
|
+
if (scope) {
|
|
74
|
+
console.debug('Stopped', { scope });
|
|
55
75
|
}
|
|
56
|
-
})
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error('Error stopping', { err, scope });
|
|
78
|
+
}
|
|
79
|
+
}));
|
|
57
80
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
81
|
+
const winner = await Promise.race([
|
|
82
|
+
Util.nonBlockingTimeout(timeout).then(() => this), // Wait N seconds and then give up if not done
|
|
83
|
+
handlers,
|
|
84
|
+
]);
|
|
62
85
|
|
|
86
|
+
if (winner !== this) {
|
|
63
87
|
console.debug('Graceful shutdown: completed');
|
|
88
|
+
} else {
|
|
89
|
+
console.debug('Graceful shutdown: timed-out', { timeout });
|
|
64
90
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
91
|
+
|
|
92
|
+
this.#pending.resolve();
|
|
68
93
|
}
|
|
69
94
|
}
|
package/src/time.ts
CHANGED
|
@@ -14,16 +14,16 @@ const TIME_UNITS = {
|
|
|
14
14
|
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
18
|
|
|
19
|
-
|
|
19
|
+
export class TimeUtil {
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Test to see if a string is valid for relative time
|
|
23
23
|
* @param val
|
|
24
24
|
*/
|
|
25
25
|
static isTimeSpan(val: string): val is TimeSpan {
|
|
26
|
-
return
|
|
26
|
+
return TIME_PATTERN.test(val);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
@@ -35,7 +35,7 @@ export class TimeUtil {
|
|
|
35
35
|
if (amount instanceof Date) {
|
|
36
36
|
return amount.getTime();
|
|
37
37
|
} else if (typeof amount === 'string') {
|
|
38
|
-
const groups: { amount?: string, unit?: TimeUnit } = amount.match(
|
|
38
|
+
const groups: { amount?: string, unit?: TimeUnit } = amount.match(TIME_PATTERN)?.groups ?? {};
|
|
39
39
|
const amountStr = groups.amount ?? `${amount}`;
|
|
40
40
|
unit = groups.unit ?? unit ?? 'ms';
|
|
41
41
|
if (!TIME_UNITS[unit]) {
|
package/src/util.ts
CHANGED
|
@@ -195,4 +195,28 @@ export class Util {
|
|
|
195
195
|
}
|
|
196
196
|
});
|
|
197
197
|
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Retry an operation, with a custom conflict handler
|
|
201
|
+
* @param op The operation to retry
|
|
202
|
+
* @param isHandledConflict Function to determine if the error is a handled conflict
|
|
203
|
+
* @param maxTries Maximum number of retries
|
|
204
|
+
*/
|
|
205
|
+
static async acquireWithRetry<T>(
|
|
206
|
+
op: () => T | Promise<T>,
|
|
207
|
+
prepareRetry: (err: unknown, count: number) => (void | undefined | boolean | Promise<(void | undefined | boolean)>),
|
|
208
|
+
maxTries = 5,
|
|
209
|
+
): Promise<T> {
|
|
210
|
+
for (let i = 0; i < maxTries; i++) {
|
|
211
|
+
try {
|
|
212
|
+
return await op();
|
|
213
|
+
} catch (err) {
|
|
214
|
+
if (i === maxTries - 1 || await prepareRetry(err, i) === false) {
|
|
215
|
+
throw err; // Stop retrying if we reached max tries or prepareRetry returns false
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
throw new AppError(`Operation failed after ${maxTries} attempts`);
|
|
221
|
+
}
|
|
198
222
|
}
|
package/src/watch.ts
CHANGED
|
@@ -17,7 +17,7 @@ export async function* watchCompiler(cfg?: { restartOnExit?: boolean, signal?: A
|
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
const ctrl = new AbortController();
|
|
20
|
-
const remove = ShutdownManager.onGracefulShutdown(async () => ctrl.abort()
|
|
20
|
+
const remove = ShutdownManager.onGracefulShutdown(async () => ctrl.abort());
|
|
21
21
|
|
|
22
22
|
await client.waitForState(['compile-end', 'watch-start'], undefined, ctrl.signal);
|
|
23
23
|
|
|
@@ -33,6 +33,7 @@ export async function* watchCompiler(cfg?: { restartOnExit?: boolean, signal?: A
|
|
|
33
33
|
|
|
34
34
|
if (cfg?.restartOnExit) {
|
|
35
35
|
// We are done, request restart
|
|
36
|
-
await ShutdownManager.gracefulShutdown(
|
|
36
|
+
await ShutdownManager.gracefulShutdown('@travetto/runtime:watchCompiler');
|
|
37
|
+
process.exit(ExecUtil.RESTART_EXIT_CODE);
|
|
37
38
|
}
|
|
38
39
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
TransformerState, OnCall, LiteralUtil, OnClass, AfterClass, OnMethod, AfterMethod,
|
|
5
|
+
AfterFunction, OnFunction, OnStaticMethod, AfterStaticMethod
|
|
6
|
+
} from '@travetto/transformer';
|
|
4
7
|
|
|
5
8
|
const CONSOLE_IMPORT = '@travetto/runtime/src/console.ts';
|
|
6
9
|
|
|
@@ -39,6 +42,8 @@ export class ConsoleLogTransformer {
|
|
|
39
42
|
return node;
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
|
|
46
|
+
@OnStaticMethod()
|
|
42
47
|
@OnMethod()
|
|
43
48
|
static startMethodForLog(state: CustomState, node: ts.MethodDeclaration): typeof node {
|
|
44
49
|
this.initState(state);
|
|
@@ -50,6 +55,7 @@ export class ConsoleLogTransformer {
|
|
|
50
55
|
return node;
|
|
51
56
|
}
|
|
52
57
|
|
|
58
|
+
@AfterStaticMethod()
|
|
53
59
|
@AfterMethod()
|
|
54
60
|
static leaveMethodForLog(state: CustomState, node: ts.MethodDeclaration): typeof node {
|
|
55
61
|
state.scope.pop();
|