@travetto/runtime 5.0.0-rc.9 → 5.0.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 +1 -1
- package/__index__.ts +2 -0
- package/package.json +5 -5
- package/src/binary.ts +132 -0
- package/src/global.d.ts +11 -2
- package/src/queue.ts +64 -0
- package/src/types.ts +31 -0
- package/src/util.ts +54 -32
package/README.md
CHANGED
|
@@ -260,7 +260,7 @@ The [FileLoader](https://github.com/travetto/travetto/tree/main/module/runtime/s
|
|
|
260
260
|
The [FileLoader](https://github.com/travetto/travetto/tree/main/module/runtime/src/file-loader.ts#L11) also supports tying itself to [Env](https://github.com/travetto/travetto/tree/main/module/runtime/src/env.ts#L109)'s `TRV_RESOURCES` information on where to attempt to find a requested resource.
|
|
261
261
|
|
|
262
262
|
## Common Utilities
|
|
263
|
-
Common utilities used throughout the framework. Currently [Util](https://github.com/travetto/travetto/tree/main/module/runtime/src/util.ts#
|
|
263
|
+
Common utilities used throughout the framework. Currently [Util](https://github.com/travetto/travetto/tree/main/module/runtime/src/util.ts#L17) includes:
|
|
264
264
|
* `uuid(len: number)` generates a simple uuid for use within the application.
|
|
265
265
|
* `allowDenyMatcher(rules[])` builds a matching function that leverages the rules as an allow/deny list, where order of the rules matters. Negative rules are prefixed by '!'.
|
|
266
266
|
* `hash(text: string, size?: number)` produces a full sha512 hash.
|
package/__index__.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/// <reference path="./src/trv.d.ts" />
|
|
2
2
|
/// <reference path="./src/global.d.ts" />
|
|
3
|
+
export * from './src/binary';
|
|
3
4
|
export * from './src/console';
|
|
4
5
|
export * from './src/context';
|
|
5
6
|
export * from './src/debug';
|
|
@@ -9,6 +10,7 @@ export * from './src/env';
|
|
|
9
10
|
export * from './src/file-loader';
|
|
10
11
|
export * from './src/function';
|
|
11
12
|
export * from './src/manifest-index';
|
|
13
|
+
export * from './src/queue';
|
|
12
14
|
export * from './src/resources';
|
|
13
15
|
export * from './src/shutdown';
|
|
14
16
|
export * from './src/time';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/runtime",
|
|
3
|
-
"version": "5.0.0
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"description": "Runtime for travetto applications.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"console-manager",
|
|
@@ -28,13 +28,13 @@
|
|
|
28
28
|
"node": ">=22.0.0"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@travetto/manifest": "^5.0.0
|
|
31
|
+
"@travetto/manifest": "^5.0.0",
|
|
32
32
|
"@types/debug": "^4.1.12",
|
|
33
|
-
"@types/node": "^22.
|
|
34
|
-
"debug": "^4.3.
|
|
33
|
+
"@types/node": "^22.3.0",
|
|
34
|
+
"debug": "^4.3.6"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@travetto/transformer": "^5.0.0
|
|
37
|
+
"@travetto/transformer": "^5.0.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependenciesMeta": {
|
|
40
40
|
"@travetto/transformer": {
|
package/src/binary.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import { PassThrough, Readable, Transform } from 'node:stream';
|
|
6
|
+
import { pipeline } from 'node:stream/promises';
|
|
7
|
+
import { ReadableStream } from 'node:stream/web';
|
|
8
|
+
import { text as toText, arrayBuffer as toBuffer } from 'node:stream/consumers';
|
|
9
|
+
|
|
10
|
+
import { BinaryInput, BlobMeta } from './types';
|
|
11
|
+
import { AppError } from './error';
|
|
12
|
+
import { Util } from './util';
|
|
13
|
+
|
|
14
|
+
const BlobMetaⲐ = Symbol.for('@travetto/runtime:blob-meta');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Common functions for dealing with binary data/streams
|
|
18
|
+
*/
|
|
19
|
+
export class BinaryUtil {
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate a proper sha512 hash from a src value
|
|
23
|
+
* @param src The seed value to build the hash from
|
|
24
|
+
* @param len The optional length of the hash to generate
|
|
25
|
+
*/
|
|
26
|
+
static hash(src: string, len: number = -1): string {
|
|
27
|
+
const hash = crypto.createHash('sha512');
|
|
28
|
+
hash.update(src);
|
|
29
|
+
const ret = hash.digest('hex');
|
|
30
|
+
return len > 0 ? ret.substring(0, len) : ret;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Compute hash from an input blob, buffer or readable stream.
|
|
35
|
+
*/
|
|
36
|
+
static async hashInput(input: BinaryInput): Promise<string> {
|
|
37
|
+
const hash = crypto.createHash('sha256').setEncoding('hex');
|
|
38
|
+
if (Buffer.isBuffer(input)) {
|
|
39
|
+
hash.write(input);
|
|
40
|
+
} else if (input instanceof Blob) {
|
|
41
|
+
await pipeline(Readable.fromWeb(input.stream()), hash);
|
|
42
|
+
} else {
|
|
43
|
+
await pipeline(input, hash);
|
|
44
|
+
}
|
|
45
|
+
return hash.digest('hex').toString();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Write file and copy over when ready
|
|
50
|
+
*/
|
|
51
|
+
static async bufferedFileWrite(file: string, content: string, checkHash = false): Promise<void> {
|
|
52
|
+
if (checkHash) {
|
|
53
|
+
const current = await fs.readFile(file, 'utf8').catch(() => '');
|
|
54
|
+
if (this.hash(current) === this.hash(content)) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const temp = path.resolve(os.tmpdir(), `${process.hrtime()[1]}.${path.basename(file)}`);
|
|
60
|
+
await fs.writeFile(temp, content, 'utf8');
|
|
61
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
62
|
+
await fs.rename(temp, file);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Make a blob, and assign metadata
|
|
67
|
+
*/
|
|
68
|
+
static readableBlob(input: () => (Readable | Promise<Readable>), metadata: Omit<BlobMeta, 'filename'> & { filename: string }): File;
|
|
69
|
+
static readableBlob(input: () => (Readable | Promise<Readable>), metadata?: BlobMeta): Blob;
|
|
70
|
+
static readableBlob(input: () => (Readable | Promise<Readable>), metadata: BlobMeta = {}): Blob | File {
|
|
71
|
+
const go = (): Readable => {
|
|
72
|
+
const stream = new PassThrough();
|
|
73
|
+
Promise.resolve(input()).then(v => v.pipe(stream), (err) => stream.destroy(err));
|
|
74
|
+
return stream;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const size = metadata.range ? (metadata.range.end - metadata.range.start) + 1 : metadata.size;
|
|
78
|
+
const out: Blob = metadata.filename ?
|
|
79
|
+
new File([], path.basename(metadata.filename), { type: metadata.contentType }) :
|
|
80
|
+
new Blob([], { type: metadata.contentType });
|
|
81
|
+
|
|
82
|
+
return Object.defineProperties(out, {
|
|
83
|
+
size: { value: size },
|
|
84
|
+
stream: { value: () => ReadableStream.from(go()) },
|
|
85
|
+
arrayBuffer: { value: () => toBuffer(go()) },
|
|
86
|
+
text: { value: () => toText(go()) },
|
|
87
|
+
bytes: { value: () => toBuffer(go()).then(v => new Uint8Array(v)) },
|
|
88
|
+
[BlobMetaⲐ]: { value: metadata }
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get blob metadata
|
|
94
|
+
*/
|
|
95
|
+
static getBlobMeta(blob: Blob): BlobMeta | undefined {
|
|
96
|
+
const withMeta: Blob & { [BlobMetaⲐ]?: BlobMeta } = blob;
|
|
97
|
+
return withMeta[BlobMetaⲐ];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Write limiter
|
|
102
|
+
* @returns
|
|
103
|
+
*/
|
|
104
|
+
static limitWrite(maxSize: number): Transform {
|
|
105
|
+
let read = 0;
|
|
106
|
+
return new Transform({
|
|
107
|
+
transform(chunk, encoding, callback): void {
|
|
108
|
+
read += (Buffer.isBuffer(chunk) || typeof chunk === 'string') ? chunk.length : (chunk instanceof Uint8Array ? chunk.byteLength : 0);
|
|
109
|
+
if (read > maxSize) {
|
|
110
|
+
callback(new AppError('File size exceeded', 'data', { read, size: maxSize }));
|
|
111
|
+
} else {
|
|
112
|
+
callback(null, chunk);
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get a hashed location/path for a blob
|
|
120
|
+
*/
|
|
121
|
+
static hashedBlobLocation(meta: BlobMeta): string {
|
|
122
|
+
const hash = meta.hash ?? Util.uuid();
|
|
123
|
+
|
|
124
|
+
let parts = hash.match(/(.{1,4})/g)!.slice();
|
|
125
|
+
if (parts.length > 4) {
|
|
126
|
+
parts = [...parts.slice(0, 4), parts.slice(4).join('')];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const ext = path.extname(meta.filename ?? '') || '.bin';
|
|
130
|
+
return `${parts.join('/')}${ext}`;
|
|
131
|
+
}
|
|
132
|
+
}
|
package/src/global.d.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import './types';
|
|
2
|
+
|
|
3
|
+
declare const write: unique symbol;
|
|
4
|
+
declare global {
|
|
5
|
+
// https://github.com/microsoft/TypeScript/issues/59012
|
|
6
|
+
interface WritableStreamDefaultWriter<W = any> {
|
|
7
|
+
[write]?: (a: W) => void;
|
|
8
|
+
}
|
|
9
|
+
interface Function {
|
|
10
|
+
Ⲑid: string;
|
|
11
|
+
}
|
|
3
12
|
}
|
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/types.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import { ReadableStream } from 'node:stream/web';
|
|
3
|
+
|
|
1
4
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2
5
|
export type Any = any;
|
|
3
6
|
|
|
@@ -7,6 +10,8 @@ export type ClassInstance<T = Any> = T & {
|
|
|
7
10
|
constructor: Class<T> & { Ⲑid: string };
|
|
8
11
|
};
|
|
9
12
|
|
|
13
|
+
export type BinaryInput = Blob | Buffer | Readable | ReadableStream;
|
|
14
|
+
|
|
10
15
|
export type TypedFunction<R = Any, V = unknown> = (this: V, ...args: Any[]) => R;
|
|
11
16
|
|
|
12
17
|
export type MethodDescriptor<V = Any, R = Any> = TypedPropertyDescriptor<TypedFunction<R, V>>;
|
|
@@ -40,3 +45,29 @@ export function classConstruct<T>(cls: Class<T>, args: unknown[] = []): ClassIns
|
|
|
40
45
|
const cons: { new(..._args: Any[]): T } = castTo(cls);
|
|
41
46
|
return castTo(new cons(...args));
|
|
42
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
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import timers from 'node:timers/promises';
|
|
3
|
-
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import os from 'node:os';
|
|
3
|
+
|
|
6
4
|
import { castTo } from './types';
|
|
7
5
|
|
|
8
6
|
type PromiseWithResolvers<T> = {
|
|
@@ -18,6 +16,31 @@ type MapFn<T, U> = (val: T, i: number) => U | Promise<U>;
|
|
|
18
16
|
*/
|
|
19
17
|
export class Util {
|
|
20
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
|
+
|
|
21
44
|
/**
|
|
22
45
|
* Generate a random UUID
|
|
23
46
|
* @param len The length of the uuid to generate
|
|
@@ -31,18 +54,6 @@ export class Util {
|
|
|
31
54
|
return bytes.toString('hex').substring(0, len);
|
|
32
55
|
}
|
|
33
56
|
|
|
34
|
-
/**
|
|
35
|
-
* Generate a proper sha512 hash from a src value
|
|
36
|
-
* @param src The seed value to build the hash from
|
|
37
|
-
* @param len The optional length of the hash to generate
|
|
38
|
-
*/
|
|
39
|
-
static hash(src: string, len: number = -1): string {
|
|
40
|
-
const hash = crypto.createHash('sha512');
|
|
41
|
-
hash.update(src);
|
|
42
|
-
const ret = hash.digest('hex');
|
|
43
|
-
return len > 0 ? ret.substring(0, len) : ret;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
57
|
/**
|
|
47
58
|
* Produce a promise that is externally resolvable
|
|
48
59
|
*/
|
|
@@ -72,23 +83,6 @@ export class Util {
|
|
|
72
83
|
}
|
|
73
84
|
}
|
|
74
85
|
|
|
75
|
-
/**
|
|
76
|
-
* Write file and copy over when ready
|
|
77
|
-
*/
|
|
78
|
-
static async bufferedFileWrite(file: string, content: string, checkHash = false): Promise<void> {
|
|
79
|
-
if (checkHash) {
|
|
80
|
-
const current = await fs.readFile(file, 'utf8').catch(() => '');
|
|
81
|
-
if (Util.hash(current) === Util.hash(content)) {
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const temp = path.resolve(os.tmpdir(), `${process.hrtime()[1]}.${path.basename(file)}`);
|
|
87
|
-
await fs.writeFile(temp, content, 'utf8');
|
|
88
|
-
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
89
|
-
await fs.rename(temp, file);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
86
|
/**
|
|
93
87
|
* Non-blocking timeout
|
|
94
88
|
*/
|
|
@@ -109,4 +103,32 @@ export class Util {
|
|
|
109
103
|
static queueMacroTask(): Promise<void> {
|
|
110
104
|
return timers.setImmediate(undefined);
|
|
111
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
|
+
}
|
|
112
134
|
}
|