@travetto/runtime 7.0.0-rc.1 → 7.0.0-rc.3
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 +24 -20
- package/__index__.ts +1 -0
- package/package.json +3 -3
- package/src/binary.ts +14 -14
- package/src/console.ts +15 -15
- package/src/context.ts +18 -22
- package/src/env.ts +30 -30
- package/src/error.ts +15 -15
- package/src/exec.ts +38 -87
- package/src/file-loader.ts +9 -0
- package/src/json.ts +74 -0
- package/src/queue.ts +2 -2
- package/src/resources.ts +3 -3
- package/src/shutdown.ts +4 -4
- package/src/time.ts +11 -11
- package/src/trv.d.ts +0 -4
- package/src/types.ts +13 -12
- package/src/util.ts +25 -101
- package/src/watch.ts +63 -15
- package/support/transformer/metadata.ts +5 -5
- package/support/transformer.concrete-type.ts +4 -4
- package/support/transformer.console-log.ts +5 -5
- package/support/transformer.debug-method.ts +1 -1
- package/support/transformer.function-metadata.ts +2 -2
- package/support/transformer.rewrite-path-import.ts +11 -9
package/src/exec.ts
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
|
-
import { ChildProcess } from 'node:child_process';
|
|
2
|
-
import { Readable } from 'node:stream';
|
|
1
|
+
import { type ChildProcess, spawn, type SpawnOptions } from 'node:child_process';
|
|
2
|
+
import type { Readable } from 'node:stream';
|
|
3
3
|
import { createInterface } from 'node:readline/promises';
|
|
4
4
|
|
|
5
|
-
import { castTo } from './types.ts';
|
|
6
|
-
|
|
7
|
-
const MINUTE = (1000 * 60);
|
|
5
|
+
import { castTo, type Any } from './types.ts';
|
|
8
6
|
|
|
9
7
|
const ResultSymbol = Symbol();
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Result of an execution
|
|
11
|
+
*/
|
|
12
|
+
export interface ExecutionResult<T extends string | Buffer = string | Buffer> {
|
|
13
|
+
/**
|
|
14
|
+
* Stdout
|
|
15
|
+
*/
|
|
16
|
+
stdout: T;
|
|
17
|
+
/**
|
|
18
|
+
* Stderr
|
|
19
|
+
*/
|
|
20
|
+
stderr: T;
|
|
12
21
|
/**
|
|
13
22
|
* Exit code
|
|
14
23
|
*/
|
|
@@ -23,77 +32,19 @@ interface ExecutionBaseResult {
|
|
|
23
32
|
valid: boolean;
|
|
24
33
|
}
|
|
25
34
|
|
|
26
|
-
|
|
27
|
-
* Result of an execution
|
|
28
|
-
*/
|
|
29
|
-
export interface ExecutionResult<T extends string | Buffer = string | Buffer> extends ExecutionBaseResult {
|
|
30
|
-
/**
|
|
31
|
-
* Stdout
|
|
32
|
-
*/
|
|
33
|
-
stdout: T;
|
|
34
|
-
/**
|
|
35
|
-
* Stderr
|
|
36
|
-
*/
|
|
37
|
-
stderr: T;
|
|
38
|
-
}
|
|
35
|
+
type ExecutionBaseResult = Omit<ExecutionResult, 'stdout' | 'stderr'>;
|
|
39
36
|
|
|
40
37
|
/**
|
|
41
38
|
* Standard utilities for managing executions
|
|
42
39
|
*/
|
|
43
40
|
export class ExecUtil {
|
|
44
41
|
|
|
45
|
-
static RESTART_EXIT_CODE = 200;
|
|
46
|
-
|
|
47
42
|
/**
|
|
48
|
-
*
|
|
49
|
-
* @param run The factory to produce the next running process
|
|
50
|
-
* @param maxRetriesPerMinute The number of times to allow a retry within a minute
|
|
43
|
+
* Spawn wrapper that ensures performant invocation of trv commands
|
|
51
44
|
*/
|
|
52
|
-
static
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
): Promise<ExecutionResult> {
|
|
56
|
-
const maxRetries = config?.maxRetriesPerMinute ?? 5;
|
|
57
|
-
const relayInterrupt = config?.relayInterrupt ?? false;
|
|
58
|
-
|
|
59
|
-
const restarts: number[] = [];
|
|
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
|
-
|
|
66
|
-
for (; ;) {
|
|
67
|
-
const proc = run();
|
|
68
|
-
const interrupt = (): void => { proc.kill('SIGINT'); };
|
|
69
|
-
const toMessage = (v: unknown): void => { proc.send?.(v!); };
|
|
70
|
-
|
|
71
|
-
// Proxy kill requests
|
|
72
|
-
process.on('message', toMessage);
|
|
73
|
-
if (relayInterrupt) {
|
|
74
|
-
process.on('SIGINT', interrupt);
|
|
75
|
-
}
|
|
76
|
-
proc.on('message', v => process.send?.(v));
|
|
77
|
-
|
|
78
|
-
const result = await this.getResult(proc, { catch: true });
|
|
79
|
-
if (result.code !== this.RESTART_EXIT_CODE) {
|
|
80
|
-
return result;
|
|
81
|
-
} else {
|
|
82
|
-
if (relayInterrupt) {
|
|
83
|
-
process.off('SIGINT', interrupt);
|
|
84
|
-
}
|
|
85
|
-
process.off('message', toMessage);
|
|
86
|
-
restarts.unshift(Date.now());
|
|
87
|
-
if (restarts.length === maxRetries) {
|
|
88
|
-
if ((restarts[0] - restarts[maxRetries - 1]) <= MINUTE) {
|
|
89
|
-
console.error(`Bailing, due to ${maxRetries} restarts in under a minute`);
|
|
90
|
-
return result;
|
|
91
|
-
}
|
|
92
|
-
restarts.pop(); // Keep list short
|
|
93
|
-
}
|
|
94
|
-
console.error('Restarting...', { pid: process.pid });
|
|
95
|
-
}
|
|
96
|
-
}
|
|
45
|
+
static spawnTrv(cmd: string, args: string[], options: SpawnOptions): ChildProcess {
|
|
46
|
+
const entry = (globalThis as Any).__entry_point__ ?? process.argv.at(1);
|
|
47
|
+
return spawn(process.argv0, [entry, cmd, ...args], options);
|
|
97
48
|
}
|
|
98
49
|
|
|
99
50
|
/**
|
|
@@ -101,15 +52,15 @@ export class ExecUtil {
|
|
|
101
52
|
* represents the entire execution. On successful completion the promise will resolve, and
|
|
102
53
|
* on failed completion the promise will reject.
|
|
103
54
|
*
|
|
104
|
-
* @param
|
|
55
|
+
* @param subProcess The process to enhance
|
|
105
56
|
* @param options The options to use to enhance the process
|
|
106
57
|
*/
|
|
107
|
-
static getResult(
|
|
108
|
-
static getResult(
|
|
109
|
-
static getResult(
|
|
110
|
-
static getResult<T extends string | Buffer>(
|
|
111
|
-
const
|
|
112
|
-
const result =
|
|
58
|
+
static getResult(subProcess: ChildProcess): Promise<ExecutionResult<string>>;
|
|
59
|
+
static getResult(subProcess: ChildProcess, options: { catch?: boolean, binary?: false }): Promise<ExecutionResult<string>>;
|
|
60
|
+
static getResult(subProcess: ChildProcess, options: { catch?: boolean, binary: true }): Promise<ExecutionResult<Buffer>>;
|
|
61
|
+
static getResult<T extends string | Buffer>(subProcess: ChildProcess, options: { catch?: boolean, binary?: boolean } = {}): Promise<ExecutionResult<T>> {
|
|
62
|
+
const typed: ChildProcess & { [ResultSymbol]?: Promise<ExecutionResult> } = subProcess;
|
|
63
|
+
const result = typed[ResultSymbol] ??= new Promise<ExecutionResult>(resolve => {
|
|
113
64
|
const stdout: Buffer[] = [];
|
|
114
65
|
const stderr: Buffer[] = [];
|
|
115
66
|
let done = false;
|
|
@@ -136,25 +87,25 @@ export class ExecUtil {
|
|
|
136
87
|
);
|
|
137
88
|
};
|
|
138
89
|
|
|
139
|
-
|
|
140
|
-
|
|
90
|
+
subProcess.stdout?.on('data', (data: string | Buffer) => stdout.push(Buffer.isBuffer(data) ? data : Buffer.from(data)));
|
|
91
|
+
subProcess.stderr?.on('data', (data: string | Buffer) => stderr.push(Buffer.isBuffer(data) ? data : Buffer.from(data)));
|
|
141
92
|
|
|
142
|
-
|
|
143
|
-
finish({ code: 1, message:
|
|
93
|
+
subProcess.on('error', (error: Error) =>
|
|
94
|
+
finish({ code: 1, message: error.message, valid: false }));
|
|
144
95
|
|
|
145
|
-
|
|
96
|
+
subProcess.on('close', (code: number) =>
|
|
146
97
|
finish({ code, valid: code === null || code === 0 }));
|
|
147
98
|
|
|
148
|
-
if (
|
|
149
|
-
finish({ code:
|
|
99
|
+
if (subProcess.exitCode !== null) { // We are already done
|
|
100
|
+
finish({ code: subProcess.exitCode, valid: subProcess.exitCode === 0 });
|
|
150
101
|
}
|
|
151
102
|
});
|
|
152
103
|
|
|
153
|
-
return castTo(options.catch ? result : result.then(
|
|
154
|
-
if (
|
|
155
|
-
return
|
|
104
|
+
return castTo(options.catch ? result : result.then(executionResult => {
|
|
105
|
+
if (executionResult.valid) {
|
|
106
|
+
return executionResult;
|
|
156
107
|
} else {
|
|
157
|
-
throw new Error(
|
|
108
|
+
throw new Error(executionResult.message);
|
|
158
109
|
}
|
|
159
110
|
}));
|
|
160
111
|
}
|
package/src/file-loader.ts
CHANGED
|
@@ -4,6 +4,7 @@ import fs from 'node:fs/promises';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
|
|
6
6
|
import { AppError } from './error.ts';
|
|
7
|
+
import { JSONUtil } from './json.ts';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* File loader that will search for files across the provided search paths
|
|
@@ -64,4 +65,12 @@ export class FileLoader {
|
|
|
64
65
|
async readFile(relativePath: string): Promise<File> {
|
|
65
66
|
return new File([await this.read(relativePath, true)], path.basename(relativePath));
|
|
66
67
|
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Read relative file as JSON
|
|
71
|
+
*/
|
|
72
|
+
async readJSON<T>(relativePath: string): Promise<T> {
|
|
73
|
+
const location = await this.resolve(relativePath);
|
|
74
|
+
return JSONUtil.readFile<T>(location);
|
|
75
|
+
}
|
|
67
76
|
}
|
package/src/json.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
import type { Any } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* JSON Utility functions
|
|
8
|
+
*/
|
|
9
|
+
export class JSONUtil {
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse JSON safely
|
|
13
|
+
*/
|
|
14
|
+
static parseSafe<T>(input: string | Buffer, reviver?: (this: unknown, key: string, value: Any) => unknown): T {
|
|
15
|
+
if (typeof input !== 'string') {
|
|
16
|
+
input = input.toString('utf8');
|
|
17
|
+
}
|
|
18
|
+
// TODO: Ensure we aren't vulnerable to prototype pollution
|
|
19
|
+
return JSON.parse(input, reviver);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Encode JSON value as base64 encoded string
|
|
24
|
+
*/
|
|
25
|
+
static stringifyBase64<T>(value: T | undefined): string | undefined {
|
|
26
|
+
if (value === undefined) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const text = JSON.stringify(value);
|
|
30
|
+
return Buffer.from(text, 'utf8').toString('base64');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Decode JSON value from base64 encoded string
|
|
35
|
+
*/
|
|
36
|
+
static parseBase64<T>(input: string): T;
|
|
37
|
+
static parseBase64<T>(input?: string | undefined): T | undefined;
|
|
38
|
+
static parseBase64<T>(input?: string | undefined): T | undefined {
|
|
39
|
+
if (!input) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let decoded = Buffer.from(input, 'base64').toString('utf8');
|
|
44
|
+
|
|
45
|
+
// Read from encoded if it happens
|
|
46
|
+
if (decoded.startsWith('%')) {
|
|
47
|
+
decoded = decodeURIComponent(decoded);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return this.parseSafe(decoded);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Read JSON file asynchronously
|
|
55
|
+
* @param file
|
|
56
|
+
* @returns
|
|
57
|
+
*/
|
|
58
|
+
static async readFile<T>(file: string): Promise<T> {
|
|
59
|
+
const content = await fs.readFile(file, 'utf8');
|
|
60
|
+
return this.parseSafe(content);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Read JSON file synchronously
|
|
65
|
+
* @param file
|
|
66
|
+
*/
|
|
67
|
+
static readFileSync<T>(file: string, onMissing?: T): T {
|
|
68
|
+
if (!existsSync(file) && onMissing !== undefined) {
|
|
69
|
+
return onMissing;
|
|
70
|
+
}
|
|
71
|
+
const content = readFileSync(file, 'utf8');
|
|
72
|
+
return this.parseSafe(content);
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/queue.ts
CHANGED
|
@@ -54,9 +54,9 @@ export class AsyncQueue<X> implements AsyncIterator<X>, AsyncIterable<X> {
|
|
|
54
54
|
/**
|
|
55
55
|
* Throw an error from the queue, rejecting and terminating immediately
|
|
56
56
|
*/
|
|
57
|
-
async throw(
|
|
57
|
+
async throw(error?: Error): Promise<IteratorResult<X>> {
|
|
58
58
|
this.#done = true;
|
|
59
|
-
this.#ready.reject(
|
|
59
|
+
this.#ready.reject(error);
|
|
60
60
|
return { value: undefined, done: this.#done };
|
|
61
61
|
}
|
|
62
62
|
}
|
package/src/resources.ts
CHANGED
|
@@ -15,9 +15,9 @@ class $RuntimeResources extends FileLoader {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
override get searchPaths(): readonly string[] {
|
|
18
|
-
if (!this.#computed || this.#env !== Env.TRV_RESOURCES.
|
|
19
|
-
this.#env = Env.TRV_RESOURCES.
|
|
20
|
-
this.#mod = Env.TRV_MODULE.
|
|
18
|
+
if (!this.#computed || this.#env !== Env.TRV_RESOURCES.value || this.#mod !== Env.TRV_MODULE.value) {
|
|
19
|
+
this.#env = Env.TRV_RESOURCES.value!;
|
|
20
|
+
this.#mod = Env.TRV_MODULE.value!;
|
|
21
21
|
this.#computed = Runtime.resourcePaths();
|
|
22
22
|
}
|
|
23
23
|
return this.#computed;
|
package/src/shutdown.ts
CHANGED
|
@@ -40,7 +40,7 @@ export class ShutdownManager {
|
|
|
40
40
|
this.#ensureExitListeners();
|
|
41
41
|
this.#handlers.push({ handler, scope });
|
|
42
42
|
return () => {
|
|
43
|
-
const idx = this.#handlers.findIndex(
|
|
43
|
+
const idx = this.#handlers.findIndex(item => item.handler === handler);
|
|
44
44
|
if (idx >= 0) {
|
|
45
45
|
this.#handlers.splice(idx, 1);
|
|
46
46
|
}
|
|
@@ -61,7 +61,7 @@ export class ShutdownManager {
|
|
|
61
61
|
|
|
62
62
|
await Util.queueMacroTask(); // Force the event loop to wait one cycle
|
|
63
63
|
|
|
64
|
-
const timeout = TimeUtil.fromValue(Env.TRV_SHUTDOWN_WAIT.
|
|
64
|
+
const timeout = TimeUtil.fromValue(Env.TRV_SHUTDOWN_WAIT.value) ?? 2000;
|
|
65
65
|
const items = this.#handlers.splice(0, this.#handlers.length);
|
|
66
66
|
console.debug('Graceful shutdown: started', { source, timeout, count: items.length });
|
|
67
67
|
const handlers = Promise.all(items.map(async ({ scope, handler }) => {
|
|
@@ -73,8 +73,8 @@ export class ShutdownManager {
|
|
|
73
73
|
if (scope) {
|
|
74
74
|
console.debug('Stopped', { scope });
|
|
75
75
|
}
|
|
76
|
-
} catch (
|
|
77
|
-
console.error('Error stopping', {
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('Error stopping', { error, scope });
|
|
78
78
|
}
|
|
79
79
|
}));
|
|
80
80
|
|
package/src/time.ts
CHANGED
|
@@ -22,8 +22,8 @@ export class TimeUtil {
|
|
|
22
22
|
* Test to see if a string is valid for relative time
|
|
23
23
|
* @param val
|
|
24
24
|
*/
|
|
25
|
-
static isTimeSpan(
|
|
26
|
-
return TIME_PATTERN.test(
|
|
25
|
+
static isTimeSpan(value: string): value is TimeSpan {
|
|
26
|
+
return TIME_PATTERN.test(value);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
@@ -36,12 +36,12 @@ export class TimeUtil {
|
|
|
36
36
|
return amount.getTime();
|
|
37
37
|
} else if (typeof amount === 'string') {
|
|
38
38
|
const groups: { amount?: string, unit?: TimeUnit } = amount.match(TIME_PATTERN)?.groups ?? {};
|
|
39
|
-
const
|
|
39
|
+
const amountString = groups.amount ?? `${amount}`;
|
|
40
40
|
unit = groups.unit ?? unit ?? 'ms';
|
|
41
41
|
if (!TIME_UNITS[unit]) {
|
|
42
42
|
return NaN;
|
|
43
43
|
}
|
|
44
|
-
amount =
|
|
44
|
+
amount = amountString.includes('.') ? parseFloat(amountString) : parseInt(amountString, 10);
|
|
45
45
|
}
|
|
46
46
|
return amount * TIME_UNITS[unit ?? 'ms'];
|
|
47
47
|
}
|
|
@@ -69,11 +69,11 @@ export class TimeUtil {
|
|
|
69
69
|
if (value === undefined) {
|
|
70
70
|
return value;
|
|
71
71
|
}
|
|
72
|
-
const
|
|
72
|
+
const result = (typeof value === 'string' && /\d{1,30}[a-z]$/i.test(value)) ?
|
|
73
73
|
(this.isTimeSpan(value) ? this.asMillis(value) : undefined) :
|
|
74
74
|
(typeof value === 'string' ? parseInt(value, 10) :
|
|
75
75
|
(value instanceof Date ? value.getTime() : value));
|
|
76
|
-
return Number.isNaN(
|
|
76
|
+
return Number.isNaN(result) ? undefined : result;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
@@ -90,11 +90,11 @@ export class TimeUtil {
|
|
|
90
90
|
* @param time Time in milliseconds
|
|
91
91
|
*/
|
|
92
92
|
static asClock(time: number): string {
|
|
93
|
-
const
|
|
93
|
+
const seconds = Math.trunc(time / 1000);
|
|
94
94
|
return [
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
`${(
|
|
98
|
-
].filter(
|
|
95
|
+
seconds > 3600 ? `${Math.trunc(seconds / 3600).toString().padStart(2, '0')}h` : '',
|
|
96
|
+
seconds > 60 ? `${Math.trunc((seconds % 3600) / 60).toString().padStart(2, '0')}m` : '',
|
|
97
|
+
`${(seconds % 60).toString().padStart(2, '0')}s`
|
|
98
|
+
].filter(part => !!part).slice(0, 2).join(' ');
|
|
99
99
|
}
|
|
100
100
|
}
|
package/src/trv.d.ts
CHANGED
|
@@ -21,10 +21,6 @@ declare module "@travetto/runtime" {
|
|
|
21
21
|
* Special role to run as, used to access additional files from the manifest during runtime.
|
|
22
22
|
*/
|
|
23
23
|
TRV_ROLE: Role;
|
|
24
|
-
/**
|
|
25
|
-
* Whether or not to run the program in dynamic mode, allowing for real-time updates
|
|
26
|
-
*/
|
|
27
|
-
TRV_DYNAMIC: boolean;
|
|
28
24
|
/**
|
|
29
25
|
* The folders to use for resource lookup
|
|
30
26
|
*/
|
package/src/types.ts
CHANGED
|
@@ -14,7 +14,7 @@ export type TypedFunction<R = Any, V = unknown> = (this: V, ...args: Any[]) => R
|
|
|
14
14
|
|
|
15
15
|
export type MethodDescriptor<V = Any, R = Any> = TypedPropertyDescriptor<TypedFunction<R, V>>;
|
|
16
16
|
export type AsyncMethodDescriptor<V = Any, R = Any> = TypedPropertyDescriptor<TypedFunction<Promise<R>, V>>;
|
|
17
|
-
export type
|
|
17
|
+
export type AsyncIterableMethodDescriptor<V = Any, R = Any> = TypedPropertyDescriptor<TypedFunction<AsyncIterable<R>, V>>;
|
|
18
18
|
export type ClassTDecorator<T extends Class = Class> = (target: T) => T | void;
|
|
19
19
|
|
|
20
20
|
export type Primitive = number | bigint | boolean | string | Date;
|
|
@@ -34,9 +34,10 @@ type ValidPrimitiveFields<T, Z = undefined> = {
|
|
|
34
34
|
export type RetainPrimitiveFields<T, Z = undefined> = Pick<T, ValidPrimitiveFields<T, Z>>;
|
|
35
35
|
|
|
36
36
|
export const TypedObject: {
|
|
37
|
-
keys<T = unknown, K extends keyof T = keyof T & string>(
|
|
37
|
+
keys<T = unknown, K extends keyof T = keyof T & string>(value: T): K[];
|
|
38
38
|
fromEntries<K extends string | symbol, V>(items: ([K, V] | readonly [K, V])[]): Record<K, V>;
|
|
39
39
|
entries<K extends Record<symbol | string, unknown>>(record: K): [keyof K, K[keyof K]][];
|
|
40
|
+
assign<T extends {}, U extends T>(target: T, ...sources: U[]): U;
|
|
40
41
|
} & ObjectConstructor = Object;
|
|
41
42
|
|
|
42
43
|
export const safeAssign = <T extends {}, U extends {}>(target: T, ...sources: U[]): T & U =>
|
|
@@ -47,6 +48,7 @@ export function castTo<T>(input: unknown): T {
|
|
|
47
48
|
return input as T;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
export const isClass = (input: unknown): input is Class => typeof input === 'function' && 'Ⲑid' in input;
|
|
50
52
|
export const castKey = <T>(input: string | number | symbol): keyof T => castTo(input);
|
|
51
53
|
export const asFull = <T>(input: Partial<T>): T => castTo(input);
|
|
52
54
|
export const asConstructable = <Z = unknown>(input: Class | unknown): { constructor: Class<Z> } => castTo(input);
|
|
@@ -56,22 +58,20 @@ export function classConstruct<T>(cls: Class<T>, args: unknown[] = []): ClassIns
|
|
|
56
58
|
return castTo(new cons(...args));
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
export const hasFunction = <T>(key: keyof T) => (
|
|
60
|
-
typeof
|
|
61
|
+
export const hasFunction = <T>(key: keyof T) => (value: unknown): value is T =>
|
|
62
|
+
typeof value === 'object' && value !== null && typeof value[castKey(key)] === 'function';
|
|
61
63
|
|
|
62
64
|
export const hasToJSON = hasFunction<{ toJSON(): object }>('toJSON');
|
|
63
65
|
|
|
66
|
+
/**
|
|
67
|
+
* A type representing unknown type
|
|
68
|
+
*/
|
|
69
|
+
export class UnknownType { }
|
|
70
|
+
|
|
64
71
|
export function toConcrete<T extends unknown>(): Class<T> {
|
|
65
72
|
return arguments[0];
|
|
66
73
|
}
|
|
67
74
|
|
|
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
75
|
/**
|
|
76
76
|
* Find parent class for a given class object
|
|
77
77
|
*/
|
|
@@ -83,7 +83,8 @@ export function getParentClass(cls: Class): Class | undefined {
|
|
|
83
83
|
/**
|
|
84
84
|
* Get the class from an instance or class
|
|
85
85
|
*/
|
|
86
|
-
export const getClass = <T = unknown>(
|
|
86
|
+
export const getClass = <T = unknown>(value: ClassInstance | Class): Class<T> =>
|
|
87
|
+
'Ⲑid' in value ? castTo(value) : asConstructable<T>(value).constructor;
|
|
87
88
|
|
|
88
89
|
/**
|
|
89
90
|
* Range of bytes, inclusive
|
package/src/util.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import timers from 'node:timers/promises';
|
|
3
3
|
|
|
4
|
-
import { castTo
|
|
4
|
+
import { castTo } from './types.ts';
|
|
5
5
|
import { AppError } from './error.ts';
|
|
6
6
|
|
|
7
|
-
type MapFn<T, U> = (
|
|
7
|
+
type MapFn<T, U> = (value: T, i: number) => U | Promise<U>;
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Grab bag of common utilities
|
|
@@ -38,35 +38,35 @@ export class Util {
|
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* Generate a random UUID
|
|
41
|
-
* @param
|
|
41
|
+
* @param length The length of the uuid to generate
|
|
42
42
|
*/
|
|
43
|
-
static uuid(
|
|
44
|
-
const bytes = crypto.randomBytes(Math.ceil(
|
|
45
|
-
if (
|
|
43
|
+
static uuid(length: number = 32): string {
|
|
44
|
+
const bytes = crypto.randomBytes(Math.ceil(length / 2));
|
|
45
|
+
if (length === 32) { // Make valid uuid-v4
|
|
46
46
|
// eslint-disable-next-line no-bitwise
|
|
47
47
|
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
48
48
|
// eslint-disable-next-line no-bitwise
|
|
49
49
|
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
50
50
|
}
|
|
51
|
-
return bytes.toString('hex').substring(0,
|
|
51
|
+
return bytes.toString('hex').substring(0, length);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
55
|
* Map an async iterable with various mapping functions
|
|
56
56
|
*/
|
|
57
|
-
static
|
|
58
|
-
static
|
|
59
|
-
static
|
|
60
|
-
static async *
|
|
57
|
+
static mapAsyncIterable<T, U, V, W>(source: AsyncIterable<T>, fn1: MapFn<T, U>, fn2: MapFn<U, V>, fn3: MapFn<V, W>): AsyncIterable<W>;
|
|
58
|
+
static mapAsyncIterable<T, U, V>(source: AsyncIterable<T>, fn1: MapFn<T, U>, fn2: MapFn<U, V>): AsyncIterable<V>;
|
|
59
|
+
static mapAsyncIterable<T, U>(source: AsyncIterable<T>, fn: MapFn<T, U>): AsyncIterable<U>;
|
|
60
|
+
static async * mapAsyncIterable<T>(input: AsyncIterable<T>, ...fns: MapFn<unknown, unknown>[]): AsyncIterable<unknown> {
|
|
61
61
|
let idx = -1;
|
|
62
|
-
for await (const
|
|
63
|
-
if (
|
|
62
|
+
for await (const item of input) {
|
|
63
|
+
if (item !== undefined) {
|
|
64
64
|
idx += 1;
|
|
65
|
-
let
|
|
65
|
+
let result = item;
|
|
66
66
|
for (const fn of fns) {
|
|
67
|
-
|
|
67
|
+
result = castTo(await fn(result, idx));
|
|
68
68
|
}
|
|
69
|
-
yield
|
|
69
|
+
yield result;
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
}
|
|
@@ -103,9 +103,9 @@ export class Util {
|
|
|
103
103
|
cacheKey?: (...keyInput: K) => string
|
|
104
104
|
): (...input: K) => boolean {
|
|
105
105
|
|
|
106
|
-
const rawRules = (Array.isArray(rules) ? rules : rules.split(/,/g).map(
|
|
106
|
+
const rawRules = (Array.isArray(rules) ? rules : rules.split(/,/g).map(rule => rule.trim()));
|
|
107
107
|
const convertedRules = rawRules.map(rule => this.#allowDenyRuleInput(rule, convert));
|
|
108
|
-
const unmatchedValue = !convertedRules.some(
|
|
108
|
+
const unmatchedValue = !convertedRules.some(rule => rule.positive);
|
|
109
109
|
|
|
110
110
|
if (convertedRules.length) {
|
|
111
111
|
if (cacheKey) {
|
|
@@ -120,99 +120,23 @@ export class Util {
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
/**
|
|
124
|
-
* Encode JSON value as base64 encoded string
|
|
125
|
-
*/
|
|
126
|
-
static encodeSafeJSON<T>(value: T | undefined): string | undefined {
|
|
127
|
-
if (value === undefined) {
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
const res = JSON.stringify(value);
|
|
131
|
-
return Buffer.from(res, 'utf8').toString('base64');
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Decode JSON value from base64 encoded string
|
|
136
|
-
*/
|
|
137
|
-
static decodeSafeJSON<T>(input: string): T;
|
|
138
|
-
static decodeSafeJSON<T>(input?: string | undefined): T | undefined;
|
|
139
|
-
static decodeSafeJSON<T>(input?: string | undefined): T | undefined {
|
|
140
|
-
if (!input) {
|
|
141
|
-
return undefined;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
let decoded = Buffer.from(input, 'base64').toString('utf8');
|
|
145
|
-
|
|
146
|
-
// Read from encoded if it happens
|
|
147
|
-
if (decoded.startsWith('%')) {
|
|
148
|
-
decoded = decodeURIComponent(decoded);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return JSON.parse(decoded, undefined);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Serialize to JSON
|
|
156
|
-
*/
|
|
157
|
-
static serializeToJSON<T>(out: T): string {
|
|
158
|
-
return JSON.stringify(out, function (k, v) {
|
|
159
|
-
const ov = this[k];
|
|
160
|
-
if (ov && ov instanceof Error) {
|
|
161
|
-
return {
|
|
162
|
-
$: true,
|
|
163
|
-
...hasToJSON(ov) ? ov.toJSON() : ov,
|
|
164
|
-
name: ov.name,
|
|
165
|
-
message: ov.message,
|
|
166
|
-
stack: ov.stack,
|
|
167
|
-
};
|
|
168
|
-
} else if (typeof v === 'bigint') {
|
|
169
|
-
return `${v.toString()}$n`;
|
|
170
|
-
} else {
|
|
171
|
-
return v;
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Deserialize from JSON
|
|
178
|
-
*/
|
|
179
|
-
static deserializeFromJson<T = unknown>(input: string): T {
|
|
180
|
-
return JSON.parse(input, function (k, v) {
|
|
181
|
-
if (v && typeof v === 'object' && '$' in v) {
|
|
182
|
-
const err = AppError.fromJSON(v) ?? new Error();
|
|
183
|
-
if (!(err instanceof AppError)) {
|
|
184
|
-
const { $: _, ...rest } = v;
|
|
185
|
-
Object.assign(err, rest);
|
|
186
|
-
}
|
|
187
|
-
err.message = v.message;
|
|
188
|
-
err.stack = v.stack;
|
|
189
|
-
err.name = v.name;
|
|
190
|
-
return err;
|
|
191
|
-
} else if (typeof v === 'string' && /^\d+[$]n$/.test(v)) {
|
|
192
|
-
return BigInt(v.split('$')[0]);
|
|
193
|
-
} else {
|
|
194
|
-
return v;
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
|
|
199
123
|
/**
|
|
200
124
|
* Retry an operation, with a custom conflict handler
|
|
201
|
-
* @param
|
|
125
|
+
* @param operation The operation to retry
|
|
202
126
|
* @param isHandledConflict Function to determine if the error is a handled conflict
|
|
203
127
|
* @param maxTries Maximum number of retries
|
|
204
128
|
*/
|
|
205
129
|
static async acquireWithRetry<T>(
|
|
206
|
-
|
|
207
|
-
prepareRetry: (
|
|
130
|
+
operation: () => T | Promise<T>,
|
|
131
|
+
prepareRetry: (error: unknown, count: number) => (void | undefined | boolean | Promise<(void | undefined | boolean)>),
|
|
208
132
|
maxTries = 5,
|
|
209
133
|
): Promise<T> {
|
|
210
134
|
for (let i = 0; i < maxTries; i++) {
|
|
211
135
|
try {
|
|
212
|
-
return await
|
|
213
|
-
} catch (
|
|
214
|
-
if (i === maxTries - 1 || await prepareRetry(
|
|
215
|
-
throw
|
|
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
|
|
216
140
|
}
|
|
217
141
|
}
|
|
218
142
|
}
|