apify 4.0.0-beta.19 → 4.0.0-beta.21
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 +29 -6
- package/dist/actor.js +46 -47
- 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 -3
- package/dist/index.js +2 -3
- package/dist/input-schemas.d.ts +1 -1
- package/dist/platform_event_manager.d.ts +2 -2
- package/dist/platform_event_manager.js +5 -5
- package/dist/proxy_configuration.d.ts +8 -2
- package/dist/proxy_configuration.js +21 -10
- 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 +6 -6
- package/dist/apify_storage_client.d.ts +0 -66
- package/dist/apify_storage_client.js +0 -200
- package/dist/key_value_store.d.ts +0 -21
- package/dist/key_value_store.js +0 -41
package/dist/actor.d.ts
CHANGED
|
@@ -1,19 +1,33 @@
|
|
|
1
1
|
import type { EventManager, EventTypeName, RecordOptions, UseStateOptions } from '@crawlee/core';
|
|
2
|
-
import { Dataset, RequestQueue } from '@crawlee/core';
|
|
3
|
-
import type { Awaitable, Dictionary,
|
|
2
|
+
import { Dataset, KeyValueStore, RequestQueue } from '@crawlee/core';
|
|
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';
|
|
10
11
|
import { Configuration } from './configuration.js';
|
|
11
|
-
import { KeyValueStore } from './key_value_store.js';
|
|
12
12
|
import type { ProxyConfigurationOptions } from './proxy_configuration.js';
|
|
13
13
|
import { ProxyConfiguration } from './proxy_configuration.js';
|
|
14
14
|
import type { OpenStorageOptions, StorageIdentifier, StorageIdentifierWithoutAlias } from './storage.js';
|
|
15
15
|
export interface InitOptions {
|
|
16
|
-
storage?:
|
|
16
|
+
storage?: StorageBackend;
|
|
17
|
+
/**
|
|
18
|
+
* Determines how request queues opened on the Apify platform are consumed.
|
|
19
|
+
*
|
|
20
|
+
* - `'single'` (default) assumes this run is the only consumer of its request queues. Requests
|
|
21
|
+
* are not locked server-side and the queue head is estimated locally, which means fewer
|
|
22
|
+
* (paid) API calls and better performance.
|
|
23
|
+
* - `'shared'` locks every fetched request server-side, so any number of concurrent consumers
|
|
24
|
+
* (e.g. several Actor runs) can safely process the same queue, at the cost of roughly one
|
|
25
|
+
* extra API call per processed request.
|
|
26
|
+
*
|
|
27
|
+
* Only applies on the Apify platform (or with `forceCloud`); local storage ignores it.
|
|
28
|
+
* @default 'single'
|
|
29
|
+
*/
|
|
30
|
+
requestQueueAccess?: RequestQueueAccessMode;
|
|
17
31
|
/**
|
|
18
32
|
* Whether to automatically handle platform shutdown signals.
|
|
19
33
|
* When enabled, `Actor.exit()` is called on `aborting` events and `Actor.reboot()` on `migrating` events.
|
|
@@ -56,6 +70,12 @@ export interface ExitOptions {
|
|
|
56
70
|
}
|
|
57
71
|
export interface MainOptions extends ExitOptions, InitOptions {
|
|
58
72
|
}
|
|
73
|
+
export interface SetStatusMessageOptions {
|
|
74
|
+
/** If `true`, the status message is treated as final and won't be overwritten by the platform. */
|
|
75
|
+
isStatusMessageTerminal?: boolean;
|
|
76
|
+
/** Log level used when the status message is also logged locally. Defaults to `INFO`. */
|
|
77
|
+
level?: 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR';
|
|
78
|
+
}
|
|
59
79
|
/**
|
|
60
80
|
* Parsed representation of the Apify environment variables.
|
|
61
81
|
* This object is returned by the {@link Actor.getEnv} function.
|
|
@@ -304,7 +324,7 @@ export declare class Actor<Data extends Dictionary = Dictionary> {
|
|
|
304
324
|
* Configuration of this SDK instance (provided to its constructor). See {@link Configuration} for details.
|
|
305
325
|
* @internal
|
|
306
326
|
*/
|
|
307
|
-
readonly
|
|
327
|
+
readonly configuration: Configuration;
|
|
308
328
|
/**
|
|
309
329
|
* Default {@link ApifyClient} instance.
|
|
310
330
|
* @internal
|
|
@@ -343,6 +363,8 @@ export declare class Actor<Data extends Dictionary = Dictionary> {
|
|
|
343
363
|
* @internal
|
|
344
364
|
*/
|
|
345
365
|
purgedStorageAliases: Set<string>;
|
|
366
|
+
/** How Apify platform request queues are consumed; set from {@link InitOptions.requestQueueAccess}. */
|
|
367
|
+
private requestQueueAccess;
|
|
346
368
|
constructor(options?: ActorOptions);
|
|
347
369
|
/**
|
|
348
370
|
* Runs the main user function that performs the job of the Actor
|
|
@@ -1361,13 +1383,14 @@ export declare class Actor<Data extends Dictionary = Dictionary> {
|
|
|
1361
1383
|
/** Default {@link ApifyClient} instance. */
|
|
1362
1384
|
static get apifyClient(): ApifyClient;
|
|
1363
1385
|
/** Default {@link Configuration} instance. */
|
|
1364
|
-
static get
|
|
1386
|
+
static get configuration(): Configuration;
|
|
1365
1387
|
/** @internal */
|
|
1366
1388
|
static getDefaultInstance(): Actor;
|
|
1367
1389
|
private usesPushDataInterception;
|
|
1368
1390
|
private pushDataViaInterceptedClient;
|
|
1369
1391
|
private pushDataWithExplicitCharging;
|
|
1370
1392
|
private _openStorage;
|
|
1393
|
+
private createApifyStorageBackend;
|
|
1371
1394
|
private _ensureActorInit;
|
|
1372
1395
|
/**
|
|
1373
1396
|
* 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,21 +1,20 @@
|
|
|
1
1
|
import { createPrivateKey } from 'node:crypto';
|
|
2
|
-
import { Dataset, purgeDefaultStorages, RequestQueue, serviceLocator } from '@crawlee/core';
|
|
3
|
-
import { sleep
|
|
2
|
+
import { Dataset, KeyValueStore, purgeDefaultStorages, RequestQueue, serviceLocator } from '@crawlee/core';
|
|
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';
|
|
14
|
-
import { KeyValueStore } from './key_value_store.js';
|
|
15
14
|
import { PlatformEventManager } from './platform_event_manager.js';
|
|
16
15
|
import { ProxyConfiguration } from './proxy_configuration.js';
|
|
17
16
|
import { openStorage } from './storage.js';
|
|
18
|
-
import { checkCrawleeVersion, getSystemInfo, isNonEmptyObject, printOutdatedSdkWarning, validate } from './utils.js';
|
|
17
|
+
import { checkCrawleeVersion, getSystemInfo, isNonEmptyObject, printOutdatedSdkWarning, snakeCaseToCamelCase, validate, } from './utils.js';
|
|
19
18
|
/**
|
|
20
19
|
* Exit codes for the Actor process.
|
|
21
20
|
* The error codes must be in the range 1-128, to avoid collision with signal exits
|
|
@@ -39,7 +38,7 @@ export class Actor {
|
|
|
39
38
|
* Configuration of this SDK instance (provided to its constructor). See {@link Configuration} for details.
|
|
40
39
|
* @internal
|
|
41
40
|
*/
|
|
42
|
-
|
|
41
|
+
configuration;
|
|
43
42
|
/**
|
|
44
43
|
* Default {@link ApifyClient} instance.
|
|
45
44
|
* @internal
|
|
@@ -78,6 +77,8 @@ export class Actor {
|
|
|
78
77
|
* @internal
|
|
79
78
|
*/
|
|
80
79
|
purgedStorageAliases = new Set();
|
|
80
|
+
/** How Apify platform request queues are consumed; set from {@link InitOptions.requestQueueAccess}. */
|
|
81
|
+
requestQueueAccess = 'single';
|
|
81
82
|
constructor(options = {}) {
|
|
82
83
|
const { configuration, ...configOptions } = options;
|
|
83
84
|
if (configuration) {
|
|
@@ -91,18 +92,18 @@ export class Actor {
|
|
|
91
92
|
throw new Error('Actor `configuration` must be an Apify SDK Configuration (imported from `apify`), ' +
|
|
92
93
|
'not a crawlee Configuration, otherwise APIFY_*/ACTOR_* environment variables are not resolved.');
|
|
93
94
|
}
|
|
94
|
-
this.
|
|
95
|
+
this.configuration = configuration;
|
|
95
96
|
}
|
|
96
97
|
else if (Object.keys(configOptions).length === 0) {
|
|
97
98
|
// use default configuration object if nothing overridden (it fallbacks to env vars)
|
|
98
|
-
this.
|
|
99
|
+
this.configuration = Configuration.getGlobalConfiguration();
|
|
99
100
|
}
|
|
100
101
|
else {
|
|
101
|
-
this.
|
|
102
|
+
this.configuration = new Configuration(configOptions);
|
|
102
103
|
}
|
|
103
104
|
this.apifyClient = this.newClient();
|
|
104
|
-
this.eventManager = new PlatformEventManager(this.
|
|
105
|
-
this.chargingManager = new ChargingManager(this.
|
|
105
|
+
this.eventManager = new PlatformEventManager(this.configuration);
|
|
106
|
+
this.chargingManager = new ChargingManager(this.configuration, this.apifyClient);
|
|
106
107
|
}
|
|
107
108
|
/**
|
|
108
109
|
* Runs the main user function that performs the job of the Actor
|
|
@@ -206,13 +207,14 @@ export class Actor {
|
|
|
206
207
|
// Register this Actor's config as the global one so crawlee storages and
|
|
207
208
|
// the event manager resolve the same instance (`availableMemoryRatio` /
|
|
208
209
|
// `disableBrowserSandbox` at-home defaults now live in `Configuration`).
|
|
209
|
-
serviceLocator.setConfiguration(this.
|
|
210
|
+
serviceLocator.setConfiguration(this.configuration);
|
|
211
|
+
this.requestQueueAccess = options.requestQueueAccess ?? 'single';
|
|
210
212
|
if (this.isAtHome()) {
|
|
211
|
-
serviceLocator.
|
|
213
|
+
serviceLocator.setStorageBackend(this.createApifyStorageBackend());
|
|
212
214
|
serviceLocator.setEventManager(this.eventManager);
|
|
213
215
|
}
|
|
214
216
|
else if (options.storage) {
|
|
215
|
-
serviceLocator.
|
|
217
|
+
serviceLocator.setStorageBackend(options.storage);
|
|
216
218
|
}
|
|
217
219
|
// Init the event manager the config uses
|
|
218
220
|
await serviceLocator.getEventManager().init();
|
|
@@ -241,7 +243,7 @@ export class Actor {
|
|
|
241
243
|
this.on(ACTOR_EVENT_NAMES.MIGRATING, this.gracefulShutdownHandlers.migrating);
|
|
242
244
|
}
|
|
243
245
|
await purgeDefaultStorages({
|
|
244
|
-
|
|
246
|
+
configuration: this.configuration,
|
|
245
247
|
onlyPurgeOnce: true,
|
|
246
248
|
});
|
|
247
249
|
log.debug(`Default storages purged`);
|
|
@@ -266,7 +268,7 @@ export class Actor {
|
|
|
266
268
|
options.exitCode ??= EXIT_CODES.SUCCESS;
|
|
267
269
|
options.timeoutSecs ??= 30;
|
|
268
270
|
this._ensureActorInit('exit');
|
|
269
|
-
const client = serviceLocator.
|
|
271
|
+
const client = serviceLocator.getStorageBackend();
|
|
270
272
|
const events = serviceLocator.getEventManager();
|
|
271
273
|
// Remove graceful shutdown handlers to prevent them from interfering with exit
|
|
272
274
|
if (this.gracefulShutdownHandlers.aborting) {
|
|
@@ -476,8 +478,8 @@ export class Actor {
|
|
|
476
478
|
log.warning('Actor.metamorph() is only supported when running on the Apify platform.');
|
|
477
479
|
return;
|
|
478
480
|
}
|
|
479
|
-
const { customAfterSleepMillis = this.
|
|
480
|
-
const runId = this.
|
|
481
|
+
const { customAfterSleepMillis = this.configuration.metamorphAfterSleepMillis, ...metamorphOpts } = options;
|
|
482
|
+
const runId = this.configuration.actorRunId;
|
|
481
483
|
await this.apifyClient.run(runId).metamorph(targetActorId, input, metamorphOpts);
|
|
482
484
|
// Wait some time for container to be stopped.
|
|
483
485
|
await sleep(customAfterSleepMillis);
|
|
@@ -513,10 +515,10 @@ export class Actor {
|
|
|
513
515
|
.listeners("migrating" /* EventType.MIGRATING */)
|
|
514
516
|
.map(async (x) => x({})),
|
|
515
517
|
]);
|
|
516
|
-
const runId = this.
|
|
518
|
+
const runId = this.configuration.actorRunId;
|
|
517
519
|
await this.apifyClient.run(runId).reboot();
|
|
518
520
|
// Wait some time for container to be stopped.
|
|
519
|
-
const { customAfterSleepMillis = this.
|
|
521
|
+
const { customAfterSleepMillis = this.configuration.metamorphAfterSleepMillis } = options;
|
|
520
522
|
await sleep(customAfterSleepMillis);
|
|
521
523
|
}
|
|
522
524
|
/**
|
|
@@ -550,7 +552,7 @@ export class Actor {
|
|
|
550
552
|
log.warning('Actor.addWebhook() is only supported when running on the Apify platform. The webhook will not be invoked.');
|
|
551
553
|
return undefined;
|
|
552
554
|
}
|
|
553
|
-
const runId = this.
|
|
555
|
+
const runId = this.configuration.actorRunId;
|
|
554
556
|
if (!runId) {
|
|
555
557
|
throw new Error(`Environment variable ${ACTOR_ENV_VARS.RUN_ID} is not set!`);
|
|
556
558
|
}
|
|
@@ -589,16 +591,10 @@ export class Actor {
|
|
|
589
591
|
log.info(loggedStatusMessage);
|
|
590
592
|
break;
|
|
591
593
|
}
|
|
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;
|
|
594
|
+
const runId = this.configuration.actorRunId;
|
|
599
595
|
if (runId) {
|
|
600
596
|
// just to be sure, this should be fast
|
|
601
|
-
const run = await addTimeoutToPromise(async () => this.apifyClient.run(runId).
|
|
597
|
+
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
598
|
if (run) {
|
|
603
599
|
return run;
|
|
604
600
|
}
|
|
@@ -769,8 +765,8 @@ export class Actor {
|
|
|
769
765
|
*/
|
|
770
766
|
async getInput() {
|
|
771
767
|
this._ensureActorInit('getInput');
|
|
772
|
-
const { inputSecretsPrivateKeyFile, inputSecretsPrivateKeyPassphrase } = this.
|
|
773
|
-
const rawInput = await this.getValue(this.
|
|
768
|
+
const { inputSecretsPrivateKeyFile, inputSecretsPrivateKeyPassphrase } = this.configuration;
|
|
769
|
+
const rawInput = await this.getValue(this.configuration.inputKey);
|
|
774
770
|
let input = rawInput;
|
|
775
771
|
if (isNonEmptyObject(rawInput) && inputSecretsPrivateKeyFile && inputSecretsPrivateKeyPassphrase) {
|
|
776
772
|
const privateKey = createPrivateKey({
|
|
@@ -838,8 +834,6 @@ export class Actor {
|
|
|
838
834
|
validate(z.object({ forceCloud: z.boolean().optional() }).strict(), options);
|
|
839
835
|
this._ensureActorInit('openRequestQueue');
|
|
840
836
|
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
837
|
return queue;
|
|
844
838
|
}
|
|
845
839
|
/**
|
|
@@ -891,7 +885,7 @@ export class Actor {
|
|
|
891
885
|
if (dontUseApifyProxy && dontUseCustomProxies) {
|
|
892
886
|
return undefined;
|
|
893
887
|
}
|
|
894
|
-
const proxyConfiguration = new ProxyConfiguration(options, this.
|
|
888
|
+
const proxyConfiguration = new ProxyConfiguration(options, this.configuration);
|
|
895
889
|
if (await proxyConfiguration.initialize({ checkAccess })) {
|
|
896
890
|
return proxyConfiguration;
|
|
897
891
|
}
|
|
@@ -981,12 +975,12 @@ export class Actor {
|
|
|
981
975
|
* @ignore
|
|
982
976
|
*/
|
|
983
977
|
newClient(options = {}) {
|
|
984
|
-
const { storageDir, ...storageClientOptions } = (this.
|
|
978
|
+
const { storageDir, ...storageClientOptions } = (this.configuration.storageClientOptions ?? {});
|
|
985
979
|
const { apifyVersion, crawleeVersion } = getSystemInfo();
|
|
986
980
|
return new ApifyClient({
|
|
987
|
-
baseUrl: this.
|
|
988
|
-
publicBaseUrl: this.
|
|
989
|
-
token: this.
|
|
981
|
+
baseUrl: this.configuration.apiBaseUrl,
|
|
982
|
+
publicBaseUrl: this.configuration.apiPublicBaseUrl,
|
|
983
|
+
token: this.configuration.token,
|
|
990
984
|
userAgentSuffix: [`SDK/${apifyVersion}`, `Crawlee/${crawleeVersion}`],
|
|
991
985
|
...storageClientOptions,
|
|
992
986
|
...options, // allow overriding the instance configuration
|
|
@@ -1011,7 +1005,7 @@ export class Actor {
|
|
|
1011
1005
|
async useState(name, defaultValue = {}, options) {
|
|
1012
1006
|
this._ensureActorInit('useState');
|
|
1013
1007
|
const kvStore = await KeyValueStore.open(options?.keyValueStoreName, {
|
|
1014
|
-
|
|
1008
|
+
configuration: options?.configuration || Configuration.getGlobalConfiguration(),
|
|
1015
1009
|
});
|
|
1016
1010
|
return kvStore.getAutoSavedValue(name || 'APIFY_GLOBAL_STATE', defaultValue);
|
|
1017
1011
|
}
|
|
@@ -1572,8 +1566,8 @@ export class Actor {
|
|
|
1572
1566
|
return Actor.getDefaultInstance().apifyClient;
|
|
1573
1567
|
}
|
|
1574
1568
|
/** Default {@link Configuration} instance. */
|
|
1575
|
-
static get
|
|
1576
|
-
return Actor.getDefaultInstance().
|
|
1569
|
+
static get configuration() {
|
|
1570
|
+
return Actor.getDefaultInstance().configuration;
|
|
1577
1571
|
}
|
|
1578
1572
|
/** @internal */
|
|
1579
1573
|
static getDefaultInstance() {
|
|
@@ -1581,7 +1575,7 @@ export class Actor {
|
|
|
1581
1575
|
return this._instance;
|
|
1582
1576
|
}
|
|
1583
1577
|
usesPushDataInterception(dataset) {
|
|
1584
|
-
return Boolean(dataset.
|
|
1578
|
+
return Boolean(dataset.backend[USES_PUSH_DATA_INTERCEPTION]);
|
|
1585
1579
|
}
|
|
1586
1580
|
async pushDataViaInterceptedClient(dataset, item, eventName) {
|
|
1587
1581
|
// PatchedDatasetClient will handle charging and item limiting.
|
|
@@ -1609,7 +1603,7 @@ export class Actor {
|
|
|
1609
1603
|
chargeableWithinLimit: {},
|
|
1610
1604
|
};
|
|
1611
1605
|
}
|
|
1612
|
-
const isDefaultDataset = dataset.id === this.
|
|
1606
|
+
const isDefaultDataset = dataset.id === this.configuration.defaultDatasetId;
|
|
1613
1607
|
return pushDataAndCharge({
|
|
1614
1608
|
chargingManager: this.chargingManager,
|
|
1615
1609
|
items,
|
|
@@ -1620,13 +1614,18 @@ export class Actor {
|
|
|
1620
1614
|
}
|
|
1621
1615
|
async _openStorage(storageClass, identifier, options = {}) {
|
|
1622
1616
|
return openStorage(storageClass, identifier, {
|
|
1623
|
-
config: this.
|
|
1624
|
-
|
|
1625
|
-
? new ApifyStorageClient(this.apifyClient, this.config, () => this.chargingManager)
|
|
1626
|
-
: undefined,
|
|
1617
|
+
config: this.configuration,
|
|
1618
|
+
backend: options.forceCloud ? this.createApifyStorageBackend() : undefined,
|
|
1627
1619
|
purgedStorageAliases: this.purgedStorageAliases,
|
|
1628
1620
|
});
|
|
1629
1621
|
}
|
|
1622
|
+
createApifyStorageBackend() {
|
|
1623
|
+
return new ApifyStorageBackend(this.apifyClient, {
|
|
1624
|
+
configuration: this.configuration,
|
|
1625
|
+
requestQueueAccess: this.requestQueueAccess,
|
|
1626
|
+
getChargingManager: () => this.chargingManager,
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1630
1629
|
_ensureActorInit(methodCalled) {
|
|
1631
1630
|
// If we already warned the user once, don't do it again to prevent spam
|
|
1632
1631
|
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
|
+
}
|