@travetto/runtime 5.0.0-rc.10
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 +356 -0
- package/__index__.ts +19 -0
- package/package.json +51 -0
- package/src/binary.ts +108 -0
- package/src/console.ts +137 -0
- package/src/context.ts +122 -0
- package/src/debug.ts +18 -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 +61 -0
- package/src/global.d.ts +20 -0
- package/src/manifest-index.ts +4 -0
- package/src/queue.ts +64 -0
- package/src/resources.ts +28 -0
- package/src/shutdown.ts +65 -0
- package/src/time.ts +100 -0
- package/src/trv.d.ts +64 -0
- package/src/types.ts +73 -0
- package/src/util.ts +134 -0
- package/src/watch.ts +38 -0
- package/support/transformer.console-log.ts +112 -0
- package/support/transformer.debug-method.ts +41 -0
- package/support/transformer.function-metadata.ts +144 -0
- package/support/transformer.rewrite-path-import.ts +39 -0
- package/support/transformer.type-helpers.ts +25 -0
package/src/function.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type FunctionMetadataTag = { hash: number, lines: [start: number, end: number, bodyStart?: number] };
|
|
2
|
+
export type FunctionMetadata = FunctionMetadataTag & {
|
|
3
|
+
id: string;
|
|
4
|
+
import: string;
|
|
5
|
+
module: string;
|
|
6
|
+
modulePath: string;
|
|
7
|
+
methods?: Record<string, FunctionMetadataTag>;
|
|
8
|
+
synthetic?: boolean;
|
|
9
|
+
class?: boolean;
|
|
10
|
+
abstract?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const METADATA = Symbol.for('@travetto/runtime:function-metadata');
|
|
14
|
+
|
|
15
|
+
const pending = new Set<Function>([]);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initialize the meta data for a function/class
|
|
19
|
+
* @param fn Class
|
|
20
|
+
* @param `file` Filename
|
|
21
|
+
* @param `hash` Hash of class contents
|
|
22
|
+
* @param `line` Line number in source
|
|
23
|
+
* @param `methods` Methods and their hashes
|
|
24
|
+
* @param `abstract` Is the class abstract
|
|
25
|
+
* @param `synthetic` Is this code generated at build time
|
|
26
|
+
* @private
|
|
27
|
+
*/
|
|
28
|
+
export function registerFunction(
|
|
29
|
+
fn: Function, [pkg, pth]: [string, string], tag: FunctionMetadataTag,
|
|
30
|
+
methods?: Record<string, FunctionMetadataTag>, abstract?: boolean, synthetic?: boolean
|
|
31
|
+
): void {
|
|
32
|
+
const modulePath = pth.replace(/[.][cm]?[tj]sx?$/, '');
|
|
33
|
+
|
|
34
|
+
const metadata: FunctionMetadata = {
|
|
35
|
+
id: (fn.name ? `${pkg}:${modulePath}○${fn.name}` : `${pkg}:${modulePath}`),
|
|
36
|
+
import: `${pkg}/${pth}`,
|
|
37
|
+
module: pkg,
|
|
38
|
+
modulePath,
|
|
39
|
+
...tag, methods, abstract, synthetic, class: abstract !== undefined
|
|
40
|
+
};
|
|
41
|
+
pending.add(fn);
|
|
42
|
+
Object.defineProperties(fn, { Ⲑid: { value: metadata.id }, [METADATA]: { value: metadata } });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Flush all pending function registers
|
|
47
|
+
*/
|
|
48
|
+
export function flushPendingFunctions(): Function[] {
|
|
49
|
+
const fns = [...pending];
|
|
50
|
+
pending.clear();
|
|
51
|
+
return fns;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Read metadata
|
|
56
|
+
*/
|
|
57
|
+
export function describeFunction(fn: Function): FunctionMetadata;
|
|
58
|
+
export function describeFunction(fn?: Function): FunctionMetadata | undefined {
|
|
59
|
+
const _fn: (Function & { [METADATA]?: FunctionMetadata }) | undefined = fn;
|
|
60
|
+
return _fn?.[METADATA];
|
|
61
|
+
}
|
package/src/global.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { BlobMeta } from './types';
|
|
2
|
+
|
|
3
|
+
// https://github.com/microsoft/TypeScript/issues/59012
|
|
4
|
+
declare const write: unique symbol;
|
|
5
|
+
declare global {
|
|
6
|
+
interface WritableStreamDefaultWriter<W = any> {
|
|
7
|
+
[write]?: (a: W) => void;
|
|
8
|
+
}
|
|
9
|
+
interface Function {
|
|
10
|
+
Ⲑid: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface Blob {
|
|
14
|
+
readonly meta?: Readonly<BlobMeta>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface File {
|
|
18
|
+
readonly meta?: Readonly<BlobMeta>;
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/queue.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Util } from './util';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* An asynchronous queue
|
|
5
|
+
*/
|
|
6
|
+
export class AsyncQueue<X> implements AsyncIterator<X>, AsyncIterable<X> {
|
|
7
|
+
|
|
8
|
+
#queue: X[] = [];
|
|
9
|
+
#done = false;
|
|
10
|
+
#ready = Util.resolvablePromise();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Initial set of items
|
|
14
|
+
*/
|
|
15
|
+
constructor(initial: Iterable<X> = [], signal?: AbortSignal) {
|
|
16
|
+
this.#queue.push(...initial);
|
|
17
|
+
signal?.addEventListener('abort', () => this.close());
|
|
18
|
+
if (signal?.aborted) {
|
|
19
|
+
this.close();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Allow for iteration
|
|
24
|
+
[Symbol.asyncIterator](): AsyncIterator<X> {
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Wait for next event to fire
|
|
30
|
+
*/
|
|
31
|
+
async next(): Promise<IteratorResult<X>> {
|
|
32
|
+
while (!this.#done && !this.#queue.length) {
|
|
33
|
+
await this.#ready.promise;
|
|
34
|
+
this.#ready = Util.resolvablePromise();
|
|
35
|
+
}
|
|
36
|
+
return { value: (this.#queue.length ? this.#queue.shift() : undefined)!, done: this.#done };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Queue next event to fire
|
|
41
|
+
* @param {boolean} immediate Determines if item(s) should be append or prepended to the queue
|
|
42
|
+
*/
|
|
43
|
+
add(item: X, immediate = false): void {
|
|
44
|
+
this.#queue[immediate ? 'unshift' : 'push'](item);
|
|
45
|
+
this.#ready.resolve();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Close the iterator
|
|
50
|
+
*/
|
|
51
|
+
close(): void {
|
|
52
|
+
this.#done = true;
|
|
53
|
+
this.#ready.resolve();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Throw an error from the queue, rejecting and terminating immediately
|
|
58
|
+
*/
|
|
59
|
+
async throw(e?: Error): Promise<IteratorResult<X>> {
|
|
60
|
+
this.#done = true;
|
|
61
|
+
this.#ready.reject(e);
|
|
62
|
+
return { value: undefined, done: this.#done };
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/resources.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
#mod: string;
|
|
12
|
+
|
|
13
|
+
constructor() {
|
|
14
|
+
super([]);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
override get searchPaths(): readonly string[] {
|
|
18
|
+
if (!this.#computed || this.#env !== Env.TRV_RESOURCES.val || this.#mod !== Env.TRV_MODULE.val) {
|
|
19
|
+
this.#env = Env.TRV_RESOURCES.val!;
|
|
20
|
+
this.#mod = Env.TRV_MODULE.val!;
|
|
21
|
+
this.#computed = Runtime.resourcePaths();
|
|
22
|
+
}
|
|
23
|
+
return this.#computed;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Runtime resources */
|
|
28
|
+
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,100 @@
|
|
|
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
|
+
const groups: { amount?: string, unit?: TimeUnit } = amount.match(this.#timePattern)?.groups ?? {};
|
|
39
|
+
const amountStr = groups.amount ?? `${amount}`;
|
|
40
|
+
unit = groups.unit ?? unit ?? 'ms';
|
|
41
|
+
if (!TIME_UNITS[unit]) {
|
|
42
|
+
return NaN;
|
|
43
|
+
}
|
|
44
|
+
amount = amountStr.includes('.') ? parseFloat(amountStr) : parseInt(amountStr, 10);
|
|
45
|
+
}
|
|
46
|
+
return amount * TIME_UNITS[unit ?? 'ms'];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Returns the time converted to seconds
|
|
51
|
+
* @param date The date to convert
|
|
52
|
+
*/
|
|
53
|
+
static asSeconds(date: Date | number | TimeSpan, unit?: TimeUnit): number {
|
|
54
|
+
return Math.trunc(this.asMillis(date, unit) / 1000);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Returns the time converted to a Date
|
|
59
|
+
* @param date The date to convert
|
|
60
|
+
*/
|
|
61
|
+
static asDate(date: Date | number | TimeSpan, unit?: TimeUnit): Date {
|
|
62
|
+
return new Date(this.asMillis(date, unit));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolve time or span to possible time
|
|
67
|
+
*/
|
|
68
|
+
static fromValue(value: Date | number | string | undefined): number | undefined {
|
|
69
|
+
if (value === undefined) {
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
const val = (typeof value === 'string' && /\d+[a-z]$/i.test(value)) ?
|
|
73
|
+
(this.isTimeSpan(value) ? this.asMillis(value) : undefined) :
|
|
74
|
+
(typeof value === 'string' ? parseInt(value, 10) :
|
|
75
|
+
(value instanceof Date ? value.getTime() : value));
|
|
76
|
+
return Number.isNaN(val) ? undefined : val;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Returns a new date with `amount` units into the future
|
|
81
|
+
* @param amount Number of units to extend
|
|
82
|
+
* @param unit Time unit to extend ('ms', 's', 'm', 'h', 'd', 'w', 'y')
|
|
83
|
+
*/
|
|
84
|
+
static fromNow(amount: number | TimeSpan, unit: TimeUnit = 'ms'): Date {
|
|
85
|
+
return new Date(Date.now() + this.asMillis(amount, unit));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Returns a pretty timestamp
|
|
90
|
+
* @param time Time in milliseconds
|
|
91
|
+
*/
|
|
92
|
+
static asClock(time: number): string {
|
|
93
|
+
const s = Math.trunc(time / 1000);
|
|
94
|
+
return [
|
|
95
|
+
s > 3600 ? `${Math.trunc(s / 3600).toString().padStart(2, '0')}h` : '',
|
|
96
|
+
s > 60 ? `${Math.trunc((s % 3600) / 60).toString().padStart(2, '0')}m` : '',
|
|
97
|
+
`${(s % 60).toString().padStart(2, '0')}s`
|
|
98
|
+
].filter(x => !!x).slice(0, 2).join(' ');
|
|
99
|
+
}
|
|
100
|
+
}
|
package/src/trv.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
* Should break on first line of a method when using the @DebugBreak decorator
|
|
60
|
+
* @default false
|
|
61
|
+
*/
|
|
62
|
+
TRV_DEBUG_BREAK: boolean;
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import { ReadableStream } from 'node:stream/web';
|
|
3
|
+
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
|
+
export type Any = any;
|
|
6
|
+
|
|
7
|
+
export type AnyMap = { [key: string]: Any };
|
|
8
|
+
export type Class<T = Any> = abstract new (...args: Any[]) => T;
|
|
9
|
+
export type ClassInstance<T = Any> = T & {
|
|
10
|
+
constructor: Class<T> & { Ⲑid: string };
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type BinaryInput = Blob | Buffer | Readable | ReadableStream;
|
|
14
|
+
|
|
15
|
+
export type TypedFunction<R = Any, V = unknown> = (this: V, ...args: Any[]) => R;
|
|
16
|
+
|
|
17
|
+
export type MethodDescriptor<V = Any, R = Any> = TypedPropertyDescriptor<TypedFunction<R, V>>;
|
|
18
|
+
export type AsyncMethodDescriptor<V = Any, R = Any> = TypedPropertyDescriptor<TypedFunction<Promise<R>, V>>;
|
|
19
|
+
export type AsyncItrMethodDescriptor<V = Any, R = Any> = TypedPropertyDescriptor<TypedFunction<AsyncIterable<R>, V>>;
|
|
20
|
+
export type ClassTDecorator<T extends Class = Class> = (target: T) => T | void;
|
|
21
|
+
|
|
22
|
+
export type Primitive = number | bigint | boolean | string | Date;
|
|
23
|
+
|
|
24
|
+
export type DeepPartial<T> = {
|
|
25
|
+
[P in keyof T]?: (T[P] extends (Primitive | undefined) ? (T[P] | undefined) :
|
|
26
|
+
(T[P] extends Any[] ? (DeepPartial<T[P][number]> | null | undefined)[] : DeepPartial<T[P]>));
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const TypedObject: {
|
|
30
|
+
keys<T = unknown, K extends keyof T = keyof T & string>(o: T): K[];
|
|
31
|
+
fromEntries<K extends string | symbol, V>(items: ([K, V] | readonly [K, V])[]): Record<K, V>;
|
|
32
|
+
entries<K extends Record<symbol | string, unknown>>(record: K): [keyof K, K[keyof K]][];
|
|
33
|
+
} & ObjectConstructor = Object;
|
|
34
|
+
|
|
35
|
+
export function castTo<T>(input: unknown): T {
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
37
|
+
return input as T;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const castKey = <T>(input: string | number | symbol): keyof T => castTo(input);
|
|
41
|
+
export const asFull = <T>(input: Partial<T>): T => castTo(input);
|
|
42
|
+
export const asConstructable = <Z = unknown>(input: Class | unknown): { constructor: Class<Z> } => castTo(input);
|
|
43
|
+
|
|
44
|
+
export function classConstruct<T>(cls: Class<T>, args: unknown[] = []): ClassInstance<T> {
|
|
45
|
+
const cons: { new(..._args: Any[]): T } = castTo(cls);
|
|
46
|
+
return castTo(new cons(...args));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Range of bytes, inclusive
|
|
51
|
+
*/
|
|
52
|
+
export type ByteRange = { start: number, end?: number };
|
|
53
|
+
|
|
54
|
+
export interface BlobMeta {
|
|
55
|
+
/** Size of blob */
|
|
56
|
+
size?: number;
|
|
57
|
+
/** Mime type of the content */
|
|
58
|
+
contentType?: string;
|
|
59
|
+
/** Hash of blob contents */
|
|
60
|
+
hash?: string;
|
|
61
|
+
/** The original base filename of the file */
|
|
62
|
+
filename?: string;
|
|
63
|
+
/** Filenames title, optional for elements like images, audio, videos */
|
|
64
|
+
title?: string;
|
|
65
|
+
/** Content encoding */
|
|
66
|
+
contentEncoding?: string;
|
|
67
|
+
/** Content language */
|
|
68
|
+
contentLanguage?: string;
|
|
69
|
+
/** Cache control */
|
|
70
|
+
cacheControl?: string;
|
|
71
|
+
/** Byte range for blob */
|
|
72
|
+
range?: Required<ByteRange>;
|
|
73
|
+
}
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import timers from 'node:timers/promises';
|
|
3
|
+
|
|
4
|
+
import { castTo } from './types';
|
|
5
|
+
|
|
6
|
+
type PromiseWithResolvers<T> = {
|
|
7
|
+
resolve: (v: T) => void;
|
|
8
|
+
reject: (err?: unknown) => void;
|
|
9
|
+
promise: Promise<T>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type MapFn<T, U> = (val: T, i: number) => U | Promise<U>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Grab bag of common utilities
|
|
16
|
+
*/
|
|
17
|
+
export class Util {
|
|
18
|
+
|
|
19
|
+
static #match<T, K extends unknown[]>(
|
|
20
|
+
rules: { value: T, positive: boolean }[],
|
|
21
|
+
compare: (rule: T, ...compareInput: K) => boolean,
|
|
22
|
+
unmatchedValue: boolean,
|
|
23
|
+
...input: K
|
|
24
|
+
): boolean {
|
|
25
|
+
for (const rule of rules) {
|
|
26
|
+
if (compare(rule.value, ...input)) {
|
|
27
|
+
return rule.positive;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return unmatchedValue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static #allowDenyRuleInput<T>(
|
|
34
|
+
rule: (string | T | [value: T, positive: boolean] | [value: T]),
|
|
35
|
+
convert: (inputRule: string) => T
|
|
36
|
+
): { value: T, positive: boolean } {
|
|
37
|
+
return typeof rule === 'string' ?
|
|
38
|
+
{ value: convert(rule.replace(/^!/, '')), positive: !rule.startsWith('!') } :
|
|
39
|
+
Array.isArray(rule) ?
|
|
40
|
+
{ value: rule[0], positive: rule[1] ?? true } :
|
|
41
|
+
{ value: rule, positive: true };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Generate a random UUID
|
|
46
|
+
* @param len The length of the uuid to generate
|
|
47
|
+
*/
|
|
48
|
+
static uuid(len: number = 32): string {
|
|
49
|
+
const bytes = crypto.randomBytes(Math.ceil(len / 2));
|
|
50
|
+
// eslint-disable-next-line no-bitwise
|
|
51
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
52
|
+
// eslint-disable-next-line no-bitwise
|
|
53
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
54
|
+
return bytes.toString('hex').substring(0, len);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Produce a promise that is externally resolvable
|
|
59
|
+
*/
|
|
60
|
+
static resolvablePromise<T = void>(): PromiseWithResolvers<T> {
|
|
61
|
+
let ops: Pick<PromiseWithResolvers<T>, 'reject' | 'resolve'>;
|
|
62
|
+
const prom = new Promise<T>((resolve, reject) => ops = { resolve, reject });
|
|
63
|
+
return { ...ops!, promise: prom };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Map an async iterable with various mapping functions
|
|
68
|
+
*/
|
|
69
|
+
static mapAsyncItr<T, U, V, W>(source: AsyncIterable<T>, fn1: MapFn<T, U>, fn2: MapFn<U, V>, fn3: MapFn<V, W>): AsyncIterable<W>;
|
|
70
|
+
static mapAsyncItr<T, U, V>(source: AsyncIterable<T>, fn1: MapFn<T, U>, fn2: MapFn<U, V>): AsyncIterable<V>;
|
|
71
|
+
static mapAsyncItr<T, U>(source: AsyncIterable<T>, fn: MapFn<T, U>): AsyncIterable<U>;
|
|
72
|
+
static async * mapAsyncItr<T>(source: AsyncIterable<T>, ...fns: MapFn<unknown, unknown>[]): AsyncIterable<unknown> {
|
|
73
|
+
let idx = -1;
|
|
74
|
+
for await (const el of source) {
|
|
75
|
+
if (el !== undefined) {
|
|
76
|
+
idx += 1;
|
|
77
|
+
let m = el;
|
|
78
|
+
for (const fn of fns) {
|
|
79
|
+
m = castTo(await fn(m, idx));
|
|
80
|
+
}
|
|
81
|
+
yield m;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Non-blocking timeout
|
|
88
|
+
*/
|
|
89
|
+
static nonBlockingTimeout(time: number): Promise<void> {
|
|
90
|
+
return timers.setTimeout(time, undefined, { ref: false }).catch(() => { });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Blocking timeout
|
|
95
|
+
*/
|
|
96
|
+
static blockingTimeout(time: number): Promise<void> {
|
|
97
|
+
return timers.setTimeout(time, undefined, { ref: true }).catch(() => { });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Queue new macro task
|
|
102
|
+
*/
|
|
103
|
+
static queueMacroTask(): Promise<void> {
|
|
104
|
+
return timers.setImmediate(undefined);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Simple check against allow/deny rules
|
|
109
|
+
* @param rules
|
|
110
|
+
*/
|
|
111
|
+
static allowDeny<T, K extends unknown[]>(
|
|
112
|
+
rules: string | (string | T | [value: T, positive: boolean])[],
|
|
113
|
+
convert: (rule: string) => T,
|
|
114
|
+
compare: (rule: T, ...compareInput: K) => boolean,
|
|
115
|
+
cacheKey?: (...keyInput: K) => string
|
|
116
|
+
): (...input: K) => boolean {
|
|
117
|
+
|
|
118
|
+
const rawRules = (Array.isArray(rules) ? rules : rules.split(/\s*,\s*/g));
|
|
119
|
+
const convertedRules = rawRules.map(rule => this.#allowDenyRuleInput(rule, convert));
|
|
120
|
+
const unmatchedValue = !convertedRules.some(r => r.positive);
|
|
121
|
+
|
|
122
|
+
if (convertedRules.length) {
|
|
123
|
+
if (cacheKey) {
|
|
124
|
+
const cache: Record<string, boolean> = {};
|
|
125
|
+
return (...input: K) =>
|
|
126
|
+
cache[cacheKey(...input)] ??= this.#match(convertedRules, compare, unmatchedValue, ...input);
|
|
127
|
+
} else {
|
|
128
|
+
return (...input: K) => this.#match(convertedRules, compare, unmatchedValue, ...input);
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
return () => true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
package/src/watch.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { RuntimeIndex } from './manifest-index';
|
|
2
|
+
import { ExecUtil } from './exec';
|
|
3
|
+
import { ShutdownManager } from './shutdown';
|
|
4
|
+
import { Util } from './util';
|
|
5
|
+
|
|
6
|
+
export type WatchEvent = { file: string, action: 'create' | 'update' | 'delete', output: string, module: string, time: number };
|
|
7
|
+
|
|
8
|
+
export async function* watchCompiler(cfg?: { restartOnExit?: boolean, signal?: AbortSignal }): AsyncIterable<WatchEvent> {
|
|
9
|
+
// Load at runtime
|
|
10
|
+
const { CompilerClient } = await import('@travetto/compiler/support/server/client');
|
|
11
|
+
|
|
12
|
+
const client = new CompilerClient(RuntimeIndex.manifest, {
|
|
13
|
+
warn(message, ...args): void { console.error('warn', message, ...args); },
|
|
14
|
+
debug(message, ...args): void { console.error('debug', message, ...args); },
|
|
15
|
+
error(message, ...args): void { console.error('error', message, ...args); },
|
|
16
|
+
info(message, ...args): void { console.error('info', message, ...args); },
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const ctrl = new AbortController();
|
|
20
|
+
const remove = ShutdownManager.onGracefulShutdown(async () => ctrl.abort(), watchCompiler);
|
|
21
|
+
|
|
22
|
+
await client.waitForState(['compile-end', 'watch-start'], undefined, ctrl.signal);
|
|
23
|
+
|
|
24
|
+
if (!await client.isWatching()) { // If we get here, without a watch
|
|
25
|
+
while (!await client.isWatching()) { // Wait until watch starts
|
|
26
|
+
await Util.nonBlockingTimeout(1000 * 60);
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
yield* client.fetchEvents('change', { signal: ctrl.signal, enforceIteration: true });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
remove();
|
|
33
|
+
|
|
34
|
+
if (cfg?.restartOnExit) {
|
|
35
|
+
// We are done, request restart
|
|
36
|
+
await ShutdownManager.gracefulShutdown(ExecUtil.RESTART_EXIT_CODE);
|
|
37
|
+
}
|
|
38
|
+
}
|