unstructured-transform-client 0.18.27 → 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 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.retries = normalizeRetries(options.retries);
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 { RetryConfig } from "./retry.js";
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";
@@ -35,6 +35,8 @@ export interface MultipartRequest<TResult> {
35
35
  filename?: string;
36
36
  fields: Record<string, string | undefined>;
37
37
  waitSeconds?: number;
38
+ signal?: AbortSignal;
39
+ timeoutMs?: number;
38
40
  /**
39
41
  * The generated mapper for the 200 body.
40
42
  *
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 function normalizeRetries(retries: RetryOption): Required<RetryConfig> | undefined;
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: 10000,
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 function normalizeRetries(retries) {
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
- return { ...DEFAULT_RETRIES, ...(retries ?? {}) };
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(request.url, request.init);
61
+ return fetchOnce(fetchApi, request.url, init.init, timeoutMs, request.fetchApi === undefined, false);
44
62
  }
45
- const method = (request.init.method ?? "GET").toUpperCase();
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(request.url, cloneInit(request.init));
52
- if (!shouldRetryStatus(method, response.status) || !canRetry(retries, attempt, started)) {
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
- if (!shouldRetryError(method, error, signal) || !canRetry(retries, attempt, started)) {
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 canRetry(retries, attempt, started) {
126
- return attempt < retries.maxAttempts && Date.now() - started < retries.maxElapsedMs;
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
@@ -38,6 +38,7 @@ export interface StreamOptions {
38
38
  signal?: AbortSignal;
39
39
  fetchApi?: typeof fetch;
40
40
  retries?: Required<RetryConfig>;
41
+ timeoutMs?: number | null;
41
42
  }
42
43
  /**
43
44
  * Yield events until the stream closes.
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
@@ -7,4 +7,4 @@
7
7
  * runtime, because importing JSON differs across module systems and bundlers
8
8
  * and would make this module's behaviour depend on how it was built.
9
9
  */
10
- export declare const VERSION = "0.18.27";
10
+ export declare const VERSION = "0.18.28";
package/dist/version.js CHANGED
@@ -7,4 +7,4 @@
7
7
  * runtime, because importing JSON differs across module systems and bundlers
8
8
  * and would make this module's behaviour depend on how it was built.
9
9
  */
10
- export const VERSION = "0.18.27";
10
+ export const VERSION = "0.18.28";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unstructured-transform-client",
3
- "version": "0.18.27",
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": {