@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 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#L12) includes:
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.0-rc.5",
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-rc.3",
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.0-rc.4"
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 { ManifestModuleUtil, type ChangeEventType, type ManifestModuleFileType } from '@travetto/manifest';
1
+ import { ChildProcess } from 'node:child_process';
2
2
 
3
- import { RuntimeIndex } from './manifest-index.ts';
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
- type WatchEvent = { file: string, action: ChangeEventType, output: string, module: string, import: string, time: number };
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 WatchCompilerOptions = {
10
- /**
11
- * Restart the watch loop on compiler exit
12
- */
13
- restartOnCompilerExit?: boolean;
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
- export async function* watchCompiler(config?: WatchCompilerOptions): AsyncIterable<WatchEvent> {
21
- // Load at runtime
22
- const { CompilerClient } = await import('@travetto/compiler/support/server/client.ts');
23
-
24
- const client = new CompilerClient(RuntimeIndex.manifest, {
25
- warn(message, ...args): void { console.error('warn', message, ...args); },
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
- await client.waitForState(['compile-end', 'watch-start'], undefined, controller.signal);
25
+ /**
26
+ * Utilities for watching resources
27
+ */
28
+ export class WatchUtil {
49
29
 
50
- if (!await client.isWatching()) { // If we get here, without a watch
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
- iterations.push(Date.now());
60
- if (iterations.length >= maxIterations) {
61
- iterationsExhausted = (Date.now() - iterations[0]) > maxWindow;
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
- remove();
67
- }
37
+ /** Exit with a restart exit code */
38
+ static exitWithRestart(): void {
39
+ process.exit(this.#RESTART_EXIT_CODE);
40
+ }
68
41
 
69
- export function listenForSourceChanges(onChange: () => void, debounceDelay = 10): void {
70
- let timeout: ReturnType<typeof setTimeout> | undefined;
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
- const validFileTypes = new Set<ManifestModuleFileType>(['ts', 'js', 'package-json', 'typings']);
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
- function send(): void {
75
- clearTimeout(timeout);
76
- timeout = setTimeout(onChange, debounceDelay);
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
- (async function (): Promise<void> {
80
- for await (const item of watchCompiler({ restartOnCompilerExit: true, onRestart: send })) {
81
- if (validFileTypes.has(ManifestModuleUtil.getFileType(item.file)) && RuntimeIndex.findModuleForArbitraryFile(item.file)) {
82
- send();
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
  }