@freemework/common
Version:
Common library of the Freemework Project.
233 lines (201 loc) • 6.94 kB
text/typescript
import { FDisposableBase } from "../lifecycle/index.js";
import { FExecutionContext } from "../execution_context/index.js";
import { FHttpClient } from "./f_http_client.js";
import { FLimit } from "../limit/f_limit.js";
import { FCancellationExecutionContext, FCancellationToken } from "../cancellation/index.js";
import { FLimitInMemory } from "../limit/f_limit_in_memory.js";
import * as http from "http";
import * as querystring from "querystring";
export class FWebClient extends FDisposableBase {
protected readonly _baseUrl: URL;
protected readonly _userAgent?: string;
private readonly _httpClient: FHttpClient.HttpInvokeChannel;
private readonly _limitHandle?: { instance: FLimit, timeout: number, isOwnInstance: boolean };
public constructor(url: URL | string, opts?: FWebClient.Opts) {
super();
this._baseUrl = typeof url === "string" ? new URL(url) : url;
if (opts !== undefined) {
const { limit, httpClient, userAgent } = opts;
if (limit !== undefined) {
this._limitHandle = FLimit.isLimitOpts(limit.instance)
? { instance: new FLimitInMemory(limit.instance), isOwnInstance: true, timeout: limit.timeout }
: { instance: limit.instance, isOwnInstance: false, timeout: limit.timeout };
}
if (httpClient !== undefined) {
if ("invoke" in httpClient) {
this._httpClient = httpClient;
} else {
this._httpClient = new FHttpClient({ ...httpClient });
}
} else {
this._httpClient = new FHttpClient();
}
if (userAgent !== undefined) {
this._userAgent = userAgent;
}
} else {
this._httpClient = new FHttpClient();
}
}
public get(
executionContext: FExecutionContext,
urlPath: string,
opts?: {
queryArgs?: { [key: string]: string },
headers?: http.OutgoingHttpHeaders,
limitWeight?: number
}
): Promise<FWebClient.Response> {
super.verifyNotDisposed();
const { queryArgs = undefined, headers = undefined, limitWeight = undefined } = (() => opts || {})();
const path = queryArgs !== undefined ?
urlPath + "?" + querystring.stringify(queryArgs) :
urlPath;
return this.invoke(executionContext, path, "GET", { headers: headers!, limitWeight: limitWeight! });
}
public postJson(
executionContext: FExecutionContext,
urlPath: string,
opts: {
postData: any,
headers?: http.OutgoingHttpHeaders,
limitWeight?: number
}
): Promise<FWebClient.Response> {
// Serialize JSON if body is object
const friendlyBody: Buffer = Buffer.from(JSON.stringify(opts.postData));
const friendlyHeaders: http.OutgoingHttpHeaders = opts.headers !== undefined ? { ...opts.headers } : {};
if (!("Content-Type" in friendlyHeaders)) {
friendlyHeaders["Content-Type"] = "application/json";
}
friendlyHeaders["Content-Length"] = friendlyBody.byteLength;
return this.invoke(executionContext, urlPath, "POST", {
headers: friendlyHeaders,
body: friendlyBody
});
}
public postForm(
executionContext: FExecutionContext,
urlPath: string,
opts: {
postArgs: { [key: string]: string },
headers?: http.OutgoingHttpHeaders,
limitWeight?: number
}
): Promise<FWebClient.Response> {
super.verifyNotDisposed();
const { postArgs = undefined, headers = undefined, limitWeight = undefined } = (() => opts || {})();
const bodyStr = postArgs && querystring.stringify(postArgs);
const { body, bodyLength } = (() => {
if (bodyStr !== undefined) {
const bodyBuffer = Buffer.from(bodyStr);
return { body: bodyBuffer, bodyLength: bodyBuffer.byteLength };
} else {
return { body: undefined, bodyLength: 0 };
}
})();
const friendlyHeaders = (() => {
const baseHeaders: http.OutgoingHttpHeaders = {
"Content-Type": "application/x-www-form-urlencoded",
"Content-Length": bodyLength
};
return headers !== undefined ? { ...baseHeaders, ...headers } : baseHeaders;
})();
return this.invoke(executionContext, urlPath, "POST", {
body: body!,
headers: friendlyHeaders,
limitWeight: limitWeight!
});
}
protected async invoke(
executionContext: FExecutionContext,
path: string,
method: FHttpClient.HttpMethod | string,
opts?: {
headers?: http.OutgoingHttpHeaders,
body?: Buffer,
limitWeight?: number
}): Promise<FWebClient.Response> {
super.verifyNotDisposed();
const cancellationToken: FCancellationToken = FCancellationExecutionContext.of(executionContext).cancellationToken;
let friendlyBody: Buffer | null = null;
let friendlyHeaders: http.OutgoingHttpHeaders = {};
let limitToken: FLimit.Token | null = null;
let limitWeight: number = 1;
if (opts !== undefined) {
const { headers, body } = opts;
if (headers !== undefined) {
friendlyHeaders = { ...headers };
}
if (this._userAgent !== undefined && !("User-Agent" in friendlyHeaders)) {
friendlyHeaders["User-Agent"] = this._userAgent;
}
if (body !== undefined) {
friendlyBody = body;
}
if (opts.limitWeight !== undefined) {
limitWeight = opts.limitWeight;
}
}
try {
if (this._limitHandle !== undefined) {
const a = await this._limitHandle.instance.accrueTokenLazy(limitWeight, this._limitHandle.timeout, cancellationToken);
limitToken = a;
} else {
friendlyHeaders["X-FLimit-Weight"] = limitWeight;
}
const url: URL = new URL(path, this._baseUrl);
const invokeArgs: { -readonly [P in keyof FHttpClient.Request]: FHttpClient.Request[P]; } = {
url, method, headers: friendlyHeaders
};
if (friendlyBody !== null) {
invokeArgs.body = friendlyBody;
}
const invokeResponse: FHttpClient.Response =
await this._httpClient.invoke(executionContext, invokeArgs);
const { statusCode, statusDescription, headers: responseHeaders, body } = invokeResponse;
const response: FWebClient.Response = {
get statusCode() { return statusCode; },
get statusDescription() { return statusDescription; },
get headers() { return responseHeaders; },
get body() { return body; },
get bodyAsJson() { return JSON.parse(body.toString()); }
};
if (limitToken !== null) {
limitToken.commit();
}
return response;
} catch (e) {
if (limitToken !== null) {
if (e instanceof FHttpClient.CommunicationError) {
// Token was not spent due server side did not do any jobs
limitToken.rollback();
} else {
limitToken.commit();
}
}
throw e;
}
}
protected async onDispose(): Promise<void> {
if (this._limitHandle !== undefined) {
if (this._limitHandle.isOwnInstance) {
await this._limitHandle.instance.dispose();
}
}
}
}
export namespace FWebClient {
export interface LimitOpts {
instance: FLimit.Opts | FLimit;
timeout: number;
}
export interface Opts {
readonly httpClient?: FHttpClient.Opts | FHttpClient.HttpInvokeChannel;
readonly limit?: LimitOpts;
readonly userAgent?: string;
}
export interface Response extends FHttpClient.Response {
readonly bodyAsJson: any;
}
}