apify 4.0.0-beta.18 → 4.0.0-beta.20
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/actor.d.ts +28 -4
- package/dist/actor.js +45 -45
- package/dist/apify_dataset_backend.d.ts +18 -0
- package/dist/apify_dataset_backend.js +33 -0
- package/dist/apify_key_value_store_backend.d.ts +23 -0
- package/dist/apify_key_value_store_backend.js +54 -0
- package/dist/apify_request_queue_backend.d.ts +78 -0
- package/dist/apify_request_queue_backend.js +114 -0
- package/dist/apify_request_queue_shared_backend.d.ts +44 -0
- package/dist/apify_request_queue_shared_backend.js +211 -0
- package/dist/apify_request_queue_single_backend.d.ts +52 -0
- package/dist/apify_request_queue_single_backend.js +256 -0
- package/dist/apify_storage_backend.d.ts +109 -0
- package/dist/apify_storage_backend.js +249 -0
- package/dist/configuration.d.ts +6 -4
- package/dist/configuration.js +4 -4
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1 -1
- package/dist/input-schemas.d.ts +1 -1
- package/dist/key_value_store.js +8 -9
- package/dist/platform_event_manager.d.ts +2 -2
- package/dist/platform_event_manager.js +5 -5
- package/dist/proxy_configuration.d.ts +24 -7
- package/dist/proxy_configuration.js +66 -35
- package/dist/storage.d.ts +3 -3
- package/dist/storage.js +4 -4
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +11 -0
- package/package.json +7 -8
- package/dist/apify_storage_client.d.ts +0 -66
- package/dist/apify_storage_client.js +0 -200
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { once } from 'node:events';
|
|
2
|
+
import { request as httpRequest } from 'node:http';
|
|
3
|
+
import { json } from 'node:stream/consumers';
|
|
1
4
|
import { ProxyConfiguration as CoreProxyConfiguration } from '@crawlee/core';
|
|
2
|
-
import { fetch, ProxyAgent } from 'undici';
|
|
3
5
|
import { z } from 'zod';
|
|
4
6
|
import { APIFY_ENV_VARS, APIFY_PROXY_VALUE_REGEX } from '@apify/consts';
|
|
7
|
+
import defaultLog from '@apify/log';
|
|
5
8
|
import { cryptoRandomObjectId } from '@apify/utilities';
|
|
6
9
|
import { Actor } from './actor.js';
|
|
7
10
|
import { Configuration } from './configuration.js';
|
|
@@ -49,7 +52,7 @@ const SESSION_ID_LENGTH = 12;
|
|
|
49
52
|
* @category Scaling
|
|
50
53
|
*/
|
|
51
54
|
export class ProxyConfiguration extends CoreProxyConfiguration {
|
|
52
|
-
|
|
55
|
+
configuration;
|
|
53
56
|
groups;
|
|
54
57
|
countryCode;
|
|
55
58
|
subdivisionCode;
|
|
@@ -57,17 +60,18 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
|
|
|
57
60
|
hostname;
|
|
58
61
|
port;
|
|
59
62
|
usesApifyProxy;
|
|
63
|
+
log = defaultLog.child({ prefix: 'ProxyConfiguration' });
|
|
60
64
|
/**
|
|
61
65
|
* @internal
|
|
62
66
|
*/
|
|
63
|
-
constructor(options = {},
|
|
67
|
+
constructor(options = {}, configuration = Configuration.getGlobalConfiguration()) {
|
|
64
68
|
const { proxyUrls, newUrlFunction, ...rest } = options;
|
|
65
69
|
super({
|
|
66
70
|
proxyUrls,
|
|
67
71
|
newUrlFunction,
|
|
68
72
|
['validateRequired']: false,
|
|
69
73
|
});
|
|
70
|
-
this.
|
|
74
|
+
this.configuration = configuration;
|
|
71
75
|
validate(z
|
|
72
76
|
.object({
|
|
73
77
|
groups: z.array(z.string().regex(APIFY_PROXY_VALUE_REGEX)).optional(),
|
|
@@ -79,12 +83,12 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
|
|
|
79
83
|
password: z.string().optional(),
|
|
80
84
|
})
|
|
81
85
|
.strict(), rest);
|
|
82
|
-
const { groups = [], apifyProxyGroups = [], countryCode, apifyProxyCountry, subdivisionCode, apifyProxySubdivision, password =
|
|
86
|
+
const { groups = [], apifyProxyGroups = [], countryCode, apifyProxyCountry, subdivisionCode, apifyProxySubdivision, password = configuration.proxyPassword, } = options;
|
|
83
87
|
const groupsToUse = groups.length ? groups : apifyProxyGroups;
|
|
84
88
|
const countryCodeToUse = countryCode || apifyProxyCountry;
|
|
85
89
|
const subdivisionCodeToUse = subdivisionCode || apifyProxySubdivision;
|
|
86
|
-
const hostname =
|
|
87
|
-
const port =
|
|
90
|
+
const hostname = configuration.proxyHostname;
|
|
91
|
+
const port = configuration.proxyPort;
|
|
88
92
|
// The Apify Proxy subdivision is expressed as part of the country
|
|
89
93
|
// username parameter (`country-US_CA`), so a country is required.
|
|
90
94
|
if (subdivisionCodeToUse && !countryCodeToUse) {
|
|
@@ -102,7 +106,7 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
|
|
|
102
106
|
this.password = password;
|
|
103
107
|
this.hostname = hostname;
|
|
104
108
|
this.port = port;
|
|
105
|
-
this.usesApifyProxy = !
|
|
109
|
+
this.usesApifyProxy = !proxyUrls && !newUrlFunction;
|
|
106
110
|
if (proxyUrls && proxyUrls.some((url) => url?.includes('apify.com'))) {
|
|
107
111
|
this.log.warning('Some Apify proxy features may work incorrectly. Please consider setting up Apify properties instead of `proxyUrls`.\n' +
|
|
108
112
|
'See https://docs.apify.com/sdk/js/docs/concepts/proxy-management#apify-proxy-configuration');
|
|
@@ -172,7 +176,7 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
|
|
|
172
176
|
* `proxyUrls`, the URLs are rotated round-robin.
|
|
173
177
|
*/
|
|
174
178
|
async newUrl(options) {
|
|
175
|
-
if (this.
|
|
179
|
+
if (!this.usesApifyProxy) {
|
|
176
180
|
return super.newUrl(options);
|
|
177
181
|
}
|
|
178
182
|
return this.composeDefaultUrl(cryptoRandomObjectId(SESSION_ID_LENGTH));
|
|
@@ -208,7 +212,7 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
|
|
|
208
212
|
*/
|
|
209
213
|
// TODO: Make this private
|
|
210
214
|
async _setPasswordIfToken() {
|
|
211
|
-
const { token } = this.
|
|
215
|
+
const { token } = this.configuration;
|
|
212
216
|
if (!token)
|
|
213
217
|
return;
|
|
214
218
|
try {
|
|
@@ -239,6 +243,8 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
|
|
|
239
243
|
return true;
|
|
240
244
|
}
|
|
241
245
|
const { connected, connectionError, isManInTheMiddle } = status;
|
|
246
|
+
// Declared `readonly false` on the base class; the status check is the one place that
|
|
247
|
+
// learns the actual value, so bypass the readonly marker.
|
|
242
248
|
this.isManInTheMiddle = isManInTheMiddle;
|
|
243
249
|
if (connected) {
|
|
244
250
|
return true;
|
|
@@ -256,38 +262,56 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
|
|
|
256
262
|
* Apify Proxy can be down for a second or a minute, but this should not crash processes.
|
|
257
263
|
*/
|
|
258
264
|
async _fetchStatus() {
|
|
259
|
-
const { proxyStatusUrl } = this.
|
|
260
|
-
const
|
|
261
|
-
// The status endpoint (`proxy.apify.com`) is requested *through* the proxy
|
|
262
|
-
// so it can report on this exact connection (auth + man-in-the-middle).
|
|
263
|
-
// `undici`'s `fetch` + `ProxyAgent` come from the same package, so the
|
|
264
|
-
// dispatcher is recognized (Node's global `fetch` uses a separate internal
|
|
265
|
-
// copy of undici that would reject this dispatcher instance).
|
|
265
|
+
const { proxyStatusUrl } = this.configuration;
|
|
266
|
+
const statusUrl = `${proxyStatusUrl}/?format=json`;
|
|
266
267
|
const proxyUrl = await this.newUrl();
|
|
267
268
|
// Without a proxy URL we can't perform the (proxied) status check.
|
|
268
269
|
if (!proxyUrl)
|
|
269
270
|
return undefined;
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
signal: AbortSignal.timeout(CHECK_ACCESS_REQUEST_TIMEOUT_MILLIS),
|
|
277
|
-
});
|
|
278
|
-
if (!response.ok)
|
|
279
|
-
continue;
|
|
280
|
-
return (await response.json());
|
|
281
|
-
}
|
|
282
|
-
catch {
|
|
283
|
-
// retry connection errors
|
|
284
|
-
}
|
|
271
|
+
for (let attempt = 1; attempt <= CHECK_ACCESS_MAX_ATTEMPTS; attempt++) {
|
|
272
|
+
try {
|
|
273
|
+
return await this._requestStatus(statusUrl, proxyUrl);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
// retry connection errors
|
|
285
277
|
}
|
|
286
|
-
return undefined;
|
|
287
278
|
}
|
|
288
|
-
|
|
289
|
-
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Fetches the Apify Proxy status endpoint once, *through* the proxy, so the
|
|
283
|
+
* response reports on this exact connection (auth + man-in-the-middle).
|
|
284
|
+
*
|
|
285
|
+
* Uses a native `node:http` forward-proxy request — an absolute request URL
|
|
286
|
+
* plus a `Proxy-Authorization` header — so no proxy-agent dependency is
|
|
287
|
+
* needed. The status endpoint (`http://proxy.apify.com`) is plain HTTP.
|
|
288
|
+
*/
|
|
289
|
+
async _requestStatus(statusUrl, proxyUrl) {
|
|
290
|
+
const target = new URL(statusUrl);
|
|
291
|
+
const proxy = new URL(proxyUrl);
|
|
292
|
+
const headers = { host: target.host };
|
|
293
|
+
if (proxy.username) {
|
|
294
|
+
const credentials = `${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`;
|
|
295
|
+
headers['proxy-authorization'] = `Basic ${Buffer.from(credentials).toString('base64')}`;
|
|
296
|
+
}
|
|
297
|
+
const request = httpRequest({
|
|
298
|
+
host: proxy.hostname,
|
|
299
|
+
port: proxy.port,
|
|
300
|
+
// Absolute-form request URI tells the proxy to forward the request.
|
|
301
|
+
path: target.href,
|
|
302
|
+
headers,
|
|
303
|
+
signal: AbortSignal.timeout(CHECK_ACCESS_REQUEST_TIMEOUT_MILLIS),
|
|
304
|
+
});
|
|
305
|
+
request.end();
|
|
306
|
+
// `once` rejects if the request emits `error` first (connection refused,
|
|
307
|
+
// timeout/abort), so failures propagate to the retry loop in `_fetchStatus`.
|
|
308
|
+
const [response] = (await once(request, 'response'));
|
|
309
|
+
const statusCode = response.statusCode ?? 0;
|
|
310
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
311
|
+
response.resume(); // drain so the socket can be freed
|
|
312
|
+
throw new Error(`Apify Proxy status check responded with status code ${statusCode}.`);
|
|
290
313
|
}
|
|
314
|
+
return (await json(response));
|
|
291
315
|
}
|
|
292
316
|
/**
|
|
293
317
|
* Throws cannot combine custom proxies with Apify Proxy
|
|
@@ -299,4 +323,11 @@ export class ProxyConfiguration extends CoreProxyConfiguration {
|
|
|
299
323
|
'"options.groups", "options.apifyProxyGroups", "options.countryCode", "options.apifyProxyCountry", ' +
|
|
300
324
|
'"options.subdivisionCode" or "options.apifyProxySubdivision".');
|
|
301
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* Throws cannot combine custom proxies with custom generating function
|
|
328
|
+
* @internal
|
|
329
|
+
*/
|
|
330
|
+
_throwCannotCombineCustomMethods() {
|
|
331
|
+
throw new Error('Cannot combine custom proxies "options.proxyUrls" with custom generating function "options.newUrlFunction".');
|
|
332
|
+
}
|
|
302
333
|
}
|
package/dist/storage.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
1
|
+
import type { IStorage, StorageOpenOptions } from '@crawlee/core';
|
|
2
|
+
import type { Constructor, StorageBackend } from '@crawlee/types';
|
|
3
3
|
import type { Configuration } from './configuration.js';
|
|
4
4
|
export interface OpenStorageOptions {
|
|
5
5
|
/**
|
|
@@ -47,7 +47,7 @@ export type StorageIdentifier = string | StorageAlias | StorageId | StorageName;
|
|
|
47
47
|
export type StorageIdentifierWithoutAlias = string | StorageId | StorageName;
|
|
48
48
|
export interface OpenStorageContext {
|
|
49
49
|
config: Configuration;
|
|
50
|
-
|
|
50
|
+
backend?: StorageBackend;
|
|
51
51
|
purgedStorageAliases: Set<string>;
|
|
52
52
|
}
|
|
53
53
|
/**
|
package/dist/storage.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ApifyStorageBackend } from './apify_storage_backend.js';
|
|
2
2
|
const STORAGE_TYPE_KEYS = {
|
|
3
3
|
Dataset: 'datasets',
|
|
4
4
|
KeyValueStore: 'keyValueStores',
|
|
@@ -57,7 +57,7 @@ function resolveStorageIdentifier(storageType, identifier, config) {
|
|
|
57
57
|
*/
|
|
58
58
|
export async function openStorage(storageClass, identifier, context) {
|
|
59
59
|
const isAlias = identifier !== null && identifier !== undefined && typeof identifier === 'object' && 'alias' in identifier;
|
|
60
|
-
if (isAlias && !context.config.isAtHome && context.
|
|
60
|
+
if (isAlias && !context.config.isAtHome && context.backend instanceof ApifyStorageBackend) {
|
|
61
61
|
throw new Error('The `alias` option is not allowed for Apify-based storages running outside of Apify');
|
|
62
62
|
}
|
|
63
63
|
const resolvedIdOrName = resolveStorageIdentifier(storageClass.name, identifier, context.config);
|
|
@@ -69,11 +69,11 @@ export async function openStorage(storageClass, identifier, context) {
|
|
|
69
69
|
!context.purgedStorageAliases.has(identifier.alias)) {
|
|
70
70
|
context.purgedStorageAliases.add(identifier.alias);
|
|
71
71
|
const existingStorage = await storageClass.open(resolvedIdOrName ?? null, {
|
|
72
|
-
|
|
72
|
+
storageBackend: context.backend,
|
|
73
73
|
});
|
|
74
74
|
await existingStorage.drop();
|
|
75
75
|
}
|
|
76
76
|
return storageClass.open(resolvedIdOrName ?? null, {
|
|
77
|
-
|
|
77
|
+
storageBackend: context.backend,
|
|
78
78
|
});
|
|
79
79
|
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -5,6 +5,11 @@ import type { z } from 'zod';
|
|
|
5
5
|
* @internal
|
|
6
6
|
*/
|
|
7
7
|
export declare function isNonEmptyObject(value: unknown): value is Record<string, unknown>;
|
|
8
|
+
/**
|
|
9
|
+
* Converts a `SNAKE_CASE` string to `camelCase` (previously provided by `@crawlee/utils`).
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare function snakeCaseToCamelCase(snakeCaseStr: string): string;
|
|
8
13
|
/**
|
|
9
14
|
* Error thrown when an argument fails validation (e.g. by `Actor.addWebhook()`
|
|
10
15
|
* or the `ProxyConfiguration` constructor).
|
package/dist/utils.js
CHANGED
|
@@ -20,6 +20,17 @@ const require = createRequire(import.meta.url);
|
|
|
20
20
|
export function isNonEmptyObject(value) {
|
|
21
21
|
return typeof value === 'object' && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Converts a `SNAKE_CASE` string to `camelCase` (previously provided by `@crawlee/utils`).
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
export function snakeCaseToCamelCase(snakeCaseStr) {
|
|
28
|
+
return snakeCaseStr
|
|
29
|
+
.toLowerCase()
|
|
30
|
+
.split('_')
|
|
31
|
+
.map((part, index) => (index > 0 ? part.charAt(0).toUpperCase() + part.slice(1) : part))
|
|
32
|
+
.join('');
|
|
33
|
+
}
|
|
23
34
|
/** Formats a zod issue path like `groups[0]` or `countryCode`. */
|
|
24
35
|
function formatIssuePath(path) {
|
|
25
36
|
let out = '';
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apify",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.20",
|
|
4
4
|
"description": "The scalable web crawling and scraping library for JavaScript/Node.js. Enables development of data extraction and web automation jobs (not only) with headless Chrome and Puppeteer.",
|
|
5
5
|
"engines": {
|
|
6
|
-
"node": ">=22.
|
|
6
|
+
"node": ">=22.0.0"
|
|
7
7
|
},
|
|
8
8
|
"type": "module",
|
|
9
9
|
"main": "./dist/index.js",
|
|
@@ -54,17 +54,17 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@apify/consts": "^2.51.0",
|
|
57
|
+
"@apify/datastructures": "^2.0.3",
|
|
57
58
|
"@apify/input_secrets": "^1.2.0",
|
|
58
59
|
"@apify/log": "^2.4.3",
|
|
59
60
|
"@apify/timeout": "^0.3.0",
|
|
60
61
|
"@apify/utilities": "^2.13.0",
|
|
61
|
-
"@crawlee/core": "^4.0.0-beta.
|
|
62
|
-
"@crawlee/types": "^4.0.0-beta.
|
|
63
|
-
"@crawlee/utils": "^4.0.0-beta.
|
|
62
|
+
"@crawlee/core": "^4.0.0-beta.105",
|
|
63
|
+
"@crawlee/types": "^4.0.0-beta.105",
|
|
64
|
+
"@crawlee/utils": "^4.0.0-beta.105",
|
|
64
65
|
"apify-client": "^2.23.4",
|
|
65
66
|
"semver": "^7.5.4",
|
|
66
67
|
"tslib": "^2.6.2",
|
|
67
|
-
"undici": "^8.0.0",
|
|
68
68
|
"ws": "^8.18.0",
|
|
69
69
|
"zod": "^4.0.0"
|
|
70
70
|
},
|
|
@@ -72,7 +72,6 @@
|
|
|
72
72
|
"@apify/oxlint-config": "^0.2.5",
|
|
73
73
|
"@apify/tsconfig": "^0.1.2",
|
|
74
74
|
"@commitlint/config-conventional": "^21.0.0",
|
|
75
|
-
"@crawlee/memory-storage": "^4.0.0-beta.61",
|
|
76
75
|
"@playwright/browser-chromium": "^1.60.0",
|
|
77
76
|
"@types/content-type": "^1.1.8",
|
|
78
77
|
"@types/node": "^24.0.0",
|
|
@@ -80,7 +79,7 @@
|
|
|
80
79
|
"@types/tough-cookie": "^4.0.5",
|
|
81
80
|
"@types/ws": "^8.5.12",
|
|
82
81
|
"commitlint": "^21.0.0",
|
|
83
|
-
"crawlee": "^4.0.0-beta.
|
|
82
|
+
"crawlee": "^4.0.0-beta.105",
|
|
84
83
|
"globby": "^16.0.0",
|
|
85
84
|
"husky": "^9.1.7",
|
|
86
85
|
"lint-staged": "^17.0.0",
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
-
import type { CreateDatasetClientOptions, CreateKeyValueStoreClientOptions, CreateRequestQueueClientOptions, DatasetClient, KeyValueStoreClient, RequestQueueClient, StorageClient } from '@crawlee/types';
|
|
3
|
-
import type { ApifyClient } from 'apify-client';
|
|
4
|
-
import { type ChargeResult, type ChargingManager } from './charging.js';
|
|
5
|
-
import type { Configuration } from './configuration.js';
|
|
6
|
-
type StorageType = 'Dataset' | 'KeyValueStore' | 'RequestQueue';
|
|
7
|
-
/** Marks a dataset client whose `pushItems` charges for pay-per-event. @internal */
|
|
8
|
-
export declare const USES_PUSH_DATA_INTERCEPTION: unique symbol;
|
|
9
|
-
/**
|
|
10
|
-
* Context of a single `Actor.pushData()` call, shared with the intercepted
|
|
11
|
-
* `pushItems()` calls so they can (1) know which event to charge and
|
|
12
|
-
* (2) aggregate the {@link ChargeResult} across the multiple `pushItems()`
|
|
13
|
-
* calls a single `pushData()` may trigger (Crawlee batches large pushes).
|
|
14
|
-
*/
|
|
15
|
-
export interface PpeAwarePushDataContext {
|
|
16
|
-
eventName: string | undefined;
|
|
17
|
-
chargeResult?: ChargeResult;
|
|
18
|
-
}
|
|
19
|
-
export declare const pushDataChargingContext: AsyncLocalStorage<PpeAwarePushDataContext>;
|
|
20
|
-
/**
|
|
21
|
-
* Bridges `apify-client`'s synchronous resource accessors (`dataset(id)`,
|
|
22
|
-
* `keyValueStore(id)`, `requestQueue(id, options?)`) to crawlee v4's
|
|
23
|
-
* `StorageClient` interface (async factory methods accepting either an `id`
|
|
24
|
-
* or a `name`).
|
|
25
|
-
*
|
|
26
|
-
* For the run's default dataset it transparently swaps in a charging-aware
|
|
27
|
-
* dataset client (pay-per-event on `Actor.pushData()`), provided a charging
|
|
28
|
-
* manager is supplied and a default-dataset-item price is configured.
|
|
29
|
-
*
|
|
30
|
-
* `storageExists()` lets `Dataset.open(idOrName)` resolve a string to an id
|
|
31
|
-
* first (when one exists on the platform) and fall back to a name otherwise —
|
|
32
|
-
* otherwise crawlee's `resolveStorageIdentifier` treats every string as a name
|
|
33
|
-
* and the SDK would silently create a new storage named like the passed id.
|
|
34
|
-
*
|
|
35
|
-
* `Actor` wires this up automatically; construct it directly only to use Apify
|
|
36
|
-
* platform storage with crawlee's storage classes outside of `Actor` — e.g. to
|
|
37
|
-
* read another run's output with an explicit token:
|
|
38
|
-
*
|
|
39
|
-
* ```ts
|
|
40
|
-
* import { ApifyClient, ApifyStorageClient, Dataset } from 'apify';
|
|
41
|
-
*
|
|
42
|
-
* const client = new ApifyClient({ token });
|
|
43
|
-
* const dataset = await Dataset.open(datasetId, { storageClient: new ApifyStorageClient(client) });
|
|
44
|
-
* const { items } = await dataset.getData();
|
|
45
|
-
* ```
|
|
46
|
-
*/
|
|
47
|
-
export declare class ApifyStorageClient implements StorageClient {
|
|
48
|
-
private readonly client;
|
|
49
|
-
private readonly config?;
|
|
50
|
-
private readonly getChargingManager?;
|
|
51
|
-
constructor(client: ApifyClient, config?: Configuration | undefined, getChargingManager?: (() => ChargingManager) | undefined);
|
|
52
|
-
storageExists(id: string, type: StorageType): Promise<boolean>;
|
|
53
|
-
createDatasetClient(options?: CreateDatasetClientOptions): Promise<DatasetClient>;
|
|
54
|
-
createKeyValueStoreClient(options?: CreateKeyValueStoreClientOptions): Promise<KeyValueStoreClient>;
|
|
55
|
-
createRequestQueueClient(options?: CreateRequestQueueClientOptions): Promise<RequestQueueClient>;
|
|
56
|
-
/**
|
|
57
|
-
* Returns a charging-aware dataset client when `id` is the run's default
|
|
58
|
-
* dataset and a default-dataset-item price is configured; otherwise
|
|
59
|
-
* `undefined` (caller uses the plain client).
|
|
60
|
-
*/
|
|
61
|
-
private chargingDatasetClient;
|
|
62
|
-
private resolveId;
|
|
63
|
-
private resourceClient;
|
|
64
|
-
private collectionClient;
|
|
65
|
-
}
|
|
66
|
-
export {};
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
/* eslint-disable max-classes-per-file */
|
|
2
|
-
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
|
-
import { DatasetClient as ApifyDatasetClient } from 'apify-client';
|
|
4
|
-
import { DEFAULT_DATASET_ITEM_EVENT, mergeChargeResults, pushDataAndCharge, } from './charging.js';
|
|
5
|
-
const DEFAULT_ID_CONFIG_KEY = {
|
|
6
|
-
Dataset: 'defaultDatasetId',
|
|
7
|
-
KeyValueStore: 'defaultKeyValueStoreId',
|
|
8
|
-
RequestQueue: 'defaultRequestQueueId',
|
|
9
|
-
};
|
|
10
|
-
/** Marks a dataset client whose `pushItems` charges for pay-per-event. @internal */
|
|
11
|
-
export const USES_PUSH_DATA_INTERCEPTION = Symbol('apify:uses-push-data-interception');
|
|
12
|
-
export const pushDataChargingContext = new AsyncLocalStorage();
|
|
13
|
-
/**
|
|
14
|
-
* Default `DatasetClient` that charges for pushed items (pay-per-event). Used
|
|
15
|
-
* only for the run's default dataset when a `apify-default-dataset-item` price
|
|
16
|
-
* is configured; for everything else the plain `apify-client` dataset client is
|
|
17
|
-
* used.
|
|
18
|
-
*/
|
|
19
|
-
class PpeAwareDatasetClient extends ApifyDatasetClient {
|
|
20
|
-
getChargingManager;
|
|
21
|
-
constructor(options, getChargingManager) {
|
|
22
|
-
super(options);
|
|
23
|
-
this.getChargingManager = getChargingManager;
|
|
24
|
-
}
|
|
25
|
-
normalizeItems(items) {
|
|
26
|
-
if (typeof items === 'string') {
|
|
27
|
-
const parsed = JSON.parse(items);
|
|
28
|
-
return Array.isArray(parsed) ? parsed : [parsed];
|
|
29
|
-
}
|
|
30
|
-
if (Array.isArray(items)) {
|
|
31
|
-
return items.flatMap((item) => typeof item === 'string' ? JSON.parse(item) : item);
|
|
32
|
-
}
|
|
33
|
-
return [items];
|
|
34
|
-
}
|
|
35
|
-
async pushItems(items) {
|
|
36
|
-
const context = pushDataChargingContext.getStore();
|
|
37
|
-
// A single JSON string may encode multiple items (e.g. '[{...},{...}]'),
|
|
38
|
-
// which the charging logic would miscount — parse strings into arrays so
|
|
39
|
-
// each logical item is counted individually.
|
|
40
|
-
const normalizedItems = this.normalizeItems(items);
|
|
41
|
-
const result = await pushDataAndCharge({
|
|
42
|
-
chargingManager: this.getChargingManager(),
|
|
43
|
-
items: normalizedItems,
|
|
44
|
-
eventName: context?.eventName,
|
|
45
|
-
isDefaultDataset: true,
|
|
46
|
-
// stringify for faster validation in the Apify client
|
|
47
|
-
pushFn: async (limitedItems) => super.pushItems(JSON.stringify(limitedItems)),
|
|
48
|
-
});
|
|
49
|
-
if (!context)
|
|
50
|
-
return;
|
|
51
|
-
// One `Actor.pushData()` may map to several `pushItems()` calls — aggregate.
|
|
52
|
-
context.chargeResult =
|
|
53
|
-
context.chargeResult === undefined ? result : mergeChargeResults(context.chargeResult, result);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
// crawlee v4's `StorageClient` sub-client interfaces use different method names
|
|
57
|
-
// than `apify-client`'s resource clients (`getValue`/`getRecord`,
|
|
58
|
-
// `pushData`/`pushItems`, `getData`/`listItems`, `getMetadata`/`get`,
|
|
59
|
-
// `drop`/`delete`). `adapt` wraps a client in a name-remapping proxy: `renames`
|
|
60
|
-
// aliases the differing methods and `overrides` replaces the few whose return
|
|
61
|
-
// shape differs; everything else — identically-named methods and the
|
|
62
|
-
// pay-per-event marker symbol — passes straight through.
|
|
63
|
-
//
|
|
64
|
-
// `purge()` has no apify-client equivalent and isn't needed on the platform
|
|
65
|
-
// (a run's storages are already fresh), so it's a no-op.
|
|
66
|
-
const noPurge = { purge: async () => { } };
|
|
67
|
-
function adapt(client, renames, overrides = {}) {
|
|
68
|
-
return new Proxy(client, {
|
|
69
|
-
get(target, prop) {
|
|
70
|
-
if (typeof prop === 'string' && prop in overrides)
|
|
71
|
-
return overrides[prop];
|
|
72
|
-
const value = Reflect.get(target, (typeof prop === 'string' && renames[prop]) || prop, target);
|
|
73
|
-
return typeof value === 'function' ? value.bind(target) : value;
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Bridges `apify-client`'s synchronous resource accessors (`dataset(id)`,
|
|
79
|
-
* `keyValueStore(id)`, `requestQueue(id, options?)`) to crawlee v4's
|
|
80
|
-
* `StorageClient` interface (async factory methods accepting either an `id`
|
|
81
|
-
* or a `name`).
|
|
82
|
-
*
|
|
83
|
-
* For the run's default dataset it transparently swaps in a charging-aware
|
|
84
|
-
* dataset client (pay-per-event on `Actor.pushData()`), provided a charging
|
|
85
|
-
* manager is supplied and a default-dataset-item price is configured.
|
|
86
|
-
*
|
|
87
|
-
* `storageExists()` lets `Dataset.open(idOrName)` resolve a string to an id
|
|
88
|
-
* first (when one exists on the platform) and fall back to a name otherwise —
|
|
89
|
-
* otherwise crawlee's `resolveStorageIdentifier` treats every string as a name
|
|
90
|
-
* and the SDK would silently create a new storage named like the passed id.
|
|
91
|
-
*
|
|
92
|
-
* `Actor` wires this up automatically; construct it directly only to use Apify
|
|
93
|
-
* platform storage with crawlee's storage classes outside of `Actor` — e.g. to
|
|
94
|
-
* read another run's output with an explicit token:
|
|
95
|
-
*
|
|
96
|
-
* ```ts
|
|
97
|
-
* import { ApifyClient, ApifyStorageClient, Dataset } from 'apify';
|
|
98
|
-
*
|
|
99
|
-
* const client = new ApifyClient({ token });
|
|
100
|
-
* const dataset = await Dataset.open(datasetId, { storageClient: new ApifyStorageClient(client) });
|
|
101
|
-
* const { items } = await dataset.getData();
|
|
102
|
-
* ```
|
|
103
|
-
*/
|
|
104
|
-
export class ApifyStorageClient {
|
|
105
|
-
client;
|
|
106
|
-
config;
|
|
107
|
-
getChargingManager;
|
|
108
|
-
constructor(client, config, getChargingManager) {
|
|
109
|
-
this.client = client;
|
|
110
|
-
this.config = config;
|
|
111
|
-
this.getChargingManager = getChargingManager;
|
|
112
|
-
}
|
|
113
|
-
async storageExists(id, type) {
|
|
114
|
-
// Apify's `GET /v2/{kind}/{idOrName}` matches by either id or name;
|
|
115
|
-
// confirm it was an *id* match so crawlee can fall through to `{ name }`.
|
|
116
|
-
const info = await this.resourceClient(id, type).get();
|
|
117
|
-
return info?.id === id;
|
|
118
|
-
}
|
|
119
|
-
async createDatasetClient(options) {
|
|
120
|
-
const id = await this.resolveId(options, 'Dataset');
|
|
121
|
-
const client = this.chargingDatasetClient(id) ?? this.client.dataset(id);
|
|
122
|
-
return adapt(client, {
|
|
123
|
-
getMetadata: 'get',
|
|
124
|
-
drop: 'delete',
|
|
125
|
-
pushData: 'pushItems',
|
|
126
|
-
getData: 'listItems',
|
|
127
|
-
}, noPurge);
|
|
128
|
-
}
|
|
129
|
-
async createKeyValueStoreClient(options) {
|
|
130
|
-
const id = await this.resolveId(options, 'KeyValueStore');
|
|
131
|
-
const client = this.client.keyValueStore(id);
|
|
132
|
-
return adapt(client, {
|
|
133
|
-
getMetadata: 'get',
|
|
134
|
-
getValue: 'getRecord',
|
|
135
|
-
setValue: 'setRecord',
|
|
136
|
-
deleteValue: 'deleteRecord',
|
|
137
|
-
drop: 'delete',
|
|
138
|
-
getPublicUrl: 'getRecordPublicUrl',
|
|
139
|
-
}, {
|
|
140
|
-
...noPurge,
|
|
141
|
-
// crawlee expects an array; apify-client returns `{ items }`.
|
|
142
|
-
listKeys: async (opts) => (await client.listKeys(opts)).items,
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
async createRequestQueueClient(options) {
|
|
146
|
-
const id = await this.resolveId(options, 'RequestQueue');
|
|
147
|
-
const client = this.client.requestQueue(id, options?.clientKey ? { clientKey: options.clientKey } : undefined);
|
|
148
|
-
return adapt(client, { getMetadata: 'get', drop: 'delete' }, noPurge);
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* Returns a charging-aware dataset client when `id` is the run's default
|
|
152
|
-
* dataset and a default-dataset-item price is configured; otherwise
|
|
153
|
-
* `undefined` (caller uses the plain client).
|
|
154
|
-
*/
|
|
155
|
-
chargingDatasetClient(id) {
|
|
156
|
-
const { getChargingManager } = this;
|
|
157
|
-
if (!getChargingManager)
|
|
158
|
-
return undefined;
|
|
159
|
-
if (id !== this.config?.defaultDatasetId)
|
|
160
|
-
return undefined;
|
|
161
|
-
const hasDefaultDatasetItemEvent = DEFAULT_DATASET_ITEM_EVENT in getChargingManager().getPricingInfo().perEventPrices;
|
|
162
|
-
if (!hasDefaultDatasetItemEvent)
|
|
163
|
-
return undefined;
|
|
164
|
-
const datasetClient = new PpeAwareDatasetClient({
|
|
165
|
-
id,
|
|
166
|
-
baseUrl: this.client.baseUrl,
|
|
167
|
-
publicBaseUrl: this.client.publicBaseUrl,
|
|
168
|
-
apifyClient: this.client,
|
|
169
|
-
httpClient: this.client.httpClient,
|
|
170
|
-
}, getChargingManager);
|
|
171
|
-
Object.assign(datasetClient, {
|
|
172
|
-
[USES_PUSH_DATA_INTERCEPTION]: true,
|
|
173
|
-
});
|
|
174
|
-
return datasetClient;
|
|
175
|
-
}
|
|
176
|
-
async resolveId(options, type) {
|
|
177
|
-
if (options?.id)
|
|
178
|
-
return options.id;
|
|
179
|
-
if (options?.name) {
|
|
180
|
-
return (await this.collectionClient(type).getOrCreate(options.name)).id;
|
|
181
|
-
}
|
|
182
|
-
// No id/name (crawlee's `__default__` alias): use the default storage
|
|
183
|
-
// id from the run's environment. apify-client rejects an empty id.
|
|
184
|
-
return this.config?.[DEFAULT_ID_CONFIG_KEY[type]] ?? '';
|
|
185
|
-
}
|
|
186
|
-
resourceClient(id, type) {
|
|
187
|
-
if (type === 'Dataset')
|
|
188
|
-
return this.client.dataset(id);
|
|
189
|
-
if (type === 'KeyValueStore')
|
|
190
|
-
return this.client.keyValueStore(id);
|
|
191
|
-
return this.client.requestQueue(id);
|
|
192
|
-
}
|
|
193
|
-
collectionClient(type) {
|
|
194
|
-
if (type === 'Dataset')
|
|
195
|
-
return this.client.datasets();
|
|
196
|
-
if (type === 'KeyValueStore')
|
|
197
|
-
return this.client.keyValueStores();
|
|
198
|
-
return this.client.requestQueues();
|
|
199
|
-
}
|
|
200
|
-
}
|