unstructured-transform-client 0.18.26 → 0.18.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +24 -0
- package/dist/client.d.ts +17 -10
- package/dist/client.js +37 -16
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -0
- package/dist/multipart.d.ts +2 -0
- package/dist/multipart.js +2 -0
- package/dist/retry.d.ts +17 -1
- package/dist/retry.js +111 -17
- package/dist/sse.d.ts +1 -0
- package/dist/sse.js +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.18.28
|
|
4
|
+
|
|
5
|
+
- Add a 60 second per-request timeout for ordinary requests, with per-client
|
|
6
|
+
and per-call overrides.
|
|
7
|
+
- Keep `GET` and `DELETE` retryable after a timeout while preserving the
|
|
8
|
+
non-idempotent `POST` rule: writes are not retried after a timeout because
|
|
9
|
+
the service may have received them.
|
|
10
|
+
- Ask Node's default fetch to close `POST` connections so non-idempotent
|
|
11
|
+
submits do not reuse stale pooled sockets.
|
|
12
|
+
- Keep `jobs.stream` long-lived; callers still stop it with an `AbortSignal`.
|
package/README.md
CHANGED
|
@@ -32,6 +32,19 @@ naming `UNSTRUCTURED_API_KEY`.
|
|
|
32
32
|
|
|
33
33
|
Requires Node 22 or newer, for native `fetch` and `FormData`.
|
|
34
34
|
|
|
35
|
+
Ordinary request/response calls have a 60 second per-request timeout. Override
|
|
36
|
+
it per client or per call with `timeoutMs`:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const client = new TransformClient({ timeoutMs: 120_000 });
|
|
40
|
+
await client.jobs.get(jobId, { timeoutMs: 30_000 });
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`jobs.stream` is long-lived and opts out of the SDK request timeout, including
|
|
44
|
+
the initial connection. Pass an `AbortSignal` when you want to stop it.
|
|
45
|
+
Large uploads and downloads also share the same 60 second default; raise
|
|
46
|
+
`timeoutMs` for slow links.
|
|
47
|
+
|
|
35
48
|
## The three flows
|
|
36
49
|
|
|
37
50
|
**Parse a document.**
|
|
@@ -164,6 +177,17 @@ connection or a DNS failure. A read timeout or a `5xx` on a write is not
|
|
|
164
177
|
retried, because the service may have acted on it already and a second attempt
|
|
165
178
|
could duplicate the effect.
|
|
166
179
|
|
|
180
|
+
The retry budget is separate from the in-flight request timeout, so a timed-out
|
|
181
|
+
`GET` or `DELETE` still gets its configured retry attempts while total backoff
|
|
182
|
+
time remains bounded. With the default policy, three 60 second attempts are
|
|
183
|
+
allowed.
|
|
184
|
+
|
|
185
|
+
When the SDK uses Node's default `fetch`, non-idempotent `POST` requests include
|
|
186
|
+
`Connection: close` so the server closes the connection after the response. This
|
|
187
|
+
limits reuse after a write, but it cannot guarantee which pooled socket Node
|
|
188
|
+
selects for the current request. If you provide `fetchApi`, the SDK leaves
|
|
189
|
+
connection management to that implementation.
|
|
190
|
+
|
|
167
191
|
## Documents need a filename
|
|
168
192
|
|
|
169
193
|
Pass a `File`, or `{ data, filename }`. A bare `Blob` is refused:
|
package/dist/client.d.ts
CHANGED
|
@@ -38,9 +38,15 @@ export interface TransformClientOptions {
|
|
|
38
38
|
userAgentSuffix?: string;
|
|
39
39
|
/** Override the fetch implementation, e.g. for tests or a proxy agent. */
|
|
40
40
|
fetchApi?: typeof fetch;
|
|
41
|
+
/** Per-request timeout for ordinary request/response calls. Defaults to 60s. */
|
|
42
|
+
timeoutMs?: number;
|
|
41
43
|
/** Per-client retries; pass `false` to disable them. */
|
|
42
44
|
retries?: RetryConfig | false;
|
|
43
45
|
}
|
|
46
|
+
export interface RequestOptions {
|
|
47
|
+
signal?: AbortSignal;
|
|
48
|
+
timeoutMs?: number;
|
|
49
|
+
}
|
|
44
50
|
export declare class TransformClient {
|
|
45
51
|
readonly parse: ParseNamespace;
|
|
46
52
|
readonly extract: ExtractNamespace;
|
|
@@ -50,10 +56,11 @@ export declare class TransformClient {
|
|
|
50
56
|
/** @internal */ readonly requestHeaders: Record<string, string>;
|
|
51
57
|
/** @internal */ readonly configuration: Configuration;
|
|
52
58
|
/** @internal */ readonly fetchApi?: typeof fetch;
|
|
59
|
+
/** @internal */ readonly timeoutMs: number;
|
|
53
60
|
/** @internal */ readonly retries: Required<RetryConfig> | undefined;
|
|
54
61
|
constructor(options?: TransformClientOptions);
|
|
55
62
|
}
|
|
56
|
-
export interface ParseOptions {
|
|
63
|
+
export interface ParseOptions extends RequestOptions {
|
|
57
64
|
input?: FileInput;
|
|
58
65
|
fileId?: string;
|
|
59
66
|
schema?: Record<string, unknown> | string;
|
|
@@ -75,13 +82,13 @@ declare class ParseNamespace {
|
|
|
75
82
|
*/
|
|
76
83
|
run(options: ParseOptions): Promise<Outcome<ParseResult>>;
|
|
77
84
|
}
|
|
78
|
-
export interface ExtractOptions {
|
|
85
|
+
export interface ExtractOptions extends RequestOptions {
|
|
79
86
|
parseId: string;
|
|
80
87
|
schema: Record<string, unknown>;
|
|
81
88
|
prompt?: string;
|
|
82
89
|
waitSeconds?: number;
|
|
83
90
|
}
|
|
84
|
-
export interface ExtractFromDocumentOptions {
|
|
91
|
+
export interface ExtractFromDocumentOptions extends RequestOptions {
|
|
85
92
|
input?: FileInput;
|
|
86
93
|
fileId?: string;
|
|
87
94
|
/** The original filename to attach when extracting from an already-uploaded file. */
|
|
@@ -104,11 +111,11 @@ declare class ExtractNamespace {
|
|
|
104
111
|
*/
|
|
105
112
|
fromDocument(options: ExtractFromDocumentOptions): Promise<Outcome<ParseResult>>;
|
|
106
113
|
}
|
|
107
|
-
export interface JobsGetOptions {
|
|
114
|
+
export interface JobsGetOptions extends RequestOptions {
|
|
108
115
|
output?: OutputFormat;
|
|
109
116
|
include?: string[];
|
|
110
117
|
}
|
|
111
|
-
export interface JobsListOptions {
|
|
118
|
+
export interface JobsListOptions extends RequestOptions {
|
|
112
119
|
cursor?: string;
|
|
113
120
|
limit?: number;
|
|
114
121
|
status?: JobStatus;
|
|
@@ -125,8 +132,8 @@ declare class JobsNamespace {
|
|
|
125
132
|
get(jobId: string, options?: JobsGetOptions): Promise<JobResult>;
|
|
126
133
|
list(options?: JobsListOptions): Promise<JobPage>;
|
|
127
134
|
iterate(options?: JobsListOptions): AsyncGenerator<JobPage["jobs"][number], void, undefined>;
|
|
128
|
-
cancel(jobId: string): Promise<JobResult>;
|
|
129
|
-
delete(jobId: string): Promise<void>;
|
|
135
|
+
cancel(jobId: string, options?: RequestOptions): Promise<JobResult>;
|
|
136
|
+
delete(jobId: string, options?: RequestOptions): Promise<void>;
|
|
130
137
|
/**
|
|
131
138
|
* Progress events for a running job, as they happen.
|
|
132
139
|
*
|
|
@@ -139,8 +146,8 @@ declare class JobsNamespace {
|
|
|
139
146
|
declare class UploadNamespace {
|
|
140
147
|
private readonly client;
|
|
141
148
|
constructor(client: TransformClient);
|
|
142
|
-
run(input: FileInput): Promise<UploadResult>;
|
|
143
|
-
get(fileId: string): Promise<Blob>;
|
|
144
|
-
delete(fileId: string): Promise<void>;
|
|
149
|
+
run(input: FileInput, options?: RequestOptions): Promise<UploadResult>;
|
|
150
|
+
get(fileId: string, options?: RequestOptions): Promise<Blob>;
|
|
151
|
+
delete(fileId: string, options?: RequestOptions): Promise<void>;
|
|
145
152
|
}
|
|
146
153
|
export {};
|
package/dist/client.js
CHANGED
|
@@ -29,7 +29,7 @@ import { asFile } from "./files.js";
|
|
|
29
29
|
import * as hostHeaders from "./hostHeaders.js";
|
|
30
30
|
import { postMultipart } from "./multipart.js";
|
|
31
31
|
import { resolveOutcome } from "./outcome.js";
|
|
32
|
-
import { fetchWithRetries, normalizeRetries } from "./retry.js";
|
|
32
|
+
import { DEFAULT_TIMEOUT_MS, fetchWithRetries, normalizeRetries, } from "./retry.js";
|
|
33
33
|
import { streamJobEvents } from "./sse.js";
|
|
34
34
|
export const DEFAULT_SERVER_URL = "https://transform.unstructured.io";
|
|
35
35
|
/** Matches the service's `securitySchemes`; see openapi.yaml. */
|
|
@@ -51,6 +51,7 @@ export class TransformClient {
|
|
|
51
51
|
/** @internal */ requestHeaders;
|
|
52
52
|
/** @internal */ configuration;
|
|
53
53
|
/** @internal */ fetchApi;
|
|
54
|
+
/** @internal */ timeoutMs;
|
|
54
55
|
/** @internal */ retries;
|
|
55
56
|
constructor(options = {}) {
|
|
56
57
|
const apiKey = options.apiKey ??
|
|
@@ -64,7 +65,8 @@ export class TransformClient {
|
|
|
64
65
|
}
|
|
65
66
|
this.serverUrl = (options.serverUrl ?? DEFAULT_SERVER_URL).replace(/\/+$/, "");
|
|
66
67
|
this.fetchApi = options.fetchApi;
|
|
67
|
-
this.
|
|
68
|
+
this.timeoutMs = validateTimeoutMs(options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
69
|
+
this.retries = normalizeRetries(options.retries, this.timeoutMs);
|
|
68
70
|
const auth = hasApiKey
|
|
69
71
|
? { [API_KEY_HEADER]: apiKey }
|
|
70
72
|
: { Authorization: `Bearer ${options.bearerToken}` };
|
|
@@ -84,6 +86,7 @@ export class TransformClient {
|
|
|
84
86
|
url: String(url),
|
|
85
87
|
init: init ?? {},
|
|
86
88
|
fetchApi: options.fetchApi,
|
|
89
|
+
timeoutMs: this.timeoutMs,
|
|
87
90
|
}),
|
|
88
91
|
});
|
|
89
92
|
this.parse = new ParseNamespace(this);
|
|
@@ -96,6 +99,21 @@ export class TransformClient {
|
|
|
96
99
|
function prefer(waitSeconds) {
|
|
97
100
|
return waitSeconds === undefined ? undefined : `wait=${waitSeconds}`;
|
|
98
101
|
}
|
|
102
|
+
function requestInit(options) {
|
|
103
|
+
const init = {};
|
|
104
|
+
if (options.signal !== undefined)
|
|
105
|
+
init.signal = options.signal;
|
|
106
|
+
if (options.timeoutMs !== undefined) {
|
|
107
|
+
init.timeoutMs = validateTimeoutMs(options.timeoutMs);
|
|
108
|
+
}
|
|
109
|
+
return init.signal !== undefined || init.timeoutMs !== undefined ? init : undefined;
|
|
110
|
+
}
|
|
111
|
+
function validateTimeoutMs(timeoutMs) {
|
|
112
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
113
|
+
throw new RangeError("timeoutMs must be a positive finite number of milliseconds");
|
|
114
|
+
}
|
|
115
|
+
return timeoutMs;
|
|
116
|
+
}
|
|
99
117
|
class ParseNamespace {
|
|
100
118
|
client;
|
|
101
119
|
constructor(client) {
|
|
@@ -125,7 +143,7 @@ class ParseNamespace {
|
|
|
125
143
|
: options.schema,
|
|
126
144
|
prompt: options.prompt,
|
|
127
145
|
profile: options.profile,
|
|
128
|
-
}));
|
|
146
|
+
}, requestInit(options)));
|
|
129
147
|
}
|
|
130
148
|
}
|
|
131
149
|
class ExtractNamespace {
|
|
@@ -142,7 +160,7 @@ class ExtractNamespace {
|
|
|
142
160
|
prompt: options.prompt,
|
|
143
161
|
},
|
|
144
162
|
prefer: prefer(options.waitSeconds),
|
|
145
|
-
}));
|
|
163
|
+
}, requestInit(options)));
|
|
146
164
|
}
|
|
147
165
|
/**
|
|
148
166
|
* Extract directly from a raw document, in one call.
|
|
@@ -174,6 +192,8 @@ class ExtractNamespace {
|
|
|
174
192
|
profile: options.profile,
|
|
175
193
|
},
|
|
176
194
|
waitSeconds: options.waitSeconds,
|
|
195
|
+
signal: options.signal,
|
|
196
|
+
timeoutMs: options.timeoutMs === undefined ? undefined : validateTimeoutMs(options.timeoutMs),
|
|
177
197
|
fromJSON: ParseResultFromJSON,
|
|
178
198
|
});
|
|
179
199
|
}
|
|
@@ -188,7 +208,7 @@ class JobsNamespace {
|
|
|
188
208
|
jobId,
|
|
189
209
|
output: options.output,
|
|
190
210
|
include: options.include,
|
|
191
|
-
});
|
|
211
|
+
}, requestInit(options));
|
|
192
212
|
}
|
|
193
213
|
async list(options = {}) {
|
|
194
214
|
return new JobsApi(this.client.configuration).jobsList({
|
|
@@ -196,7 +216,7 @@ class JobsNamespace {
|
|
|
196
216
|
limit: options.limit,
|
|
197
217
|
status: options.status,
|
|
198
218
|
operation: options.operation,
|
|
199
|
-
});
|
|
219
|
+
}, requestInit(options));
|
|
200
220
|
}
|
|
201
221
|
async *iterate(options = {}) {
|
|
202
222
|
let cursor = options.cursor;
|
|
@@ -210,11 +230,11 @@ class JobsNamespace {
|
|
|
210
230
|
return;
|
|
211
231
|
}
|
|
212
232
|
}
|
|
213
|
-
async cancel(jobId) {
|
|
214
|
-
return new JobsApi(this.client.configuration).jobsCancel({ jobId });
|
|
233
|
+
async cancel(jobId, options = {}) {
|
|
234
|
+
return new JobsApi(this.client.configuration).jobsCancel({ jobId }, requestInit(options));
|
|
215
235
|
}
|
|
216
|
-
async delete(jobId) {
|
|
217
|
-
await new JobsApi(this.client.configuration).jobsDelete({ jobId });
|
|
236
|
+
async delete(jobId, options = {}) {
|
|
237
|
+
await new JobsApi(this.client.configuration).jobsDelete({ jobId }, requestInit(options));
|
|
218
238
|
}
|
|
219
239
|
/**
|
|
220
240
|
* Progress events for a running job, as they happen.
|
|
@@ -240,6 +260,7 @@ class JobsNamespace {
|
|
|
240
260
|
signal: options.signal,
|
|
241
261
|
fetchApi: this.client.fetchApi,
|
|
242
262
|
retries: this.client.retries,
|
|
263
|
+
timeoutMs: null,
|
|
243
264
|
});
|
|
244
265
|
}
|
|
245
266
|
}
|
|
@@ -248,13 +269,13 @@ class UploadNamespace {
|
|
|
248
269
|
constructor(client) {
|
|
249
270
|
this.client = client;
|
|
250
271
|
}
|
|
251
|
-
async run(input) {
|
|
252
|
-
return new UploadApi(this.client.configuration).uploadRun({ file: asFile(input) });
|
|
272
|
+
async run(input, options = {}) {
|
|
273
|
+
return new UploadApi(this.client.configuration).uploadRun({ file: asFile(input) }, requestInit(options));
|
|
253
274
|
}
|
|
254
|
-
async get(fileId) {
|
|
255
|
-
return new UploadApi(this.client.configuration).uploadGet({ fileId });
|
|
275
|
+
async get(fileId, options = {}) {
|
|
276
|
+
return new UploadApi(this.client.configuration).uploadGet({ fileId }, requestInit(options));
|
|
256
277
|
}
|
|
257
|
-
async delete(fileId) {
|
|
258
|
-
await new UploadApi(this.client.configuration).uploadDelete({ fileId });
|
|
278
|
+
async delete(fileId, options = {}) {
|
|
279
|
+
await new UploadApi(this.client.configuration).uploadDelete({ fileId }, requestInit(options));
|
|
259
280
|
}
|
|
260
281
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* response types this SDK returns, so a caller needs to be able to name them.
|
|
14
14
|
*/
|
|
15
15
|
export { DEFAULT_SERVER_URL, TransformClient } from "./client.js";
|
|
16
|
-
export type { ExtractFromDocumentOptions, ExtractOptions, JobsGetOptions, JobsListOptions, JobsStreamOptions, ParseOptions, TransformClientOptions, } from "./client.js";
|
|
17
|
-
export type
|
|
16
|
+
export type { ExtractFromDocumentOptions, ExtractOptions, JobsGetOptions, JobsListOptions, JobsStreamOptions, ParseOptions, RequestOptions, TransformClientOptions, } from "./client.js";
|
|
17
|
+
export { RequestTimeoutError, type RetryConfig } from "./retry.js";
|
|
18
18
|
export { isTerminal, type JobEvent } from "./sse.js";
|
|
19
19
|
export { asFile, type FileInput } from "./files.js";
|
|
20
20
|
export { isAccepted, type Outcome } from "./outcome.js";
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* response types this SDK returns, so a caller needs to be able to name them.
|
|
14
14
|
*/
|
|
15
15
|
export { DEFAULT_SERVER_URL, TransformClient } from "./client.js";
|
|
16
|
+
export { RequestTimeoutError } from "./retry.js";
|
|
16
17
|
export { isTerminal } from "./sse.js";
|
|
17
18
|
export { asFile } from "./files.js";
|
|
18
19
|
export { isAccepted } from "./outcome.js";
|
package/dist/multipart.d.ts
CHANGED
package/dist/multipart.js
CHANGED
|
@@ -47,10 +47,12 @@ export async function postMultipart(client, request) {
|
|
|
47
47
|
const response = await fetchWithRetries(client.retries, {
|
|
48
48
|
url: `${client.serverUrl}${request.path}`,
|
|
49
49
|
fetchApi: client.fetchApi,
|
|
50
|
+
timeoutMs: request.timeoutMs ?? client.timeoutMs,
|
|
50
51
|
init: {
|
|
51
52
|
method: "POST",
|
|
52
53
|
headers,
|
|
53
54
|
body: form,
|
|
55
|
+
signal: request.signal,
|
|
54
56
|
},
|
|
55
57
|
});
|
|
56
58
|
if (!response.ok) {
|
package/dist/retry.d.ts
CHANGED
|
@@ -3,6 +3,10 @@ export interface RetryConfig {
|
|
|
3
3
|
maxAttempts?: number;
|
|
4
4
|
initialDelayMs?: number;
|
|
5
5
|
maxDelayMs?: number;
|
|
6
|
+
/**
|
|
7
|
+
* Total retry budget, excluding the time spent waiting for each in-flight
|
|
8
|
+
* request to answer or hit its own timeout.
|
|
9
|
+
*/
|
|
6
10
|
maxElapsedMs?: number;
|
|
7
11
|
retryAfterMaxMs?: number;
|
|
8
12
|
}
|
|
@@ -10,7 +14,19 @@ export interface RetryRequest {
|
|
|
10
14
|
url: string;
|
|
11
15
|
init: RequestInit;
|
|
12
16
|
fetchApi?: typeof fetch;
|
|
17
|
+
/**
|
|
18
|
+
* Per-request timeout. `null` means no SDK timeout, which is used by the
|
|
19
|
+
* long-lived SSE stream after the initial request is accepted.
|
|
20
|
+
*/
|
|
21
|
+
timeoutMs?: number | null;
|
|
13
22
|
}
|
|
14
23
|
export type RetryOption = RetryConfig | false | undefined;
|
|
15
|
-
export declare
|
|
24
|
+
export declare const DEFAULT_TIMEOUT_MS = 60000;
|
|
25
|
+
export declare class RequestTimeoutError extends Error {
|
|
26
|
+
name: string;
|
|
27
|
+
readonly timeoutMs: number;
|
|
28
|
+
cause?: unknown;
|
|
29
|
+
constructor(timeoutMs: number, cause?: unknown);
|
|
30
|
+
}
|
|
31
|
+
export declare function normalizeRetries(retries: RetryOption, timeoutMs?: number): Required<RetryConfig> | undefined;
|
|
16
32
|
export declare function fetchWithRetries(retries: Required<RetryConfig> | undefined, request: RetryRequest): Promise<Response>;
|
package/dist/retry.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/** Retry policy for the hand-written SDK facade. */
|
|
2
|
+
export const DEFAULT_TIMEOUT_MS = 60000;
|
|
2
3
|
const DEFAULT_RETRIES = {
|
|
3
4
|
maxAttempts: 3,
|
|
4
5
|
initialDelayMs: 250,
|
|
5
6
|
maxDelayMs: 2000,
|
|
6
|
-
maxElapsedMs:
|
|
7
|
+
maxElapsedMs: 3 * DEFAULT_TIMEOUT_MS,
|
|
7
8
|
retryAfterMaxMs: 2000,
|
|
8
9
|
};
|
|
9
10
|
// Errors that prove the request never left this process, identified by their
|
|
@@ -32,38 +33,60 @@ const BEFORE_SEND_CODES = new Set([
|
|
|
32
33
|
]);
|
|
33
34
|
const SAFE_METHODS = new Set(["GET", "DELETE"]);
|
|
34
35
|
const RETRYABLE_STATUSES = new Set([429, 500, 502, 503, 504]);
|
|
35
|
-
export
|
|
36
|
+
export class RequestTimeoutError extends Error {
|
|
37
|
+
name = "TimeoutError";
|
|
38
|
+
timeoutMs;
|
|
39
|
+
cause;
|
|
40
|
+
constructor(timeoutMs, cause) {
|
|
41
|
+
super(`request timed out after ${timeoutMs}ms`);
|
|
42
|
+
this.timeoutMs = timeoutMs;
|
|
43
|
+
this.cause = cause;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function normalizeRetries(retries, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
36
47
|
if (retries === false)
|
|
37
48
|
return undefined;
|
|
38
|
-
|
|
49
|
+
const defaults = {
|
|
50
|
+
...DEFAULT_RETRIES,
|
|
51
|
+
maxElapsedMs: Math.max(DEFAULT_RETRIES.maxElapsedMs, DEFAULT_RETRIES.maxAttempts * timeoutMs),
|
|
52
|
+
};
|
|
53
|
+
return { ...defaults, ...(retries ?? {}) };
|
|
39
54
|
}
|
|
40
55
|
export async function fetchWithRetries(retries, request) {
|
|
41
56
|
const fetchApi = request.fetchApi ?? fetch;
|
|
57
|
+
const init = extractInternalInit(request.init);
|
|
58
|
+
const timeoutMs = init.timeoutMs ??
|
|
59
|
+
(request.timeoutMs === undefined ? DEFAULT_TIMEOUT_MS : request.timeoutMs);
|
|
42
60
|
if (retries === undefined || retries.maxAttempts <= 1) {
|
|
43
|
-
return fetchApi
|
|
61
|
+
return fetchOnce(fetchApi, request.url, init.init, timeoutMs, request.fetchApi === undefined, false);
|
|
44
62
|
}
|
|
45
|
-
const method = (
|
|
46
|
-
const signal = request.init.signal ?? undefined;
|
|
63
|
+
const method = (init.init.method ?? "GET").toUpperCase();
|
|
47
64
|
const started = Date.now();
|
|
65
|
+
let elapsedMs = 0;
|
|
48
66
|
let attempt = 1;
|
|
49
67
|
for (;;) {
|
|
68
|
+
const attemptStarted = Date.now();
|
|
50
69
|
try {
|
|
51
|
-
const response = await fetchApi
|
|
52
|
-
|
|
70
|
+
const response = await fetchOnce(fetchApi, request.url, cloneInit(init.init), timeoutMs, request.fetchApi === undefined, SAFE_METHODS.has(method));
|
|
71
|
+
elapsedMs += Date.now() - attemptStarted;
|
|
72
|
+
if (!shouldRetryStatus(method, response.status) ||
|
|
73
|
+
!canRetry(retries, attempt, started, elapsedMs)) {
|
|
53
74
|
return response;
|
|
54
75
|
}
|
|
55
76
|
await drain(response);
|
|
56
|
-
await sleepFor(retries, attempt, response.headers.get("Retry-After"), started, signal);
|
|
57
|
-
if (!canRetry(retries, attempt, started)) {
|
|
77
|
+
await sleepFor(retries, attempt, response.headers.get("Retry-After"), started, elapsedMs, init.init.signal ?? undefined);
|
|
78
|
+
if (!canRetry(retries, attempt, started, elapsedMs)) {
|
|
58
79
|
return response;
|
|
59
80
|
}
|
|
60
81
|
}
|
|
61
82
|
catch (error) {
|
|
62
|
-
|
|
83
|
+
elapsedMs += Date.now() - attemptStarted;
|
|
84
|
+
if (!shouldRetryError(method, error, init.init.signal ?? undefined) ||
|
|
85
|
+
!canRetry(retries, attempt, started, elapsedMs)) {
|
|
63
86
|
throw error;
|
|
64
87
|
}
|
|
65
|
-
await sleepFor(retries, attempt, undefined, started, signal);
|
|
66
|
-
if (!canRetry(retries, attempt, started)) {
|
|
88
|
+
await sleepFor(retries, attempt, undefined, started, elapsedMs, init.init.signal ?? undefined);
|
|
89
|
+
if (!canRetry(retries, attempt, started, elapsedMs)) {
|
|
67
90
|
throw error;
|
|
68
91
|
}
|
|
69
92
|
}
|
|
@@ -122,16 +145,87 @@ function isBeforeSendError(error) {
|
|
|
122
145
|
}
|
|
123
146
|
return false;
|
|
124
147
|
}
|
|
125
|
-
function
|
|
126
|
-
|
|
148
|
+
async function fetchOnce(fetchApi, url, init, timeoutMs, defaultFetch, readBodyBeforeReturn) {
|
|
149
|
+
const callerSignal = init.signal ?? undefined;
|
|
150
|
+
let timeoutSignal;
|
|
151
|
+
let signal = callerSignal;
|
|
152
|
+
if (timeoutMs !== null) {
|
|
153
|
+
timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
154
|
+
signal = signal === undefined ? timeoutSignal : AbortSignal.any([signal, timeoutSignal]);
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const response = await fetchApi(url, prepareInit(init, signal, defaultFetch));
|
|
158
|
+
// SSE bodies are read incrementally by the caller as the job runs, not
|
|
159
|
+
// buffered up front — pre-reading one here would block `jobs.stream` from
|
|
160
|
+
// yielding any event until the stream closes. Buffer once (no clone) and
|
|
161
|
+
// return a response backed by that buffer rather than leaving the
|
|
162
|
+
// original's body unread, which would otherwise double transient memory
|
|
163
|
+
// for a large result.
|
|
164
|
+
if (readBodyBeforeReturn && !isEventStream(response)) {
|
|
165
|
+
const buffer = await response.arrayBuffer();
|
|
166
|
+
return new Response(isNullBodyStatus(response.status) ? null : buffer, {
|
|
167
|
+
status: response.status,
|
|
168
|
+
statusText: response.statusText,
|
|
169
|
+
headers: response.headers,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return response;
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (timeoutSignal?.aborted && !callerSignal?.aborted) {
|
|
176
|
+
throw new RequestTimeoutError(timeoutMs, error);
|
|
177
|
+
}
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function isNullBodyStatus(status) {
|
|
182
|
+
return status === 204 || status === 205 || status === 304;
|
|
183
|
+
}
|
|
184
|
+
function isEventStream(response) {
|
|
185
|
+
return (response.headers.get("content-type") ?? "")
|
|
186
|
+
.toLowerCase()
|
|
187
|
+
.startsWith("text/event-stream");
|
|
188
|
+
}
|
|
189
|
+
function prepareInit(init, signal, defaultFetch) {
|
|
190
|
+
const next = { ...init, signal };
|
|
191
|
+
if (defaultFetch &&
|
|
192
|
+
isNodeRuntime() &&
|
|
193
|
+
(next.method ?? "GET").toUpperCase() === "POST") {
|
|
194
|
+
next.headers = headersWith(next.headers, "Connection", "close");
|
|
195
|
+
}
|
|
196
|
+
return next;
|
|
197
|
+
}
|
|
198
|
+
function isNodeRuntime() {
|
|
199
|
+
return typeof process !== "undefined" && process.versions?.node !== undefined;
|
|
200
|
+
}
|
|
201
|
+
function headersWith(headers, name, value) {
|
|
202
|
+
if (headers instanceof Headers) {
|
|
203
|
+
const next = new Headers(headers);
|
|
204
|
+
next.set(name, value);
|
|
205
|
+
return next;
|
|
206
|
+
}
|
|
207
|
+
if (Array.isArray(headers)) {
|
|
208
|
+
const next = new Headers(headers);
|
|
209
|
+
next.set(name, value);
|
|
210
|
+
return next;
|
|
211
|
+
}
|
|
212
|
+
return { ...(headers ?? {}), [name]: value };
|
|
213
|
+
}
|
|
214
|
+
function extractInternalInit(init) {
|
|
215
|
+
const { timeoutMs, ...cleanInit } = init;
|
|
216
|
+
return { init: cleanInit, timeoutMs };
|
|
217
|
+
}
|
|
218
|
+
function canRetry(retries, attempt, started, elapsedMs) {
|
|
219
|
+
const retryBudgetMs = Date.now() - started - elapsedMs;
|
|
220
|
+
return attempt < retries.maxAttempts && retryBudgetMs < retries.maxElapsedMs;
|
|
127
221
|
}
|
|
128
|
-
async function sleepFor(retries, attempt, retryAfter, started, signal) {
|
|
222
|
+
async function sleepFor(retries, attempt, retryAfter, started, elapsedMs, signal) {
|
|
129
223
|
const parsed = retryAfterDelayMs(retryAfter);
|
|
130
224
|
const delay = parsed === undefined
|
|
131
225
|
? Math.min(retries.maxDelayMs, retries.initialDelayMs * 2 ** (attempt - 1)) *
|
|
132
226
|
(1 + Math.random() * 0.2)
|
|
133
227
|
: Math.min(parsed, retries.retryAfterMaxMs);
|
|
134
|
-
const remaining = retries.maxElapsedMs - (Date.now() - started);
|
|
228
|
+
const remaining = retries.maxElapsedMs - (Date.now() - started - elapsedMs);
|
|
135
229
|
const boundedDelay = Math.max(0, Math.min(delay, remaining));
|
|
136
230
|
if (!signal) {
|
|
137
231
|
await new Promise((resolve) => setTimeout(resolve, boundedDelay));
|
package/dist/sse.d.ts
CHANGED
package/dist/sse.js
CHANGED
|
@@ -39,6 +39,7 @@ export async function* streamJobEvents(options) {
|
|
|
39
39
|
const response = await fetchWithRetries(options.retries, {
|
|
40
40
|
url: options.url,
|
|
41
41
|
fetchApi: options.fetchApi,
|
|
42
|
+
timeoutMs: options.timeoutMs,
|
|
42
43
|
init: {
|
|
43
44
|
method: "GET",
|
|
44
45
|
headers: { ...options.headers, Accept: "text/event-stream" },
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unstructured-transform-client",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.28",
|
|
4
4
|
"description": "TypeScript client for the Unstructured Transform v2 API — one document in, structured output back.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Unstructured",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"files": [
|
|
17
17
|
"dist",
|
|
18
18
|
"README.md",
|
|
19
|
+
"CHANGELOG.md",
|
|
19
20
|
"LICENSE"
|
|
20
21
|
],
|
|
21
22
|
"engines": {
|