scrapely 1.2.3 → 1.2.5
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 +119 -1
- package/dist/actor.d.ts +703 -0
- package/dist/actor.js +1441 -0
- package/dist/browser-configuration.d.ts +58 -0
- package/dist/browser-configuration.js +128 -0
- package/dist/browser-locations.d.ts +5 -0
- package/dist/browser-locations.js +5469 -0
- package/dist/charging.d.ts +153 -0
- package/dist/charging.js +782 -0
- package/dist/charging_client.d.ts +18 -0
- package/dist/charging_client.js +49 -0
- package/dist/configuration.d.ts +121 -0
- package/dist/configuration.js +128 -0
- package/dist/crypto/secrets.d.ts +44 -0
- package/dist/crypto/secrets.js +164 -0
- package/dist/events/events-client.d.ts +59 -0
- package/dist/events/events-client.js +120 -0
- package/dist/events/scrapely-event-manager.d.ts +13 -0
- package/dist/events/scrapely-event-manager.js +61 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +95 -0
- package/dist/log-env.d.ts +1 -0
- package/dist/log-env.js +12 -0
- package/dist/manifest.d.ts +30 -0
- package/dist/manifest.js +47 -0
- package/dist/patch-browser-pool.d.ts +66 -0
- package/dist/patch-browser-pool.js +827 -0
- package/dist/proxy-configuration.d.ts +118 -0
- package/dist/proxy-configuration.js +88 -0
- package/dist/request-list.d.ts +57 -0
- package/dist/request-list.js +79 -0
- package/dist/runner/docker-manager.d.ts +88 -0
- package/dist/runner/docker-manager.js +283 -0
- package/dist/runner/index.d.ts +2 -0
- package/dist/runner/index.js +7 -0
- package/dist/runner/timeout-manager.d.ts +43 -0
- package/dist/runner/timeout-manager.js +90 -0
- package/dist/scrapely-proxy-configuration.d.ts +99 -0
- package/dist/scrapely-proxy-configuration.js +559 -0
- package/dist/storage/api-storage-client.d.ts +199 -0
- package/dist/storage/api-storage-client.js +910 -0
- package/dist/storage/billing-tracker.d.ts +163 -0
- package/dist/storage/billing-tracker.js +260 -0
- package/dist/storage/dataset-client.d.ts +43 -0
- package/dist/storage/dataset-client.js +232 -0
- package/dist/storage/dataset-collection-client.d.ts +8 -0
- package/dist/storage/dataset-collection-client.js +75 -0
- package/dist/storage/dataset-push-script.d.ts +18 -0
- package/dist/storage/dataset-push-script.js +38 -0
- package/dist/storage/dragonfly-client.d.ts +94 -0
- package/dist/storage/dragonfly-client.js +167 -0
- package/dist/storage/index.d.ts +11 -0
- package/dist/storage/index.js +25 -0
- package/dist/storage/key-value-store-client.d.ts +61 -0
- package/dist/storage/key-value-store-client.js +306 -0
- package/dist/storage/kv-store-collection-client.d.ts +8 -0
- package/dist/storage/kv-store-collection-client.js +76 -0
- package/dist/storage/kvs-iteration.d.ts +17 -0
- package/dist/storage/kvs-iteration.js +126 -0
- package/dist/storage/log-manager.d.ts +114 -0
- package/dist/storage/log-manager.js +244 -0
- package/dist/storage/patch-storage.d.ts +11 -0
- package/dist/storage/patch-storage.js +177 -0
- package/dist/storage/request-queue-client.d.ts +93 -0
- package/dist/storage/request-queue-client.js +522 -0
- package/dist/storage/rq-collection-client.d.ts +8 -0
- package/dist/storage/rq-collection-client.js +89 -0
- package/dist/storage/rq-list-requests.d.ts +59 -0
- package/dist/storage/rq-list-requests.js +114 -0
- package/dist/storage/rq-lock-script.d.ts +56 -0
- package/dist/storage/rq-lock-script.js +127 -0
- package/dist/storage/run-metadata.d.ts +98 -0
- package/dist/storage/run-metadata.js +256 -0
- package/dist/storage/s3-client.d.ts +103 -0
- package/dist/storage/s3-client.js +302 -0
- package/dist/storage/ttl-manager.d.ts +70 -0
- package/dist/storage/ttl-manager.js +115 -0
- package/dist/storage/utils.d.ts +3 -0
- package/dist/storage/utils.js +19 -0
- package/dist/storage-identifiers.d.ts +20 -0
- package/dist/storage-identifiers.js +6 -0
- package/dist/utils.d.ts +54 -0
- package/dist/utils.js +63 -0
- package/dist/validator.d.ts +24 -0
- package/dist/validator.js +158 -0
- package/package.json +90 -9
|
@@ -0,0 +1,910 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ApiStorageClientFactory = exports.ApiRequestQueueClient = exports.ApiKeyValueStoreClient = exports.ApiDatasetClient = exports.ApiStorageClientError = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const kvs_iteration_js_1 = require("./kvs-iteration.js");
|
|
6
|
+
function buildRqQueryParams(baseClientKey, options) {
|
|
7
|
+
const params = new URLSearchParams();
|
|
8
|
+
params.set('clientKey', options?.clientKey ?? baseClientKey);
|
|
9
|
+
if (options?.forefront)
|
|
10
|
+
params.set('forefront', 'true');
|
|
11
|
+
if (options?.lockSecs !== undefined)
|
|
12
|
+
params.set('lockSecs', String(options.lockSecs));
|
|
13
|
+
if (options?.limit !== undefined)
|
|
14
|
+
params.set('limit', String(options.limit));
|
|
15
|
+
if (options?.runId)
|
|
16
|
+
params.set('runId', options.runId);
|
|
17
|
+
const query = params.toString();
|
|
18
|
+
return query ? `?${query}` : '';
|
|
19
|
+
}
|
|
20
|
+
function mapRequestQueueInfo(id, result) {
|
|
21
|
+
return {
|
|
22
|
+
id: result.id ?? id,
|
|
23
|
+
name: result.name ?? undefined,
|
|
24
|
+
userId: result.userId ?? undefined,
|
|
25
|
+
createdAt: parseApiDate(result.createdAt),
|
|
26
|
+
modifiedAt: parseApiDate(result.modifiedAt),
|
|
27
|
+
accessedAt: parseApiDate(result.accessedAt),
|
|
28
|
+
expireAt: result.expireAt,
|
|
29
|
+
totalRequestCount: result.totalRequestCount,
|
|
30
|
+
handledRequestCount: result.handledRequestCount,
|
|
31
|
+
pendingRequestCount: result.pendingRequestCount,
|
|
32
|
+
hadMultipleClients: result.hadMultipleClients,
|
|
33
|
+
actId: result.actId ?? result.actorId,
|
|
34
|
+
actRunId: result.actRunId,
|
|
35
|
+
stats: result.stats,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Adaptive rate limiter that tracks server-provided rate limit headers.
|
|
40
|
+
* Coordinates across all runs for a user by reading X-RateLimit-* headers.
|
|
41
|
+
*/
|
|
42
|
+
class AdaptiveRateLimiter {
|
|
43
|
+
limit = 1000;
|
|
44
|
+
remaining = 1000;
|
|
45
|
+
resetAt = 0;
|
|
46
|
+
lowWaterMark = 50; // Start throttling when remaining < this
|
|
47
|
+
minDelay = 50; // Minimum delay between requests (ms)
|
|
48
|
+
maxDelay = 1000; // Maximum delay when rate limited (ms)
|
|
49
|
+
lastRequestTime = 0;
|
|
50
|
+
/**
|
|
51
|
+
* Update internal state from response headers.
|
|
52
|
+
* Called after each successful API response.
|
|
53
|
+
*/
|
|
54
|
+
updateFromHeaders(headers) {
|
|
55
|
+
const limitHeader = headers.get('X-RateLimit-Limit');
|
|
56
|
+
const remainingHeader = headers.get('X-RateLimit-Remaining');
|
|
57
|
+
const resetHeader = headers.get('X-RateLimit-Reset');
|
|
58
|
+
if (limitHeader)
|
|
59
|
+
this.limit = parseInt(limitHeader, 10) || 600;
|
|
60
|
+
if (remainingHeader)
|
|
61
|
+
this.remaining = parseInt(remainingHeader, 10) || 0;
|
|
62
|
+
if (resetHeader)
|
|
63
|
+
this.resetAt = parseInt(resetHeader, 10) * 1000; // Convert to ms
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Wait before making the next request if needed.
|
|
67
|
+
* Uses adaptive throttling based on remaining capacity.
|
|
68
|
+
*/
|
|
69
|
+
async waitForSlot() {
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
// If we're past the reset time, reset our tracking
|
|
72
|
+
if (now >= this.resetAt) {
|
|
73
|
+
this.remaining = this.limit;
|
|
74
|
+
}
|
|
75
|
+
// Calculate adaptive delay based on remaining capacity
|
|
76
|
+
let delay = this.minDelay;
|
|
77
|
+
if (this.remaining < this.lowWaterMark) {
|
|
78
|
+
// We're running low - slow down proportionally
|
|
79
|
+
const ratio = this.remaining / this.lowWaterMark;
|
|
80
|
+
delay = Math.min(this.maxDelay, this.minDelay + (1 - ratio) * 500);
|
|
81
|
+
}
|
|
82
|
+
else if (this.remaining < this.limit * 0.2) {
|
|
83
|
+
// Under 20% capacity - moderate slowdown
|
|
84
|
+
delay = 100;
|
|
85
|
+
}
|
|
86
|
+
// Ensure minimum time between requests
|
|
87
|
+
const timeSinceLastRequest = now - this.lastRequestTime;
|
|
88
|
+
if (timeSinceLastRequest < delay) {
|
|
89
|
+
await new Promise(resolve => setTimeout(resolve, delay - timeSinceLastRequest));
|
|
90
|
+
}
|
|
91
|
+
this.lastRequestTime = Date.now();
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Handle a 429 rate limit response.
|
|
95
|
+
* Parses Retry-After header and waits the appropriate time.
|
|
96
|
+
*/
|
|
97
|
+
async handleRateLimited(response) {
|
|
98
|
+
const retryAfter = response.headers.get('Retry-After');
|
|
99
|
+
let waitMs = 1000; // Default 1 second
|
|
100
|
+
if (retryAfter) {
|
|
101
|
+
// Retry-After can be seconds or a date
|
|
102
|
+
const seconds = parseInt(retryAfter, 10);
|
|
103
|
+
if (!isNaN(seconds)) {
|
|
104
|
+
waitMs = seconds * 1000;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// It's a date string
|
|
108
|
+
const retryDate = new Date(retryAfter).getTime();
|
|
109
|
+
if (!isNaN(retryDate)) {
|
|
110
|
+
waitMs = Math.max(0, retryDate - Date.now());
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Add jitter to prevent thundering herd
|
|
115
|
+
waitMs = waitMs + Math.random() * 100;
|
|
116
|
+
console.warn(`[RateLimiter] Rate limited, waiting ${Math.round(waitMs)}ms before retry`);
|
|
117
|
+
await new Promise(resolve => setTimeout(resolve, waitMs));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Unwrap Apify-style v2 response envelopes ({ data: ... }).
|
|
122
|
+
*/
|
|
123
|
+
function unwrapData(value) {
|
|
124
|
+
if (Array.isArray(value))
|
|
125
|
+
return value;
|
|
126
|
+
if (value && typeof value === 'object' && 'data' in value) {
|
|
127
|
+
return value.data;
|
|
128
|
+
}
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
function parseApiDate(value) {
|
|
132
|
+
if (value instanceof Date)
|
|
133
|
+
return value;
|
|
134
|
+
if (typeof value === 'string' && value) {
|
|
135
|
+
const parsed = new Date(value);
|
|
136
|
+
if (!isNaN(parsed.getTime()))
|
|
137
|
+
return parsed;
|
|
138
|
+
}
|
|
139
|
+
return new Date();
|
|
140
|
+
}
|
|
141
|
+
const STORAGE_UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
142
|
+
/** Crawlee opens by name or id; only UUIDs are valid API resource ids. */
|
|
143
|
+
function isStorageUuid(id) {
|
|
144
|
+
return STORAGE_UUID_REGEX.test(id);
|
|
145
|
+
}
|
|
146
|
+
class ApiStorageClientError extends Error {
|
|
147
|
+
statusCode;
|
|
148
|
+
constructor(message, statusCode) {
|
|
149
|
+
super(message);
|
|
150
|
+
this.name = 'ApiStorageClientError';
|
|
151
|
+
this.statusCode = statusCode;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
exports.ApiStorageClientError = ApiStorageClientError;
|
|
155
|
+
function isApiStorageClientError(err) {
|
|
156
|
+
return err instanceof ApiStorageClientError;
|
|
157
|
+
}
|
|
158
|
+
/** Map GET storage errors: rethrow 403, return undefined for 404 (triggers getOrCreate for names only). */
|
|
159
|
+
function handleStorageGetError(err) {
|
|
160
|
+
if (isApiStorageClientError(err)) {
|
|
161
|
+
if (err.statusCode === 403)
|
|
162
|
+
throw err;
|
|
163
|
+
if (err.statusCode === 404)
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
throw err;
|
|
167
|
+
}
|
|
168
|
+
function storageCollectionHeaders(config) {
|
|
169
|
+
return {
|
|
170
|
+
Authorization: `Bearer ${config.token}`,
|
|
171
|
+
'X-Scrapely-Run-Id': config.runId,
|
|
172
|
+
'Content-Type': 'application/json',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/** POST /v2/{datasets|key-value-stores|request-queues} to create or get-by-name. */
|
|
176
|
+
async function postStorageCollection(config, path, name) {
|
|
177
|
+
const query = name ? `?name=${encodeURIComponent(name)}` : '';
|
|
178
|
+
const url = `${config.apiUrl}/v2${path}${query}`;
|
|
179
|
+
const response = await fetch(url, {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
headers: storageCollectionHeaders(config),
|
|
182
|
+
});
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
const error = await response.json().catch(() => ({ error: { message: response.statusText } }));
|
|
185
|
+
throw new Error(error.error?.message || `Failed to create storage: ${response.status}`);
|
|
186
|
+
}
|
|
187
|
+
const body = await response.json();
|
|
188
|
+
return unwrapData(body);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Helper to create AsyncIterable + Promise result (matches Crawlee pattern)
|
|
192
|
+
*/
|
|
193
|
+
function makeAsyncIterableResult(promiseFn) {
|
|
194
|
+
const promise = promiseFn();
|
|
195
|
+
const iterable = {
|
|
196
|
+
[Symbol.asyncIterator]: () => {
|
|
197
|
+
let items = null;
|
|
198
|
+
let index = 0;
|
|
199
|
+
return {
|
|
200
|
+
async next() {
|
|
201
|
+
if (!items)
|
|
202
|
+
items = (await promise).items;
|
|
203
|
+
if (index >= items.length)
|
|
204
|
+
return { done: true, value: undefined };
|
|
205
|
+
return { done: false, value: items[index++] };
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
},
|
|
209
|
+
then: promise.then.bind(promise),
|
|
210
|
+
catch: promise.catch.bind(promise),
|
|
211
|
+
finally: promise.finally.bind(promise),
|
|
212
|
+
[Symbol.toStringTag]: 'AsyncIterablePromise',
|
|
213
|
+
};
|
|
214
|
+
return iterable;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Base class for API-based storage clients.
|
|
218
|
+
* Communicates with internal storage API instead of direct DB access.
|
|
219
|
+
* No sensitive credentials are stored - only the user's API key.
|
|
220
|
+
*/
|
|
221
|
+
class BaseApiClient {
|
|
222
|
+
config;
|
|
223
|
+
headers;
|
|
224
|
+
// Shared rate limiter instance - coordinates across all API clients
|
|
225
|
+
// This is static so it's shared across all storage clients for this run
|
|
226
|
+
static rateLimiter = new AdaptiveRateLimiter();
|
|
227
|
+
// Maximum retries for rate-limited requests
|
|
228
|
+
static maxRetries = 3;
|
|
229
|
+
// Default timeout for requests (30 seconds)
|
|
230
|
+
static defaultTimeoutMs = 30000;
|
|
231
|
+
constructor(config) {
|
|
232
|
+
this.config = config;
|
|
233
|
+
this.headers = {
|
|
234
|
+
'Authorization': `Bearer ${config.token}`,
|
|
235
|
+
'X-Scrapely-Run-Id': config.runId,
|
|
236
|
+
'Content-Type': 'application/json',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
async request(method, path, body, contentType, options) {
|
|
240
|
+
const url = `${this.config.apiUrl}/v2${path}`;
|
|
241
|
+
const headers = { ...this.headers };
|
|
242
|
+
if (contentType) {
|
|
243
|
+
headers['Content-Type'] = contentType;
|
|
244
|
+
}
|
|
245
|
+
const timeoutMs = options?.timeoutSecs
|
|
246
|
+
? options.timeoutSecs * 1000
|
|
247
|
+
: BaseApiClient.defaultTimeoutMs;
|
|
248
|
+
const shouldRetryTimeouts = options?.doNotRetryTimeouts !== true;
|
|
249
|
+
let lastError = null;
|
|
250
|
+
for (let attempt = 0; attempt <= BaseApiClient.maxRetries; attempt++) {
|
|
251
|
+
// Wait for rate limiter slot before making request
|
|
252
|
+
await BaseApiClient.rateLimiter.waitForSlot();
|
|
253
|
+
// Create AbortController for timeout
|
|
254
|
+
const controller = new AbortController();
|
|
255
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
256
|
+
try {
|
|
257
|
+
const response = await fetch(url, {
|
|
258
|
+
method,
|
|
259
|
+
headers,
|
|
260
|
+
body: body !== undefined ? (typeof body === 'string' ? body : JSON.stringify(body)) : undefined,
|
|
261
|
+
signal: controller.signal,
|
|
262
|
+
});
|
|
263
|
+
// Update rate limiter state from response headers
|
|
264
|
+
BaseApiClient.rateLimiter.updateFromHeaders(response.headers);
|
|
265
|
+
// Handle rate limiting (429)
|
|
266
|
+
if (response.status === 429) {
|
|
267
|
+
if (attempt < BaseApiClient.maxRetries) {
|
|
268
|
+
await BaseApiClient.rateLimiter.handleRateLimited(response);
|
|
269
|
+
continue; // Retry
|
|
270
|
+
}
|
|
271
|
+
// Max retries exceeded
|
|
272
|
+
const error = await response.json().catch(() => ({ error: { message: 'Rate limit exceeded' } }));
|
|
273
|
+
throw new Error(error.error?.message || 'Rate limit exceeded');
|
|
274
|
+
}
|
|
275
|
+
if (!response.ok) {
|
|
276
|
+
const error = await response.json().catch(() => ({ error: { message: response.statusText } }));
|
|
277
|
+
throw new ApiStorageClientError(error.error?.message || `API request failed: ${response.status}`, response.status);
|
|
278
|
+
}
|
|
279
|
+
// Handle empty responses (204 No Content)
|
|
280
|
+
if (response.status === 204) {
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
// Handle text/plain responses
|
|
284
|
+
const responseContentType = response.headers.get('content-type') || '';
|
|
285
|
+
if (responseContentType.includes('text/plain')) {
|
|
286
|
+
return response.text();
|
|
287
|
+
}
|
|
288
|
+
// Handle binary responses
|
|
289
|
+
if (responseContentType.includes('application/octet-stream')) {
|
|
290
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
291
|
+
return buffer;
|
|
292
|
+
}
|
|
293
|
+
const json = await response.json();
|
|
294
|
+
return unwrapData(json);
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
// Handle timeout/abort errors
|
|
298
|
+
if (err.name === 'AbortError') {
|
|
299
|
+
const timeoutError = new Error(`Request timed out after ${timeoutMs}ms`);
|
|
300
|
+
timeoutError.isTimeout = true;
|
|
301
|
+
lastError = timeoutError;
|
|
302
|
+
// Don't retry if doNotRetryTimeouts is true
|
|
303
|
+
if (!shouldRetryTimeouts || attempt >= BaseApiClient.maxRetries) {
|
|
304
|
+
throw timeoutError;
|
|
305
|
+
}
|
|
306
|
+
// Wait a bit before retrying
|
|
307
|
+
await new Promise(resolve => setTimeout(resolve, 100 * (attempt + 1)));
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
lastError = err;
|
|
311
|
+
// Don't retry non-timeout errors unless it's a network issue
|
|
312
|
+
const isNetworkError = !err.response && !err.status;
|
|
313
|
+
if (!isNetworkError || attempt >= BaseApiClient.maxRetries) {
|
|
314
|
+
throw err;
|
|
315
|
+
}
|
|
316
|
+
// Wait before retrying network errors
|
|
317
|
+
await new Promise(resolve => setTimeout(resolve, 100 * (attempt + 1)));
|
|
318
|
+
}
|
|
319
|
+
finally {
|
|
320
|
+
clearTimeout(timeoutId);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
throw lastError || new Error('Request failed after retries');
|
|
324
|
+
}
|
|
325
|
+
async requestWithHeaders(method, path, body, contentType) {
|
|
326
|
+
const url = `${this.config.apiUrl}/v2${path}`;
|
|
327
|
+
const headers = { ...this.headers };
|
|
328
|
+
if (contentType) {
|
|
329
|
+
headers['Content-Type'] = contentType;
|
|
330
|
+
}
|
|
331
|
+
for (let attempt = 0; attempt <= BaseApiClient.maxRetries; attempt++) {
|
|
332
|
+
await BaseApiClient.rateLimiter.waitForSlot();
|
|
333
|
+
const response = await fetch(url, {
|
|
334
|
+
method,
|
|
335
|
+
headers,
|
|
336
|
+
body: body !== undefined ? (typeof body === 'string' ? body : JSON.stringify(body)) : undefined,
|
|
337
|
+
});
|
|
338
|
+
BaseApiClient.rateLimiter.updateFromHeaders(response.headers);
|
|
339
|
+
if (response.status === 429) {
|
|
340
|
+
if (attempt < BaseApiClient.maxRetries) {
|
|
341
|
+
await BaseApiClient.rateLimiter.handleRateLimited(response);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const error = await response.json().catch(() => ({ error: { message: 'Rate limit exceeded' } }));
|
|
345
|
+
throw new Error(error.error?.message || 'Rate limit exceeded');
|
|
346
|
+
}
|
|
347
|
+
if (!response.ok) {
|
|
348
|
+
const error = await response.json().catch(() => ({ error: { message: response.statusText } }));
|
|
349
|
+
throw new Error(error.error?.message || `API request failed: ${response.status}`);
|
|
350
|
+
}
|
|
351
|
+
if (response.status === 204) {
|
|
352
|
+
return { data: undefined, contentType: '' };
|
|
353
|
+
}
|
|
354
|
+
const responseContentType = response.headers.get('content-type') || 'application/json';
|
|
355
|
+
if (responseContentType.includes('text/plain')) {
|
|
356
|
+
return { data: await response.text(), contentType: 'text/plain' };
|
|
357
|
+
}
|
|
358
|
+
if (responseContentType.includes('application/octet-stream')) {
|
|
359
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
360
|
+
return { data: buffer, contentType: 'application/octet-stream' };
|
|
361
|
+
}
|
|
362
|
+
const json = await response.json();
|
|
363
|
+
return { data: json, contentType: responseContentType.split(';')[0].trim() || 'application/json' };
|
|
364
|
+
}
|
|
365
|
+
throw new Error('Request failed after retries');
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Dataset client that communicates via internal API
|
|
370
|
+
*/
|
|
371
|
+
class ApiDatasetClient extends BaseApiClient {
|
|
372
|
+
id;
|
|
373
|
+
constructor(config, datasetId) {
|
|
374
|
+
super(config);
|
|
375
|
+
this.id = datasetId;
|
|
376
|
+
}
|
|
377
|
+
async get() {
|
|
378
|
+
if (!isStorageUuid(this.id))
|
|
379
|
+
return undefined;
|
|
380
|
+
try {
|
|
381
|
+
const data = await this.request('GET', `/datasets/${this.id}`);
|
|
382
|
+
return {
|
|
383
|
+
id: String(data.id ?? this.id),
|
|
384
|
+
name: data.name ?? undefined,
|
|
385
|
+
createdAt: parseApiDate(data.createdAt),
|
|
386
|
+
modifiedAt: parseApiDate(data.modifiedAt),
|
|
387
|
+
accessedAt: parseApiDate(data.accessedAt),
|
|
388
|
+
itemCount: typeof data.itemCount === 'number'
|
|
389
|
+
? data.itemCount
|
|
390
|
+
: parseInt(String(data.itemCount ?? '0'), 10),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
catch (err) {
|
|
394
|
+
return handleStorageGetError(err);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
async update(newFields) {
|
|
398
|
+
throw new Error('Dataset rename not supported from container');
|
|
399
|
+
}
|
|
400
|
+
async delete() {
|
|
401
|
+
await this.request('DELETE', `/datasets/${this.id}`);
|
|
402
|
+
}
|
|
403
|
+
async downloadItems() {
|
|
404
|
+
const result = await this._listItems();
|
|
405
|
+
return Buffer.from(JSON.stringify(result.items));
|
|
406
|
+
}
|
|
407
|
+
listItems(options) {
|
|
408
|
+
return makeAsyncIterableResult(() => this._listItems(options));
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Iterates over dataset entries (index-value pairs) using an async generator.
|
|
412
|
+
* Required by Crawlee's Dataset.entries() method.
|
|
413
|
+
*/
|
|
414
|
+
listEntries(options) {
|
|
415
|
+
return makeAsyncIterableResult(async () => {
|
|
416
|
+
const result = await this._listItems(options);
|
|
417
|
+
const entries = result.items.map((item, idx) => [
|
|
418
|
+
(result.offset ?? 0) + idx,
|
|
419
|
+
item,
|
|
420
|
+
]);
|
|
421
|
+
return {
|
|
422
|
+
total: result.total,
|
|
423
|
+
count: entries.length,
|
|
424
|
+
offset: result.offset,
|
|
425
|
+
limit: result.limit,
|
|
426
|
+
items: entries,
|
|
427
|
+
};
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
async _listItems(options) {
|
|
431
|
+
const params = new URLSearchParams();
|
|
432
|
+
const offset = options?.offset ?? 0;
|
|
433
|
+
const limit = options?.limit;
|
|
434
|
+
if (options?.offset)
|
|
435
|
+
params.set('offset', String(options.offset));
|
|
436
|
+
if (options?.limit)
|
|
437
|
+
params.set('limit', String(options.limit));
|
|
438
|
+
if (options?.desc)
|
|
439
|
+
params.set('desc', 'true');
|
|
440
|
+
if (options?.fields)
|
|
441
|
+
params.set('fields', options.fields.join(','));
|
|
442
|
+
if (options?.clean)
|
|
443
|
+
params.set('clean', 'true');
|
|
444
|
+
if (options?.skipHidden)
|
|
445
|
+
params.set('skipHidden', 'true');
|
|
446
|
+
if (options?.skipEmpty)
|
|
447
|
+
params.set('skipEmpty', 'true');
|
|
448
|
+
const queryString = params.toString();
|
|
449
|
+
const path = `/datasets/${this.id}/items${queryString ? `?${queryString}` : ''}`;
|
|
450
|
+
const result = await this.request('GET', path);
|
|
451
|
+
// v2 returns a bare JSON array; internal API returned { total, count, offset, limit, items }
|
|
452
|
+
if (Array.isArray(result)) {
|
|
453
|
+
return {
|
|
454
|
+
total: result.length,
|
|
455
|
+
count: result.length,
|
|
456
|
+
offset,
|
|
457
|
+
limit: limit ?? result.length,
|
|
458
|
+
items: result,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
return result;
|
|
462
|
+
}
|
|
463
|
+
async pushItems(items) {
|
|
464
|
+
let parsed;
|
|
465
|
+
if (typeof items === 'string') {
|
|
466
|
+
parsed = JSON.parse(items);
|
|
467
|
+
if (!Array.isArray(parsed))
|
|
468
|
+
parsed = [parsed];
|
|
469
|
+
}
|
|
470
|
+
else if (Array.isArray(items)) {
|
|
471
|
+
parsed = items.map(i => (typeof i === 'string' ? JSON.parse(i) : i));
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
parsed = [items];
|
|
475
|
+
}
|
|
476
|
+
await this.request('POST', `/datasets/${this.id}/items`, parsed);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
exports.ApiDatasetClient = ApiDatasetClient;
|
|
480
|
+
/**
|
|
481
|
+
* Key-Value Store client that communicates via internal API
|
|
482
|
+
*/
|
|
483
|
+
class ApiKeyValueStoreClient extends BaseApiClient {
|
|
484
|
+
id;
|
|
485
|
+
constructor(config, storeId) {
|
|
486
|
+
super(config);
|
|
487
|
+
this.id = storeId;
|
|
488
|
+
}
|
|
489
|
+
async get() {
|
|
490
|
+
if (!isStorageUuid(this.id))
|
|
491
|
+
return undefined;
|
|
492
|
+
try {
|
|
493
|
+
const data = await this.request('GET', `/key-value-stores/${this.id}`);
|
|
494
|
+
return {
|
|
495
|
+
id: String(data.id ?? this.id),
|
|
496
|
+
name: data.name ?? undefined,
|
|
497
|
+
createdAt: parseApiDate(data.createdAt),
|
|
498
|
+
modifiedAt: parseApiDate(data.modifiedAt),
|
|
499
|
+
accessedAt: parseApiDate(data.accessedAt),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
catch (err) {
|
|
503
|
+
return handleStorageGetError(err);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
async update(newFields) {
|
|
507
|
+
throw new Error('KVS rename not supported from container');
|
|
508
|
+
}
|
|
509
|
+
async delete() {
|
|
510
|
+
if (!isStorageUuid(this.id))
|
|
511
|
+
return;
|
|
512
|
+
await this.request('DELETE', `/key-value-stores/${this.id}`);
|
|
513
|
+
}
|
|
514
|
+
async recordExists(key) {
|
|
515
|
+
try {
|
|
516
|
+
await this.getRecord(key);
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
async getRecord(key, options) {
|
|
524
|
+
try {
|
|
525
|
+
const { data, contentType } = await this.requestWithHeaders('GET', `/key-value-stores/${this.id}/records/${key}`);
|
|
526
|
+
if (data === undefined || data === null)
|
|
527
|
+
return undefined;
|
|
528
|
+
let value = data;
|
|
529
|
+
if (options?.buffer && !Buffer.isBuffer(value)) {
|
|
530
|
+
value = Buffer.from(typeof value === 'string' ? value : JSON.stringify(value));
|
|
531
|
+
}
|
|
532
|
+
return { key, value, contentType: contentType || 'application/json' };
|
|
533
|
+
}
|
|
534
|
+
catch {
|
|
535
|
+
return undefined;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
async setRecord(record, options) {
|
|
539
|
+
const { key, value, contentType } = record;
|
|
540
|
+
let body;
|
|
541
|
+
let ct;
|
|
542
|
+
// Handle Readable streams (e.g., fs.createReadStream) - Apify compatibility
|
|
543
|
+
// Streams are objects with a pipe method. We need to buffer them.
|
|
544
|
+
if (value && typeof value === 'object' && typeof value.pipe === 'function') {
|
|
545
|
+
const chunks = [];
|
|
546
|
+
for await (const chunk of value) {
|
|
547
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
548
|
+
}
|
|
549
|
+
const buffered = Buffer.concat(chunks);
|
|
550
|
+
body = buffered.toString('base64');
|
|
551
|
+
ct = 'application/octet-stream';
|
|
552
|
+
}
|
|
553
|
+
else if (Buffer.isBuffer(value)) {
|
|
554
|
+
body = value.toString('base64');
|
|
555
|
+
ct = 'application/octet-stream';
|
|
556
|
+
}
|
|
557
|
+
else if (typeof value === 'string') {
|
|
558
|
+
body = value;
|
|
559
|
+
ct = 'text/plain';
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
body = value;
|
|
563
|
+
ct = 'application/json';
|
|
564
|
+
}
|
|
565
|
+
// Pass timeout/retry options from KeyValueStoreRecordOptions to the API request
|
|
566
|
+
const requestOptions = options ? {
|
|
567
|
+
timeoutSecs: options.timeoutSecs,
|
|
568
|
+
doNotRetryTimeouts: options.doNotRetryTimeouts,
|
|
569
|
+
} : undefined;
|
|
570
|
+
await this.request('PUT', `/key-value-stores/${this.id}/records/${key}`, body, contentType || ct, requestOptions);
|
|
571
|
+
}
|
|
572
|
+
async deleteRecord(key) {
|
|
573
|
+
await this.request('DELETE', `/key-value-stores/${this.id}/records/${key}`);
|
|
574
|
+
}
|
|
575
|
+
async listKeys(options) {
|
|
576
|
+
const params = new URLSearchParams();
|
|
577
|
+
if (options?.prefix)
|
|
578
|
+
params.set('prefix', options.prefix);
|
|
579
|
+
if (options?.limit)
|
|
580
|
+
params.set('limit', String(options.limit));
|
|
581
|
+
if (options?.exclusiveStartKey)
|
|
582
|
+
params.set('exclusiveStartKey', options.exclusiveStartKey);
|
|
583
|
+
if (options?.collection)
|
|
584
|
+
params.set('collection', options.collection);
|
|
585
|
+
const queryString = params.toString();
|
|
586
|
+
const path = `/key-value-stores/${this.id}/keys${queryString ? `?${queryString}` : ''}`;
|
|
587
|
+
return this.request('GET', path);
|
|
588
|
+
}
|
|
589
|
+
keys(options) {
|
|
590
|
+
return (0, kvs_iteration_js_1.makePaginatingKeysIterable)((opts) => this.listKeys(opts), options);
|
|
591
|
+
}
|
|
592
|
+
values(options) {
|
|
593
|
+
return (0, kvs_iteration_js_1.makePaginatingValuesIterable)((opts) => this.listKeys(opts), (key) => this.getRecord(key), options);
|
|
594
|
+
}
|
|
595
|
+
entries(options) {
|
|
596
|
+
return (0, kvs_iteration_js_1.makePaginatingEntriesIterable)((opts) => this.listKeys(opts), (key) => this.getRecord(key), options);
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Iterates over all keys in the key-value store, calling the iteratee function for each key.
|
|
600
|
+
* Similar to Apify's KeyValueStore.forEachKey() method.
|
|
601
|
+
*/
|
|
602
|
+
async forEachKey(iteratee, options) {
|
|
603
|
+
return (0, kvs_iteration_js_1.forEachKeyPaginated)((opts) => this.listKeys(opts), iteratee, options);
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Gets a value from the key-value store that is automatically saved when modified.
|
|
607
|
+
* If the key doesn't exist, returns the defaultValue and stores it for future use.
|
|
608
|
+
* Similar to Apify's KeyValueStore.getAutoSavedValue() method.
|
|
609
|
+
*
|
|
610
|
+
* @param key - The key of the record to get
|
|
611
|
+
* @param defaultValue - The default value to return and store if the key doesn't exist
|
|
612
|
+
* @returns The stored value or the defaultValue
|
|
613
|
+
*/
|
|
614
|
+
async getAutoSavedValue(key, defaultValue) {
|
|
615
|
+
const record = await this.getRecord(key);
|
|
616
|
+
if (record === undefined) {
|
|
617
|
+
// Store the default value and return it
|
|
618
|
+
await this.setRecord({ key, value: defaultValue });
|
|
619
|
+
return defaultValue;
|
|
620
|
+
}
|
|
621
|
+
return record.value;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Returns a URL for the given key that may be used to publicly access the value
|
|
625
|
+
* in the remote key-value store.
|
|
626
|
+
* Similar to Apify's KeyValueStore.getPublicUrl() method.
|
|
627
|
+
*
|
|
628
|
+
* @param key - The key of the record to get the public URL for
|
|
629
|
+
* @returns A public URL for accessing the record
|
|
630
|
+
*/
|
|
631
|
+
getPublicUrl(key) {
|
|
632
|
+
return `${this.config.apiUrl}/v2/key-value-stores/${this.id}/records/${key}`;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
exports.ApiKeyValueStoreClient = ApiKeyValueStoreClient;
|
|
636
|
+
/**
|
|
637
|
+
* Request Queue client that communicates via internal API
|
|
638
|
+
*/
|
|
639
|
+
class ApiRequestQueueClient extends BaseApiClient {
|
|
640
|
+
id;
|
|
641
|
+
clientKey;
|
|
642
|
+
constructor(config, queueId, options) {
|
|
643
|
+
super(config);
|
|
644
|
+
this.id = queueId;
|
|
645
|
+
this.clientKey = options?.clientKey || (0, node_crypto_1.randomUUID)().replace(/-/g, '').slice(0, 16);
|
|
646
|
+
}
|
|
647
|
+
async get() {
|
|
648
|
+
if (!isStorageUuid(this.id))
|
|
649
|
+
return undefined;
|
|
650
|
+
try {
|
|
651
|
+
const result = await this.request('GET', `/request-queues/${this.id}`);
|
|
652
|
+
return mapRequestQueueInfo(this.id, result);
|
|
653
|
+
}
|
|
654
|
+
catch (err) {
|
|
655
|
+
return handleStorageGetError(err);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async update(newFields) {
|
|
659
|
+
if (!isStorageUuid(this.id))
|
|
660
|
+
return undefined;
|
|
661
|
+
const result = await this.request('PUT', `/request-queues/${this.id}`, newFields);
|
|
662
|
+
return { name: result.name ?? newFields.name };
|
|
663
|
+
}
|
|
664
|
+
async delete() {
|
|
665
|
+
if (!isStorageUuid(this.id))
|
|
666
|
+
return;
|
|
667
|
+
await this.request('DELETE', `/request-queues/${this.id}`);
|
|
668
|
+
}
|
|
669
|
+
async addRequest(request, options) {
|
|
670
|
+
const query = buildRqQueryParams(this.clientKey, { forefront: options?.forefront });
|
|
671
|
+
return this.request('POST', `/request-queues/${this.id}/requests${query}`, request);
|
|
672
|
+
}
|
|
673
|
+
async batchAddRequests(requests, options) {
|
|
674
|
+
const processedRequests = [];
|
|
675
|
+
const unprocessedRequests = [];
|
|
676
|
+
const BATCH_SIZE = 25;
|
|
677
|
+
for (let i = 0; i < requests.length; i += BATCH_SIZE) {
|
|
678
|
+
const chunk = requests.slice(i, i + BATCH_SIZE);
|
|
679
|
+
try {
|
|
680
|
+
const query = buildRqQueryParams(this.clientKey, { forefront: options?.forefront });
|
|
681
|
+
const result = await this.request('POST', `/request-queues/${this.id}/requests/batch${query}`, chunk);
|
|
682
|
+
processedRequests.push(...result.processedRequests);
|
|
683
|
+
unprocessedRequests.push(...result.unprocessedRequests);
|
|
684
|
+
}
|
|
685
|
+
catch {
|
|
686
|
+
for (const request of chunk) {
|
|
687
|
+
unprocessedRequests.push({
|
|
688
|
+
uniqueKey: request.uniqueKey,
|
|
689
|
+
url: request.url,
|
|
690
|
+
method: request.method,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return { processedRequests, unprocessedRequests };
|
|
696
|
+
}
|
|
697
|
+
async getRequest(id) {
|
|
698
|
+
try {
|
|
699
|
+
return await this.request('GET', `/request-queues/${this.id}/requests/${id}`);
|
|
700
|
+
}
|
|
701
|
+
catch {
|
|
702
|
+
return undefined;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
async updateRequest(request, options) {
|
|
706
|
+
const query = buildRqQueryParams(this.clientKey, { forefront: options?.forefront });
|
|
707
|
+
return this.request('PUT', `/request-queues/${this.id}/requests/${request.id}${query}`, request);
|
|
708
|
+
}
|
|
709
|
+
async deleteRequest(id) {
|
|
710
|
+
await this.request('DELETE', `/request-queues/${this.id}/requests/${id}`);
|
|
711
|
+
}
|
|
712
|
+
async listHead(options) {
|
|
713
|
+
const query = buildRqQueryParams(this.clientKey, { limit: options?.limit });
|
|
714
|
+
const result = await this.request('GET', `/request-queues/${this.id}/head${query}`);
|
|
715
|
+
return {
|
|
716
|
+
limit: result.limit,
|
|
717
|
+
queueModifiedAt: result.queueModifiedAt ? new Date(result.queueModifiedAt) : new Date(),
|
|
718
|
+
hadMultipleClients: result.hadMultipleClients,
|
|
719
|
+
items: result.items,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
async listAndLockHead(options) {
|
|
723
|
+
const query = buildRqQueryParams(this.clientKey, {
|
|
724
|
+
limit: options.limit || 25,
|
|
725
|
+
lockSecs: options.lockSecs,
|
|
726
|
+
});
|
|
727
|
+
const result = await this.request('POST', `/request-queues/${this.id}/head/lock${query}`);
|
|
728
|
+
return {
|
|
729
|
+
...result,
|
|
730
|
+
queueModifiedAt: result.queueModifiedAt ? new Date(result.queueModifiedAt) : new Date(),
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
async prolongRequestLock(id, options) {
|
|
734
|
+
const query = buildRqQueryParams(this.clientKey, {
|
|
735
|
+
lockSecs: options.lockSecs,
|
|
736
|
+
forefront: options.forefront,
|
|
737
|
+
});
|
|
738
|
+
const result = await this.request('PUT', `/request-queues/${this.id}/requests/${id}/lock${query}`);
|
|
739
|
+
return { lockExpiresAt: new Date(result.lockExpiresAt) };
|
|
740
|
+
}
|
|
741
|
+
async deleteRequestLock(id, options) {
|
|
742
|
+
const query = buildRqQueryParams(this.clientKey, { forefront: options?.forefront });
|
|
743
|
+
await this.request('DELETE', `/request-queues/${this.id}/requests/${id}/lock${query}`);
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* List requests in the queue with optional filtering and pagination (Apify REST API).
|
|
747
|
+
*/
|
|
748
|
+
async listRequests(options) {
|
|
749
|
+
const params = new URLSearchParams();
|
|
750
|
+
params.set('clientKey', options?.clientKey ?? this.clientKey);
|
|
751
|
+
if (options?.limit !== undefined)
|
|
752
|
+
params.set('limit', String(options.limit));
|
|
753
|
+
if (options?.cursor)
|
|
754
|
+
params.set('cursor', options.cursor);
|
|
755
|
+
if (options?.exclusiveStartId)
|
|
756
|
+
params.set('exclusiveStartId', options.exclusiveStartId);
|
|
757
|
+
if (options?.filter?.length) {
|
|
758
|
+
for (const f of options.filter)
|
|
759
|
+
params.append('filter', f);
|
|
760
|
+
}
|
|
761
|
+
const query = params.toString();
|
|
762
|
+
return this.request('GET', `/request-queues/${this.id}/requests${query ? `?${query}` : ''}`);
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Unlock all requests locked by this clientKey (Apify REST API).
|
|
766
|
+
*/
|
|
767
|
+
async unlockRequests(options) {
|
|
768
|
+
const query = buildRqQueryParams(options.clientKey ?? this.clientKey, {
|
|
769
|
+
forefront: options.forefront,
|
|
770
|
+
runId: options.runId,
|
|
771
|
+
});
|
|
772
|
+
return this.request('POST', `/request-queues/${this.id}/requests/unlock${query}`);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
exports.ApiRequestQueueClient = ApiRequestQueueClient;
|
|
776
|
+
/**
|
|
777
|
+
* Dataset collection client (API-based)
|
|
778
|
+
*/
|
|
779
|
+
class ApiDatasetCollectionClient {
|
|
780
|
+
config;
|
|
781
|
+
constructor(config) {
|
|
782
|
+
this.config = config;
|
|
783
|
+
}
|
|
784
|
+
async getOrCreate(name) {
|
|
785
|
+
const data = await postStorageCollection(this.config, '/datasets', name);
|
|
786
|
+
return {
|
|
787
|
+
id: String(data.id),
|
|
788
|
+
name: data.name ?? name,
|
|
789
|
+
createdAt: parseApiDate(data.createdAt),
|
|
790
|
+
modifiedAt: parseApiDate(data.modifiedAt),
|
|
791
|
+
accessedAt: parseApiDate(data.accessedAt),
|
|
792
|
+
itemCount: typeof data.itemCount === 'number'
|
|
793
|
+
? data.itemCount
|
|
794
|
+
: parseInt(String(data.itemCount ?? '0'), 10),
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
async list() {
|
|
798
|
+
// Containers don't list all datasets - return empty
|
|
799
|
+
return { total: 0, count: 0, offset: 0, limit: 0, items: [] };
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Key-value store collection client (API-based)
|
|
804
|
+
*/
|
|
805
|
+
class ApiKeyValueStoreCollectionClient {
|
|
806
|
+
config;
|
|
807
|
+
constructor(config) {
|
|
808
|
+
this.config = config;
|
|
809
|
+
}
|
|
810
|
+
async getOrCreate(name) {
|
|
811
|
+
const data = await postStorageCollection(this.config, '/key-value-stores', name);
|
|
812
|
+
return {
|
|
813
|
+
id: String(data.id),
|
|
814
|
+
name: data.name ?? name,
|
|
815
|
+
createdAt: parseApiDate(data.createdAt),
|
|
816
|
+
modifiedAt: parseApiDate(data.modifiedAt),
|
|
817
|
+
accessedAt: parseApiDate(data.accessedAt),
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
async list() {
|
|
821
|
+
return { total: 0, count: 0, offset: 0, limit: 0, items: [] };
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Request queue collection client (API-based)
|
|
826
|
+
*/
|
|
827
|
+
class ApiRequestQueueCollectionClient {
|
|
828
|
+
config;
|
|
829
|
+
constructor(config) {
|
|
830
|
+
this.config = config;
|
|
831
|
+
}
|
|
832
|
+
async getOrCreate(name) {
|
|
833
|
+
const data = await postStorageCollection(this.config, '/request-queues', name);
|
|
834
|
+
return {
|
|
835
|
+
id: String(data.id),
|
|
836
|
+
name: data.name ?? name,
|
|
837
|
+
createdAt: parseApiDate(data.createdAt),
|
|
838
|
+
modifiedAt: parseApiDate(data.modifiedAt),
|
|
839
|
+
accessedAt: parseApiDate(data.accessedAt),
|
|
840
|
+
totalRequestCount: typeof data.totalRequestCount === 'number'
|
|
841
|
+
? data.totalRequestCount
|
|
842
|
+
: parseInt(String(data.totalRequestCount ?? '0'), 10),
|
|
843
|
+
handledRequestCount: typeof data.handledRequestCount === 'number'
|
|
844
|
+
? data.handledRequestCount
|
|
845
|
+
: parseInt(String(data.handledRequestCount ?? '0'), 10),
|
|
846
|
+
pendingRequestCount: typeof data.pendingRequestCount === 'number'
|
|
847
|
+
? data.pendingRequestCount
|
|
848
|
+
: parseInt(String(data.pendingRequestCount ?? '0'), 10),
|
|
849
|
+
hadMultipleClients: data.hadMultipleClients === true || data.hadMultipleClients === '1',
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
async list() {
|
|
853
|
+
const url = `${this.config.apiUrl}/v2/request-queues`;
|
|
854
|
+
const response = await fetch(url, {
|
|
855
|
+
headers: storageCollectionHeaders(this.config),
|
|
856
|
+
});
|
|
857
|
+
if (!response.ok) {
|
|
858
|
+
const error = await response.json().catch(() => ({ error: { message: response.statusText } }));
|
|
859
|
+
throw new Error(error.error?.message
|
|
860
|
+
|| `Failed to list request queues: ${response.status}`);
|
|
861
|
+
}
|
|
862
|
+
const body = await response.json();
|
|
863
|
+
const data = unwrapData(body);
|
|
864
|
+
return {
|
|
865
|
+
total: data.total,
|
|
866
|
+
count: data.count,
|
|
867
|
+
offset: data.offset,
|
|
868
|
+
limit: data.limit,
|
|
869
|
+
items: data.items.map((item) => mapRequestQueueInfo(String(item.id), item)),
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Factory for creating API-based storage clients.
|
|
875
|
+
* Implements StorageClient interface for Crawlee compatibility.
|
|
876
|
+
*/
|
|
877
|
+
class ApiStorageClientFactory {
|
|
878
|
+
config;
|
|
879
|
+
constructor(config) {
|
|
880
|
+
this.config = config;
|
|
881
|
+
}
|
|
882
|
+
datasets() {
|
|
883
|
+
return new ApiDatasetCollectionClient(this.config);
|
|
884
|
+
}
|
|
885
|
+
dataset(id) {
|
|
886
|
+
return new ApiDatasetClient(this.config, id);
|
|
887
|
+
}
|
|
888
|
+
keyValueStores() {
|
|
889
|
+
return new ApiKeyValueStoreCollectionClient(this.config);
|
|
890
|
+
}
|
|
891
|
+
keyValueStore(id) {
|
|
892
|
+
return new ApiKeyValueStoreClient(this.config, id);
|
|
893
|
+
}
|
|
894
|
+
requestQueues() {
|
|
895
|
+
return new ApiRequestQueueCollectionClient(this.config);
|
|
896
|
+
}
|
|
897
|
+
requestQueue(id, options) {
|
|
898
|
+
return new ApiRequestQueueClient(this.config, id, options);
|
|
899
|
+
}
|
|
900
|
+
async purge() {
|
|
901
|
+
// Containers don't purge storage
|
|
902
|
+
}
|
|
903
|
+
async teardown() {
|
|
904
|
+
// no-op: cleanup handled by server
|
|
905
|
+
}
|
|
906
|
+
async setStatusMessage(message) {
|
|
907
|
+
console.log(`[Scrapely] Status: ${message}`);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
exports.ApiStorageClientFactory = ApiStorageClientFactory;
|