apify 4.0.0-beta.20 → 4.0.0-beta.22

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,5 +1,5 @@
1
1
  import type { EventManager, EventTypeName, RecordOptions, UseStateOptions } from '@crawlee/core';
2
- import { Dataset, RequestQueue } from '@crawlee/core';
2
+ import { Dataset, KeyValueStore, RequestQueue } from '@crawlee/core';
3
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';
@@ -9,7 +9,6 @@ import type { ChargeOptions, ChargeResult } from './charging.js';
9
9
  import { ChargingManager } from './charging.js';
10
10
  import type { ConfigurationOptions } from './configuration.js';
11
11
  import { Configuration } from './configuration.js';
12
- import { KeyValueStore } from './key_value_store.js';
13
12
  import type { ProxyConfigurationOptions } from './proxy_configuration.js';
14
13
  import { ProxyConfiguration } from './proxy_configuration.js';
15
14
  import type { OpenStorageOptions, StorageIdentifier, StorageIdentifierWithoutAlias } from './storage.js';
package/dist/actor.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createPrivateKey } from 'node:crypto';
2
- import { Dataset, purgeDefaultStorages, RequestQueue, serviceLocator } from '@crawlee/core';
2
+ import { Dataset, KeyValueStore, purgeDefaultStorages, RequestQueue, serviceLocator } from '@crawlee/core';
3
3
  import { sleep } from '@crawlee/utils';
4
4
  import { ApifyClient } from 'apify-client';
5
5
  import { z } from 'zod';
@@ -11,7 +11,6 @@ import { ApifyStorageBackend, pushDataChargingContext, USES_PUSH_DATA_INTERCEPTI
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';
@@ -634,7 +633,7 @@ export class Actor {
634
633
  }
635
634
  const dataset = await this.openDataset();
636
635
  // Two code paths for charging:
637
- // 1. Intercepted client: PatchedDatasetClient intercepts pushItems() calls, handling charging
636
+ // 1. Intercepted client: PpeAwareDatasetClient intercepts pushItems() calls, handling charging
638
637
  // internally. This is needed because Crawlee's Dataset may call pushItems() directly,
639
638
  // bypassing Actor.pushData(). We propagate eventName via AsyncLocalStorage context.
640
639
  // 2. Direct charging: When using a non-patched client (e.g., forceCloud option or custom client),
@@ -1579,7 +1578,7 @@ export class Actor {
1579
1578
  return Boolean(dataset.backend[USES_PUSH_DATA_INTERCEPTION]);
1580
1579
  }
1581
1580
  async pushDataViaInterceptedClient(dataset, item, eventName) {
1582
- // PatchedDatasetClient will handle charging and item limiting.
1581
+ // PpeAwareDatasetClient will handle charging and item limiting.
1583
1582
  // We only need to propagate `eventName` and (optionally) return aggregated charge info.
1584
1583
  const context = {
1585
1584
  eventName,
@@ -2,8 +2,9 @@ import type { DatasetBackend, DatasetBackendListOptions, DatasetInfo, Dictionary
2
2
  import type { DatasetClient } from 'apify-client';
3
3
  /**
4
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`).
5
+ * dataset API. Mostly a thin method-mapping wrapper (`getMetadata`/`get`, `drop`/`delete`,
6
+ * `getData`/`listItems`), except `pushData`, which also splits large pushes into chunks
7
+ * fitting the API's payload size limit.
7
8
  *
8
9
  * @internal
9
10
  */
@@ -1,7 +1,14 @@
1
+ import { MAX_PAYLOAD_SIZE_BYTES } from '@apify/consts';
2
+ /** Slight reduction of the API's 9MB payload limit, to stay safely below it. */
3
+ const SAFETY_BUFFER_PERCENT = 0.01 / 100; // 0.01%
4
+ const EFFECTIVE_LIMIT_BYTES = MAX_PAYLOAD_SIZE_BYTES - Math.ceil(MAX_PAYLOAD_SIZE_BYTES * SAFETY_BUFFER_PERCENT);
5
+ /** Per-item ceiling — 2 bytes under the chunk limit, so even a lone item fits its `[]` wrapper. */
6
+ const MAX_ITEM_BYTES = EFFECTIVE_LIMIT_BYTES - 2;
1
7
  /**
2
8
  * 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`).
9
+ * dataset API. Mostly a thin method-mapping wrapper (`getMetadata`/`get`, `drop`/`delete`,
10
+ * `getData`/`listItems`), except `pushData`, which also splits large pushes into chunks
11
+ * fitting the API's payload size limit.
5
12
  *
6
13
  * @internal
7
14
  */
@@ -25,9 +32,46 @@ export class ApifyDatasetBackend {
25
32
  'Use `drop()` to delete the dataset entirely, or open a new dataset instead.');
26
33
  }
27
34
  async pushData(items) {
28
- await this.client.pushItems(items);
35
+ // The platform API rejects payloads over 9MB — split the items into chunks
36
+ // that fit, pushed sequentially to preserve item order.
37
+ const payloads = items.map((item, index) => serializeToSizeLimit(item, index));
38
+ for (const chunk of chunkBySize(payloads, EFFECTIVE_LIMIT_BYTES)) {
39
+ await this.client.pushItems(chunk);
40
+ }
29
41
  }
30
42
  async getData(options) {
31
43
  return await this.client.listItems(options);
32
44
  }
33
45
  }
46
+ /** Serializes a dataset item, throwing if it alone exceeds the payload size limit. */
47
+ function serializeToSizeLimit(item, index) {
48
+ const payload = JSON.stringify(item);
49
+ const bytes = Buffer.byteLength(payload);
50
+ if (bytes > MAX_ITEM_BYTES) {
51
+ throw new Error(`Data item at index ${index} is too large (size: ${bytes} bytes, limit: ${MAX_ITEM_BYTES} bytes)`);
52
+ }
53
+ return payload;
54
+ }
55
+ /**
56
+ * Takes an array of JSON-serialized items and groups them into JSON array strings
57
+ * of at most `limitBytes` each, preserving item order. Assumes (and does not
58
+ * validate) that no single item exceeds the limit.
59
+ */
60
+ function chunkBySize(payloads, limitBytes) {
61
+ const chunks = [];
62
+ let chunkBytes = Infinity; // Forces the first item to open a new chunk.
63
+ for (const payload of payloads) {
64
+ const bytes = Buffer.byteLength(payload);
65
+ if (chunkBytes + bytes + 1 <= limitBytes) {
66
+ // Fits into the current chunk — add 1 byte for the ',' separator.
67
+ chunks[chunks.length - 1].push(payload);
68
+ chunkBytes += bytes + 1;
69
+ }
70
+ else {
71
+ // Open a new chunk — add 2 bytes for the '[]' wrapper.
72
+ chunks.push([payload]);
73
+ chunkBytes = bytes + 2;
74
+ }
75
+ }
76
+ return chunks.map((chunk) => `[${chunk.join(',')}]`);
77
+ }
@@ -11,7 +11,8 @@ export declare const USES_PUSH_DATA_INTERCEPTION: unique symbol;
11
11
  * Context of a single `Actor.pushData()` call, shared with the intercepted
12
12
  * `pushItems()` calls so they can (1) know which event to charge and
13
13
  * (2) aggregate the {@link ChargeResult} across the multiple `pushItems()`
14
- * calls a single `pushData()` may trigger (Crawlee batches large pushes).
14
+ * calls a single `pushData()` may trigger (the backend splits pushes exceeding
15
+ * the API's payload size limit).
15
16
  */
16
17
  export interface PpeAwarePushDataContext {
17
18
  eventName: string | undefined;
package/dist/index.d.ts CHANGED
@@ -7,7 +7,6 @@ export { ChargeOptions, ChargeResult, ActorPricingInfo, ChargingManager } from '
7
7
  export * from './configuration.js';
8
8
  export * from './proxy_configuration.js';
9
9
  export * from './platform_event_manager.js';
10
- export * from './key_value_store.js';
11
- export { Dataset, DatasetDataOptions, DatasetIteratorOptions, DatasetConsumer, DatasetMapper, DatasetReducer, DatasetOptions, DatasetContent, RequestQueue, RequestQueueOperationOptions, RequestQueueOptions, KeyConsumer, KeyValueStoreOptions, RecordOptions, KeyValueStoreIteratorOptions, log, Log, LoggerOptions, LogLevel, Logger, LoggerJson, LoggerText, } from '@crawlee/core';
10
+ export { Dataset, DatasetDataOptions, DatasetIteratorOptions, DatasetConsumer, DatasetMapper, DatasetReducer, DatasetOptions, DatasetContent, RequestQueue, RequestQueueOperationOptions, RequestQueueOptions, KeyValueStore, KeyConsumer, KeyValueStoreOptions, RecordOptions, KeyValueStoreIteratorOptions, log, Log, LoggerOptions, LogLevel, Logger, LoggerJson, LoggerText, } from '@crawlee/core';
12
11
  export type { QueueOperationInfo } from '@crawlee/types';
13
12
  export { ApifyClient, ApifyClientOptions } from 'apify-client';
package/dist/index.js CHANGED
@@ -5,6 +5,5 @@ export { ChargingManager } from './charging.js';
5
5
  export * from './configuration.js';
6
6
  export * from './proxy_configuration.js';
7
7
  export * from './platform_event_manager.js';
8
- export * from './key_value_store.js';
9
- export { Dataset, RequestQueue, log, Log, LogLevel, Logger, LoggerJson, LoggerText, } from '@crawlee/core';
8
+ export { Dataset, RequestQueue, KeyValueStore, log, Log, LogLevel, Logger, LoggerJson, LoggerText, } from '@crawlee/core';
10
9
  export { ApifyClient } from 'apify-client';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apify",
3
- "version": "4.0.0-beta.20",
3
+ "version": "4.0.0-beta.22",
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"
@@ -1,21 +0,0 @@
1
- import type { StorageOpenOptions } from '@crawlee/core';
2
- import { KeyValueStore as CoreKeyValueStore } from '@crawlee/core';
3
- /**
4
- * @inheritDoc
5
- */
6
- export declare class KeyValueStore extends CoreKeyValueStore {
7
- /**
8
- * Returns a URL for the given key that may be used to publicly
9
- * access the value in the remote key-value store.
10
- *
11
- * On the Apify platform the URL is signed with the store's
12
- * `urlSigningSecretKey` so that anyone with the URL can read the record
13
- * without authentication. Locally we delegate to crawlee's default
14
- * implementation (which produces a `file://` URL or returns `undefined`).
15
- */
16
- getPublicUrl(key: string): Promise<string | undefined>;
17
- /**
18
- * @inheritDoc
19
- */
20
- static open(storeIdOrName?: string | null, options?: StorageOpenOptions): Promise<KeyValueStore>;
21
- }
@@ -1,40 +0,0 @@
1
- import { KeyValueStore as CoreKeyValueStore, serviceLocator } from '@crawlee/core';
2
- import { createHmacSignature } from '@apify/utilities';
3
- import { ApifyKeyValueStoreBackend } from './apify_key_value_store_backend.js';
4
- /**
5
- * @inheritDoc
6
- */
7
- export class KeyValueStore extends CoreKeyValueStore {
8
- /**
9
- * Returns a URL for the given key that may be used to publicly
10
- * access the value in the remote key-value store.
11
- *
12
- * On the Apify platform the URL is signed with the store's
13
- * `urlSigningSecretKey` so that anyone with the URL can read the record
14
- * without authentication. Locally we delegate to crawlee's default
15
- * implementation (which produces a `file://` URL or returns `undefined`).
16
- */
17
- async getPublicUrl(key) {
18
- const config = serviceLocator.getConfiguration();
19
- // Detect a remote (Apify) store by its backend type rather than by
20
- // `isAtHome`, so that a `forceCloud` store opened locally still gets a
21
- // signed Apify URL (matching the platform behaviour). `backend` is
22
- // `private` on `CoreKeyValueStore`, so bypass the visibility check.
23
- const { backend } = this;
24
- if (!(backend instanceof ApifyKeyValueStoreBackend)) {
25
- return super.getPublicUrl(key);
26
- }
27
- const publicUrl = new URL(`${config.apiPublicBaseUrl}/v2/key-value-stores/${this.id}/records/${key}`);
28
- const metadata = (await backend.getMetadata());
29
- if (metadata?.urlSigningSecretKey) {
30
- publicUrl.searchParams.append('signature', createHmacSignature(metadata.urlSigningSecretKey, key));
31
- }
32
- return publicUrl.toString();
33
- }
34
- /**
35
- * @inheritDoc
36
- */
37
- static async open(storeIdOrName, options = {}) {
38
- return super.open(storeIdOrName, options);
39
- }
40
- }