@flatfile/safe-api
Version:
Flatfile Safe API client with streaming capabilities
246 lines (245 loc) • 9.87 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SafeRequest = void 0;
const modern_async_1 = require("modern-async");
const request_errors_1 = require("./request.errors");
const rate_limit_1 = require("../rate-limit");
const config_1 = require("../config");
class SafeRequest {
constructor(params, query, options) {
this.params = params;
this.query = query;
this.rateLimitManager = rate_limit_1.RateLimitManager.getInstance();
this.config = config_1.InternalConfig.getInstance();
/**
* prefix url with the API base if this is true
*/
this.isApiRequest = true;
/**
* This request can fail safely in the event of an error or rate limit hit
* and doesn't need to be retried. Useful for things like progress reporting.
*
* @default false
*/
this.canMiss = false;
this.isRaw = false;
this.attempts = 0;
this.nextAttemptDelay = 0;
this._isRun = false;
this._headers = {};
if (options) {
this.retryOptions = options;
}
}
setHeaders(headers) {
this._headers = { ...this._headers, ...headers };
return this;
}
async fetch(method) {
const headers = {
Authorization: `Bearer ${process.env.FLATFILE_API_KEY || process.env.FLATFILE_BEARER_TOKEN}`,
};
if (!this.isRaw) {
headers["Content-Type"] = "application/json";
}
if (this._headers) {
Object.assign(headers, this._headers);
}
const body = this.payload ? await this.serializeBody(this.payload) : undefined;
const url = this.buildUrl();
let lastError = null;
const retryConfig = this.config.mergeRetryConfig(this.retryOptions);
for (let attempt = 1; attempt <= retryConfig.maxAttempts; attempt++) {
try {
if (this.config.debug) {
console.log(`DEBUG: Making attempt ${attempt}/${retryConfig.maxAttempts} for ${method} request to ${url}`);
}
const res = await fetch(url, {
method,
headers,
body,
});
try {
await this.handleResponseCodes(res);
this.afterSuccess();
const responseBody = this.isRaw ? await res.text() : await res.json();
if (this.config.debug) {
console.log(`DEBUG: Response body:`, responseBody);
}
return await this.parseBody(responseBody);
}
catch (e) {
if (e instanceof request_errors_1.RateError && retryConfig.retry) {
lastError = e;
// Calculate delay for next attempt
this.nextAttemptDelay = this.rateLimitManager.getRetryDelay(e.res.headers);
const backoffMultiplier = Math.pow(2, attempt - 1);
this.nextAttemptDelay = this.nextAttemptDelay * backoffMultiplier;
if (this.config.debug) {
console.log(`DEBUG: Rate limited (429). Will wait ${this.nextAttemptDelay}ms before retry`);
}
// Wait before continuing to next attempt
await (0, modern_async_1.asyncSleep)(this.nextAttemptDelay);
continue;
}
throw e;
}
}
catch (e) {
lastError = e;
if (!(e instanceof request_errors_1.RateError) || attempt === retryConfig.maxAttempts) {
throw e;
}
}
}
throw lastError || new Error('Max attempts reached');
}
async parseBody(body) {
return body;
}
afterSuccess() { }
serializeBody(input) {
return JSON.stringify(input);
}
async handleResponseCodes(res) {
let rawText = "";
if (res.status >= 300) {
try {
rawText = await res.text();
}
catch (e) {
// this isn't criticla
}
}
if ([500, 502, 503].includes(res.status)) {
throw new request_errors_1.ServerError(this, res, rawText);
}
if ([504, 408].includes(res.status)) {
throw new request_errors_1.TimeoutError(this, res, rawText);
}
if (res.status === 429) {
throw new request_errors_1.RateError(this, res, rawText);
}
if (res.status === 400) {
throw new request_errors_1.PayloadError(this, res, rawText);
}
if (res.status === 404) {
throw new request_errors_1.NotFoundError(this, res, rawText);
}
if ([401, 403].includes(res.status)) {
throw new request_errors_1.UnauthorizedError(this, res, rawText);
}
if ([301, 302, 303, 307, 308].includes(res.status)) {
throw new request_errors_1.RedirectError(this, res, rawText);
}
// allow not modified as non error
if (res.status === 304) {
return true;
}
// all other statuses
if (res.status >= 300) {
throw new request_errors_1.FatalError(this, res, rawText);
}
return true;
}
async _run() {
try {
if (this.nextAttemptDelay) {
if (this.config.debug) {
console.log(`DEBUG: Waiting ${this.nextAttemptDelay}ms before attempt ${this.attempts + 1}`);
}
await (0, modern_async_1.asyncSleep)(this.nextAttemptDelay);
}
this.attempts++;
if (this.config.debug) {
console.log(`DEBUG: Making attempt ${this.attempts} for ${this.buildUrl()}`);
}
return await this.execute();
}
catch (e) {
if (this.config.debug) {
console.log(`DEBUG: Request failed with error:`, e instanceof Error ? e.message : e);
}
const retryConfig = this.config.mergeRetryConfig(this.retryOptions);
if (e instanceof request_errors_1.RecoverableError && retryConfig.retry) {
if (this.config.debug) {
console.log(`DEBUG: Recoverable error detected. Attempt ${this.attempts}/${retryConfig.maxAttempts}`);
}
if (this.attempts < retryConfig.maxAttempts) {
if (e instanceof request_errors_1.RateError) {
this.nextAttemptDelay = this.rateLimitManager.getRetryDelay(e.res.headers);
const backoffMultiplier = Math.pow(2, this.attempts - 1);
this.nextAttemptDelay = this.nextAttemptDelay * backoffMultiplier;
if (this.config.debug) {
console.log(`DEBUG: Rate limited (429). Attempt ${this.attempts}/${retryConfig.maxAttempts}. Will wait ${this.nextAttemptDelay}ms before retry.`);
}
}
else {
this.nextAttemptDelay = retryConfig.timeoutDelay * Math.pow(2, this.attempts - 1);
if (this.config.debug) {
console.log(`DEBUG: Other recoverable error. Will wait ${this.nextAttemptDelay}ms before retry.`);
}
}
return this._run();
}
else {
if (this.config.debug) {
console.log(`DEBUG: Max attempts (${retryConfig.maxAttempts}) reached. Giving up.`);
}
throw new request_errors_1.RetryError(this, e);
}
}
if (this.config.debug) {
console.log(`DEBUG: Non-recoverable error or retries not enabled. Failing request.`);
}
throw e;
}
}
async run() {
if (this._isRun) {
throw new Error("You cannot execute the same request instance twice.");
}
this._isRun = true;
return this._run();
}
defaultQuery() {
return {};
}
buildQuery() {
const query = this.query ? { ...this.defaultQuery(), ...this.query } : this.defaultQuery();
const queryParams = new URLSearchParams();
Object.entries(query).forEach(([key, value]) => {
if (value !== undefined) {
if (Array.isArray(value)) {
value.map((v) => queryParams.append(key, String(v)));
}
else {
queryParams.set(key, String(value));
}
}
});
return queryParams;
}
/**
* @todo handle mismatched params
*/
buildPath() {
const params = Array.isArray(this.params) ? [...this.params] : this.params.split("/");
const basePath = this.path.split("/").map((seg) => (seg.startsWith(":") ? params.shift() : seg));
return basePath.join("/");
}
buildUrl() {
const apiBase = process.env.FLATFILE_API_URL || process.env.AGENT_INTERNAL_URL;
const query = this.buildQuery().toString();
const path = this.buildPath();
return `${this.isApiRequest ? apiBase : ""}${path}${query ? `?${query}` : ""}`;
}
then(resolve, reject) {
return this.run().then(resolve, reject);
}
}
exports.SafeRequest = SafeRequest;
process.on("uncaughtException", function (err) {
console.error(err.stack);
console.log("Node NOT Exiting...");
});