@ailang/parse
Advanced tools
+1
-1
| { | ||
| "name": "@ailang/parse", | ||
| "version": "0.7.1", | ||
| "version": "0.8.0", | ||
| "mcpName": "io.github.sunholo-data/parse", | ||
@@ -5,0 +5,0 @@ "description": "JavaScript/TypeScript client and MCP server for the AILANG Parse document parsing API", |
+20
-1
| # @ailang/parse | ||
| JavaScript/TypeScript client and MCP server for the [AILANG Parse](https://www.sunholo.com/docparse/) document parsing API. Parse 18 formats (including LaTeX/arXiv), generate 9 — zero dependencies, native fetch. | ||
| JavaScript/TypeScript client and MCP server for the [AILANG Parse](https://www.sunholo.com/docparse/) document parsing API. Parse 19 formats (including LaTeX/arXiv and RTF), generate 9 — zero dependencies, native fetch. | ||
@@ -194,2 +194,21 @@ ## Install | ||
| ### Retry on transient failures | ||
| `parse` / `parseFile` can retry transient AI-provider failures (the server | ||
| returns `502`/`503`/`504`, and marks safe-to-retry `5xx` with | ||
| `X-AilangParse-Replayable`). Retry is **off by default** — opt in with `retry`: | ||
| ```typescript | ||
| const client = new DocParse({ | ||
| apiKey: 'dp_your_key', | ||
| retry: { | ||
| maxRetries: 3, // default 0 (no retry) | ||
| retryableStatuses: [502, 503, 504], | ||
| respectReplayable: true, // also retry replayable 5xx | ||
| backoffBaseMs: 1000, // delay N = min(base * 2**N, max) | ||
| backoffMaxMs: 30000, | ||
| }, | ||
| }); | ||
| ``` | ||
| ## Browser Usage | ||
@@ -196,0 +215,0 @@ |
+100
-56
@@ -6,3 +6,3 @@ /** | ||
| import type { ParseResult, HealthResult, FormatsResult, DocParseOptions, ResponseMeta } from "./types.js"; | ||
| import type { ParseResult, HealthResult, FormatsResult, DocParseOptions, ResponseMeta, RetryPolicy } from "./types.js"; | ||
| import { DocParseError, AuthError, QuotaError } from "./types.js"; | ||
@@ -19,2 +19,4 @@ import { KeyManager } from "./keys.js"; | ||
| private timeout: number; | ||
| /** Resolved retry policy (defaults applied). */ | ||
| private retry: Required<RetryPolicy>; | ||
| /** | ||
@@ -60,5 +62,56 @@ * Stored key id, populated from saved credentials or a successful | ||
| this.keys = new KeyManager(this); | ||
| const r = opts?.retry ?? {}; | ||
| this.retry = { | ||
| maxRetries: r.maxRetries ?? 0, | ||
| retryableStatuses: r.retryableStatuses ?? [502, 503, 504], | ||
| respectReplayable: r.respectReplayable ?? true, | ||
| backoffBaseMs: r.backoffBaseMs ?? 1000, | ||
| backoffMaxMs: r.backoffMaxMs ?? 30000, | ||
| }; | ||
| } | ||
| private _shouldRetry(status: number, replayable: boolean): boolean { | ||
| if (this.retry.maxRetries <= 0) return false; | ||
| if (this.retry.retryableStatuses.includes(status)) return true; | ||
| return this.retry.respectReplayable && status >= 500 && status < 600 && replayable; | ||
| } | ||
| private _retryDelayMs(attempt: number): number { | ||
| return Math.min(this.retry.backoffBaseMs * 2 ** attempt, this.retry.backoffMaxMs); | ||
| } | ||
| private static _sleep(ms: number): Promise<void> { | ||
| return new Promise((resolve) => setTimeout(resolve, ms)); | ||
| } | ||
| /** | ||
| * Issue `makeRequest()` and retry transient failures (502/503/504, plus | ||
| * replayable 5xx) per the retry policy. `makeRequest` must build a fresh | ||
| * Request — including its own AbortController/timeout — on each call, since | ||
| * fetch consumes the body. Network errors are retried on the same budget as | ||
| * HTTP 5xx. Returns the final Response for the caller to unwrap. | ||
| */ | ||
| private async _sendWithRetry(makeRequest: () => Promise<Response>): Promise<Response> { | ||
| let attempt = 0; | ||
| for (;;) { | ||
| let resp: Response; | ||
| try { | ||
| resp = await makeRequest(); | ||
| } catch (e) { | ||
| if (attempt >= this.retry.maxRetries) throw e; | ||
| await DocParse._sleep(this._retryDelayMs(attempt)); | ||
| attempt++; | ||
| continue; | ||
| } | ||
| const replayable = (resp.headers.get("X-AilangParse-Replayable") || "").toLowerCase() === "true"; | ||
| if (!this._shouldRetry(resp.status, replayable) || attempt >= this.retry.maxRetries) { | ||
| return resp; | ||
| } | ||
| await DocParse._sleep(this._retryDelayMs(attempt)); | ||
| attempt++; | ||
| } | ||
| } | ||
| /** | ||
| * Raise the right exception type from a non-2xx response, populating | ||
@@ -128,21 +181,17 @@ * requestId / replayable / details / suggestedFix from headers + body. | ||
| const controller = new AbortController(); | ||
| const timer = setTimeout(() => controller.abort(), this.timeout); | ||
| try { | ||
| const headers: Record<string, string> = { "Content-Type": "application/json" }; | ||
| if (this.apiKey) headers["x-api-key"] = this.apiKey; | ||
| const resp = await fetch(url, { | ||
| method: "POST", | ||
| headers, | ||
| body: JSON.stringify(body), | ||
| signal: controller.signal, | ||
| }); | ||
| await DocParse._raiseForResponse(resp); | ||
| const meta = DocParse._extractMeta(resp.headers); | ||
| const result = DocParse._buildParseResult(this._unwrap(await resp.json()), outputFormat); | ||
| result.responseMeta = meta; | ||
| return result; | ||
| } finally { | ||
| clearTimeout(timer); | ||
| } | ||
| const headers: Record<string, string> = { "Content-Type": "application/json" }; | ||
| if (this.apiKey) headers["x-api-key"] = this.apiKey; | ||
| const payload = JSON.stringify(body); | ||
| const resp = await this._sendWithRetry(() => { | ||
| const controller = new AbortController(); | ||
| const timer = setTimeout(() => controller.abort(), this.timeout); | ||
| return fetch(url, { method: "POST", headers, body: payload, signal: controller.signal }) | ||
| .finally(() => clearTimeout(timer)); | ||
| }); | ||
| await DocParse._raiseForResponse(resp); | ||
| const meta = DocParse._extractMeta(resp.headers); | ||
| const result = DocParse._buildParseResult(this._unwrap(await resp.json()), outputFormat); | ||
| result.responseMeta = meta; | ||
| return result; | ||
| } | ||
@@ -169,49 +218,44 @@ | ||
| const url = this.baseUrl + "/api/v1/parse"; | ||
| let form: any; | ||
| // Detect Node.js vs browser | ||
| const isNode = typeof process !== "undefined" && process.versions?.node; | ||
| // Build a fresh FormData per attempt — fetch consumes the body, so a retry | ||
| // must re-create it. (Node: re-wrap the file bytes; browser: re-append the File.) | ||
| let makeForm: () => FormData; | ||
| if (isNode) { | ||
| // Node.js: read file from disk using native FormData/Blob (Node 18+) | ||
| const { readFileSync } = await import("fs"); | ||
| const { basename } = await import("path"); | ||
| const fileData = readFileSync(filepath); | ||
| const blob = new Blob([fileData]); | ||
| form = new FormData(); | ||
| form.append("filepath", blob, basename(filepath)); | ||
| form.append("outputFormat", outputFormat); | ||
| if (this.apiKey) form.append("apiKey", this.apiKey); | ||
| const name = basename(filepath); | ||
| makeForm = () => { | ||
| const form = new FormData(); | ||
| form.append("filepath", new Blob([fileData]), name); | ||
| form.append("outputFormat", outputFormat); | ||
| if (this.apiKey) form.append("apiKey", this.apiKey); | ||
| return form; | ||
| }; | ||
| } else { | ||
| // Browser: expect a File object or use native FormData | ||
| form = new FormData(); | ||
| form.append("filepath", filepath as any); | ||
| form.append("outputFormat", outputFormat); | ||
| if (this.apiKey) form.append("apiKey", this.apiKey); | ||
| makeForm = () => { | ||
| const form = new FormData(); | ||
| form.append("filepath", filepath as any); | ||
| form.append("outputFormat", outputFormat); | ||
| if (this.apiKey) form.append("apiKey", this.apiKey); | ||
| return form; | ||
| }; | ||
| } | ||
| const controller = new AbortController(); | ||
| const timer = setTimeout(() => controller.abort(), this.timeout); | ||
| const headers: Record<string, string> = {}; | ||
| if (this.apiKey) headers["x-api-key"] = this.apiKey; | ||
| try { | ||
| const headers: Record<string, string> = {}; | ||
| if (this.apiKey) headers["x-api-key"] = this.apiKey; | ||
| const resp = await fetch(url, { | ||
| method: "POST", | ||
| headers, | ||
| body: form, | ||
| signal: controller.signal, | ||
| }); | ||
| await DocParse._raiseForResponse(resp); | ||
| const meta = DocParse._extractMeta(resp.headers); | ||
| const result = DocParse._buildParseResult(this._unwrap(await resp.json()), outputFormat); | ||
| result.responseMeta = meta; | ||
| return result; | ||
| } finally { | ||
| clearTimeout(timer); | ||
| } | ||
| const resp = await this._sendWithRetry(() => { | ||
| const controller = new AbortController(); | ||
| const timer = setTimeout(() => controller.abort(), this.timeout); | ||
| return fetch(url, { method: "POST", headers, body: makeForm() as any, signal: controller.signal }) | ||
| .finally(() => clearTimeout(timer)); | ||
| }); | ||
| await DocParse._raiseForResponse(resp); | ||
| const meta = DocParse._extractMeta(resp.headers); | ||
| const result = DocParse._buildParseResult(this._unwrap(await resp.json()), outputFormat); | ||
| result.responseMeta = meta; | ||
| return result; | ||
| } | ||
@@ -218,0 +262,0 @@ |
+28
-0
@@ -278,2 +278,28 @@ /** AILANG Parse types — Block ADT, ParseResult, metadata, errors. */ | ||
| /** | ||
| * Retry configuration for transient parse failures. Mirrors the Python SDK's | ||
| * RetryPolicy. The server returns 502/503/504 for transient AI-provider | ||
| * failures and marks safe-to-retry 5xx with `X-AilangParse-Replayable`. | ||
| * | ||
| * The default (no `retry` option) does NOT retry — opt in with `maxRetries`: | ||
| * | ||
| * ```ts | ||
| * new DocParse({ retry: { maxRetries: 3 } }); | ||
| * ``` | ||
| * | ||
| * Delay before retry N is `min(backoffBaseMs * 2 ** N, backoffMaxMs)`. | ||
| */ | ||
| export interface RetryPolicy { | ||
| /** Maximum number of retries (0 = no retry). */ | ||
| maxRetries?: number; | ||
| /** HTTP statuses that always trigger a retry. Default `[502, 503, 504]`. */ | ||
| retryableStatuses?: number[]; | ||
| /** Also retry any 5xx carrying `X-AilangParse-Replayable: true`. Default `true`. */ | ||
| respectReplayable?: boolean; | ||
| /** Exponential backoff base, milliseconds. Default `1000`. */ | ||
| backoffBaseMs?: number; | ||
| /** Upper bound on per-retry delay, milliseconds. Default `30000`. */ | ||
| backoffMaxMs?: number; | ||
| } | ||
| export interface DocParseOptions { | ||
@@ -283,2 +309,4 @@ apiKey?: string; | ||
| timeout?: number; | ||
| /** Retry policy for transient parse failures. Default: no retry. */ | ||
| retry?: RetryPolicy; | ||
| } |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
51203
8.25%1226
6.06%236
8.76%