apify-client 3.0.0-beta.19 → 3.0.0-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundle.js +3 -3
- package/dist/bundle.js.map +1 -1
- package/dist/http_client.js +4 -3
- package/dist/interceptors.js +5 -0
- package/dist/resource_clients/request_queue.d.ts +11 -2
- package/dist/resource_clients/request_queue.js +26 -13
- package/dist/utils.d.ts +14 -2
- package/dist/utils.js +26 -19
- package/package.json +1 -1
package/dist/http_client.js
CHANGED
|
@@ -54,9 +54,10 @@ export class HttpClient {
|
|
|
54
54
|
return new URLSearchParams(formattedParams).toString();
|
|
55
55
|
},
|
|
56
56
|
validateStatus: null,
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
// Interceptors serialize requests and parse responses instead. Empty arrays rather than `undefined`,
|
|
58
|
+
// which axios fills in with its default transforms.
|
|
59
|
+
transformRequest: [],
|
|
60
|
+
transformResponse: [],
|
|
60
61
|
responseType: 'arraybuffer',
|
|
61
62
|
// Every request sets its own timeout in `createRequestHandler`, so the default only backs a raw
|
|
62
63
|
// `axios.request()` call.
|
package/dist/interceptors.js
CHANGED
|
@@ -31,6 +31,11 @@ function getHeader(config, name) {
|
|
|
31
31
|
return typeof value === 'string' ? value : undefined;
|
|
32
32
|
}
|
|
33
33
|
function serializeRequest(config) {
|
|
34
|
+
// A string body with an explicit content type is already serialized and goes out as it is. The axios default
|
|
35
|
+
// transform would otherwise parse a JSON one in full just to check that it is valid, which for a body assembled
|
|
36
|
+
// from thousands of pre-serialized requests costs about as much as serializing them did.
|
|
37
|
+
if (typeof config.data === 'string' && getHeader(config, 'content-type'))
|
|
38
|
+
return config;
|
|
34
39
|
const [defaultTransform] = axios.defaults.transformRequest;
|
|
35
40
|
// The function not only serializes data, but it also adds correct headers.
|
|
36
41
|
const data = defaultTransform(config.data, config.headers);
|
|
@@ -3,6 +3,15 @@ import type { ApiClientSubResourceOptions } from '../base/api_client.js';
|
|
|
3
3
|
import { ResourceClient } from '../base/resource_client.js';
|
|
4
4
|
import type { RequestQueue, RequestQueueClientAddRequestResult, RequestQueueClientBatchDeleteRequestsResult, RequestQueueClientBatchRequestsOperationResult, RequestQueueClientListAndLockHeadResult, RequestQueueClientListHeadResult, RequestQueueClientListRequestsResult, RequestQueueClientProlongRequestLockResult, RequestQueueClientRequestSchema, RequestQueueClientRequestToAdd, RequestQueueClientRequestToUpdate, RequestQueueClientUnlockRequestsResult } from '../models.js';
|
|
5
5
|
import type { TimeoutOptions } from '../timeouts.js';
|
|
6
|
+
/**
|
|
7
|
+
* A request to add, serialized once up front: `byteLength` decides which batch it goes into, `json` is what the body of
|
|
8
|
+
* that batch is assembled from, and `request` is what the result bookkeeping needs.
|
|
9
|
+
*/
|
|
10
|
+
interface SerializedRequestToAdd {
|
|
11
|
+
request: RequestQueueClientRequestToAdd;
|
|
12
|
+
json: string;
|
|
13
|
+
byteLength: number;
|
|
14
|
+
}
|
|
6
15
|
export type { AllowedHttpMethods, RequestQueue, RequestQueueClientAddRequestResult, RequestQueueClientBatchDeleteRequestsResult, RequestQueueClientBatchRequestsOperationResult, RequestQueueClientListAndLockHeadResult, RequestQueueClientListHeadResult, RequestQueueClientListItem, RequestQueueClientListRequestsResult, RequestQueueClientLockedListItem, RequestQueueClientProlongRequestLockResult, RequestQueueClientRequestSchema, RequestQueueClientRequestToAdd, RequestQueueClientRequestToUpdate, RequestQueueClientUnlockRequestsResult, RequestQueueStats, } from '../models.js';
|
|
7
16
|
/**
|
|
8
17
|
* Client for managing a specific Request queue.
|
|
@@ -160,8 +169,8 @@ export declare class RequestQueueClient extends ResourceClient {
|
|
|
160
169
|
*
|
|
161
170
|
* @private
|
|
162
171
|
*/
|
|
163
|
-
protected addRequestBatch(requests:
|
|
164
|
-
protected addRequestBatchWithRetries(requests:
|
|
172
|
+
protected addRequestBatch(requests: SerializedRequestToAdd[], options?: RequestQueueClientAddRequestOptions): Promise<RequestQueueClientBatchRequestsOperationResult>;
|
|
173
|
+
protected addRequestBatchWithRetries(requests: SerializedRequestToAdd[], options?: RequestQueueClientBatchAddRequestWithRetriesOptions): Promise<RequestQueueClientBatchRequestsOperationResult>;
|
|
165
174
|
/**
|
|
166
175
|
* Adds multiple requests to the queue in a single operation.
|
|
167
176
|
*
|
|
@@ -5,7 +5,7 @@ import { ResourceClient } from '../base/resource_client.js';
|
|
|
5
5
|
import { ResponseValidationError } from '../response_validation_error.js';
|
|
6
6
|
import * as schemas from '../schemas.js';
|
|
7
7
|
import { optionalTimeoutSchema, timeoutOptionsSchema, timeoutOptionsShape } from '../timeouts.js';
|
|
8
|
-
import { anyObjectSchema, catchNotFoundOrThrow, isNonArrayObject, parseArgument, parseResponse, RequestQueuePaginationIterator,
|
|
8
|
+
import { anyObjectSchema, catchNotFoundOrThrow, isNonArrayObject, parseArgument, parseResponse, RequestQueuePaginationIterator, splitIntoJsonArrayBatches, utf8ByteLength, } from '../utils.js';
|
|
9
9
|
const DEFAULT_PARALLEL_BATCH_ADD_REQUESTS = 5;
|
|
10
10
|
const DEFAULT_UNPROCESSED_RETRIES_BATCH_ADD_REQUESTS = 3;
|
|
11
11
|
const DEFAULT_MIN_DELAY_BETWEEN_UNPROCESSED_REQUESTS_RETRIES_MILLIS = 500;
|
|
@@ -21,7 +21,6 @@ const listAndLockHeadOptionsSchema = z.strictObject({
|
|
|
21
21
|
// every key of every request. `id` is assigned by the API, so a new request must not carry one.
|
|
22
22
|
const newRequestSchema = z.custom((value) => isNonArrayObject(value) && value.id === undefined, 'Expected a request object without an `id`');
|
|
23
23
|
const forefrontOptionsSchema = z.strictObject({ forefront: z.boolean().optional(), ...timeoutOptionsShape });
|
|
24
|
-
const batchAddRequestsSchema = z.array(newRequestSchema).min(1).max(REQUEST_QUEUE_MAX_REQUESTS_PER_BATCH_OPERATION);
|
|
25
24
|
const batchAddRequestsWithRetriesSchema = z.array(newRequestSchema).min(1);
|
|
26
25
|
const optionalBooleanSchema = z.boolean().optional();
|
|
27
26
|
const optionalNumberSchema = z.number().optional();
|
|
@@ -277,13 +276,15 @@ export class RequestQueueClient extends ResourceClient {
|
|
|
277
276
|
* @private
|
|
278
277
|
*/
|
|
279
278
|
async addRequestBatch(requests, options = {}) {
|
|
280
|
-
parseArgument(requests, batchAddRequestsSchema);
|
|
281
279
|
const parsed = parseArgument(options, forefrontOptionsSchema, 'RequestQueueClientAddRequestOptions');
|
|
282
280
|
const response = await this.httpClient.call({
|
|
283
281
|
url: this.buildUrl('requests/batch'),
|
|
284
282
|
method: 'POST',
|
|
285
283
|
timeoutSecs: this.#resolveTimeout(parsed.timeoutSecs, 'medium'),
|
|
286
|
-
|
|
284
|
+
// The body is assembled from the requests as `batchAddRequests` serialized them; the explicit content type
|
|
285
|
+
// makes the request interceptor send the string as it is instead of serializing the requests again.
|
|
286
|
+
headers: { 'content-type': 'application/json' },
|
|
287
|
+
data: `[${requests.map(({ json }) => json).join(',')}]`,
|
|
287
288
|
params: this.buildParams({
|
|
288
289
|
forefront: parsed.forefront,
|
|
289
290
|
clientKey: this.#clientKey,
|
|
@@ -316,7 +317,7 @@ export class RequestQueueClient extends ResourceClient {
|
|
|
316
317
|
// Get unique keys of all requests processed so far
|
|
317
318
|
const processedRequestsUniqueKeys = processedRequests.map(({ uniqueKey }) => uniqueKey);
|
|
318
319
|
// Requests remaining to be processed are the all that remain
|
|
319
|
-
remainingRequests = requests.filter(({
|
|
320
|
+
remainingRequests = requests.filter(({ request }) => !processedRequestsUniqueKeys.includes(request.uniqueKey));
|
|
320
321
|
// Stop if all requests have been processed
|
|
321
322
|
if (remainingRequests.length === 0) {
|
|
322
323
|
break;
|
|
@@ -332,8 +333,8 @@ export class RequestQueueClient extends ResourceClient {
|
|
|
332
333
|
// This ensures that this method does not throw and keeps the signature.
|
|
333
334
|
const processedRequestsUniqueKeys = processedRequests.map(({ uniqueKey }) => uniqueKey);
|
|
334
335
|
unprocessedRequests = requests
|
|
335
|
-
.filter(({
|
|
336
|
-
.map(({ method, uniqueKey, url }) => ({ method, uniqueKey, url }));
|
|
336
|
+
.filter(({ request }) => !processedRequestsUniqueKeys.includes(request.uniqueKey))
|
|
337
|
+
.map(({ request: { method, uniqueKey, url } }) => ({ method, uniqueKey, url }));
|
|
337
338
|
break;
|
|
338
339
|
}
|
|
339
340
|
// Exponential backoff
|
|
@@ -394,12 +395,25 @@ export class RequestQueueClient extends ResourceClient {
|
|
|
394
395
|
const executingRequests = new Set();
|
|
395
396
|
const individualResults = [];
|
|
396
397
|
const payloadSizeLimitBytes = MAX_PAYLOAD_SIZE_BYTES - Math.ceil(MAX_PAYLOAD_SIZE_BYTES * SAFETY_BUFFER_PERCENT);
|
|
398
|
+
// Serialize every request once: the byte lengths decide the batch boundaries, and the same strings are joined
|
|
399
|
+
// into the batch bodies, so nothing is stringified a second time when it is sent.
|
|
400
|
+
const serializedRequests = requests.map((request, index) => {
|
|
401
|
+
const json = JSON.stringify(request);
|
|
402
|
+
const byteLength = utf8ByteLength(json);
|
|
403
|
+
// Two more bytes for the brackets, which even a batch of one request carries.
|
|
404
|
+
if (byteLength + 2 > payloadSizeLimitBytes) {
|
|
405
|
+
throw new Error(`RequestQueueClient.batchAddRequests: The size of the request with index: ${index} ` +
|
|
406
|
+
`exceeds the maximum allowed size (${payloadSizeLimitBytes} bytes).`);
|
|
407
|
+
}
|
|
408
|
+
return { request, json, byteLength };
|
|
409
|
+
});
|
|
410
|
+
const batches = splitIntoJsonArrayBatches(serializedRequests, {
|
|
411
|
+
maxCount: REQUEST_QUEUE_MAX_REQUESTS_PER_BATCH_OPERATION,
|
|
412
|
+
maxByteLength: payloadSizeLimitBytes,
|
|
413
|
+
});
|
|
397
414
|
// Keep a pool of up to `maxParallel` requests running at once
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
const slicedRequests = requests.slice(i, i + REQUEST_QUEUE_MAX_REQUESTS_PER_BATCH_OPERATION);
|
|
401
|
-
const requestsInBatch = sliceArrayByByteLength(slicedRequests, payloadSizeLimitBytes, i);
|
|
402
|
-
const requestPromise = this.addRequestBatchWithRetries(requestsInBatch, options);
|
|
415
|
+
for (const batch of batches) {
|
|
416
|
+
const requestPromise = this.addRequestBatchWithRetries(batch, options);
|
|
403
417
|
executingRequests.add(requestPromise);
|
|
404
418
|
// A rejection reaches the caller through the awaits below; this bookkeeping chain only has to avoid
|
|
405
419
|
// turning it into an unhandled one of its own.
|
|
@@ -410,7 +424,6 @@ export class RequestQueueClient extends ResourceClient {
|
|
|
410
424
|
if (executingRequests.size >= maxParallel) {
|
|
411
425
|
await Promise.race(executingRequests);
|
|
412
426
|
}
|
|
413
|
-
i += requestsInBatch.length;
|
|
414
427
|
}
|
|
415
428
|
// Get results from remaining operations
|
|
416
429
|
await Promise.all(executingRequests);
|
package/dist/utils.d.ts
CHANGED
|
@@ -79,9 +79,21 @@ export declare function isCompressibleContentType(contentType?: string): boolean
|
|
|
79
79
|
*/
|
|
80
80
|
export declare function maybeCompressValue(value: unknown): Promise<CompressedValue | undefined>;
|
|
81
81
|
/**
|
|
82
|
-
*
|
|
82
|
+
* Returns the UTF-8 byte length of a string.
|
|
83
83
|
*/
|
|
84
|
-
export declare function
|
|
84
|
+
export declare function utf8ByteLength(value: string): number;
|
|
85
|
+
/**
|
|
86
|
+
* Splits JSON-serialized items into consecutive batches of at most `maxCount` items, each of which fits into a JSON
|
|
87
|
+
* array body - the items joined by commas between brackets - of at most `maxByteLength` bytes. The `byteLength` of an
|
|
88
|
+
* item is the UTF-8 byte length of its serialization. An item too large for a body of its own still gets one, so a
|
|
89
|
+
* caller that cannot send such an item has to reject it beforehand.
|
|
90
|
+
*/
|
|
91
|
+
export declare function splitIntoJsonArrayBatches<T extends {
|
|
92
|
+
byteLength: number;
|
|
93
|
+
}>(items: readonly T[], { maxCount, maxByteLength }: {
|
|
94
|
+
maxCount: number;
|
|
95
|
+
maxByteLength: number;
|
|
96
|
+
}): T[][];
|
|
85
97
|
export declare function isNode(): boolean;
|
|
86
98
|
export declare function isBuffer(value: unknown): value is Buffer | ArrayBuffer | TypedArray;
|
|
87
99
|
export declare function isStream(value: unknown): value is Readable;
|
package/dist/utils.js
CHANGED
|
@@ -213,28 +213,35 @@ export async function maybeCompressValue(value) {
|
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
/**
|
|
216
|
-
*
|
|
216
|
+
* Returns the UTF-8 byte length of a string.
|
|
217
217
|
*/
|
|
218
|
-
export function
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
218
|
+
export function utf8ByteLength(value) {
|
|
219
|
+
return isNode() ? Buffer.byteLength(value) : new Blob([value]).size;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Splits JSON-serialized items into consecutive batches of at most `maxCount` items, each of which fits into a JSON
|
|
223
|
+
* array body - the items joined by commas between brackets - of at most `maxByteLength` bytes. The `byteLength` of an
|
|
224
|
+
* item is the UTF-8 byte length of its serialization. An item too large for a body of its own still gets one, so a
|
|
225
|
+
* caller that cannot send such an item has to reject it beforehand.
|
|
226
|
+
*/
|
|
227
|
+
export function splitIntoJsonArrayBatches(items, { maxCount, maxByteLength }) {
|
|
228
|
+
const batches = [];
|
|
229
|
+
let batch = [];
|
|
230
|
+
// One byte for the opening bracket; each item then adds its own bytes plus one for the comma or the closing
|
|
231
|
+
// bracket that follows it.
|
|
232
|
+
let byteLength = 1;
|
|
233
|
+
for (const item of items) {
|
|
234
|
+
if (batch.length > 0 && (batch.length >= maxCount || byteLength + item.byteLength + 1 > maxByteLength)) {
|
|
235
|
+
batches.push(batch);
|
|
236
|
+
batch = [];
|
|
237
|
+
byteLength = 1;
|
|
231
238
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
byteLength += itemByteSize;
|
|
235
|
-
slicedArray.push(item);
|
|
239
|
+
batch.push(item);
|
|
240
|
+
byteLength += item.byteLength + 1;
|
|
236
241
|
}
|
|
237
|
-
|
|
242
|
+
if (batch.length > 0)
|
|
243
|
+
batches.push(batch);
|
|
244
|
+
return batches;
|
|
238
245
|
}
|
|
239
246
|
export function isNode() {
|
|
240
247
|
if (typeof BROWSER_BUILD !== 'undefined')
|