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