@tinify-dev/client 0.1.0
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 +31 -0
- package/LICENSE +21 -0
- package/README.md +232 -0
- package/dist/chunk-NYRKB5BB.js +573 -0
- package/dist/chunk-NYRKB5BB.js.map +1 -0
- package/dist/cli.cjs +885 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +37 -0
- package/dist/cli.d.ts +37 -0
- package/dist/cli.js +307 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +586 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +130 -0
- package/dist/index.d.ts +130 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/types-D58KVL3U.d.cts +219 -0
- package/dist/types-D58KVL3U.d.ts +219 -0
- package/package.json +64 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/constants.ts
|
|
4
|
+
var DEFAULT_BASE_URL = "https://api.tinify.dev";
|
|
5
|
+
var ENV_API_KEY = "TINIFY_API_KEY";
|
|
6
|
+
var ENV_BASE_URL = "TINIFY_BASE_URL";
|
|
7
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
8
|
+
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
9
|
+
var BACKOFF_BASE_MS = 500;
|
|
10
|
+
var BACKOFF_CAP_MS = 8e3;
|
|
11
|
+
var MAX_FILE_BYTES = 41943040;
|
|
12
|
+
var MAX_BATCH_FILES = 200;
|
|
13
|
+
var DEFAULT_UPLOAD_CONCURRENCY = 4;
|
|
14
|
+
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
15
|
+
var DEFAULT_POLL_GROWTH = 1.5;
|
|
16
|
+
var DEFAULT_MAX_POLL_INTERVAL_MS = 1e4;
|
|
17
|
+
var DEFAULT_WAIT_TIMEOUT_MS = 6e5;
|
|
18
|
+
var BATCH_TERMINAL_STATUSES = [
|
|
19
|
+
"succeeded",
|
|
20
|
+
"partially_succeeded",
|
|
21
|
+
"failed",
|
|
22
|
+
"canceled",
|
|
23
|
+
"expired"
|
|
24
|
+
];
|
|
25
|
+
var IDEMPOTENCY_KEY_MIN_LENGTH = 8;
|
|
26
|
+
var IDEMPOTENCY_KEY_MAX_LENGTH = 160;
|
|
27
|
+
|
|
28
|
+
// src/errors.ts
|
|
29
|
+
var TinifyError = class extends Error {
|
|
30
|
+
constructor(message, options) {
|
|
31
|
+
super(message, options);
|
|
32
|
+
this.name = "TinifyError";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var TinifyApiError = class extends TinifyError {
|
|
36
|
+
status;
|
|
37
|
+
code;
|
|
38
|
+
requestId;
|
|
39
|
+
details;
|
|
40
|
+
retryAfter;
|
|
41
|
+
constructor(init) {
|
|
42
|
+
super(`${init.code} (HTTP ${init.status}): ${init.message} [request_id: ${init.requestId}]`);
|
|
43
|
+
this.name = "TinifyApiError";
|
|
44
|
+
this.status = init.status;
|
|
45
|
+
this.code = init.code;
|
|
46
|
+
this.requestId = init.requestId;
|
|
47
|
+
this.details = init.details ?? {};
|
|
48
|
+
if (init.retryAfter !== void 0) this.retryAfter = init.retryAfter;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var TinifyNetworkError = class extends TinifyError {
|
|
52
|
+
constructor(message, options) {
|
|
53
|
+
super(message, options);
|
|
54
|
+
this.name = "TinifyNetworkError";
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var TinifyTimeoutError = class extends TinifyError {
|
|
58
|
+
timeoutMs;
|
|
59
|
+
constructor(message, timeoutMs) {
|
|
60
|
+
super(message);
|
|
61
|
+
this.name = "TinifyTimeoutError";
|
|
62
|
+
this.timeoutMs = timeoutMs;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// src/inputs.ts
|
|
67
|
+
var DEFAULT_FILENAME = "image";
|
|
68
|
+
async function normalizeInput(input, filename) {
|
|
69
|
+
if (typeof input === "string") {
|
|
70
|
+
let fs;
|
|
71
|
+
let path;
|
|
72
|
+
try {
|
|
73
|
+
fs = await import('fs/promises');
|
|
74
|
+
path = await import('path');
|
|
75
|
+
} catch {
|
|
76
|
+
throw new TinifyError(
|
|
77
|
+
"File path inputs are only supported in Node.js. Pass a Blob or File instead."
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
const bytes = await fs.readFile(input);
|
|
81
|
+
return {
|
|
82
|
+
blob: new Blob([bytesToView(bytes)]),
|
|
83
|
+
filename: filename ?? path.basename(input)
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (typeof Blob !== "undefined" && input instanceof Blob) {
|
|
87
|
+
const name = filename ?? (typeof File !== "undefined" && input instanceof File && input.name ? input.name : DEFAULT_FILENAME);
|
|
88
|
+
return { blob: input, filename: name };
|
|
89
|
+
}
|
|
90
|
+
if (input instanceof ArrayBuffer) {
|
|
91
|
+
return { blob: new Blob([input]), filename: filename ?? DEFAULT_FILENAME };
|
|
92
|
+
}
|
|
93
|
+
if (ArrayBuffer.isView(input)) {
|
|
94
|
+
return {
|
|
95
|
+
blob: new Blob([bytesToView(input)]),
|
|
96
|
+
filename: filename ?? DEFAULT_FILENAME
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
throw new TypeError(
|
|
100
|
+
"Unsupported input: expected a file path, Uint8Array, ArrayBuffer, Blob, or File."
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
function bytesToView(view) {
|
|
104
|
+
const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
|
|
105
|
+
if (bytes.buffer instanceof ArrayBuffer && bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength) {
|
|
106
|
+
return bytes;
|
|
107
|
+
}
|
|
108
|
+
const copy = new Uint8Array(new ArrayBuffer(bytes.byteLength));
|
|
109
|
+
copy.set(bytes);
|
|
110
|
+
return copy;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/batches.ts
|
|
114
|
+
var TERMINAL = new Set(BATCH_TERMINAL_STATUSES);
|
|
115
|
+
function isTerminalBatchStatus(status) {
|
|
116
|
+
return TERMINAL.has(status);
|
|
117
|
+
}
|
|
118
|
+
async function uploadBatchFiles(fetchImpl, session, files, options = {}) {
|
|
119
|
+
const missing = session.uploads.map((upload) => upload.client_id).filter((clientId) => !(clientId in files));
|
|
120
|
+
if (missing.length > 0) {
|
|
121
|
+
throw new TinifyError(
|
|
122
|
+
`Missing input for batch client_id(s): ${missing.join(", ")}. Pass a { client_id: input } record covering every file in the session.`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const queue = [...session.uploads];
|
|
126
|
+
const concurrency = Math.max(
|
|
127
|
+
1,
|
|
128
|
+
Math.min(options.concurrency ?? DEFAULT_UPLOAD_CONCURRENCY, queue.length)
|
|
129
|
+
);
|
|
130
|
+
const workers = Array.from({ length: concurrency }, async () => {
|
|
131
|
+
for (; ; ) {
|
|
132
|
+
const upload = queue.shift();
|
|
133
|
+
if (upload === void 0) return;
|
|
134
|
+
options.signal?.throwIfAborted();
|
|
135
|
+
await putOne(fetchImpl, upload, files[upload.client_id], options.signal);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
await Promise.all(workers);
|
|
139
|
+
}
|
|
140
|
+
async function putOne(fetchImpl, upload, input, signal) {
|
|
141
|
+
const { blob } = await normalizeInput(input);
|
|
142
|
+
let response;
|
|
143
|
+
try {
|
|
144
|
+
response = await fetchImpl(upload.upload_url, {
|
|
145
|
+
method: "PUT",
|
|
146
|
+
headers: upload.headers,
|
|
147
|
+
body: blob,
|
|
148
|
+
...signal !== void 0 ? { signal } : {}
|
|
149
|
+
});
|
|
150
|
+
} catch (error) {
|
|
151
|
+
throw new TinifyNetworkError(
|
|
152
|
+
`Upload for client_id "${upload.client_id}" failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
153
|
+
{ cause: error }
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
const text = await response.text().catch(() => "");
|
|
158
|
+
throw new TinifyError(
|
|
159
|
+
`Upload for client_id "${upload.client_id}" was rejected with HTTP ${response.status}` + (text ? `: ${text.slice(0, 200)}` : ".")
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
await response.arrayBuffer().catch(() => void 0);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/http.ts
|
|
166
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 503]);
|
|
167
|
+
function generateIdempotencyKey() {
|
|
168
|
+
return globalThis.crypto.randomUUID();
|
|
169
|
+
}
|
|
170
|
+
function validateIdempotencyKey(key) {
|
|
171
|
+
if (key.length < IDEMPOTENCY_KEY_MIN_LENGTH || key.length > IDEMPOTENCY_KEY_MAX_LENGTH) {
|
|
172
|
+
throw new TinifyError(
|
|
173
|
+
`Idempotency keys must be ${IDEMPOTENCY_KEY_MIN_LENGTH}-${IDEMPOTENCY_KEY_MAX_LENGTH} characters (got ${key.length}).`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function parseRetryAfterMs(header) {
|
|
178
|
+
if (header === null || header.trim() === "") return void 0;
|
|
179
|
+
const value = header.trim();
|
|
180
|
+
if (/^\d+$/.test(value)) return Number(value) * 1e3;
|
|
181
|
+
const date = Date.parse(value);
|
|
182
|
+
if (Number.isNaN(date)) return void 0;
|
|
183
|
+
return Math.max(0, date - Date.now());
|
|
184
|
+
}
|
|
185
|
+
function backoffMs(attempt) {
|
|
186
|
+
const ceiling = Math.min(BACKOFF_CAP_MS, BACKOFF_BASE_MS * 2 ** attempt);
|
|
187
|
+
return Math.random() * ceiling;
|
|
188
|
+
}
|
|
189
|
+
function rateLimitFromHeaders(headers) {
|
|
190
|
+
const limit = headers.get("x-ratelimit-limit");
|
|
191
|
+
const remaining = headers.get("x-ratelimit-remaining");
|
|
192
|
+
const reset = headers.get("x-ratelimit-reset");
|
|
193
|
+
if (limit === null || remaining === null || reset === null) return null;
|
|
194
|
+
return { limit: Number(limit), remaining: Number(remaining), reset: Number(reset) };
|
|
195
|
+
}
|
|
196
|
+
function sleep(ms, signal) {
|
|
197
|
+
return new Promise((resolve, reject) => {
|
|
198
|
+
if (signal?.aborted) {
|
|
199
|
+
reject(abortReason(signal));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const timer = setTimeout(() => {
|
|
203
|
+
signal?.removeEventListener("abort", onAbort);
|
|
204
|
+
resolve();
|
|
205
|
+
}, ms);
|
|
206
|
+
function onAbort() {
|
|
207
|
+
clearTimeout(timer);
|
|
208
|
+
reject(abortReason(signal));
|
|
209
|
+
}
|
|
210
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
function abortReason(signal) {
|
|
214
|
+
return signal.reason ?? new TinifyError("The operation was aborted.");
|
|
215
|
+
}
|
|
216
|
+
async function throwApiError(response) {
|
|
217
|
+
const text = await response.text().catch(() => "");
|
|
218
|
+
let body;
|
|
219
|
+
try {
|
|
220
|
+
body = text ? JSON.parse(text) : void 0;
|
|
221
|
+
} catch {
|
|
222
|
+
body = void 0;
|
|
223
|
+
}
|
|
224
|
+
const envelope = body ?? {};
|
|
225
|
+
const retryAfterMs = parseRetryAfterMs(response.headers.get("retry-after"));
|
|
226
|
+
throw new TinifyApiError({
|
|
227
|
+
status: response.status,
|
|
228
|
+
code: envelope.error?.code ?? "unknown_error",
|
|
229
|
+
message: envelope.error?.message ?? `Unexpected HTTP ${response.status} response.`,
|
|
230
|
+
requestId: envelope.request_id ?? response.headers.get("x-request-id") ?? "unavailable",
|
|
231
|
+
details: envelope.error?.details ?? {},
|
|
232
|
+
...retryAfterMs !== void 0 ? { retryAfter: retryAfterMs / 1e3 } : {}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
var HttpClient = class {
|
|
236
|
+
config;
|
|
237
|
+
constructor(config) {
|
|
238
|
+
this.config = config;
|
|
239
|
+
}
|
|
240
|
+
get baseUrl() {
|
|
241
|
+
return this.config.baseUrl;
|
|
242
|
+
}
|
|
243
|
+
get fetchImpl() {
|
|
244
|
+
return this.config.fetchImpl;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Performs a request with retry semantics:
|
|
248
|
+
* - retries 429/503 (honoring Retry-After) and network failures,
|
|
249
|
+
* - never retries any other 4xx/5xx,
|
|
250
|
+
* - reuses one Idempotency-Key across every attempt of the call.
|
|
251
|
+
*/
|
|
252
|
+
async request(options) {
|
|
253
|
+
const url = new URL(options.path, `${this.config.baseUrl}/`).toString();
|
|
254
|
+
let idempotencyKey;
|
|
255
|
+
if (options.idempotencyKey === "auto") {
|
|
256
|
+
idempotencyKey = generateIdempotencyKey();
|
|
257
|
+
} else if (options.idempotencyKey !== void 0) {
|
|
258
|
+
validateIdempotencyKey(options.idempotencyKey);
|
|
259
|
+
idempotencyKey = options.idempotencyKey;
|
|
260
|
+
}
|
|
261
|
+
let attempt = 0;
|
|
262
|
+
for (; ; ) {
|
|
263
|
+
options.signal?.throwIfAborted();
|
|
264
|
+
const headers = {
|
|
265
|
+
authorization: `Bearer ${this.config.apiKey}`,
|
|
266
|
+
accept: "application/json",
|
|
267
|
+
"user-agent": "@tinify-dev/client/0.1.0"
|
|
268
|
+
};
|
|
269
|
+
if (idempotencyKey !== void 0) headers["idempotency-key"] = idempotencyKey;
|
|
270
|
+
if (options.contentType !== void 0) headers["content-type"] = options.contentType;
|
|
271
|
+
let response;
|
|
272
|
+
try {
|
|
273
|
+
response = await this.fetchWithTimeout(
|
|
274
|
+
url,
|
|
275
|
+
{
|
|
276
|
+
method: options.method,
|
|
277
|
+
headers,
|
|
278
|
+
...options.body !== void 0 ? { body: options.body() } : {}
|
|
279
|
+
},
|
|
280
|
+
options.signal
|
|
281
|
+
);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (error instanceof TinifyTimeoutError) throw error;
|
|
284
|
+
if (options.signal?.aborted) throw error;
|
|
285
|
+
if (attempt < this.config.maxRetries) {
|
|
286
|
+
await sleep(backoffMs(attempt), options.signal);
|
|
287
|
+
attempt += 1;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
throw new TinifyNetworkError(
|
|
291
|
+
`Request to ${url} failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
292
|
+
{ cause: error }
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
if (RETRYABLE_STATUSES.has(response.status) && attempt < this.config.maxRetries) {
|
|
296
|
+
const retryAfter = parseRetryAfterMs(response.headers.get("retry-after"));
|
|
297
|
+
await response.arrayBuffer().catch(() => void 0);
|
|
298
|
+
await sleep(retryAfter ?? backoffMs(attempt), options.signal);
|
|
299
|
+
attempt += 1;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (!response.ok) await throwApiError(response);
|
|
303
|
+
return {
|
|
304
|
+
response,
|
|
305
|
+
requestIdHeader: response.headers.get("x-request-id"),
|
|
306
|
+
rateLimit: rateLimitFromHeaders(response.headers)
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/** JSON-parsing wrapper around {@link request}. */
|
|
311
|
+
async requestJson(options) {
|
|
312
|
+
const result = await this.request(options);
|
|
313
|
+
let body;
|
|
314
|
+
if (result.response.status === 204) {
|
|
315
|
+
body = void 0;
|
|
316
|
+
} else {
|
|
317
|
+
body = await result.response.json();
|
|
318
|
+
}
|
|
319
|
+
const bodyRequestId = body?.request_id;
|
|
320
|
+
return {
|
|
321
|
+
body,
|
|
322
|
+
requestId: bodyRequestId ?? result.requestIdHeader ?? "unavailable",
|
|
323
|
+
rateLimit: result.rateLimit
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
async fetchWithTimeout(url, init, signal) {
|
|
327
|
+
const controller = new AbortController();
|
|
328
|
+
const timeoutError = new TinifyTimeoutError(
|
|
329
|
+
`Request timed out after ${this.config.timeoutMs} ms.`,
|
|
330
|
+
this.config.timeoutMs
|
|
331
|
+
);
|
|
332
|
+
const timer = setTimeout(() => controller.abort(timeoutError), this.config.timeoutMs);
|
|
333
|
+
const onAbort = () => controller.abort(abortReason(signal));
|
|
334
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
335
|
+
try {
|
|
336
|
+
return await this.config.fetchImpl(url, { ...init, signal: controller.signal });
|
|
337
|
+
} catch (error) {
|
|
338
|
+
if (error === timeoutError) throw error;
|
|
339
|
+
if (controller.signal.aborted && controller.signal.reason === timeoutError) {
|
|
340
|
+
throw timeoutError;
|
|
341
|
+
}
|
|
342
|
+
throw error;
|
|
343
|
+
} finally {
|
|
344
|
+
clearTimeout(timer);
|
|
345
|
+
signal?.removeEventListener("abort", onAbort);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
// src/client.ts
|
|
351
|
+
var TinifyClient = class {
|
|
352
|
+
http;
|
|
353
|
+
constructor(options = {}) {
|
|
354
|
+
const env = typeof process !== "undefined" && process.env !== void 0 ? process.env : {};
|
|
355
|
+
const apiKey = options.apiKey ?? env[ENV_API_KEY];
|
|
356
|
+
if (apiKey === void 0 || apiKey === "") {
|
|
357
|
+
throw new TinifyError(
|
|
358
|
+
`Missing API key. Pass { apiKey } or set ${ENV_API_KEY}. Keys are created at https://tinify.dev/developers.`
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
const baseUrl = (options.baseUrl ?? env[ENV_BASE_URL] ?? DEFAULT_BASE_URL).replace(
|
|
362
|
+
/\/+$/,
|
|
363
|
+
""
|
|
364
|
+
);
|
|
365
|
+
const fetchImpl = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
366
|
+
this.http = new HttpClient({
|
|
367
|
+
apiKey,
|
|
368
|
+
baseUrl,
|
|
369
|
+
maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
370
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
371
|
+
fetchImpl
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
// -------------------------------------------------------------------------
|
|
375
|
+
// Synchronous image operations
|
|
376
|
+
// -------------------------------------------------------------------------
|
|
377
|
+
/** Compresses an image. Never returns more bytes than the input. */
|
|
378
|
+
compress(input, options = {}) {
|
|
379
|
+
return this.imageOperation(
|
|
380
|
+
"compress",
|
|
381
|
+
input,
|
|
382
|
+
{
|
|
383
|
+
quality_mode: options.quality_mode,
|
|
384
|
+
target_size_bytes: options.target_size_bytes
|
|
385
|
+
},
|
|
386
|
+
options
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
/** Resizes an image by width/height/scale. */
|
|
390
|
+
resize(input, options = {}) {
|
|
391
|
+
return this.imageOperation(
|
|
392
|
+
"resize",
|
|
393
|
+
input,
|
|
394
|
+
{
|
|
395
|
+
width: options.width,
|
|
396
|
+
height: options.height,
|
|
397
|
+
scale: options.scale,
|
|
398
|
+
keep_aspect_ratio: options.keep_aspect_ratio,
|
|
399
|
+
optimize: options.optimize
|
|
400
|
+
},
|
|
401
|
+
options
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
/** Crops an image to the given rectangle. */
|
|
405
|
+
crop(input, options) {
|
|
406
|
+
return this.imageOperation(
|
|
407
|
+
"crop",
|
|
408
|
+
input,
|
|
409
|
+
{
|
|
410
|
+
x: options.x,
|
|
411
|
+
y: options.y,
|
|
412
|
+
width: options.width,
|
|
413
|
+
height: options.height
|
|
414
|
+
},
|
|
415
|
+
options
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Converts an image to another format (AVIF, WebP, JPEG, or PNG).
|
|
420
|
+
*
|
|
421
|
+
* @beta The `/api/v1/images/convert` endpoint is rolling out server-side
|
|
422
|
+
* and may not be available on every account yet.
|
|
423
|
+
*/
|
|
424
|
+
convert(input, options) {
|
|
425
|
+
return this.imageOperation(
|
|
426
|
+
"convert",
|
|
427
|
+
input,
|
|
428
|
+
{
|
|
429
|
+
format: options.format,
|
|
430
|
+
quality_mode: options.quality_mode
|
|
431
|
+
},
|
|
432
|
+
options
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
/** Returns the account's current billing-period usage. */
|
|
436
|
+
async usage(options = {}) {
|
|
437
|
+
const { body, requestId, rateLimit } = await this.http.requestJson({
|
|
438
|
+
method: "GET",
|
|
439
|
+
path: "/api/v1/usage",
|
|
440
|
+
...options.signal !== void 0 ? { signal: options.signal } : {}
|
|
441
|
+
});
|
|
442
|
+
return { data: body, requestId, rateLimit };
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Downloads a result as a `Blob`. Accepts a result object, a response
|
|
446
|
+
* envelope, or a download URL. Download URLs are unauthenticated and
|
|
447
|
+
* expire two hours after the operation.
|
|
448
|
+
*/
|
|
449
|
+
async download(resultOrUrl, options = {}) {
|
|
450
|
+
const raw = typeof resultOrUrl === "string" ? resultOrUrl : "download_url" in resultOrUrl ? resultOrUrl.download_url : resultOrUrl.data.download_url;
|
|
451
|
+
const url = new URL(raw, `${this.http.baseUrl}/`).toString();
|
|
452
|
+
const response = await this.http.fetchImpl(url, {
|
|
453
|
+
method: "GET",
|
|
454
|
+
...options.signal !== void 0 ? { signal: options.signal } : {}
|
|
455
|
+
});
|
|
456
|
+
if (!response.ok) await throwApiError(response);
|
|
457
|
+
return response.blob();
|
|
458
|
+
}
|
|
459
|
+
// -------------------------------------------------------------------------
|
|
460
|
+
// Batches
|
|
461
|
+
// -------------------------------------------------------------------------
|
|
462
|
+
/**
|
|
463
|
+
* Creates a durable batch. Returns one presigned upload slot per file;
|
|
464
|
+
* upload the bytes, then {@link commitBatch}. Requires a paid plan.
|
|
465
|
+
*/
|
|
466
|
+
async createBatch(params, options = {}) {
|
|
467
|
+
const { body, requestId, rateLimit } = await this.http.requestJson({
|
|
468
|
+
method: "POST",
|
|
469
|
+
path: "/api/v1/batches",
|
|
470
|
+
contentType: "application/json",
|
|
471
|
+
body: () => JSON.stringify(params),
|
|
472
|
+
idempotencyKey: options.idempotencyKey ?? "auto",
|
|
473
|
+
...options.signal !== void 0 ? { signal: options.signal } : {}
|
|
474
|
+
});
|
|
475
|
+
return { data: body, requestId, rateLimit };
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Uploads file bytes to the presigned URLs of an upload session, keyed by
|
|
479
|
+
* `client_id`. Runs up to `concurrency` (default 4) PUTs in parallel.
|
|
480
|
+
*/
|
|
481
|
+
uploadBatchFiles(session, files, options = {}) {
|
|
482
|
+
return uploadBatchFiles(this.http.fetchImpl, session, files, options);
|
|
483
|
+
}
|
|
484
|
+
/** Commits an uploaded batch for processing. */
|
|
485
|
+
async commitBatch(batchId, options = {}) {
|
|
486
|
+
const { body, requestId, rateLimit } = await this.http.requestJson({
|
|
487
|
+
method: "POST",
|
|
488
|
+
path: `/api/v1/batches/${encodeURIComponent(batchId)}/commit`,
|
|
489
|
+
idempotencyKey: options.idempotencyKey ?? "auto",
|
|
490
|
+
...options.signal !== void 0 ? { signal: options.signal } : {}
|
|
491
|
+
});
|
|
492
|
+
return { data: body, requestId, rateLimit };
|
|
493
|
+
}
|
|
494
|
+
/** Fetches a batch with per-file state. */
|
|
495
|
+
async getBatch(batchId, options = {}) {
|
|
496
|
+
const { body, requestId, rateLimit } = await this.http.requestJson({
|
|
497
|
+
method: "GET",
|
|
498
|
+
path: `/api/v1/batches/${encodeURIComponent(batchId)}`,
|
|
499
|
+
...options.signal !== void 0 ? { signal: options.signal } : {}
|
|
500
|
+
});
|
|
501
|
+
return { data: body, requestId, rateLimit };
|
|
502
|
+
}
|
|
503
|
+
/** Cancels a batch. Idempotent: canceling a terminal batch is a no-op. */
|
|
504
|
+
async cancelBatch(batchId, options = {}) {
|
|
505
|
+
await this.http.request({
|
|
506
|
+
method: "DELETE",
|
|
507
|
+
path: `/api/v1/batches/${encodeURIComponent(batchId)}`,
|
|
508
|
+
...options.signal !== void 0 ? { signal: options.signal } : {}
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Polls {@link getBatch} until the batch reaches a terminal status
|
|
513
|
+
* (`succeeded`, `partially_succeeded`, `failed`, `canceled`, `expired`).
|
|
514
|
+
* Polls start at 1s and grow x1.5 up to 10s; gives up after `timeoutMs`
|
|
515
|
+
* (default 10 minutes) with a {@link TinifyTimeoutError}.
|
|
516
|
+
*/
|
|
517
|
+
async waitForBatch(batchId, options = {}) {
|
|
518
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
519
|
+
const maxInterval = options.maxPollIntervalMs ?? DEFAULT_MAX_POLL_INTERVAL_MS;
|
|
520
|
+
let interval = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
521
|
+
const startedAt = Date.now();
|
|
522
|
+
for (; ; ) {
|
|
523
|
+
const result = await this.getBatch(batchId, options);
|
|
524
|
+
if (isTerminalBatchStatus(result.data.status)) return result;
|
|
525
|
+
const elapsed = Date.now() - startedAt;
|
|
526
|
+
if (elapsed + interval > timeoutMs) {
|
|
527
|
+
throw new TinifyTimeoutError(
|
|
528
|
+
`Batch ${batchId} did not reach a terminal status within ${timeoutMs} ms (last status: ${result.data.status}).`,
|
|
529
|
+
timeoutMs
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
await sleep(interval, options.signal);
|
|
533
|
+
interval = Math.min(interval * DEFAULT_POLL_GROWTH, maxInterval);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Downloads the ZIP archive of a completed batch's successful results.
|
|
538
|
+
* The batch must be in `succeeded`, `partially_succeeded`, or `failed`
|
|
539
|
+
* state and contain at least one successful file.
|
|
540
|
+
*/
|
|
541
|
+
async downloadBatchArchive(batchId, options = {}) {
|
|
542
|
+
const result = await this.http.request({
|
|
543
|
+
method: "GET",
|
|
544
|
+
path: `/api/v1/batches/${encodeURIComponent(batchId)}/archive`,
|
|
545
|
+
...options.signal !== void 0 ? { signal: options.signal } : {}
|
|
546
|
+
});
|
|
547
|
+
return result.response.blob();
|
|
548
|
+
}
|
|
549
|
+
// -------------------------------------------------------------------------
|
|
550
|
+
// Internals
|
|
551
|
+
// -------------------------------------------------------------------------
|
|
552
|
+
async imageOperation(operation, input, fields, options) {
|
|
553
|
+
const { blob, filename } = await normalizeInput(input, options.filename);
|
|
554
|
+
const makeForm = () => {
|
|
555
|
+
const form = new FormData();
|
|
556
|
+
form.append("file", blob, filename);
|
|
557
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
558
|
+
if (value !== void 0) form.append(key, String(value));
|
|
559
|
+
}
|
|
560
|
+
return form;
|
|
561
|
+
};
|
|
562
|
+
const { body, requestId, rateLimit } = await this.http.requestJson({
|
|
563
|
+
method: "POST",
|
|
564
|
+
path: `/api/v1/images/${operation}`,
|
|
565
|
+
body: makeForm,
|
|
566
|
+
idempotencyKey: options.idempotencyKey ?? "auto",
|
|
567
|
+
...options.signal !== void 0 ? { signal: options.signal } : {}
|
|
568
|
+
});
|
|
569
|
+
return { data: body.data, requestId, rateLimit };
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
exports.BATCH_TERMINAL_STATUSES = BATCH_TERMINAL_STATUSES;
|
|
574
|
+
exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
|
|
575
|
+
exports.MAX_BATCH_FILES = MAX_BATCH_FILES;
|
|
576
|
+
exports.MAX_FILE_BYTES = MAX_FILE_BYTES;
|
|
577
|
+
exports.TinifyApiError = TinifyApiError;
|
|
578
|
+
exports.TinifyClient = TinifyClient;
|
|
579
|
+
exports.TinifyError = TinifyError;
|
|
580
|
+
exports.TinifyNetworkError = TinifyNetworkError;
|
|
581
|
+
exports.TinifyTimeoutError = TinifyTimeoutError;
|
|
582
|
+
exports.isTerminalBatchStatus = isTerminalBatchStatus;
|
|
583
|
+
exports.normalizeInput = normalizeInput;
|
|
584
|
+
exports.uploadBatchFiles = uploadBatchFiles;
|
|
585
|
+
//# sourceMappingURL=index.cjs.map
|
|
586
|
+
//# sourceMappingURL=index.cjs.map
|