@travetto/runtime 7.1.4 → 8.0.0-alpha.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/src/codec.ts ADDED
@@ -0,0 +1,105 @@
1
+ import { createInterface } from 'node:readline/promises';
2
+
3
+ import { BinaryUtil, type BinaryArray, type BinaryType } from './binary.ts';
4
+ import { RuntimeError } from './error.ts';
5
+ import { castTo, type Any } from './types.ts';
6
+
7
+ type TextInput = string | BinaryArray;
8
+
9
+ const UTF8_DECODER = new TextDecoder('utf8');
10
+ const UTF8_ENCODER = new TextEncoder();
11
+
12
+ /**
13
+ * Utilities for encoding and decoding common formats
14
+ */
15
+ export class CodecUtil {
16
+
17
+ /** Generate buffer from hex string */
18
+ static fromHexString(value: string): BinaryArray {
19
+ try {
20
+ return Uint8Array.fromHex(value);
21
+ } catch (err) {
22
+ if (err instanceof SyntaxError) {
23
+ throw new RuntimeError('Invalid hex string', { cause: err });
24
+ }
25
+ throw err;
26
+ }
27
+ }
28
+
29
+ /** Convert hex bytes to string */
30
+ static toHexString(value: BinaryArray): string {
31
+ return BinaryUtil.binaryArrayToUint8Array(value).toHex();
32
+ }
33
+
34
+ /** Return buffer from base64 string */
35
+ static fromBase64String(value: string): BinaryArray {
36
+ try {
37
+ return Uint8Array.fromBase64(value);
38
+ } catch (err) {
39
+ if (err instanceof SyntaxError) {
40
+ throw new RuntimeError('Invalid base64 string', { cause: err });
41
+ }
42
+ throw err;
43
+ }
44
+ }
45
+
46
+ /** Convert value to base64 string */
47
+ static toBase64String(value: BinaryArray): string {
48
+ return BinaryUtil.binaryArrayToUint8Array(value).toBase64();
49
+ }
50
+
51
+ /** Return buffer from utf8 string */
52
+ static fromUTF8String(value: string): BinaryArray {
53
+ return UTF8_ENCODER.encode(value);
54
+ }
55
+
56
+ /** Return utf8 string from bytes */
57
+ static toUTF8String(value: BinaryArray): string {
58
+ return UTF8_DECODER.decode(BinaryUtil.binaryArrayToUint8Array(value));
59
+ }
60
+
61
+ /** Convert utf8 value to base64 value string */
62
+ static utf8ToBase64(value: TextInput): string {
63
+ return this.toBase64String(typeof value === 'string' ? this.fromUTF8String(value) : value);
64
+ }
65
+
66
+ /** Convert base64 value to utf8 string */
67
+ static base64ToUTF8(value: TextInput): string {
68
+ const result = this.toUTF8String(typeof value === 'string' ? this.fromBase64String(value) : value);
69
+ return result;
70
+ }
71
+
72
+ /** Convert url encoded base64 value to utf8 string */
73
+ static urlEncodedBase64ToUTF8(value: TextInput): string {
74
+ const result = this.base64ToUTF8(value);
75
+ return result.startsWith('%') ? decodeURIComponent(result) : result;
76
+ }
77
+
78
+ /** Detect encoding of a binary type, if possible */
79
+ static detectEncoding(input: BinaryType): string | undefined {
80
+ if (input && typeof input === 'object' && 'readableEncoding' in input && typeof input.readableEncoding === 'string') {
81
+ return input.readableEncoding;
82
+ }
83
+ }
84
+
85
+ /** Consume lines */
86
+ static async readLines(stream: BinaryType, handler: (input: string) => unknown | Promise<unknown>): Promise<void> {
87
+ for await (const item of createInterface(BinaryUtil.toReadable(stream))) {
88
+ await handler(item);
89
+ }
90
+ }
91
+
92
+ /** Read chunk as utf8 if not a binary array */
93
+ static readUtf8Chunk(chunk: Any): BinaryArray {
94
+ return BinaryUtil.isBinaryArray(chunk) ? chunk : this.fromUTF8String(typeof chunk === 'string' ? chunk : `${chunk}`);
95
+ }
96
+
97
+ /** Read chunk, default to toString if type is unknown */
98
+ static readChunk(chunk: Any, encoding?: string | null): BinaryArray {
99
+ if (!encoding) {
100
+ return this.readUtf8Chunk(chunk);
101
+ }
102
+ return BinaryUtil.isBinaryArray(chunk) ? chunk :
103
+ Buffer.from(typeof chunk === 'string' ? chunk : `${chunk}`, castTo(encoding ?? 'utf8'));
104
+ }
105
+ }
package/src/console.ts CHANGED
@@ -79,7 +79,6 @@ class $ConsoleManager implements ConsoleListener {
79
79
  * Enable/disable enhanced debugging
80
80
  */
81
81
  enhanceDebug(active: boolean): void {
82
- Error.stackTraceLimit = active ? 50 : 10;
83
82
  if (active) {
84
83
  debug.formatArgs = function (args: string[]): void {
85
84
  args.unshift(this.namespace);
package/src/context.ts CHANGED
@@ -6,8 +6,8 @@ import { type ManifestIndex, type ManifestContext, ManifestModuleUtil } from '@t
6
6
  import { Env } from './env.ts';
7
7
  import { RuntimeIndex } from './manifest-index.ts';
8
8
  import { describeFunction } from './function.ts';
9
- import { JSONUtil } from './json.ts';
10
9
  import type { Role } from './trv';
10
+ import { JSONUtil } from './json.ts';
11
11
 
12
12
  /** Constrained version of {@type ManifestContext} */
13
13
  class $Runtime {
@@ -119,7 +119,7 @@ class $Runtime {
119
119
  throw new Error(`Unable to find ${location}, not in the manifest`);
120
120
  } else if (location.endsWith('.json')) {
121
121
  location = this.#idx.getFromImport(location)?.sourceFile ?? location;
122
- return fs.readFile(location, 'utf8').then(JSONUtil.parseSafe<T>);
122
+ return fs.readFile(location).then(JSONUtil.fromBinaryArray<T>);
123
123
  }
124
124
 
125
125
  if (!ManifestModuleUtil.SOURCE_EXT_REGEX.test(location)) {
package/src/error.ts CHANGED
@@ -9,70 +9,33 @@ export type ErrorCategory =
9
9
  'timeout' |
10
10
  'unavailable';
11
11
 
12
- export type AppErrorOptions<T> =
13
- ErrorOptions &
14
- {
15
- at?: Date | string | number;
16
- type?: string;
17
- category?: ErrorCategory;
18
- } &
19
- (T extends undefined ?
20
- { details?: T } :
21
- { details: T });
12
+ export type RuntimeErrorOptions<T> = Omit<Partial<RuntimeError>, 'details'> & (T extends undefined ? { details?: T } : { details: T });
22
13
 
23
14
  /**
24
15
  * Framework error class, with the aim of being extensible
25
16
  */
26
- export class AppError<T = Record<string, unknown> | undefined> extends Error {
17
+ export class RuntimeError<T = Record<string, unknown> | undefined> extends Error {
27
18
 
28
- static defaultCategory?: ErrorCategory;
29
-
30
- /** Convert from JSON object */
31
- static fromJSON(error: unknown): AppError | undefined {
32
- if (!!error && typeof error === 'object' &&
33
- ('message' in error && typeof error.message === 'string') &&
34
- ('category' in error && typeof error.category === 'string') &&
35
- ('type' in error && typeof error.type === 'string') &&
36
- ('at' in error && typeof error.at === 'string')
37
- ) {
38
- return new AppError(error.message, castTo<AppErrorOptions<Record<string, unknown>>>(error));
39
- }
40
- }
19
+ static defaultCategory: ErrorCategory = 'general';
41
20
 
42
21
  type: string;
43
22
  category: ErrorCategory;
44
- at: string;
23
+ at: Date;
45
24
  details: T;
46
25
 
47
26
  /**
48
- * Build an app error
27
+ * Build a runtime error
49
28
  *
50
29
  * @param message The error message
51
30
  */
52
31
  constructor(
53
32
  ...[message, options]:
54
- T extends undefined ? ([string] | [string, AppErrorOptions<T>]) : [string, AppErrorOptions<T>]
33
+ T extends undefined ? ([string] | [string, RuntimeErrorOptions<T>]) : [string, RuntimeErrorOptions<T>]
55
34
  ) {
56
35
  super(message, options?.cause ? { cause: options.cause } : undefined);
57
36
  this.type = options?.type ?? this.constructor.name;
58
37
  this.details = options?.details!;
59
- this.category = options?.category ?? castTo<typeof AppError>(this.constructor).defaultCategory ?? 'general';
60
- this.at = new Date(options?.at ?? Date.now()).toISOString();
61
- }
62
-
63
- /**
64
- * Serializes an error to a basic object
65
- */
66
- toJSON(): AppErrorOptions<T> & { message: string } {
67
- const options: AppErrorOptions<unknown> = {
68
- category: this.category,
69
- ...(this.cause ? { cause: `${this.cause}` } : undefined),
70
- type: this.type,
71
- at: this.at,
72
- ...(this.details ? { details: this.details } : undefined!),
73
- };
74
-
75
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
76
- return { message: this.message, ...options as AppErrorOptions<T> };
38
+ this.category = options?.category ?? castTo<typeof RuntimeError>(this.constructor).defaultCategory ?? 'general';
39
+ this.at = new Date(options?.at ?? Date.now());
77
40
  }
78
41
  }
package/src/exec.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  import { type ChildProcess, spawn, type SpawnOptions } from 'node:child_process';
2
- import type { Readable } from 'node:stream';
3
- import { createInterface } from 'node:readline/promises';
4
2
 
5
3
  import { castTo } from './types.ts';
6
4
  import { RuntimeIndex } from './manifest-index.ts';
5
+ import { BinaryUtil, type BinaryArray } from './binary.ts';
6
+ import { CodecUtil } from './codec.ts';
7
7
 
8
8
  const ResultSymbol = Symbol();
9
9
 
10
10
  /**
11
11
  * Result of an execution
12
12
  */
13
- export interface ExecutionResult<T extends string | Buffer = string | Buffer> {
13
+ export interface ExecutionResult<T extends string | BinaryArray = string | BinaryArray> {
14
14
  /**
15
15
  * Stdout
16
16
  */
@@ -50,12 +50,12 @@ export class ExecUtil {
50
50
  */
51
51
  static getResult(subProcess: ChildProcess): Promise<ExecutionResult<string>>;
52
52
  static getResult(subProcess: ChildProcess, options: { catch?: boolean, binary?: false }): Promise<ExecutionResult<string>>;
53
- static getResult(subProcess: ChildProcess, options: { catch?: boolean, binary: true }): Promise<ExecutionResult<Buffer>>;
54
- static getResult<T extends string | Buffer>(subProcess: ChildProcess, options: { catch?: boolean, binary?: boolean } = {}): Promise<ExecutionResult<T>> {
53
+ static getResult(subProcess: ChildProcess, options: { catch?: boolean, binary: true }): Promise<ExecutionResult<BinaryArray>>;
54
+ static getResult<T extends string | BinaryArray>(subProcess: ChildProcess, options: { catch?: boolean, binary?: boolean } = {}): Promise<ExecutionResult<T>> {
55
55
  const typed: ChildProcess & { [ResultSymbol]?: Promise<ExecutionResult> } = subProcess;
56
56
  const result = typed[ResultSymbol] ??= new Promise<ExecutionResult>(resolve => {
57
- const stdout: Buffer[] = [];
58
- const stderr: Buffer[] = [];
57
+ const stdout: BinaryArray[] = [];
58
+ const stderr: BinaryArray[] = [];
59
59
  let done = false;
60
60
  const finish = (finalResult: ExecutionBaseResult): void => {
61
61
  if (done) {
@@ -64,13 +64,13 @@ export class ExecUtil {
64
64
  done = true;
65
65
 
66
66
  const buffers = {
67
- stdout: Buffer.concat(stdout),
68
- stderr: Buffer.concat(stderr),
67
+ stdout: BinaryUtil.combineBinaryArrays(stdout),
68
+ stderr: BinaryUtil.combineBinaryArrays(stderr),
69
69
  };
70
70
 
71
71
  const final = {
72
- stdout: options.binary ? buffers.stdout : buffers.stdout.toString('utf8'),
73
- stderr: options.binary ? buffers.stderr : buffers.stderr.toString('utf8'),
72
+ stdout: options.binary ? buffers.stdout : buffers.stdout.toString(subProcess.stdout?.readableEncoding ?? 'utf8'),
73
+ stderr: options.binary ? buffers.stderr : buffers.stderr.toString(subProcess.stderr?.readableEncoding ?? 'utf8'),
74
74
  ...finalResult
75
75
  };
76
76
 
@@ -80,8 +80,8 @@ export class ExecUtil {
80
80
  );
81
81
  };
82
82
 
83
- subProcess.stdout?.on('data', (data: string | Buffer) => stdout.push(Buffer.isBuffer(data) ? data : Buffer.from(data)));
84
- subProcess.stderr?.on('data', (data: string | Buffer) => stderr.push(Buffer.isBuffer(data) ? data : Buffer.from(data)));
83
+ subProcess.stdout?.on('data', data => stdout.push(CodecUtil.readChunk(data, subProcess.stdout?.readableEncoding)));
84
+ subProcess.stderr?.on('data', data => stderr.push(CodecUtil.readChunk(data, subProcess.stderr?.readableEncoding)));
85
85
 
86
86
  subProcess.on('error', (error: Error) =>
87
87
  finish({ code: 1, message: error.message, valid: false }));
@@ -103,15 +103,6 @@ export class ExecUtil {
103
103
  }));
104
104
  }
105
105
 
106
- /**
107
- * Consume lines
108
- */
109
- static async readLines(stream: Readable, handler: (input: string) => unknown | Promise<unknown>): Promise<void> {
110
- for await (const item of createInterface(stream)) {
111
- await handler(item);
112
- }
113
- }
114
-
115
106
  /** Spawn a package command */
116
107
  static spawnPackageCommand(cmd: string, args: string[], config: SpawnOptions = {}): ChildProcess {
117
108
  return spawn(process.argv0, [RuntimeIndex.resolvePackageCommand(cmd), ...args], config);
@@ -1,10 +1,9 @@
1
1
  import { createReadStream } from 'node:fs';
2
- import type { Readable } from 'node:stream';
3
2
  import fs from 'node:fs/promises';
4
3
  import path from 'node:path';
5
4
 
6
- import { AppError } from './error.ts';
7
- import { JSONUtil } from './json.ts';
5
+ import { RuntimeError } from './error.ts';
6
+ import { BinaryUtil, type BinaryArray, type BinaryStream } from './binary.ts';
8
7
 
9
8
  /**
10
9
  * File loader that will search for files across the provided search paths
@@ -35,42 +34,42 @@ export class FileLoader {
35
34
  return resolved;
36
35
  }
37
36
  }
38
- throw new AppError(`Unable to find: ${relativePath}, searched=${this.searchPaths.join(',')}`, { category: 'notfound' });
37
+ throw new RuntimeError(`Unable to find: ${relativePath}, searched=${this.searchPaths.join(',')}`, { category: 'notfound' });
39
38
  }
40
39
 
41
40
  /**
42
- * Read a file, after resolving the path
41
+ * Read a file as utf8 text, after resolving the path
43
42
  * @param relativePath The path to read
44
43
  */
45
- async read(relativePath: string, binary?: false): Promise<string>;
46
- async read(relativePath: string, binary: true): Promise<Buffer>;
47
- async read(relativePath: string, binary = false): Promise<string | Buffer> {
44
+ async readText(relativePath: string): Promise<string> {
48
45
  const file = await this.resolve(relativePath);
49
- return fs.readFile(file, binary ? undefined : 'utf8');
46
+ return fs.readFile(file, 'utf8');
50
47
  }
51
48
 
52
49
  /**
53
- * Read a file as a stream
50
+ * Read a file as a byte array, after resolving the path
54
51
  * @param relativePath The path to read
55
52
  */
56
- async readStream(relativePath: string, binary = true): Promise<Readable> {
53
+ async readBinaryArray(relativePath: string): Promise<BinaryArray> {
57
54
  const file = await this.resolve(relativePath);
58
- return createReadStream(file, { encoding: binary ? undefined : 'utf8' });
55
+ return fs.readFile(file);
59
56
  }
60
57
 
61
58
  /**
62
- * Read a file as a File object
59
+ * Read a file as a stream
63
60
  * @param relativePath The path to read
64
61
  */
65
- async readFile(relativePath: string): Promise<File> {
66
- return new File([await this.read(relativePath, true)], path.basename(relativePath));
62
+ async readBinaryStream(relativePath: string): Promise<BinaryStream> {
63
+ const file = await this.resolve(relativePath);
64
+ return createReadStream(file);
67
65
  }
68
66
 
69
67
  /**
70
- * Read relative file as JSON
68
+ * Read a file as a File object
69
+ * @param relativePath The path to read
71
70
  */
72
- async readJSON<T>(relativePath: string): Promise<T> {
73
- const location = await this.resolve(relativePath);
74
- return JSONUtil.readFile<T>(location);
71
+ async readFile(relativePath: string): Promise<File> {
72
+ const buffer = BinaryUtil.binaryArrayToBuffer(await this.readBinaryArray(relativePath));
73
+ return new File([buffer], path.basename(relativePath));
75
74
  }
76
75
  }
package/src/function.ts CHANGED
@@ -17,8 +17,8 @@ const pending = new Set<Function>([]);
17
17
 
18
18
  /**
19
19
  * Initialize the meta data for a function/class
20
- * @param fn Class
21
- * @param `file` Filename
20
+ * @param input Class or function to register
21
+ * @param [module, relativePath] File location
22
22
  * @param `hash` Hash of class contents
23
23
  * @param `line` Line number in source
24
24
  * @param `methods` Methods and their hashes
@@ -26,48 +26,44 @@ const pending = new Set<Function>([]);
26
26
  * @private
27
27
  */
28
28
  export function registerFunction(
29
- fn: Function, [pkg, pth]: [string, string], tag: FunctionMetadataTag,
29
+ input: Function, [module, relativePath]: [string, string], tag: FunctionMetadataTag,
30
30
  methods?: Record<string, FunctionMetadataTag>, abstract?: boolean,
31
31
  ): void {
32
- const modulePath = ManifestModuleUtil.withoutSourceExtension(pth);
32
+ const modulePath = ManifestModuleUtil.withoutSourceExtension(relativePath);
33
33
 
34
34
  const metadata: FunctionMetadata = {
35
- id: (fn.name ? `${pkg}:${modulePath}#${fn.name}` : `${pkg}:${modulePath}`),
36
- import: `${pkg}/${pth}`,
37
- module: pkg,
35
+ id: (input.name ? `${module}:${modulePath}#${input.name}` : `${module}:${modulePath}`),
36
+ import: `${module}/${relativePath}`,
37
+ module,
38
38
  modulePath,
39
39
  ...tag,
40
40
  methods,
41
41
  abstract,
42
42
  class: methods !== undefined
43
43
  };
44
- pending.add(fn);
45
- Object.defineProperties(fn, { Ⲑid: { value: metadata.id }, [MetadataSymbol]: { value: metadata } });
44
+ pending.add(input);
45
+ Object.defineProperties(input, { Ⲑid: { value: metadata.id }, [MetadataSymbol]: { value: metadata } });
46
46
  }
47
47
 
48
48
  /**
49
49
  * Flush all pending function registers
50
50
  */
51
51
  export function flushPendingFunctions(): Function[] {
52
- const fns = [...pending];
52
+ const functions = [...pending];
53
53
  pending.clear();
54
- return fns;
54
+ return functions;
55
55
  }
56
56
 
57
57
  /**
58
58
  * Read metadata
59
59
  */
60
- export function describeFunction(fn: Function): FunctionMetadata;
61
- export function describeFunction(fn?: Function): FunctionMetadata | undefined {
62
- const _fn: (Function & { [MetadataSymbol]?: FunctionMetadata }) | undefined = fn;
63
- return _fn?.[MetadataSymbol];
60
+ export function describeFunction(input: Function): FunctionMetadata;
61
+ export function describeFunction(input?: Function): FunctionMetadata | undefined {
62
+ const resolved: (Function & { [MetadataSymbol]?: FunctionMetadata }) | undefined = input;
63
+ return resolved?.[MetadataSymbol];
64
64
  }
65
65
 
66
66
  const foreignTypeRegistry = new Map<string, Function>();
67
67
  export function foreignType(id: string): Function {
68
- if (!foreignTypeRegistry.has(id)) {
69
- const type = class { static Ⲑid = id; };
70
- foreignTypeRegistry.set(id, type);
71
- }
72
- return foreignTypeRegistry.get(id)!;
68
+ return foreignTypeRegistry.getOrInsert(id, class { static Ⲑid = id; });
73
69
  }
package/src/global.d.ts CHANGED
@@ -22,6 +22,16 @@ declare global {
22
22
  */
23
23
  interface File { }
24
24
 
25
+ /**
26
+ * @concrete node:stream/web#ReadableStream
27
+ */
28
+ interface ReadableStream { }
29
+
30
+ /**
31
+ * @concrete node:buffer#Buffer
32
+ */
33
+ interface Buffer { }
34
+
25
35
  namespace NodeJS {
26
36
  /**
27
37
  * @concrete node:stream#Readable
@@ -30,9 +40,28 @@ declare global {
30
40
  }
31
41
  }
32
42
 
43
+ declare module 'buffer' {
44
+ /**
45
+ * @concrete node:buffer#Blob
46
+ */
47
+ interface Blob { }
48
+
49
+ /**
50
+ * @concrete node:buffer#File
51
+ */
52
+ interface File { }
53
+ }
54
+
33
55
  declare module 'stream' {
34
56
  /**
35
57
  * @concrete node:stream#Readable
36
58
  */
37
59
  interface Readable { }
60
+ }
61
+
62
+ declare module 'stream/web' {
63
+ /**
64
+ * @concrete node:stream/web#ReadableStream
65
+ */
66
+ interface ReadableStream { }
38
67
  }