scrapely 1.2.36 → 1.2.37
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 +7 -0
- package/dist/actor.d.ts +7 -0
- package/dist/actor.js +29 -3
- package/dist/storage/api-storage-client.d.ts +7 -6
- package/dist/storage/api-storage-client.js +12 -8
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [1.2.37] - 2026-10-26
|
|
8
|
+
|
|
9
|
+
- Opening a named dataset, key-value store or request queue (`Actor.openDataset('name')` and the like) retries when the API answers `503 storage-name-busy`. The API sends it when another request kept creating or renaming a storage of that name for over 15 s; nothing was changed, so the open is simply repeated. It used to fail the open.
|
|
10
|
+
- Once `Actor.fail()` has run, the run stays failed: a later `Actor.exit()`, or the end of `Actor.main()`, keeps the failure's exit code and status message. `fail()` normally ends the process at once, so this only changed runs that called `fail({ exit: false })` and then `exit()` or returned from `Actor.main()`; they used to exit with 0. A second `fail()` keeps the first failure. `fail({ exitCode: undefined })` exits with 1.
|
|
11
|
+
|
|
5
12
|
## [1.2.36] - 2026-10-22
|
|
6
13
|
|
|
7
14
|
- Debug captures (`Actor.debugCapture`, `Actor.debugCaptureSync`) keep payloads over 256 KiB whole. They are stored gzip-compressed and base64-encoded as `{ key, payload, encoding: 'gzip-base64', length, truncated }` instead of being cut at 262,144 characters; up to 8 Mi characters are kept. A payload that does not compress enough keeps its first 256 KiB, now marked `truncated: true` with its full `length`. `Uint8Array` / `Buffer` payloads are stored as UTF-8 text. Captures are only active on the platform's own diagnostic runs (`SCRAPELY_DEBUG_CAPTURE=1`).
|
package/dist/actor.d.ts
CHANGED
|
@@ -332,6 +332,12 @@ export declare class Actor {
|
|
|
332
332
|
* succeeds.
|
|
333
333
|
*/
|
|
334
334
|
private _lifecycleStarted;
|
|
335
|
+
/**
|
|
336
|
+
* Set by {@link Actor.fail} for the rest of the run: a later {@link Actor.exit}
|
|
337
|
+
* keeps the failure's exit code and status message, so a failed run never
|
|
338
|
+
* ends as SUCCEEDED (e.g. `fail({ exit: false })` followed by `exit()`).
|
|
339
|
+
*/
|
|
340
|
+
private static _failure;
|
|
335
341
|
/** Created on first use: scrapely-client is only loaded when something needs the API client. */
|
|
336
342
|
private _apifyClient;
|
|
337
343
|
private readonly chargingManager;
|
|
@@ -388,6 +394,7 @@ export declare class Actor {
|
|
|
388
394
|
* @param options - Exit options
|
|
389
395
|
*/
|
|
390
396
|
static exit(messageOrOptions?: string | ExitOptions, options?: ExitOptions): Promise<void>;
|
|
397
|
+
private static _exitWith;
|
|
391
398
|
static fail(messageOrOptions?: string | ExitOptions, options?: ExitOptions): Promise<void>;
|
|
392
399
|
static getInput<T = ActorInput>(): Promise<T | null>;
|
|
393
400
|
static getInputOrThrow<T = ActorInput>(): Promise<T>;
|
package/dist/actor.js
CHANGED
|
@@ -112,6 +112,12 @@ class Actor {
|
|
|
112
112
|
* succeeds.
|
|
113
113
|
*/
|
|
114
114
|
_lifecycleStarted = false;
|
|
115
|
+
/**
|
|
116
|
+
* Set by {@link Actor.fail} for the rest of the run: a later {@link Actor.exit}
|
|
117
|
+
* keeps the failure's exit code and status message, so a failed run never
|
|
118
|
+
* ends as SUCCEEDED (e.g. `fail({ exit: false })` followed by `exit()`).
|
|
119
|
+
*/
|
|
120
|
+
static _failure;
|
|
115
121
|
/** Created on first use: scrapely-client is only loaded when something needs the API client. */
|
|
116
122
|
_apifyClient;
|
|
117
123
|
chargingManager;
|
|
@@ -189,6 +195,7 @@ class Actor {
|
|
|
189
195
|
if (this.initialized)
|
|
190
196
|
return;
|
|
191
197
|
cachedInput = undefined; // a fresh init reads this run's input again
|
|
198
|
+
Actor._failure = undefined; // a new lifecycle starts unfailed
|
|
192
199
|
// NOTE: `initialized` is set at the END of init — a failed platform init
|
|
193
200
|
// must stay retryable instead of leaving a half-initialized Actor.
|
|
194
201
|
Actor.installFatalChargeHandlers();
|
|
@@ -375,7 +382,8 @@ class Actor {
|
|
|
375
382
|
const result = await userFunc();
|
|
376
383
|
await Actor._shutdownActor(options?.timeoutSecs ?? 30);
|
|
377
384
|
await new Promise((resolve) => process.stdout.write('', () => resolve()));
|
|
378
|
-
|
|
385
|
+
// A run that called fail({ exit: false }) stays failed.
|
|
386
|
+
process.exit(Actor._failure?.exitCode ?? options?.exitCode ?? 0);
|
|
379
387
|
return result;
|
|
380
388
|
}
|
|
381
389
|
catch (err) {
|
|
@@ -403,10 +411,22 @@ class Actor {
|
|
|
403
411
|
const opts = typeof messageOrOptions === 'string'
|
|
404
412
|
? { ...options, statusMessage: messageOrOptions }
|
|
405
413
|
: { ...messageOrOptions, ...options };
|
|
414
|
+
Actor._ensureActorInit('exit');
|
|
415
|
+
const failure = Actor._failure;
|
|
416
|
+
if (failure) {
|
|
417
|
+
// Actor.fail() already ran: the run stays failed. Keep its exit code and
|
|
418
|
+
// status message; this exit() only finishes the shutdown.
|
|
419
|
+
if (opts.statusMessage)
|
|
420
|
+
console.log(`[Scrapely] ${opts.statusMessage}`);
|
|
421
|
+
console.log(`[Scrapely] Actor.exit() after Actor.fail(): the run stays failed (exit code ${failure.exitCode}).`);
|
|
422
|
+
return Actor._exitWith({ ...opts, exitCode: failure.exitCode, statusMessage: undefined });
|
|
423
|
+
}
|
|
424
|
+
return Actor._exitWith(opts);
|
|
425
|
+
}
|
|
426
|
+
static async _exitWith(opts) {
|
|
406
427
|
const code = opts.exitCode ?? 0;
|
|
407
428
|
const timeoutSecs = opts.timeoutSecs ?? 30;
|
|
408
429
|
const shouldExit = opts.exit ?? true;
|
|
409
|
-
Actor._ensureActorInit('exit');
|
|
410
430
|
// The exit/fail message becomes the run's terminal status
|
|
411
431
|
// message. Stored before teardown, while the run and process are alive.
|
|
412
432
|
if (opts.statusMessage)
|
|
@@ -423,7 +443,13 @@ class Actor {
|
|
|
423
443
|
: { exitCode: 1, ...messageOrOptions, ...options };
|
|
424
444
|
console.error(`[Scrapely] Actor failed: ${opts.statusMessage ?? 'Unknown error'}`);
|
|
425
445
|
await new Promise((resolve) => process.stderr.write('', () => resolve()));
|
|
426
|
-
|
|
446
|
+
Actor._ensureActorInit('fail');
|
|
447
|
+
if (Actor._failure) {
|
|
448
|
+
// Failed already: the first failure's code and message stay.
|
|
449
|
+
return Actor._exitWith({ ...opts, exitCode: Actor._failure.exitCode, statusMessage: undefined });
|
|
450
|
+
}
|
|
451
|
+
Actor._failure = { exitCode: opts.exitCode ?? 1, statusMessage: opts.statusMessage };
|
|
452
|
+
await Actor._exitWith({ ...opts, exitCode: opts.exitCode ?? 1 });
|
|
427
453
|
}
|
|
428
454
|
static async getInput() {
|
|
429
455
|
Actor._ensureActorInit('getInput');
|
|
@@ -15,12 +15,13 @@ export declare class ApiStorageClientError extends Error {
|
|
|
15
15
|
/**
|
|
16
16
|
* Whether a non-2xx answer is retried. 429 always: the run API limiter and
|
|
17
17
|
* dataset backpressure refuse it before anything is written. Idempotent
|
|
18
|
-
* methods: also 500/502/503/504. Others (dataset push, KV PUT
|
|
19
|
-
* that certainly was not processed: the rate limiter
|
|
20
|
-
* ("rate-limit-unavailable", before the handler),
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
18
|
+
* methods: also 500/502/503/504. Others (dataset push, KV PUT, named storage
|
|
19
|
+
* create) only on a 503 that certainly was not processed: the rate limiter
|
|
20
|
+
* refused it ("rate-limit-unavailable", before the handler), another request
|
|
21
|
+
* held the storage name ("storage-name-busy", nothing changed), or HAProxy had
|
|
22
|
+
* no backend (its own page, nothing forwarded). Any other 503 may mean
|
|
23
|
+
* "outcome unknown" (e.g. a direct dataset write whose commit was not
|
|
24
|
+
* confirmed), and re-sending could store the items twice.
|
|
24
25
|
*/
|
|
25
26
|
export declare function isRetryableResponse(status: number, idempotent: boolean, errorType?: string, bodyText?: string): boolean;
|
|
26
27
|
/** Default time budget for retrying one API call (429, 503, network errors). */
|
|
@@ -147,12 +147,13 @@ const CONNECT_FAILURE_CODES = new Set(['ECONNREFUSED', 'ENOTFOUND', 'EAI_AGAIN',
|
|
|
147
147
|
/**
|
|
148
148
|
* Whether a non-2xx answer is retried. 429 always: the run API limiter and
|
|
149
149
|
* dataset backpressure refuse it before anything is written. Idempotent
|
|
150
|
-
* methods: also 500/502/503/504. Others (dataset push, KV PUT
|
|
151
|
-
* that certainly was not processed: the rate limiter
|
|
152
|
-
* ("rate-limit-unavailable", before the handler),
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
150
|
+
* methods: also 500/502/503/504. Others (dataset push, KV PUT, named storage
|
|
151
|
+
* create) only on a 503 that certainly was not processed: the rate limiter
|
|
152
|
+
* refused it ("rate-limit-unavailable", before the handler), another request
|
|
153
|
+
* held the storage name ("storage-name-busy", nothing changed), or HAProxy had
|
|
154
|
+
* no backend (its own page, nothing forwarded). Any other 503 may mean
|
|
155
|
+
* "outcome unknown" (e.g. a direct dataset write whose commit was not
|
|
156
|
+
* confirmed), and re-sending could store the items twice.
|
|
156
157
|
*/
|
|
157
158
|
function isRetryableResponse(status, idempotent, errorType, bodyText) {
|
|
158
159
|
if (status === 429)
|
|
@@ -161,7 +162,9 @@ function isRetryableResponse(status, idempotent, errorType, bodyText) {
|
|
|
161
162
|
return status === 500 || status === 502 || status === 503 || status === 504;
|
|
162
163
|
if (status !== 503)
|
|
163
164
|
return false;
|
|
164
|
-
return errorType === 'rate-limit-unavailable'
|
|
165
|
+
return errorType === 'rate-limit-unavailable'
|
|
166
|
+
|| errorType === 'storage-name-busy'
|
|
167
|
+
|| (bodyText ?? '').includes(NO_BACKEND_MARKER);
|
|
165
168
|
}
|
|
166
169
|
/** Default time budget for retrying one API call (429, 503, network errors). */
|
|
167
170
|
exports.DEFAULT_API_RETRY_WINDOW_MS = 300_000;
|
|
@@ -450,7 +453,8 @@ async function postStorageCollection(config, path, name) {
|
|
|
450
453
|
// Not marked repeatable even by name: a 500 after the create (e.g. scoped
|
|
451
454
|
// token registration failed) must surface, since a re-send would take the
|
|
452
455
|
// existing-name path and hide it. POST rules apply (limiter/HAProxy 503,
|
|
453
|
-
// refused
|
|
456
|
+
// "storage-name-busy" 503 while a parallel create holds the name, refused
|
|
457
|
+
// connection).
|
|
454
458
|
return new CollectionApiClient(config).call('POST', `${path}${query}`);
|
|
455
459
|
}
|
|
456
460
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scrapely",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.37",
|
|
4
4
|
"description": "Scrapely SDK for building JavaScript and TypeScript Actors",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"test:run-control": "npx tsx --test tests/run-control.test.ts",
|
|
70
70
|
"test:rq-list": "npx tsx --test tests/rq-list-requests.test.ts",
|
|
71
71
|
"test:terminal-status": "npx tsx --test tests/terminal-status-message.test.ts",
|
|
72
|
+
"test:fail-then-exit": "npx tsx --test tests/fail-then-exit.test.ts",
|
|
72
73
|
"test:cdp-retry": "npx tsx --test tests/cdp-retry.test.ts",
|
|
73
74
|
"test:proxy-input-aliases": "npx tsx --test tests/proxy-input-aliases.test.ts",
|
|
74
75
|
"test:charge-retries": "npx tsx --test tests/charge-retries.test.ts",
|