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 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, SetStatusMessageOptions, StorageClient } from '@crawlee/types';
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?: StorageClient;
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 config: Configuration;
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 config(): Configuration;
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, snakeCaseToCamelCase } from '@crawlee/utils';
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 { ApifyStorageClient, pushDataChargingContext, USES_PUSH_DATA_INTERCEPTION, } from './apify_storage_client.js';
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
- config;
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.config = configuration;
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.config = Configuration.getGlobalConfig();
100
+ this.configuration = Configuration.getGlobalConfiguration();
99
101
  }
100
102
  else {
101
- this.config = new Configuration(configOptions);
103
+ this.configuration = new Configuration(configOptions);
102
104
  }
103
105
  this.apifyClient = this.newClient();
104
- this.eventManager = new PlatformEventManager(this.config);
105
- this.chargingManager = new ChargingManager(this.config, this.apifyClient);
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.config);
211
+ serviceLocator.setConfiguration(this.configuration);
212
+ this.requestQueueAccess = options.requestQueueAccess ?? 'single';
210
213
  if (this.isAtHome()) {
211
- serviceLocator.setStorageClient(new ApifyStorageClient(this.apifyClient, this.config, () => this.chargingManager));
214
+ serviceLocator.setStorageBackend(this.createApifyStorageBackend());
212
215
  serviceLocator.setEventManager(this.eventManager);
213
216
  }
214
217
  else if (options.storage) {
215
- serviceLocator.setStorageClient(options.storage);
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
- config: this.config,
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.getStorageClient();
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.config.metamorphAfterSleepMillis, ...metamorphOpts } = options;
480
- const runId = this.config.actorRunId;
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.config.actorRunId;
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.config.metamorphAfterSleepMillis } = options;
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.config.actorRunId;
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 client = serviceLocator.getStorageClient();
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).get(), 1000, 'Getting the current run timed out after 1s').catch((e) => log.warning(e.message));
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.config;
773
- const rawInput = await this.getValue(this.config.inputKey);
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.config);
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.config.storageClientOptions ?? {});
979
+ const { storageDir, ...storageClientOptions } = (this.configuration.storageClientOptions ?? {});
985
980
  const { apifyVersion, crawleeVersion } = getSystemInfo();
986
981
  return new ApifyClient({
987
- baseUrl: this.config.apiBaseUrl,
988
- publicBaseUrl: this.config.apiPublicBaseUrl,
989
- token: this.config.token,
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
- config: options?.config || Configuration.getGlobalConfig(),
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 config() {
1576
- return Actor.getDefaultInstance().config;
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.client[USES_PUSH_DATA_INTERCEPTION]);
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.config.defaultDatasetId;
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.config,
1624
- client: options.forceCloud
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
+ }