scrapely 1.2.3 → 1.2.4
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,283 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DockerManager = void 0;
|
|
7
|
+
const dockerode_1 = __importDefault(require("dockerode"));
|
|
8
|
+
const timeout_manager_js_1 = require("./timeout-manager.js");
|
|
9
|
+
const validator_js_1 = require("../validator.js");
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const run_metadata_js_1 = require("../storage/run-metadata.js");
|
|
12
|
+
const log_manager_js_1 = require("../storage/log-manager.js");
|
|
13
|
+
class DockerManager {
|
|
14
|
+
docker;
|
|
15
|
+
timeoutManager;
|
|
16
|
+
imageName;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.docker = new dockerode_1.default({
|
|
19
|
+
socketPath: options?.socketPath || '/var/run/docker.sock',
|
|
20
|
+
});
|
|
21
|
+
this.timeoutManager = new timeout_manager_js_1.TimeoutManager();
|
|
22
|
+
this.imageName = options?.imageName || 'scrapely-runner:latest';
|
|
23
|
+
// Set up timeout event handlers
|
|
24
|
+
this.timeoutManager.on('timeout', async ({ containerId }) => {
|
|
25
|
+
console.log(`[DockerManager] Container ${containerId} timed out, sending SIGTERM...`);
|
|
26
|
+
await this.stop(containerId, 30);
|
|
27
|
+
});
|
|
28
|
+
this.timeoutManager.on('kill', async ({ containerId }) => {
|
|
29
|
+
console.log(`[DockerManager] Container ${containerId} grace period expired, sending SIGKILL...`);
|
|
30
|
+
await this.kill(containerId);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Run an actor package in a container
|
|
35
|
+
*/
|
|
36
|
+
async run(options) {
|
|
37
|
+
// Validate package
|
|
38
|
+
const validation = (0, validator_js_1.validatePackage)(options.packagePath);
|
|
39
|
+
if (!validation.valid) {
|
|
40
|
+
throw new Error(`Invalid package: ${validation.errors.join(', ')}`);
|
|
41
|
+
}
|
|
42
|
+
const manifest = validation.manifest;
|
|
43
|
+
const startTime = Date.now();
|
|
44
|
+
const memoryMb = options.memoryMb || manifest.maxMemory || 512;
|
|
45
|
+
const timeout = options.timeout || manifest.maxTimeout || 1800;
|
|
46
|
+
// Build environment variables
|
|
47
|
+
const envVars = [];
|
|
48
|
+
if (options.env) {
|
|
49
|
+
for (const [key, value] of Object.entries(options.env)) {
|
|
50
|
+
envVars.push(`${key}=${value}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Build volume mounts
|
|
54
|
+
const binds = [
|
|
55
|
+
`${path_1.default.resolve(options.packagePath)}:/actor/package:ro`,
|
|
56
|
+
];
|
|
57
|
+
if (options.envPath) {
|
|
58
|
+
binds.push(`${path_1.default.resolve(options.envPath)}:/actor/.env:ro`);
|
|
59
|
+
}
|
|
60
|
+
// Create container
|
|
61
|
+
console.log(`[DockerManager] Creating container for ${manifest.name}@${manifest.version}...`);
|
|
62
|
+
const container = await this.docker.createContainer({
|
|
63
|
+
Image: this.imageName,
|
|
64
|
+
Env: envVars,
|
|
65
|
+
HostConfig: {
|
|
66
|
+
Binds: binds,
|
|
67
|
+
Memory: memoryMb * 1024 * 1024, // Convert MB to bytes
|
|
68
|
+
AutoRemove: false, // We'll remove it manually after getting logs
|
|
69
|
+
},
|
|
70
|
+
Labels: {
|
|
71
|
+
'scrapely.actor.name': manifest.name,
|
|
72
|
+
'scrapely.actor.version': manifest.version,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
const containerId = container.id;
|
|
76
|
+
console.log(`[DockerManager] Container created: ${containerId.substring(0, 12)}`);
|
|
77
|
+
// Start timeout timer
|
|
78
|
+
const timeoutOptions = {
|
|
79
|
+
maxTimeout: timeout,
|
|
80
|
+
gracePeriod: 30,
|
|
81
|
+
};
|
|
82
|
+
this.timeoutManager.startTimer(containerId, timeoutOptions);
|
|
83
|
+
// Set log TTL now so the key has the correct expiry from the first byte written
|
|
84
|
+
if (options.dragonflyClient && options.runId) {
|
|
85
|
+
const logManager = new log_manager_js_1.LogManager(options.dragonflyClient);
|
|
86
|
+
await logManager.setLogTtl(options.runId, options.userPlan ?? 'free');
|
|
87
|
+
}
|
|
88
|
+
// Start container
|
|
89
|
+
await container.start();
|
|
90
|
+
console.log(`[DockerManager] Container started with ${timeout}s timeout`);
|
|
91
|
+
// Wait for container to finish
|
|
92
|
+
let exitCode = 0;
|
|
93
|
+
let status = 'success';
|
|
94
|
+
try {
|
|
95
|
+
const result = await container.wait();
|
|
96
|
+
exitCode = result.StatusCode || 0;
|
|
97
|
+
// Check if it was timed out
|
|
98
|
+
const timerState = this.timeoutManager.getState(containerId);
|
|
99
|
+
if (timerState?.status === 'killed') {
|
|
100
|
+
status = 'killed';
|
|
101
|
+
}
|
|
102
|
+
else if (timerState?.status === 'timed_out') {
|
|
103
|
+
status = 'timed_out';
|
|
104
|
+
}
|
|
105
|
+
else if (exitCode !== 0) {
|
|
106
|
+
status = 'failed';
|
|
107
|
+
}
|
|
108
|
+
// Stop the timer
|
|
109
|
+
this.timeoutManager.stopTimer(containerId);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
// Container might have been killed
|
|
113
|
+
const timerState = this.timeoutManager.getState(containerId);
|
|
114
|
+
if (timerState?.status === 'killed') {
|
|
115
|
+
status = 'killed';
|
|
116
|
+
exitCode = 137; // SIGKILL exit code
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
status = 'failed';
|
|
120
|
+
exitCode = 1;
|
|
121
|
+
}
|
|
122
|
+
this.timeoutManager.stopTimer(containerId);
|
|
123
|
+
}
|
|
124
|
+
// Get logs
|
|
125
|
+
const logs = await this.getLogs(containerId);
|
|
126
|
+
// Get network stats and track data transfer before removing container
|
|
127
|
+
await this.trackDataTransfer(container, options);
|
|
128
|
+
// Remove container
|
|
129
|
+
try {
|
|
130
|
+
await container.remove();
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
// Ignore removal errors
|
|
134
|
+
}
|
|
135
|
+
const duration = Math.round((Date.now() - startTime) / 1000);
|
|
136
|
+
console.log(`[DockerManager] Container finished: ${status} (exit code: ${exitCode}, duration: ${duration}s)`);
|
|
137
|
+
// Cleanup after run completes
|
|
138
|
+
const cleanedUp = await this.cleanupAfterRun(options, status);
|
|
139
|
+
return {
|
|
140
|
+
containerId,
|
|
141
|
+
exitCode,
|
|
142
|
+
logs,
|
|
143
|
+
status,
|
|
144
|
+
duration,
|
|
145
|
+
cleanedUp,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Cleanup storage after run completes.
|
|
150
|
+
* - Deletes request queue immediately (no longer needed)
|
|
151
|
+
* - Uploads logs to S3 for long-term storage
|
|
152
|
+
* - Updates run metadata with final status
|
|
153
|
+
*/
|
|
154
|
+
async cleanupAfterRun(options, status) {
|
|
155
|
+
const result = { requestQueueDeleted: false, logsUploadedToS3: false };
|
|
156
|
+
if (!options.dragonflyClient || !options.runId) {
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
// Get run metadata to find storage IDs
|
|
161
|
+
const metadataManager = new run_metadata_js_1.RunMetadataManager(options.dragonflyClient);
|
|
162
|
+
const metadata = await metadataManager.get(options.runId);
|
|
163
|
+
if (metadata) {
|
|
164
|
+
// Delete request queue immediately - it's only useful during execution
|
|
165
|
+
const rqClient = options.dragonflyClient.requestQueue(metadata.storageIds.requestQueue);
|
|
166
|
+
await rqClient.delete();
|
|
167
|
+
result.requestQueueDeleted = true;
|
|
168
|
+
console.log(`[DockerManager] Deleted request queue: ${metadata.storageIds.requestQueue}`);
|
|
169
|
+
// Upload logs to S3 for long-term storage
|
|
170
|
+
const logManager = new log_manager_js_1.LogManager(options.dragonflyClient);
|
|
171
|
+
const s3Key = await logManager.uploadToS3(options.runId, metadata.userId);
|
|
172
|
+
result.logsUploadedToS3 = s3Key !== null;
|
|
173
|
+
// Delete logs from Redis now that they're in S3 — saves RAM.
|
|
174
|
+
// If S3 upload wasn't configured/failed, TTL set at run start handles expiry.
|
|
175
|
+
if (result.logsUploadedToS3) {
|
|
176
|
+
await logManager.deleteLogs(options.runId);
|
|
177
|
+
console.log(`[DockerManager] Deleted Redis logs for run ${options.runId} (persisted to S3)`);
|
|
178
|
+
}
|
|
179
|
+
// Update run metadata with final status
|
|
180
|
+
const runStatus = status === 'success' ? 'success'
|
|
181
|
+
: status === 'timed_out' ? 'timed_out'
|
|
182
|
+
: status === 'killed' ? 'killed'
|
|
183
|
+
: 'failed';
|
|
184
|
+
await metadataManager.finish(options.runId, runStatus);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
console.error(`[DockerManager] Cleanup error: ${error.message}`);
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Stop a container (SIGTERM)
|
|
194
|
+
*/
|
|
195
|
+
async stop(containerId, timeoutSeconds = 30) {
|
|
196
|
+
const container = this.docker.getContainer(containerId);
|
|
197
|
+
try {
|
|
198
|
+
await container.stop({ t: timeoutSeconds });
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
if (error.statusCode !== 304) { // Not modified - already stopped
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Kill a container (SIGKILL)
|
|
208
|
+
*/
|
|
209
|
+
async kill(containerId) {
|
|
210
|
+
const container = this.docker.getContainer(containerId);
|
|
211
|
+
try {
|
|
212
|
+
await container.kill();
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
if (error.statusCode !== 409) { // Not running
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Get logs from a container
|
|
222
|
+
*/
|
|
223
|
+
async getLogs(containerId) {
|
|
224
|
+
const container = this.docker.getContainer(containerId);
|
|
225
|
+
try {
|
|
226
|
+
const logStream = await container.logs({
|
|
227
|
+
stdout: true,
|
|
228
|
+
stderr: true,
|
|
229
|
+
timestamps: false,
|
|
230
|
+
});
|
|
231
|
+
// Docker logs are prefixed with headers, clean them
|
|
232
|
+
return logStream.toString('utf-8').replace(/\x00\x00\x00[\x00-\xff][\x00-\xff][\x00-\xff]/g, '');
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
return '';
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Get container status
|
|
240
|
+
*/
|
|
241
|
+
async getStatus(containerId) {
|
|
242
|
+
const container = this.docker.getContainer(containerId);
|
|
243
|
+
try {
|
|
244
|
+
const info = await container.inspect();
|
|
245
|
+
return info.State.Status;
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* List all scrapely containers
|
|
253
|
+
*/
|
|
254
|
+
async listContainers() {
|
|
255
|
+
const containers = await this.docker.listContainers({
|
|
256
|
+
all: true,
|
|
257
|
+
filters: {
|
|
258
|
+
label: ['scrapely.actor.name'],
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
return containers;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Get the timeout manager
|
|
265
|
+
*/
|
|
266
|
+
getTimeoutManager() {
|
|
267
|
+
return this.timeoutManager;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Track internal data transfer from container network stats.
|
|
271
|
+
* Following Apify's model:
|
|
272
|
+
* - Ingress (rx_bytes) is FREE - scraped content coming in
|
|
273
|
+
* - Internal transfer is tracked separately in storage clients
|
|
274
|
+
* - External egress is tracked in API layer when users fetch results
|
|
275
|
+
*/
|
|
276
|
+
async trackDataTransfer(container, options) {
|
|
277
|
+
// Ingress is free - no billing for data coming into the platform from internet
|
|
278
|
+
// Internal transfer (actor ↔ storage) is tracked in storage clients
|
|
279
|
+
// External egress (platform → user) is tracked in API routes
|
|
280
|
+
// This method is kept for future use if needed
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
exports.DockerManager = DockerManager;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TimeoutManager = exports.DockerManager = void 0;
|
|
4
|
+
var docker_manager_js_1 = require("./docker-manager.js");
|
|
5
|
+
Object.defineProperty(exports, "DockerManager", { enumerable: true, get: function () { return docker_manager_js_1.DockerManager; } });
|
|
6
|
+
var timeout_manager_js_1 = require("./timeout-manager.js");
|
|
7
|
+
Object.defineProperty(exports, "TimeoutManager", { enumerable: true, get: function () { return timeout_manager_js_1.TimeoutManager; } });
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
export interface TimeoutOptions {
|
|
3
|
+
/** Maximum runtime in seconds */
|
|
4
|
+
maxTimeout: number;
|
|
5
|
+
/** Grace period after SIGTERM before SIGKILL (default: 30s) */
|
|
6
|
+
gracePeriod?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface TimerState {
|
|
9
|
+
containerId: string;
|
|
10
|
+
startedAt: Date;
|
|
11
|
+
timeoutAt: Date;
|
|
12
|
+
status: 'running' | 'stopping' | 'timed_out' | 'completed' | 'killed';
|
|
13
|
+
timeoutTimer?: NodeJS.Timeout;
|
|
14
|
+
killTimer?: NodeJS.Timeout;
|
|
15
|
+
}
|
|
16
|
+
export declare class TimeoutManager extends EventEmitter {
|
|
17
|
+
private timers;
|
|
18
|
+
private defaultGracePeriod;
|
|
19
|
+
/**
|
|
20
|
+
* Start a timeout timer for a container
|
|
21
|
+
*/
|
|
22
|
+
startTimer(containerId: string, options: TimeoutOptions): TimerState;
|
|
23
|
+
/**
|
|
24
|
+
* Stop a timer (container completed successfully)
|
|
25
|
+
*/
|
|
26
|
+
stopTimer(containerId: string): void;
|
|
27
|
+
/**
|
|
28
|
+
* Get timer state for a container
|
|
29
|
+
*/
|
|
30
|
+
getState(containerId: string): TimerState | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Get remaining time in seconds for a container
|
|
33
|
+
*/
|
|
34
|
+
getRemainingTime(containerId: string): number | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Get all active timers
|
|
37
|
+
*/
|
|
38
|
+
getActiveTimers(): TimerState[];
|
|
39
|
+
/**
|
|
40
|
+
* Clear all timers
|
|
41
|
+
*/
|
|
42
|
+
clearAll(): void;
|
|
43
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TimeoutManager = void 0;
|
|
4
|
+
const events_1 = require("events");
|
|
5
|
+
class TimeoutManager extends events_1.EventEmitter {
|
|
6
|
+
timers = new Map();
|
|
7
|
+
defaultGracePeriod = 30; // seconds
|
|
8
|
+
/**
|
|
9
|
+
* Start a timeout timer for a container
|
|
10
|
+
*/
|
|
11
|
+
startTimer(containerId, options) {
|
|
12
|
+
// Stop existing timer if any
|
|
13
|
+
this.stopTimer(containerId);
|
|
14
|
+
const startedAt = new Date();
|
|
15
|
+
const timeoutAt = new Date(startedAt.getTime() + options.maxTimeout * 1000);
|
|
16
|
+
const gracePeriod = options.gracePeriod ?? this.defaultGracePeriod;
|
|
17
|
+
const state = {
|
|
18
|
+
containerId,
|
|
19
|
+
startedAt,
|
|
20
|
+
timeoutAt,
|
|
21
|
+
status: 'running',
|
|
22
|
+
};
|
|
23
|
+
// Set up timeout timer
|
|
24
|
+
state.timeoutTimer = setTimeout(async () => {
|
|
25
|
+
this.emit('timeout', { containerId, state });
|
|
26
|
+
state.status = 'timed_out';
|
|
27
|
+
this.emit('stopping', { containerId, gracePeriod });
|
|
28
|
+
// Set up kill timer after timeout
|
|
29
|
+
state.killTimer = setTimeout(() => {
|
|
30
|
+
state.status = 'killed';
|
|
31
|
+
this.emit('kill', { containerId });
|
|
32
|
+
this.timers.delete(containerId);
|
|
33
|
+
}, gracePeriod * 1000);
|
|
34
|
+
}, options.maxTimeout * 1000);
|
|
35
|
+
this.timers.set(containerId, state);
|
|
36
|
+
this.emit('started', { containerId, timeoutAt });
|
|
37
|
+
return state;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Stop a timer (container completed successfully)
|
|
41
|
+
*/
|
|
42
|
+
stopTimer(containerId) {
|
|
43
|
+
const state = this.timers.get(containerId);
|
|
44
|
+
if (!state)
|
|
45
|
+
return;
|
|
46
|
+
// Clear timers
|
|
47
|
+
if (state.timeoutTimer) {
|
|
48
|
+
clearTimeout(state.timeoutTimer);
|
|
49
|
+
}
|
|
50
|
+
if (state.killTimer) {
|
|
51
|
+
clearTimeout(state.killTimer);
|
|
52
|
+
}
|
|
53
|
+
if (state.status === 'running') {
|
|
54
|
+
state.status = 'completed';
|
|
55
|
+
this.emit('completed', { containerId, state });
|
|
56
|
+
}
|
|
57
|
+
this.timers.delete(containerId);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get timer state for a container
|
|
61
|
+
*/
|
|
62
|
+
getState(containerId) {
|
|
63
|
+
return this.timers.get(containerId);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get remaining time in seconds for a container
|
|
67
|
+
*/
|
|
68
|
+
getRemainingTime(containerId) {
|
|
69
|
+
const state = this.timers.get(containerId);
|
|
70
|
+
if (!state || state.status !== 'running')
|
|
71
|
+
return undefined;
|
|
72
|
+
const remaining = Math.max(0, (state.timeoutAt.getTime() - Date.now()) / 1000);
|
|
73
|
+
return remaining;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Get all active timers
|
|
77
|
+
*/
|
|
78
|
+
getActiveTimers() {
|
|
79
|
+
return Array.from(this.timers.values()).filter(s => s.status === 'running');
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Clear all timers
|
|
83
|
+
*/
|
|
84
|
+
clearAll() {
|
|
85
|
+
for (const [containerId] of this.timers) {
|
|
86
|
+
this.stopTimer(containerId);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
exports.TimeoutManager = TimeoutManager;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { ProxyConfiguration as CrawleeProxyConfiguration } from '@crawlee/core';
|
|
2
|
+
import { Configuration } from './configuration.js';
|
|
3
|
+
import { type PlatformProxyEnv, type ProxyConfigurationOptions, type ProxyInfo } from './proxy-configuration.js';
|
|
4
|
+
/** Options accepted by newUrl()/newProxyInfo() (second parameter). */
|
|
5
|
+
type TieredProxyOptions = Parameters<CrawleeProxyConfiguration['newUrl']>[1];
|
|
6
|
+
/** Returns true for platform-proxy sentinel values ('auto', 'http://auto'). */
|
|
7
|
+
export declare function isPlatformProxySentinel(url: string | null | undefined): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Validates a proxy session ID the same way the Apify SDK does: at most 50
|
|
10
|
+
* characters, and only `0-9`, `a-z`, `A-Z`, `.`, `_` and `~`. Throws otherwise.
|
|
11
|
+
*/
|
|
12
|
+
export declare function validateProxySessionId(sessionId: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Resolves and validates the country/subdivision options (Apify parity).
|
|
15
|
+
* Country must be a two-letter ISO code; subdivision an ISO 3166-2 code
|
|
16
|
+
* (1-3 uppercase alphanumeric characters) and requires a country. Returns
|
|
17
|
+
* the normalized values used by the username grammar.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveCountryOptions(options: ProxyConfigurationOptions): {
|
|
20
|
+
countryCode?: string;
|
|
21
|
+
subdivisionCode?: string;
|
|
22
|
+
};
|
|
23
|
+
/** Status payload returned by the gateway status endpoint (Apify format). */
|
|
24
|
+
export interface ProxyStatus {
|
|
25
|
+
connected: boolean;
|
|
26
|
+
connectionError: string;
|
|
27
|
+
isManInTheMiddle: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build a platform proxy URL using the Apify username grammar:
|
|
31
|
+
* http://GROUPS-G1+G2,country-US,session-a1b2c3:PASSWORD@HOST:PORT
|
|
32
|
+
*
|
|
33
|
+
* `groups` (joined with `+`), `country` and `session` fragments are
|
|
34
|
+
* comma-joined. When the server injected a subaccount username
|
|
35
|
+
* (SCRAPELY_PROXY_USERNAME / APIFY_PROXY_USERNAME) it is prepended so the
|
|
36
|
+
* gateway can attribute traffic to the right Scrapely tenant; without it the
|
|
37
|
+
* username falls back to Apify's `auto` when no fragments apply. The password
|
|
38
|
+
* and gateway host/port come from the environment credentials injected by the
|
|
39
|
+
* server.
|
|
40
|
+
*/
|
|
41
|
+
export declare function buildPlatformProxyUrl(options: ProxyConfigurationOptions, env: PlatformProxyEnv, sessionId?: string): string;
|
|
42
|
+
/** True when any proxy URL list reserves a platform slot via the 'auto' sentinel. */
|
|
43
|
+
export declare function hasPlatformSentinel(options: ProxyConfigurationOptions): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Apify-compatible ProxyConfiguration.
|
|
46
|
+
*
|
|
47
|
+
* Modes:
|
|
48
|
+
* - Platform proxy: when the server injected APIFY_PROXY_* or SCRAPELY_PROXY_*
|
|
49
|
+
* credentials (Evomi-backed) and no custom proxies were requested, the
|
|
50
|
+
* configuration routes through the platform gateway using the Apify
|
|
51
|
+
* username grammar (groups/country/session fragments).
|
|
52
|
+
* - BYOP: custom proxyUrls / newUrlFunction / tieredProxyUrls are passed
|
|
53
|
+
* through to Crawlee unchanged.
|
|
54
|
+
* - Mixed: the 'auto' (or 'http://auto') sentinel inside proxyUrls /
|
|
55
|
+
* tieredProxyUrls / tieredProxyConfig[].proxyUrls reserves a slot for the
|
|
56
|
+
* platform proxy alongside BYOP entries. The sentinel is translated to a
|
|
57
|
+
* concrete platform URL lazily on every newUrl() call, so Crawlee's
|
|
58
|
+
* rotation and session-stickiness semantics apply to the mix unchanged.
|
|
59
|
+
*/
|
|
60
|
+
export declare class ProxyConfiguration extends CrawleeProxyConfiguration {
|
|
61
|
+
readonly config: Configuration;
|
|
62
|
+
private readonly platformEnv;
|
|
63
|
+
private readonly platformOptions;
|
|
64
|
+
/** Platform options to use when translating each per-tier placeholder. */
|
|
65
|
+
private readonly tierPlaceholderOptions;
|
|
66
|
+
constructor(options?: ProxyConfigurationOptions, config?: Configuration);
|
|
67
|
+
/**
|
|
68
|
+
* Returns a new proxy URL. Translates the internal platform-proxy
|
|
69
|
+
* placeholder to a concrete platform URL (applying groups/countryCode and
|
|
70
|
+
* session stickiness) whenever Crawlee's rotation lands on it; BYOP
|
|
71
|
+
* entries pass through unchanged. Session IDs are validated like in the
|
|
72
|
+
* Apify SDK (max 50 chars, only `0-9 a-z A-Z . _ ~`).
|
|
73
|
+
*/
|
|
74
|
+
newUrl(sessionId?: string | number, options?: TieredProxyOptions): Promise<string | undefined>;
|
|
75
|
+
/**
|
|
76
|
+
* Returns a new ProxyInfo. When the resolved URL points at the platform
|
|
77
|
+
* gateway, `groups`, `countryCode` and `subdivisionCode` are attached
|
|
78
|
+
* (Apify parity).
|
|
79
|
+
*/
|
|
80
|
+
newProxyInfo(sessionId?: string | number, options?: TieredProxyOptions): Promise<ProxyInfo | undefined>;
|
|
81
|
+
/**
|
|
82
|
+
* Fetches the gateway status payload (Apify format) by sending
|
|
83
|
+
* `GET {statusUrl}/?format=json` *through the platform gateway*, so the
|
|
84
|
+
* proxy handshake itself authenticates the run's credentials. Retries
|
|
85
|
+
* connection errors (Apify parity: 4s timeout, 2 attempts); returns
|
|
86
|
+
* undefined when the endpoint stays unreachable.
|
|
87
|
+
*/
|
|
88
|
+
private fetchProxyStatus;
|
|
89
|
+
/**
|
|
90
|
+
* Verifies the proxy configuration. For platform proxy mode with an
|
|
91
|
+
* injected status URL, performs the Apify-format access check through the
|
|
92
|
+
* gateway (`GET {statusUrl}/?format=json`, best-effort, never fatal);
|
|
93
|
+
* BYOP mode only verifies that a test URL can be generated.
|
|
94
|
+
*/
|
|
95
|
+
initialize(options?: {
|
|
96
|
+
checkAccess?: boolean;
|
|
97
|
+
}): Promise<boolean>;
|
|
98
|
+
}
|
|
99
|
+
export {};
|