Sign In

@ailang/parse

Package Overview
Dependencies
Maintainers
1
Versions
23
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ailang/parse - npm Package Compare versions

Comparing version
0.7.1
to
0.8.0
+1
-1
package.json
{
"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",

# @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 @@

@@ -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 @@

@@ -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;
}