@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 +39 -38
- package/__index__.ts +3 -1
- package/package.json +5 -4
- package/src/binary-metadata.ts +174 -0
- package/src/binary.ts +162 -101
- package/src/codec.ts +105 -0
- package/src/console.ts +0 -1
- package/src/context.ts +2 -2
- package/src/error.ts +8 -45
- package/src/exec.ts +13 -22
- package/src/file-loader.ts +18 -19
- package/src/function.ts +16 -20
- package/src/global.d.ts +29 -0
- package/src/json.ts +132 -51
- package/src/queue.ts +5 -5
- package/src/shutdown.ts +1 -1
- package/src/time.ts +62 -76
- package/src/types.ts +10 -40
- package/src/util.ts +32 -4
- package/src/watch.ts +2 -2
- package/support/polyfill.js +9 -0
- package/support/transformer/metadata.ts +4 -4
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 [
|
|
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 [
|
|
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#
|
|
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#
|
|
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
|
-
##
|
|
257
|
-
The
|
|
258
|
-
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
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#
|
|
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#
|
|
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
|
-
*
|
|
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
|
|
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(
|
|
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(
|
|
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": "
|
|
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": "^
|
|
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": "^
|
|
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
|
|
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
|
|
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 {
|
|
12
|
-
import { Util } from './util.ts';
|
|
7
|
+
import { castTo, hasFunction, toConcrete } from './types.ts';
|
|
13
8
|
|
|
14
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
static
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
101
|
+
return input.arrayBuffer();
|
|
61
102
|
}
|
|
62
|
-
return hash.digest('hex').toString();
|
|
63
103
|
}
|
|
64
104
|
|
|
65
|
-
/**
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
*
|
|
181
|
+
* Convert an inbound binary type or factory into a synchronous binary type
|
|
120
182
|
*/
|
|
121
|
-
static
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
}
|