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.
Files changed (2) hide show
  1. package/lib/instance.js +21 -35
  2. 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
- let attempts = 0;
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
- try {
256
- return await withRetry(attemptDownload, {
257
- // The shared policy (@polotno/core/retry), with this path's own pacing:
258
- // local media is staged before the render starts, so it can afford to be
259
- // quicker off the mark than an in-render asset fetch.
260
- baseDelayMs: 300,
261
- // Stated as what is TERMINAL, not what is transient: a transport
262
- // allow-list silently missed this loop's own timeout abort.
263
- shouldRetry: (e) => {
264
- // Blocked by policy (SSRF) — deterministic, and re-resolving the same
265
- // bad host would only burn the budget.
266
- if (e?.ssrfBlocked)
267
- return false;
268
- const status = e?.details
269
- ?.status;
270
- // A 4xx means the media is not there. This loop used to retry it three
271
- // times before saying so.
272
- if (typeof status === 'number')
273
- return isRetryableStatus(status);
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.0",
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": {