@travetto/runtime 7.1.4 → 8.0.0-alpha.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 CHANGED
@@ -19,6 +19,8 @@ Runtime is the foundation of all [Travetto](https://travetto.dev) applications.
19
19
  * Standard Error Support
20
20
  * Console Management
21
21
  * Resource Access
22
+ * Encoding and Decoding Utilities
23
+ * Binary Utilities
22
24
  * JSON Utilities
23
25
  * Common Utilities
24
26
  * Time Utilities
@@ -163,9 +165,9 @@ export class EnvProp<T> {
163
165
  ```
164
166
 
165
167
  ## Standard Error Support
166
- While the framework is 100 % compatible with standard `Error` instances, there are cases in which additional functionality is desired. Within the framework we use [AppError](https://github.com/travetto/travetto/tree/main/module/runtime/src/error.ts#L26) (or its derivatives) to represent framework errors. This class is available for use in your own projects. Some of the additional benefits of using this class is enhanced error reporting, as well as better integration with other modules (e.g. the [Web API](https://github.com/travetto/travetto/tree/main/module/web#readme "Declarative support for creating Web Applications") module and HTTP status codes).
168
+ While the framework is 100 % compatible with standard `Error` instances, there are cases in which additional functionality is desired. Within the framework we use [RuntimeError](https://github.com/travetto/travetto/tree/main/module/runtime/src/error.ts#L17) (or its derivatives) to represent framework errors. This class is available for use in your own projects. Some of the additional benefits of using this class is enhanced error reporting, as well as better integration with other modules (e.g. the [Web API](https://github.com/travetto/travetto/tree/main/module/web#readme "Declarative support for creating Web Applications") module and HTTP status codes).
167
169
 
168
- The [AppError](https://github.com/travetto/travetto/tree/main/module/runtime/src/error.ts#L26) takes in a message, and an optional payload and / or error classification. The currently supported error classifications are:
170
+ The [RuntimeError](https://github.com/travetto/travetto/tree/main/module/runtime/src/error.ts#L17) takes in a message, and an optional payload and / or error classification. The currently supported error classifications are:
169
171
  * `general` - General purpose errors
170
172
  * `system` - Synonym for `general`
171
173
  * `data` - Data format, content, etc are incorrect. Generally correlated to bad input.
@@ -249,25 +251,26 @@ $ DEBUG=express:*,@travetto/web npx trv run web
249
251
  ## Resource Access
250
252
  The primary access patterns for resources, is to directly request a file, and to resolve that file either via file-system look up or leveraging the [Manifest](https://github.com/travetto/travetto/tree/main/module/manifest#readme "Support for project indexing, manifesting, along with file watching")'s data for what resources were found at manifesting time.
251
253
 
252
- The [FileLoader](https://github.com/travetto/travetto/tree/main/module/runtime/src/file-loader.ts#L12) allows for accessing information about the resources, and subsequently reading the file as text/binary or to access the resource as a `Readable` stream. If a file is not found, it will throw an [AppError](https://github.com/travetto/travetto/tree/main/module/runtime/src/error.ts#L26) with a category of 'notfound'.
254
+ The [FileLoader](https://github.com/travetto/travetto/tree/main/module/runtime/src/file-loader.ts#L11) allows for accessing information about the resources, and subsequently reading the file as text/binary or to access the resource as a `Readable` stream. If a file is not found, it will throw an [RuntimeError](https://github.com/travetto/travetto/tree/main/module/runtime/src/error.ts#L17) with a category of 'notfound'.
253
255
 
254
- The [FileLoader](https://github.com/travetto/travetto/tree/main/module/runtime/src/file-loader.ts#L12) also supports tying itself to [Env](https://github.com/travetto/travetto/tree/main/module/runtime/src/env.ts#L114)'s `TRV_RESOURCES` information on where to attempt to find a requested resource.
256
+ 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#L114)'s `TRV_RESOURCES` information on where to attempt to find a requested resource.
255
257
 
256
- ## JSON Utilities
257
- The framework provides utilities for working with JSON data. This module provides methods for reading and writing JSON files, as well as serializing and deserializing JSON data. It also provides support for working with Base64 encoded data for web safe transfer. The primary goal is ease of use, but also a centralized location for performance and security improvements over time.
258
-
259
- * `parseSafe(input: string | Buffer)` parses JSON safely from a string or Buffer.
260
- * `stringifyBase64(value: any)` encodes a JSON value as a base64 encoded string.
261
- * `parseBase64(input: string)` decodes a JSON value from a base64 encoded string.
262
- * `readFile(file: string)` reads a JSON file asynchronously.
263
- * `readFileSync(file: string, onMissing?: any)` reads a JSON file synchronously.
258
+ ## Encoding and Decoding Utilities
259
+ The [CodecUtil](https://github.com/travetto/travetto/tree/main/module/runtime/src/codec.ts#L15) class provides a variety of static methods for encoding and decoding data. When working with JSON data, it also provide security checks to prevent prototype pollution. The utility supports the following formats:
260
+ * Hex
261
+ * Base64
262
+ * UTF8
263
+ * UTT8 Encoded JSON
264
+ * Base64 Encoded JSON
265
+ * New Line Delimited UTF8
264
266
 
265
267
  ## Common Utilities
266
- Common utilities used throughout the framework. Currently [Util](https://github.com/travetto/travetto/tree/main/module/runtime/src/util.ts#L11) includes:
268
+ Common utilities used throughout the framework. Currently [Util](https://github.com/travetto/travetto/tree/main/module/runtime/src/util.ts#L14) includes:
267
269
  * `uuid(len: number)` generates a simple uuid for use within the application.
268
270
  * `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 '!'.
269
271
  * `hash(text: string, size?: number)` produces a full sha512 hash.
270
272
  * `resolvablePromise()` produces a `Promise` instance with the `resolve` and `reject` methods attached to the instance. This is extremely useful for integrating promises into async iterations, or any other situation in which the promise creation and the execution flow don't always match up.
273
+ * `bufferedFileWrite(file:string, content: string)` will write the file, using a temporary buffer file to ensure that the entire file is written before being moved to the final location. This helps minimize file watch noise when writing files.
271
274
 
272
275
  **Code: Sample makeTemplate Usage**
273
276
  ```typescript
@@ -277,48 +280,46 @@ tpl`{{age:20}} {{name: 'bob'}}</>;
277
280
  '**age: 20** **name: bob**'
278
281
  ```
279
282
 
283
+ ## Binary Utilities
284
+ The [BinaryUtil](https://github.com/travetto/travetto/tree/main/module/runtime/src/binary.ts#L59) class provides a unified interface for working with binary data across different formats, especially bridging the gap between Node.js specific types (`Buffer`, `Stream`) and Web Standard types (`Blob`, `ArrayBuffer`). The framework leverages this to allow for seamless handling of binary data, regardless of the source.
285
+
286
+ ## JSON Utilities
287
+ The [JSONUtil](https://github.com/travetto/travetto/tree/main/module/runtime/src/json.ts#L31) class provides a comprehensive set of utilities for working with JSON data, including serialization, deserialization, encoding, and deep cloning capabilities. The utility handles special types like `Date`, `BigInt`, and `Error` objects seamlessly. Key features include:
288
+ * `fromUTF8(input, config?)` - Parse JSON from a UTF-8 string
289
+ * `toUTF8(value, config?)` - Serialize a value to JSON string
290
+ * `toUTF8Pretty(value)` - Serialize with pretty formatting (2-space indent)
291
+ * `fromBinaryArray(input)` - Parse JSON from binary array
292
+ * `toBinaryArray(value, config?)` - Serialize to binary array (UTF-8 encoded)
293
+ * `toBase64(value)` - Encode JSON as base64 string
294
+ * `fromBase64(input)` - Decode JSON from base64 string
295
+ * `clone(input, config?)` - Deep clone objects with optional transformations
296
+ * `cloneForTransmit(input)` - Clone for transmission with error serialization
297
+ * `cloneFromTransmit(input)` - Clone from transmission with type restoration
298
+
299
+ The `TRANSMIT_REVIVER` automatically restores `Date` objects and `BigInt` values during deserialization, making it ideal for transmitting complex data structures across network boundaries.
300
+
280
301
  ## Time Utilities
281
- [TimeUtil](https://github.com/travetto/travetto/tree/main/module/runtime/src/time.ts#L20) contains general helper methods, created to assist with time-based inputs via environment variables, command line interfaces, and other string-heavy based input.
302
+ [TimeUtil](https://github.com/travetto/travetto/tree/main/module/runtime/src/time.ts#L21) contains general helper methods, created to assist with time-based inputs via environment variables, command line interfaces, and other string-heavy based input.
282
303
 
283
304
  **Code: Time Utilities**
284
305
  ```typescript
285
306
  export class TimeUtil {
286
307
  /**
287
308
  * Test to see if a string is valid for relative time
288
- * @param val
289
309
  */
290
310
  static isTimeSpan(value: string): value is TimeSpan;
291
311
  /**
292
- * Returns time units convert to ms
293
- * @param amount Number of units to extend
294
- * @param unit Time unit to extend ('ms', 's', 'm', 'h', 'd', 'w', 'y')
295
- */
296
- static asMillis(amount: Date | number | TimeSpan, unit?: TimeUnit): number;
297
- /**
298
- * Returns the time converted to seconds
299
- * @param date The date to convert
300
- */
301
- static asSeconds(date: Date | number | TimeSpan, unit?: TimeUnit): number;
302
- /**
303
- * Returns the time converted to a Date
304
- * @param date The date to convert
305
- */
306
- static asDate(date: Date | number | TimeSpan, unit?: TimeUnit): Date;
307
- /**
308
- * Resolve time or span to possible time
312
+ * Exposes the ability to create a duration succinctly
309
313
  */
310
- static fromValue(value: Date | number | string | undefined): number | undefined;
314
+ static duration(input: TimeSpan | number | string, outputUnit: TimeUnit): number;
311
315
  /**
312
316
  * Returns a new date with `amount` units into the future
313
- * @param amount Number of units to extend
314
- * @param unit Time unit to extend ('ms', 's', 'm', 'h', 'd', 'w', 'y')
315
317
  */
316
- static fromNow(amount: number | TimeSpan, unit: TimeUnit = 'ms'): Date;
318
+ static fromNow(input: TimeSpan | number | string): Date;
317
319
  /**
318
320
  * Returns a pretty timestamp
319
- * @param time Time in milliseconds
320
321
  */
321
- static asClock(time: number): string;
322
+ static asClock(input: TimeSpan | number | string): string;
322
323
  }
323
324
  ```
324
325
 
package/__index__.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  import type { } from './src/global.d.ts';
2
2
  import type { } from './src/trv.d.ts';
3
3
  export * from './src/binary.ts';
4
+ export * from './src/binary-metadata.ts';
4
5
  export * from './src/console.ts';
5
6
  export * from './src/context.ts';
6
7
  export * from './src/debug.ts';
7
8
  export * from './src/error.ts';
8
9
  export * from './src/exec.ts';
10
+ export * from './src/codec.ts';
9
11
  export * from './src/env.ts';
10
12
  export * from './src/file-loader.ts';
11
13
  export * from './src/function.ts';
@@ -17,4 +19,4 @@ export * from './src/shutdown.ts';
17
19
  export * from './src/time.ts';
18
20
  export * from './src/types.ts';
19
21
  export * from './src/watch.ts';
20
- export * from './src/util.ts';
22
+ export * from './src/util.ts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/runtime",
3
- "version": "7.1.4",
3
+ "version": "8.0.0-alpha.0",
4
4
  "type": "module",
5
5
  "description": "Runtime for travetto applications.",
6
6
  "keywords": [
@@ -26,12 +26,13 @@
26
26
  "directory": "module/runtime"
27
27
  },
28
28
  "dependencies": {
29
- "@travetto/manifest": "^7.1.3",
29
+ "@travetto/manifest": "^8.0.0-alpha.0",
30
30
  "@types/debug": "^4.1.12",
31
- "debug": "^4.4.3"
31
+ "debug": "^4.4.3",
32
+ "temporal-polyfill-lite": "^0.2.2"
32
33
  },
33
34
  "peerDependencies": {
34
- "@travetto/transformer": "^7.1.3"
35
+ "@travetto/transformer": "^8.0.0-alpha.0"
35
36
  },
36
37
  "peerDependenciesMeta": {
37
38
  "@travetto/transformer": {
@@ -0,0 +1,174 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { createReadStream, ReadStream } from 'node:fs';
5
+
6
+ import { BinaryUtil, type BinaryArray, type BinaryContainer, type BinaryStream, type BinaryType } from './binary.ts';
7
+ import { RuntimeError } from './error.ts';
8
+ import { CodecUtil } from './codec.ts';
9
+
10
+ type BlobInput = BinaryType | (() => (BinaryType | Promise<BinaryType>));
11
+
12
+ /** Range of bytes, inclusive */
13
+ export type ByteRange = { start: number, end?: number };
14
+
15
+ export interface BinaryMetadata {
16
+ /** Size of binary data */
17
+ size?: number;
18
+ /** Mime type of the content */
19
+ contentType?: string;
20
+ /** Hash of binary data contents */
21
+ hash?: string;
22
+ /** The original base filename of the file */
23
+ filename?: string;
24
+ /** Filenames title, optional for elements like images, audio, videos */
25
+ title?: string;
26
+ /** Content encoding */
27
+ contentEncoding?: string;
28
+ /** Content language */
29
+ contentLanguage?: string;
30
+ /** Cache control */
31
+ cacheControl?: string;
32
+ /** Byte range for binary data */
33
+ range?: Required<ByteRange>;
34
+ }
35
+
36
+ type HashConfig = {
37
+ length?: number;
38
+ hashAlgorithm?: 'sha1' | 'sha256' | 'sha512' | 'md5';
39
+ outputEncoding?: crypto.BinaryToTextEncoding;
40
+ };
41
+
42
+ const BinaryMetaSymbol = Symbol();
43
+
44
+ export class BinaryMetadataUtil {
45
+ /** Set metadata for a binary type */
46
+ static write(input: BinaryType, metadata: BinaryMetadata): BinaryMetadata {
47
+ const withMeta: BinaryType & { [BinaryMetaSymbol]?: BinaryMetadata } = input;
48
+ return withMeta[BinaryMetaSymbol] = metadata;
49
+ }
50
+
51
+ /** Read metadata for a binary type, if available */
52
+ static read(input: BinaryType): BinaryMetadata {
53
+ const withMeta: BinaryType & { [BinaryMetaSymbol]?: BinaryMetadata } = input;
54
+ return withMeta[BinaryMetaSymbol] ?? {};
55
+ }
56
+
57
+ /** Generate a hash from an input value * @param input The seed value to build the hash from
58
+ * @param length The optional length of the hash to generate
59
+ * @param hashAlgorithm The hash algorithm to use
60
+ * @param outputEncoding The output encoding format
61
+ */
62
+ static hash(input: string | BinaryArray, config?: HashConfig): string;
63
+ static hash(input: BinaryStream | BinaryContainer, config?: HashConfig): Promise<string>;
64
+ static hash(input: string | BinaryType, config?: HashConfig): string | Promise<string> {
65
+ const hashAlgorithm = config?.hashAlgorithm ?? 'sha512';
66
+ const outputEncoding = config?.outputEncoding ?? 'hex';
67
+ const length = config?.length;
68
+ const hash = crypto.createHash(hashAlgorithm).setEncoding(outputEncoding);
69
+
70
+ if (typeof input === 'string') {
71
+ input = CodecUtil.fromUTF8String(input);
72
+ }
73
+
74
+ if (BinaryUtil.isBinaryArray(input)) {
75
+ hash.update(BinaryUtil.binaryArrayToUint8Array(input));
76
+ return hash.digest(outputEncoding).substring(0, length);
77
+ } else {
78
+ return BinaryUtil.pipeline(input, hash).then(() =>
79
+ hash.digest(outputEncoding).substring(0, length)
80
+ );
81
+ }
82
+ }
83
+
84
+ /** Compute the length of the binary data to be returned */
85
+ static readLength(metadata: BinaryMetadata): number | undefined {
86
+ return metadata.range ? (metadata.range.end - metadata.range.start + 1) : metadata.size;
87
+ }
88
+
89
+ /** Compute metadata for a given binary input */
90
+ static async compute(input: BlobInput, base: BinaryMetadata = {}): Promise<BinaryMetadata> {
91
+ if (typeof input === 'function') {
92
+ input = await input();
93
+ }
94
+ const metadata = { ...BinaryMetadataUtil.read(input), ...base };
95
+
96
+ if (BinaryUtil.isBinaryContainer(input)) {
97
+ metadata.size ??= input.size;
98
+ metadata.contentType ??= input.type;
99
+ if (input instanceof File) {
100
+ metadata.filename ??= input.name;
101
+ }
102
+ } else if (BinaryUtil.isBinaryArray(input)) {
103
+ metadata.size ??= input.byteLength;
104
+ metadata.hash ??= this.hash(input, { hashAlgorithm: 'sha256' });
105
+ } else if (input instanceof ReadStream) {
106
+ const location = input.path.toString();
107
+ metadata.filename ??= path.basename(location);
108
+ metadata.contentEncoding ??= input.readableEncoding!;
109
+ metadata.size ??= (await fs.stat(location)).size;
110
+ metadata.hash ??= await this.hash(createReadStream(location), { hashAlgorithm: 'sha256' });
111
+ } else if (input && typeof input === 'object' && 'readableEncoding' in input && typeof input.readableEncoding === 'string') {
112
+ metadata.contentEncoding ??= input.readableEncoding!;
113
+ }
114
+
115
+ return metadata;
116
+ }
117
+
118
+ /**
119
+ * Rewrite a blob to support metadata, and provide a dynamic input source
120
+ */
121
+ static defineBlob<T extends Blob>(target: T, input: BlobInput, metadata: BinaryMetadata = {}): typeof target {
122
+ const inputFn = async (): Promise<BinaryType> => typeof input === 'function' ? await input() : input;
123
+ this.write(target, metadata);
124
+
125
+ Object.defineProperties(target, {
126
+ size: { get() { return BinaryMetadataUtil.readLength(metadata); } },
127
+ type: { get() { return metadata.contentType; } },
128
+ name: { get() { return metadata.filename; } },
129
+ arrayBuffer: { value: () => inputFn().then(BinaryUtil.toArrayBuffer) },
130
+ stream: { value: () => BinaryUtil.toReadableStream(BinaryUtil.toSynchronous(input)) },
131
+ bytes: { value: () => inputFn().then(BinaryUtil.toBuffer) },
132
+ text: { value: () => inputFn().then(BinaryUtil.toBinaryArray).then(CodecUtil.toUTF8String) },
133
+ slice: {
134
+ value: (start?: number, end?: number, _contentType?: string) => {
135
+ const result = target instanceof File ? new File([], '') : new Blob([]);
136
+ return BinaryMetadataUtil.defineBlob(result,
137
+ () => inputFn().then(BinaryUtil.toBinaryArray).then(data => BinaryUtil.sliceByteArray(data, start, end)),
138
+ {
139
+ ...metadata,
140
+ range: { start: start ?? 0, end: end ?? metadata.size! - 1 },
141
+ }
142
+ );
143
+ }
144
+ }
145
+ });
146
+ return target;
147
+ }
148
+
149
+ /**
150
+ * Make a blob that contains the appropriate metadata
151
+ */
152
+ static makeBlob(source: BlobInput, metadata?: BinaryMetadata): Blob {
153
+ return this.defineBlob(new Blob([]), source, metadata);
154
+ }
155
+
156
+ /**
157
+ * Enforce byte range for stream stream/file of a certain size
158
+ */
159
+ static enforceRange(range: ByteRange, metadata?: BinaryMetadata): Required<ByteRange> {
160
+ if (!metadata || metadata.size === undefined) {
161
+ throw new RuntimeError('Cannot enforce range on data with unknown size', { category: 'data' });
162
+ }
163
+ const size = metadata.size;
164
+
165
+ // End is inclusive
166
+ const [start, end] = [range.start, Math.min(range.end ?? (size - 1), size - 1)];
167
+
168
+ if (Number.isNaN(start) || Number.isNaN(end) || !Number.isFinite(start) || start >= size || start < 0 || start > end) {
169
+ throw new RuntimeError('Invalid position, out of range', { category: 'data', details: { start, end, size } });
170
+ }
171
+
172
+ return { start, end };
173
+ }
174
+ }
package/src/binary.ts CHANGED
@@ -1,132 +1,193 @@
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 } from 'node:stream';
1
+ import { PassThrough, Readable, type Writable } from 'node:stream';
6
2
  import { pipeline } from 'node:stream/promises';
7
3
  import { ReadableStream } from 'node:stream/web';
8
- import { text as toText, arrayBuffer as toArrayBuffer } from 'node:stream/consumers';
9
- import { isArrayBuffer } from 'node:util/types';
4
+ import consumers from 'node:stream/consumers';
5
+ import { isArrayBuffer, isPromise, isTypedArray, isUint16Array, isUint32Array, isUint8Array, isUint8ClampedArray } from 'node:util/types';
10
6
 
11
- import { type BinaryInput, type BlobMeta, hasFunction } from './types.ts';
12
- import { Util } from './util.ts';
7
+ import { castTo, hasFunction, toConcrete } from './types.ts';
13
8
 
14
- const BlobMetaSymbol = Symbol();
9
+ /**
10
+ * Binary Array
11
+ * @concrete
12
+ */
13
+ export type BinaryArray = Uint32Array | Uint16Array | Uint8Array | Uint8ClampedArray | Buffer<ArrayBuffer> | ArrayBuffer;
14
+ /**
15
+ * Binary Stream
16
+ * @concrete
17
+ */
18
+ export type BinaryStream = Readable | ReadableStream | AsyncIterable<BinaryArray>;
19
+ /**
20
+ * Binary Container
21
+ * @concrete
22
+ */
23
+ export type BinaryContainer = Blob | File;
24
+ /**
25
+ * Binary Type
26
+ * @concrete
27
+ */
28
+ export type BinaryType = BinaryArray | BinaryStream | BinaryContainer;
29
+
30
+ const BINARY_CONSTRUCTOR_SET = new Set<unknown>([
31
+ Readable, Buffer, Blob, ReadableStream, ArrayBuffer, Uint8Array,
32
+ Uint16Array, Uint32Array, Uint8ClampedArray, File
33
+ ]);
34
+
35
+ let BINARY_REFS: Set<unknown> | undefined;
36
+ const isBinaryTypeReference = (value: unknown): boolean =>
37
+ BINARY_CONSTRUCTOR_SET.has(value) ||
38
+ BINARY_CONSTRUCTOR_SET.has(Object.getPrototypeOf(value)) ||
39
+ (BINARY_REFS ||= new Set<unknown>([
40
+ toConcrete<BinaryType>(),
41
+ toConcrete<BinaryStream>(),
42
+ toConcrete<BinaryArray>(),
43
+ toConcrete<BinaryContainer>(),
44
+ ])).has(value);
45
+
46
+ const isReadable = hasFunction<Readable>('pipe');
47
+ const isReadableStream = hasFunction<ReadableStream>('pipeTo');
48
+ const isAsyncIterable = (value: unknown): value is AsyncIterable<unknown> =>
49
+ !!value && (typeof value === 'object' || typeof value === 'function') && Symbol.asyncIterator in value;
50
+ const isBinaryArray = (value: unknown): value is BinaryArray =>
51
+ isUint8Array(value) || isArrayBuffer(value) || isUint16Array(value) || isUint32Array(value) || isUint8ClampedArray(value);
52
+ const isBinaryStream = (value: unknown): value is BinaryStream => isReadable(value) || isReadableStream(value) || isAsyncIterable(value);
53
+ const isBinaryContainer = (value: unknown): value is BinaryContainer => value instanceof Blob;
54
+ const isBinaryType = (value: unknown): value is BinaryType => !!value && (isBinaryArray(value) || isBinaryStream(value) || isBinaryContainer(value));
15
55
 
16
56
  /**
17
57
  * Common functions for dealing with binary data/streams
18
58
  */
19
59
  export class BinaryUtil {
20
- /** Is Array Buffer */
21
- static isArrayBuffer = isArrayBuffer;
22
- /** Is Readable */
23
- static isReadable = hasFunction<Readable>('pipe');
24
- /** Is ReadableStream */
25
- static isReadableStream = hasFunction<ReadableStream>('pipeTo');
26
- /** Is Async Iterable */
27
- static isAsyncIterable = (value: unknown): value is AsyncIterable<unknown> =>
28
- !!value && (typeof value === 'object' || typeof value === 'function') && Symbol.asyncIterator in value;
29
60
 
30
- /**
31
- * Is value a binary type
32
- */
33
- static isBinaryType(value: unknown): boolean {
34
- return value instanceof Blob || Buffer.isBuffer(value) || this.isReadable(value) ||
35
- this.isArrayBuffer(value) || this.isReadableStream(value) || this.isAsyncIterable(value);
61
+ /** Is the input a byte array */
62
+ static isBinaryArray = isBinaryArray;
63
+ /** Is the input a byte stream */
64
+ static isBinaryStream = isBinaryStream;
65
+ /** Is the input a binary container */
66
+ static isBinaryContainer = isBinaryContainer;
67
+ /** Is value a binary type */
68
+ static isBinaryType = isBinaryType;
69
+ /** Is a binary reference */
70
+ static isBinaryTypeReference = isBinaryTypeReference;
71
+
72
+ /** Convert binary array to an explicit buffer */
73
+ static binaryArrayToBuffer(input: BinaryArray): Buffer<ArrayBuffer> {
74
+ if (Buffer.isBuffer(input)) {
75
+ return castTo(input);
76
+ } else if (isTypedArray(input)) {
77
+ return castTo(Buffer.from(input.buffer));
78
+ } else {
79
+ return Buffer.from(input);
80
+ }
36
81
  }
37
82
 
38
- /**
39
- * Generate a proper sha512 hash from an input value
40
- * @param input The seed value to build the hash from
41
- * @param length The optional length of the hash to generate
42
- */
43
- static hash(input: string, length: number = -1): string {
44
- const hash = crypto.createHash('sha512');
45
- hash.update(input);
46
- const digest = hash.digest('hex');
47
- return length > 0 ? digest.substring(0, length) : digest;
83
+ /** Convert binary array to an explicit uint8array */
84
+ static binaryArrayToUint8Array(input: BinaryArray): Uint8Array {
85
+ if (isUint8Array(input)) {
86
+ return castTo(input);
87
+ } else if (isTypedArray(input)) {
88
+ return castTo(Buffer.from(input.buffer));
89
+ } else {
90
+ return Buffer.from(input);
91
+ }
48
92
  }
49
93
 
50
- /**
51
- * Compute hash from an input blob, buffer or readable stream.
52
- */
53
- static async hashInput(input: BinaryInput): Promise<string> {
54
- const hash = crypto.createHash('sha256').setEncoding('hex');
55
- if (Buffer.isBuffer(input)) {
56
- hash.write(input);
57
- } else if (input instanceof Blob) {
58
- await pipeline(Readable.fromWeb(input.stream()), hash);
94
+ /** Convert input to a binary array */
95
+ static async toBinaryArray(input: BinaryType): Promise<BinaryArray> {
96
+ if (isBinaryArray(input)) {
97
+ return input;
98
+ } else if (isBinaryStream(input)) {
99
+ return consumers.buffer(input);
59
100
  } else {
60
- await pipeline(input, hash);
101
+ return input.arrayBuffer();
61
102
  }
62
- return hash.digest('hex').toString();
63
103
  }
64
104
 
65
- /**
66
- * Write file and copy over when ready
67
- */
68
- static async bufferedFileWrite(file: string, content: string, checkHash = false): Promise<void> {
69
- if (checkHash) {
70
- const current = await fs.readFile(file, 'utf8').catch(() => '');
71
- if (this.hash(current) === this.hash(content)) {
72
- return;
73
- }
105
+ /** Convert input to a buffer */
106
+ static async toBuffer(input: BinaryType): Promise<Buffer<ArrayBuffer>> {
107
+ const bytes = await BinaryUtil.toBinaryArray(input);
108
+ return BinaryUtil.binaryArrayToBuffer(bytes);
109
+ }
110
+
111
+ /** Convert input to an ArrayBuffer */
112
+ static async toArrayBuffer(input: BinaryType): Promise<ArrayBuffer> {
113
+ const data = await BinaryUtil.toBuffer(input);
114
+ return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
115
+ }
116
+
117
+ /** Convert input to a readable stream */
118
+ static toReadable(input: BinaryType): Readable {
119
+ if (isReadable(input)) {
120
+ return input;
121
+ } else if (isBinaryArray(input)) {
122
+ return Readable.from(BinaryUtil.binaryArrayToBuffer(input));
123
+ } else if (isBinaryContainer(input)) {
124
+ return Readable.fromWeb(input.stream());
125
+ } else if (isReadableStream(input)) {
126
+ return Readable.fromWeb(input);
127
+ } else {
128
+ return Readable.from(input);
74
129
  }
130
+ }
75
131
 
76
- const temp = path.resolve(os.tmpdir(), `${process.hrtime()[1]}.${path.basename(file)}`);
77
- await fs.writeFile(temp, content, 'utf8');
78
- await fs.mkdir(path.dirname(file), { recursive: true });
79
- await fs.copyFile(temp, file);
80
- await fs.rm(temp, { force: true });
132
+ /** Convert input to a binary ReadableStream */
133
+ static toReadableStream(input: BinaryType): ReadableStream {
134
+ if (isReadableStream(input)) {
135
+ return input;
136
+ } else if (isReadable(input)) {
137
+ return Readable.toWeb(input);
138
+ } else if (isBinaryContainer(input)) {
139
+ return input.stream();
140
+ } else {
141
+ return Readable.toWeb(BinaryUtil.toReadable(input));
142
+ }
81
143
  }
82
144
 
83
- /**
84
- * Make a blob, and assign metadata
85
- */
86
- static readableBlob(input: () => (Readable | Promise<Readable>), metadata: Omit<BlobMeta, 'filename'> & { filename: string }): File;
87
- static readableBlob(input: () => (Readable | Promise<Readable>), metadata?: BlobMeta): Blob;
88
- static readableBlob(input: () => (Readable | Promise<Readable>), metadata: BlobMeta = {}): Blob | File {
89
- const go = (): Readable => {
90
- const stream = new PassThrough();
91
- Promise.resolve(input()).then(readable => readable.pipe(stream), (error) => stream.destroy(error));
92
- return stream;
93
- };
94
-
95
- const size = metadata.range ? (metadata.range.end - metadata.range.start) + 1 : metadata.size;
96
- const out: Blob = metadata.filename ?
97
- new File([], path.basename(metadata.filename), { type: metadata.contentType }) :
98
- new Blob([], { type: metadata.contentType });
99
-
100
- return Object.defineProperties(out, {
101
- size: { value: size },
102
- stream: { value: () => ReadableStream.from(go()) },
103
- arrayBuffer: { value: () => toArrayBuffer(go()) },
104
- text: { value: () => toText(go()) },
105
- bytes: { value: () => toArrayBuffer(go()).then(buffer => new Uint8Array(buffer)) },
106
- [BlobMetaSymbol]: { value: metadata }
107
- });
145
+ /** Convert input to a binary stream */
146
+ static toBinaryStream(input: BinaryType): BinaryStream {
147
+ if (isBinaryStream(input)) {
148
+ return input;
149
+ } else {
150
+ return BinaryUtil.toReadableStream(input);
151
+ }
108
152
  }
109
153
 
110
- /**
111
- * Get blob metadata
112
- */
113
- static getBlobMeta(blob: Blob): BlobMeta | undefined {
114
- const withMeta: Blob & { [BlobMetaSymbol]?: BlobMeta } = blob;
115
- return withMeta[BlobMetaSymbol];
154
+ /** Combine binary arrays */
155
+ static combineBinaryArrays(arrays: BinaryArray[]): BinaryArray {
156
+ return Buffer.concat(arrays.map(x => BinaryUtil.binaryArrayToBuffer(x)));
157
+ }
158
+
159
+ /** Agnostic slice of binary array */
160
+ static sliceByteArray(input: BinaryArray, start?: number, end?: number): BinaryArray {
161
+ if (Buffer.isBuffer(input)) {
162
+ return input.subarray(start, end);
163
+ } else if (isArrayBuffer(input)) {
164
+ return input.slice(start, end);
165
+ } else {
166
+ return input.slice(start, end);
167
+ }
168
+ }
169
+
170
+ /** Consume input into output */
171
+ static pipeline(input: BinaryType, output: Writable): Promise<void> {
172
+ return pipeline(BinaryUtil.toBinaryStream(input), output);
173
+ }
174
+
175
+ /** Create a binary array of specified size, optionally filled with a value */
176
+ static makeBinaryArray(size: number, fill?: string | number): BinaryArray {
177
+ return Buffer.alloc(size, fill);
116
178
  }
117
179
 
118
180
  /**
119
- * Get a hashed location/path for a blob
181
+ * Convert an inbound binary type or factory into a synchronous binary type
120
182
  */
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('')];
183
+ static toSynchronous(input: BinaryType | (() => (BinaryType | Promise<BinaryType>))): BinaryType {
184
+ const value = (typeof input === 'function') ? input() : input;
185
+ if (isPromise(value)) {
186
+ const stream = new PassThrough();
187
+ value.then(result => BinaryUtil.pipeline(result, stream)).catch(error => stream.destroy(error));
188
+ return stream;
189
+ } else {
190
+ return value;
127
191
  }
128
-
129
- const ext = path.extname(meta.filename ?? '') || '.bin';
130
- return `${parts.join('/')}${ext}`;
131
192
  }
132
193
  }