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
package/dist/actor.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { EventManager, EventTypeName, RecordOptions, UseStateOptions } from '@crawlee/core';
|
|
2
2
|
import { Dataset, RequestQueue } from '@crawlee/core';
|
|
3
|
-
import type { Awaitable, Dictionary,
|
|
3
|
+
import type { Awaitable, Dictionary, StorageBackend } from '@crawlee/types';
|
|
4
4
|
import type { ActorCallOptions, ActorStartOptions, ApifyClientOptions, RunAbortOptions, TaskCallOptions, Webhook, WebhookEventType } from 'apify-client';
|
|
5
5
|
import { ActorRun as ClientActorRun, ApifyClient } from 'apify-client';
|
|
6
6
|
import { type ACTOR_PERMISSION_LEVEL } from '@apify/consts';
|
|
7
|
+
import type { RequestQueueAccessMode } from './apify_request_queue_backend.js';
|
|
7
8
|
import type { ChargeOptions, ChargeResult } from './charging.js';
|
|
8
9
|
import { ChargingManager } from './charging.js';
|
|
9
10
|
import type { ConfigurationOptions } from './configuration.js';
|
|
@@ -13,7 +14,21 @@ import type { ProxyConfigurationOptions } from './proxy_configuration.js';
|
|
|
13
14
|
import { ProxyConfiguration } from './proxy_configuration.js';
|
|
14
15
|
import type { OpenStorageOptions, StorageIdentifier, StorageIdentifierWithoutAlias } from './storage.js';
|
|
15
16
|
export interface InitOptions {
|
|
16
|
-
storage?:
|
|
17
|
+
storage?: StorageBackend;
|
|
18
|
+
/**
|
|
19
|
+
* Determines how request queues opened on the Apify platform are consumed.
|
|
20
|
+
*
|
|
21
|
+
* - `'single'` (default) assumes this run is the only consumer of its request queues. Requests
|
|
22
|
+
* are not locked server-side and the queue head is estimated locally, which means fewer
|
|
23
|
+
* (paid) API calls and better performance.
|
|
24
|
+
* - `'shared'` locks every fetched request server-side, so any number of concurrent consumers
|
|
25
|
+
* (e.g. several Actor runs) can safely process the same queue, at the cost of roughly one
|
|
26
|
+
* extra API call per processed request.
|
|
27
|
+
*
|
|
28
|
+
* Only applies on the Apify platform (or with `forceCloud`); local storage ignores it.
|
|
29
|
+
* @default 'single'
|
|
30
|
+
*/
|
|
31
|
+
requestQueueAccess?: RequestQueueAccessMode;
|
|
17
32
|
/**
|
|
18
33
|
* Whether to automatically handle platform shutdown signals.
|
|
19
34
|
* When enabled, `Actor.exit()` is called on `aborting` events and `Actor.reboot()` on `migrating` events.
|
|
@@ -56,6 +71,12 @@ export interface ExitOptions {
|
|
|
56
71
|
}
|
|
57
72
|
export interface MainOptions extends ExitOptions, InitOptions {
|
|
58
73
|
}
|
|
74
|
+
export interface SetStatusMessageOptions {
|
|
75
|
+
/** If `true`, the status message is treated as final and won't be overwritten by the platform. */
|
|
76
|
+
isStatusMessageTerminal?: boolean;
|
|
77
|
+
/** Log level used when the status message is also logged locally. Defaults to `INFO`. */
|
|
78
|
+
level?: 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR';
|
|
79
|
+
}
|
|
59
80
|
/**
|
|
60
81
|
* Parsed representation of the Apify environment variables.
|
|
61
82
|
* This object is returned by the {@link Actor.getEnv} function.
|
|
@@ -304,7 +325,7 @@ export declare class Actor<Data extends Dictionary = Dictionary> {
|
|
|
304
325
|
* Configuration of this SDK instance (provided to its constructor). See {@link Configuration} for details.
|
|
305
326
|
* @internal
|
|
306
327
|
*/
|
|
307
|
-
readonly
|
|
328
|
+
readonly configuration: Configuration;
|
|
308
329
|
/**
|
|
309
330
|
* Default {@link ApifyClient} instance.
|
|
310
331
|
* @internal
|
|
@@ -343,6 +364,8 @@ export declare class Actor<Data extends Dictionary = Dictionary> {
|
|
|
343
364
|
* @internal
|
|
344
365
|
*/
|
|
345
366
|
purgedStorageAliases: Set<string>;
|
|
367
|
+
/** How Apify platform request queues are consumed; set from {@link InitOptions.requestQueueAccess}. */
|
|
368
|
+
private requestQueueAccess;
|
|
346
369
|
constructor(options?: ActorOptions);
|
|
347
370
|
/**
|
|
348
371
|
* Runs the main user function that performs the job of the Actor
|
|
@@ -1361,13 +1384,14 @@ export declare class Actor<Data extends Dictionary = Dictionary> {
|
|
|
1361
1384
|
/** Default {@link ApifyClient} instance. */
|
|
1362
1385
|
static get apifyClient(): ApifyClient;
|
|
1363
1386
|
/** Default {@link Configuration} instance. */
|
|
1364
|
-
static get
|
|
1387
|
+
static get configuration(): Configuration;
|
|
1365
1388
|
/** @internal */
|
|
1366
1389
|
static getDefaultInstance(): Actor;
|
|
1367
1390
|
private usesPushDataInterception;
|
|
1368
1391
|
private pushDataViaInterceptedClient;
|
|
1369
1392
|
private pushDataWithExplicitCharging;
|
|
1370
1393
|
private _openStorage;
|
|
1394
|
+
private createApifyStorageBackend;
|
|
1371
1395
|
private _ensureActorInit;
|
|
1372
1396
|
/**
|
|
1373
1397
|
* Get time remaining from the Actor run timeout. Returns `undefined` if not on an Apify platform or the current
|
package/dist/actor.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { createPrivateKey } from 'node:crypto';
|
|
2
2
|
import { Dataset, purgeDefaultStorages, RequestQueue, serviceLocator } from '@crawlee/core';
|
|
3
|
-
import { sleep
|
|
3
|
+
import { sleep } from '@crawlee/utils';
|
|
4
4
|
import { ApifyClient } from 'apify-client';
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { ACTOR_ENV_VARS, ACTOR_EVENT_NAMES, APIFY_ENV_VARS, INTEGER_ENV_VARS, } from '@apify/consts';
|
|
7
7
|
import { decryptInputSecrets } from '@apify/input_secrets';
|
|
8
8
|
import log from '@apify/log';
|
|
9
9
|
import { addTimeoutToPromise } from '@apify/timeout';
|
|
10
|
-
import {
|
|
10
|
+
import { ApifyStorageBackend, pushDataChargingContext, USES_PUSH_DATA_INTERCEPTION, } from './apify_storage_backend.js';
|
|
11
11
|
import { ChargingManager, pushDataAndCharge } from './charging.js';
|
|
12
12
|
import { Configuration } from './configuration.js';
|
|
13
13
|
import { getDefaultsFromInputSchema, noActorInputSchemaDefinedMarker, readInputSchema } from './input-schemas.js';
|
|
@@ -15,7 +15,7 @@ import { KeyValueStore } from './key_value_store.js';
|
|
|
15
15
|
import { PlatformEventManager } from './platform_event_manager.js';
|
|
16
16
|
import { ProxyConfiguration } from './proxy_configuration.js';
|
|
17
17
|
import { openStorage } from './storage.js';
|
|
18
|
-
import { checkCrawleeVersion, getSystemInfo, isNonEmptyObject, printOutdatedSdkWarning, validate } from './utils.js';
|
|
18
|
+
import { checkCrawleeVersion, getSystemInfo, isNonEmptyObject, printOutdatedSdkWarning, snakeCaseToCamelCase, validate, } from './utils.js';
|
|
19
19
|
/**
|
|
20
20
|
* Exit codes for the Actor process.
|
|
21
21
|
* The error codes must be in the range 1-128, to avoid collision with signal exits
|
|
@@ -39,7 +39,7 @@ export class Actor {
|
|
|
39
39
|
* Configuration of this SDK instance (provided to its constructor). See {@link Configuration} for details.
|
|
40
40
|
* @internal
|
|
41
41
|
*/
|
|
42
|
-
|
|
42
|
+
configuration;
|
|
43
43
|
/**
|
|
44
44
|
* Default {@link ApifyClient} instance.
|
|
45
45
|
* @internal
|
|
@@ -78,6 +78,8 @@ export class Actor {
|
|
|
78
78
|
* @internal
|
|
79
79
|
*/
|
|
80
80
|
purgedStorageAliases = new Set();
|
|
81
|
+
/** How Apify platform request queues are consumed; set from {@link InitOptions.requestQueueAccess}. */
|
|
82
|
+
requestQueueAccess = 'single';
|
|
81
83
|
constructor(options = {}) {
|
|
82
84
|
const { configuration, ...configOptions } = options;
|
|
83
85
|
if (configuration) {
|
|
@@ -91,18 +93,18 @@ export class Actor {
|
|
|
91
93
|
throw new Error('Actor `configuration` must be an Apify SDK Configuration (imported from `apify`), ' +
|
|
92
94
|
'not a crawlee Configuration, otherwise APIFY_*/ACTOR_* environment variables are not resolved.');
|
|
93
95
|
}
|
|
94
|
-
this.
|
|
96
|
+
this.configuration = configuration;
|
|
95
97
|
}
|
|
96
98
|
else if (Object.keys(configOptions).length === 0) {
|
|
97
99
|
// use default configuration object if nothing overridden (it fallbacks to env vars)
|
|
98
|
-
this.
|
|
100
|
+
this.configuration = Configuration.getGlobalConfiguration();
|
|
99
101
|
}
|
|
100
102
|
else {
|
|
101
|
-
this.
|
|
103
|
+
this.configuration = new Configuration(configOptions);
|
|
102
104
|
}
|
|
103
105
|
this.apifyClient = this.newClient();
|
|
104
|
-
this.eventManager = new PlatformEventManager(this.
|
|
105
|
-
this.chargingManager = new ChargingManager(this.
|
|
106
|
+
this.eventManager = new PlatformEventManager(this.configuration);
|
|
107
|
+
this.chargingManager = new ChargingManager(this.configuration, this.apifyClient);
|
|
106
108
|
}
|
|
107
109
|
/**
|
|
108
110
|
* Runs the main user function that performs the job of the Actor
|
|
@@ -206,13 +208,14 @@ export class Actor {
|
|
|
206
208
|
// Register this Actor's config as the global one so crawlee storages and
|
|
207
209
|
// the event manager resolve the same instance (`availableMemoryRatio` /
|
|
208
210
|
// `disableBrowserSandbox` at-home defaults now live in `Configuration`).
|
|
209
|
-
serviceLocator.setConfiguration(this.
|
|
211
|
+
serviceLocator.setConfiguration(this.configuration);
|
|
212
|
+
this.requestQueueAccess = options.requestQueueAccess ?? 'single';
|
|
210
213
|
if (this.isAtHome()) {
|
|
211
|
-
serviceLocator.
|
|
214
|
+
serviceLocator.setStorageBackend(this.createApifyStorageBackend());
|
|
212
215
|
serviceLocator.setEventManager(this.eventManager);
|
|
213
216
|
}
|
|
214
217
|
else if (options.storage) {
|
|
215
|
-
serviceLocator.
|
|
218
|
+
serviceLocator.setStorageBackend(options.storage);
|
|
216
219
|
}
|
|
217
220
|
// Init the event manager the config uses
|
|
218
221
|
await serviceLocator.getEventManager().init();
|
|
@@ -241,7 +244,7 @@ export class Actor {
|
|
|
241
244
|
this.on(ACTOR_EVENT_NAMES.MIGRATING, this.gracefulShutdownHandlers.migrating);
|
|
242
245
|
}
|
|
243
246
|
await purgeDefaultStorages({
|
|
244
|
-
|
|
247
|
+
configuration: this.configuration,
|
|
245
248
|
onlyPurgeOnce: true,
|
|
246
249
|
});
|
|
247
250
|
log.debug(`Default storages purged`);
|
|
@@ -266,7 +269,7 @@ export class Actor {
|
|
|
266
269
|
options.exitCode ??= EXIT_CODES.SUCCESS;
|
|
267
270
|
options.timeoutSecs ??= 30;
|
|
268
271
|
this._ensureActorInit('exit');
|
|
269
|
-
const client = serviceLocator.
|
|
272
|
+
const client = serviceLocator.getStorageBackend();
|
|
270
273
|
const events = serviceLocator.getEventManager();
|
|
271
274
|
// Remove graceful shutdown handlers to prevent them from interfering with exit
|
|
272
275
|
if (this.gracefulShutdownHandlers.aborting) {
|
|
@@ -476,8 +479,8 @@ export class Actor {
|
|
|
476
479
|
log.warning('Actor.metamorph() is only supported when running on the Apify platform.');
|
|
477
480
|
return;
|
|
478
481
|
}
|
|
479
|
-
const { customAfterSleepMillis = this.
|
|
480
|
-
const runId = this.
|
|
482
|
+
const { customAfterSleepMillis = this.configuration.metamorphAfterSleepMillis, ...metamorphOpts } = options;
|
|
483
|
+
const runId = this.configuration.actorRunId;
|
|
481
484
|
await this.apifyClient.run(runId).metamorph(targetActorId, input, metamorphOpts);
|
|
482
485
|
// Wait some time for container to be stopped.
|
|
483
486
|
await sleep(customAfterSleepMillis);
|
|
@@ -513,10 +516,10 @@ export class Actor {
|
|
|
513
516
|
.listeners("migrating" /* EventType.MIGRATING */)
|
|
514
517
|
.map(async (x) => x({})),
|
|
515
518
|
]);
|
|
516
|
-
const runId = this.
|
|
519
|
+
const runId = this.configuration.actorRunId;
|
|
517
520
|
await this.apifyClient.run(runId).reboot();
|
|
518
521
|
// Wait some time for container to be stopped.
|
|
519
|
-
const { customAfterSleepMillis = this.
|
|
522
|
+
const { customAfterSleepMillis = this.configuration.metamorphAfterSleepMillis } = options;
|
|
520
523
|
await sleep(customAfterSleepMillis);
|
|
521
524
|
}
|
|
522
525
|
/**
|
|
@@ -550,7 +553,7 @@ export class Actor {
|
|
|
550
553
|
log.warning('Actor.addWebhook() is only supported when running on the Apify platform. The webhook will not be invoked.');
|
|
551
554
|
return undefined;
|
|
552
555
|
}
|
|
553
|
-
const runId = this.
|
|
556
|
+
const runId = this.configuration.actorRunId;
|
|
554
557
|
if (!runId) {
|
|
555
558
|
throw new Error(`Environment variable ${ACTOR_ENV_VARS.RUN_ID} is not set!`);
|
|
556
559
|
}
|
|
@@ -589,16 +592,10 @@ export class Actor {
|
|
|
589
592
|
log.info(loggedStatusMessage);
|
|
590
593
|
break;
|
|
591
594
|
}
|
|
592
|
-
const
|
|
593
|
-
// just to be sure, this should be fast
|
|
594
|
-
await addTimeoutToPromise(async () => client.setStatusMessage(statusMessage, {
|
|
595
|
-
isStatusMessageTerminal,
|
|
596
|
-
level,
|
|
597
|
-
}), 1000, 'Setting status message timed out after 1s').catch((e) => log.warning(e.message));
|
|
598
|
-
const runId = this.config.actorRunId;
|
|
595
|
+
const runId = this.configuration.actorRunId;
|
|
599
596
|
if (runId) {
|
|
600
597
|
// just to be sure, this should be fast
|
|
601
|
-
const run = await addTimeoutToPromise(async () => this.apifyClient.run(runId).
|
|
598
|
+
const run = await addTimeoutToPromise(async () => this.apifyClient.run(runId).update({ statusMessage, isStatusMessageTerminal }), 1000, 'Setting status message timed out after 1s').catch((e) => log.warning(e.message));
|
|
602
599
|
if (run) {
|
|
603
600
|
return run;
|
|
604
601
|
}
|
|
@@ -769,8 +766,8 @@ export class Actor {
|
|
|
769
766
|
*/
|
|
770
767
|
async getInput() {
|
|
771
768
|
this._ensureActorInit('getInput');
|
|
772
|
-
const { inputSecretsPrivateKeyFile, inputSecretsPrivateKeyPassphrase } = this.
|
|
773
|
-
const rawInput = await this.getValue(this.
|
|
769
|
+
const { inputSecretsPrivateKeyFile, inputSecretsPrivateKeyPassphrase } = this.configuration;
|
|
770
|
+
const rawInput = await this.getValue(this.configuration.inputKey);
|
|
774
771
|
let input = rawInput;
|
|
775
772
|
if (isNonEmptyObject(rawInput) && inputSecretsPrivateKeyFile && inputSecretsPrivateKeyPassphrase) {
|
|
776
773
|
const privateKey = createPrivateKey({
|
|
@@ -838,8 +835,6 @@ export class Actor {
|
|
|
838
835
|
validate(z.object({ forceCloud: z.boolean().optional() }).strict(), options);
|
|
839
836
|
this._ensureActorInit('openRequestQueue');
|
|
840
837
|
const queue = await this._openStorage(RequestQueue, queueIdOrName, options);
|
|
841
|
-
// eslint-disable-next-line dot-notation
|
|
842
|
-
queue['initialCount'] = (await queue.client.getMetadata())?.totalRequestCount ?? 0;
|
|
843
838
|
return queue;
|
|
844
839
|
}
|
|
845
840
|
/**
|
|
@@ -891,7 +886,7 @@ export class Actor {
|
|
|
891
886
|
if (dontUseApifyProxy && dontUseCustomProxies) {
|
|
892
887
|
return undefined;
|
|
893
888
|
}
|
|
894
|
-
const proxyConfiguration = new ProxyConfiguration(options, this.
|
|
889
|
+
const proxyConfiguration = new ProxyConfiguration(options, this.configuration);
|
|
895
890
|
if (await proxyConfiguration.initialize({ checkAccess })) {
|
|
896
891
|
return proxyConfiguration;
|
|
897
892
|
}
|
|
@@ -981,12 +976,12 @@ export class Actor {
|
|
|
981
976
|
* @ignore
|
|
982
977
|
*/
|
|
983
978
|
newClient(options = {}) {
|
|
984
|
-
const { storageDir, ...storageClientOptions } = (this.
|
|
979
|
+
const { storageDir, ...storageClientOptions } = (this.configuration.storageClientOptions ?? {});
|
|
985
980
|
const { apifyVersion, crawleeVersion } = getSystemInfo();
|
|
986
981
|
return new ApifyClient({
|
|
987
|
-
baseUrl: this.
|
|
988
|
-
publicBaseUrl: this.
|
|
989
|
-
token: this.
|
|
982
|
+
baseUrl: this.configuration.apiBaseUrl,
|
|
983
|
+
publicBaseUrl: this.configuration.apiPublicBaseUrl,
|
|
984
|
+
token: this.configuration.token,
|
|
990
985
|
userAgentSuffix: [`SDK/${apifyVersion}`, `Crawlee/${crawleeVersion}`],
|
|
991
986
|
...storageClientOptions,
|
|
992
987
|
...options, // allow overriding the instance configuration
|
|
@@ -1011,7 +1006,7 @@ export class Actor {
|
|
|
1011
1006
|
async useState(name, defaultValue = {}, options) {
|
|
1012
1007
|
this._ensureActorInit('useState');
|
|
1013
1008
|
const kvStore = await KeyValueStore.open(options?.keyValueStoreName, {
|
|
1014
|
-
|
|
1009
|
+
configuration: options?.configuration || Configuration.getGlobalConfiguration(),
|
|
1015
1010
|
});
|
|
1016
1011
|
return kvStore.getAutoSavedValue(name || 'APIFY_GLOBAL_STATE', defaultValue);
|
|
1017
1012
|
}
|
|
@@ -1572,8 +1567,8 @@ export class Actor {
|
|
|
1572
1567
|
return Actor.getDefaultInstance().apifyClient;
|
|
1573
1568
|
}
|
|
1574
1569
|
/** Default {@link Configuration} instance. */
|
|
1575
|
-
static get
|
|
1576
|
-
return Actor.getDefaultInstance().
|
|
1570
|
+
static get configuration() {
|
|
1571
|
+
return Actor.getDefaultInstance().configuration;
|
|
1577
1572
|
}
|
|
1578
1573
|
/** @internal */
|
|
1579
1574
|
static getDefaultInstance() {
|
|
@@ -1581,7 +1576,7 @@ export class Actor {
|
|
|
1581
1576
|
return this._instance;
|
|
1582
1577
|
}
|
|
1583
1578
|
usesPushDataInterception(dataset) {
|
|
1584
|
-
return Boolean(dataset.
|
|
1579
|
+
return Boolean(dataset.backend[USES_PUSH_DATA_INTERCEPTION]);
|
|
1585
1580
|
}
|
|
1586
1581
|
async pushDataViaInterceptedClient(dataset, item, eventName) {
|
|
1587
1582
|
// PatchedDatasetClient will handle charging and item limiting.
|
|
@@ -1609,7 +1604,7 @@ export class Actor {
|
|
|
1609
1604
|
chargeableWithinLimit: {},
|
|
1610
1605
|
};
|
|
1611
1606
|
}
|
|
1612
|
-
const isDefaultDataset = dataset.id === this.
|
|
1607
|
+
const isDefaultDataset = dataset.id === this.configuration.defaultDatasetId;
|
|
1613
1608
|
return pushDataAndCharge({
|
|
1614
1609
|
chargingManager: this.chargingManager,
|
|
1615
1610
|
items,
|
|
@@ -1620,13 +1615,18 @@ export class Actor {
|
|
|
1620
1615
|
}
|
|
1621
1616
|
async _openStorage(storageClass, identifier, options = {}) {
|
|
1622
1617
|
return openStorage(storageClass, identifier, {
|
|
1623
|
-
config: this.
|
|
1624
|
-
|
|
1625
|
-
? new ApifyStorageClient(this.apifyClient, this.config, () => this.chargingManager)
|
|
1626
|
-
: undefined,
|
|
1618
|
+
config: this.configuration,
|
|
1619
|
+
backend: options.forceCloud ? this.createApifyStorageBackend() : undefined,
|
|
1627
1620
|
purgedStorageAliases: this.purgedStorageAliases,
|
|
1628
1621
|
});
|
|
1629
1622
|
}
|
|
1623
|
+
createApifyStorageBackend() {
|
|
1624
|
+
return new ApifyStorageBackend(this.apifyClient, {
|
|
1625
|
+
configuration: this.configuration,
|
|
1626
|
+
requestQueueAccess: this.requestQueueAccess,
|
|
1627
|
+
getChargingManager: () => this.chargingManager,
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
1630
|
_ensureActorInit(methodCalled) {
|
|
1631
1631
|
// If we already warned the user once, don't do it again to prevent spam
|
|
1632
1632
|
if (this.warnedAboutMissingInitCall) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DatasetBackend, DatasetBackendListOptions, DatasetInfo, Dictionary, PaginatedList } from '@crawlee/types';
|
|
2
|
+
import type { DatasetClient } from 'apify-client';
|
|
3
|
+
/**
|
|
4
|
+
* Implements crawlee v4's {@link DatasetBackend} interface on top of `apify-client`'s
|
|
5
|
+
* dataset API. A thin method-mapping wrapper — the interfaces differ only in naming
|
|
6
|
+
* (`getMetadata`/`get`, `drop`/`delete`, `pushData`/`pushItems`, `getData`/`listItems`).
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
export declare class ApifyDatasetBackend implements DatasetBackend {
|
|
11
|
+
private readonly client;
|
|
12
|
+
constructor(client: DatasetClient);
|
|
13
|
+
getMetadata(): Promise<DatasetInfo>;
|
|
14
|
+
drop(): Promise<void>;
|
|
15
|
+
purge(): Promise<void>;
|
|
16
|
+
pushData(items: Dictionary[]): Promise<void>;
|
|
17
|
+
getData(options?: DatasetBackendListOptions): Promise<PaginatedList<Dictionary>>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Implements crawlee v4's {@link DatasetBackend} interface on top of `apify-client`'s
|
|
3
|
+
* dataset API. A thin method-mapping wrapper — the interfaces differ only in naming
|
|
4
|
+
* (`getMetadata`/`get`, `drop`/`delete`, `pushData`/`pushItems`, `getData`/`listItems`).
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export class ApifyDatasetBackend {
|
|
9
|
+
client;
|
|
10
|
+
constructor(client) {
|
|
11
|
+
this.client = client;
|
|
12
|
+
}
|
|
13
|
+
async getMetadata() {
|
|
14
|
+
const metadata = await this.client.get();
|
|
15
|
+
if (!metadata) {
|
|
16
|
+
throw new Error('Dataset not found or has been deleted.');
|
|
17
|
+
}
|
|
18
|
+
return metadata;
|
|
19
|
+
}
|
|
20
|
+
async drop() {
|
|
21
|
+
await this.client.delete();
|
|
22
|
+
}
|
|
23
|
+
async purge() {
|
|
24
|
+
throw new Error('Purging a dataset is not supported on the Apify platform. ' +
|
|
25
|
+
'Use `drop()` to delete the dataset entirely, or open a new dataset instead.');
|
|
26
|
+
}
|
|
27
|
+
async pushData(items) {
|
|
28
|
+
await this.client.pushItems(items);
|
|
29
|
+
}
|
|
30
|
+
async getData(options) {
|
|
31
|
+
return await this.client.listItems(options);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { KeyValueStoreBackend, KeyValueStoreInfo, KeyValueStoreInputRecord, KeyValueStoreListKeysOptions, KeyValueStoreListKeysResult, KeyValueStoreRecord } from '@crawlee/types';
|
|
2
|
+
import type { KeyValueStoreClient } from 'apify-client';
|
|
3
|
+
/**
|
|
4
|
+
* Implements crawlee v4's {@link KeyValueStoreBackend} interface on top of `apify-client`'s
|
|
5
|
+
* key-value store API. Mostly a method-mapping wrapper (`getValue`/`getRecord`,
|
|
6
|
+
* `setValue`/`setRecord`, `drop`/`delete`, ...); the one semantic difference is that storage
|
|
7
|
+
* backends are byte transports, so records are read unparsed (see {@link getValue}).
|
|
8
|
+
*
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export declare class ApifyKeyValueStoreBackend implements KeyValueStoreBackend {
|
|
12
|
+
private readonly client;
|
|
13
|
+
constructor(client: KeyValueStoreClient);
|
|
14
|
+
getMetadata(): Promise<KeyValueStoreInfo>;
|
|
15
|
+
drop(): Promise<void>;
|
|
16
|
+
purge(): Promise<void>;
|
|
17
|
+
getValue(key: string): Promise<KeyValueStoreRecord | undefined>;
|
|
18
|
+
setValue(record: KeyValueStoreInputRecord): Promise<void>;
|
|
19
|
+
deleteValue(key: string): Promise<void>;
|
|
20
|
+
listKeys(options?: KeyValueStoreListKeysOptions): Promise<KeyValueStoreListKeysResult>;
|
|
21
|
+
getPublicUrl(key: string): Promise<string | undefined>;
|
|
22
|
+
recordExists(key: string): Promise<boolean>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Implements crawlee v4's {@link KeyValueStoreBackend} interface on top of `apify-client`'s
|
|
3
|
+
* key-value store API. Mostly a method-mapping wrapper (`getValue`/`getRecord`,
|
|
4
|
+
* `setValue`/`setRecord`, `drop`/`delete`, ...); the one semantic difference is that storage
|
|
5
|
+
* backends are byte transports, so records are read unparsed (see {@link getValue}).
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export class ApifyKeyValueStoreBackend {
|
|
10
|
+
client;
|
|
11
|
+
constructor(client) {
|
|
12
|
+
this.client = client;
|
|
13
|
+
}
|
|
14
|
+
async getMetadata() {
|
|
15
|
+
const metadata = await this.client.get();
|
|
16
|
+
if (!metadata) {
|
|
17
|
+
throw new Error('Key-value store not found or has been deleted.');
|
|
18
|
+
}
|
|
19
|
+
return metadata;
|
|
20
|
+
}
|
|
21
|
+
async drop() {
|
|
22
|
+
await this.client.delete();
|
|
23
|
+
}
|
|
24
|
+
async purge() {
|
|
25
|
+
throw new Error('Purging a key-value store is not supported on the Apify platform. ' +
|
|
26
|
+
'Use `drop()` to delete the store entirely, or open a new store instead.');
|
|
27
|
+
}
|
|
28
|
+
async getValue(key) {
|
|
29
|
+
// Storage backends are byte transports — the KeyValueStore frontend parses values
|
|
30
|
+
// according to their content type, so the record must be returned unparsed.
|
|
31
|
+
return this.client.getRecord(key, { buffer: true });
|
|
32
|
+
}
|
|
33
|
+
async setValue(record) {
|
|
34
|
+
await this.client.setRecord(record);
|
|
35
|
+
}
|
|
36
|
+
async deleteValue(key) {
|
|
37
|
+
await this.client.deleteRecord(key);
|
|
38
|
+
}
|
|
39
|
+
async listKeys(options) {
|
|
40
|
+
const result = await this.client.listKeys(options);
|
|
41
|
+
// The API does not report a content type for listed keys; crawlee's item shape
|
|
42
|
+
// requires the field, so it is left undefined via the cast.
|
|
43
|
+
return {
|
|
44
|
+
...result,
|
|
45
|
+
items: result.items.map(({ key, size }) => ({ key, size })),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
async getPublicUrl(key) {
|
|
49
|
+
return this.client.getRecordPublicUrl(key);
|
|
50
|
+
}
|
|
51
|
+
async recordExists(key) {
|
|
52
|
+
return this.client.recordExists(key);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { BatchAddRequestsResult, QueueOperationInfo, RequestQueueBackend, RequestQueueInfo, RequestQueueOperationOptions, RequestSchema, UpdateRequestSchema } from '@crawlee/types';
|
|
2
|
+
import type { RequestQueueClient as ApifyRequestQueueApiClient } from 'apify-client';
|
|
3
|
+
/**
|
|
4
|
+
* Determines how an Apify platform request queue is consumed.
|
|
5
|
+
*
|
|
6
|
+
* - `'single'` — optimized for a single consumer. The client keeps a local estimate of the queue
|
|
7
|
+
* head and never locks requests, which means fewer API calls, better performance and lower cost.
|
|
8
|
+
* Multiple producers may still add requests concurrently, but only one client may *consume*
|
|
9
|
+
* (fetch and process) them.
|
|
10
|
+
* - `'shared'` — safe for multiple concurrent consumers (e.g. several Actor runs processing one
|
|
11
|
+
* queue). Requests are locked server-side while they are being processed, at the cost of more
|
|
12
|
+
* API calls.
|
|
13
|
+
*/
|
|
14
|
+
export type RequestQueueAccessMode = 'single' | 'shared';
|
|
15
|
+
/**
|
|
16
|
+
* Derives a request id from its unique key, exactly as the Apify platform does
|
|
17
|
+
* (`sha256(uniqueKey)` → base64 → strip `+`/`/`/`=` → first 15 chars). Lets us
|
|
18
|
+
* address a request by unique key without an extra round-trip.
|
|
19
|
+
*/
|
|
20
|
+
export declare function uniqueKeyToRequestId(uniqueKey: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Common base of the Apify platform implementations of Crawlee v4's stateful, pull-based
|
|
23
|
+
* {@link RequestQueueBackend} interface, built on top of `apify-client`'s REST request-queue API.
|
|
24
|
+
*
|
|
25
|
+
* The mode-specific consumption logic lives in the subclasses:
|
|
26
|
+
* {@link ApifyRequestQueueSingleBackend} (single consumer, no locking) and
|
|
27
|
+
* {@link ApifyRequestQueueSharedBackend} (multiple consumers, server-side locking).
|
|
28
|
+
* Modeled on the Apify Python SDK's request-queue clients.
|
|
29
|
+
*
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
export declare abstract class ApifyRequestQueueBackend implements RequestQueueBackend {
|
|
33
|
+
protected readonly client: ApifyRequestQueueApiClient;
|
|
34
|
+
/**
|
|
35
|
+
* Local estimates of the queue counters, updated as this client adds/handles requests. The API
|
|
36
|
+
* counters can lag behind by a few seconds, so {@link getMetadata} reports whichever is higher.
|
|
37
|
+
*/
|
|
38
|
+
protected estimatedTotalRequestCount: number;
|
|
39
|
+
protected estimatedHandledRequestCount: number;
|
|
40
|
+
constructor(client: ApifyRequestQueueApiClient);
|
|
41
|
+
abstract addBatchOfRequests(requests: RequestSchema[], options?: RequestQueueOperationOptions): Promise<BatchAddRequestsResult>;
|
|
42
|
+
abstract getRequest(uniqueKey: string): Promise<UpdateRequestSchema | undefined>;
|
|
43
|
+
abstract fetchNextRequest(): Promise<UpdateRequestSchema | undefined>;
|
|
44
|
+
abstract markRequestAsHandled(request: UpdateRequestSchema): Promise<QueueOperationInfo | undefined>;
|
|
45
|
+
abstract reclaimRequest(request: UpdateRequestSchema, options?: RequestQueueOperationOptions): Promise<QueueOperationInfo | undefined>;
|
|
46
|
+
abstract isEmpty(): Promise<boolean>;
|
|
47
|
+
abstract isFinished(): Promise<boolean>;
|
|
48
|
+
setExpectedRequestProcessingTimeSecs(_secs: number): Promise<void>;
|
|
49
|
+
getMetadata(): Promise<RequestQueueInfo>;
|
|
50
|
+
drop(): Promise<void>;
|
|
51
|
+
purge(): Promise<void>;
|
|
52
|
+
protected requestIdFromUniqueKey(uniqueKey: string): string;
|
|
53
|
+
/**
|
|
54
|
+
* Fetches the full request record by id.
|
|
55
|
+
*
|
|
56
|
+
* The apify-client return type understates the payload (the API returns the complete request
|
|
57
|
+
* record including `userData`, `payload`, `handledAt`, ...), hence the cast.
|
|
58
|
+
*/
|
|
59
|
+
protected getRequestById(id: string): Promise<UpdateRequestSchema | undefined>;
|
|
60
|
+
/**
|
|
61
|
+
* Adds new requests to the platform queue. The API assigns ids itself, so any incoming id is
|
|
62
|
+
* stripped to pass its strict input validation. `apify-client` internally chunks the batch and
|
|
63
|
+
* retries transient failures.
|
|
64
|
+
*/
|
|
65
|
+
protected sendBatch(requests: RequestSchema[], forefront?: boolean): Promise<BatchAddRequestsResult>;
|
|
66
|
+
/** Updates a request record on the platform and maps the result to crawlee's shape. */
|
|
67
|
+
protected updateRequestOnPlatform(request: UpdateRequestSchema, forefront?: boolean): Promise<QueueOperationInfo>;
|
|
68
|
+
/** Counts freshly added requests from an add-batch result into the local metadata estimates. */
|
|
69
|
+
protected recordAddedRequests(result: BatchAddRequestsResult): void;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A minimal FIFO mutex — serializes the async critical sections passed to {@link runExclusive}.
|
|
73
|
+
* @internal
|
|
74
|
+
*/
|
|
75
|
+
export declare class AsyncLock {
|
|
76
|
+
private tail;
|
|
77
|
+
runExclusive<T>(fn: () => Promise<T>): Promise<T>;
|
|
78
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
/** Apify request IDs are the first 15 chars of a base64 SHA-256 of the unique key. */
|
|
3
|
+
const REQUEST_ID_LENGTH = 15;
|
|
4
|
+
/**
|
|
5
|
+
* Derives a request id from its unique key, exactly as the Apify platform does
|
|
6
|
+
* (`sha256(uniqueKey)` → base64 → strip `+`/`/`/`=` → first 15 chars). Lets us
|
|
7
|
+
* address a request by unique key without an extra round-trip.
|
|
8
|
+
*/
|
|
9
|
+
export function uniqueKeyToRequestId(uniqueKey) {
|
|
10
|
+
const hash = createHash('sha256').update(uniqueKey).digest('base64').replace(/[+/=]/g, '');
|
|
11
|
+
return hash.slice(0, REQUEST_ID_LENGTH);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Common base of the Apify platform implementations of Crawlee v4's stateful, pull-based
|
|
15
|
+
* {@link RequestQueueBackend} interface, built on top of `apify-client`'s REST request-queue API.
|
|
16
|
+
*
|
|
17
|
+
* The mode-specific consumption logic lives in the subclasses:
|
|
18
|
+
* {@link ApifyRequestQueueSingleBackend} (single consumer, no locking) and
|
|
19
|
+
* {@link ApifyRequestQueueSharedBackend} (multiple consumers, server-side locking).
|
|
20
|
+
* Modeled on the Apify Python SDK's request-queue clients.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
export class ApifyRequestQueueBackend {
|
|
25
|
+
client;
|
|
26
|
+
/**
|
|
27
|
+
* Local estimates of the queue counters, updated as this client adds/handles requests. The API
|
|
28
|
+
* counters can lag behind by a few seconds, so {@link getMetadata} reports whichever is higher.
|
|
29
|
+
*/
|
|
30
|
+
estimatedTotalRequestCount = 0;
|
|
31
|
+
estimatedHandledRequestCount = 0;
|
|
32
|
+
constructor(client) {
|
|
33
|
+
this.client = client;
|
|
34
|
+
}
|
|
35
|
+
async setExpectedRequestProcessingTimeSecs(_secs) {
|
|
36
|
+
// Only relevant for backends that reserve requests via locking; see the shared backend.
|
|
37
|
+
}
|
|
38
|
+
async getMetadata() {
|
|
39
|
+
const metadata = await this.client.get();
|
|
40
|
+
if (!metadata) {
|
|
41
|
+
throw new Error('Request queue not found or has been deleted.');
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
id: metadata.id,
|
|
45
|
+
name: metadata.name,
|
|
46
|
+
createdAt: metadata.createdAt,
|
|
47
|
+
modifiedAt: metadata.modifiedAt,
|
|
48
|
+
accessedAt: metadata.accessedAt,
|
|
49
|
+
totalRequestCount: Math.max(metadata.totalRequestCount, this.estimatedTotalRequestCount),
|
|
50
|
+
handledRequestCount: Math.max(metadata.handledRequestCount, this.estimatedHandledRequestCount),
|
|
51
|
+
pendingRequestCount: metadata.pendingRequestCount,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async drop() {
|
|
55
|
+
await this.client.delete();
|
|
56
|
+
}
|
|
57
|
+
async purge() {
|
|
58
|
+
throw new Error('Purging a request queue is not supported on the Apify platform. ' +
|
|
59
|
+
'Use `drop()` to delete the queue entirely, or open a new queue instead.');
|
|
60
|
+
}
|
|
61
|
+
requestIdFromUniqueKey(uniqueKey) {
|
|
62
|
+
return uniqueKeyToRequestId(uniqueKey);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Fetches the full request record by id.
|
|
66
|
+
*
|
|
67
|
+
* The apify-client return type understates the payload (the API returns the complete request
|
|
68
|
+
* record including `userData`, `payload`, `handledAt`, ...), hence the cast.
|
|
69
|
+
*/
|
|
70
|
+
async getRequestById(id) {
|
|
71
|
+
const request = await this.client.getRequest(id);
|
|
72
|
+
return request ?? undefined;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Adds new requests to the platform queue. The API assigns ids itself, so any incoming id is
|
|
76
|
+
* stripped to pass its strict input validation. `apify-client` internally chunks the batch and
|
|
77
|
+
* retries transient failures.
|
|
78
|
+
*/
|
|
79
|
+
async sendBatch(requests, forefront) {
|
|
80
|
+
const apiRequests = requests.map((request) => {
|
|
81
|
+
const { id: _id, ...rest } = request;
|
|
82
|
+
return rest;
|
|
83
|
+
});
|
|
84
|
+
const result = await this.client.batchAddRequests(apiRequests, { forefront });
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
/** Updates a request record on the platform and maps the result to crawlee's shape. */
|
|
88
|
+
async updateRequestOnPlatform(request, forefront) {
|
|
89
|
+
const result = await this.client.updateRequest(request, { forefront });
|
|
90
|
+
return {
|
|
91
|
+
requestId: result.requestId,
|
|
92
|
+
wasAlreadyPresent: result.wasAlreadyPresent,
|
|
93
|
+
wasAlreadyHandled: result.wasAlreadyHandled,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** Counts freshly added requests from an add-batch result into the local metadata estimates. */
|
|
97
|
+
recordAddedRequests(result) {
|
|
98
|
+
const newRequestCount = result.processedRequests.filter((request) => !request.wasAlreadyPresent && !request.wasAlreadyHandled).length;
|
|
99
|
+
this.estimatedTotalRequestCount += newRequestCount;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* A minimal FIFO mutex — serializes the async critical sections passed to {@link runExclusive}.
|
|
104
|
+
* @internal
|
|
105
|
+
*/
|
|
106
|
+
export class AsyncLock {
|
|
107
|
+
tail = Promise.resolve();
|
|
108
|
+
async runExclusive(fn) {
|
|
109
|
+
const run = this.tail.then(fn);
|
|
110
|
+
// Keep the chain alive even when the critical section throws.
|
|
111
|
+
this.tail = run.catch(() => { });
|
|
112
|
+
return run;
|
|
113
|
+
}
|
|
114
|
+
}
|