apify 4.0.0-beta.39 → 4.0.0-beta.41

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/README.md CHANGED
@@ -26,7 +26,7 @@ import { Actor } from 'apify';
26
26
 
27
27
  await Actor.init();
28
28
 
29
- const input = (await Actor.getInput()) ?? {};
29
+ const input = await Actor.getInput();
30
30
  await Actor.setValue('OUTPUT', {
31
31
  message: 'Hello from Apify SDK!',
32
32
  input,
package/dist/actor.d.ts CHANGED
@@ -673,17 +673,17 @@ export declare class Actor<Data extends Dictionary = Dictionary> {
673
673
  /**
674
674
  * Gets the Actor input value from the default {@link KeyValueStore} associated with the current Actor run.
675
675
  *
676
- * This is just a convenient shortcut for [`keyValueStore.getValue('INPUT')`](core/class/KeyValueStore#getValue).
677
- * For example, calling the following code:
678
- * ```js
679
- * const input = await Actor.getInput();
680
- * ```
676
+ * The input is the record stored under the configured input key (`ACTOR_INPUT_KEY`, default `INPUT`).
677
+ * A record stored as `application/octet-stream` — which is what a local input file without an
678
+ * extension is read as — is parsed with JSON5 when it parses, and returned as a `Buffer` otherwise.
681
679
  *
682
- * is equivalent to:
683
- * ```js
684
- * const store = await Actor.openKeyValueStore();
685
- * await store.getValue('INPUT');
686
- * ```
680
+ * When running locally and the store holds no such record, the input is read from a `<inputKey>` or
681
+ * `<inputKey>.json` file in the current working directory instead.
682
+ *
683
+ * Throws an {@link ActorInputError} when no input is found (`code: 'NOT_FOUND'`), when both input
684
+ * files exist in the working directory (`'MULTIPLE_FILES'`), when a JSON input does not parse
685
+ * (`'PARSE_FAILED'`) or when its secret fields cannot be decrypted (`'DECRYPTION_FAILED'`). If your
686
+ * Actor can run without input, catch the `'NOT_FOUND'` case and rethrow the rest.
687
687
  *
688
688
  * Note that the `getInput()` function does not cache the value read from the key-value store.
689
689
  * If you need to use the input multiple times in your Actor,
@@ -695,16 +695,20 @@ export declare class Actor<Data extends Dictionary = Dictionary> {
695
695
  * @returns
696
696
  * Returns a promise that resolves to an object, string
697
697
  * or [`Buffer`](https://nodejs.org/api/buffer.html), depending
698
- * on the MIME content type of the record, or `null`
699
- * if the record is missing.
698
+ * on the MIME content type of the record.
699
+ * @throws {ActorInputError} When the input cannot be produced; see `code`.
700
700
  * @ignore
701
701
  */
702
- getInput<T = Dictionary | string | Buffer>(): Promise<T | null>;
702
+ getInput<T = Dictionary | string | Buffer>(): Promise<T>;
703
703
  /**
704
- * Gets the Actor input value just like the {@link Actor.getInput} method,
705
- * but throws if it is not found.
704
+ * Reads the input from a `<inputKey>` or `<inputKey>.json` file in the working directory, for local runs
705
+ * whose default key-value store holds no input record. Mirrors how `ApifyFileSystemStorageBackend` adopts
706
+ * such files inside the store directory: the `.json` file is JSON, the bare one is bytes that
707
+ * {@link parseInputValue} tries as JSON5. Both files present is an error rather than a guess.
708
+ *
709
+ * @returns `undefined` when running on the platform or when neither file exists.
706
710
  */
707
- getInputOrThrow<T = Dictionary | string | Buffer>(): Promise<T>;
711
+ private readInputFromWorkingDirectory;
708
712
  /**
709
713
  * Opens a key-value store and returns a promise resolving to an instance of the {@link KeyValueStore} class.
710
714
  *
@@ -1218,16 +1222,28 @@ export declare class Actor<Data extends Dictionary = Dictionary> {
1218
1222
  /**
1219
1223
  * Gets the Actor input value from the default {@link KeyValueStore} associated with the current Actor run.
1220
1224
  *
1221
- * This is just a convenient shortcut for {@link KeyValueStore.getValue | `keyValueStore.getValue('INPUT')`}.
1222
- * For example, calling the following code:
1225
+ * The input is the record stored under the configured input key (`ACTOR_INPUT_KEY`, default `INPUT`):
1223
1226
  * ```js
1224
1227
  * const input = await Actor.getInput();
1225
1228
  * ```
1226
1229
  *
1227
- * is equivalent to:
1230
+ * A record stored as `application/octet-stream` — which is what a local input file without an
1231
+ * extension is read as — is parsed with JSON5 when it parses, and returned as a `Buffer` otherwise.
1232
+ *
1233
+ * When running locally and the store holds no such record, the input is read from a `<inputKey>` or
1234
+ * `<inputKey>.json` file in the current working directory instead.
1235
+ *
1236
+ * Throws an {@link ActorInputError} when no input is found (`code: 'NOT_FOUND'`), when both input
1237
+ * files exist in the working directory (`'MULTIPLE_FILES'`), when a JSON input does not parse
1238
+ * (`'PARSE_FAILED'`) or when its secret fields cannot be decrypted (`'DECRYPTION_FAILED'`). If your
1239
+ * Actor can run without input, catch the `'NOT_FOUND'` case and rethrow the rest:
1228
1240
  * ```js
1229
- * const store = await Actor.openKeyValueStore();
1230
- * await store.getValue('INPUT');
1241
+ * let input = {};
1242
+ * try {
1243
+ * input = await Actor.getInput();
1244
+ * } catch (error) {
1245
+ * if (!(error instanceof ActorInputError) || error.code !== 'NOT_FOUND') throw error;
1246
+ * }
1231
1247
  * ```
1232
1248
  *
1233
1249
  * Note that the `getInput()` function does not cache the value read from the key-value store.
@@ -1239,15 +1255,10 @@ export declare class Actor<Data extends Dictionary = Dictionary> {
1239
1255
  * @returns
1240
1256
  * Returns a promise that resolves to an object, string
1241
1257
  * or [`Buffer`](https://nodejs.org/api/buffer.html), depending
1242
- * on the MIME content type of the record, or `null`
1243
- * if the record is missing.
1244
- */
1245
- static getInput<T = Dictionary | string | Buffer>(): Promise<T | null>;
1246
- /**
1247
- * Gets the Actor input value just like the {@link Actor.getInput} method,
1248
- * but throws if it is not found.
1258
+ * on the MIME content type of the record.
1259
+ * @throws {ActorInputError} When the input cannot be produced; see `code`.
1249
1260
  */
1250
- static getInputOrThrow<T = Dictionary | string | Buffer>(): Promise<T>;
1261
+ static getInput<T = Dictionary | string | Buffer>(): Promise<T>;
1251
1262
  /**
1252
1263
  * Opens a key-value store and returns a promise resolving to an instance of the {@link KeyValueStore} class.
1253
1264
  *
@@ -1375,6 +1386,12 @@ export declare class Actor<Data extends Dictionary = Dictionary> {
1375
1386
  * @internal
1376
1387
  */
1377
1388
  static setDefaultInstance(instance?: Actor): void;
1389
+ /**
1390
+ * The same choice crawlee's `ServiceLocator` makes for its implicit default backend, with the
1391
+ * input-aware {@link ApifyFileSystemStorageBackend} in place of crawlee's — crawlee has no
1392
+ * notion of a run input.
1393
+ */
1394
+ private createLocalStorageBackend;
1378
1395
  /**
1379
1396
  * crawlee's service locator is set-once, so a backend registered before `init()` wins and the
1380
1397
  * Actor cannot install its own over it. Replaces the resulting `ServiceConflictError` with the
package/dist/actor.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { createPrivateKey } from 'node:crypto';
2
- import { Dataset, EventType, KeyValueStore, purgeDefaultStorages, rejectOperationInTransaction, RequestQueue, ServiceLocator, serviceLocator, } from '@crawlee/core';
2
+ import { readFile, stat } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { Dataset, EventType, KeyValueStore, MemoryStorageBackend, purgeDefaultStorages, rejectOperationInTransaction, RequestQueue, serviceLocator, } from '@crawlee/core';
3
5
  import { sleep } from '@crawlee/utils';
4
6
  import { ApifyClient } from 'apify-client';
5
7
  import { z } from 'zod';
@@ -8,15 +10,29 @@ import { decryptInputSecrets } from '@apify/input_secrets';
8
10
  import log from '@apify/log';
9
11
  import { addTimeoutToPromise } from '@apify/timeout';
10
12
  import { parseArgument } from '@apify/validations';
13
+ import { ApifyFileSystemStorageBackend } from './apify_file_system_storage_backend.js';
11
14
  import { ApifyStorageBackend } from './apify_storage_backend.js';
12
15
  import { ChargingManager, DEFAULT_DATASET_ITEM_EVENT } from './charging.js';
13
16
  import { ChargingDatasetBackend, ChargingStorageBackend } from './charging_storage_backend.js';
14
17
  import { Configuration } from './configuration.js';
18
+ import { ActorInputError } from './errors.js';
15
19
  import { getDefaultsFromInputSchema, noActorInputSchemaDefinedMarker, readInputSchema } from './input-schemas.js';
16
20
  import { PlatformEventManager } from './platform_event_manager.js';
17
21
  import { ProxyConfiguration } from './proxy_configuration.js';
18
22
  import { SmartApifyStorageBackend } from './smart_apify_storage_backend.js';
19
- import { checkCrawleeVersion, getSystemInfo, isNonEmptyObject, printOutdatedSdkWarning, snakeCaseToCamelCase, } from './utils.js';
23
+ import { BINARY_CONTENT_TYPE, checkCrawleeVersion, getSystemInfo, isNonEmptyObject, JSON_CONTENT_TYPE, parseInputValue, printOutdatedSdkWarning, snakeCaseToCamelCase, } from './utils.js';
24
+ /**
25
+ * {@link parseInputValue} with the parser error wrapped as an {@link ActorInputError}, so a malformed
26
+ * JSON input reports where it came from.
27
+ */
28
+ function parseInput(value, contentType, source) {
29
+ try {
30
+ return parseInputValue(value, contentType);
31
+ }
32
+ catch (cause) {
33
+ throw new ActorInputError('PARSE_FAILED', `The input in ${source} is not valid JSON.`, { cause });
34
+ }
35
+ }
20
36
  /**
21
37
  * Exit codes for the Actor process.
22
38
  * The error codes must be in the range 1-128, to avoid collision with signal exits
@@ -797,17 +813,17 @@ export class Actor {
797
813
  /**
798
814
  * Gets the Actor input value from the default {@link KeyValueStore} associated with the current Actor run.
799
815
  *
800
- * This is just a convenient shortcut for [`keyValueStore.getValue('INPUT')`](core/class/KeyValueStore#getValue).
801
- * For example, calling the following code:
802
- * ```js
803
- * const input = await Actor.getInput();
804
- * ```
816
+ * The input is the record stored under the configured input key (`ACTOR_INPUT_KEY`, default `INPUT`).
817
+ * A record stored as `application/octet-stream` — which is what a local input file without an
818
+ * extension is read as — is parsed with JSON5 when it parses, and returned as a `Buffer` otherwise.
805
819
  *
806
- * is equivalent to:
807
- * ```js
808
- * const store = await Actor.openKeyValueStore();
809
- * await store.getValue('INPUT');
810
- * ```
820
+ * When running locally and the store holds no such record, the input is read from a `<inputKey>` or
821
+ * `<inputKey>.json` file in the current working directory instead.
822
+ *
823
+ * Throws an {@link ActorInputError} when no input is found (`code: 'NOT_FOUND'`), when both input
824
+ * files exist in the working directory (`'MULTIPLE_FILES'`), when a JSON input does not parse
825
+ * (`'PARSE_FAILED'`) or when its secret fields cannot be decrypted (`'DECRYPTION_FAILED'`). If your
826
+ * Actor can run without input, catch the `'NOT_FOUND'` case and rethrow the rest.
811
827
  *
812
828
  * Note that the `getInput()` function does not cache the value read from the key-value store.
813
829
  * If you need to use the input multiple times in your Actor,
@@ -819,21 +835,39 @@ export class Actor {
819
835
  * @returns
820
836
  * Returns a promise that resolves to an object, string
821
837
  * or [`Buffer`](https://nodejs.org/api/buffer.html), depending
822
- * on the MIME content type of the record, or `null`
823
- * if the record is missing.
838
+ * on the MIME content type of the record.
839
+ * @throws {ActorInputError} When the input cannot be produced; see `code`.
824
840
  * @ignore
825
841
  */
826
842
  async getInput() {
827
843
  this.ensureActorInit('getInput');
828
- const { inputSecretsPrivateKeyFile, inputSecretsPrivateKeyPassphrase } = this.configuration;
829
- const rawInput = await this.getValue(this.configuration.inputKey);
844
+ const { inputKey, inputSecretsPrivateKeyFile, inputSecretsPrivateKeyPassphrase } = this.configuration;
845
+ const store = await this.openKeyValueStore();
846
+ const record = await store.getRecord(inputKey);
847
+ const rawInput = record
848
+ ? parseInput(record.value, record.contentType, `the "${inputKey}" record of the default key-value store`)
849
+ : await this.readInputFromWorkingDirectory(inputKey);
850
+ if (rawInput == null) {
851
+ const locations = [`the "${inputKey}" record of the default key-value store`];
852
+ if (!this.configuration.isAtHome) {
853
+ locations.push(`a "${inputKey}" or "${inputKey}.json" file in the working directory`);
854
+ }
855
+ throw new ActorInputError('NOT_FOUND', `Input does not exist. Expected ${locations.join(' or ')}.`);
856
+ }
830
857
  let input = rawInput;
831
858
  if (isNonEmptyObject(rawInput) && inputSecretsPrivateKeyFile && inputSecretsPrivateKeyPassphrase) {
832
- const privateKey = createPrivateKey({
833
- key: Buffer.from(inputSecretsPrivateKeyFile, 'base64'),
834
- passphrase: inputSecretsPrivateKeyPassphrase,
835
- });
836
- input = decryptInputSecrets({ input: rawInput, privateKey });
859
+ try {
860
+ const privateKey = createPrivateKey({
861
+ key: Buffer.from(inputSecretsPrivateKeyFile, 'base64'),
862
+ passphrase: inputSecretsPrivateKeyPassphrase,
863
+ });
864
+ input = decryptInputSecrets({ input: rawInput, privateKey });
865
+ }
866
+ catch (cause) {
867
+ throw new ActorInputError('DECRYPTION_FAILED', 'Failed to decrypt the secret fields of the input.', {
868
+ cause,
869
+ });
870
+ }
837
871
  }
838
872
  if (isNonEmptyObject(input) && !Buffer.isBuffer(input)) {
839
873
  input = await this.inferDefaultsFromInputSchema(input);
@@ -841,15 +875,37 @@ export class Actor {
841
875
  return input;
842
876
  }
843
877
  /**
844
- * Gets the Actor input value just like the {@link Actor.getInput} method,
845
- * but throws if it is not found.
878
+ * Reads the input from a `<inputKey>` or `<inputKey>.json` file in the working directory, for local runs
879
+ * whose default key-value store holds no input record. Mirrors how `ApifyFileSystemStorageBackend` adopts
880
+ * such files inside the store directory: the `.json` file is JSON, the bare one is bytes that
881
+ * {@link parseInputValue} tries as JSON5. Both files present is an error rather than a guess.
882
+ *
883
+ * @returns `undefined` when running on the platform or when neither file exists.
846
884
  */
847
- async getInputOrThrow() {
848
- const input = await this.getInput();
849
- if (input == null) {
850
- throw new Error('Input does not exist');
885
+ async readInputFromWorkingDirectory(inputKey) {
886
+ if (this.configuration.isAtHome) {
887
+ return undefined;
851
888
  }
852
- return input;
889
+ const candidates = [
890
+ { filename: inputKey, contentType: BINARY_CONTENT_TYPE },
891
+ { filename: `${inputKey}.json`, contentType: JSON_CONTENT_TYPE },
892
+ ];
893
+ const found = [];
894
+ for (const candidate of candidates) {
895
+ const path = join(process.cwd(), candidate.filename);
896
+ const stats = await stat(path).catch(() => null);
897
+ if (stats?.isFile()) {
898
+ found.push({ ...candidate, path });
899
+ }
900
+ }
901
+ if (found.length > 1) {
902
+ throw new ActorInputError('MULTIPLE_FILES', `Found multiple input files in the working directory: ${found.map((file) => `"${file.filename}"`).join(', ')}. Keep only one of them.`);
903
+ }
904
+ if (found.length === 0) {
905
+ return undefined;
906
+ }
907
+ const [file] = found;
908
+ return parseInput(await readFile(file.path), file.contentType, `the "${file.filename}" file in the working directory`);
853
909
  }
854
910
  /**
855
911
  * Opens a key-value store and returns a promise resolving to an instance of the {@link KeyValueStore} class.
@@ -1463,16 +1519,28 @@ export class Actor {
1463
1519
  /**
1464
1520
  * Gets the Actor input value from the default {@link KeyValueStore} associated with the current Actor run.
1465
1521
  *
1466
- * This is just a convenient shortcut for {@link KeyValueStore.getValue | `keyValueStore.getValue('INPUT')`}.
1467
- * For example, calling the following code:
1522
+ * The input is the record stored under the configured input key (`ACTOR_INPUT_KEY`, default `INPUT`):
1468
1523
  * ```js
1469
1524
  * const input = await Actor.getInput();
1470
1525
  * ```
1471
1526
  *
1472
- * is equivalent to:
1527
+ * A record stored as `application/octet-stream` — which is what a local input file without an
1528
+ * extension is read as — is parsed with JSON5 when it parses, and returned as a `Buffer` otherwise.
1529
+ *
1530
+ * When running locally and the store holds no such record, the input is read from a `<inputKey>` or
1531
+ * `<inputKey>.json` file in the current working directory instead.
1532
+ *
1533
+ * Throws an {@link ActorInputError} when no input is found (`code: 'NOT_FOUND'`), when both input
1534
+ * files exist in the working directory (`'MULTIPLE_FILES'`), when a JSON input does not parse
1535
+ * (`'PARSE_FAILED'`) or when its secret fields cannot be decrypted (`'DECRYPTION_FAILED'`). If your
1536
+ * Actor can run without input, catch the `'NOT_FOUND'` case and rethrow the rest:
1473
1537
  * ```js
1474
- * const store = await Actor.openKeyValueStore();
1475
- * await store.getValue('INPUT');
1538
+ * let input = {};
1539
+ * try {
1540
+ * input = await Actor.getInput();
1541
+ * } catch (error) {
1542
+ * if (!(error instanceof ActorInputError) || error.code !== 'NOT_FOUND') throw error;
1543
+ * }
1476
1544
  * ```
1477
1545
  *
1478
1546
  * Note that the `getInput()` function does not cache the value read from the key-value store.
@@ -1484,19 +1552,12 @@ export class Actor {
1484
1552
  * @returns
1485
1553
  * Returns a promise that resolves to an object, string
1486
1554
  * or [`Buffer`](https://nodejs.org/api/buffer.html), depending
1487
- * on the MIME content type of the record, or `null`
1488
- * if the record is missing.
1555
+ * on the MIME content type of the record.
1556
+ * @throws {ActorInputError} When the input cannot be produced; see `code`.
1489
1557
  */
1490
1558
  static async getInput() {
1491
1559
  return Actor.getDefaultInstance().getInput();
1492
1560
  }
1493
- /**
1494
- * Gets the Actor input value just like the {@link Actor.getInput} method,
1495
- * but throws if it is not found.
1496
- */
1497
- static async getInputOrThrow() {
1498
- return Actor.getDefaultInstance().getInputOrThrow();
1499
- }
1500
1561
  /**
1501
1562
  * Opens a key-value store and returns a promise resolving to an instance of the {@link KeyValueStore} class.
1502
1563
  *
@@ -1668,10 +1729,27 @@ export class Actor {
1668
1729
  configuration: this.configuration,
1669
1730
  requestQueueAccess: this.#requestQueueAccess,
1670
1731
  }), charging),
1671
- localStorageBackend: new ChargingStorageBackend(new ServiceLocator(this.configuration).getStorageBackend(), charging),
1732
+ localStorageBackend: new ChargingStorageBackend(this.createLocalStorageBackend(), charging),
1672
1733
  configuration: this.configuration,
1673
1734
  }));
1674
1735
  }
1736
+ /**
1737
+ * The same choice crawlee's `ServiceLocator` makes for its implicit default backend, with the
1738
+ * input-aware {@link ApifyFileSystemStorageBackend} in place of crawlee's — crawlee has no
1739
+ * notion of a run input.
1740
+ */
1741
+ createLocalStorageBackend() {
1742
+ const { persistStorage, storageDir, inputKey } = this.configuration;
1743
+ const logger = serviceLocator.getLogger();
1744
+ if (!persistStorage) {
1745
+ return new MemoryStorageBackend({ logger: logger.child({ prefix: 'MemoryStorageBackend' }) });
1746
+ }
1747
+ return new ApifyFileSystemStorageBackend({
1748
+ localDataDirectory: storageDir,
1749
+ inputKey,
1750
+ logger: logger.child({ prefix: 'ApifyFileSystemStorageBackend' }),
1751
+ });
1752
+ }
1675
1753
  /** Whether items reaching the run's default dataset are counted for pay-per-event charging. */
1676
1754
  #chargesDefaultDatasetItems() {
1677
1755
  const installed = serviceLocator.getStorageBackend();
@@ -0,0 +1,34 @@
1
+ import type { AdoptionCandidate, FileSystemStorageOptions, KeyValueStoreHookOptions, PurgeableKeyValueStoreBackend } from '@crawlee/fs-storage';
2
+ import { FileSystemStorageBackend } from '@crawlee/fs-storage';
3
+ export interface ApifyFileSystemStorageOptions extends FileSystemStorageOptions {
4
+ /**
5
+ * The key the Actor input is read from — `Configuration.inputKey` (`ACTOR_INPUT_KEY`).
6
+ *
7
+ * `INPUT` is treated as an input key regardless of this setting, so a project's `INPUT.json` stays
8
+ * intact when the Apify CLI redirects the run to a temporary key.
9
+ *
10
+ * @default 'INPUT'
11
+ */
12
+ inputKey?: string;
13
+ }
14
+ /**
15
+ * Crawlee's file-system storage backend, aware of the Actor input.
16
+ *
17
+ * Locally the input arrives in the default key-value store as a bare `INPUT` or `INPUT.json` file with
18
+ * no metadata sidecar — written by the Apify CLI, a project template, or by hand. This backend adopts
19
+ * such a file as the record under the input key when the store is opened (both files present fails the
20
+ * open), and keeps the input keys when the default store is purged on start. Everything else is
21
+ * inherited from {@link FileSystemStorageBackend}.
22
+ *
23
+ * `Actor` installs it as the local backend; pass an instance to `Actor.init({ storage })` only if you
24
+ * need to construct it yourself.
25
+ */
26
+ export declare class ApifyFileSystemStorageBackend extends FileSystemStorageBackend {
27
+ /** `INPUT` plus the configured input key, deduplicated. */
28
+ private readonly inputKeys;
29
+ constructor(options: ApifyFileSystemStorageOptions);
30
+ /** Claims a bare `<key>` or `<key>.json` in the default store for each input key, ahead of the generic sweep. */
31
+ protected keyValueStoreAdoptionCandidates(options: KeyValueStoreHookOptions): AdoptionCandidate[];
32
+ /** Spares the input keys in the default store; every other run-scoped store is emptied. */
33
+ protected purgeKeyValueStore(store: PurgeableKeyValueStoreBackend, options: KeyValueStoreHookOptions): Promise<void>;
34
+ }
@@ -0,0 +1,41 @@
1
+ import { FileSystemStorageBackend } from '@crawlee/fs-storage';
2
+ import { KEY_VALUE_STORE_KEYS } from '@apify/consts';
3
+ import { BINARY_CONTENT_TYPE, JSON_CONTENT_TYPE } from './utils.js';
4
+ /**
5
+ * Crawlee's file-system storage backend, aware of the Actor input.
6
+ *
7
+ * Locally the input arrives in the default key-value store as a bare `INPUT` or `INPUT.json` file with
8
+ * no metadata sidecar — written by the Apify CLI, a project template, or by hand. This backend adopts
9
+ * such a file as the record under the input key when the store is opened (both files present fails the
10
+ * open), and keeps the input keys when the default store is purged on start. Everything else is
11
+ * inherited from {@link FileSystemStorageBackend}.
12
+ *
13
+ * `Actor` installs it as the local backend; pass an instance to `Actor.init({ storage })` only if you
14
+ * need to construct it yourself.
15
+ */
16
+ export class ApifyFileSystemStorageBackend extends FileSystemStorageBackend {
17
+ /** `INPUT` plus the configured input key, deduplicated. */
18
+ inputKeys;
19
+ constructor(options) {
20
+ const { inputKey = KEY_VALUE_STORE_KEYS.INPUT, ...fileSystemOptions } = options;
21
+ super(fileSystemOptions);
22
+ this.inputKeys = [...new Set([KEY_VALUE_STORE_KEYS.INPUT, inputKey])];
23
+ }
24
+ /** Claims a bare `<key>` or `<key>.json` in the default store for each input key, ahead of the generic sweep. */
25
+ keyValueStoreAdoptionCandidates(options) {
26
+ const inputCandidates = options.isDefaultStore
27
+ ? this.inputKeys.map((key) => ({
28
+ key,
29
+ files: [
30
+ { filename: key, contentType: BINARY_CONTENT_TYPE },
31
+ { filename: `${key}.json`, contentType: JSON_CONTENT_TYPE },
32
+ ],
33
+ }))
34
+ : [];
35
+ return [...inputCandidates, ...super.keyValueStoreAdoptionCandidates(options)];
36
+ }
37
+ /** Spares the input keys in the default store; every other run-scoped store is emptied. */
38
+ async purgeKeyValueStore(store, options) {
39
+ await (options.isDefaultStore ? store.purgeExcept([...this.inputKeys]) : store.purge());
40
+ }
41
+ }
@@ -9,7 +9,6 @@ export declare const apifyConfigFields: {
9
9
  persistStorage: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodBoolean, unknown>>>;
10
10
  storageDir: ConfigField<z.ZodDefault<z.ZodString>>;
11
11
  containerized: ConfigField<z.ZodOptional<z.ZodPreprocess<z.ZodBoolean, unknown>>>;
12
- inputKey: ConfigField<z.ZodDefault<z.ZodString>>;
13
12
  memoryMbytes: ConfigField<z.ZodOptional<z.ZodPreprocess<z.ZodNumber, unknown>>>;
14
13
  availableMemoryRatio: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodNumber, unknown>>>;
15
14
  disableBrowserSandbox: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodBoolean, unknown>>>;
@@ -22,6 +21,7 @@ export declare const apifyConfigFields: {
22
21
  defaultDatasetId: ConfigField<z.ZodDefault<z.ZodString>>;
23
22
  defaultKeyValueStoreId: ConfigField<z.ZodDefault<z.ZodString>>;
24
23
  defaultRequestQueueId: ConfigField<z.ZodDefault<z.ZodString>>;
24
+ inputKey: 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,7 +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
- inputKey: withApifyEnv(crawleeConfigFields.inputKey, [ACTOR_ENV_VARS.INPUT_KEY, APIFY_ENV_VARS.INPUT_KEY]),
24
23
  memoryMbytes: withApifyEnv(crawleeConfigFields.memoryMbytes, [
25
24
  ACTOR_ENV_VARS.MEMORY_MBYTES,
26
25
  APIFY_ENV_VARS.MEMORY_MBYTES,
@@ -51,6 +50,13 @@ export const apifyConfigFields = {
51
50
  APIFY_ENV_VARS.DEFAULT_REQUEST_QUEUE_ID,
52
51
  ]),
53
52
  // Apify-specific fields
53
+ // Crawlee has no notion of a run input; `CRAWLEE_INPUT_KEY` stays as the
54
+ // lowest-priority alias because the Apify CLI sets all three variables.
55
+ inputKey: field(z.string().default('INPUT'), [
56
+ ACTOR_ENV_VARS.INPUT_KEY,
57
+ APIFY_ENV_VARS.INPUT_KEY,
58
+ 'CRAWLEE_INPUT_KEY',
59
+ ]),
54
60
  metamorphAfterSleepMillis: field(coerceNumber.default(300_000), APIFY_ENV_VARS.METAMORPH_AFTER_SLEEP_MILLIS),
55
61
  actorEventsWsUrl: field(z.string().optional(), [
56
62
  ACTOR_ENV_VARS.EVENTS_WEBSOCKET_URL,
@@ -0,0 +1,31 @@
1
+ /** Why {@link Actor.getInput} could not produce the input. */
2
+ export type ActorInputErrorCode =
3
+ /** No input record in the default key-value store, and locally no input file in the working directory either. */
4
+ 'NOT_FOUND'
5
+ /** Both `<inputKey>` and `<inputKey>.json` exist in the working directory, so neither is picked. */
6
+ | 'MULTIPLE_FILES'
7
+ /** The secret fields of the input could not be decrypted; `cause` holds the underlying error. */
8
+ | 'DECRYPTION_FAILED'
9
+ /** A JSON input record or file does not parse; `cause` holds the parser error. */
10
+ | 'PARSE_FAILED';
11
+ /**
12
+ * Thrown by {@link Actor.getInput} when the Actor input cannot be produced. `code` says why, and
13
+ * `cause` carries the underlying error where there is one. Anything else thrown by `getInput()`, such as
14
+ * an Apify API error, is not an input problem and is passed through as is.
15
+ *
16
+ * ```js
17
+ * try {
18
+ * input = await Actor.getInput();
19
+ * } catch (error) {
20
+ * if (!(error instanceof ActorInputError) || error.code !== 'NOT_FOUND') throw error;
21
+ * input = {};
22
+ * }
23
+ * ```
24
+ */
25
+ export declare class ActorInputError extends Error {
26
+ readonly name = "ActorInputError";
27
+ readonly code: ActorInputErrorCode;
28
+ constructor(code: ActorInputErrorCode, message: string, options?: {
29
+ cause?: unknown;
30
+ });
31
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Thrown by {@link Actor.getInput} when the Actor input cannot be produced. `code` says why, and
3
+ * `cause` carries the underlying error where there is one. Anything else thrown by `getInput()`, such as
4
+ * an Apify API error, is not an input problem and is passed through as is.
5
+ *
6
+ * ```js
7
+ * try {
8
+ * input = await Actor.getInput();
9
+ * } catch (error) {
10
+ * if (!(error instanceof ActorInputError) || error.code !== 'NOT_FOUND') throw error;
11
+ * input = {};
12
+ * }
13
+ * ```
14
+ */
15
+ export class ActorInputError extends Error {
16
+ name = 'ActorInputError';
17
+ code;
18
+ constructor(code, message, options) {
19
+ super(message, options);
20
+ this.code = code;
21
+ }
22
+ }
package/dist/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  export * from './actor.js';
2
2
  export { ApifyStorageBackend, type ApifyStorageBackendOptions } from './apify_storage_backend.js';
3
+ export { ApifyFileSystemStorageBackend, type ApifyFileSystemStorageOptions, } from './apify_file_system_storage_backend.js';
3
4
  export type { RequestQueueAccessMode } from './apify_request_queue_backend.js';
4
5
  export { ArgumentValidationError } from './utils.js';
6
+ export { ActorInputError, type ActorInputErrorCode } from './errors.js';
5
7
  export { createTransformRequestFunction, type GlobInput, type PseudoUrlInput, type UrlPatternFilters, type UrlPatternRequestOptions, } from './enqueue_links_filters.js';
6
8
  export type { OpenStorageOptions, StorageAlias, StorageId, StorageName, StorageIdentifier } from './storage.js';
7
9
  export { ChargeOptions, ChargeResult, ActorPricingInfo, ChargingManager } from './charging.js';
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  export * from './actor.js';
2
2
  export { ApifyStorageBackend } from './apify_storage_backend.js';
3
+ export { ApifyFileSystemStorageBackend, } from './apify_file_system_storage_backend.js';
3
4
  export { ArgumentValidationError } from './utils.js';
5
+ export { ActorInputError } from './errors.js';
4
6
  export { createTransformRequestFunction, } from './enqueue_links_filters.js';
5
7
  export { ChargingManager } from './charging.js';
6
8
  export * from './configuration.js';
package/dist/utils.d.ts CHANGED
@@ -5,6 +5,17 @@ export { ArgumentValidationError } from '@apify/validations';
5
5
  * @internal
6
6
  */
7
7
  export declare function isNonEmptyObject(value: unknown): value is Record<string, unknown>;
8
+ /** Content type of a JSON record, as `KeyValueStore` and the file-system adoption write it. */
9
+ export declare const JSON_CONTENT_TYPE = "application/json; charset=utf-8";
10
+ /** Content type of raw bytes, which is what a local input file without an extension is adopted as. */
11
+ export declare const BINARY_CONTENT_TYPE = "application/octet-stream";
12
+ /**
13
+ * Parses a raw input record the way `KeyValueStore.getValue` would, with one exception:
14
+ * `application/octet-stream` — the type a local input file without an extension is adopted as — is
15
+ * tried as JSON5 first, so such a file still yields an object. Bytes that do not parse come back as they are.
16
+ * @internal
17
+ */
18
+ export declare function parseInputValue(value: Buffer | ArrayBuffer, contentType: string | null): unknown;
8
19
  /**
9
20
  * Converts a `SNAKE_CASE` string to `camelCase` (previously provided by `@crawlee/utils`).
10
21
  * @internal
package/dist/utils.js CHANGED
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { createRequire } from 'node:module';
3
3
  import { type } from 'node:os';
4
4
  import { normalize } from 'node:path';
5
+ import { parseValue } from '@crawlee/core';
5
6
  // @ts-ignore if we enable resolveJsonModule, we end up with `src` folder in `dist`
6
7
  import crawleePkgJson from '@crawlee/core/package.json' with { type: 'json' };
7
8
  // @ts-ignore if we enable resolveJsonModule, we end up with `src` folder in `dist`
@@ -21,6 +22,28 @@ const require = createRequire(import.meta.url);
21
22
  export function isNonEmptyObject(value) {
22
23
  return typeof value === 'object' && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
23
24
  }
25
+ /** Content type of a JSON record, as `KeyValueStore` and the file-system adoption write it. */
26
+ export const JSON_CONTENT_TYPE = 'application/json; charset=utf-8';
27
+ /** Content type of raw bytes, which is what a local input file without an extension is adopted as. */
28
+ export const BINARY_CONTENT_TYPE = 'application/octet-stream';
29
+ /**
30
+ * Parses a raw input record the way `KeyValueStore.getValue` would, with one exception:
31
+ * `application/octet-stream` — the type a local input file without an extension is adopted as — is
32
+ * tried as JSON5 first, so such a file still yields an object. Bytes that do not parse come back as they are.
33
+ * @internal
34
+ */
35
+ export function parseInputValue(value, contentType) {
36
+ const mediaType = contentType?.split(';')[0].trim().toLowerCase();
37
+ if (mediaType === BINARY_CONTENT_TYPE) {
38
+ try {
39
+ return parseValue(value, JSON_CONTENT_TYPE);
40
+ }
41
+ catch {
42
+ return value;
43
+ }
44
+ }
45
+ return parseValue(value, contentType);
46
+ }
24
47
  /**
25
48
  * Converts a `SNAKE_CASE` string to `camelCase` (previously provided by `@crawlee/utils`).
26
49
  * @internal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apify",
3
- "version": "4.0.0-beta.39",
3
+ "version": "4.0.0-beta.41",
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"
@@ -61,9 +61,10 @@
61
61
  "@apify/timeout": "^1.0.1",
62
62
  "@apify/utilities": "^3.0.1",
63
63
  "@apify/validations": "^1.1.0",
64
- "@crawlee/core": ">=4.0.0-beta.194 <4.0.0-rc.0 || >4.0.0-rc.0 <5.0.0",
65
- "@crawlee/types": ">=4.0.0-beta.194 <4.0.0-rc.0 || >4.0.0-rc.0 <5.0.0",
66
- "@crawlee/utils": ">=4.0.0-beta.194 <4.0.0-rc.0 || >4.0.0-rc.0 <5.0.0",
64
+ "@crawlee/core": ">=4.0.0-beta.198 <4.0.0-rc.0 || >4.0.0-rc.0 <5.0.0",
65
+ "@crawlee/fs-storage": ">=4.0.0-beta.198 <4.0.0-rc.0 || >4.0.0-rc.0 <5.0.0",
66
+ "@crawlee/types": ">=4.0.0-beta.198 <4.0.0-rc.0 || >4.0.0-rc.0 <5.0.0",
67
+ "@crawlee/utils": ">=4.0.0-beta.198 <4.0.0-rc.0 || >4.0.0-rc.0 <5.0.0",
67
68
  "apify-client": "^3.0.0-beta.26",
68
69
  "minimatch": "^10.2.6",
69
70
  "semver": "^7.8.5",
@@ -81,7 +82,7 @@
81
82
  "@types/tough-cookie": "^4.0.5",
82
83
  "@types/ws": "^8.18.1",
83
84
  "commitlint": "^21.2.2",
84
- "crawlee": ">=4.0.0-beta.194 <4.0.0-rc.0 || >4.0.0-rc.0 <5.0.0",
85
+ "crawlee": ">=4.0.0-beta.198 <4.0.0-rc.0 || >4.0.0-rc.0 <5.0.0",
85
86
  "husky": "^9.1.7",
86
87
  "lint-staged": "^17.3.0",
87
88
  "oxfmt": "0.64.0",
@@ -97,7 +98,7 @@
97
98
  "devEngines": {
98
99
  "packageManager": {
99
100
  "name": "pnpm",
100
- "version": "10.33.4",
101
+ "version": "12.6.0",
101
102
  "onFail": "warn"
102
103
  }
103
104
  },