From f30cdf626293d90f454a1310bb58b85118ae6a21 Mon Sep 17 00:00:00 2001 From: Richard Moore Date: Fri, 9 Sep 2022 03:37:38 -0400 Subject: [PATCH] docs: added jsdocs to utils. --- src.ts/utils/base64.ts | 10 +- src.ts/utils/data.ts | 58 +++++++++++- src.ts/utils/fetch.ts | 185 ++++++++++++++++++++++++++++++++++--- src.ts/utils/maths.ts | 27 +++++- src.ts/utils/rlp-decode.ts | 3 + src.ts/utils/rlp-encode.ts | 3 + src.ts/utils/rlp.ts | 3 + 7 files changed, 270 insertions(+), 19 deletions(-) diff --git a/src.ts/utils/base64.ts b/src.ts/utils/base64.ts index 8f1e23072..74ed13cbb 100644 --- a/src.ts/utils/base64.ts +++ b/src.ts/utils/base64.ts @@ -3,10 +3,16 @@ import { getBytes, getBytesCopy } from "./data.js"; import type { BytesLike } from "./data.js"; -export function decodeBase64(textData: string): Uint8Array { - return getBytesCopy(Buffer.from(textData, "base64")); +/** + * Decodes the base-64 encoded %%base64Data%%. + */ +export function decodeBase64(base64Data: string): Uint8Array { + return getBytesCopy(Buffer.from(base64Data, "base64")); }; +/** + * Encodes %%data%% as base-64 encoded data. + */ export function encodeBase64(data: BytesLike): string { return Buffer.from(getBytes(data)).toString("base64"); } diff --git a/src.ts/utils/data.ts b/src.ts/utils/data.ts index 56e485eae..a83ccb430 100644 --- a/src.ts/utils/data.ts +++ b/src.ts/utils/data.ts @@ -22,15 +22,39 @@ function _getBytes(value: BytesLike, name?: string, copy?: boolean): Uint8Array return throwArgumentError("invalid BytesLike value", name || "value", value); } +/** + * Get a typed Uint8Array for %%value%%. If already a Uint8Array + * the original %%value%% is returned; if a copy is required use + * [[getBytesCopy]]. + * + * @see: getBytesCopy + */ export function getBytes(value: BytesLike, name?: string): Uint8Array { return _getBytes(value, name, false); } +/** + * Get a typed Uint8Array for %%value%%, creating a copy if necessary + * to prevent any modifications of the returned value from being + * reflected elsewhere. + * + * @see: getBytes + */ export function getBytesCopy(value: BytesLike, name?: string): Uint8Array { return _getBytes(value, name, true); } +/** + * Returns true if %%value%% is a valid [[HexString]], with additional + * optional constraints depending on %%length%%. + * + * If %%length%% is //true//, then %%value%% must additionally be a valid + * [[HexDataString]] (i.e. even length). + * + * If %%length%% is //a number//, then %%value%% must represent that many + * bytes of data (e.g. ``0x1234`` is 2 bytes). + */ export function isHexString(value: any, length?: number | boolean): value is `0x${ string }` { if (typeof(value) !== "string" || !value.match(/^0x[0-9A-Fa-f]*$/)) { return false @@ -42,11 +66,19 @@ export function isHexString(value: any, length?: number | boolean): value is `0x return true; } +/** + * Returns true if %%value%% is a valid representation of arbitrary + * data (i.e. a valid [[HexDataString]] or a Uint8Array). + */ export function isBytesLike(value: any): value is BytesLike { return (isHexString(value, true) || (value instanceof Uint8Array)); } const HexCharacters: string = "0123456789abcdef"; + +/** + * Returns a [[HexDataString]] representation of %%data%%. + */ export function hexlify(data: BytesLike): string { const bytes = getBytes(data); @@ -58,15 +90,28 @@ export function hexlify(data: BytesLike): string { return result; } +/** + * Returns a [[HexDataString]] by concatenating all values + * within %%data%%. + */ export function concat(datas: ReadonlyArray): string { return "0x" + datas.map((d) => hexlify(d).substring(2)).join(""); } +/** + * Returns the length of %%data%%, in bytes. + */ export function dataLength(data: BytesLike): number { if (isHexString(data, true)) { return (data.length - 2) / 2; } return getBytes(data).length; } +/** + * Returns a [[HexDataString]] by slicing %%data%% from the %%start%% + * offset to the %%end%% offset. + * + * By default %%start%% is 0 and %%end%% is the length of %%data%%. + */ export function dataSlice(data: BytesLike, start?: number, end?: number): string { const bytes = getBytes(data); if (end != null && end > bytes.length) { throwError("cannot slice beyond data bounds", "BUFFER_OVERRUN", { @@ -75,13 +120,16 @@ export function dataSlice(data: BytesLike, start?: number, end?: number): string return hexlify(bytes.slice((start == null) ? 0: start, (end == null) ? bytes.length: end)); } +/** + * Return the [[HexDataString]] result by stripping all **leading** + ** zero bytes from %%data%%. + */ export function stripZerosLeft(data: BytesLike): string { let bytes = hexlify(data).substring(2); while (bytes.substring(0, 2) == "00") { bytes = bytes.substring(2); } return "0x" + bytes; } - function zeroPad(data: BytesLike, length: number, left: boolean): string { const bytes = getBytes(data); if (length < bytes.length) { @@ -103,10 +151,18 @@ function zeroPad(data: BytesLike, length: number, left: boolean): string { return hexlify(result); } +/** + * Return the [[HexDataString]] of %%data%% padded on the **left** + * to %%length%% bytes. + */ export function zeroPadValue(data: BytesLike, length: number): string { return zeroPad(data, length, true); } +/** + * Return the [[HexDataString]] of %%data%% padded on the **right** + * to %%length%% bytes. + */ export function zeroPadBytes(data: BytesLike, length: number): string { return zeroPad(data, length, false); } diff --git a/src.ts/utils/fetch.ts b/src.ts/utils/fetch.ts index 0a3a00de9..ae4e79d4a 100644 --- a/src.ts/utils/fetch.ts +++ b/src.ts/utils/fetch.ts @@ -72,12 +72,16 @@ async function gatewayData(url: string, signal?: FetchCancelSignal): Promise { try { const match = url.match(reIpfs); if (!match) { throw new Error("invalid link"); } - return new FetchRequest(`${ base }${ match[2] }`); + return new FetchRequest(`${ baseUrl }${ match[2] }`); } catch (error) { return new FetchResponse(599, "BAD REQUEST (invalid IPFS URI)", { }, null, new FetchRequest(url)); } @@ -136,6 +140,12 @@ function checkSignal(signal?: FetchCancelSignal): FetchCancelSignal { return signal; } +/** + * Represents a request for a resource using a URI. + * + * Requests can occur over http/https, data: URI or any + * URI scheme registered via the static [[register]] method. + */ export class FetchRequest implements Iterable<[ key: string, value: string ]> { #allowInsecure: boolean; #gzip: boolean; @@ -155,13 +165,33 @@ export class FetchRequest implements Iterable<[ key: string, value: string ]> { #signal?: FetchCancelSignal; - // URL + /** + * The fetch URI to requrest. + */ get url(): string { return this.#url; } set url(url: string) { this.#url = String(url); } - // Body + /** + * The fetch body, if any, to send as the request body. + * + * When setting a body, the intrinsic ``Content-Type`` is automatically + * set and will be used if **not overridden** by setting a custom + * header. + * + * If %%body%% is null, the body is cleared (along with the + * intrinsic ``Content-Type``) and the . + * + * If %%body%% is a string, the intrincis ``Content-Type`` is set to + * ``text/plain``. + * + * If %%body%% is a Uint8Array, the intrincis ``Content-Type`` is set to + * ``application/octet-stream``. + * + * If %%body%% is any other object, the intrincis ``Content-Type`` is + * set to ``application/json``. + */ get body(): null | Uint8Array { if (this.#body == null) { return null; } return new Uint8Array(this.#body); @@ -184,11 +214,18 @@ export class FetchRequest implements Iterable<[ key: string, value: string ]> { } } + /** + * Returns true if the request has a body. + */ hasBody(): this is FetchRequestWithBody { return (this.#body != null); } - // Method (default: GET with no body, POST with a body) + /** + * The HTTP method to use when requesting the URI. If no method + * has been explicitly set, then ``GET`` is used if the body is + * null and ``POST`` otherwise. + */ get method(): string { if (this.#method) { return this.#method; } if (this.hasBody()) { return "POST"; } @@ -199,7 +236,9 @@ export class FetchRequest implements Iterable<[ key: string, value: string ]> { this.#method = String(method).toUpperCase(); } - // Headers (automatically fills content-type if not explicitly set) + /** + * The headers that will be used when requesting the URI. + */ get headers(): Readonly> { const headers = Object.assign({ }, this.#headers); @@ -218,12 +257,25 @@ export class FetchRequest implements Iterable<[ key: string, value: string ]> { return Object.freeze(headers); } + + /** + * Get the header for %%key%%. + */ getHeader(key: string): string { return this.headers[key.toLowerCase()]; } + + /** + * Set the header for %%key%% to %%value%%. All values are coerced + * to a string. + */ setHeader(key: string, value: string | number): void { this.#headers[String(key).toLowerCase()] = String(value); } + + /** + * Clear all headers. + */ clearHeaders(): void { this.#headers = { }; } @@ -245,10 +297,16 @@ export class FetchRequest implements Iterable<[ key: string, value: string ]> { }; } - // Configure an Authorization header + /** + * The value that will be sent for the ``Authorization`` header. + */ get credentials(): null | string { return this.#creds || null; } + + /** + * Sets an ``Authorization`` for %%username%% with %%password%%. + */ setCredentials(username: string, password: string): void { if (username.match(/:/)) { throwArgumentError("invalid basic authentication username", "username", "[REDACTED]"); @@ -256,7 +314,9 @@ export class FetchRequest implements Iterable<[ key: string, value: string ]> { this.#creds = `${ username }:${ password }`; } - // Configure the request to allow gzipped responses + /** + * Allow gzip-encoded responses. + */ get allowGzip(): boolean { return this.#gzip; } @@ -264,7 +324,10 @@ export class FetchRequest implements Iterable<[ key: string, value: string ]> { this.#gzip = !!value; } - // Allow credentials to be sent over an insecure (non-HTTPS) channel + /** + * Allow ``Authentication`` credentials to be sent over insecure + * channels. + */ get allowInsecureAuthentication(): boolean { return !!this.#allowInsecure; } @@ -272,14 +335,22 @@ export class FetchRequest implements Iterable<[ key: string, value: string ]> { this.#allowInsecure = !!value; } - // Timeout (milliseconds) + /** + * The timeout (in milliseconds) to wait for a complere response. + */ get timeout(): number { return this.#timeout; } set timeout(timeout: number) { assertArgument(timeout >= 0, "timeout must be non-zero", "timeout", timeout); this.#timeout = timeout; } - // Preflight called before each request is sent + /** + * This function is called prior to each request, for example + * during a redirection or retry in case of server throttling. + * + * This offers an opportunity to populate headers or update + * content before sending a request. + */ get preflightFunc(): null | FetchPreflightFunc { return this.#preflight || null; } @@ -287,7 +358,16 @@ export class FetchRequest implements Iterable<[ key: string, value: string ]> { this.#preflight = preflight; } - // Preflight called before each request is sent + /** + * This function is called after each response, offering an + * opportunity to provide client-level throttling or updating + * response data. + * + * Any error thrown in this causes the ``send()`` to throw. + * + * To schedule a retry attempt (assuming the maximum retry limit + * has not been reached), use [[response.throwThrottleError]]. + */ get processFunc(): null | FetchProcessFunc { return this.#process || null; } @@ -295,7 +375,9 @@ export class FetchRequest implements Iterable<[ key: string, value: string ]> { this.#process = process; } - // Preflight called before each request is sent + /** + * This function is called on each retry attempt. + */ get retryFunc(): null | FetchRetryFunc { return this.#retry || null; } @@ -407,6 +489,9 @@ export class FetchRequest implements Iterable<[ key: string, value: string ]> { return response; } + /** + * Resolves to the response by sending the request. + */ send(): Promise { if (this.#signal != null) { return throwError("request already sent", "UNSUPPORTED_OPERATION", { operation: "fetchRequest.send" }); @@ -415,6 +500,10 @@ export class FetchRequest implements Iterable<[ key: string, value: string ]> { return this.#send(0, getTime() + this.timeout, 0, this, new FetchResponse(0, "", { }, null, this)); } + /** + * Cancels the inflight response, causing a ``CANCELLED`` + * error to be rejected from the [[send]]. + */ cancel(): void { if (this.#signal == null) { return throwError("request has not been sent", "UNSUPPORTED_OPERATION", { operation: "fetchRequest.cancel" }); @@ -460,6 +549,9 @@ export class FetchRequest implements Iterable<[ key: string, value: string ]> { return req; } + /** + * Create a new copy of this request. + */ clone(): FetchRequest { const clone = new FetchRequest(this.url); @@ -488,14 +580,24 @@ export class FetchRequest implements Iterable<[ key: string, value: string ]> { return clone; } + /** + * Locks all static configuration for gateways and FetchGetUrlFunc + * registration. + */ static lockConfig(): void { locked = true; } + /** + * Get the current Gateway function for %%scheme%%. + */ static getGateway(scheme: string): null | FetchGatewayFunc { return Gateways[scheme.toLowerCase()] || null; } + /** + * Set the FetchGatewayFunc for %%scheme%% to %%func%%. + */ static registerGateway(scheme: string, func: FetchGatewayFunc): void { scheme = scheme.toLowerCase(); if (scheme === "http" || scheme === "https") { @@ -505,6 +607,9 @@ export class FetchRequest implements Iterable<[ key: string, value: string ]> { Gateways[scheme] = func; } + /** + * Set a custom function for fetching HTTP and HTTPS requests. + */ static registerGetUrl(getUrl: FetchGetUrlFunc): void { if (locked) { throw new Error("gateways locked"); } getUrlFunc = getUrl; @@ -521,6 +626,9 @@ interface ThrottleError extends Error { throttle: true; }; +/** + * The response for a FetchREquest. + */ export class FetchResponse implements Iterable<[ key: string, value: string ]> { #statusCode: number; #statusMessage: string; @@ -534,12 +642,33 @@ export class FetchResponse implements Iterable<[ key: string, value: string ]> { return ``; } + /** + * The response status code. + */ get statusCode(): number { return this.#statusCode; } + + /** + * The response status message. + */ get statusMessage(): string { return this.#statusMessage; } + + /** + * The response headers. + */ get headers(): Record { return this.#headers; } + + /** + * The response body. + */ get body(): null | Readonly { return (this.#body == null) ? null: new Uint8Array(this.#body); } + + /** + * The response body as a UTF-8 encoded string. + * + * An error is thrown if the body is invalid UTF-8 data. + */ get bodyText(): string { try { return (this.#body == null) ? "": toUtf8String(this.#body); @@ -549,6 +678,12 @@ export class FetchResponse implements Iterable<[ key: string, value: string ]> { }); } } + + /** + * The response body, decoded as JSON. + * + * An error is thrown if the body is invalid JSON-encoded data. + */ get bodyJson(): any { try { return JSON.parse(this.bodyText); @@ -589,6 +724,11 @@ export class FetchResponse implements Iterable<[ key: string, value: string ]> { this.#error = { message: "" }; } + /** + * Return a Response with matching headers and body, but with + * an error status code (i.e. 599) and %%message%% with an + * optional %%error%%. + */ makeServerError(message?: string, error?: Error): FetchResponse { let statusMessage: string; if (!message) { @@ -603,6 +743,10 @@ export class FetchResponse implements Iterable<[ key: string, value: string ]> { return response; } + /** + * If called within the [[processFunc]], causes the request to + * retry as if throttled for %%stall%% milliseconds. + */ throwThrottleError(message?: string, stall?: number): never { if (stall == null) { stall = -1; @@ -617,20 +761,35 @@ export class FetchResponse implements Iterable<[ key: string, value: string ]> { throw error; } + /** + * Get the header value for %%key%%. + */ getHeader(key: string): string { return this.headers[key.toLowerCase()]; } + /** + * Returns true of the response has a body. + */ hasBody(): this is FetchResponseWithBody { return (this.#body != null); } + /** + * The request made for this response. + */ get request(): null | FetchRequest { return this.#request; } + /** + * Returns true if this response was a success statuscode. + */ ok(): boolean { return (this.#error.message === "" && this.statusCode >= 200 && this.statusCode < 300); } + /** + * Throws a ``SERVER_ERROR`` if this response is not ok. + */ assertOk(): void { if (this.ok()) { return; } let { message, error } = this.#error; diff --git a/src.ts/utils/maths.ts b/src.ts/utils/maths.ts index 34af42e65..7a4a59d50 100644 --- a/src.ts/utils/maths.ts +++ b/src.ts/utils/maths.ts @@ -3,8 +3,14 @@ import { throwArgumentError } from "./errors.js"; import type { BytesLike } from "./data.js"; - +/** + * Any type that can be used where a numeric value is needed. + */ export type Numeric = number | bigint; + +/** + * Any type that can be used where a big number is needed. + */ export type BigNumberish = string | Numeric; @@ -54,6 +60,10 @@ export function mask(_value: BigNumberish, _bits: Numeric): bigint { return value & ((BN_1 << bits) - BN_1); } +/** + * Gets a [[BigInt]] from %%value%%. If it is an invalid value for + * a BigInt, then an ArgumentError will be thrown for %%name%%. + */ export function getBigInt(value: BigNumberish, name?: string): bigint { switch (typeof(value)) { case "bigint": return value; @@ -75,11 +85,12 @@ export function getBigInt(value: BigNumberish, name?: string): bigint { } +const Nibbles = "0123456789abcdef"; + /* * Converts %%value%% to a BigInt. If %%value%% is a Uint8Array, it * is treated as Big Endian data. */ -const Nibbles = "0123456789abcdef"; export function toBigInt(value: BigNumberish | Uint8Array): bigint { if (value instanceof Uint8Array) { let result = "0x0"; @@ -93,6 +104,10 @@ export function toBigInt(value: BigNumberish | Uint8Array): bigint { return getBigInt(value); } +/** + * Gets a //number// from %%value%%. If it is an invalid value for + * a //number//, then an ArgumentError will be thrown for %%name%%. + */ export function getNumber(value: BigNumberish, name?: string): number { switch (typeof(value)) { case "bigint": @@ -130,7 +145,6 @@ export function toNumber(value: BigNumberish | Uint8Array): number { * Converts %%value%% to a Big Endian hexstring, optionally padded to * %%width%% bytes. */ -// Converts value to hex, optionally padding on the left to width bytes export function toHex(_value: BigNumberish, _width?: Numeric): string { const value = getBigInt(_value, "value"); if (value < 0) { throw new Error("cannot convert negative value to hex"); } @@ -173,6 +187,13 @@ export function toArray(_value: BigNumberish): Uint8Array { return result; } +/** + * Returns a [[HexString]] for %%value%% safe to use as a //Quantity//. + * + * A //Quantity// does not have and leading 0 values unless the value is + * the literal value `0x0`. This is most commonly used for JSSON-RPC + * numeric values. + */ export function toQuantity(value: BytesLike | BigNumberish): string { let result = hexlify(isBytesLike(value) ? value: toArray(value)).substring(2); while (result.substring(0, 1) === "0") { result = result.substring(1); } diff --git a/src.ts/utils/rlp-decode.ts b/src.ts/utils/rlp-decode.ts index 084749b0d..01d5d095d 100644 --- a/src.ts/utils/rlp-decode.ts +++ b/src.ts/utils/rlp-decode.ts @@ -98,6 +98,9 @@ function _decode(data: Uint8Array, offset: number): { consumed: number, result: return { consumed: 1, result: hexlifyByte(data[offset]) }; } +/** + * Decodes %%data%% into the structured data it represents. + */ export function decodeRlp(_data: BytesLike): RlpStructuredData { const data = getBytes(_data, "data"); const decoded = _decode(data, 0); diff --git a/src.ts/utils/rlp-encode.ts b/src.ts/utils/rlp-encode.ts index 622ac8c12..8fca87aff 100644 --- a/src.ts/utils/rlp-encode.ts +++ b/src.ts/utils/rlp-encode.ts @@ -51,6 +51,9 @@ function _encode(object: Array | string): Array { const nibbles = "0123456789abcdef"; +/** + * Encodes %%object%% as an RLP-encoded [[HexDataString]]. + */ export function encodeRlp(object: RlpStructuredData): string { let result = "0x"; for (const v of _encode(object)) { diff --git a/src.ts/utils/rlp.ts b/src.ts/utils/rlp.ts index 8f5c7209d..d455a7f9e 100644 --- a/src.ts/utils/rlp.ts +++ b/src.ts/utils/rlp.ts @@ -1,4 +1,7 @@ +/** + * An RLP-encoded structure. + */ export type RlpStructuredData = string | Array; export { decodeRlp } from "./rlp-decode.js";