@travetto/runtime 6.0.0 → 7.0.0-rc.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 +2 -2
- package/package.json +4 -4
- package/src/debug.ts +1 -1
- package/src/exec.ts +16 -7
- package/src/function.ts +5 -2
- package/src/shutdown.ts +58 -33
- package/src/time.ts +4 -4
- package/src/types.ts +32 -0
- package/src/util.ts +24 -0
- package/src/watch.ts +3 -2
- package/support/transformer.console-log.ts +7 -1
- package/support/transformer.dynamic-import.ts +1 -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
|
|
@@ -278,7 +278,7 @@ tpl`{{age:20}} {{name: 'bob'}}</>;
|
|
|
278
278
|
```
|
|
279
279
|
|
|
280
280
|
## Time Utilities
|
|
281
|
-
[TimeUtil](https://github.com/travetto/travetto/tree/main/module/runtime/src/time.ts#
|
|
281
|
+
[TimeUtil](https://github.com/travetto/travetto/tree/main/module/runtime/src/time.ts#L19) contains general helper methods, created to assist with time-based inputs via environment variables, command line interfaces, and other string-heavy based input.
|
|
282
282
|
|
|
283
283
|
**Code: Time Utilities**
|
|
284
284
|
```typescript
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/runtime",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0-rc.0",
|
|
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": "^
|
|
28
|
+
"@travetto/manifest": "^7.0.0-rc.0",
|
|
29
29
|
"@types/debug": "^4.1.12",
|
|
30
|
-
"debug": "^4.4.
|
|
30
|
+
"debug": "^4.4.3"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@travetto/transformer": "^
|
|
33
|
+
"@travetto/transformer": "^7.0.0-rc.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependenciesMeta": {
|
|
36
36
|
"@travetto/transformer": {
|
package/src/debug.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { ClassInstance } from './types.ts';
|
|
|
6
6
|
* @augments `@travetto/runtime:DebugBreak`
|
|
7
7
|
*/
|
|
8
8
|
export function DebugBreak(): MethodDecorator {
|
|
9
|
-
return (
|
|
9
|
+
return (instance: ClassInstance, property: string | symbol, descriptor: PropertyDescriptor) => descriptor;
|
|
10
10
|
|
|
11
11
|
}
|
|
12
12
|
|
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/function.ts
CHANGED
|
@@ -6,7 +6,7 @@ export type FunctionMetadata = FunctionMetadataTag & {
|
|
|
6
6
|
import: string;
|
|
7
7
|
module: string;
|
|
8
8
|
modulePath: string;
|
|
9
|
-
methods?: Record<string, FunctionMetadataTag>;
|
|
9
|
+
methods?: Record<string | symbol, FunctionMetadataTag>;
|
|
10
10
|
class?: boolean;
|
|
11
11
|
abstract?: boolean;
|
|
12
12
|
};
|
|
@@ -36,7 +36,10 @@ export function registerFunction(
|
|
|
36
36
|
import: `${pkg}/${pth}`,
|
|
37
37
|
module: pkg,
|
|
38
38
|
modulePath,
|
|
39
|
-
...tag,
|
|
39
|
+
...tag,
|
|
40
|
+
methods,
|
|
41
|
+
abstract,
|
|
42
|
+
class: methods !== undefined
|
|
40
43
|
};
|
|
41
44
|
pending.add(fn);
|
|
42
45
|
Object.defineProperties(fn, { Ⲑid: { value: metadata.id }, [MetadataSymbol]: { value: metadata } });
|
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/types.ts
CHANGED
|
@@ -24,12 +24,24 @@ export type DeepPartial<T> = {
|
|
|
24
24
|
(T[P] extends Any[] ? (DeepPartial<T[P][number]> | null | undefined)[] : DeepPartial<T[P]>));
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
type ValidPrimitiveFields<T, Z = undefined> = {
|
|
28
|
+
[K in keyof T]:
|
|
29
|
+
(T[K] extends (Primitive | Z | undefined) ? K :
|
|
30
|
+
(T[K] extends (Function | undefined) ? never :
|
|
31
|
+
K))
|
|
32
|
+
}[keyof T];
|
|
33
|
+
|
|
34
|
+
export type RetainPrimitiveFields<T, Z = undefined> = Pick<T, ValidPrimitiveFields<T, Z>>;
|
|
35
|
+
|
|
27
36
|
export const TypedObject: {
|
|
28
37
|
keys<T = unknown, K extends keyof T = keyof T & string>(o: T): K[];
|
|
29
38
|
fromEntries<K extends string | symbol, V>(items: ([K, V] | readonly [K, V])[]): Record<K, V>;
|
|
30
39
|
entries<K extends Record<symbol | string, unknown>>(record: K): [keyof K, K[keyof K]][];
|
|
31
40
|
} & ObjectConstructor = Object;
|
|
32
41
|
|
|
42
|
+
export const safeAssign = <T extends {}, U extends {}>(target: T, ...sources: U[]): T & U =>
|
|
43
|
+
Object.assign(target, ...sources);
|
|
44
|
+
|
|
33
45
|
export function castTo<T>(input: unknown): T {
|
|
34
46
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
35
47
|
return input as T;
|
|
@@ -53,6 +65,26 @@ export function toConcrete<T extends unknown>(): Class<T> {
|
|
|
53
65
|
return arguments[0];
|
|
54
66
|
}
|
|
55
67
|
|
|
68
|
+
export function getAllEntries<V>(obj: Record<string | symbol, V>): [string | symbol, V][] {
|
|
69
|
+
return [
|
|
70
|
+
...Object.keys(obj),
|
|
71
|
+
...Object.getOwnPropertySymbols(obj)
|
|
72
|
+
].map(k => [k, obj[k]] as const);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Find parent class for a given class object
|
|
77
|
+
*/
|
|
78
|
+
export function getParentClass(cls: Class): Class | undefined {
|
|
79
|
+
const parent: Class = Object.getPrototypeOf(cls);
|
|
80
|
+
return parent.name && parent !== Object ? parent : undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get the class from an instance or class
|
|
85
|
+
*/
|
|
86
|
+
export const getClass = <T = unknown>(x: ClassInstance | Class): Class<T> => 'Ⲑid' in x ? castTo(x) : asConstructable<T>(x).constructor;
|
|
87
|
+
|
|
56
88
|
/**
|
|
57
89
|
* Range of bytes, inclusive
|
|
58
90
|
*/
|
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();
|
|
@@ -8,7 +8,7 @@ import { OnCall, TransformerState } from '@travetto/transformer';
|
|
|
8
8
|
export class DynamicImportTransformer {
|
|
9
9
|
|
|
10
10
|
@OnCall()
|
|
11
|
-
static
|
|
11
|
+
static onCall(state: TransformerState, node: ts.CallExpression): typeof node | ts.Identifier {
|
|
12
12
|
if (
|
|
13
13
|
ts.isCallExpression(node) &&
|
|
14
14
|
node.expression.kind === ts.SyntaxKind.ImportKeyword &&
|