polotno-node 3.8.0 → 3.8.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/lib/instance.js +21 -35
- package/package.json +2 -3
package/lib/instance.js
CHANGED
|
@@ -11,7 +11,6 @@ import * as fontCache from './font-cache.js';
|
|
|
11
11
|
import * as localMedia from './local-media.js';
|
|
12
12
|
import { polotnoError, rehydrateError } from './errors.js';
|
|
13
13
|
import { assertUrlAllowed } from './ssrf-guard.js';
|
|
14
|
-
import { isRetryableStatus, withRetry } from '@polotno/core/retry';
|
|
15
14
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
15
|
// Compiled to lib/instance.js; the client bundle is published to ../dist.
|
|
17
16
|
const DEFAULT_CLIENT = `file:${path.join(__dirname, '..', 'dist', 'index.html')}`;
|
|
@@ -205,13 +204,12 @@ const guardedFetch = async (startUrl, policy, signal) => {
|
|
|
205
204
|
// start failing exports that work today. A connect/TTFB that never arrives is
|
|
206
205
|
// unambiguous, so that is what this bounds.
|
|
207
206
|
const HEADERS_TIMEOUT_MS = 30000;
|
|
207
|
+
const MAX_ATTEMPTS = 3;
|
|
208
208
|
const downloadUrlToFile = async (urlString, destinationPath, policy = {}) => {
|
|
209
209
|
if (typeof fetch !== 'function') {
|
|
210
210
|
throw polotnoError('FETCH_FAILED', 'Global fetch API is not available in this Node.js runtime. Please use Node 18+.', { url: urlString });
|
|
211
211
|
}
|
|
212
|
-
|
|
213
|
-
const attemptDownload = async (attempt) => {
|
|
214
|
-
attempts = attempt;
|
|
212
|
+
const attemptDownload = async () => {
|
|
215
213
|
// Ensure no partial file exists from a previous attempt
|
|
216
214
|
await fs.promises.rm(destinationPath, { force: true });
|
|
217
215
|
const controller = new AbortController();
|
|
@@ -252,38 +250,26 @@ const downloadUrlToFile = async (urlString, destinationPath, policy = {}) => {
|
|
|
252
250
|
await pipeline(Readable.fromWeb(res.body), fs.createWriteStream(destinationPath));
|
|
253
251
|
return res.headers.get('content-type') || '';
|
|
254
252
|
};
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
return true;
|
|
275
|
-
},
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
catch (lastError) {
|
|
279
|
-
// A policy rejection is surfaced untouched, as it was before this loop
|
|
280
|
-
// delegated its retrying: `ssrfBlocked` is what callers branch on, and a
|
|
281
|
-
// second FETCH_FAILED wrap buries it on `cause`.
|
|
282
|
-
if (lastError?.ssrfBlocked) {
|
|
283
|
-
throw lastError;
|
|
253
|
+
// A local loop rather than @polotno/core/retry: importing that made core (and
|
|
254
|
+
// konva) a runtime dependency of polotno-node for one call site.
|
|
255
|
+
for (let attempt = 1;; attempt++) {
|
|
256
|
+
try {
|
|
257
|
+
return await attemptDownload();
|
|
258
|
+
}
|
|
259
|
+
catch (e) {
|
|
260
|
+
// Surfaced untouched: callers branch on `ssrfBlocked`, and re-resolving
|
|
261
|
+
// the same blocked host cannot succeed.
|
|
262
|
+
if (e?.ssrfBlocked)
|
|
263
|
+
throw e;
|
|
264
|
+
const status = e?.details?.status;
|
|
265
|
+
// A 4xx other than 429 is deterministic; anything else may be transient.
|
|
266
|
+
const retryable = status === undefined || status === 429 || status >= 500;
|
|
267
|
+
if (!retryable || attempt >= MAX_ATTEMPTS) {
|
|
268
|
+
const message = e?.message || String(e);
|
|
269
|
+
throw polotnoError('FETCH_FAILED', `Failed to download media after ${attempt} attempt${attempt === 1 ? '' : 's'}: ${urlString}\nLast error: ${message}`, { url: urlString, attempts: attempt }, e);
|
|
270
|
+
}
|
|
271
|
+
await new Promise((r) => setTimeout(r, 300 * 2 ** (attempt - 1)));
|
|
284
272
|
}
|
|
285
|
-
const message = lastError?.message || String(lastError);
|
|
286
|
-
throw polotnoError('FETCH_FAILED', `Failed to download media after ${attempts} attempt${attempts === 1 ? '' : 's'}: ${urlString}\nLast error: ${message}`, { url: urlString, attempts }, lastError);
|
|
287
273
|
}
|
|
288
274
|
};
|
|
289
275
|
// Returns a human-readable reason string if the video file needs transcoding
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polotno-node",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.1",
|
|
4
4
|
"description": "Polotno workflow from NodeJS",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"type": "module",
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"vite": "^8.2.2",
|
|
52
52
|
"vitest": "^4.1.11",
|
|
53
53
|
"web-audio-api": "^1.5.5",
|
|
54
|
-
"polotno": "4.12.0",
|
|
55
54
|
"@polotno/video-export": "0.2.5",
|
|
55
|
+
"polotno": "4.12.0",
|
|
56
56
|
"@polotno/design-fixtures": "0.0.0"
|
|
57
57
|
},
|
|
58
58
|
"overrides": {
|
|
@@ -66,7 +66,6 @@
|
|
|
66
66
|
"@sparticuz/chromium": "^149.0.0",
|
|
67
67
|
"puppeteer": "25.9.0",
|
|
68
68
|
"puppeteer-core": "25.9.0",
|
|
69
|
-
"@polotno/core": "0.10.0",
|
|
70
69
|
"@polotno/schema": "0.6.1"
|
|
71
70
|
},
|
|
72
71
|
"bugs": {
|