Add comprehensive installation and setup documentation

- Add GETTING_STARTED.md with quick start guide and development modes
- Add INSTALL.sh automated installation script
- Add INSTALLATION_CHECKLIST.md, INSTALLATION_SUCCESS.md, and INSTALLATION_SUMMARY.md
- Add QUICK_REFERENCE.md for common commands
- Add SETUP_GUIDE.md with detailed setup instructions
- Update README.md with improved project overview
- Add did-wallet app dependencies and node_modules
This commit is contained in:
Dorian
2026-01-27 17:18:21 +00:00
parent a81f655133
commit 0d073fa89e
22658 changed files with 4494151 additions and 6 deletions
+2
View File
@@ -0,0 +1,2 @@
import TTLCache from '@isaacs/ttlcache';
export { TTLCache as TtlCache };
+444
View File
@@ -0,0 +1,444 @@
import type { Multibase } from 'multiformats';
import { base32z } from 'multiformats/bases/base32';
import { base58btc } from 'multiformats/bases/base58';
import { base64url } from 'multiformats/bases/base64';
import { isAsyncIterable, isArrayBufferSlice, universalTypeOf } from './type-utils.js';
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
export class Convert {
data: any;
format: string;
constructor(data: any, format: string) {
this.data = data;
this.format = format;
}
static arrayBuffer(data: ArrayBuffer): Convert {
return new Convert(data, 'ArrayBuffer');
}
static asyncIterable(data: AsyncIterable<any>): Convert {
if (!isAsyncIterable(data)) {
throw new TypeError('Input must be of type AsyncIterable.');
}
return new Convert(data, 'AsyncIterable');
}
static base32Z(data: string): Convert {
return new Convert(data, 'Base32Z');
}
static base58Btc(data: string): Convert {
return new Convert(data, 'Base58Btc');
}
static base64Url(data: string): Convert {
return new Convert(data, 'Base64Url');
}
/**
* Reference:
* The BufferSource type is a TypeScript type that represents an ArrayBuffer
* or one of the ArrayBufferView types, such a TypedArray (e.g., Uint8Array)
* or a DataView.
*/
static bufferSource(data: BufferSource): Convert {
return new Convert(data, 'BufferSource');
}
static hex(data: string): Convert {
if (typeof data !== 'string') {
throw new TypeError('Hex input must be a string.');
}
if (data.length % 2 !== 0) {
throw new TypeError('Hex input must have an even number of characters.');
}
return new Convert(data, 'Hex');
}
static multibase(data: string): Convert {
return new Convert(data, 'Multibase');
}
static object(data: Record<string, any>): Convert {
return new Convert(data, 'Object');
}
static string(data: string): Convert {
return new Convert(data, 'String');
}
static uint8Array(data: Uint8Array): Convert {
return new Convert(data, 'Uint8Array');
}
toArrayBuffer(): ArrayBuffer {
switch (this.format) {
case 'Base58Btc': {
return base58btc.baseDecode(this.data).buffer;
}
case 'Base64Url': {
return base64url.baseDecode(this.data).buffer;
}
case 'BufferSource': {
const dataType = universalTypeOf(this.data);
if (dataType === 'ArrayBuffer') {
// Data is already an ArrayBuffer, No conversion is necessary.
return this.data;
} else if (ArrayBuffer.isView(this.data)) {
// Data is a DataView or a different TypedArray (e.g., Uint16Array).
if (isArrayBufferSlice(this.data)) {
// Data is a slice of an ArrayBuffer. Return a new ArrayBuffer or ArrayBufferView of the same slice.
return this.data.buffer.slice(this.data.byteOffset, this.data.byteOffset + this.data.byteLength);
} else {
// Data is a whole ArrayBuffer viewed as a different TypedArray or DataView. Return the whole ArrayBuffer.
return this.data.buffer;
}
} else {
throw new TypeError(`${this.format} value is not of type: ArrayBuffer, DataView, or TypedArray.`);
}
}
case 'Hex': {
return this.toUint8Array().buffer;
}
case 'String': {
return this.toUint8Array().buffer;
}
case 'Uint8Array': {
return this.data.buffer;
}
default:
throw new TypeError(`Conversion from ${this.format} to ArrayBuffer is not supported.`);
}
}
async toArrayBufferAsync(): Promise<ArrayBuffer> {
switch (this.format) {
case 'AsyncIterable': {
const blob = await this.toBlobAsync();
return await blob.arrayBuffer();
}
default:
throw new TypeError(`Asynchronous conversion from ${this.format} to ArrayBuffer is not supported.`);
}
}
toBase32Z(): string {
switch (this.format) {
case 'Uint8Array': {
return base32z.baseEncode(this.data);
}
default:
throw new TypeError(`Conversion from ${this.format} to Base64Z is not supported.`);
}
}
toBase58Btc(): string {
switch (this.format) {
case 'ArrayBuffer': {
const u8a = new Uint8Array(this.data);
return base58btc.baseEncode(u8a);
}
case 'Multibase': {
return this.data.substring(1);
}
case 'Uint8Array': {
return base58btc.baseEncode(this.data);
}
default:
throw new TypeError(`Conversion from ${this.format} to Base58Btc is not supported.`);
}
}
toBase64Url(): string {
switch (this.format) {
case 'ArrayBuffer': {
const u8a = new Uint8Array(this.data);
return base64url.baseEncode(u8a);
}
case 'BufferSource': {
const u8a = this.toUint8Array();
return base64url.baseEncode(u8a);
}
case 'Object': {
const string = JSON.stringify(this.data);
const u8a = textEncoder.encode(string);
return base64url.baseEncode(u8a);
}
case 'String': {
const u8a = textEncoder.encode(this.data);
return base64url.baseEncode(u8a);
}
case 'Uint8Array': {
return base64url.baseEncode(this.data);
}
default:
throw new TypeError(`Conversion from ${this.format} to Base64Url is not supported.`);
}
}
async toBlobAsync(): Promise<Blob> {
switch (this.format) {
case 'AsyncIterable': {
// Initialize an array to hold the chunks from the AsyncIterable.
const chunks = [];
// Asynchronously iterate over each chunk in the AsyncIterable.
for await (const chunk of (this.data as AsyncIterable<any>)) {
// Append each chunk to the chunks array. These chunks can be of any type, typically binary data or text.
chunks.push(chunk);
}
// Create a new Blob from the aggregated chunks.
// The Blob constructor combines these chunks into a single Blob object.
const blob = new Blob(chunks);
return blob;
}
default:
throw new TypeError(`Asynchronous conversion from ${this.format} to Blob is not supported.`);
}
}
toHex(): string {
// pre-calculating Hex values improves runtime by 6-10x.
const hexes = Array.from({ length: 256 }, (v, i) => i.toString(16).padStart(2, '0'));
switch (this.format) {
case 'ArrayBuffer': {
const u8a = this.toUint8Array();
return Convert.uint8Array(u8a).toHex();
}
case 'Base64Url': {
const u8a = this.toUint8Array();
return Convert.uint8Array(u8a).toHex();
}
case 'Uint8Array': {
let hex = '';
for (let i = 0; i < this.data.length; i++) {
hex += hexes[this.data[i]];
}
return hex;
}
default:
throw new TypeError(`Conversion from ${this.format} to Hex is not supported.`);
}
}
toMultibase(): Multibase<any> {
switch (this.format) {
case 'Base58Btc': {
return `z${this.data}`;
}
default:
throw new TypeError(`Conversion from ${this.format} to Multibase is not supported.`);
}
}
toObject(): object {
switch (this.format) {
case 'Base64Url': {
const u8a = base64url.baseDecode(this.data);
const text = textDecoder.decode(u8a);
return JSON.parse(text);
}
case 'String': {
return JSON.parse(this.data);
}
case 'Uint8Array': {
const text = textDecoder.decode(this.data);
return JSON.parse(text);
}
default:
throw new TypeError(`Conversion from ${this.format} to Object is not supported.`);
}
}
async toObjectAsync(): Promise<any> {
switch (this.format) {
case 'AsyncIterable': {
// Convert the AsyncIterable to a String.
const text = await this.toStringAsync();
// Parse the string as JSON. This step assumes that the string represents a valid JSON structure.
// JSON.parse() will convert the string into a corresponding JavaScript object.
const json = JSON.parse(text);
// Return the parsed JavaScript object. The type of this object will depend on the structure
// of the JSON in the stream. It could be an object, array, string, number, etc.
return json;
}
default:
throw new TypeError(`Asynchronous conversion from ${this.format} to Object is not supported.`);
}
}
toString(): string {
switch (this.format) {
case 'ArrayBuffer': {
return textDecoder.decode(this.data);
}
case 'Base64Url': {
const u8a = base64url.baseDecode(this.data);
return textDecoder.decode(u8a);
}
case 'Object': {
return JSON.stringify(this.data);
}
case 'Uint8Array': {
return textDecoder.decode(this.data);
}
default:
throw new TypeError(`Conversion from ${this.format} to String is not supported.`);
}
}
async toStringAsync(): Promise<string> {
switch (this.format) {
case 'AsyncIterable': {
// Initialize an empty string to accumulate the decoded text.
let str = '';
// Iterate over the chunks from the AsyncIterable.
for await (const chunk of (this.data as AsyncIterable<any>)) {
// If the chunk is already a string, concatenate it directly.
if (typeof chunk === 'string')
str += chunk;
else
// If the chunk is a Uint8Array or similar, use the decoder to convert it to a string.
// The `stream: true` option lets the decoder handle multi-byte characters spanning
// multiple chunks.
str += textDecoder.decode(chunk, { stream: true });
}
// Finalize the decoding process to handle any remaining bytes and signal the end of the stream.
// The `stream: false` option flushes the decoder's internal state.
str += textDecoder.decode(undefined, { stream: false });
// Return the accumulated string.
return str;
}
default:
throw new TypeError(`Asynchronous conversion from ${this.format} to String is not supported.`);
}
}
toUint8Array(): Uint8Array {
switch (this.format) {
case 'ArrayBuffer': {
// Çreate Uint8Array as a view on the ArrayBuffer.
// Note: The Uint8Array shares the same memory as the ArrayBuffer, so this operation is very efficient.
return new Uint8Array(this.data);
}
case 'Base32Z': {
return base32z.baseDecode(this.data);
}
case 'Base58Btc': {
return base58btc.baseDecode(this.data);
}
case 'Base64Url': {
return base64url.baseDecode(this.data);
}
case 'BufferSource': {
const dataType = universalTypeOf(this.data);
if (dataType === 'Uint8Array') {
// Data is already a Uint8Array. No conversion is necessary.
// Note: Uint8Array is a type of BufferSource.
return this.data;
} else if (dataType === 'ArrayBuffer') {
// Data is an ArrayBuffer, create Uint8Array as a view on the ArrayBuffer.
// Note: The Uint8Array shares the same memory as the ArrayBuffer, so this operation is very efficient.
return new Uint8Array(this.data);
} else if (ArrayBuffer.isView(this.data)) {
// Data is a DataView or a different TypedArray (e.g., Uint16Array).
return new Uint8Array(this.data.buffer, this.data.byteOffset, this.data.byteLength);
} else {
throw new TypeError(`${this.format} value is not of type: ArrayBuffer, DataView, or TypedArray.`);
}
}
case 'Hex': {
const u8a = new Uint8Array(this.data.length / 2);
for (let i = 0; i < this.data.length; i += 2) {
const byteValue = parseInt(this.data.substring(i, i + 2), 16);
if (isNaN(byteValue)) {
throw new TypeError('Input is not a valid hexadecimal string.');
}
u8a[i / 2] = byteValue;
}
return u8a;
}
case 'Object': {
const string = JSON.stringify(this.data);
return textEncoder.encode(string);
}
case 'String': {
return textEncoder.encode(this.data);
}
default:
throw new TypeError(`Conversion from ${this.format} to Uint8Array is not supported.`);
}
}
async toUint8ArrayAsync(): Promise<Uint8Array> {
switch (this.format) {
case 'AsyncIterable': {
const arrayBuffer = await this.toArrayBufferAsync();
return new Uint8Array(arrayBuffer);
}
default:
throw new TypeError(`Asynchronous conversion from ${this.format} to Uint8Array is not supported.`);
}
}
}
+10
View File
@@ -0,0 +1,10 @@
export type * from './types.js';
export * from './cache.js';
export * from './convert.js';
export * from './multicodec.js';
export * from './object.js';
export * from './stores.js';
export * from './stream.js';
export * from './stream-node.js';
export * from './type-utils.js';
+176
View File
@@ -0,0 +1,176 @@
import { varint } from 'multiformats';
export type MulticodecCode = number;
export type MulticodecDefinition<MulticodecCode> = {
code: MulticodecCode;
// codeBytes: Uint8Array;
name: string;
}
/**
* The `Multicodec` class provides an interface to prepend binary data
* with a prefix that identifies the data that follows.
* https://github.com/multiformats/multicodec/blob/master/table.csv
*
* Multicodec is a self-describing multiformat, it wraps other formats with
* a tiny bit of self-description. A multicodec identifier is a
* varint (variable integer) that indicates the format of the data.
*
* The canonical table of multicodecs can be access at the following URL:
* https://github.com/multiformats/multicodec/blob/master/table.csv
*
* Example usage:
*
* ```ts
* Multicodec.registerCodec({ code: 0xed, name: 'ed25519-pub' });
* const prefixedData = Multicodec.addPrefix({ code: 0xed, data: new Uint8Array(32) });
* ```
*/
export class Multicodec {
/**
* A static field containing a map of codec codes to their corresponding names.
*/
static codeToName = new Map<MulticodecCode, string>();
/**
* A static field containing a map of codec names to their corresponding codes.
*/
static nameToCode = new Map<string, MulticodecCode>();
/**
* Adds a multicodec prefix to input data.
*
* @param options - The options for adding a prefix.
* @param options.code - The codec code. Either the code or name must be provided.
* @param options.name - The codec name. Either the code or name must be provided.
* @param options.data - The data to be prefixed.
* @returns The data with the added prefix as a Uint8Array.
*/
public static addPrefix(options: {
code?: MulticodecCode,
data: Uint8Array,
name?: string,
}): Uint8Array {
let { code, data, name } = options;
if (!(name ? !code : code)) {
throw new Error(`Either 'name' or 'code' must be defined, but not both.`);
}
// If code was given, confirm it exists, or lookup code by name.
code = Multicodec.codeToName.has(code!) ? code : Multicodec.nameToCode.get(name!);
// Throw error if a registered Codec wasn't found.
if (code === undefined) {
throw new Error(`Unsupported multicodec: ${options.name ?? options.code}`);
}
// Create a new array to store the prefix and input data.
const prefixLength = varint.encodingLength(code);
const dataWithPrefix = new Uint8Array(prefixLength + data.byteLength);
dataWithPrefix.set(data, prefixLength);
// Prepend the prefix.
varint.encodeTo(code, dataWithPrefix);
return dataWithPrefix;
}
/**
* Get the Multicodec code from given prefixed data.
*
* @param options - The options for getting the codec code.
* @param options.prefixedData - The data to extract the codec code from.
* @returns - The Multicodec code as a number.
*/
public static getCodeFromData(options: {
prefixedData: Uint8Array
}): MulticodecCode {
const { prefixedData } = options;
const [code, _] = varint.decode(prefixedData);
return code;
}
/**
* Get the Multicodec code from given Multicodec name.
*
* @param options - The options for getting the codec code.
* @param options.name - The name to lookup.
* @returns - The Multicodec code as a number.
*/
public static getCodeFromName(options: {
name: string
}): MulticodecCode {
const { name } = options;
// Throw error if a registered Codec wasn't found.
const code = Multicodec.nameToCode.get(name);
if (code === undefined) {
throw new Error(`Unsupported multicodec: ${name}`);
}
return code;
}
/**
* Get the Multicodec name from given Multicodec code.
*
* @param options - The options for getting the codec name.
* @param options.name - The code to lookup.
* @returns - The Multicodec name as a string.
*/
public static getNameFromCode(options: {
code: MulticodecCode
}): string {
const { code } = options;
// Throw error if a registered Codec wasn't found.
const name = Multicodec.codeToName.get(code);
if (name === undefined) {
throw new Error(`Unsupported multicodec: ${code}`);
}
return name;
}
/**
* Registers a new codec in the Multicodec class.
*
* @param codec - The codec to be registered.
*/
public static registerCodec(codec: MulticodecDefinition<MulticodecCode>) {
Multicodec.codeToName.set(codec.code, codec.name);
Multicodec.nameToCode.set(codec.name, codec.code);
}
/**
* Returns the data with the Multicodec prefix removed.
*
* @param refixedData - The data to extract the codec code from.
* @returns {Uint8Array}
*/
public static removePrefix(options: {
prefixedData: Uint8Array
}): { code: MulticodecCode, name: string, data: Uint8Array } {
const { prefixedData } = options;
const [code, codeByteLength] = varint.decode(prefixedData);
// Throw error if a registered Codec wasn't found.
const name = Multicodec.codeToName.get(code);
if (name === undefined) {
throw new Error(`Unsupported multicodec: ${code}`);
}
return { code, data: prefixedData.slice(codeByteLength), name };
}
}
// Pre-defined registered codecs:
Multicodec.registerCodec({ code: 0xed, name: 'ed25519-pub' });
Multicodec.registerCodec({ code: 0x1300, name: 'ed25519-priv' });
Multicodec.registerCodec({ code: 0xec, name: 'x25519-pub' });
Multicodec.registerCodec({ code: 0x1302, name: 'x25519-priv' });
Multicodec.registerCodec({ code: 0xe7, name: 'secp256k1-pub' });
Multicodec.registerCodec({ code: 0x1301, name: 'secp256k1-priv' });
+43
View File
@@ -0,0 +1,43 @@
/**
* Checks whether the given object has any properties.
*/
export function isEmptyObject(obj: unknown): boolean {
if (typeof obj !== 'object' || obj === null) {
return false;
}
if (Object.getOwnPropertySymbols(obj).length > 0) {
return false;
}
return Object.keys(obj).length === 0;
}
/**
* Recursively removes all properties with an empty object or array as its value from the given object.
*/
export function removeEmptyObjects(obj: Record<string, unknown>): void {
Object.keys(obj).forEach(key => {
if (typeof(obj[key]) === 'object') {
// recursive remove empty object or array properties in nested objects
removeEmptyObjects(obj[key] as Record<string, unknown>);
}
if (isEmptyObject(obj[key])) {
delete obj[key];
}
});
}
/**
* Recursively removes all properties with `undefined` as its value from the given object.
*/
export function removeUndefinedProperties(obj: Record<string, unknown>): void {
Object.keys(obj).forEach(key => {
if (obj[key] === undefined) {
delete obj[key];
} else if (typeof(obj[key]) === 'object') {
removeUndefinedProperties(obj[key] as Record<string, unknown>); // recursive remove `undefined` properties in nested objects
}
});
}
+135
View File
@@ -0,0 +1,135 @@
import type { AbstractLevel } from 'abstract-level';
import { Level } from 'level';
import type { KeyValueStore } from './types.js';
export class LevelStore<K = string, V = any> implements KeyValueStore<K, V> {
private store: AbstractLevel<string | Buffer | Uint8Array, K, V>;
constructor({ db, location = 'DATASTORE' }: {
db?: AbstractLevel<string | Buffer | Uint8Array, K, V>;
location?: string;
} = {}) {
this.store = db ?? new Level<K, V>(location);
}
async clear(): Promise<void> {
await this.store.clear();
}
async close(): Promise<void> {
await this.store.close();
}
async delete(key: K): Promise<void> {
await this.store.del(key);
}
async get(key: K): Promise<V | undefined> {
try {
return await this.store.get(key);
} catch (error: any) {
// Don't throw when a key wasn't found.
if (error.notFound) return undefined;
throw error;
}
}
async set(key: K, value: V): Promise<void> {
await this.store.put(key, value);
}
}
/**
* The `MemoryStore` class is an implementation of
* `KeyValueStore` that holds data in memory.
*
* It provides a basic key-value store that works synchronously and keeps all
* data in memory. This can be used for testing, or for handling small amounts
* of data with simple key-value semantics.
*
* Example usage:
*
* ```ts
* const memoryStore = new MemoryStore<string, number>();
* await memoryStore.set("key1", 1);
* const value = await memoryStore.get("key1");
* console.log(value); // 1
* ```
*
* @public
*/
export class MemoryStore<K, V> implements KeyValueStore<K, V> {
/**
* A private field that contains the Map used as the key-value store.
*/
private store: Map<K, V> = new Map();
/**
* Clears all entries in the key-value store.
*
* @returns A Promise that resolves when the operation is complete.
*/
async clear(): Promise<void> {
this.store.clear();
}
/**
* This operation is no-op for `MemoryStore`
* and will log a warning if called.
*/
async close(): Promise<void> {
/** no-op */
}
/**
* Deletes an entry from the key-value store by its key.
*
* @param id - The key of the entry to delete.
* @returns A Promise that resolves to a boolean indicating whether the entry was successfully deleted.
*/
async delete(id: K): Promise<boolean> {
return this.store.delete(id);
}
/**
* Retrieves the value of an entry by its key.
*
* @param id - The key of the entry to retrieve.
* @returns A Promise that resolves to the value of the entry, or `undefined` if the entry does not exist.
*/
async get(id: K): Promise<V | undefined> {
return this.store.get(id);
}
/**
* Checks for the presence of an entry by key.
*
* @param id - The key to check for the existence of.
* @returns A Promise that resolves to a boolean indicating whether an element with the specified key exists or not.
*/
async has(id: K): Promise<boolean> {
return this.store.has(id);
}
/**
* Retrieves all values in the key-value store.
*
* @returns A Promise that resolves to an array of all values in the store.
*/
async list(): Promise<V[]> {
return Array.from(this.store.values());
}
/**
* Sets the value of an entry in the key-value store.
*
* @param id - The key of the entry to set.
* @param key - The new value for the entry.
* @returns A Promise that resolves when the operation is complete.
*/
async set(id: K, key: V): Promise<void> {
this.store.set(id, key);
}
}
+381
View File
@@ -0,0 +1,381 @@
import type { Duplex, ReadableStateOptions, Transform, Writable } from 'readable-stream';
import { Readable } from 'readable-stream';
import { Stream } from './stream.js';
import { Convert } from './convert.js';
export { Readable } from 'readable-stream';
export class NodeStream {
/**
* Consumes a `Readable` stream and returns its contents as an `ArrayBuffer`.
*
* This method reads all data from a Node.js `Readable` stream, collects it, and converts it into
* an `ArrayBuffer`.
*
* @example
* ```ts
* const nodeReadable = getReadableStreamSomehow();
* const arrayBuffer = await NodeStream.consumeToArrayBuffer({ readable: nodeReadable });
* ```
*
* @param readable - The Node.js Readable stream whose data will be consumed.
* @returns A Promise that resolves to an `ArrayBuffer` containing all the data from the stream.
*/
public static async consumeToArrayBuffer({ readable }: { readable: Readable}): Promise<ArrayBuffer> {
const arrayBuffer = await Convert.asyncIterable(readable).toArrayBufferAsync();
return arrayBuffer;
}
/**
* Consumes a `Readable` stream and returns its contents as a `Blob`.
*
* This method reads all data from a Node.js `Readable` stream, collects it, and converts it into
* a `Blob`.
*
* @example
* ```ts
* const nodeReadable = getReadableStreamSomehow();
* const blob = await NodeStream.consumeToBlob({ readable: nodeReadable });
* ```
*
* @param readableStream - The Node.js `Readable` stream whose data will be consumed.
* @returns A Promise that resolves to a `Blob` containing all the data from the stream.
*/
public static async consumeToBlob({ readable }: { readable: Readable }): Promise<Blob> {
const blob = await Convert.asyncIterable(readable).toBlobAsync();
return blob;
}
/**
* Consumes a `Readable` stream and returns its contents as a `Uint8Array`.
*
* This method reads all data from a Node.js `Readable`, collects it, and converts it into a
* `Uint8Array`.
*
* @example
* ```ts
* const nodeReadable = getReadableStreamSomehow();
* const bytes = await NodeStream.consumeToBytes({ readable: nodeReadable });
* ```
*
* @param readableStream - The Node.js `Readable` stream whose data will be consumed.
* @returns A Promise that resolves to a `Uint8Array` containing all the data from the stream.
*/
public static async consumeToBytes({ readable }: { readable: Readable }): Promise<Uint8Array> {
const bytes = await Convert.asyncIterable(readable).toUint8ArrayAsync();
return bytes;
}
/**
* Consumes a `Readable` stream and parses its contents as JSON.
*
* This method reads all the data from the stream, converts it to a text string, and then parses
* it as JSON, returning the resulting object.
*
* @example
* ```ts
* const nodeReadable = getReadableStreamSomehow();
* const jsonData = await NodeStream.consumeToJson({ readable: nodeReadable });
* ```
*
* @param readableStream - The Node.js `Readable` stream whose JSON content will be consumed.
* @returns A Promise that resolves to the parsed JSON object from the stream's data.
*/
public static async consumeToJson({ readable }: { readable: Readable }): Promise<any> {
const object = await Convert.asyncIterable(readable).toObjectAsync();
return object;
}
/**
* Consumes a `Readable` stream and returns its contents as a text string.
*
* This method reads all the data from the stream, converting it into a single string.
*
* @example
* ```ts
* const nodeReadable = getReadableStreamSomehow();
* const text = await NodeStream.consumeToText({ readable: nodeReadable });
* ```
*
* @param readableStream - The Node.js `Readable` stream whose text content will be consumed.
* @returns A Promise that resolves to a string containing all the data from the stream.
*/
public static async consumeToText({ readable }: { readable: Readable}): Promise<string> {
const text = await Convert.asyncIterable(readable).toStringAsync();
return text;
}
/**
* Converts a Web `ReadableStream` to a Node.js `Readable` stream.
*
* This method takes a Web `ReadableStream` and converts it to a Node.js `Readable` stream.
* The conversion is done by reading chunks from the Web `ReadableStream` and pushing them
* into the Node.js `Readable` stream.
*
* @example
* ```ts
* const webReadableStream = getWebReadableStreamSomehow();
* const nodeReadableStream = NodeStream.fromWebReadable({ readableStream: webReadableStream });
* ```
*
* @param readableStream - The Web `ReadableStream` to be converted.
* @param readableOptions - Optional `Readable` stream options for the Node.js stream.
* @returns The Node.js `Readable` stream.
*/
public static fromWebReadable({ readableStream, readableOptions }: {
readableStream: ReadableStream,
readableOptions?: ReadableStateOptions
}): Readable {
if (!Stream.isReadableStream(readableStream)) {
throw new TypeError(`NodeStream.fromWebReadable: 'readableStream' is not a Web ReadableStream.`);
}
const reader = readableStream.getReader();
let closed = false;
const nodeReadable = new Readable({
...readableOptions,
read: function () {
reader.read().then(({ done, value }) => {
if (done) {
this.push(null); // Push null to signify end of stream.
} else {
if (!this.push(value)) {
// When push returns false, we should stop reading until _read is called again.
return;
}
}
}).catch((error) => {
// If an error occurs while reading, destroy the stream.
this.destroy(error);
});
},
destroy: function (error, callback) {
function done() {
callback(error);
}
if (!closed) {
reader.cancel(error)
.then(done)
.catch(done);
return;
}
done();
}
});
reader.closed
.then(() => {
closed = true; // Prevents reader.cancel() from being called in destroy()
})
.catch((error) => {
closed = true; // Prevents reader.cancel() from being called in destroy()
nodeReadable.destroy(error);
});
return nodeReadable;
}
/**
* Checks if a Node.js stream (`Readable`, `Writable`, `Duplex`, or `Transform`) has been destroyed.
*
* This method determines whether the provided Node.js stream has been destroyed. A stream
* is considered destroyed if its 'destroyed' property is set to true or if its internal state
* indicates it has been destroyed.
*
* @example
* ```ts
* const stream = getStreamSomehow();
* stream.destroy(); // Destroy the stream.
* const isDestroyed = NodeStream.isDestroyed({ stream });
* console.log(isDestroyed); // Output: true
* ```
*
* @param stream - The Node.js stream to check.
* @returns `true` if the stream has been destroyed; otherwise, `false`.
*/
public static isDestroyed({ stream }: { stream: Readable | Writable | Duplex | Transform }): boolean {
if (!NodeStream.isStream(stream)) {
throw new TypeError(`NodeStream.isDestroyed: 'stream' is not a Node stream.`);
}
const writableState = '_writableState' in stream ? stream._writableState : undefined;
const readableState = stream._readableState;
const state = writableState || readableState;
return !!(stream.destroyed || state.destroyed);
}
/**
* Checks if a Node.js `Readable` stream is still readable.
*
* This method checks if a Node.js `Readable` stream is still in a state that allows reading from
* it. A stream is considered readable if it has not ended, has not been destroyed, and is not
* currently paused.
*
* @example
* ```ts
* const readableStream = new Readable();
* const isReadable = NodeStream.isReadable({ readable: readableStream });
* console.log(isReadable); // Output: true or false
* ```
*
* @param readable - The Node.js `Readable` stream to be checked.
* @returns `true` if the stream is still readable; otherwise, `false`.
*/
public static isReadable({ readable }: { readable: Readable }): boolean {
// Check if the object is a Node Readable stream.
if (!NodeStream.isReadableStream(readable)) {
return false;
}
// Check if the stream is still readable.
return (
readable.readable && // Is the stream readable?
(typeof readable._readableState.ended === 'boolean' && !readable._readableState.ended) && // Has the 'end' method been called?
(typeof readable._readableState.endEmitted === 'boolean' && !readable._readableState.endEmitted) && // Has the 'end' event been emitted?
!readable.destroyed && // Has the 'destroy' method been called?
!readable.isPaused() // Is the stream paused?
);
}
/**
* Checks if an object is a Node.js `Readable` stream.
*
* This method verifies if the provided object is a Node.js `Readable` stream by checking for
* specific properties and methods typical of a `Readable` stream in Node.js.
*
* @example
* ```ts
* const obj = getSomeObject();
* if (NodeStream.isReadableStream(obj)) {
* // obj is a Node.js Readable stream
* }
* ```
*
* @param obj - The object to be checked.
* @returns `true` if `obj` is a Node.js `Readable` stream; otherwise, `false`.
*/
static isReadableStream(obj: unknown): obj is Readable {
return (
typeof obj === 'object' &&
obj !== null &&
('pipe' in obj && typeof obj.pipe === 'function') &&
('on' in obj && typeof obj.on === 'function') &&
(!('_writableState' in obj) && '_readableState' in obj)
);
}
/**
* Checks if the provided object is a Node.js stream (`Duplex`, `Readable`, `Writable`, or `Transform`).
*
* This method checks for the presence of internal properties specific to Node.js streams:
* `_readableState` and `_writableState`. These properties are present in Node.js stream
* instances, allowing identification of the stream type.
*
* The `_readableState` property is found in `Readable` and `Duplex` streams (including
* `Transform` streams, which are a type of `Duplex` stream), indicating that the stream can be
* read from. The `_writableState` property is found in `Writable` and `Duplex` streams,
* indicating that the stream can be written to.
*
* @example
* ```ts
* const { Readable, Writable, Duplex, Transform } = require('stream');
*
* const readableStream = new Readable();
* console.log(NodeStream.isStream(readableStream)); // Output: true
*
* const writableStream = new Writable();
* console.log(NodeStream.isStream(writableStream)); // Output: true
*
* const duplexStream = new Duplex();
* console.log(NodeStream.isStream(duplexStream)); // Output: true
*
* const transformStream = new Transform();
* console.log(NodeStream.isStream(transformStream)); // Output: true
*
* const nonStreamObject = {};
* console.log(NodeStream.isStream(nonStreamObject)); // Output: false
* ```
*
* @remarks
* - This method does not differentiate between the different types of streams (Readable,
* Writable, Duplex, Transform). It simply checks if the object is any kind of Node.js stream.
* - While this method can identify standard Node.js streams, it may not recognize custom or
* third-party stream-like objects that do not inherit directly from Node.js's stream classes
* or do not have these internal state properties. This is intentional as many of the methods
* in this library are designed to work with standard Node.js streams.
*
* @param obj - The object to be checked for being a Node.js stream.
* @returns `true` if the object is a Node.js stream (`Duplex`, `Readable`, `Writable`, or `Transform`); otherwise, `false`.
*/
public static isStream(obj: unknown): obj is Duplex | Readable | Writable | Transform {
return (
typeof obj === 'object' && obj !== null &&
('_readableState' in obj || '_writableState' in obj)
);
}
/**
* Converts a Node.js `Readable` stream to a Web `ReadableStream`.
*
* This method provides a bridge between Node.js streams and the Web Streams API by converting a
* Node.js `Readable` stream into a Web `ReadableStream`. It listens for 'data', 'end', and 'error'
* events on the Node.js stream and appropriately enqueues data, closes, or errors the Web
* `ReadableStream`.
*
* If the Node.js stream is already destroyed, the method returns an immediately cancelled
* Web `ReadableStream`.
*
* @example
* ```ts
* const nodeReadable = getNodeReadableStreamSomehow();
* const webReadableStream = NodeStream.toWebReadable({ readable: nodeReadable });
* ```
*
* @param readable - The Node.js `Readable` stream to be converted.
* @returns A Web `ReadableStream` corresponding to the provided Node.js `Readable` stream.
* @throws TypeError if `readable` is not a Node.js `Readable` stream.
* @throws Error if the Node.js `Readable` stream is already destroyed.
*/
static toWebReadable({ readable }: { readable: Readable }): ReadableStream {
if (!NodeStream.isReadableStream(readable)) {
throw new TypeError(`NodeStream.toWebReadable: 'readable' is not a Node Readable stream.`);
}
if (NodeStream.isDestroyed({ stream: readable })) {
const readable = new ReadableStream();
readable.cancel();
return readable;
}
return new ReadableStream({
start(controller) {
readable.on('data', (chunk) => {
controller.enqueue(chunk);
});
readable.on('end', () => {
controller.close();
});
readable.on('error', (err) => {
controller.error(err);
});
},
cancel() {
readable.destroy();
}
});
}
}
+406
View File
@@ -0,0 +1,406 @@
import { Convert } from './convert.js';
export class Stream {
/**
* Transforms a `ReadableStream` into an `AsyncIterable`. This allows for the asynchronous
* iteration over the stream's data chunks.
*
* This method creates an async iterator from a `ReadableStream`, enabling the use of
* `for await...of` loops to process stream data. It reads from the stream until it's closed or
* errored, yielding each chunk as it becomes available.
*
* @example
* ```ts
* const readableStream = new ReadableStream({ ... });
* for await (const chunk of Stream.asAsyncIterator(readableStream)) {
* // process each chunk
* }
* ```
*
* @remarks
* - The method ensures proper cleanup by releasing the reader lock when iteration is completed or
* if an error occurs.
*
* @param readableStream - The Web `ReadableStream` to be transformed into an `AsyncIterable`.
* @returns An `AsyncIterable` that yields data chunks from the `ReadableStream`.
*/
public static async * asAsyncIterator<T>(readableStream: ReadableStream<T>): AsyncIterable<T> {
const reader = readableStream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
yield value;
}
} finally {
reader.releaseLock();
}
}
/**
* Consumes a `ReadableStream` and returns its contents as an `ArrayBuffer`.
*
* This method reads all data from a `ReadableStream`, collects it, and converts it into an
* `ArrayBuffer`.
*
* @example
* ```ts
* const readableStream = new ReadableStream({ ... });
* const arrayBuffer = await Stream.consumeToArrayBuffer({ readableStream });
* ```
*
* @param readableStream - The Web `ReadableStream` whose data will be consumed.
* @returns A Promise that resolves to an `ArrayBuffer` containing all the data from the stream.
*/
public static async consumeToArrayBuffer({ readableStream }: { readableStream: ReadableStream}): Promise<ArrayBuffer> {
const iterableStream = Stream.asAsyncIterator(readableStream);
const arrayBuffer = await Convert.asyncIterable(iterableStream).toArrayBufferAsync();
return arrayBuffer;
}
/**
* Consumes a `ReadableStream` and returns its contents as a `Blob`.
*
* This method reads all data from a `ReadableStream`, collects it, and converts it into a `Blob`.
*
* @example
* ```ts
* const readableStream = new ReadableStream({ ... });
* const blob = await Stream.consumeToBlob({ readableStream });
* ```
*
* @param readableStream - The Web `ReadableStream` whose data will be consumed.
* @returns A Promise that resolves to a `Blob` containing all the data from the stream.
*/
public static async consumeToBlob({ readableStream }: { readableStream: ReadableStream}): Promise<Blob> {
const iterableStream = Stream.asAsyncIterator(readableStream);
const blob = await Convert.asyncIterable(iterableStream).toBlobAsync();
return blob;
}
/**
* Consumes a `ReadableStream` and returns its contents as a `Uint8Array`.
*
* This method reads all data from a `ReadableStream`, collects it, and converts it into a
* `Uint8Array`.
*
* @example
* ```ts
* const readableStream = new ReadableStream({ ... });
* const bytes = await Stream.consumeToBytes({ readableStream });
* ```
*
* @param readableStream - The Web `ReadableStream` whose data will be consumed.
* @returns A Promise that resolves to a `Uint8Array` containing all the data from the stream.
*/
public static async consumeToBytes({ readableStream }: { readableStream: ReadableStream }): Promise<Uint8Array> {
const iterableStream = Stream.asAsyncIterator(readableStream);
const bytes = await Convert.asyncIterable(iterableStream).toUint8ArrayAsync();
return bytes;
}
/**
* Consumes a `ReadableStream` and parses its contents as JSON.
*
* This method reads all the data from the stream, converts it to a text string, and then parses
* it as JSON, returning the resulting object.
*
* @example
* ```ts
* const readableStream = new ReadableStream({ ... });
* const jsonData = await Stream.consumeToJson({ readableStream });
* ```
*
* @param readableStream - The Web `ReadableStream` whose JSON content will be consumed.
* @returns A Promise that resolves to the parsed JSON object from the stream's data.
*/
public static async consumeToJson({ readableStream }: { readableStream: ReadableStream}): Promise<any> {
const iterableStream = Stream.asAsyncIterator(readableStream);
const object = await Convert.asyncIterable(iterableStream).toObjectAsync();
return object;
}
/**
* Consumes a `ReadableStream` and returns its contents as a text string.
*
* This method reads all the data from the stream, converting it into a single string.
*
* @example
* ```ts
* const readableStream = new ReadableStream({ ... });
* const text = await Stream.consumeToText({ readableStream });
* ```
*
* @param readableStream - The Web `ReadableStream` whose text content will be consumed.
* @returns A Promise that resolves to a string containing all the data from the stream.
*/
public static async consumeToText({ readableStream }: { readableStream: ReadableStream}): Promise<string> {
const iterableStream = Stream.asAsyncIterator(readableStream);
const text = await Convert.asyncIterable(iterableStream).toStringAsync();
return text;
}
/**
* Generates a `ReadableStream` of `Uint8Array` chunks with customizable length and fill value.
*
* This method creates a `ReadableStream` that emits `Uint8Array` chunks. You can specify the
* total length of the stream, the length of individual chunks, and a fill value or range for the
* chunks. It's useful for testing or when specific binary data streams are required.
*
* @example
* ```ts
* // Create a stream of 1000 bytes with 100-byte chunks filled with 0xAA.
* const byteStream = Stream.generateByteStream({
* streamLength: 1000,
* chunkLength: 100,
* fillValue: 0xAA
* });
*
* // Create an unending stream of 100KB chunks filled with values that range from 1 to 99.
* const byteStream = Stream.generateByteStream({
* chunkLength: 100 * 1024,
* fillValue: [1, 99]
* });
* ```
*
* @param streamLength - The total length of the stream in bytes. If omitted, the stream is infinite.
* @param chunkLength - The length of each chunk. If omitted, each chunk is the size of `streamLength`.
* @param fillValue - A value or range to fill the chunks with. Can be a single number or a tuple [min, max].
* @returns A `ReadableStream` that emits `Uint8Array` chunks.
*/
public static generateByteStream({ streamLength, chunkLength, fillValue }: {
streamLength?: number,
chunkLength?: number,
fillValue?: number | [number, number]
}): ReadableStream<Uint8Array> {
let bytesRemaining = streamLength ?? Infinity;
let controller: ReadableStreamDefaultController<Uint8Array>;
function enqueueChunk() {
const currentChunkLength = Math.min(bytesRemaining, chunkLength ?? Infinity);
bytesRemaining -= currentChunkLength;
let chunk: Uint8Array;
if (typeof fillValue === 'number') {
chunk = new Uint8Array(currentChunkLength).fill(fillValue);
} else if (Array.isArray(fillValue)) {
chunk = new Uint8Array(currentChunkLength);
const [min, max] = fillValue;
const range = max - min + 1;
for (let i = 0; i < currentChunkLength; i++) {
chunk[i] = Math.floor(Math.random() * range) + min;
}
} else {
chunk = new Uint8Array(currentChunkLength);
}
controller.enqueue(chunk);
// If there are no more bytes to send, close the stream
if (bytesRemaining <= 0) {
controller.close();
}
}
return new ReadableStream<Uint8Array>({
start(c) {
controller = c;
enqueueChunk();
},
pull() {
enqueueChunk();
},
});
}
/**
* Checks if the provided Web `ReadableStream` is in a readable state.
*
* After verifying that the stream is a Web {@link https://streams.spec.whatwg.org/#rs-model | ReadableStream},
* this method checks the {@link https://streams.spec.whatwg.org/#readablestream-locked | locked}
* property of the ReadableStream. The `locked` property is `true` if a reader is currently
* active, meaning the stream is either being read or has already been read (and hence is not in a
* readable state). If `locked` is `false`, it means the stream is still in a state where it can
* be read.
*
* In the case where a `ReadableStream` has been unlocked but is no longer readable (for example,
* if it has been fully read or cancelled), additional checks are needed beyond just examining the
* locked property. The ReadableStream API does not provide a direct way to check if the stream
* has data left or if it's in a readable state once it's been unlocked.
*
* Per {@link https://streams.spec.whatwg.org/#other-specs-rs-introspect | WHATWG Streams, Section 9.1.3. Introspection}:
*
* > ...note that apart from checking whether or not the stream is locked, this direct
* > introspection is not possible via the public JavaScript API, and so specifications should
* > instead use the algorithms in §9.1.2 Reading. (For example, instead of testing if the stream
* > is readable, attempt to get a reader and handle any exception.)
*
* This implementation employs the technique suggested by the WHATWG Streams standard by
* attempting to acquire a reader and checking the state of the reader. If acquiring a reader
* succeeds, it immediately releases the lock and returns `true`, indicating the stream is
* readable. If an error occurs while trying to get a reader (which can happen if the stream is
* already closed or errored), it catches the error and returns `false`, indicating the stream is
* not readable.
*
* @example
* ```ts
* const readableStream = new ReadableStream({ ... });
* const isStreamReadable = Stream.isReadable({ readableStream });
* console.log(isStreamReadable); // Output: true or false
* ```
*
* @remarks
* - This method does not check whether the stream has data left to read; it only checks if the
* stream is in a state that allows reading. It is possible for a stream to be unlocked but
* still have no data left if it has never been locked to a reader.
*
* @param readableStream - The Web `ReadableStream` to be checked for readability.
*
* @returns `true` if the stream is a `ReadableStream` and is in a readable state (not locked and
* no error on getting a reader); otherwise, `false`.
*/
public static isReadable({ readableStream }: { readableStream: ReadableStream }): boolean {
// Check if the stream is a WHATWG `ReadableStream`.
if (!Stream.isReadableStream(readableStream)) {
return false;
}
// Check if the stream is locked.
if (readableStream.locked) {
return false;
}
try {
// Try to get a reader to check if the stream is readable.
const reader = readableStream.getReader();
// If successful, immediately release the lock.
reader.releaseLock();
return true;
} catch (error) {
// If an error occurs (e.g., the stream is not readable), return false.
return false;
}
}
/**
* Checks if an object is a Web `ReadableStream`.
*
* This method verifies whether the given object is a `ReadableStream` by checking its type and
* the presence of the `getReader` function.
*
* @example
* ```ts
* const obj = getSomeObject();
* if (Stream.isReadableStream(obj)) {
* // obj is a ReadableStream
* }
* ```
*
* @param obj - The object to be checked.
* @returns `true` if `obj` is a `ReadableStream`; otherwise, `false`.
*/
public static isReadableStream(obj: unknown): obj is ReadableStream {
return (
typeof obj === 'object' && obj !== null &&
'getReader' in obj && typeof obj.getReader === 'function'
);
}
/**
* Checks if an object is a Web `ReadableStream`, `WritableStream`, or `TransformStream`.
*
* This method verifies the type of a given object to determine if it is one of the standard
* stream types in the Web Streams API: `ReadableStream`, `WritableStream`, or `TransformStream`.
* It employs type-checking strategies that are specific to each stream type.
*
* The method checks for the specific functions and properties associated with each stream type:
* - `ReadableStream`: Identified by the presence of a `getReader` method.
* - `WritableStream`: Identified by the presence of a `getWriter` and `abort` methods.
* - `TransformStream`: Identified by having both `readable` and `writable` properties.
*
* @example
* ```ts
* const readableStream = new ReadableStream();
* console.log(Stream.isStream(readableStream)); // Output: true
*
* const writableStream = new WritableStream();
* console.log(Stream.isStream(writableStream)); // Output: true
*
* const transformStream = new TransformStream();
* console.log(Stream.isStream(transformStream)); // Output: true
*
* const nonStreamObject = {};
* console.log(Stream.isStream(nonStreamObject)); // Output: false
* ```
*
* @remarks
* - This method does not differentiate between `ReadableStream`, `WritableStream`, and
* `TransformStream`. It checks if the object conforms to any of these types.
* - This method is specific to the Web Streams API and may not recognize non-standard or custom
* stream-like objects that do not adhere to the Web Streams API specifications.
*
* @param obj - The object to be checked for being a Web `ReadableStream`, `WritableStream`, or `TransformStream`.
* @returns `true` if the object is a `ReadableStream`, `WritableStream`, or `TransformStream`; otherwise, `false`.
*/
public static isStream(obj: unknown): obj is ReadableStream | WritableStream | TransformStream {
return Stream.isReadableStream(obj) || Stream.isWritableStream(obj) || Stream.isTransformStream(obj);
}
/**
* Checks if an object is a `TransformStream`.
*
* This method verifies whether the given object is a `TransformStream` by checking its type and
* the presence of `readable` and `writable` properties.
*
* @example
* ```ts
* const obj = getSomeObject();
* if (Stream.isTransformStream(obj)) {
* // obj is a TransformStream
* }
* ```
*
* @param obj - The object to be checked.
* @returns `true` if `obj` is a `TransformStream`; otherwise, `false`.
*/
public static isTransformStream(obj: unknown): obj is TransformStream {
return (
typeof obj === 'object' && obj !== null &&
'readable' in obj && typeof obj.readable === 'object' &&
'writable' in obj && typeof obj.writable === 'object'
);
}
/**
* Checks if an object is a `WritableStream`.
*
* This method determines whether the given object is a `WritableStream` by verifying its type and
* the presence of the `getWriter` and `abort` functions.
*
* @example
* ```ts
* const obj = getSomeObject();
* if (Stream.isWritableStream(obj)) {
* // obj is a WritableStream
* }
* ```
*
* @param obj - The object to be checked.
* @returns `true` if `obj` is a `TransformStream`; otherwise, `false`.
*/
public static isWritableStream(obj: unknown): obj is WritableStream {
return (
typeof obj === 'object' && obj !== null &&
'getWriter' in obj && typeof obj.getWriter === 'function' &&
'abort' in obj && typeof obj.abort === 'function'
);
}
}
+232
View File
@@ -0,0 +1,232 @@
/**
* Represents an array of a fixed length, preventing modifications to its size.
*
* The `FixedLengthArray` utility type transforms a standard array into a variant where
* methods that could alter the length are omitted. It leverages TypeScript's advanced types,
* such as conditional types and mapped types, to ensure that the array cannot be resized
* through methods like `push`, `pop`, `splice`, `shift`, and `unshift`. The utility type
* maintains all other characteristics of a standard array, including indexing, iteration,
* and type checking for its elements.
*
* Note: The type does not prevent direct assignment to indices, even if it would exceed
* the original length. However, such actions would lead to TypeScript type errors.
*
* @example
* ```ts
* // Declare a variable with a type of fixed-length array of three strings.
* let myFixedLengthArray: FixedLengthArray< [string, string, string]>;
*
* // Array declaration tests
* myFixedLengthArray = [ 'a', 'b', 'c' ]; // OK
* myFixedLengthArray = [ 'a', 'b', 123 ]; // TYPE ERROR
* myFixedLengthArray = [ 'a' ]; // LENGTH ERROR
* myFixedLengthArray = [ 'a', 'b' ]; // LENGTH ERROR
*
* // Index assignment tests
* myFixedLengthArray[1] = 'foo'; // OK
* myFixedLengthArray[1000] = 'foo'; // INVALID INDEX ERROR
*
* // Methods that mutate array length
* myFixedLengthArray.push('foo'); // MISSING METHOD ERROR
* myFixedLengthArray.pop(); // MISSING METHOD ERROR
*
* // Direct length manipulation
* myFixedLengthArray.length = 123; // READ-ONLY ERROR
*
* // Destructuring
* let [ a ] = myFixedLengthArray; // OK
* let [ a, b ] = myFixedLengthArray; // OK
* let [ a, b, c ] = myFixedLengthArray; // OK
* let [ a, b, c, d ] = myFixedLengthArray; // INVALID INDEX ERROR
* ```
*
* @template T extends any[] - The array type to be transformed.
*/
export type FixedLengthArray<T extends any[]> =
Pick<T, Exclude<keyof T, ArrayLengthMutationKeys>>
& {
/**
* Custom iterator for the `FixedLengthArray` type.
*
* This iterator allows the `FixedLengthArray` to be used in standard iteration
* contexts, such as `for...of` loops and spread syntax. It ensures that even though
* the array is of a fixed length with disabled mutation methods, it still retains
* iterable behavior similar to a regular array.
*
* @returns An IterableIterator for the array items.
*/
[Symbol.iterator]: () => IterableIterator<ArrayItems<T>>
};
/** Helper types for {@link FixedLengthArray} */
type ArrayLengthMutationKeys = 'splice' | 'push' | 'pop' | 'shift' | 'unshift' | number;
type ArrayItems<T extends Array<any>> = T extends Array<infer TItems> ? TItems : never;
/**
* isArrayBufferSlice
*
* Checks if the ArrayBufferView represents a slice (subarray or a subview)
* of an ArrayBuffer.
*
* An ArrayBufferView (TypedArray or DataView) can represent a portion of an
* ArrayBuffer - such a view is said to be a "slice" of the original buffer.
* This can occur when the `subarray` or `slice` method is called on a
* TypedArray or when a DataView is created with a byteOffset and/or
* byteLength that doesn't cover the full ArrayBuffer.
*
* @param arrayBufferView - The ArrayBufferView to be checked
* @returns true if the ArrayBufferView represents a slice of an ArrayBuffer; false otherwise.
*/
export function isArrayBufferSlice(arrayBufferView: ArrayBufferView): boolean {
return arrayBufferView.byteOffset !== 0 || arrayBufferView.byteLength !== arrayBufferView.buffer.byteLength;
}
/**
* Checks if the given object is an AsyncIterable.
*
* An AsyncIterable is an object that implements the AsyncIterable protocol,
* which means it has a [Symbol.asyncIterator] method. This function checks
* if the provided object conforms to this protocol by verifying the presence
* and type of the [Symbol.asyncIterator] method.
*
* @param obj - The object to be checked for AsyncIterable conformity.
* @returns True if the object is an AsyncIterable, false otherwise.
*
* @example
* ```ts
* // Returns true for a valid AsyncIterable
* const asyncIterable = {
* async *[Symbol.asyncIterator]() {
* yield 1;
* yield 2;
* }
* };
* console.log(isAsyncIterable(asyncIterable)); // true
* ```
*
* @example
* ```ts
* // Returns false for a regular object
* console.log(isAsyncIterable({ a: 1, b: 2 })); // false
* ```
*/
export function isAsyncIterable(obj: any): obj is AsyncIterable<any> {
if (typeof obj !== 'object' || obj === null) {
return false;
}
return typeof obj[Symbol.asyncIterator] === 'function';
}
/**
* isDefined
*
* Utility function to check if a variable is neither null nor undefined.
* This function helps in making TypeScript infer the type of the variable
* as being defined, excluding `null` and `undefined`.
*
* The function uses strict equality (`!==`) for the comparison, ensuring
* that the variable is not just falsy (like an empty string or zero),
* but is truly either `null` or `undefined`.
*
* @param arg - The variable to be checked
* @returns true if the variable is neither `null` nor `undefined`
*/
export function isDefined<T>(arg: T): arg is Exclude<T, null | undefined> {
return arg !== null && typeof arg !== 'undefined';
}
/**
* Utility type that transforms a type `T` to have only certain keys `K` as required, while the
* rest remain optional, except for keys specified in `O`, which are omitted entirely.
*
* This type is useful when you need a variation of a type where only specific properties are
* required, and others are either optional or not included at all. It allows for more flexible type
* definitions based on existing types without the need to redefine them.
*
* @template T - The original type to be transformed.
* @template K - The keys of `T` that should be required.
* @template O - The keys of `T` that should be omitted from the resulting type (optional).
*
* @example
* ```ts
* // Given an interface
* interface Example {
* requiredProp: string;
* optionalProp?: number;
* anotherOptionalProp?: boolean;
* }
*
* // Making 'optionalProp' required and omitting 'anotherOptionalProp'
* type ModifiedExample = RequireOnly<Example, 'optionalProp', 'anotherOptionalProp'>;
* // Result: { requiredProp?: string; optionalProp: number; }
* ```
*/
export type RequireOnly<T, K extends keyof T, O extends keyof T = never> = Required<Pick<T, K>> & Omit<Partial<T>, O>;
/**
* universalTypeOf
*
* Why does this function exist?
*
* You can typically check if a value is of a particular type, such as
* Uint8Array or ArrayBuffer, by using the `instanceof` operator. The
* `instanceof` operator checks the prototype property of a constructor
* in the object's prototype chain.
*
* However, there is a caveat with the `instanceof` check if the value
* was created from a different JavaScript context (like an iframe or
* a web worker). In those cases, the `instanceof` check might fail
* because each context has a different global object, and therefore,
* different built-in constructor functions.
*
* The `typeof` operator provides information about the type of the
* operand in a less detailed way. For basic data types like number,
* string, boolean, and undefined, the `typeof` operator works as
* expected. However, for objects, including arrays and null,
* it always returns "object". For functions, it returns "function".
* So, while `typeof` is good for basic type checking, it doesn't
* give detailed information about complex data types.
*
* Unlike `instanceof` and `typeof`, `Object.prototype.toString.call(value)`
* can ensure a consistent result across different JavaScript
* contexts.
*
* Credit for inspiration:
* Angus Croll
* https://github.com/angus-c
* https://javascriptweblog.wordpress.com/2011/08/08/fixing-the-javascript-typeof-operator/
*/
export function universalTypeOf(value: unknown) {
// Returns '[Object Type]' string.
const typeString = Object.prototype.toString.call(value);
// Returns ['Object', 'Type'] array or null.
const match = typeString.match(/\s([a-zA-Z0-9]+)/);
// Deconstructs the array and gets just the type from index 1.
const [_, type] = match as RegExpMatchArray;
return type;
}
/**
* Utility type to extract the type resolved by a Promise.
*
* This type unwraps the type `T` from `Promise<T>` if `T` is a Promise, otherwise returns `T` as
* is. It's useful in situations where you need to handle the type returned by a promise-based
* function in a synchronous context, such as defining types for test vectors or handling return
* types in non-async code blocks.
*
* @template T - The type to unwrap from the Promise.
*
* @example
* ```ts
* // For a Promise type, it extracts the resolved type.
* type AsyncNumber = Promise<number>;
* type UnwrappedNumber = UnwrapPromise<AsyncNumber>; // number
*
* // For a non-Promise type, it returns the type as is.
* type StringValue = string;
* type UnwrappedString = UnwrapPromise<StringValue>; // string
* ```
*/
export type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
+43
View File
@@ -0,0 +1,43 @@
/**
* Interface for a generic key-value store.
*/
export interface KeyValueStore<K, V> {
/**
* Clears the store, removing all key-value pairs.
*
* @returns A promise that resolves when the store has been cleared.
*/
clear(): Promise<void>;
/**
* Closes the store, freeing up any resources used. After calling this method, no other operations can be performed on the store.
*
* @returns A promise that resolves when the store has been closed.
*/
close(): Promise<void>;
/**
* Deletes a key-value pair from the store.
*
* @param key - The key of the value to delete.
* @returns A promise that resolves to true if the element existed and has been removed, or false if the element does not exist.
*/
delete(key: K): Promise<boolean | void>;
/**
* Fetches a value from the store given its key.
*
* @param key - The key of the value to retrieve.
* @returns A promise that resolves with the value associated with the key, or `undefined` if no value exists for that key.
*/
get(key: K): Promise<V | undefined>;
/**
* Sets the value for a key in the store.
*
* @param key - The key under which to store the value.
* @param value - The value to be stored.
* @returns A promise that resolves when the value has been set.
*/
set(key: K, value: V): Promise<void>;
}