apify 4.0.0-beta.34 → 4.0.0-beta.36

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
@@ -1381,22 +1381,6 @@ export declare class Actor<Data extends Dictionary = Dictionary> {
1381
1381
  * @internal
1382
1382
  */
1383
1383
  static setDefaultInstance(instance?: Actor): void;
1384
- /**
1385
- * The backend the Actor installs: the caller's, the platform's, or crawlee's local default.
1386
- *
1387
- * Only the last two are wrapped for dataset-item charging. The platform counts an
1388
- * `apify-default-dataset-item` per item written to the run's default dataset through Apify
1389
- * storage, so a caller-supplied backend is billed nothing and must not be accounted for -
1390
- * charging for it would spend a budget nobody is consuming and trim the caller's items to fit
1391
- * it. crawlee's local default stands in for Apify storage, so it is wrapped to keep local
1392
- * pay-per-event testing faithful.
1393
- *
1394
- * Wrapping means owning the instance, which is why the local default is constructed here
1395
- * rather than left to be created lazily on first use.
1396
- */
1397
- private createStorageBackend;
1398
- /** Wrapped here rather than at the `init()` call site so that `forceCloud` storages are charged too. */
1399
- private createApifyStorageBackend;
1400
1384
  /**
1401
1385
  * crawlee's service locator is set-once, so a backend registered before `init()` wins and the
1402
1386
  * Actor cannot install its own over it. Replaces the resulting `ServiceConflictError` with the
package/dist/actor.js CHANGED
@@ -15,6 +15,7 @@ import { Configuration } from './configuration.js';
15
15
  import { getDefaultsFromInputSchema, noActorInputSchemaDefinedMarker, readInputSchema } from './input-schemas.js';
16
16
  import { PlatformEventManager } from './platform_event_manager.js';
17
17
  import { ProxyConfiguration } from './proxy_configuration.js';
18
+ import { SmartApifyStorageBackend } from './smart_apify_storage_backend.js';
18
19
  import { checkCrawleeVersion, getSystemInfo, isNonEmptyObject, printOutdatedSdkWarning, snakeCaseToCamelCase, } from './utils.js';
19
20
  /**
20
21
  * Exit codes for the Actor process.
@@ -78,6 +79,8 @@ export class Actor {
78
79
  #chargingManager;
79
80
  /** How Apify platform request queues are consumed; set from {@link InitOptions.requestQueueAccess}. */
80
81
  #requestQueueAccess = 'single';
82
+ /** Lazily built by the `#storageBackend` getter. */
83
+ #cachedStorageBackend;
81
84
  constructor(options = {}) {
82
85
  const { configuration, ...configOptions } = options;
83
86
  if (configuration) {
@@ -212,7 +215,7 @@ export class Actor {
212
215
  serviceLocator.setEventManager(this.eventManager);
213
216
  }
214
217
  if (!serviceLocator.getServicesIfSet().storageBackend) {
215
- this.installStorageBackend(this.createStorageBackend(options.storage));
218
+ this.installStorageBackend(options.storage ?? this.#storageBackend);
216
219
  }
217
220
  else if (options.storage) {
218
221
  this.installStorageBackend(options.storage);
@@ -255,8 +258,7 @@ export class Actor {
255
258
  log.debug(`ChargingManager initialized`, this.#chargingManager.getPricingInfo());
256
259
  // Only knowable once the pricing is loaded. Reachable with a caller-supplied backend or one
257
260
  // registered with crawlee directly; the storages the Actor installs itself are wrapped.
258
- if (this.#chargingManager.isPayPerEvent &&
259
- !(serviceLocator.getStorageBackend() instanceof ChargingStorageBackend)) {
261
+ if (this.#chargingManager.isPayPerEvent && !this.#chargesDefaultDatasetItems()) {
260
262
  log.warning('Items pushed to the default dataset will not be charged for, because this run does not use Apify ' +
261
263
  'storage - the platform only counts items it stores itself.');
262
264
  }
@@ -721,7 +723,9 @@ export class Actor {
721
723
  parseArgument(options, z.object({ forceCloud: z.boolean().optional() }).strict());
722
724
  this.ensureActorInit('openDataset');
723
725
  return Dataset.open(datasetIdOrName ?? null, {
724
- storageBackend: options.forceCloud ? this.createApifyStorageBackend() : undefined,
726
+ storageBackend: options.forceCloud
727
+ ? this.#storageBackend.getSuitableStorageBackend({ forceCloud: true })
728
+ : undefined,
725
729
  });
726
730
  }
727
731
  /**
@@ -871,7 +875,9 @@ export class Actor {
871
875
  parseArgument(options, z.object({ forceCloud: z.boolean().optional() }).strict());
872
876
  this.ensureActorInit('openKeyValueStore');
873
877
  return KeyValueStore.open(storeIdOrName ?? null, {
874
- storageBackend: options.forceCloud ? this.createApifyStorageBackend() : undefined,
878
+ storageBackend: options.forceCloud
879
+ ? this.#storageBackend.getSuitableStorageBackend({ forceCloud: true })
880
+ : undefined,
875
881
  });
876
882
  }
877
883
  /**
@@ -897,7 +903,9 @@ export class Actor {
897
903
  parseArgument(options, z.object({ forceCloud: z.boolean().optional() }).strict());
898
904
  this.ensureActorInit('openRequestQueue');
899
905
  return RequestQueue.open(queueIdOrName ?? null, {
900
- storageBackend: options.forceCloud ? this.createApifyStorageBackend() : undefined,
906
+ storageBackend: options.forceCloud
907
+ ? this.#storageBackend.getSuitableStorageBackend({ forceCloud: true })
908
+ : undefined,
901
909
  });
902
910
  }
903
911
  /**
@@ -1646,40 +1654,34 @@ export class Actor {
1646
1654
  Actor.#instance = instance;
1647
1655
  }
1648
1656
  /**
1649
- * The backend the Actor installs: the caller's, the platform's, or crawlee's local default.
1657
+ * The backend the Actor installs unless the caller brings one: Apify platform storage on the
1658
+ * platform, crawlee's local default outside of it.
1650
1659
  *
1651
- * Only the last two are wrapped for dataset-item charging. The platform counts an
1652
- * `apify-default-dataset-item` per item written to the run's default dataset through Apify
1653
- * storage, so a caller-supplied backend is billed nothing and must not be accounted for -
1654
- * charging for it would spend a budget nobody is consuming and trim the caller's items to fit
1655
- * it. crawlee's local default stands in for Apify storage, so it is wrapped to keep local
1656
- * pay-per-event testing faithful.
1660
+ * Both sides are wrapped for dataset-item charging - crawlee's local default stands in for
1661
+ * Apify storage, so local pay-per-event testing stays faithful. A caller-supplied backend
1662
+ * (`Actor.init({ storage })`) is billed nothing and is installed unwrapped, in place of this.
1657
1663
  *
1658
- * Wrapping means owning the instance, which is why the local default is constructed here
1659
- * rather than left to be created lazily on first use.
1664
+ * One instance per Actor: a second platform backend would mint unnamed storages of its own for
1665
+ * aliases this one has already resolved.
1660
1666
  */
1661
- createStorageBackend(storage) {
1662
- if (storage) {
1663
- return storage;
1664
- }
1665
- if (this.isAtHome()) {
1666
- return this.createApifyStorageBackend();
1667
- }
1668
- return new ChargingStorageBackend(new ServiceLocator(this.configuration).getStorageBackend(), {
1667
+ get #storageBackend() {
1668
+ const charging = {
1669
1669
  configuration: this.configuration,
1670
1670
  getChargingManager: () => this.#chargingManager,
1671
- });
1672
- }
1673
- /** Wrapped here rather than at the `init()` call site so that `forceCloud` storages are charged too. */
1674
- createApifyStorageBackend() {
1675
- const backend = new ApifyStorageBackend(this.apifyClient, {
1671
+ };
1672
+ return (this.#cachedStorageBackend ??= new SmartApifyStorageBackend({
1673
+ cloudStorageBackend: new ChargingStorageBackend(new ApifyStorageBackend(this.apifyClient, {
1674
+ configuration: this.configuration,
1675
+ requestQueueAccess: this.#requestQueueAccess,
1676
+ }), charging),
1677
+ localStorageBackend: new ChargingStorageBackend(new ServiceLocator(this.configuration).getStorageBackend(), charging),
1676
1678
  configuration: this.configuration,
1677
- requestQueueAccess: this.#requestQueueAccess,
1678
- });
1679
- return new ChargingStorageBackend(backend, {
1680
- configuration: this.configuration,
1681
- getChargingManager: () => this.#chargingManager,
1682
- });
1679
+ }));
1680
+ }
1681
+ /** Whether items reaching the run's default dataset are counted for pay-per-event charging. */
1682
+ #chargesDefaultDatasetItems() {
1683
+ const installed = serviceLocator.getStorageBackend();
1684
+ return installed instanceof SmartApifyStorageBackend || installed instanceof ChargingStorageBackend;
1683
1685
  }
1684
1686
  /**
1685
1687
  * crawlee's service locator is set-once, so a backend registered before `init()` wins and the
@@ -2,7 +2,7 @@ import type { DatasetBackend, KeyValueStoreBackend, RequestQueueBackend, Storage
2
2
  import type { ApifyClient } from 'apify-client';
3
3
  import { type RequestQueueAccessMode } from './apify_request_queue_backend.js';
4
4
  import type { Configuration } from './configuration.js';
5
- type StorageType = 'Dataset' | 'KeyValueStore' | 'RequestQueue';
5
+ export type StorageType = 'Dataset' | 'KeyValueStore' | 'RequestQueue';
6
6
  /** The reserved alias crawlee uses for the default (unnamed) storage. */
7
7
  export declare const DEFAULT_STORAGE_ALIAS = "__default__";
8
8
  export interface ApifyStorageBackendOptions {
@@ -88,4 +88,3 @@ export declare class ApifyStorageBackend implements StorageBackend {
88
88
  private resourceClient;
89
89
  private collectionClient;
90
90
  }
91
- export {};
@@ -9,9 +9,6 @@ export declare const apifyConfigFields: {
9
9
  persistStorage: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodBoolean>>>;
10
10
  storageDir: ConfigField<z.ZodDefault<z.ZodString>>;
11
11
  containerized: ConfigField<z.ZodOptional<z.ZodPreprocess<z.ZodBoolean>>>;
12
- defaultDatasetId: ConfigField<z.ZodDefault<z.ZodString>>;
13
- defaultKeyValueStoreId: ConfigField<z.ZodDefault<z.ZodString>>;
14
- defaultRequestQueueId: ConfigField<z.ZodDefault<z.ZodString>>;
15
12
  inputKey: ConfigField<z.ZodDefault<z.ZodString>>;
16
13
  memoryMbytes: ConfigField<z.ZodOptional<z.ZodPreprocess<z.ZodNumber, unknown>>>;
17
14
  availableMemoryRatio: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodNumber, unknown>>>;
@@ -22,6 +19,9 @@ export declare const apifyConfigFields: {
22
19
  chromeExecutablePath: ConfigField<z.ZodOptional<z.ZodString>>;
23
20
  defaultBrowserPath: ConfigField<z.ZodOptional<z.ZodString>>;
24
21
  purgeOnStart: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodBoolean, unknown>>>;
22
+ defaultDatasetId: ConfigField<z.ZodDefault<z.ZodString>>;
23
+ defaultKeyValueStoreId: ConfigField<z.ZodDefault<z.ZodString>>;
24
+ defaultRequestQueueId: ConfigField<z.ZodDefault<z.ZodString>>;
25
25
  metamorphAfterSleepMillis: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodNumber, unknown>>>;
26
26
  actorEventsWsUrl: ConfigField<z.ZodOptional<z.ZodString>>;
27
27
  token: ConfigField<z.ZodOptional<z.ZodString>>;
@@ -20,9 +20,6 @@ export const apifyConfigFields = {
20
20
  // take precedence; crawlee's own CRAWLEE_* var is reused as the fallback,
21
21
  // never re-typed). A schema is passed only where the SDK needs a different
22
22
  // default than crawlee's.
23
- defaultDatasetId: withApifyEnv(crawleeConfigFields.defaultDatasetId, [ACTOR_ENV_VARS.DEFAULT_DATASET_ID, APIFY_ENV_VARS.DEFAULT_DATASET_ID], z.string().default(LOCAL_ACTOR_ENV_VARS[ACTOR_ENV_VARS.DEFAULT_DATASET_ID])),
24
- defaultKeyValueStoreId: withApifyEnv(crawleeConfigFields.defaultKeyValueStoreId, [ACTOR_ENV_VARS.DEFAULT_KEY_VALUE_STORE_ID, APIFY_ENV_VARS.DEFAULT_KEY_VALUE_STORE_ID], z.string().default(LOCAL_ACTOR_ENV_VARS[ACTOR_ENV_VARS.DEFAULT_KEY_VALUE_STORE_ID])),
25
- defaultRequestQueueId: withApifyEnv(crawleeConfigFields.defaultRequestQueueId, [ACTOR_ENV_VARS.DEFAULT_REQUEST_QUEUE_ID, APIFY_ENV_VARS.DEFAULT_REQUEST_QUEUE_ID], z.string().default(LOCAL_ACTOR_ENV_VARS[ACTOR_ENV_VARS.DEFAULT_REQUEST_QUEUE_ID])),
26
23
  inputKey: withApifyEnv(crawleeConfigFields.inputKey, [ACTOR_ENV_VARS.INPUT_KEY, APIFY_ENV_VARS.INPUT_KEY]),
27
24
  memoryMbytes: withApifyEnv(crawleeConfigFields.memoryMbytes, [
28
25
  ACTOR_ENV_VARS.MEMORY_MBYTES,
@@ -39,6 +36,20 @@ export const apifyConfigFields = {
39
36
  chromeExecutablePath: withApifyEnv(crawleeConfigFields.chromeExecutablePath, APIFY_ENV_VARS.CHROME_EXECUTABLE_PATH),
40
37
  defaultBrowserPath: withApifyEnv(crawleeConfigFields.defaultBrowserPath, 'APIFY_DEFAULT_BROWSER_PATH'),
41
38
  purgeOnStart: withApifyEnv(crawleeConfigFields.purgeOnStart, APIFY_ENV_VARS.PURGE_ON_START),
39
+ // Crawlee addresses the default storage by a reserved alias; on the platform
40
+ // each run gets real storage IDs, which the API calls need.
41
+ defaultDatasetId: field(z.string().default(LOCAL_ACTOR_ENV_VARS[ACTOR_ENV_VARS.DEFAULT_DATASET_ID]), [
42
+ ACTOR_ENV_VARS.DEFAULT_DATASET_ID,
43
+ APIFY_ENV_VARS.DEFAULT_DATASET_ID,
44
+ ]),
45
+ defaultKeyValueStoreId: field(z.string().default(LOCAL_ACTOR_ENV_VARS[ACTOR_ENV_VARS.DEFAULT_KEY_VALUE_STORE_ID]), [
46
+ ACTOR_ENV_VARS.DEFAULT_KEY_VALUE_STORE_ID,
47
+ APIFY_ENV_VARS.DEFAULT_KEY_VALUE_STORE_ID,
48
+ ]),
49
+ defaultRequestQueueId: field(z.string().default(LOCAL_ACTOR_ENV_VARS[ACTOR_ENV_VARS.DEFAULT_REQUEST_QUEUE_ID]), [
50
+ ACTOR_ENV_VARS.DEFAULT_REQUEST_QUEUE_ID,
51
+ APIFY_ENV_VARS.DEFAULT_REQUEST_QUEUE_ID,
52
+ ]),
42
53
  // Apify-specific fields
43
54
  metamorphAfterSleepMillis: field(coerceNumber.default(300_000), APIFY_ENV_VARS.METAMORPH_AFTER_SLEEP_MILLIS),
44
55
  actorEventsWsUrl: field(z.string().optional(), [
@@ -0,0 +1,52 @@
1
+ import type { DatasetBackend, KeyValueStoreBackend, RequestQueueBackend, StorageBackend, StorageIdentifier } from '@crawlee/types';
2
+ import type { StorageType } from './apify_storage_backend.js';
3
+ import type { Configuration } from './configuration.js';
4
+ import type { OpenStorageOptions } from './storage.js';
5
+ export interface SmartApifyStorageBackendOptions {
6
+ /** Used on the Apify platform, and locally for `forceCloud` storages. */
7
+ cloudStorageBackend: StorageBackend;
8
+ /** Used outside of the Apify platform, except for `forceCloud` storages. */
9
+ localStorageBackend: StorageBackend;
10
+ /** Supplies `isAtHome` and the token that cloud storage needs. */
11
+ configuration: Configuration;
12
+ }
13
+ /**
14
+ * Routes storages to Apify cloud storage or to a local one, depending on where the Actor runs.
15
+ *
16
+ * On the Apify platform (detected through the `APIFY_IS_AT_HOME` environment variable) storages are
17
+ * opened in the cloud; locally they are opened through `localStorageBackend`, unless `forceCloud`
18
+ * asks for the cloud one. `Actor` installs it; callers who want storages of their own pass a
19
+ * backend to `Actor.init({ storage })` instead.
20
+ *
21
+ * @internal
22
+ */
23
+ export declare class SmartApifyStorageBackend implements StorageBackend {
24
+ #private;
25
+ constructor(options: SmartApifyStorageBackendOptions);
26
+ /**
27
+ * The backend a storage is opened through: the cloud one on the platform or with `forceCloud`,
28
+ * the local one otherwise.
29
+ *
30
+ * Exposed so that `forceCloud` storages can be opened through the very same cloud backend
31
+ * instance this one delegates to — two instances would resolve run-scoped aliases to two
32
+ * separate storages.
33
+ */
34
+ getSuitableStorageBackend(options?: OpenStorageOptions): StorageBackend;
35
+ get stats(): {
36
+ rateLimitErrors: number[];
37
+ } | undefined;
38
+ /** Repeats crawlee's own fallback, so that routing a backend does not change how storages are cached. */
39
+ getStorageBackendCacheKey(): string;
40
+ createDatasetBackend(options?: StorageIdentifier): Promise<DatasetBackend>;
41
+ createKeyValueStoreBackend(options?: StorageIdentifier): Promise<KeyValueStoreBackend>;
42
+ createRequestQueueBackend(options?: StorageIdentifier): Promise<RequestQueueBackend>;
43
+ /**
44
+ * Declared unconditionally, unlike on the backends this one delegates to: crawlee branches on
45
+ * the presence of the three methods below, and delegating to a backend that lacks one is
46
+ * indistinguishable from not having it — a `false` from `storageExists` means the same as its
47
+ * absence (crawlee falls through to a name lookup), and the other two have no return value.
48
+ */
49
+ storageExists(id: string, type: StorageType): Promise<boolean>;
50
+ purge(): Promise<void>;
51
+ teardown(): Promise<void>;
52
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Routes storages to Apify cloud storage or to a local one, depending on where the Actor runs.
3
+ *
4
+ * On the Apify platform (detected through the `APIFY_IS_AT_HOME` environment variable) storages are
5
+ * opened in the cloud; locally they are opened through `localStorageBackend`, unless `forceCloud`
6
+ * asks for the cloud one. `Actor` installs it; callers who want storages of their own pass a
7
+ * backend to `Actor.init({ storage })` instead.
8
+ *
9
+ * @internal
10
+ */
11
+ export class SmartApifyStorageBackend {
12
+ #cloudStorageBackend;
13
+ #localStorageBackend;
14
+ #configuration;
15
+ constructor(options) {
16
+ this.#cloudStorageBackend = options.cloudStorageBackend;
17
+ this.#localStorageBackend = options.localStorageBackend;
18
+ this.#configuration = options.configuration;
19
+ }
20
+ /**
21
+ * The backend a storage is opened through: the cloud one on the platform or with `forceCloud`,
22
+ * the local one otherwise.
23
+ *
24
+ * Exposed so that `forceCloud` storages can be opened through the very same cloud backend
25
+ * instance this one delegates to — two instances would resolve run-scoped aliases to two
26
+ * separate storages.
27
+ */
28
+ getSuitableStorageBackend(options = {}) {
29
+ if (this.#configuration.isAtHome) {
30
+ return this.#cloudStorageBackend;
31
+ }
32
+ if (!options.forceCloud) {
33
+ return this.#localStorageBackend;
34
+ }
35
+ if (!this.#configuration.token) {
36
+ throw new Error('In order to use the Apify cloud storage from your computer, you need to provide an Apify token ' +
37
+ 'using the APIFY_TOKEN environment variable.');
38
+ }
39
+ return this.#cloudStorageBackend;
40
+ }
41
+ get stats() {
42
+ return this.getSuitableStorageBackend().stats;
43
+ }
44
+ /** Repeats crawlee's own fallback, so that routing a backend does not change how storages are cached. */
45
+ getStorageBackendCacheKey() {
46
+ const backend = this.getSuitableStorageBackend();
47
+ return backend.getStorageBackendCacheKey?.() ?? backend.constructor.name;
48
+ }
49
+ async createDatasetBackend(options) {
50
+ return await this.getSuitableStorageBackend().createDatasetBackend(options);
51
+ }
52
+ async createKeyValueStoreBackend(options) {
53
+ return await this.getSuitableStorageBackend().createKeyValueStoreBackend(options);
54
+ }
55
+ async createRequestQueueBackend(options) {
56
+ return await this.getSuitableStorageBackend().createRequestQueueBackend(options);
57
+ }
58
+ /**
59
+ * Declared unconditionally, unlike on the backends this one delegates to: crawlee branches on
60
+ * the presence of the three methods below, and delegating to a backend that lacks one is
61
+ * indistinguishable from not having it — a `false` from `storageExists` means the same as its
62
+ * absence (crawlee falls through to a name lookup), and the other two have no return value.
63
+ */
64
+ async storageExists(id, type) {
65
+ return (await this.getSuitableStorageBackend().storageExists?.(id, type)) ?? false;
66
+ }
67
+ async purge() {
68
+ await this.getSuitableStorageBackend().purge?.();
69
+ }
70
+ async teardown() {
71
+ await this.getSuitableStorageBackend().teardown?.();
72
+ }
73
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apify",
3
- "version": "4.0.0-beta.34",
3
+ "version": "4.0.0-beta.36",
4
4
  "description": "The scalable web crawling and scraping library for JavaScript/Node.js. Enables development of data extraction and web automation jobs (not only) with headless Chrome and Puppeteer.",
5
5
  "engines": {
6
6
  "node": ">=22.0.0"