scrapely 1.2.33 → 1.2.34
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/NOTICE +12 -26
- package/README.md +1 -2
- package/dist/actor.d.ts +32 -25
- package/dist/actor.js +39 -23
- package/dist/charging.d.ts +49 -4
- package/dist/charging.js +293 -44
- package/dist/configuration.js +2 -2
- package/dist/index.js +1 -1
- package/dist/node-wrapper.js +1 -1
- package/dist/patch-browser-pool.js +7 -3
- package/dist/proxy-configuration.d.ts +11 -11
- package/dist/proxy-configuration.js +1 -1
- package/dist/proxy-input-aliases.d.ts +1 -1
- package/dist/proxy-input-aliases.js +1 -1
- package/dist/request-list.d.ts +2 -2
- package/dist/request-list.js +1 -1
- package/dist/scrapely-proxy-configuration.d.ts +15 -16
- package/dist/scrapely-proxy-configuration.js +27 -31
- package/dist/storage/api-storage-client.d.ts +4 -5
- package/dist/storage/api-storage-client.js +17 -15
- package/dist/storage/billing-tracker.d.ts +1 -1
- package/dist/storage/billing-tracker.js +1 -1
- package/dist/storage/key-value-store-client.d.ts +0 -3
- package/dist/storage/key-value-store-client.js +2 -5
- package/dist/storage/kv-store-collection-client.js +6 -2
- package/dist/storage/kvs-iteration.d.ts +1 -1
- package/dist/storage/kvs-iteration.js +1 -1
- package/dist/storage/request-queue-client.d.ts +1 -1
- package/dist/storage/request-queue-client.js +5 -5
- package/dist/storage/rq-lock-script.d.ts +2 -2
- package/dist/storage/rq-lock-script.js +2 -2
- package/dist/storage/run-metadata.js +1 -1
- package/dist/storage/ttl-manager.d.ts +7 -6
- package/dist/storage/ttl-manager.js +5 -4
- package/dist/storage-identifiers.d.ts +1 -2
- package/dist/storage-identifiers.js +1 -2
- package/dist/utils.d.ts +41 -8
- package/dist/utils.js +49 -3
- package/package.json +22 -11
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Proxy input objects with both namings. An input-schema proxy field may be
|
|
4
4
|
* written with the Scrapely names (`useScrapelyProxy`, `scrapelyProxyGroups`,
|
|
5
|
-
* `scrapelyProxyCountry`, `scrapelyProxySubdivision`) or the
|
|
5
|
+
* `scrapelyProxyCountry`, `scrapelyProxySubdivision`) or the legacy names
|
|
6
6
|
* (`useApifyProxy`, `apifyProxy*`). Actor.getInput() and Actor.input return
|
|
7
7
|
* every set option under both names, so actor code that reads either naming
|
|
8
8
|
* sees the user's choice.
|
package/dist/request-list.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ScrapelyRequestList - parses the standard Scrapely
|
|
2
|
+
* ScrapelyRequestList - parses the standard Scrapely `requestListSources` input format.
|
|
3
3
|
*
|
|
4
4
|
* Parity port of the Python SDK's `scrapely.request_loaders.ScrapelyRequestList`.
|
|
5
5
|
* It supports two kinds of entries:
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
import { Request } from '@crawlee/core';
|
|
25
25
|
/**
|
|
26
26
|
* A single entry of the `requestListSources` input format.
|
|
27
|
-
* Accepts both camelCase (
|
|
27
|
+
* Accepts both camelCase (Scrapely UI) and snake_case keys.
|
|
28
28
|
*/
|
|
29
29
|
export interface RequestListSource {
|
|
30
30
|
/** Direct URL to crawl (mutually exclusive with `requestsFromUrl`). */
|
package/dist/request-list.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* ScrapelyRequestList - parses the standard Scrapely
|
|
3
|
+
* ScrapelyRequestList - parses the standard Scrapely `requestListSources` input format.
|
|
4
4
|
*
|
|
5
5
|
* Parity port of the Python SDK's `scrapely.request_loaders.ScrapelyRequestList`.
|
|
6
6
|
* It supports two kinds of entries:
|
|
@@ -6,12 +6,12 @@ type TieredProxyOptions = Parameters<CrawleeProxyConfiguration['newUrl']>[1];
|
|
|
6
6
|
/** Returns true for platform-proxy sentinel values ('auto', 'http://auto'). */
|
|
7
7
|
export declare function isPlatformProxySentinel(url: string | null | undefined): boolean;
|
|
8
8
|
/**
|
|
9
|
-
* Validates a proxy session ID
|
|
9
|
+
* Validates a proxy session ID: at most 50
|
|
10
10
|
* characters, and only `0-9`, `a-z`, `A-Z`, `.`, `_` and `~`. Throws otherwise.
|
|
11
11
|
*/
|
|
12
12
|
export declare function validateProxySessionId(sessionId: string): string;
|
|
13
13
|
/**
|
|
14
|
-
* Resolves and validates the country/subdivision options
|
|
14
|
+
* Resolves and validates the country/subdivision options.
|
|
15
15
|
* Country must be a two-letter ISO code; subdivision an ISO 3166-2 code
|
|
16
16
|
* (1-3 uppercase alphanumeric characters) and requires a country. Returns
|
|
17
17
|
* the normalized values used by the username grammar.
|
|
@@ -20,21 +20,21 @@ export declare function resolveCountryOptions(options: ProxyConfigurationOptions
|
|
|
20
20
|
countryCode?: string;
|
|
21
21
|
subdivisionCode?: string;
|
|
22
22
|
};
|
|
23
|
-
/** Status payload returned by the gateway status endpoint
|
|
23
|
+
/** Status payload returned by the gateway status endpoint. */
|
|
24
24
|
export interface ProxyStatus {
|
|
25
25
|
connected: boolean;
|
|
26
26
|
connectionError: string;
|
|
27
27
|
isManInTheMiddle: boolean;
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
|
-
* Build a platform proxy URL using the
|
|
30
|
+
* Build a platform proxy URL using the gateway username grammar:
|
|
31
31
|
* http://GROUPS-G1+G2,country-US,session-a1b2c3:PASSWORD@HOST:PORT
|
|
32
32
|
*
|
|
33
33
|
* `groups` (joined with `+`), `country` and `session` fragments are
|
|
34
34
|
* comma-joined. When the server injected a subaccount username
|
|
35
35
|
* (SCRAPELY_PROXY_USERNAME / APIFY_PROXY_USERNAME) it is prepended so the
|
|
36
36
|
* gateway can attribute traffic to the right Scrapely tenant; without it the
|
|
37
|
-
* username falls back to
|
|
37
|
+
* username falls back to `auto` when no fragments apply. The password
|
|
38
38
|
* and gateway host/port come from the environment credentials injected by the
|
|
39
39
|
* server.
|
|
40
40
|
*/
|
|
@@ -42,12 +42,12 @@ export declare function buildPlatformProxyUrl(options: ProxyConfigurationOptions
|
|
|
42
42
|
/** True when any proxy URL list reserves a platform slot via the 'auto' sentinel. */
|
|
43
43
|
export declare function hasPlatformSentinel(options: ProxyConfigurationOptions): boolean;
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
45
|
+
* Scrapely ProxyConfiguration.
|
|
46
46
|
*
|
|
47
47
|
* Modes:
|
|
48
48
|
* - Platform proxy: when the server injected APIFY_PROXY_* or SCRAPELY_PROXY_*
|
|
49
49
|
* credentials (Evomi-backed) and no custom proxies were requested, the
|
|
50
|
-
* configuration routes through the platform gateway using the
|
|
50
|
+
* configuration routes through the platform gateway using the gateway
|
|
51
51
|
* username grammar (groups/country/session fragments).
|
|
52
52
|
* - BYOP: custom proxyUrls / newUrlFunction / tieredProxyUrls are passed
|
|
53
53
|
* through to Crawlee unchanged.
|
|
@@ -68,31 +68,30 @@ export declare class ProxyConfiguration extends CrawleeProxyConfiguration {
|
|
|
68
68
|
* Returns a new proxy URL. Translates the internal platform-proxy
|
|
69
69
|
* placeholder to a concrete platform URL (applying groups/countryCode and
|
|
70
70
|
* session stickiness) whenever Crawlee's rotation lands on it; BYOP
|
|
71
|
-
* entries pass through unchanged. Session IDs are validated
|
|
72
|
-
*
|
|
71
|
+
* entries pass through unchanged. Session IDs are validated (max 50 chars,
|
|
72
|
+
* only `0-9 a-z A-Z . _ ~`).
|
|
73
73
|
*/
|
|
74
74
|
newUrl(sessionId?: string | number, options?: TieredProxyOptions): Promise<string | undefined>;
|
|
75
75
|
/**
|
|
76
76
|
* Returns a new ProxyInfo. When the resolved URL points at the platform
|
|
77
|
-
* gateway, `groups`, `countryCode` and `subdivisionCode` are attached
|
|
78
|
-
* (Apify parity).
|
|
77
|
+
* gateway, `groups`, `countryCode` and `subdivisionCode` are attached.
|
|
79
78
|
*/
|
|
80
79
|
newProxyInfo(sessionId?: string | number, options?: TieredProxyOptions): Promise<ProxyInfo | undefined>;
|
|
81
80
|
/**
|
|
82
|
-
* Fetches the gateway status payload
|
|
81
|
+
* Fetches the gateway status payload by sending
|
|
83
82
|
* `GET {statusUrl}/?format=json` *through the platform gateway*, so the
|
|
84
83
|
* proxy handshake itself authenticates the run's credentials. Retries
|
|
85
|
-
* connection errors (
|
|
84
|
+
* connection errors (4s timeout, 2 attempts); returns
|
|
86
85
|
* undefined when the endpoint stays unreachable.
|
|
87
86
|
*/
|
|
88
87
|
private fetchProxyStatus;
|
|
89
88
|
/**
|
|
90
89
|
* Verifies the proxy configuration. For platform proxy mode with an
|
|
91
|
-
* injected status URL, performs the
|
|
90
|
+
* injected status URL, performs the access check through the
|
|
92
91
|
* gateway (`GET {statusUrl}/?format=json`). When the gateway answers
|
|
93
92
|
* `connected: false` (e.g. an unknown proxy group), it throws with the
|
|
94
|
-
* gateway's `connectionError` on the platform and warns locally
|
|
95
|
-
*
|
|
93
|
+
* gateway's `connectionError` on the platform and warns locally;
|
|
94
|
+
* timeouts and HTTP errors only warn.
|
|
96
95
|
* BYOP mode only verifies that a test URL can be generated.
|
|
97
96
|
*/
|
|
98
97
|
initialize(options?: {
|
|
@@ -18,26 +18,25 @@ const node_tls_1 = __importDefault(require("node:tls"));
|
|
|
18
18
|
const core_1 = require("@crawlee/core");
|
|
19
19
|
const configuration_js_1 = require("./configuration.js");
|
|
20
20
|
const proxy_configuration_js_1 = require("./proxy-configuration.js");
|
|
21
|
-
/** Max length of a proxy session ID
|
|
21
|
+
/** Max length of a proxy session ID. */
|
|
22
22
|
const MAX_SESSION_ID_LENGTH = 50;
|
|
23
|
-
/** Allowed session ID characters
|
|
23
|
+
/** Allowed session ID characters: 0-9 a-z A-Z . _ ~ */
|
|
24
24
|
const SESSION_ID_REGEX = /^[a-zA-Z0-9._~]+$/;
|
|
25
|
-
/** Two-letter ISO country code
|
|
25
|
+
/** Two-letter ISO country code. */
|
|
26
26
|
const COUNTRY_CODE_REGEX = /^[A-Z]{2}$/;
|
|
27
27
|
/**
|
|
28
28
|
* ISO 3166-2 subdivision codes are 1-3 uppercase alphanumeric characters,
|
|
29
|
-
* e.g. 'CA' (California), 'NSW' (New South Wales), '9' (Wien, AT-9)
|
|
30
|
-
* (Apify parity).
|
|
29
|
+
* e.g. 'CA' (California), 'NSW' (New South Wales), '9' (Wien, AT-9).
|
|
31
30
|
*/
|
|
32
31
|
const SUBDIVISION_CODE_REGEX = /^[A-Z0-9]{1,3}$/;
|
|
33
|
-
/** Status-check timeout / attempts
|
|
32
|
+
/** Status-check timeout / attempts. */
|
|
34
33
|
const CHECK_ACCESS_REQUEST_TIMEOUT_MILLIS = 4_000;
|
|
35
34
|
const CHECK_ACCESS_MAX_ATTEMPTS = 2;
|
|
36
35
|
/**
|
|
37
36
|
* Sentinel values that reserve a slot for the platform proxy inside
|
|
38
37
|
* `proxyUrls` / `tieredProxyUrls` / `tieredProxyConfig[].proxyUrls`,
|
|
39
38
|
* allowing BYOP and platform proxies to be mixed in one configuration
|
|
40
|
-
* (
|
|
39
|
+
* (both `'auto'` and `'http://auto'` are accepted).
|
|
41
40
|
*/
|
|
42
41
|
const PLATFORM_PROXY_SENTINELS = new Set(['auto', 'http://auto']);
|
|
43
42
|
/**
|
|
@@ -64,7 +63,7 @@ function isPlatformProxySentinel(url) {
|
|
|
64
63
|
return url != null && PLATFORM_PROXY_SENTINELS.has(url);
|
|
65
64
|
}
|
|
66
65
|
/**
|
|
67
|
-
* Validates a proxy session ID
|
|
66
|
+
* Validates a proxy session ID: at most 50
|
|
68
67
|
* characters, and only `0-9`, `a-z`, `A-Z`, `.`, `_` and `~`. Throws otherwise.
|
|
69
68
|
*/
|
|
70
69
|
function validateProxySessionId(sessionId) {
|
|
@@ -84,7 +83,7 @@ function platformGroups(options) {
|
|
|
84
83
|
return options.scrapelyProxyGroups ?? options.apifyProxyGroups;
|
|
85
84
|
}
|
|
86
85
|
/**
|
|
87
|
-
* Resolves and validates the country/subdivision options
|
|
86
|
+
* Resolves and validates the country/subdivision options.
|
|
88
87
|
* Country must be a two-letter ISO code; subdivision an ISO 3166-2 code
|
|
89
88
|
* (1-3 uppercase alphanumeric characters) and requires a country. Returns
|
|
90
89
|
* the normalized values used by the username grammar.
|
|
@@ -109,7 +108,7 @@ function resolveCountryOptions(options) {
|
|
|
109
108
|
/**
|
|
110
109
|
* Strips URL-breaking characters from a username fragment. The gateway grammar
|
|
111
110
|
* is comma-based, so fragments themselves must never contain `,@:/?#` or
|
|
112
|
-
* whitespace (
|
|
111
|
+
* whitespace (the username is NOT percent-encoded).
|
|
113
112
|
*/
|
|
114
113
|
function sanitizeUsernameFragment(value) {
|
|
115
114
|
return value.replace(/[:@/?#\s]/g, '');
|
|
@@ -126,8 +125,7 @@ function encodeUserInfoPart(value) {
|
|
|
126
125
|
}
|
|
127
126
|
/**
|
|
128
127
|
* Gateway upstream error codes (590-599, proxy-chain semantics). The gateway
|
|
129
|
-
* uses these instead of a bare 502 to describe upstream failures
|
|
130
|
-
* table Apify Proxy uses. See docs/evomi-integration.md for the contract.
|
|
128
|
+
* uses these instead of a bare 502 to describe upstream failures. See docs/evomi-integration.md for the contract.
|
|
131
129
|
*/
|
|
132
130
|
const PROXY_STATUS_CODE_DESCRIPTIONS = {
|
|
133
131
|
590: 'Non Successful: upstream responded with a non-200 status code',
|
|
@@ -223,14 +221,14 @@ async function requestThroughProxy(targetUrl, proxyUrl, timeoutMs) {
|
|
|
223
221
|
});
|
|
224
222
|
}
|
|
225
223
|
/**
|
|
226
|
-
* Build a platform proxy URL using the
|
|
224
|
+
* Build a platform proxy URL using the gateway username grammar:
|
|
227
225
|
* http://GROUPS-G1+G2,country-US,session-a1b2c3:PASSWORD@HOST:PORT
|
|
228
226
|
*
|
|
229
227
|
* `groups` (joined with `+`), `country` and `session` fragments are
|
|
230
228
|
* comma-joined. When the server injected a subaccount username
|
|
231
229
|
* (SCRAPELY_PROXY_USERNAME / APIFY_PROXY_USERNAME) it is prepended so the
|
|
232
230
|
* gateway can attribute traffic to the right Scrapely tenant; without it the
|
|
233
|
-
* username falls back to
|
|
231
|
+
* username falls back to `auto` when no fragments apply. The password
|
|
234
232
|
* and gateway host/port come from the environment credentials injected by the
|
|
235
233
|
* server.
|
|
236
234
|
*/
|
|
@@ -238,7 +236,7 @@ function buildPlatformProxyUrl(options, env, sessionId) {
|
|
|
238
236
|
const groups = platformGroups(options);
|
|
239
237
|
const { countryCode, subdivisionCode } = resolveCountryOptions(options);
|
|
240
238
|
const password = options.password ?? env.password;
|
|
241
|
-
//
|
|
239
|
+
// Username grammar: groups, then session, then country — with the
|
|
242
240
|
// subdivision merged into the country fragment (ISO 3166-2: `country-US_CA`).
|
|
243
241
|
const usernameParts = [];
|
|
244
242
|
if (groups?.length) {
|
|
@@ -291,8 +289,7 @@ function hasRealCustomProxyOptions(options) {
|
|
|
291
289
|
* `proxyUrls` pass through (sentinels become per-tier placeholders); tiers
|
|
292
290
|
* without any URLs become per-tier placeholders too, so their platform
|
|
293
291
|
* options (groups/countryCode/password) and the session ID are applied
|
|
294
|
-
* lazily in `newUrl()`
|
|
295
|
-
* preserving session stickiness.
|
|
292
|
+
* lazily in `newUrl()` while preserving session stickiness.
|
|
296
293
|
*/
|
|
297
294
|
function mapTieredProxyConfig(tieredProxyConfig, globalOptions, env, placeholderOptions) {
|
|
298
295
|
return tieredProxyConfig.map((tier, index) => {
|
|
@@ -364,12 +361,12 @@ function needsPlatformProxyUrls(options) {
|
|
|
364
361
|
!!options.tieredProxyConfig?.some((tier) => !tier.proxyUrls));
|
|
365
362
|
}
|
|
366
363
|
/**
|
|
367
|
-
*
|
|
364
|
+
* Scrapely ProxyConfiguration.
|
|
368
365
|
*
|
|
369
366
|
* Modes:
|
|
370
367
|
* - Platform proxy: when the server injected APIFY_PROXY_* or SCRAPELY_PROXY_*
|
|
371
368
|
* credentials (Evomi-backed) and no custom proxies were requested, the
|
|
372
|
-
* configuration routes through the platform gateway using the
|
|
369
|
+
* configuration routes through the platform gateway using the gateway
|
|
373
370
|
* username grammar (groups/country/session fragments).
|
|
374
371
|
* - BYOP: custom proxyUrls / newUrlFunction / tieredProxyUrls are passed
|
|
375
372
|
* through to Crawlee unchanged.
|
|
@@ -388,7 +385,7 @@ class ProxyConfiguration extends core_1.ProxyConfiguration {
|
|
|
388
385
|
constructor(options = {}, config = configuration_js_1.Configuration.getGlobalConfig()) {
|
|
389
386
|
const { proxyUrls, newUrlFunction } = options;
|
|
390
387
|
const platformEnv = (0, proxy_configuration_js_1.getPlatformProxyEnv)();
|
|
391
|
-
// Validate country/subdivision options eagerly (
|
|
388
|
+
// Validate country/subdivision options eagerly (bad
|
|
392
389
|
// values throw at construction, not on the first newUrl() call).
|
|
393
390
|
resolveCountryOptions(options);
|
|
394
391
|
if (hasRealCustomProxyOptions(options) && hasApifyOnlyOptions(options)) {
|
|
@@ -429,8 +426,8 @@ class ProxyConfiguration extends core_1.ProxyConfiguration {
|
|
|
429
426
|
* Returns a new proxy URL. Translates the internal platform-proxy
|
|
430
427
|
* placeholder to a concrete platform URL (applying groups/countryCode and
|
|
431
428
|
* session stickiness) whenever Crawlee's rotation lands on it; BYOP
|
|
432
|
-
* entries pass through unchanged. Session IDs are validated
|
|
433
|
-
*
|
|
429
|
+
* entries pass through unchanged. Session IDs are validated (max 50 chars,
|
|
430
|
+
* only `0-9 a-z A-Z . _ ~`).
|
|
434
431
|
*/
|
|
435
432
|
async newUrl(sessionId, options) {
|
|
436
433
|
const sid = sessionId != null ? validateProxySessionId(String(sessionId)) : undefined;
|
|
@@ -448,8 +445,7 @@ class ProxyConfiguration extends core_1.ProxyConfiguration {
|
|
|
448
445
|
}
|
|
449
446
|
/**
|
|
450
447
|
* Returns a new ProxyInfo. When the resolved URL points at the platform
|
|
451
|
-
* gateway, `groups`, `countryCode` and `subdivisionCode` are attached
|
|
452
|
-
* (Apify parity).
|
|
448
|
+
* gateway, `groups`, `countryCode` and `subdivisionCode` are attached.
|
|
453
449
|
*/
|
|
454
450
|
async newProxyInfo(sessionId, options) {
|
|
455
451
|
const sid = sessionId != null ? validateProxySessionId(String(sessionId)) : undefined;
|
|
@@ -467,10 +463,10 @@ class ProxyConfiguration extends core_1.ProxyConfiguration {
|
|
|
467
463
|
return info;
|
|
468
464
|
}
|
|
469
465
|
/**
|
|
470
|
-
* Fetches the gateway status payload
|
|
466
|
+
* Fetches the gateway status payload by sending
|
|
471
467
|
* `GET {statusUrl}/?format=json` *through the platform gateway*, so the
|
|
472
468
|
* proxy handshake itself authenticates the run's credentials. Retries
|
|
473
|
-
* connection errors (
|
|
469
|
+
* connection errors (4s timeout, 2 attempts); returns
|
|
474
470
|
* undefined when the endpoint stays unreachable.
|
|
475
471
|
*/
|
|
476
472
|
async fetchProxyStatus() {
|
|
@@ -519,18 +515,18 @@ class ProxyConfiguration extends core_1.ProxyConfiguration {
|
|
|
519
515
|
}
|
|
520
516
|
}
|
|
521
517
|
catch {
|
|
522
|
-
// Retry connection errors
|
|
518
|
+
// Retry connection errors.
|
|
523
519
|
}
|
|
524
520
|
}
|
|
525
521
|
return undefined;
|
|
526
522
|
}
|
|
527
523
|
/**
|
|
528
524
|
* Verifies the proxy configuration. For platform proxy mode with an
|
|
529
|
-
* injected status URL, performs the
|
|
525
|
+
* injected status URL, performs the access check through the
|
|
530
526
|
* gateway (`GET {statusUrl}/?format=json`). When the gateway answers
|
|
531
527
|
* `connected: false` (e.g. an unknown proxy group), it throws with the
|
|
532
|
-
* gateway's `connectionError` on the platform and warns locally
|
|
533
|
-
*
|
|
528
|
+
* gateway's `connectionError` on the platform and warns locally;
|
|
529
|
+
* timeouts and HTTP errors only warn.
|
|
534
530
|
* BYOP mode only verifies that a test URL can be generated.
|
|
535
531
|
*/
|
|
536
532
|
async initialize(options = {}) {
|
|
@@ -538,7 +534,7 @@ class ProxyConfiguration extends core_1.ProxyConfiguration {
|
|
|
538
534
|
if (!checkAccess) {
|
|
539
535
|
return true;
|
|
540
536
|
}
|
|
541
|
-
//
|
|
537
|
+
// Access check: GET {statusUrl}/?format=json *through the
|
|
542
538
|
// platform gateway* so the proxy handshake itself authenticates the
|
|
543
539
|
// run's credentials.
|
|
544
540
|
if (checkAccess && this.platformEnv?.statusUrl) {
|
|
@@ -114,6 +114,8 @@ export declare abstract class BaseApiClient {
|
|
|
114
114
|
*/
|
|
115
115
|
private send;
|
|
116
116
|
protected request<T>(method: string, path: string, body?: any, contentType?: string, options?: ApiRequestOptions): Promise<T>;
|
|
117
|
+
/** HEAD request: true on 2xx, false on 404; other errors throw. */
|
|
118
|
+
protected exists(path: string): Promise<boolean>;
|
|
117
119
|
protected requestWithHeaders<T>(method: string, path: string, body?: any, contentType?: string): Promise<{
|
|
118
120
|
data: T;
|
|
119
121
|
contentType: string;
|
|
@@ -171,7 +173,6 @@ export declare class ApiKeyValueStoreClient extends BaseApiClient implements Key
|
|
|
171
173
|
entries(options?: KeyValueStoreClientListOptions): AsyncIterable<[string, unknown]> & Promise<[string, unknown][]>;
|
|
172
174
|
/**
|
|
173
175
|
* Iterates over all keys in the key-value store, calling the iteratee function for each key.
|
|
174
|
-
* Similar to Apify's KeyValueStore.forEachKey() method.
|
|
175
176
|
*/
|
|
176
177
|
forEachKey(iteratee: (key: string, index: number, info: {
|
|
177
178
|
size: number;
|
|
@@ -179,7 +180,6 @@ export declare class ApiKeyValueStoreClient extends BaseApiClient implements Key
|
|
|
179
180
|
/**
|
|
180
181
|
* Gets a value from the key-value store that is automatically saved when modified.
|
|
181
182
|
* If the key doesn't exist, returns the defaultValue and stores it for future use.
|
|
182
|
-
* Similar to Apify's KeyValueStore.getAutoSavedValue() method.
|
|
183
183
|
*
|
|
184
184
|
* @param key - The key of the record to get
|
|
185
185
|
* @param defaultValue - The default value to return and store if the key doesn't exist
|
|
@@ -189,7 +189,6 @@ export declare class ApiKeyValueStoreClient extends BaseApiClient implements Key
|
|
|
189
189
|
/**
|
|
190
190
|
* Returns a URL for the given key that may be used to publicly access the value
|
|
191
191
|
* in the remote key-value store.
|
|
192
|
-
* Similar to Apify's KeyValueStore.getPublicUrl() method.
|
|
193
192
|
*
|
|
194
193
|
* @param key - The key of the record to get the public URL for
|
|
195
194
|
* @returns A public URL for accessing the record
|
|
@@ -218,11 +217,11 @@ export declare class ApiRequestQueueClient extends BaseApiClient implements Requ
|
|
|
218
217
|
prolongRequestLock(id: string, options: ProlongRequestLockOptions): Promise<ProlongRequestLockResult>;
|
|
219
218
|
deleteRequestLock(id: string, options?: DeleteRequestLockOptions): Promise<void>;
|
|
220
219
|
/**
|
|
221
|
-
* List requests in the queue with optional filtering and pagination
|
|
220
|
+
* List requests in the queue with optional filtering and pagination.
|
|
222
221
|
*/
|
|
223
222
|
listRequests(options?: ListRequestsOptions): Promise<ListRequestsResult>;
|
|
224
223
|
/**
|
|
225
|
-
* Unlock all requests locked by this clientKey
|
|
224
|
+
* Unlock all requests locked by this clientKey.
|
|
226
225
|
*/
|
|
227
226
|
unlockRequests(options: {
|
|
228
227
|
clientKey?: string;
|
|
@@ -100,7 +100,7 @@ class AdaptiveRateLimiter {
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
/**
|
|
103
|
-
* Unwrap
|
|
103
|
+
* Unwrap v2 response envelopes ({ data: ... }).
|
|
104
104
|
*/
|
|
105
105
|
function unwrapData(value) {
|
|
106
106
|
if (Array.isArray(value))
|
|
@@ -545,6 +545,17 @@ class BaseApiClient {
|
|
|
545
545
|
return unwrapData(await response.json());
|
|
546
546
|
});
|
|
547
547
|
}
|
|
548
|
+
/** HEAD request: true on 2xx, false on 404; other errors throw. */
|
|
549
|
+
async exists(path) {
|
|
550
|
+
try {
|
|
551
|
+
return await this.send('HEAD', path, undefined, undefined, undefined, async () => true);
|
|
552
|
+
}
|
|
553
|
+
catch (err) {
|
|
554
|
+
if (isApiStorageClientError(err) && err.statusCode === 404)
|
|
555
|
+
return false;
|
|
556
|
+
throw err;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
548
559
|
async requestWithHeaders(method, path, body, contentType) {
|
|
549
560
|
return this.send(method, path, body, contentType, undefined, async (response) => {
|
|
550
561
|
if (response.status === 204) {
|
|
@@ -723,13 +734,7 @@ class ApiKeyValueStoreClient extends BaseApiClient {
|
|
|
723
734
|
await this.request('DELETE', `/key-value-stores/${this.id}`);
|
|
724
735
|
}
|
|
725
736
|
async recordExists(key) {
|
|
726
|
-
|
|
727
|
-
await this.getRecord(key);
|
|
728
|
-
return true;
|
|
729
|
-
}
|
|
730
|
-
catch {
|
|
731
|
-
return false;
|
|
732
|
-
}
|
|
737
|
+
return this.exists(`/key-value-stores/${this.id}/records/${key}`);
|
|
733
738
|
}
|
|
734
739
|
async getRecord(key, options) {
|
|
735
740
|
try {
|
|
@@ -754,7 +759,7 @@ class ApiKeyValueStoreClient extends BaseApiClient {
|
|
|
754
759
|
const { key, value, contentType } = record;
|
|
755
760
|
let body;
|
|
756
761
|
let ct;
|
|
757
|
-
// Handle Readable streams (e.g., fs.createReadStream)
|
|
762
|
+
// Handle Readable streams (e.g., fs.createReadStream)
|
|
758
763
|
// Streams are objects with a pipe method. We need to buffer them.
|
|
759
764
|
if (value && typeof value === 'object' && typeof value.pipe === 'function') {
|
|
760
765
|
const chunks = [];
|
|
@@ -765,7 +770,7 @@ class ApiKeyValueStoreClient extends BaseApiClient {
|
|
|
765
770
|
ct = 'application/octet-stream';
|
|
766
771
|
}
|
|
767
772
|
else if (value instanceof Uint8Array) {
|
|
768
|
-
// Raw bytes, as
|
|
773
|
+
// Raw bytes, stored as-is; the API returns them unchanged on GET.
|
|
769
774
|
body = value;
|
|
770
775
|
ct = 'application/octet-stream';
|
|
771
776
|
}
|
|
@@ -816,7 +821,6 @@ class ApiKeyValueStoreClient extends BaseApiClient {
|
|
|
816
821
|
}
|
|
817
822
|
/**
|
|
818
823
|
* Iterates over all keys in the key-value store, calling the iteratee function for each key.
|
|
819
|
-
* Similar to Apify's KeyValueStore.forEachKey() method.
|
|
820
824
|
*/
|
|
821
825
|
async forEachKey(iteratee, options) {
|
|
822
826
|
return (0, kvs_iteration_js_1.forEachKeyPaginated)((opts) => this.listKeys(opts), iteratee, options);
|
|
@@ -824,7 +828,6 @@ class ApiKeyValueStoreClient extends BaseApiClient {
|
|
|
824
828
|
/**
|
|
825
829
|
* Gets a value from the key-value store that is automatically saved when modified.
|
|
826
830
|
* If the key doesn't exist, returns the defaultValue and stores it for future use.
|
|
827
|
-
* Similar to Apify's KeyValueStore.getAutoSavedValue() method.
|
|
828
831
|
*
|
|
829
832
|
* @param key - The key of the record to get
|
|
830
833
|
* @param defaultValue - The default value to return and store if the key doesn't exist
|
|
@@ -842,7 +845,6 @@ class ApiKeyValueStoreClient extends BaseApiClient {
|
|
|
842
845
|
/**
|
|
843
846
|
* Returns a URL for the given key that may be used to publicly access the value
|
|
844
847
|
* in the remote key-value store.
|
|
845
|
-
* Similar to Apify's KeyValueStore.getPublicUrl() method.
|
|
846
848
|
*
|
|
847
849
|
* @param key - The key of the record to get the public URL for
|
|
848
850
|
* @returns A public URL for accessing the record
|
|
@@ -965,7 +967,7 @@ class ApiRequestQueueClient extends BaseApiClient {
|
|
|
965
967
|
await this.request('DELETE', `/request-queues/${this.id}/requests/${id}/lock${query}`);
|
|
966
968
|
}
|
|
967
969
|
/**
|
|
968
|
-
* List requests in the queue with optional filtering and pagination
|
|
970
|
+
* List requests in the queue with optional filtering and pagination.
|
|
969
971
|
*/
|
|
970
972
|
async listRequests(options) {
|
|
971
973
|
const params = new URLSearchParams();
|
|
@@ -984,7 +986,7 @@ class ApiRequestQueueClient extends BaseApiClient {
|
|
|
984
986
|
return this.request('GET', `/request-queues/${this.id}/requests${query ? `?${query}` : ''}`);
|
|
985
987
|
}
|
|
986
988
|
/**
|
|
987
|
-
* Unlock all requests locked by this clientKey
|
|
989
|
+
* Unlock all requests locked by this clientKey.
|
|
988
990
|
*/
|
|
989
991
|
async unlockRequests(options) {
|
|
990
992
|
const query = buildRqQueryParams(options.clientKey ?? this.clientKey, {
|
|
@@ -15,7 +15,7 @@ export interface BillingClient {
|
|
|
15
15
|
* - Request queues (requests stored)
|
|
16
16
|
* - Logs (log size)
|
|
17
17
|
*
|
|
18
|
-
* This data is used to calculate storage-hour charges
|
|
18
|
+
* This data is used to calculate storage-hour charges.
|
|
19
19
|
*/
|
|
20
20
|
export declare class BillingTracker {
|
|
21
21
|
private client;
|
|
@@ -10,7 +10,7 @@ exports.BillingTracker = void 0;
|
|
|
10
10
|
* - Request queues (requests stored)
|
|
11
11
|
* - Logs (log size)
|
|
12
12
|
*
|
|
13
|
-
* This data is used to calculate storage-hour charges
|
|
13
|
+
* This data is used to calculate storage-hour charges.
|
|
14
14
|
*/
|
|
15
15
|
class BillingTracker {
|
|
16
16
|
client;
|
|
@@ -36,13 +36,11 @@ export declare class DragonflyKeyValueStoreClient implements KeyValueStoreClient
|
|
|
36
36
|
entries(options?: KeyValueStoreClientListOptions): AsyncIterable<[string, unknown]> & Promise<[string, unknown][]>;
|
|
37
37
|
/**
|
|
38
38
|
* Iterates over all keys in the key-value store, calling the iteratee function for each key.
|
|
39
|
-
* Similar to Apify's KeyValueStore.forEachKey() method.
|
|
40
39
|
*/
|
|
41
40
|
forEachKey(iteratee: KeyConsumer, options?: ForEachKeyOptions): Promise<void>;
|
|
42
41
|
/**
|
|
43
42
|
* Gets a value from the key-value store that is automatically saved when modified.
|
|
44
43
|
* If the key doesn't exist, returns the defaultValue and stores it for future use.
|
|
45
|
-
* Similar to Apify's KeyValueStore.getAutoSavedValue() method.
|
|
46
44
|
*
|
|
47
45
|
* @param key - The key of the record to get
|
|
48
46
|
* @param defaultValue - The default value to return and store if the key doesn't exist
|
|
@@ -52,7 +50,6 @@ export declare class DragonflyKeyValueStoreClient implements KeyValueStoreClient
|
|
|
52
50
|
/**
|
|
53
51
|
* Returns a URL for the given key that may be used to publicly access the value
|
|
54
52
|
* in the remote key-value store.
|
|
55
|
-
* Similar to Apify's KeyValueStore.getPublicUrl() method.
|
|
56
53
|
*
|
|
57
54
|
* @param key - The key of the record to get the public URL for
|
|
58
55
|
* @returns A public URL for accessing the record
|
|
@@ -134,7 +134,7 @@ class DragonflyKeyValueStoreClient {
|
|
|
134
134
|
const { key, value, contentType } = record;
|
|
135
135
|
let stored;
|
|
136
136
|
let billingValue = value; // Use for billing estimation (may differ from original if stream)
|
|
137
|
-
// Handle Readable streams (e.g., fs.createReadStream)
|
|
137
|
+
// Handle Readable streams (e.g., fs.createReadStream)
|
|
138
138
|
// Streams are objects with a pipe method. We need to buffer them.
|
|
139
139
|
if (value && typeof value === 'object' && typeof value.pipe === 'function') {
|
|
140
140
|
const chunks = [];
|
|
@@ -266,7 +266,6 @@ class DragonflyKeyValueStoreClient {
|
|
|
266
266
|
}
|
|
267
267
|
/**
|
|
268
268
|
* Iterates over all keys in the key-value store, calling the iteratee function for each key.
|
|
269
|
-
* Similar to Apify's KeyValueStore.forEachKey() method.
|
|
270
269
|
*/
|
|
271
270
|
async forEachKey(iteratee, options) {
|
|
272
271
|
return (0, kvs_iteration_js_1.forEachKeyPaginated)((opts) => this._listKeys(opts), iteratee, options);
|
|
@@ -274,7 +273,6 @@ class DragonflyKeyValueStoreClient {
|
|
|
274
273
|
/**
|
|
275
274
|
* Gets a value from the key-value store that is automatically saved when modified.
|
|
276
275
|
* If the key doesn't exist, returns the defaultValue and stores it for future use.
|
|
277
|
-
* Similar to Apify's KeyValueStore.getAutoSavedValue() method.
|
|
278
276
|
*
|
|
279
277
|
* @param key - The key of the record to get
|
|
280
278
|
* @param defaultValue - The default value to return and store if the key doesn't exist
|
|
@@ -292,14 +290,13 @@ class DragonflyKeyValueStoreClient {
|
|
|
292
290
|
/**
|
|
293
291
|
* Returns a URL for the given key that may be used to publicly access the value
|
|
294
292
|
* in the remote key-value store.
|
|
295
|
-
* Similar to Apify's KeyValueStore.getPublicUrl() method.
|
|
296
293
|
*
|
|
297
294
|
* @param key - The key of the record to get the public URL for
|
|
298
295
|
* @returns A public URL for accessing the record
|
|
299
296
|
*/
|
|
300
297
|
getPublicUrl(key) {
|
|
301
298
|
const config = configuration_js_1.Configuration.getGlobalConfig();
|
|
302
|
-
const baseUrl = config.get('apiPublicBaseUrl') || config.get('apiBaseUrl') || 'https://api
|
|
299
|
+
const baseUrl = config.get('apiPublicBaseUrl') || config.get('apiBaseUrl') || 'https://api.scrape.ly';
|
|
303
300
|
return `${baseUrl}/v2/key-value-stores/${this.id}/records/${key}`;
|
|
304
301
|
}
|
|
305
302
|
}
|
|
@@ -16,10 +16,14 @@ class DragonflyKeyValueStoreCollectionClient {
|
|
|
16
16
|
}
|
|
17
17
|
const id = (0, utils_js_1.generateId)();
|
|
18
18
|
const now = (0, utils_js_1.nowISO)();
|
|
19
|
+
// Derive ownership from the active billing context (as the request-queue
|
|
20
|
+
// client does); the server authorizes against this persisted field.
|
|
21
|
+
// The legacy '1' placeholder applies only when no context is present.
|
|
22
|
+
const userId = this.client.billingContext?.userId ?? '1';
|
|
19
23
|
const data = {
|
|
20
24
|
id,
|
|
21
25
|
name,
|
|
22
|
-
userId
|
|
26
|
+
userId,
|
|
23
27
|
createdAt: now,
|
|
24
28
|
modifiedAt: now,
|
|
25
29
|
accessedAt: now,
|
|
@@ -33,7 +37,7 @@ class DragonflyKeyValueStoreCollectionClient {
|
|
|
33
37
|
await this.client.redis.hset(infoKey, {
|
|
34
38
|
id,
|
|
35
39
|
name: name ?? '',
|
|
36
|
-
userId
|
|
40
|
+
userId,
|
|
37
41
|
createdAt: now.toISOString(),
|
|
38
42
|
modifiedAt: now.toISOString(),
|
|
39
43
|
accessedAt: now.toISOString(),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { KeyValueStoreClientListData, KeyValueStoreClientListOptions, KeyValueStoreItemData } from '@crawlee/types';
|
|
2
2
|
export type ListKeysFn = (options?: KeyValueStoreClientListOptions) => Promise<KeyValueStoreClientListData>;
|
|
3
3
|
/**
|
|
4
|
-
* Filter keys by
|
|
4
|
+
* Filter keys by collection config from a KVS schema.
|
|
5
5
|
*/
|
|
6
6
|
export declare function filterKeysByCollection(allKeys: string[], schemaJson: string | undefined, collection: string): string[];
|
|
7
7
|
export declare function paginateKeyItems(listKeys: ListKeysFn, options?: KeyValueStoreClientListOptions): AsyncGenerator<KeyValueStoreItemData>;
|
|
@@ -7,7 +7,7 @@ exports.makePaginatingKeysIterable = makePaginatingKeysIterable;
|
|
|
7
7
|
exports.makePaginatingValuesIterable = makePaginatingValuesIterable;
|
|
8
8
|
exports.makePaginatingEntriesIterable = makePaginatingEntriesIterable;
|
|
9
9
|
/**
|
|
10
|
-
* Filter keys by
|
|
10
|
+
* Filter keys by collection config from a KVS schema.
|
|
11
11
|
*/
|
|
12
12
|
function filterKeysByCollection(allKeys, schemaJson, collection) {
|
|
13
13
|
if (!schemaJson) {
|
|
@@ -44,7 +44,7 @@ export declare class DragonflyRequestQueueClient implements RequestQueueClient {
|
|
|
44
44
|
*/
|
|
45
45
|
private trackStorageBytes;
|
|
46
46
|
/**
|
|
47
|
-
* Track clientKey access for hadMultipleClients
|
|
47
|
+
* Track clientKey access for hadMultipleClients.
|
|
48
48
|
* Returns the current hadMultipleClients flag after update.
|
|
49
49
|
*/
|
|
50
50
|
private trackClientKey;
|
|
@@ -66,7 +66,7 @@ class DragonflyRequestQueueClient {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
|
-
* Track clientKey access for hadMultipleClients
|
|
69
|
+
* Track clientKey access for hadMultipleClients.
|
|
70
70
|
* Returns the current hadMultipleClients flag after update.
|
|
71
71
|
*/
|
|
72
72
|
async trackClientKey(clientKey) {
|
|
@@ -333,11 +333,11 @@ class DragonflyRequestQueueClient {
|
|
|
333
333
|
async listRequests(options) {
|
|
334
334
|
const limit = Math.min(options?.limit ?? 100, 10000);
|
|
335
335
|
const now = Date.now();
|
|
336
|
-
// Validate that filter is not used with deprecated exclusiveStartId
|
|
336
|
+
// Validate that filter is not used with deprecated exclusiveStartId
|
|
337
337
|
if (options?.filter && options.filter.length > 0 && options?.exclusiveStartId) {
|
|
338
338
|
throw new Error('filter parameter is not compatible with deprecated exclusiveStartId parameter');
|
|
339
339
|
}
|
|
340
|
-
// Track clientKey for hadMultipleClients
|
|
340
|
+
// Track clientKey for hadMultipleClients
|
|
341
341
|
if (options?.clientKey) {
|
|
342
342
|
const existingClientKey = await this.client.redis.hget(this.infoKey, 'clientKey');
|
|
343
343
|
if (existingClientKey && existingClientKey !== options.clientKey) {
|
|
@@ -348,7 +348,7 @@ class DragonflyRequestQueueClient {
|
|
|
348
348
|
}
|
|
349
349
|
}
|
|
350
350
|
else {
|
|
351
|
-
// No clientKey provided - treat as new client
|
|
351
|
+
// No clientKey provided - treat as new client
|
|
352
352
|
const existingClientKey = await this.client.redis.hget(this.infoKey, 'clientKey');
|
|
353
353
|
if (existingClientKey) {
|
|
354
354
|
await this.client.redis.hset(this.infoKey, 'hadMultipleClients', '1');
|
|
@@ -456,7 +456,7 @@ class DragonflyRequestQueueClient {
|
|
|
456
456
|
lockSecs: options.lockSecs,
|
|
457
457
|
items,
|
|
458
458
|
// From the atomic lock script: active locks held by ANY client
|
|
459
|
-
// (including this call's own)
|
|
459
|
+
// (including this call's own).
|
|
460
460
|
queueHasLockedRequests: activeLockCount > 0,
|
|
461
461
|
};
|
|
462
462
|
}
|