UNPKG

@flatfile/safe-api

Version:

Flatfile Safe API client with streaming capabilities

246 lines (245 loc) 9.87 kB
"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..."); });