@tinify-dev/client 0.1.0 → 0.1.1
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/dist/{chunk-NYRKB5BB.js → chunk-F4JYNVER.js} +123 -88
- package/dist/chunk-F4JYNVER.js.map +1 -0
- package/dist/cli.cjs +169 -134
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +168 -133
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-NYRKB5BB.js.map +0 -1
package/dist/cli.cjs
CHANGED
|
@@ -13,6 +13,7 @@ var DEFAULT_MAX_RETRIES = 3;
|
|
|
13
13
|
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
14
14
|
var BACKOFF_BASE_MS = 500;
|
|
15
15
|
var BACKOFF_CAP_MS = 8e3;
|
|
16
|
+
var MAX_RETRY_AFTER_MS = 3e4;
|
|
16
17
|
var MAX_FILE_BYTES = 41943040;
|
|
17
18
|
var DEFAULT_UPLOAD_CONCURRENCY = 4;
|
|
18
19
|
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
@@ -67,105 +68,6 @@ var TinifyTimeoutError = class extends TinifyError {
|
|
|
67
68
|
}
|
|
68
69
|
};
|
|
69
70
|
|
|
70
|
-
// src/inputs.ts
|
|
71
|
-
var DEFAULT_FILENAME = "image";
|
|
72
|
-
async function normalizeInput(input, filename) {
|
|
73
|
-
if (typeof input === "string") {
|
|
74
|
-
let fs;
|
|
75
|
-
let path;
|
|
76
|
-
try {
|
|
77
|
-
fs = await import('fs/promises');
|
|
78
|
-
path = await import('path');
|
|
79
|
-
} catch {
|
|
80
|
-
throw new TinifyError(
|
|
81
|
-
"File path inputs are only supported in Node.js. Pass a Blob or File instead."
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
const bytes = await fs.readFile(input);
|
|
85
|
-
return {
|
|
86
|
-
blob: new Blob([bytesToView(bytes)]),
|
|
87
|
-
filename: filename ?? path.basename(input)
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
if (typeof Blob !== "undefined" && input instanceof Blob) {
|
|
91
|
-
const name = filename ?? (typeof File !== "undefined" && input instanceof File && input.name ? input.name : DEFAULT_FILENAME);
|
|
92
|
-
return { blob: input, filename: name };
|
|
93
|
-
}
|
|
94
|
-
if (input instanceof ArrayBuffer) {
|
|
95
|
-
return { blob: new Blob([input]), filename: filename ?? DEFAULT_FILENAME };
|
|
96
|
-
}
|
|
97
|
-
if (ArrayBuffer.isView(input)) {
|
|
98
|
-
return {
|
|
99
|
-
blob: new Blob([bytesToView(input)]),
|
|
100
|
-
filename: filename ?? DEFAULT_FILENAME
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
throw new TypeError(
|
|
104
|
-
"Unsupported input: expected a file path, Uint8Array, ArrayBuffer, Blob, or File."
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
function bytesToView(view) {
|
|
108
|
-
const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
|
|
109
|
-
if (bytes.buffer instanceof ArrayBuffer && bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength) {
|
|
110
|
-
return bytes;
|
|
111
|
-
}
|
|
112
|
-
const copy = new Uint8Array(new ArrayBuffer(bytes.byteLength));
|
|
113
|
-
copy.set(bytes);
|
|
114
|
-
return copy;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// src/batches.ts
|
|
118
|
-
var TERMINAL = new Set(BATCH_TERMINAL_STATUSES);
|
|
119
|
-
function isTerminalBatchStatus(status) {
|
|
120
|
-
return TERMINAL.has(status);
|
|
121
|
-
}
|
|
122
|
-
async function uploadBatchFiles(fetchImpl, session, files, options = {}) {
|
|
123
|
-
const missing = session.uploads.map((upload) => upload.client_id).filter((clientId) => !(clientId in files));
|
|
124
|
-
if (missing.length > 0) {
|
|
125
|
-
throw new TinifyError(
|
|
126
|
-
`Missing input for batch client_id(s): ${missing.join(", ")}. Pass a { client_id: input } record covering every file in the session.`
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
const queue = [...session.uploads];
|
|
130
|
-
const concurrency = Math.max(
|
|
131
|
-
1,
|
|
132
|
-
Math.min(options.concurrency ?? DEFAULT_UPLOAD_CONCURRENCY, queue.length)
|
|
133
|
-
);
|
|
134
|
-
const workers = Array.from({ length: concurrency }, async () => {
|
|
135
|
-
for (; ; ) {
|
|
136
|
-
const upload = queue.shift();
|
|
137
|
-
if (upload === void 0) return;
|
|
138
|
-
options.signal?.throwIfAborted();
|
|
139
|
-
await putOne(fetchImpl, upload, files[upload.client_id], options.signal);
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
await Promise.all(workers);
|
|
143
|
-
}
|
|
144
|
-
async function putOne(fetchImpl, upload, input, signal) {
|
|
145
|
-
const { blob } = await normalizeInput(input);
|
|
146
|
-
let response;
|
|
147
|
-
try {
|
|
148
|
-
response = await fetchImpl(upload.upload_url, {
|
|
149
|
-
method: "PUT",
|
|
150
|
-
headers: upload.headers,
|
|
151
|
-
body: blob,
|
|
152
|
-
...signal !== void 0 ? { signal } : {}
|
|
153
|
-
});
|
|
154
|
-
} catch (error) {
|
|
155
|
-
throw new TinifyNetworkError(
|
|
156
|
-
`Upload for client_id "${upload.client_id}" failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
157
|
-
{ cause: error }
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
if (!response.ok) {
|
|
161
|
-
const text = await response.text().catch(() => "");
|
|
162
|
-
throw new TinifyError(
|
|
163
|
-
`Upload for client_id "${upload.client_id}" was rejected with HTTP ${response.status}` + (text ? `: ${text.slice(0, 200)}` : ".")
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
await response.arrayBuffer().catch(() => void 0);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
71
|
// src/http.ts
|
|
170
72
|
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 503]);
|
|
171
73
|
function generateIdempotencyKey() {
|
|
@@ -236,6 +138,38 @@ async function throwApiError(response) {
|
|
|
236
138
|
...retryAfterMs !== void 0 ? { retryAfter: retryAfterMs / 1e3 } : {}
|
|
237
139
|
});
|
|
238
140
|
}
|
|
141
|
+
function withTimeoutSignal(timeoutMs, signal) {
|
|
142
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
143
|
+
if (signal === void 0) return { signal: timeout, timeout };
|
|
144
|
+
const anyOf = AbortSignal.any;
|
|
145
|
+
if (typeof anyOf === "function") return { signal: anyOf([signal, timeout]), timeout };
|
|
146
|
+
const controller = new AbortController();
|
|
147
|
+
const abortFrom = (src) => controller.abort(src.reason);
|
|
148
|
+
if (signal.aborted) controller.abort(signal.reason);
|
|
149
|
+
else if (timeout.aborted) controller.abort(timeout.reason);
|
|
150
|
+
else {
|
|
151
|
+
signal.addEventListener("abort", () => abortFrom(signal), { once: true });
|
|
152
|
+
timeout.addEventListener("abort", () => abortFrom(timeout), { once: true });
|
|
153
|
+
}
|
|
154
|
+
return { signal: controller.signal, timeout };
|
|
155
|
+
}
|
|
156
|
+
function asTimeoutError(error, timeoutMs) {
|
|
157
|
+
if (typeof error === "object" && error !== null && error.name === "TimeoutError") {
|
|
158
|
+
return new TinifyTimeoutError(`Request timed out after ${timeoutMs} ms.`, timeoutMs);
|
|
159
|
+
}
|
|
160
|
+
return error;
|
|
161
|
+
}
|
|
162
|
+
async function fetchWithTimeout(fetchImpl, url, init, timeoutMs, userSignal) {
|
|
163
|
+
const { signal, timeout } = withTimeoutSignal(timeoutMs, userSignal);
|
|
164
|
+
try {
|
|
165
|
+
return await fetchImpl(url, { ...init, signal });
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (timeout.aborted) {
|
|
168
|
+
throw new TinifyTimeoutError(`Request timed out after ${timeoutMs} ms.`, timeoutMs);
|
|
169
|
+
}
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
239
173
|
var HttpClient = class {
|
|
240
174
|
config;
|
|
241
175
|
constructor(config) {
|
|
@@ -268,19 +202,21 @@ var HttpClient = class {
|
|
|
268
202
|
const headers = {
|
|
269
203
|
authorization: `Bearer ${this.config.apiKey}`,
|
|
270
204
|
accept: "application/json",
|
|
271
|
-
"user-agent": "@tinify-dev/client/0.1.
|
|
205
|
+
"user-agent": "@tinify-dev/client/0.1.1"
|
|
272
206
|
};
|
|
273
207
|
if (idempotencyKey !== void 0) headers["idempotency-key"] = idempotencyKey;
|
|
274
208
|
if (options.contentType !== void 0) headers["content-type"] = options.contentType;
|
|
275
209
|
let response;
|
|
276
210
|
try {
|
|
277
|
-
response = await
|
|
211
|
+
response = await fetchWithTimeout(
|
|
212
|
+
this.config.fetchImpl,
|
|
278
213
|
url,
|
|
279
214
|
{
|
|
280
215
|
method: options.method,
|
|
281
216
|
headers,
|
|
282
217
|
...options.body !== void 0 ? { body: options.body() } : {}
|
|
283
218
|
},
|
|
219
|
+
this.config.timeoutMs,
|
|
284
220
|
options.signal
|
|
285
221
|
);
|
|
286
222
|
} catch (error) {
|
|
@@ -299,7 +235,8 @@ var HttpClient = class {
|
|
|
299
235
|
if (RETRYABLE_STATUSES.has(response.status) && attempt < this.config.maxRetries) {
|
|
300
236
|
const retryAfter = parseRetryAfterMs(response.headers.get("retry-after"));
|
|
301
237
|
await response.arrayBuffer().catch(() => void 0);
|
|
302
|
-
|
|
238
|
+
const wait = Math.min(retryAfter ?? backoffMs(attempt), MAX_RETRY_AFTER_MS);
|
|
239
|
+
await sleep(wait, options.signal);
|
|
303
240
|
attempt += 1;
|
|
304
241
|
continue;
|
|
305
242
|
}
|
|
@@ -315,10 +252,14 @@ var HttpClient = class {
|
|
|
315
252
|
async requestJson(options) {
|
|
316
253
|
const result = await this.request(options);
|
|
317
254
|
let body;
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
255
|
+
try {
|
|
256
|
+
if (result.response.status === 204) {
|
|
257
|
+
body = void 0;
|
|
258
|
+
} else {
|
|
259
|
+
body = await result.response.json();
|
|
260
|
+
}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
throw asTimeoutError(error, this.config.timeoutMs);
|
|
322
263
|
}
|
|
323
264
|
const bodyRequestId = body?.request_id;
|
|
324
265
|
return {
|
|
@@ -327,29 +268,112 @@ var HttpClient = class {
|
|
|
327
268
|
rateLimit: result.rateLimit
|
|
328
269
|
};
|
|
329
270
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
271
|
+
/** The configured per-request timeout (ms); used by download/upload paths. */
|
|
272
|
+
get timeoutMs() {
|
|
273
|
+
return this.config.timeoutMs;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// src/inputs.ts
|
|
278
|
+
var DEFAULT_FILENAME = "image";
|
|
279
|
+
async function normalizeInput(input, filename) {
|
|
280
|
+
if (typeof input === "string") {
|
|
281
|
+
let fs;
|
|
282
|
+
let path;
|
|
339
283
|
try {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
throw error;
|
|
347
|
-
} finally {
|
|
348
|
-
clearTimeout(timer);
|
|
349
|
-
signal?.removeEventListener("abort", onAbort);
|
|
284
|
+
fs = await import('fs/promises');
|
|
285
|
+
path = await import('path');
|
|
286
|
+
} catch {
|
|
287
|
+
throw new TinifyError(
|
|
288
|
+
"File path inputs are only supported in Node.js. Pass a Blob or File instead."
|
|
289
|
+
);
|
|
350
290
|
}
|
|
291
|
+
const bytes = await fs.readFile(input);
|
|
292
|
+
return {
|
|
293
|
+
blob: new Blob([bytesToView(bytes)]),
|
|
294
|
+
filename: filename ?? path.basename(input)
|
|
295
|
+
};
|
|
351
296
|
}
|
|
352
|
-
|
|
297
|
+
if (typeof Blob !== "undefined" && input instanceof Blob) {
|
|
298
|
+
const name = filename ?? (typeof File !== "undefined" && input instanceof File && input.name ? input.name : DEFAULT_FILENAME);
|
|
299
|
+
return { blob: input, filename: name };
|
|
300
|
+
}
|
|
301
|
+
if (input instanceof ArrayBuffer) {
|
|
302
|
+
return { blob: new Blob([input]), filename: filename ?? DEFAULT_FILENAME };
|
|
303
|
+
}
|
|
304
|
+
if (ArrayBuffer.isView(input)) {
|
|
305
|
+
return {
|
|
306
|
+
blob: new Blob([bytesToView(input)]),
|
|
307
|
+
filename: filename ?? DEFAULT_FILENAME
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
throw new TypeError(
|
|
311
|
+
"Unsupported input: expected a file path, Uint8Array, ArrayBuffer, Blob, or File."
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
function bytesToView(view) {
|
|
315
|
+
const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
|
|
316
|
+
if (bytes.buffer instanceof ArrayBuffer && bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength) {
|
|
317
|
+
return bytes;
|
|
318
|
+
}
|
|
319
|
+
const copy = new Uint8Array(new ArrayBuffer(bytes.byteLength));
|
|
320
|
+
copy.set(bytes);
|
|
321
|
+
return copy;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/batches.ts
|
|
325
|
+
var TERMINAL = new Set(BATCH_TERMINAL_STATUSES);
|
|
326
|
+
function isTerminalBatchStatus(status) {
|
|
327
|
+
return TERMINAL.has(status);
|
|
328
|
+
}
|
|
329
|
+
async function uploadBatchFiles(fetchImpl, session, files, options, timeoutMs) {
|
|
330
|
+
const missing = session.uploads.map((upload) => upload.client_id).filter((clientId) => !(clientId in files));
|
|
331
|
+
if (missing.length > 0) {
|
|
332
|
+
throw new TinifyError(
|
|
333
|
+
`Missing input for batch client_id(s): ${missing.join(", ")}. Pass a { client_id: input } record covering every file in the session.`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
const queue = [...session.uploads];
|
|
337
|
+
const concurrency = Math.max(
|
|
338
|
+
1,
|
|
339
|
+
Math.min(options.concurrency ?? DEFAULT_UPLOAD_CONCURRENCY, queue.length)
|
|
340
|
+
);
|
|
341
|
+
const workers = Array.from({ length: concurrency }, async () => {
|
|
342
|
+
for (; ; ) {
|
|
343
|
+
const upload = queue.shift();
|
|
344
|
+
if (upload === void 0) return;
|
|
345
|
+
options.signal?.throwIfAborted();
|
|
346
|
+
await putOne(fetchImpl, upload, files[upload.client_id], timeoutMs, options.signal);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
await Promise.all(workers);
|
|
350
|
+
}
|
|
351
|
+
async function putOne(fetchImpl, upload, input, timeoutMs, signal) {
|
|
352
|
+
const { blob } = await normalizeInput(input);
|
|
353
|
+
let response;
|
|
354
|
+
try {
|
|
355
|
+
response = await fetchWithTimeout(
|
|
356
|
+
fetchImpl,
|
|
357
|
+
upload.upload_url,
|
|
358
|
+
{ method: "PUT", headers: upload.headers, body: blob },
|
|
359
|
+
timeoutMs,
|
|
360
|
+
signal
|
|
361
|
+
);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
if (error instanceof TinifyTimeoutError) throw error;
|
|
364
|
+
throw new TinifyNetworkError(
|
|
365
|
+
`Upload for client_id "${upload.client_id}" failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
366
|
+
{ cause: error }
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
if (!response.ok) {
|
|
370
|
+
const text = await response.text().catch(() => "");
|
|
371
|
+
throw new TinifyError(
|
|
372
|
+
`Upload for client_id "${upload.client_id}" was rejected with HTTP ${response.status}` + (text ? `: ${text.slice(0, 200)}` : ".")
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
await response.arrayBuffer().catch(() => void 0);
|
|
376
|
+
}
|
|
353
377
|
|
|
354
378
|
// src/client.ts
|
|
355
379
|
var TinifyClient = class {
|
|
@@ -453,12 +477,19 @@ var TinifyClient = class {
|
|
|
453
477
|
async download(resultOrUrl, options = {}) {
|
|
454
478
|
const raw = typeof resultOrUrl === "string" ? resultOrUrl : "download_url" in resultOrUrl ? resultOrUrl.download_url : resultOrUrl.data.download_url;
|
|
455
479
|
const url = new URL(raw, `${this.http.baseUrl}/`).toString();
|
|
456
|
-
const response = await
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
480
|
+
const response = await fetchWithTimeout(
|
|
481
|
+
this.http.fetchImpl,
|
|
482
|
+
url,
|
|
483
|
+
{ method: "GET" },
|
|
484
|
+
this.http.timeoutMs,
|
|
485
|
+
options.signal
|
|
486
|
+
);
|
|
460
487
|
if (!response.ok) await throwApiError(response);
|
|
461
|
-
|
|
488
|
+
try {
|
|
489
|
+
return await response.blob();
|
|
490
|
+
} catch (error) {
|
|
491
|
+
throw asTimeoutError(error, this.http.timeoutMs);
|
|
492
|
+
}
|
|
462
493
|
}
|
|
463
494
|
// -------------------------------------------------------------------------
|
|
464
495
|
// Batches
|
|
@@ -483,7 +514,7 @@ var TinifyClient = class {
|
|
|
483
514
|
* `client_id`. Runs up to `concurrency` (default 4) PUTs in parallel.
|
|
484
515
|
*/
|
|
485
516
|
uploadBatchFiles(session, files, options = {}) {
|
|
486
|
-
return uploadBatchFiles(this.http.fetchImpl, session, files, options);
|
|
517
|
+
return uploadBatchFiles(this.http.fetchImpl, session, files, options, this.http.timeoutMs);
|
|
487
518
|
}
|
|
488
519
|
/** Commits an uploaded batch for processing. */
|
|
489
520
|
async commitBatch(batchId, options = {}) {
|
|
@@ -548,7 +579,11 @@ var TinifyClient = class {
|
|
|
548
579
|
path: `/api/v1/batches/${encodeURIComponent(batchId)}/archive`,
|
|
549
580
|
...options.signal !== void 0 ? { signal: options.signal } : {}
|
|
550
581
|
});
|
|
551
|
-
|
|
582
|
+
try {
|
|
583
|
+
return await result.response.blob();
|
|
584
|
+
} catch (error) {
|
|
585
|
+
throw asTimeoutError(error, this.http.timeoutMs);
|
|
586
|
+
}
|
|
552
587
|
}
|
|
553
588
|
// -------------------------------------------------------------------------
|
|
554
589
|
// Internals
|
|
@@ -584,7 +619,7 @@ var CliUsageError = class extends Error {
|
|
|
584
619
|
this.name = "CliUsageError";
|
|
585
620
|
}
|
|
586
621
|
};
|
|
587
|
-
var HELP_TEXT = `tinify-dev
|
|
622
|
+
var HELP_TEXT = `tinify-dev - Tinify.dev image API CLI
|
|
588
623
|
|
|
589
624
|
Usage:
|
|
590
625
|
tinify-dev <command> [options] <files...>
|