@travetto/runtime 7.0.0-rc.5 → 7.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 +3 -3
- package/src/exec.ts +28 -0
- package/src/util.ts +0 -25
- package/src/watch.ts +118 -68
package/README.md
CHANGED
|
@@ -267,7 +267,7 @@ The framework provides utilities for working with JSON data. This module provid
|
|
|
267
267
|
* `readFileSync(file: string, onMissing?: any)` reads a JSON file synchronously.
|
|
268
268
|
|
|
269
269
|
## Common Utilities
|
|
270
|
-
Common utilities used throughout the framework. Currently [Util](https://github.com/travetto/travetto/tree/main/module/runtime/src/util.ts#
|
|
270
|
+
Common utilities used throughout the framework. Currently [Util](https://github.com/travetto/travetto/tree/main/module/runtime/src/util.ts#L11) includes:
|
|
271
271
|
* `uuid(len: number)` generates a simple uuid for use within the application.
|
|
272
272
|
* `allowDenyMatcher(rules[])` builds a matching function that leverages the rules as an allow/deny list, where order of the rules matters. Negative rules are prefixed by '!'.
|
|
273
273
|
* `hash(text: string, size?: number)` produces a full sha512 hash.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/runtime",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.1",
|
|
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.0
|
|
29
|
+
"@travetto/manifest": "^7.0.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.0.1"
|
|
35
35
|
},
|
|
36
36
|
"peerDependenciesMeta": {
|
|
37
37
|
"@travetto/transformer": {
|
package/src/exec.ts
CHANGED
|
@@ -39,6 +39,34 @@ type ExecutionBaseResult = Omit<ExecutionResult, 'stdout' | 'stderr'>;
|
|
|
39
39
|
*/
|
|
40
40
|
export class ExecUtil {
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Defer control to subprocess execution, mainly used for nested execution
|
|
44
|
+
*/
|
|
45
|
+
static async deferToSubprocess(child: ChildProcess, relayInterrupt: boolean = true): Promise<ExecutionResult> {
|
|
46
|
+
if (!relayInterrupt) {
|
|
47
|
+
process.removeAllListeners('SIGINT'); // Remove any existing listeners
|
|
48
|
+
process.on('SIGINT', () => { }); // Prevents SIGINT from killing parent process, the child will handle
|
|
49
|
+
}
|
|
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
|
+
}
|
|
69
|
+
|
|
42
70
|
/**
|
|
43
71
|
* Take a child process, and some additional options, and produce a promise that
|
|
44
72
|
* represents the entire execution. On successful completion the promise will resolve, and
|
package/src/util.ts
CHANGED
|
@@ -2,7 +2,6 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import timers from 'node:timers/promises';
|
|
3
3
|
|
|
4
4
|
import { castTo } from './types.ts';
|
|
5
|
-
import { AppError } from './error.ts';
|
|
6
5
|
|
|
7
6
|
type MapFn<T, U> = (value: T, i: number) => U | Promise<U>;
|
|
8
7
|
|
|
@@ -119,28 +118,4 @@ export class Util {
|
|
|
119
118
|
return () => true;
|
|
120
119
|
}
|
|
121
120
|
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Retry an operation, with a custom conflict handler
|
|
125
|
-
* @param operation The operation to retry
|
|
126
|
-
* @param isHandledConflict Function to determine if the error is a handled conflict
|
|
127
|
-
* @param maxTries Maximum number of retries
|
|
128
|
-
*/
|
|
129
|
-
static async acquireWithRetry<T>(
|
|
130
|
-
operation: () => T | Promise<T>,
|
|
131
|
-
prepareRetry: (error: unknown, count: number) => (void | undefined | boolean | Promise<(void | undefined | boolean)>),
|
|
132
|
-
maxTries = 5,
|
|
133
|
-
): Promise<T> {
|
|
134
|
-
for (let i = 0; i < maxTries; i++) {
|
|
135
|
-
try {
|
|
136
|
-
return await operation();
|
|
137
|
-
} catch (error) {
|
|
138
|
-
if (i === maxTries - 1 || await prepareRetry(error, i) === false) {
|
|
139
|
-
throw error; // Stop retrying if we reached max tries or prepareRetry returns false
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
throw new AppError(`Operation failed after ${maxTries} attempts`);
|
|
145
|
-
}
|
|
146
121
|
}
|
package/src/watch.ts
CHANGED
|
@@ -1,86 +1,136 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ChildProcess } from 'node:child_process';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { ShutdownManager } from './shutdown.ts';
|
|
5
|
-
import { Util } from './util.ts';
|
|
3
|
+
import type { CompilerEventPayload, CompilerEventType } from '@travetto/compiler/support/types.ts';
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
import { AppError } from './error';
|
|
6
|
+
import { Util } from './util';
|
|
7
|
+
import { RuntimeIndex } from './manifest-index';
|
|
8
|
+
import { ShutdownManager } from './shutdown';
|
|
9
|
+
import { castTo } from './types';
|
|
8
10
|
|
|
9
|
-
type
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Run on restart
|
|
16
|
-
*/
|
|
17
|
-
onRestart?: () => void;
|
|
11
|
+
type RetryRunState = {
|
|
12
|
+
iteration: number;
|
|
13
|
+
startTime: number;
|
|
14
|
+
errorIterations: number;
|
|
15
|
+
result?: 'error' | 'restart' | 'stop';
|
|
18
16
|
};
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
debug(message, ...args): void { console.error('debug', message, ...args); },
|
|
27
|
-
error(message, ...args): void { console.error('error', message, ...args); },
|
|
28
|
-
info(message, ...args): void { console.error('info', message, ...args); },
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
const controller = new AbortController();
|
|
32
|
-
const remove = ShutdownManager.onGracefulShutdown(async () => controller.abort());
|
|
33
|
-
|
|
34
|
-
const maxIterations = 10;
|
|
35
|
-
const maxWindow = 10 * 1000;
|
|
36
|
-
const iterations: number[] = [];
|
|
37
|
-
let iterationsExhausted = false;
|
|
38
|
-
|
|
39
|
-
while (
|
|
40
|
-
!controller.signal.aborted &&
|
|
41
|
-
!iterationsExhausted &&
|
|
42
|
-
(config?.restartOnCompilerExit || iterations.length === 0)
|
|
43
|
-
) {
|
|
44
|
-
if (iterations.length) { // Wait on next iteration
|
|
45
|
-
await Util.nonBlockingTimeout(10);
|
|
46
|
-
}
|
|
18
|
+
type RetryRunConfig = {
|
|
19
|
+
maxRetries: number;
|
|
20
|
+
maxRetryWindow: number;
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
onRetry: (state: RetryRunState, config: RetryRunConfig) => (unknown | Promise<unknown>);
|
|
23
|
+
};
|
|
47
24
|
|
|
48
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Utilities for watching resources
|
|
27
|
+
*/
|
|
28
|
+
export class WatchUtil {
|
|
49
29
|
|
|
50
|
-
|
|
51
|
-
await Util.nonBlockingTimeout(maxWindow / (maxIterations * 2));
|
|
52
|
-
} else {
|
|
53
|
-
if (iterations.length) {
|
|
54
|
-
config?.onRestart?.();
|
|
55
|
-
}
|
|
56
|
-
yield* client.fetchEvents('change', { signal: controller.signal, enforceIteration: true });
|
|
57
|
-
}
|
|
30
|
+
static #RESTART_EXIT_CODE = 200;
|
|
58
31
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
iterations.shift();
|
|
63
|
-
}
|
|
32
|
+
/** Convert exit code to a result type */
|
|
33
|
+
static exitCodeToResult(code: number): RetryRunState['result'] {
|
|
34
|
+
return code === this.#RESTART_EXIT_CODE ? 'restart' : code > 0 ? 'error' : 'stop';
|
|
64
35
|
}
|
|
65
36
|
|
|
66
|
-
|
|
67
|
-
|
|
37
|
+
/** Exit with a restart exit code */
|
|
38
|
+
static exitWithRestart(): void {
|
|
39
|
+
process.exit(this.#RESTART_EXIT_CODE);
|
|
40
|
+
}
|
|
68
41
|
|
|
69
|
-
|
|
70
|
-
|
|
42
|
+
/** Listen for restart signals */
|
|
43
|
+
static listenForRestartSignal(): void {
|
|
44
|
+
const listener = (event: unknown): void => {
|
|
45
|
+
if (event === 'WATCH_RESTART') { this.exitWithRestart(); }
|
|
46
|
+
};
|
|
47
|
+
process.on('message', listener);
|
|
48
|
+
ShutdownManager.onGracefulShutdown(() => { process.removeListener('message', listener); });
|
|
49
|
+
}
|
|
71
50
|
|
|
72
|
-
|
|
51
|
+
/** Trigger a restart signal to a subprocess */
|
|
52
|
+
static triggerRestartSignal(subprocess?: ChildProcess): void {
|
|
53
|
+
subprocess?.connected && subprocess.send?.('WATCH_RESTART');
|
|
54
|
+
}
|
|
73
55
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
56
|
+
/** Compute the delay before restarting */
|
|
57
|
+
static computeRestartDelay(state: RetryRunState, config: RetryRunConfig): number {
|
|
58
|
+
return state.result === 'error'
|
|
59
|
+
? config.maxRetryWindow / (config.maxRetries + 1)
|
|
60
|
+
: 10;
|
|
77
61
|
}
|
|
78
62
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Run with restart capability
|
|
65
|
+
*/
|
|
66
|
+
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
|
+
let retryExhausted = false;
|
|
70
|
+
|
|
71
|
+
const state: RetryRunState = {
|
|
72
|
+
iteration: 0,
|
|
73
|
+
errorIterations: 0,
|
|
74
|
+
startTime: Date.now()
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const config: RetryRunConfig = {
|
|
78
|
+
maxRetryWindow: 10 * 1000,
|
|
79
|
+
maxRetries: 10,
|
|
80
|
+
onRetry: () => Util.nonBlockingTimeout(this.computeRestartDelay(state, config)),
|
|
81
|
+
...options,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
while (!controller.signal.aborted && !retryExhausted) {
|
|
86
|
+
if (state.iteration > 0) {
|
|
87
|
+
await config.onRetry(state, config);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
state.result = await run({ ...state, signal: controller.signal }).catch(() => 'error' as const);
|
|
91
|
+
switch (state.result) {
|
|
92
|
+
case 'stop': controller.abort(); break;
|
|
93
|
+
case 'error': state.errorIterations += 1; break;
|
|
94
|
+
case 'restart': {
|
|
95
|
+
state.startTime = Date.now();
|
|
96
|
+
state.errorIterations = 0;
|
|
97
|
+
}
|
|
83
98
|
}
|
|
99
|
+
|
|
100
|
+
retryExhausted = (state.errorIterations >= config.maxRetries) || (Date.now() - state.startTime >= config.maxRetryWindow);
|
|
101
|
+
state.iteration += 1;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (retryExhausted) {
|
|
105
|
+
throw new AppError(`Operation failed after ${state.errorIterations} attempts`);
|
|
84
106
|
}
|
|
85
|
-
|
|
107
|
+
|
|
108
|
+
cleanup?.();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Watch compiler events */
|
|
112
|
+
static async watchCompilerEvents<K extends CompilerEventType, T extends CompilerEventPayload<K>>(
|
|
113
|
+
type: K,
|
|
114
|
+
onChange: (input: T) => unknown,
|
|
115
|
+
filter?: (input: T) => boolean,
|
|
116
|
+
options?: Partial<RetryRunConfig>,
|
|
117
|
+
): Promise<void> {
|
|
118
|
+
const { CompilerClient } = await import('@travetto/compiler/support/server/client.ts');
|
|
119
|
+
const client = new CompilerClient(RuntimeIndex.manifest, console);
|
|
120
|
+
|
|
121
|
+
return this.runWithRetry(async ({ signal }) => {
|
|
122
|
+
await client.waitForState(['compile-end', 'watch-start'], undefined, signal);
|
|
123
|
+
|
|
124
|
+
if (!await client.isWatching()) { // If we get here, without a watch
|
|
125
|
+
return 'error';
|
|
126
|
+
} else {
|
|
127
|
+
for await (const event of client.fetchEvents(type, { signal, enforceIteration: true })) {
|
|
128
|
+
if (!filter || filter(castTo(event))) {
|
|
129
|
+
await onChange(castTo(event));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return 'restart';
|
|
133
|
+
}
|
|
134
|
+
}, options);
|
|
135
|
+
}
|
|
86
136
|
}
|