@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 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 api for Web Applications with support for the dependency injection.") module and HTTP status codes).
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#L17) contains general helper methods, created to assist with time-based inputs via environment variables, command line interfaces, and other string-heavy based input.
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": "6.0.0",
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": "^6.0.0",
28
+ "@travetto/manifest": "^7.0.0-rc.0",
29
29
  "@types/debug": "^4.1.12",
30
- "debug": "^4.4.0"
30
+ "debug": "^4.4.3"
31
31
  },
32
32
  "peerDependencies": {
33
- "@travetto/transformer": "^6.0.0"
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 (inst: ClassInstance, prop: string | symbol, descriptor: PropertyDescriptor) => descriptor;
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
- process.on('SIGINT', toKill);
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
- process.off('SIGINT', toKill);
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, methods, abstract, class: abstract !== undefined
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: { name?: string, handler: () => (void | Promise<void>) }[] = [];
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 name name to log for
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>), name?: string | { constructor: Function }): () => void {
19
- if (!this.#registered) {
20
- this.#registered = true;
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(code: number | string | undefined = process.exitCode): Promise<void> {
37
- await Util.queueMacroTask(); // Force the event loop to wait one cycle
38
-
39
- if (code !== undefined) {
40
- process.exitCode = code;
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
- if (this.#handlers.length) {
44
- console.debug('Graceful shutdown: started');
60
+ this.#pending = { ...Promise.withResolvers<void>(), time: Date.now() };
45
61
 
46
- const items = this.#handlers.splice(0, this.#handlers.length);
47
- const handlers = Promise.all(items.map(async ({ name, handler }) => {
48
- if (name) {
49
- console.debug('Stopping', { name });
50
- }
51
- try {
52
- return await handler();
53
- } catch (err) {
54
- console.error('Error shutting down', { name, err });
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
- await Promise.race([
59
- Util.nonBlockingTimeout(TimeUtil.fromValue(Env.TRV_SHUTDOWN_WAIT.val) ?? 2000), // Wait 2s and then force finish
60
- handlers,
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
- if (code !== undefined) {
66
- process.exit();
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
- export class TimeUtil {
17
+ const TIME_PATTERN = new RegExp(`^(?<amount>-?[0-9.]+)(?<unit>${Object.keys(TIME_UNITS).join('|')})$`);
18
18
 
19
- static #timePattern = new RegExp(`^(?<amount>-?[0-9.]+)(?<unit>${Object.keys(TIME_UNITS).join('|')})$`);
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 this.#timePattern.test(val);
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(this.#timePattern)?.groups ?? {};
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(), watchCompiler);
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(ExecUtil.RESTART_EXIT_CODE);
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 { TransformerState, OnCall, LiteralUtil, OnClass, AfterClass, OnMethod, AfterMethod, AfterFunction, OnFunction } from '@travetto/transformer';
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 onLogCall(state: TransformerState, node: ts.CallExpression): typeof node | ts.Identifier {
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 &&