@travetto/runtime 5.0.0-rc.2
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/LICENSE +21 -0
- package/README.md +304 -0
- package/__index__.ts +16 -0
- package/package.json +48 -0
- package/src/console.ts +137 -0
- package/src/context.ts +100 -0
- package/src/env.ts +109 -0
- package/src/error.ts +66 -0
- package/src/exec.ts +156 -0
- package/src/file-loader.ts +59 -0
- package/src/function.ts +42 -0
- package/src/global.d.ts +3 -0
- package/src/manifest-index.ts +4 -0
- package/src/resources.ts +26 -0
- package/src/shutdown.ts +65 -0
- package/src/time.ts +101 -0
- package/src/trv.d.ts +59 -0
- package/src/types.ts +12 -0
- package/src/util.ts +104 -0
- package/src/watch.ts +38 -0
- package/support/transformer.console-log.ts +113 -0
- package/support/transformer.function-metadata.ts +137 -0
- package/support/transformer.rewrite-path-import.ts +39 -0
package/src/env.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const IS_TRUE = /^(true|yes|on|1)$/i;
|
|
2
|
+
const IS_FALSE = /^(false|no|off|0)$/i;
|
|
3
|
+
|
|
4
|
+
export class EnvProp<T> {
|
|
5
|
+
constructor(public readonly key: string) { }
|
|
6
|
+
|
|
7
|
+
/** Set value according to prop type */
|
|
8
|
+
set(val: T | undefined | null): void {
|
|
9
|
+
if (val === undefined || val === null) {
|
|
10
|
+
delete process.env[this.key];
|
|
11
|
+
} else {
|
|
12
|
+
process.env[this.key] = Array.isArray(val) ? `${val.join(',')}` : `${val}`;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Remove value */
|
|
17
|
+
clear(): void {
|
|
18
|
+
this.set(null);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Export value */
|
|
22
|
+
export(val: T | undefined): Record<string, string> {
|
|
23
|
+
let out: string;
|
|
24
|
+
if (val === undefined || val === '' || val === null) {
|
|
25
|
+
out = '';
|
|
26
|
+
} else if (Array.isArray(val)) {
|
|
27
|
+
out = val.join(',');
|
|
28
|
+
} else if (typeof val === 'object') {
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
30
|
+
out = Object.entries(val as Record<string, string>).map(([k, v]) => `${k}=${v}`).join(',');
|
|
31
|
+
} else {
|
|
32
|
+
out = `${val}`;
|
|
33
|
+
}
|
|
34
|
+
return { [this.key]: out };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Read value as string */
|
|
38
|
+
get val(): string | undefined { return process.env[this.key] || undefined; }
|
|
39
|
+
|
|
40
|
+
/** Read value as list */
|
|
41
|
+
get list(): string[] | undefined {
|
|
42
|
+
const val = this.val;
|
|
43
|
+
return (val === undefined || val === '') ?
|
|
44
|
+
undefined : val.split(/[, ]+/g).map(x => x.trim()).filter(x => !!x);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Read value as object */
|
|
48
|
+
get object(): Record<string, string> | undefined {
|
|
49
|
+
const items = this.list;
|
|
50
|
+
return items ? Object.fromEntries(items.map(x => x.split(/[:=]/g))) : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Add values to list */
|
|
54
|
+
add(...items: string[]): void {
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
56
|
+
this.set([... new Set([...this.list ?? [], ...items])] as T);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Read value as int */
|
|
60
|
+
get int(): number | undefined {
|
|
61
|
+
const vi = parseInt(this.val ?? '', 10);
|
|
62
|
+
return Number.isNaN(vi) ? undefined : vi;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Read value as boolean */
|
|
66
|
+
get bool(): boolean | undefined {
|
|
67
|
+
const val = this.val;
|
|
68
|
+
return (val === undefined || val === '') ? undefined : IS_TRUE.test(val);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Determine if the underlying value is truthy */
|
|
72
|
+
get isTrue(): boolean {
|
|
73
|
+
return IS_TRUE.test(this.val ?? '');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Determine if the underlying value is falsy */
|
|
77
|
+
get isFalse(): boolean {
|
|
78
|
+
return IS_FALSE.test(this.val ?? '');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Determine if the underlying value is set */
|
|
82
|
+
get isSet(): boolean {
|
|
83
|
+
const val = this.val;
|
|
84
|
+
return val !== undefined && val !== '';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
type AllType = {
|
|
89
|
+
[K in keyof TravettoEnv]: Pick<EnvProp<TravettoEnv[K]>, 'key' | 'export' | 'val' | 'set' | 'clear' | 'isSet' |
|
|
90
|
+
(TravettoEnv[K] extends unknown[] ? 'list' | 'add' : never) |
|
|
91
|
+
(Extract<TravettoEnv[K], object> extends never ? never : 'object') |
|
|
92
|
+
(Extract<TravettoEnv[K], number> extends never ? never : 'int') |
|
|
93
|
+
(Extract<TravettoEnv[K], boolean> extends never ? never : 'bool' | 'isTrue' | 'isFalse')
|
|
94
|
+
>
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function delegate<T extends object>(base: T): AllType & T {
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
99
|
+
return new Proxy(base as AllType & T, {
|
|
100
|
+
get(target, prop): unknown {
|
|
101
|
+
return typeof prop !== 'string' ? undefined :
|
|
102
|
+
// @ts-expect-error
|
|
103
|
+
(prop in base ? base[prop] : target[prop] ??= new EnvProp(prop));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Basic utils for reading known environment variables */
|
|
109
|
+
export const Env = delegate({});
|
package/src/error.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export type ErrorCategory =
|
|
2
|
+
'general' |
|
|
3
|
+
'notfound' |
|
|
4
|
+
'data' |
|
|
5
|
+
'permissions' |
|
|
6
|
+
'authentication' |
|
|
7
|
+
'timeout' |
|
|
8
|
+
'unavailable';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Framework error class, with the aim of being extensible
|
|
12
|
+
*/
|
|
13
|
+
export class AppError<T = unknown> extends Error {
|
|
14
|
+
|
|
15
|
+
/** Convert from JSON object */
|
|
16
|
+
static fromJSON(e: unknown): AppError | undefined {
|
|
17
|
+
if (!!e && typeof e === 'object' &&
|
|
18
|
+
('message' in e && typeof e.message === 'string') &&
|
|
19
|
+
('category' in e && typeof e.category === 'string') &&
|
|
20
|
+
('type' in e && typeof e.type === 'string') &&
|
|
21
|
+
('at' in e && typeof e.at === 'number')
|
|
22
|
+
) {
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
24
|
+
const err = new AppError(e.message, e.category as ErrorCategory, 'details' in e ? e.details : undefined);
|
|
25
|
+
err.at = new Date(e.at);
|
|
26
|
+
err.type = e.type;
|
|
27
|
+
return err;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type: string;
|
|
32
|
+
at = new Date();
|
|
33
|
+
details: T;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build an app error
|
|
37
|
+
*
|
|
38
|
+
* @param message The error message
|
|
39
|
+
* @param category The error category, can be mapped to HTTP statuses
|
|
40
|
+
* @param details Optional error payload
|
|
41
|
+
*/
|
|
42
|
+
constructor(
|
|
43
|
+
message: string,
|
|
44
|
+
public category: ErrorCategory = 'general',
|
|
45
|
+
details?: T
|
|
46
|
+
|
|
47
|
+
) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.type = this.constructor.name;
|
|
50
|
+
this.details = details!;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The format of the JSON output
|
|
55
|
+
*/
|
|
56
|
+
toJSON(): { message: string, category: string, type: string, at: string, details?: Record<string, unknown> } {
|
|
57
|
+
return {
|
|
58
|
+
message: this.message,
|
|
59
|
+
category: this.category,
|
|
60
|
+
type: this.type,
|
|
61
|
+
at: this.at.toISOString(),
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
63
|
+
details: this.details as Record<string, unknown>,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/exec.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { ChildProcess } from 'node:child_process';
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
3
|
+
import { createInterface } from 'node:readline/promises';
|
|
4
|
+
|
|
5
|
+
const MINUTE = (1000 * 60);
|
|
6
|
+
|
|
7
|
+
const RESULT = Symbol.for('@travetto/runtime:exec-result');
|
|
8
|
+
|
|
9
|
+
interface ExecutionBaseResult {
|
|
10
|
+
/**
|
|
11
|
+
* Exit code
|
|
12
|
+
*/
|
|
13
|
+
code: number;
|
|
14
|
+
/**
|
|
15
|
+
* Execution result message, should be inline with code
|
|
16
|
+
*/
|
|
17
|
+
message?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Whether or not the execution completed successfully
|
|
20
|
+
*/
|
|
21
|
+
valid: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Result of an execution
|
|
26
|
+
*/
|
|
27
|
+
export interface ExecutionResult<T extends string | Buffer = string | Buffer> extends ExecutionBaseResult {
|
|
28
|
+
/**
|
|
29
|
+
* Stdout
|
|
30
|
+
*/
|
|
31
|
+
stdout: T;
|
|
32
|
+
/**
|
|
33
|
+
* Stderr
|
|
34
|
+
*/
|
|
35
|
+
stderr: T;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Standard utilities for managing executions
|
|
40
|
+
*/
|
|
41
|
+
export class ExecUtil {
|
|
42
|
+
|
|
43
|
+
static RESTART_EXIT_CODE = 200;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Run with automatic restart support
|
|
47
|
+
* @param run The factory to produce the next running process
|
|
48
|
+
* @param maxRetriesPerMinute The number of times to allow a retry within a minute
|
|
49
|
+
*/
|
|
50
|
+
static async withRestart(run: () => ChildProcess, maxRetriesPerMinute?: number): Promise<ExecutionResult> {
|
|
51
|
+
const maxRetries = maxRetriesPerMinute ?? 5;
|
|
52
|
+
const restarts: number[] = [];
|
|
53
|
+
|
|
54
|
+
for (; ;) {
|
|
55
|
+
const proc = run();
|
|
56
|
+
|
|
57
|
+
const toKill = (): void => { proc.kill('SIGKILL'); };
|
|
58
|
+
const toMessage = (v: unknown): void => { proc.send?.(v!); };
|
|
59
|
+
|
|
60
|
+
// Proxy kill requests
|
|
61
|
+
process.on('message', toMessage);
|
|
62
|
+
process.on('SIGINT', toKill);
|
|
63
|
+
proc.on('message', v => process.send?.(v));
|
|
64
|
+
|
|
65
|
+
const result = await this.getResult(proc, { catch: true });
|
|
66
|
+
if (result.code !== this.RESTART_EXIT_CODE) {
|
|
67
|
+
return result;
|
|
68
|
+
} else {
|
|
69
|
+
process.off('SIGINT', toKill);
|
|
70
|
+
process.off('message', toMessage);
|
|
71
|
+
restarts.unshift(Date.now());
|
|
72
|
+
if (restarts.length === maxRetries) {
|
|
73
|
+
if ((restarts[0] - restarts[maxRetries - 1]) <= MINUTE) {
|
|
74
|
+
console.error(`Bailing, due to ${maxRetries} restarts in under a minute`);
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
restarts.pop(); // Keep list short
|
|
78
|
+
}
|
|
79
|
+
console.error('Restarting...', { pid: process.pid });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Take a child process, and some additional options, and produce a promise that
|
|
86
|
+
* represents the entire execution. On successful completion the promise will resolve, and
|
|
87
|
+
* on failed completion the promise will reject.
|
|
88
|
+
*
|
|
89
|
+
* @param proc The process to enhance
|
|
90
|
+
* @param options The options to use to enhance the process
|
|
91
|
+
*/
|
|
92
|
+
static getResult(proc: ChildProcess): Promise<ExecutionResult<string>>;
|
|
93
|
+
static getResult(proc: ChildProcess, options: { catch?: boolean, binary?: false }): Promise<ExecutionResult<string>>;
|
|
94
|
+
static getResult(proc: ChildProcess, options: { catch?: boolean, binary: true }): Promise<ExecutionResult<Buffer>>;
|
|
95
|
+
static getResult<T extends string | Buffer>(proc: ChildProcess, options: { catch?: boolean, binary?: boolean } = {}): Promise<ExecutionResult<T>> {
|
|
96
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
97
|
+
const res = (proc as unknown as { [RESULT]: Promise<ExecutionResult> })[RESULT] ??= new Promise<ExecutionResult>(resolve => {
|
|
98
|
+
const stdout: Buffer[] = [];
|
|
99
|
+
const stderr: Buffer[] = [];
|
|
100
|
+
let done = false;
|
|
101
|
+
const finish = (result: ExecutionBaseResult): void => {
|
|
102
|
+
if (done) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
done = true;
|
|
106
|
+
|
|
107
|
+
const buffers = {
|
|
108
|
+
stdout: Buffer.concat(stdout),
|
|
109
|
+
stderr: Buffer.concat(stderr),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const final = {
|
|
113
|
+
stdout: options.binary ? buffers.stdout : buffers.stdout.toString('utf8'),
|
|
114
|
+
stderr: options.binary ? buffers.stderr : buffers.stderr.toString('utf8'),
|
|
115
|
+
...result
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
resolve(!final.valid ?
|
|
119
|
+
{ ...final, message: `${final.message || final.stderr || final.stdout || 'failed'}` } :
|
|
120
|
+
final
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
proc.stdout?.on('data', (d: string | Buffer) => stdout.push(Buffer.from(d)));
|
|
125
|
+
proc.stderr?.on('data', (d: string | Buffer) => stderr.push(Buffer.from(d)));
|
|
126
|
+
|
|
127
|
+
proc.on('error', (err: Error) =>
|
|
128
|
+
finish({ code: 1, message: err.message, valid: false }));
|
|
129
|
+
|
|
130
|
+
proc.on('close', (code: number) =>
|
|
131
|
+
finish({ code, valid: code === null || code === 0 }));
|
|
132
|
+
|
|
133
|
+
if (proc.exitCode !== null) { // We are already done
|
|
134
|
+
finish({ code: proc.exitCode, valid: proc.exitCode === 0 });
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
139
|
+
return (options.catch ? res : res.then(v => {
|
|
140
|
+
if (v.valid) {
|
|
141
|
+
return v;
|
|
142
|
+
} else {
|
|
143
|
+
throw new Error(v.message);
|
|
144
|
+
}
|
|
145
|
+
})) as Promise<ExecutionResult<T>>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Consume lines
|
|
150
|
+
*/
|
|
151
|
+
static async readLines(stream: Readable, handler: (input: string) => unknown | Promise<unknown>): Promise<void> {
|
|
152
|
+
for await (const item of createInterface(stream)) {
|
|
153
|
+
await handler(item);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createReadStream } from 'node:fs';
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { AppError } from './error';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* File loader that will search for files across the provided search paths
|
|
10
|
+
*/
|
|
11
|
+
export class FileLoader {
|
|
12
|
+
|
|
13
|
+
#searchPaths: readonly string[];
|
|
14
|
+
|
|
15
|
+
constructor(paths: string[]) {
|
|
16
|
+
this.#searchPaths = paths;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The paths that will be searched on resolve
|
|
21
|
+
*/
|
|
22
|
+
get searchPaths(): readonly string[] {
|
|
23
|
+
return this.#searchPaths;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Return the absolute path for the given relative path
|
|
28
|
+
* @param relativePath The path to resolve
|
|
29
|
+
*/
|
|
30
|
+
async resolve(relativePath: string): Promise<string> {
|
|
31
|
+
for (const sub of this.searchPaths) {
|
|
32
|
+
const resolved = path.join(sub, relativePath);
|
|
33
|
+
if (await fs.stat(resolved).catch(() => false)) {
|
|
34
|
+
return resolved;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
throw new AppError(`Unable to find: ${relativePath}, searched=${this.searchPaths.join(',')}`, 'notfound');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Read a file, after resolving the path
|
|
42
|
+
* @param relativePath The path to read
|
|
43
|
+
*/
|
|
44
|
+
async read(relativePath: string, binary?: false): Promise<string>;
|
|
45
|
+
async read(relativePath: string, binary: true): Promise<Buffer>;
|
|
46
|
+
async read(relativePath: string, binary = false): Promise<string | Buffer> {
|
|
47
|
+
const file = await this.resolve(relativePath);
|
|
48
|
+
return fs.readFile(file, binary ? undefined : 'utf8');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Read a file as a stream
|
|
53
|
+
* @param relativePath The path to read
|
|
54
|
+
*/
|
|
55
|
+
async readStream(relativePath: string, binary = true): Promise<Readable> {
|
|
56
|
+
const file = await this.resolve(relativePath);
|
|
57
|
+
return createReadStream(file, { encoding: binary ? undefined : 'utf8' });
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/function.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type FunctionMetadataTag = { hash: number, lines: [number, number] };
|
|
2
|
+
export type FunctionMetadata = FunctionMetadataTag & {
|
|
3
|
+
id: string;
|
|
4
|
+
import: string;
|
|
5
|
+
methods?: Record<string, FunctionMetadataTag>;
|
|
6
|
+
synthetic?: boolean;
|
|
7
|
+
abstract?: boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const METADATA = Symbol.for('@travetto/runtime:function-metadata');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Initialize the meta data for a function/class
|
|
14
|
+
* @param fn Class
|
|
15
|
+
* @param `file` Filename
|
|
16
|
+
* @param `hash` Hash of class contents
|
|
17
|
+
* @param `line` Line number in source
|
|
18
|
+
* @param `methods` Methods and their hashes
|
|
19
|
+
* @param `abstract` Is the class abstract
|
|
20
|
+
* @param `synthetic` Is this code generated at build time
|
|
21
|
+
* @private
|
|
22
|
+
*/
|
|
23
|
+
export function register(
|
|
24
|
+
fn: Function, module: [string, string], tag: FunctionMetadataTag,
|
|
25
|
+
methods?: Record<string, FunctionMetadataTag>, abstract?: boolean, synthetic?: boolean
|
|
26
|
+
): void {
|
|
27
|
+
let id = module.join(':');
|
|
28
|
+
if (fn.name) {
|
|
29
|
+
id = `${id}○${fn.name}`;
|
|
30
|
+
}
|
|
31
|
+
const value = { id, import: module.join('/'), ...tag, methods, abstract, synthetic };
|
|
32
|
+
Object.defineProperties(fn, { Ⲑid: { value: id }, [METADATA]: { value } });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read metadata
|
|
37
|
+
*/
|
|
38
|
+
export function describeFunction(fn: Function): FunctionMetadata;
|
|
39
|
+
export function describeFunction(fn?: Function): FunctionMetadata | undefined {
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
41
|
+
return (fn as unknown as { [METADATA]: FunctionMetadata })?.[METADATA];
|
|
42
|
+
}
|
package/src/global.d.ts
ADDED
package/src/resources.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Runtime } from './context';
|
|
2
|
+
import { Env } from './env';
|
|
3
|
+
import { FileLoader } from './file-loader';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Environment aware file loader
|
|
7
|
+
*/
|
|
8
|
+
class $RuntimeResources extends FileLoader {
|
|
9
|
+
#computed: string[];
|
|
10
|
+
#env: string;
|
|
11
|
+
|
|
12
|
+
constructor() {
|
|
13
|
+
super(Runtime.resourcePaths());
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
override get searchPaths(): readonly string[] {
|
|
17
|
+
if (this.#env !== Env.TRV_RESOURCES.val) {
|
|
18
|
+
this.#env = Env.TRV_RESOURCES.val!;
|
|
19
|
+
this.#computed = Runtime.resourcePaths();
|
|
20
|
+
}
|
|
21
|
+
return this.#computed ?? super.searchPaths;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Runtime resources */
|
|
26
|
+
export const RuntimeResources = new $RuntimeResources();
|
package/src/shutdown.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Env } from './env';
|
|
2
|
+
import { Util } from './util';
|
|
3
|
+
import { TimeUtil } from './time';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shutdown manager, allowing for listening for graceful shutdowns
|
|
7
|
+
*/
|
|
8
|
+
export class ShutdownManager {
|
|
9
|
+
|
|
10
|
+
static #registered = false;
|
|
11
|
+
static #handlers: { name?: string, handler: () => Promise<void> }[] = [];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* On Shutdown requested
|
|
15
|
+
* @param name name to log for
|
|
16
|
+
* @param handler synchronous or asynchronous handler
|
|
17
|
+
*/
|
|
18
|
+
static onGracefulShutdown(handler: () => Promise<void>, name?: string | { constructor: { Ⲑid: string } }): () => 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 });
|
|
25
|
+
return () => {
|
|
26
|
+
const idx = this.#handlers.findIndex(x => x.handler === handler);
|
|
27
|
+
if (idx >= 0) {
|
|
28
|
+
this.#handlers.splice(idx, 1);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Wait for graceful shutdown to run and complete
|
|
35
|
+
*/
|
|
36
|
+
static async gracefulShutdown(code: number | string | undefined = process.exitCode): Promise<void> {
|
|
37
|
+
if (code !== undefined) {
|
|
38
|
+
process.exitCode = code;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (this.#handlers.length) {
|
|
42
|
+
console.debug('Graceful shutdown: started');
|
|
43
|
+
|
|
44
|
+
const items = this.#handlers.splice(0, this.#handlers.length);
|
|
45
|
+
const handlers = Promise.all(items.map(({ name, handler }) => {
|
|
46
|
+
if (name) {
|
|
47
|
+
console.debug('Stopping', { name });
|
|
48
|
+
}
|
|
49
|
+
return handler().catch(err => {
|
|
50
|
+
console.error('Error shutting down', { name, err });
|
|
51
|
+
});
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
await Promise.race([
|
|
55
|
+
Util.nonBlockingTimeout(TimeUtil.fromValue(Env.TRV_SHUTDOWN_WAIT.val) ?? 2000), // Wait 2s and then force finish
|
|
56
|
+
handlers,
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
console.debug('Graceful shutdown: completed');
|
|
60
|
+
}
|
|
61
|
+
if (code !== undefined) {
|
|
62
|
+
process.exit();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/time.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const MIN = 1000 * 60;
|
|
2
|
+
const DAY = 24 * MIN * 60;
|
|
3
|
+
const TIME_UNITS = {
|
|
4
|
+
y: DAY * 365,
|
|
5
|
+
M: DAY * 30,
|
|
6
|
+
w: DAY * 7,
|
|
7
|
+
d: DAY,
|
|
8
|
+
h: MIN * 60,
|
|
9
|
+
m: MIN,
|
|
10
|
+
s: 1000,
|
|
11
|
+
ms: 1
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type TimeSpan = `${number}${keyof typeof TIME_UNITS}`;
|
|
15
|
+
export type TimeUnit = keyof typeof TIME_UNITS;
|
|
16
|
+
|
|
17
|
+
export class TimeUtil {
|
|
18
|
+
|
|
19
|
+
static #timePattern = new RegExp(`^(?<amount>-?[0-9.]+)(?<unit>${Object.keys(TIME_UNITS).join('|')})$`);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Test to see if a string is valid for relative time
|
|
23
|
+
* @param val
|
|
24
|
+
*/
|
|
25
|
+
static isTimeSpan(val: string): val is TimeSpan {
|
|
26
|
+
return this.#timePattern.test(val);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Returns time units convert to ms
|
|
31
|
+
* @param amount Number of units to extend
|
|
32
|
+
* @param unit Time unit to extend ('ms', 's', 'm', 'h', 'd', 'w', 'y')
|
|
33
|
+
*/
|
|
34
|
+
static asMillis(amount: Date | number | TimeSpan, unit?: TimeUnit): number {
|
|
35
|
+
if (amount instanceof Date) {
|
|
36
|
+
return amount.getTime();
|
|
37
|
+
} else if (typeof amount === 'string') {
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
39
|
+
const { groups } = (amount.match(this.#timePattern) as { groups: { amount?: string, unit?: TimeUnit } });
|
|
40
|
+
const amountStr = groups?.amount ?? `${amount}`;
|
|
41
|
+
unit = groups?.unit ?? unit ?? 'ms';
|
|
42
|
+
if (!TIME_UNITS[unit]) {
|
|
43
|
+
return NaN;
|
|
44
|
+
}
|
|
45
|
+
amount = amountStr.includes('.') ? parseFloat(amountStr) : parseInt(amountStr, 10);
|
|
46
|
+
}
|
|
47
|
+
return amount * TIME_UNITS[unit ?? 'ms'];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Returns the time converted to seconds
|
|
52
|
+
* @param date The date to convert
|
|
53
|
+
*/
|
|
54
|
+
static asSeconds(date: Date | number | TimeSpan, unit?: TimeUnit): number {
|
|
55
|
+
return Math.trunc(this.asMillis(date, unit) / 1000);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Returns the time converted to a Date
|
|
60
|
+
* @param date The date to convert
|
|
61
|
+
*/
|
|
62
|
+
static asDate(date: Date | number | TimeSpan, unit?: TimeUnit): Date {
|
|
63
|
+
return new Date(this.asMillis(date, unit));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve time or span to possible time
|
|
68
|
+
*/
|
|
69
|
+
static fromValue(value: Date | number | string | undefined): number | undefined {
|
|
70
|
+
if (value === undefined) {
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
const val = (typeof value === 'string' && /\d+[a-z]$/i.test(value)) ?
|
|
74
|
+
(this.isTimeSpan(value) ? this.asMillis(value) : undefined) :
|
|
75
|
+
(typeof value === 'string' ? parseInt(value, 10) :
|
|
76
|
+
(value instanceof Date ? value.getTime() : value));
|
|
77
|
+
return Number.isNaN(val) ? undefined : val;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Returns a new date with `amount` units into the future
|
|
82
|
+
* @param amount Number of units to extend
|
|
83
|
+
* @param unit Time unit to extend ('ms', 's', 'm', 'h', 'd', 'w', 'y')
|
|
84
|
+
*/
|
|
85
|
+
static fromNow(amount: number | TimeSpan, unit: TimeUnit = 'ms'): Date {
|
|
86
|
+
return new Date(Date.now() + this.asMillis(amount, unit));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Returns a pretty timestamp
|
|
91
|
+
* @param time Time in milliseconds
|
|
92
|
+
*/
|
|
93
|
+
static asClock(time: number): string {
|
|
94
|
+
const s = Math.trunc(time / 1000);
|
|
95
|
+
return [
|
|
96
|
+
s > 3600 ? `${Math.trunc(s / 3600).toString().padStart(2, '0')}h` : '',
|
|
97
|
+
s > 60 ? `${Math.trunc((s % 3600) / 60).toString().padStart(2, '0')}m` : '',
|
|
98
|
+
`${(s % 60).toString().padStart(2, '0')}s`
|
|
99
|
+
].filter(x => !!x).slice(0, 2).join(' ');
|
|
100
|
+
}
|
|
101
|
+
}
|
package/src/trv.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ManifestModuleRole } from '@travetto/manifest';
|
|
2
|
+
|
|
3
|
+
import type { TimeSpan } from './time';
|
|
4
|
+
|
|
5
|
+
type Role = Exclude<ManifestModuleRole, 'std' | 'compile'>;
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
declare global {
|
|
9
|
+
interface TravettoEnv {
|
|
10
|
+
/**
|
|
11
|
+
* The node environment we are running in
|
|
12
|
+
* @default development
|
|
13
|
+
*/
|
|
14
|
+
NODE_ENV: 'development' | 'production';
|
|
15
|
+
/**
|
|
16
|
+
* Outputs all console.debug messages, defaults to `local` in dev, and `off` in prod.
|
|
17
|
+
*/
|
|
18
|
+
DEBUG: boolean | string;
|
|
19
|
+
/**
|
|
20
|
+
* Environment to deploy, defaults to `NODE_ENV` if not `TRV_ENV` is not specified.
|
|
21
|
+
*/
|
|
22
|
+
TRV_ENV: string;
|
|
23
|
+
/**
|
|
24
|
+
* Special role to run as, used to access additional files from the manifest during runtime.
|
|
25
|
+
*/
|
|
26
|
+
TRV_ROLE: Role;
|
|
27
|
+
/**
|
|
28
|
+
* Whether or not to run the program in dynamic mode, allowing for real-time updates
|
|
29
|
+
*/
|
|
30
|
+
TRV_DYNAMIC: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* The folders to use for resource lookup
|
|
33
|
+
*/
|
|
34
|
+
TRV_RESOURCES: string[];
|
|
35
|
+
/**
|
|
36
|
+
* Resource path overrides
|
|
37
|
+
* @private
|
|
38
|
+
*/
|
|
39
|
+
TRV_RESOURCE_OVERRIDES: Record<string, string>;
|
|
40
|
+
/**
|
|
41
|
+
* The max time to wait for shutdown to finish after initial SIGINT,
|
|
42
|
+
* @default 2s
|
|
43
|
+
*/
|
|
44
|
+
TRV_SHUTDOWN_WAIT: TimeSpan | number;
|
|
45
|
+
/**
|
|
46
|
+
* The desired runtime module
|
|
47
|
+
*/
|
|
48
|
+
TRV_MODULE: string;
|
|
49
|
+
/**
|
|
50
|
+
* The location of the manifest file
|
|
51
|
+
* @default undefined
|
|
52
|
+
*/
|
|
53
|
+
TRV_MANIFEST: string;
|
|
54
|
+
/**
|
|
55
|
+
* trvc log level
|
|
56
|
+
*/
|
|
57
|
+
TRV_BUILD: 'none' | 'info' | 'debug' | 'error' | 'warn'
|
|
58
|
+
}
|
|
59
|
+
}
|