scrapely 1.2.32 → 1.2.33

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/solver.js CHANGED
@@ -31,7 +31,7 @@ exports.solve = solve;
31
31
  exports.reportSolveOutcome = reportSolveOutcome;
32
32
  exports.clearSolverStatusCache = clearSolverStatusCache;
33
33
  exports.DEGRADED_STATUS_MESSAGE = 'Site protection temporarily unavailable, results may be incomplete';
34
- const COMPAT_SUFFIXES = ['/compat/v1', '/compat/xhrdev'];
34
+ const COMPAT_SUFFIXES = ['/compat/v1'];
35
35
  const STATUS_TTL_MS = 15_000;
36
36
  const MAX_BODY = 4 * 1024 * 1024;
37
37
  const UNAVAILABLE_CODES = new Set(['vendor_degraded', 'vendor_down', 'provider_unavailable', 'vendor_unsupported', 'solver_not_enabled']);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scrapely",
3
- "version": "1.2.32",
3
+ "version": "1.2.33",
4
4
  "description": "Scrapely SDK for building JavaScript and TypeScript Actors",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,88 +0,0 @@
1
- import Docker from 'dockerode';
2
- import { TimeoutManager } from './timeout-manager.js';
3
- import { DragonflyClient } from '../storage/dragonfly-client.js';
4
- import type { UserPlan } from '../storage/ttl-manager.js';
5
- export interface RunOptions {
6
- /** Path to the actor package directory */
7
- packagePath: string;
8
- /** Path to the .env file */
9
- envPath?: string;
10
- /** Environment variables to pass to container */
11
- env?: Record<string, string>;
12
- /** Maximum memory in MB (default: from manifest or 512) */
13
- memoryMb?: number;
14
- /** Maximum timeout in seconds (default: from manifest or 1800) */
15
- timeout?: number;
16
- /** Run ID for tracking */
17
- runId?: string;
18
- /** User ID */
19
- userId?: string;
20
- /** User plan for TTL */
21
- userPlan?: UserPlan;
22
- /** Dragonfly client for storage cleanup */
23
- dragonflyClient?: DragonflyClient;
24
- }
25
- export interface RunResult {
26
- containerId: string;
27
- exitCode: number;
28
- logs: string;
29
- status: 'success' | 'failed' | 'timed_out' | 'killed';
30
- duration: number;
31
- /** Storage IDs that were cleaned up */
32
- cleanedUp?: {
33
- requestQueueDeleted: boolean;
34
- logsUploadedToS3: boolean;
35
- };
36
- }
37
- export declare class DockerManager {
38
- private docker;
39
- private timeoutManager;
40
- private imageName;
41
- constructor(options?: {
42
- socketPath?: string;
43
- imageName?: string;
44
- });
45
- /**
46
- * Run an actor package in a container
47
- */
48
- run(options: RunOptions): Promise<RunResult>;
49
- /**
50
- * Cleanup storage after run completes.
51
- * - Deletes request queue immediately (no longer needed)
52
- * - Uploads logs to S3 for long-term storage
53
- * - Updates run metadata with final status
54
- */
55
- private cleanupAfterRun;
56
- /**
57
- * Stop a container (SIGTERM)
58
- */
59
- stop(containerId: string, timeoutSeconds?: number): Promise<void>;
60
- /**
61
- * Kill a container (SIGKILL)
62
- */
63
- kill(containerId: string): Promise<void>;
64
- /**
65
- * Get logs from a container
66
- */
67
- getLogs(containerId: string): Promise<string>;
68
- /**
69
- * Get container status
70
- */
71
- getStatus(containerId: string): Promise<string | null>;
72
- /**
73
- * List all scrapely containers
74
- */
75
- listContainers(): Promise<Docker.ContainerInfo[]>;
76
- /**
77
- * Get the timeout manager
78
- */
79
- getTimeoutManager(): TimeoutManager;
80
- /**
81
- * Track internal data transfer from container network stats.
82
- * Following Apify's model:
83
- * - Ingress (rx_bytes) is FREE - scraped content coming in
84
- * - Internal transfer is tracked separately in storage clients
85
- * - External egress is tracked in API layer when users fetch results
86
- */
87
- private trackDataTransfer;
88
- }
@@ -1,283 +0,0 @@
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;
@@ -1,2 +0,0 @@
1
- export { DockerManager, RunOptions, RunResult } from './docker-manager.js';
2
- export { TimeoutManager, TimeoutOptions, TimerState } from './timeout-manager.js';
@@ -1,7 +0,0 @@
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; } });
@@ -1,43 +0,0 @@
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
- }
@@ -1,90 +0,0 @@
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;