mirror of
https://github.com/Xahau/xahau.js.git
synced 2025-12-06 17:27:59 +00:00
* add apiVersion support to requests and AccountInfoResponse v1/v2 types * fix submitAndWait signature * update docker container README * update tests * fix apiVersion param in wrong position of Client.request * add integ tests * update HISTORY.md * fix request.api_version * update RIPPLED_DOCKER_IMAGE to use v2.1.0 * refactor Client.request signature * update rippled docker image * fix Client.requestAll * update rippled docker image to use v2.1.1 * update README * use import type * fix faucet; unrelated to PR * add api_version v2 support and set as default while providing support for v1 * refactor: add apiVersion to Client * resolve errors * use DeliverMax for isPartialPayment check * update fixtures * resolve lint errors * add API v1 support for isPartialPayment * update CONTRIBUTING * update accountTx JSDoc * revert deleted JSDoc comments in accountTx * update JSDoc for account_info response * only use client.apiVersion in Client.request() * add ledger_hash * remove API v1 comment from v2 model * update meta_blob JSDoc * delete second AccountTxRequest matching * add close_time_iso * set close_time_iso as optional field * add meta_blob to BaseResponse * Revert "add meta_blob to BaseResponse" This reverts commit 89794c629dc515915e28752d7c2552bfeab266a3. * use DEFAULT_API_VERSION throughout call stack * improve JSDoc explanation of ledger_index * remove this.apiVersion from getLedgerIndex * refactor Client.request() * refactor RequestManger.resolve() * add TODO to fix TxResponse type assertion * use @category ResponsesV1 for API v1 types * refactor accountTxHasPartialPayment() * remove TODO
226 lines
7.3 KiB
TypeScript
226 lines
7.3 KiB
TypeScript
import {
|
|
ResponseFormatError,
|
|
RippledError,
|
|
TimeoutError,
|
|
XrplError,
|
|
} from '../errors'
|
|
import type { APIVersion } from '../models'
|
|
import { Response, RequestResponseMap } from '../models/methods'
|
|
import { BaseRequest, ErrorResponse } from '../models/methods/baseMethod'
|
|
|
|
interface PromiseEntry<T> {
|
|
resolve: (value: T | PromiseLike<T>) => void
|
|
reject: (value: Error) => void
|
|
timer: ReturnType<typeof setTimeout>
|
|
}
|
|
|
|
/**
|
|
* Manage all the requests made to the websocket, and their async responses
|
|
* that come in from the WebSocket. Responses come in over the WS connection
|
|
* after-the-fact, so this manager will tie that response to resolve the
|
|
* original request.
|
|
*/
|
|
export default class RequestManager {
|
|
private nextId = 0
|
|
private readonly promisesAwaitingResponse = new Map<
|
|
string | number,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Necessary and typed wrapper in addPromise method
|
|
PromiseEntry<any>
|
|
>()
|
|
|
|
/**
|
|
* Adds a promise to the collection of promises awaiting response. Handles typing with generics.
|
|
*
|
|
* @template T The generic type parameter representing the resolved value type.
|
|
* @param newId - The identifier for the new promise.
|
|
* @param timer - The timer associated with the promise.
|
|
* @returns A promise that resolves to the specified generic type.
|
|
*/
|
|
public async addPromise<
|
|
R extends BaseRequest,
|
|
T = RequestResponseMap<R, APIVersion>,
|
|
>(newId: string | number, timer: ReturnType<typeof setTimeout>): Promise<T> {
|
|
return new Promise<T>((resolve, reject) => {
|
|
this.promisesAwaitingResponse.set(newId, {
|
|
resolve,
|
|
reject,
|
|
timer,
|
|
})
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Successfully resolves a request.
|
|
*
|
|
* @param id - ID of the request.
|
|
* @param response - Response to return.
|
|
* @throws Error if no existing promise with the given ID.
|
|
*/
|
|
public resolve(
|
|
id: string | number,
|
|
response: Partial<Response<APIVersion>>,
|
|
): void {
|
|
const promise = this.promisesAwaitingResponse.get(id)
|
|
if (promise == null) {
|
|
throw new XrplError(`No existing promise with id ${id}`, {
|
|
type: 'resolve',
|
|
response,
|
|
})
|
|
}
|
|
clearTimeout(promise.timer)
|
|
promise.resolve(response)
|
|
this.deletePromise(id)
|
|
}
|
|
|
|
/**
|
|
* Rejects a request.
|
|
*
|
|
* @param id - ID of the request.
|
|
* @param error - Error to throw with the reject.
|
|
* @throws Error if no existing promise with the given ID.
|
|
*/
|
|
public reject(id: string | number, error: Error): void {
|
|
const promise = this.promisesAwaitingResponse.get(id)
|
|
if (promise == null) {
|
|
throw new XrplError(`No existing promise with id ${id}`, {
|
|
type: 'reject',
|
|
error,
|
|
})
|
|
}
|
|
clearTimeout(promise.timer)
|
|
// TODO: figure out how to have a better stack trace for an error
|
|
promise.reject(error)
|
|
this.deletePromise(id)
|
|
}
|
|
|
|
/**
|
|
* Reject all pending requests.
|
|
*
|
|
* @param error - Error to throw with the reject.
|
|
*/
|
|
public rejectAll(error: Error): void {
|
|
this.promisesAwaitingResponse.forEach((_promise, id, _map) => {
|
|
this.reject(id, error)
|
|
this.deletePromise(id)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Creates a new WebSocket request. This sets up a timeout timer to catch
|
|
* hung responses, and a promise that will resolve with the response once
|
|
* the response is seen & handled.
|
|
*
|
|
* @param request - Request to create.
|
|
* @param timeout - Timeout length to catch hung responses.
|
|
* @returns Request ID, new request form, and the promise for resolving the request.
|
|
* @throws XrplError if request with the same ID is already pending.
|
|
*/
|
|
public createRequest<
|
|
R extends BaseRequest,
|
|
T = RequestResponseMap<R, APIVersion>,
|
|
>(request: R, timeout: number): [string | number, string, Promise<T>] {
|
|
let newId: string | number
|
|
if (request.id == null) {
|
|
newId = this.nextId
|
|
this.nextId += 1
|
|
} else {
|
|
newId = request.id
|
|
}
|
|
const newRequest = JSON.stringify({ ...request, id: newId })
|
|
// Typing required for Jest running in browser
|
|
const timer: ReturnType<typeof setTimeout> = setTimeout(() => {
|
|
this.reject(
|
|
newId,
|
|
new TimeoutError(
|
|
`Timeout for request: ${JSON.stringify(request)} with id ${newId}`,
|
|
request,
|
|
),
|
|
)
|
|
}, timeout)
|
|
/*
|
|
* Node.js won't exit if a timer is still running, so we tell Node to ignore.
|
|
* (Node will still wait for the request to complete).
|
|
*/
|
|
// The following type assertions are required to get this code to pass in browser environments
|
|
// where setTimeout has a different type
|
|
// eslint-disable-next-line max-len -- Necessary to disable both rules.
|
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access -- Reason above.
|
|
if ((timer as unknown as any).unref) {
|
|
// eslint-disable-next-line max-len -- Necessary to disable both rules.
|
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call -- Reason above.
|
|
;(timer as unknown as any).unref()
|
|
}
|
|
if (this.promisesAwaitingResponse.has(newId)) {
|
|
clearTimeout(timer)
|
|
throw new XrplError(
|
|
`Response with id '${newId}' is already pending`,
|
|
request,
|
|
)
|
|
}
|
|
const newPromise = new Promise<T>((resolve, reject) => {
|
|
this.promisesAwaitingResponse.set(newId, {
|
|
resolve,
|
|
reject,
|
|
timer,
|
|
})
|
|
})
|
|
|
|
return [newId, newRequest, newPromise]
|
|
}
|
|
|
|
/**
|
|
* Handle a "response". Responses match to the earlier request handlers,
|
|
* and resolve/reject based on the data received.
|
|
*
|
|
* @param response - The response to handle.
|
|
* @throws ResponseFormatError if the response format is invalid, RippledError if rippled returns an error.
|
|
*/
|
|
public handleResponse(
|
|
response: Partial<Response<APIVersion> | ErrorResponse>,
|
|
): void {
|
|
if (
|
|
response.id == null ||
|
|
!(typeof response.id === 'string' || typeof response.id === 'number')
|
|
) {
|
|
throw new ResponseFormatError('valid id not found in response', response)
|
|
}
|
|
if (!this.promisesAwaitingResponse.has(response.id)) {
|
|
return
|
|
}
|
|
if (response.status == null) {
|
|
const error = new ResponseFormatError('Response has no status')
|
|
this.reject(response.id, error)
|
|
}
|
|
if (response.status === 'error') {
|
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- We know this must be true
|
|
const errorResponse = response as Partial<ErrorResponse>
|
|
const error = new RippledError(
|
|
errorResponse.error_message ?? errorResponse.error,
|
|
errorResponse,
|
|
)
|
|
this.reject(response.id, error)
|
|
return
|
|
}
|
|
if (response.status !== 'success') {
|
|
const error = new ResponseFormatError(
|
|
`unrecognized response.status: ${response.status ?? ''}`,
|
|
response,
|
|
)
|
|
this.reject(response.id, error)
|
|
return
|
|
}
|
|
// status no longer needed because error is thrown if status is not "success"
|
|
delete response.status
|
|
this.resolve(response.id, response)
|
|
}
|
|
|
|
/**
|
|
* Delete a promise after it has been returned.
|
|
*
|
|
* @param id - ID of the request.
|
|
*/
|
|
private deletePromise(id: string | number): void {
|
|
this.promisesAwaitingResponse.delete(id)
|
|
}
|
|
}
|