@travetto/runtime 5.0.0-rc.9 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,7 +34,6 @@ While running any code within the framework, there are common patterns/goals for
34
34
  ```typescript
35
35
  class $Runtime {
36
36
  constructor(idx: ManifestIndex, resourceOverrides?: Record<string, string>);
37
- get #moduleAliases(): Record<string, string>;
38
37
  /** Get env name, with support for the default env */
39
38
  get env(): string | undefined;
40
39
  /** Are we in development mode */
@@ -260,7 +259,7 @@ The [FileLoader](https://github.com/travetto/travetto/tree/main/module/runtime/s
260
259
  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
260
 
262
261
  ## Common Utilities
263
- Common utilities used throughout the framework. Currently [Util](https://github.com/travetto/travetto/tree/main/module/runtime/src/util.ts#L19) includes:
262
+ Common utilities used throughout the framework. Currently [Util](https://github.com/travetto/travetto/tree/main/module/runtime/src/util.ts#L17) includes:
264
263
  * `uuid(len: number)` generates a simple uuid for use within the application.
265
264
  * `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
265
  * `hash(text: string, size?: number)` produces a full sha512 hash.
@@ -353,4 +352,4 @@ export function registerShutdownHandler() {
353
352
  ```
354
353
 
355
354
  ## Path Behavior
356
- To ensure consistency in path usage throughout the framework, imports pointing at $`node:path` and $`path` are rewritten at compile time. These imports are pointing towards [Manifest](https://github.com/travetto/travetto/tree/main/module/manifest#readme "Support for project indexing, manifesting, along with file watching")'s `path` implementation. This allows for seamless import/usage patterns with the reliability needed for cross platform support.
355
+ To ensure consistency in path usage throughout the framework, imports pointing at `node:path` and `path` are rewritten at compile time. These imports are pointing towards [Manifest](https://github.com/travetto/travetto/tree/main/module/manifest#readme "Support for project indexing, manifesting, along with file watching")'s `path` implementation. This allows for seamless import/usage patterns with the reliability needed for cross platform support.
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-rc.9",
3
+ "version": "5.0.1",
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-rc.5",
31
+ "@travetto/manifest": "^5.0.1",
32
32
  "@types/debug": "^4.1.12",
33
- "@types/node": "^22.1.0",
34
- "debug": "^4.3.5"
33
+ "@types/node": "^22.5.1",
34
+ "debug": "^4.3.6"
35
35
  },
36
36
  "peerDependencies": {
37
- "@travetto/transformer": "^5.0.0-rc.6"
37
+ "@travetto/transformer": "^5.0.1"
38
38
  },
39
39
  "peerDependenciesMeta": {
40
40
  "@travetto/transformer": {
package/src/binary.ts ADDED
@@ -0,0 +1,133 @@
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.copyFile(temp, file);
63
+ await fs.rm(temp, { force: true });
64
+ }
65
+
66
+ /**
67
+ * Make a blob, and assign metadata
68
+ */
69
+ static readableBlob(input: () => (Readable | Promise<Readable>), metadata: Omit<BlobMeta, 'filename'> & { filename: string }): File;
70
+ static readableBlob(input: () => (Readable | Promise<Readable>), metadata?: BlobMeta): Blob;
71
+ static readableBlob(input: () => (Readable | Promise<Readable>), metadata: BlobMeta = {}): Blob | File {
72
+ const go = (): Readable => {
73
+ const stream = new PassThrough();
74
+ Promise.resolve(input()).then(v => v.pipe(stream), (err) => stream.destroy(err));
75
+ return stream;
76
+ };
77
+
78
+ const size = metadata.range ? (metadata.range.end - metadata.range.start) + 1 : metadata.size;
79
+ const out: Blob = metadata.filename ?
80
+ new File([], path.basename(metadata.filename), { type: metadata.contentType }) :
81
+ new Blob([], { type: metadata.contentType });
82
+
83
+ return Object.defineProperties(out, {
84
+ size: { value: size },
85
+ stream: { value: () => ReadableStream.from(go()) },
86
+ arrayBuffer: { value: () => toBuffer(go()) },
87
+ text: { value: () => toText(go()) },
88
+ bytes: { value: () => toBuffer(go()).then(v => new Uint8Array(v)) },
89
+ [BlobMetaⲐ]: { value: metadata }
90
+ });
91
+ }
92
+
93
+ /**
94
+ * Get blob metadata
95
+ */
96
+ static getBlobMeta(blob: Blob): BlobMeta | undefined {
97
+ const withMeta: Blob & { [BlobMetaⲐ]?: BlobMeta } = blob;
98
+ return withMeta[BlobMetaⲐ];
99
+ }
100
+
101
+ /**
102
+ * Write limiter
103
+ * @returns
104
+ */
105
+ static limitWrite(maxSize: number): Transform {
106
+ let read = 0;
107
+ return new Transform({
108
+ transform(chunk, encoding, callback): void {
109
+ read += (Buffer.isBuffer(chunk) || typeof chunk === 'string') ? chunk.length : (chunk instanceof Uint8Array ? chunk.byteLength : 0);
110
+ if (read > maxSize) {
111
+ callback(new AppError('File size exceeded', 'data', { read, size: maxSize }));
112
+ } else {
113
+ callback(null, chunk);
114
+ }
115
+ },
116
+ });
117
+ }
118
+
119
+ /**
120
+ * Get a hashed location/path for a blob
121
+ */
122
+ static hashedBlobLocation(meta: BlobMeta): string {
123
+ const hash = meta.hash ?? Util.uuid();
124
+
125
+ let parts = hash.match(/(.{1,4})/g)!.slice();
126
+ if (parts.length > 4) {
127
+ parts = [...parts.slice(0, 4), parts.slice(4).join('')];
128
+ }
129
+
130
+ const ext = path.extname(meta.filename ?? '') || '.bin';
131
+ return `${parts.join('/')}${ext}`;
132
+ }
133
+ }
package/src/global.d.ts CHANGED
@@ -1,3 +1,12 @@
1
- declare interface Function {
2
- Ⲑid: string;
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,32 @@ 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
+ export const hasFunction = <T>(key: keyof T) => (o: unknown): o is T =>
50
+ typeof o === 'object' && o !== null && typeof o[castKey(key)] === 'function';
51
+
52
+ /**
53
+ * Range of bytes, inclusive
54
+ */
55
+ export type ByteRange = { start: number, end?: number };
56
+
57
+ export interface BlobMeta {
58
+ /** Size of blob */
59
+ size?: number;
60
+ /** Mime type of the content */
61
+ contentType?: string;
62
+ /** Hash of blob contents */
63
+ hash?: string;
64
+ /** The original base filename of the file */
65
+ filename?: string;
66
+ /** Filenames title, optional for elements like images, audio, videos */
67
+ title?: string;
68
+ /** Content encoding */
69
+ contentEncoding?: string;
70
+ /** Content language */
71
+ contentLanguage?: string;
72
+ /** Cache control */
73
+ cacheControl?: string;
74
+ /** Byte range for blob */
75
+ range?: Required<ByteRange>;
76
+ }
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
- import fs from 'node:fs/promises';
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
  }