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,103 @@
|
|
|
1
|
+
import type { UserPlan } from './ttl-manager.js';
|
|
2
|
+
/**
|
|
3
|
+
* S3 storage configuration for Hetzner Object Storage
|
|
4
|
+
*/
|
|
5
|
+
export interface S3Config {
|
|
6
|
+
endpoint: string;
|
|
7
|
+
accessKey: string;
|
|
8
|
+
secretKey: string;
|
|
9
|
+
bucket: string;
|
|
10
|
+
region?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Metadata stored with each S3 log object
|
|
14
|
+
*/
|
|
15
|
+
export interface LogMetadata {
|
|
16
|
+
userId: string;
|
|
17
|
+
runId: string;
|
|
18
|
+
createdAt: string;
|
|
19
|
+
expiresAt: string;
|
|
20
|
+
userPlan: UserPlan;
|
|
21
|
+
contentType: string;
|
|
22
|
+
size?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* S3 client for log storage with Hetzner Object Storage support.
|
|
26
|
+
*
|
|
27
|
+
* Storage model:
|
|
28
|
+
* - Path: logs/{userId}/{runId}.log
|
|
29
|
+
* - Metadata includes TTL info for cleanup job
|
|
30
|
+
* - Supports streaming for large logs
|
|
31
|
+
*/
|
|
32
|
+
export declare class S3LogClient {
|
|
33
|
+
private client;
|
|
34
|
+
private bucket;
|
|
35
|
+
constructor(config: S3Config);
|
|
36
|
+
/**
|
|
37
|
+
* Get the S3 key for a run's log
|
|
38
|
+
*/
|
|
39
|
+
private getLogKey;
|
|
40
|
+
/**
|
|
41
|
+
* Calculate expiration timestamp based on user plan
|
|
42
|
+
*/
|
|
43
|
+
private calculateExpiresAt;
|
|
44
|
+
/**
|
|
45
|
+
* Upload log content to S3 with metadata for TTL tracking.
|
|
46
|
+
* Called when a run completes.
|
|
47
|
+
*/
|
|
48
|
+
uploadLog(userId: string, runId: string, content: string | Buffer, userPlan: UserPlan): Promise<{
|
|
49
|
+
key: string;
|
|
50
|
+
metadata: LogMetadata;
|
|
51
|
+
}>;
|
|
52
|
+
/**
|
|
53
|
+
* Get log content from S3.
|
|
54
|
+
* Returns null if not found.
|
|
55
|
+
*/
|
|
56
|
+
getLog(userId: string, runId: string): Promise<string | null>;
|
|
57
|
+
/**
|
|
58
|
+
* Get log as a stream for large logs.
|
|
59
|
+
*/
|
|
60
|
+
getLogStream(userId: string, runId: string): Promise<NodeJS.ReadableStream | null>;
|
|
61
|
+
/**
|
|
62
|
+
* Get metadata for a log object without fetching content.
|
|
63
|
+
*/
|
|
64
|
+
getLogMetadata(userId: string, runId: string): Promise<LogMetadata | null>;
|
|
65
|
+
/**
|
|
66
|
+
* Delete a log from S3.
|
|
67
|
+
*/
|
|
68
|
+
deleteLog(userId: string, runId: string): Promise<boolean>;
|
|
69
|
+
/**
|
|
70
|
+
* List logs that have expired (for cleanup job).
|
|
71
|
+
* Returns up to 1000 expired logs at a time.
|
|
72
|
+
*/
|
|
73
|
+
listExpiredLogs(): Promise<Array<{
|
|
74
|
+
key: string;
|
|
75
|
+
userId: string;
|
|
76
|
+
runId: string;
|
|
77
|
+
expiresAt: string;
|
|
78
|
+
}>>;
|
|
79
|
+
/**
|
|
80
|
+
* Delete multiple expired logs in batch.
|
|
81
|
+
* Returns count of deleted objects.
|
|
82
|
+
*/
|
|
83
|
+
deleteExpiredLogs(): Promise<{
|
|
84
|
+
deleted: number;
|
|
85
|
+
errors: number;
|
|
86
|
+
}>;
|
|
87
|
+
/**
|
|
88
|
+
* Check if S3 is properly configured and accessible.
|
|
89
|
+
*/
|
|
90
|
+
healthCheck(): Promise<boolean>;
|
|
91
|
+
/**
|
|
92
|
+
* Create S3Config from environment variables.
|
|
93
|
+
*/
|
|
94
|
+
static fromEnv(): S3Config;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Get or create the S3 log client singleton.
|
|
98
|
+
*/
|
|
99
|
+
export declare function getS3LogClient(): S3LogClient | null;
|
|
100
|
+
/**
|
|
101
|
+
* Initialize S3 client with explicit config.
|
|
102
|
+
*/
|
|
103
|
+
export declare function initS3LogClient(config: S3Config): S3LogClient;
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.S3LogClient = void 0;
|
|
4
|
+
exports.getS3LogClient = getS3LogClient;
|
|
5
|
+
exports.initS3LogClient = initS3LogClient;
|
|
6
|
+
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
7
|
+
/**
|
|
8
|
+
* TTL for logs based on user plan (in seconds)
|
|
9
|
+
*/
|
|
10
|
+
const LOG_TTL = {
|
|
11
|
+
free: 7 * 24 * 60 * 60, // 7 days
|
|
12
|
+
paid: 31 * 24 * 60 * 60, // 31 days
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* S3 client for log storage with Hetzner Object Storage support.
|
|
16
|
+
*
|
|
17
|
+
* Storage model:
|
|
18
|
+
* - Path: logs/{userId}/{runId}.log
|
|
19
|
+
* - Metadata includes TTL info for cleanup job
|
|
20
|
+
* - Supports streaming for large logs
|
|
21
|
+
*/
|
|
22
|
+
class S3LogClient {
|
|
23
|
+
client;
|
|
24
|
+
bucket;
|
|
25
|
+
constructor(config) {
|
|
26
|
+
this.bucket = config.bucket;
|
|
27
|
+
this.client = new client_s3_1.S3Client({
|
|
28
|
+
endpoint: config.endpoint,
|
|
29
|
+
region: config.region || 'us-east-1',
|
|
30
|
+
credentials: {
|
|
31
|
+
accessKeyId: config.accessKey,
|
|
32
|
+
secretAccessKey: config.secretKey,
|
|
33
|
+
},
|
|
34
|
+
forcePathStyle: true, // Required for Hetzner S3-compatible storage
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get the S3 key for a run's log
|
|
39
|
+
*/
|
|
40
|
+
getLogKey(userId, runId) {
|
|
41
|
+
return `logs/${userId}/${runId}.log`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Calculate expiration timestamp based on user plan
|
|
45
|
+
*/
|
|
46
|
+
calculateExpiresAt(userPlan) {
|
|
47
|
+
const ttlSeconds = LOG_TTL[userPlan];
|
|
48
|
+
const expiresAt = new Date(Date.now() + ttlSeconds * 1000);
|
|
49
|
+
return expiresAt.toISOString();
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Upload log content to S3 with metadata for TTL tracking.
|
|
53
|
+
* Called when a run completes.
|
|
54
|
+
*/
|
|
55
|
+
async uploadLog(userId, runId, content, userPlan) {
|
|
56
|
+
const now = new Date();
|
|
57
|
+
const key = this.getLogKey(userId, runId);
|
|
58
|
+
const expiresAt = this.calculateExpiresAt(userPlan);
|
|
59
|
+
const buffer = typeof content === 'string' ? Buffer.from(content, 'utf-8') : content;
|
|
60
|
+
const metadata = {
|
|
61
|
+
userId,
|
|
62
|
+
runId,
|
|
63
|
+
createdAt: now.toISOString(),
|
|
64
|
+
expiresAt,
|
|
65
|
+
userPlan,
|
|
66
|
+
contentType: 'text/plain; charset=utf-8',
|
|
67
|
+
size: buffer.length,
|
|
68
|
+
};
|
|
69
|
+
await this.client.send(new client_s3_1.PutObjectCommand({
|
|
70
|
+
Bucket: this.bucket,
|
|
71
|
+
Key: key,
|
|
72
|
+
Body: buffer,
|
|
73
|
+
ContentType: 'text/plain; charset=utf-8',
|
|
74
|
+
Metadata: {
|
|
75
|
+
'user-id': userId,
|
|
76
|
+
'run-id': runId,
|
|
77
|
+
'created-at': metadata.createdAt,
|
|
78
|
+
'expires-at': expiresAt,
|
|
79
|
+
'user-plan': userPlan,
|
|
80
|
+
},
|
|
81
|
+
}));
|
|
82
|
+
return { key, metadata };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Get log content from S3.
|
|
86
|
+
* Returns null if not found.
|
|
87
|
+
*/
|
|
88
|
+
async getLog(userId, runId) {
|
|
89
|
+
const key = this.getLogKey(userId, runId);
|
|
90
|
+
try {
|
|
91
|
+
const response = await this.client.send(new client_s3_1.GetObjectCommand({
|
|
92
|
+
Bucket: this.bucket,
|
|
93
|
+
Key: key,
|
|
94
|
+
}));
|
|
95
|
+
if (!response.Body)
|
|
96
|
+
return null;
|
|
97
|
+
const content = await response.Body.transformToString('utf-8');
|
|
98
|
+
return content;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (error.name === 'NoSuchKey' || error.$metadata?.httpStatusCode === 404) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get log as a stream for large logs.
|
|
109
|
+
*/
|
|
110
|
+
async getLogStream(userId, runId) {
|
|
111
|
+
const key = this.getLogKey(userId, runId);
|
|
112
|
+
try {
|
|
113
|
+
const response = await this.client.send(new client_s3_1.GetObjectCommand({
|
|
114
|
+
Bucket: this.bucket,
|
|
115
|
+
Key: key,
|
|
116
|
+
}));
|
|
117
|
+
if (!response.Body)
|
|
118
|
+
return null;
|
|
119
|
+
// Convert WebStream to NodeJS ReadableStream if needed
|
|
120
|
+
return response.Body;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (error.name === 'NoSuchKey' || error.$metadata?.httpStatusCode === 404) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Get metadata for a log object without fetching content.
|
|
131
|
+
*/
|
|
132
|
+
async getLogMetadata(userId, runId) {
|
|
133
|
+
const key = this.getLogKey(userId, runId);
|
|
134
|
+
try {
|
|
135
|
+
const response = await this.client.send(new client_s3_1.HeadObjectCommand({
|
|
136
|
+
Bucket: this.bucket,
|
|
137
|
+
Key: key,
|
|
138
|
+
}));
|
|
139
|
+
const metadata = response.Metadata || {};
|
|
140
|
+
return {
|
|
141
|
+
userId: metadata['user-id'] || userId,
|
|
142
|
+
runId: metadata['run-id'] || runId,
|
|
143
|
+
createdAt: metadata['created-at'] || new Date().toISOString(),
|
|
144
|
+
expiresAt: metadata['expires-at'] || '',
|
|
145
|
+
userPlan: metadata['user-plan'] || 'free',
|
|
146
|
+
contentType: 'text/plain; charset=utf-8',
|
|
147
|
+
size: response.ContentLength,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
if (error.$metadata?.httpStatusCode === 404) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Delete a log from S3.
|
|
159
|
+
*/
|
|
160
|
+
async deleteLog(userId, runId) {
|
|
161
|
+
const key = this.getLogKey(userId, runId);
|
|
162
|
+
try {
|
|
163
|
+
await this.client.send(new client_s3_1.DeleteObjectCommand({
|
|
164
|
+
Bucket: this.bucket,
|
|
165
|
+
Key: key,
|
|
166
|
+
}));
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (error.$metadata?.httpStatusCode === 404) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* List logs that have expired (for cleanup job).
|
|
178
|
+
* Returns up to 1000 expired logs at a time.
|
|
179
|
+
*/
|
|
180
|
+
async listExpiredLogs() {
|
|
181
|
+
const now = new Date();
|
|
182
|
+
const expiredLogs = [];
|
|
183
|
+
let continuationToken;
|
|
184
|
+
do {
|
|
185
|
+
const response = await this.client.send(new client_s3_1.ListObjectsV2Command({
|
|
186
|
+
Bucket: this.bucket,
|
|
187
|
+
Prefix: 'logs/',
|
|
188
|
+
ContinuationToken: continuationToken,
|
|
189
|
+
}));
|
|
190
|
+
if (!response.Contents)
|
|
191
|
+
break;
|
|
192
|
+
for (const object of response.Contents) {
|
|
193
|
+
if (!object.Key)
|
|
194
|
+
continue;
|
|
195
|
+
// Parse metadata to check expiration
|
|
196
|
+
try {
|
|
197
|
+
const headResponse = await this.client.send(new client_s3_1.HeadObjectCommand({
|
|
198
|
+
Bucket: this.bucket,
|
|
199
|
+
Key: object.Key,
|
|
200
|
+
}));
|
|
201
|
+
const expiresAtStr = headResponse.Metadata?.['expires-at'];
|
|
202
|
+
if (!expiresAtStr)
|
|
203
|
+
continue;
|
|
204
|
+
const expiresAt = new Date(expiresAtStr);
|
|
205
|
+
if (expiresAt <= now) {
|
|
206
|
+
// Parse userId and runId from key: logs/{userId}/{runId}.log
|
|
207
|
+
const parts = object.Key.split('/');
|
|
208
|
+
if (parts.length >= 3) {
|
|
209
|
+
const userId = parts[1];
|
|
210
|
+
const runId = parts[2].replace('.log', '');
|
|
211
|
+
expiredLogs.push({
|
|
212
|
+
key: object.Key,
|
|
213
|
+
userId,
|
|
214
|
+
runId,
|
|
215
|
+
expiresAt: expiresAtStr,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// Skip objects we can't read metadata for
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
continuationToken = response.NextContinuationToken;
|
|
226
|
+
} while (continuationToken);
|
|
227
|
+
return expiredLogs;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Delete multiple expired logs in batch.
|
|
231
|
+
* Returns count of deleted objects.
|
|
232
|
+
*/
|
|
233
|
+
async deleteExpiredLogs() {
|
|
234
|
+
const expiredLogs = await this.listExpiredLogs();
|
|
235
|
+
let deleted = 0;
|
|
236
|
+
let errors = 0;
|
|
237
|
+
for (const log of expiredLogs) {
|
|
238
|
+
try {
|
|
239
|
+
await this.deleteLog(log.userId, log.runId);
|
|
240
|
+
deleted++;
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
errors++;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return { deleted, errors };
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Check if S3 is properly configured and accessible.
|
|
250
|
+
*/
|
|
251
|
+
async healthCheck() {
|
|
252
|
+
try {
|
|
253
|
+
await this.client.send(new client_s3_1.HeadObjectCommand({
|
|
254
|
+
Bucket: this.bucket,
|
|
255
|
+
Key: '__health_check__',
|
|
256
|
+
}));
|
|
257
|
+
return true; // Object exists or bucket is accessible
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
// NoSuchKey means bucket exists but object doesn't - still healthy
|
|
261
|
+
if (error.name === 'NoSuchKey' || error.$metadata?.httpStatusCode === 404) {
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Create S3Config from environment variables.
|
|
269
|
+
*/
|
|
270
|
+
static fromEnv() {
|
|
271
|
+
return {
|
|
272
|
+
endpoint: process.env.S3_ENDPOINT || 'https://nbg1.your-objectstorage.com',
|
|
273
|
+
accessKey: process.env.S3_ACCESS_KEY || '',
|
|
274
|
+
secretKey: process.env.S3_SECRET_KEY || '',
|
|
275
|
+
bucket: process.env.S3_BUCKET || 'scrapely-logs',
|
|
276
|
+
region: process.env.S3_REGION || 'us-east-1',
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
exports.S3LogClient = S3LogClient;
|
|
281
|
+
// Singleton instance
|
|
282
|
+
let s3LogClient = null;
|
|
283
|
+
/**
|
|
284
|
+
* Get or create the S3 log client singleton.
|
|
285
|
+
*/
|
|
286
|
+
function getS3LogClient() {
|
|
287
|
+
const config = S3LogClient.fromEnv();
|
|
288
|
+
if (!config.accessKey || !config.secretKey) {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
if (!s3LogClient) {
|
|
292
|
+
s3LogClient = new S3LogClient(config);
|
|
293
|
+
}
|
|
294
|
+
return s3LogClient;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Initialize S3 client with explicit config.
|
|
298
|
+
*/
|
|
299
|
+
function initS3LogClient(config) {
|
|
300
|
+
s3LogClient = new S3LogClient(config);
|
|
301
|
+
return s3LogClient;
|
|
302
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type Redis from 'ioredis';
|
|
2
|
+
/**
|
|
3
|
+
* TTL configuration in seconds
|
|
4
|
+
*/
|
|
5
|
+
export interface TtlConfig {
|
|
6
|
+
/** TTL for unnamed datasets (free users) - default 7 days */
|
|
7
|
+
datasetFree: number;
|
|
8
|
+
/** TTL for unnamed datasets (paid users) - default 31 days */
|
|
9
|
+
datasetPaid: number;
|
|
10
|
+
/** TTL for unnamed KV stores (free users) - default 7 days */
|
|
11
|
+
kvFree: number;
|
|
12
|
+
/** TTL for unnamed KV stores (paid users) - default 31 days */
|
|
13
|
+
kvPaid: number;
|
|
14
|
+
/** Fallback TTL for request queues (safety net for orphaned RQs) - default 24 hours */
|
|
15
|
+
requestQueueFallback: number;
|
|
16
|
+
/** TTL for logs in Redis (free users) - default 12 hours */
|
|
17
|
+
logsFree: number;
|
|
18
|
+
/** TTL for logs in Redis (paid users) - default 7 days */
|
|
19
|
+
logsPaid: number;
|
|
20
|
+
/** Maximum log size in bytes - default 5MB */
|
|
21
|
+
logMaxSize: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Default TTL configuration based on Apify's model
|
|
25
|
+
*/
|
|
26
|
+
export declare const DEFAULT_TTL_CONFIG: TtlConfig;
|
|
27
|
+
export type StorageType = 'dataset' | 'kv' | 'rq' | 'logs';
|
|
28
|
+
export type UserPlan = 'free' | 'paid';
|
|
29
|
+
/**
|
|
30
|
+
* Manages TTL (Time-To-Live) for Dragonfly storage keys.
|
|
31
|
+
*
|
|
32
|
+
* Retention policy:
|
|
33
|
+
* - Named storage: Indefinite (billed), no TTL set
|
|
34
|
+
* - Unnamed datasets/KV stores: 7 days (free) or 31 days (paid)
|
|
35
|
+
* - Request queues: Explicitly deleted after run, 24h fallback TTL for orphaned RQs
|
|
36
|
+
* - Logs: Short TTL in Redis, persisted to S3
|
|
37
|
+
*/
|
|
38
|
+
export declare class TtlManager {
|
|
39
|
+
private config;
|
|
40
|
+
constructor(config?: Partial<TtlConfig>);
|
|
41
|
+
/**
|
|
42
|
+
* Get the TTL for a storage type based on user plan and whether it's named.
|
|
43
|
+
* Returns null for named storage (no TTL = indefinite).
|
|
44
|
+
*/
|
|
45
|
+
getTtl(storageType: 'dataset' | 'kv', plan: UserPlan, isNamed: boolean): number | null;
|
|
46
|
+
getTtl(storageType: 'rq', plan: UserPlan): number;
|
|
47
|
+
getTtl(storageType: 'logs', plan: UserPlan): number;
|
|
48
|
+
/**
|
|
49
|
+
* Set expiry on multiple keys atomically using a pipeline.
|
|
50
|
+
* Uses 'NX' flag to only set TTL if it doesn't already exist.
|
|
51
|
+
*/
|
|
52
|
+
setExpiry(redis: Redis, keys: string[], ttl: number): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Set expiry on multiple keys, overwriting any existing TTL.
|
|
55
|
+
* Use this when you want to guarantee the TTL is set to a specific value.
|
|
56
|
+
*/
|
|
57
|
+
setExpiryForce(redis: Redis, keys: string[], ttl: number): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Get the maximum log size in bytes
|
|
60
|
+
*/
|
|
61
|
+
getLogMaxSize(): number;
|
|
62
|
+
/**
|
|
63
|
+
* Get the full TTL config
|
|
64
|
+
*/
|
|
65
|
+
getConfig(): Readonly<TtlConfig>;
|
|
66
|
+
/**
|
|
67
|
+
* Create TtlConfig from environment variables
|
|
68
|
+
*/
|
|
69
|
+
static fromEnv(): TtlConfig;
|
|
70
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TtlManager = exports.DEFAULT_TTL_CONFIG = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Default TTL configuration based on Apify's model
|
|
6
|
+
*/
|
|
7
|
+
exports.DEFAULT_TTL_CONFIG = {
|
|
8
|
+
datasetFree: 7 * 24 * 60 * 60, // 7 days
|
|
9
|
+
datasetPaid: 31 * 24 * 60 * 60, // 31 days
|
|
10
|
+
kvFree: 7 * 24 * 60 * 60, // 7 days
|
|
11
|
+
kvPaid: 31 * 24 * 60 * 60, // 31 days
|
|
12
|
+
requestQueueFallback: 24 * 60 * 60, // 24 hours
|
|
13
|
+
logsFree: 12 * 60 * 60, // 12 hours
|
|
14
|
+
logsPaid: 7 * 24 * 60 * 60, // 7 days
|
|
15
|
+
logMaxSize: 5 * 1024 * 1024, // 5MB
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Manages TTL (Time-To-Live) for Dragonfly storage keys.
|
|
19
|
+
*
|
|
20
|
+
* Retention policy:
|
|
21
|
+
* - Named storage: Indefinite (billed), no TTL set
|
|
22
|
+
* - Unnamed datasets/KV stores: 7 days (free) or 31 days (paid)
|
|
23
|
+
* - Request queues: Explicitly deleted after run, 24h fallback TTL for orphaned RQs
|
|
24
|
+
* - Logs: Short TTL in Redis, persisted to S3
|
|
25
|
+
*/
|
|
26
|
+
class TtlManager {
|
|
27
|
+
config;
|
|
28
|
+
constructor(config) {
|
|
29
|
+
this.config = { ...exports.DEFAULT_TTL_CONFIG, ...config };
|
|
30
|
+
}
|
|
31
|
+
getTtl(storageType, plan, isNamed) {
|
|
32
|
+
// Named storage gets no TTL (indefinite retention, billed)
|
|
33
|
+
if ((storageType === 'dataset' || storageType === 'kv') && isNamed) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
switch (storageType) {
|
|
37
|
+
case 'dataset':
|
|
38
|
+
return plan === 'paid' ? this.config.datasetPaid : this.config.datasetFree;
|
|
39
|
+
case 'kv':
|
|
40
|
+
return plan === 'paid' ? this.config.kvPaid : this.config.kvFree;
|
|
41
|
+
case 'rq':
|
|
42
|
+
return this.config.requestQueueFallback;
|
|
43
|
+
case 'logs':
|
|
44
|
+
return plan === 'paid' ? this.config.logsPaid : this.config.logsFree;
|
|
45
|
+
default:
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Set expiry on multiple keys atomically using a pipeline.
|
|
51
|
+
* Uses 'NX' flag to only set TTL if it doesn't already exist.
|
|
52
|
+
*/
|
|
53
|
+
async setExpiry(redis, keys, ttl) {
|
|
54
|
+
if (keys.length === 0 || ttl <= 0)
|
|
55
|
+
return;
|
|
56
|
+
const pipeline = redis.pipeline();
|
|
57
|
+
for (const key of keys) {
|
|
58
|
+
// 'NX' = only set expiry if key has no expiry
|
|
59
|
+
// This prevents accidentally shortening TTL on access
|
|
60
|
+
pipeline.expire(key, ttl, 'NX');
|
|
61
|
+
}
|
|
62
|
+
await pipeline.exec();
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Set expiry on multiple keys, overwriting any existing TTL.
|
|
66
|
+
* Use this when you want to guarantee the TTL is set to a specific value.
|
|
67
|
+
*/
|
|
68
|
+
async setExpiryForce(redis, keys, ttl) {
|
|
69
|
+
if (keys.length === 0 || ttl <= 0)
|
|
70
|
+
return;
|
|
71
|
+
const pipeline = redis.pipeline();
|
|
72
|
+
for (const key of keys) {
|
|
73
|
+
pipeline.expire(key, ttl);
|
|
74
|
+
}
|
|
75
|
+
await pipeline.exec();
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get the maximum log size in bytes
|
|
79
|
+
*/
|
|
80
|
+
getLogMaxSize() {
|
|
81
|
+
return this.config.logMaxSize;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Get the full TTL config
|
|
85
|
+
*/
|
|
86
|
+
getConfig() {
|
|
87
|
+
return { ...this.config };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Create TtlConfig from environment variables
|
|
91
|
+
*/
|
|
92
|
+
static fromEnv() {
|
|
93
|
+
const parseEnv = (key, defaultValue) => {
|
|
94
|
+
const val = process.env[key];
|
|
95
|
+
if (val) {
|
|
96
|
+
const parsed = parseInt(val, 10);
|
|
97
|
+
if (!isNaN(parsed) && parsed > 0)
|
|
98
|
+
return parsed;
|
|
99
|
+
}
|
|
100
|
+
return defaultValue;
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
datasetFree: parseEnv('SCRAPELY_TTL_DATASET_FREE', exports.DEFAULT_TTL_CONFIG.datasetFree),
|
|
104
|
+
datasetPaid: parseEnv('SCRAPELY_TTL_DATASET_PAID', exports.DEFAULT_TTL_CONFIG.datasetPaid),
|
|
105
|
+
// Bug fix: these two env vars were silently ignored before
|
|
106
|
+
kvFree: parseEnv('SCRAPELY_TTL_KV_FREE', exports.DEFAULT_TTL_CONFIG.kvFree),
|
|
107
|
+
kvPaid: parseEnv('SCRAPELY_TTL_KV_PAID', exports.DEFAULT_TTL_CONFIG.kvPaid),
|
|
108
|
+
requestQueueFallback: parseEnv('SCRAPELY_TTL_RQ_FALLBACK', exports.DEFAULT_TTL_CONFIG.requestQueueFallback),
|
|
109
|
+
logsFree: parseEnv('SCRAPELY_TTL_LOGS_FREE', exports.DEFAULT_TTL_CONFIG.logsFree),
|
|
110
|
+
logsPaid: parseEnv('SCRAPELY_TTL_LOGS_PAID', exports.DEFAULT_TTL_CONFIG.logsPaid),
|
|
111
|
+
logMaxSize: parseEnv('SCRAPELY_LOG_MAX_SIZE', exports.DEFAULT_TTL_CONFIG.logMaxSize),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
exports.TtlManager = TtlManager;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.uniqueKeyToRequestId = uniqueKeyToRequestId;
|
|
4
|
+
exports.generateId = generateId;
|
|
5
|
+
exports.nowISO = nowISO;
|
|
6
|
+
const node_crypto_1 = require("node:crypto");
|
|
7
|
+
function uniqueKeyToRequestId(uniqueKey) {
|
|
8
|
+
return (0, node_crypto_1.createHash)('sha256')
|
|
9
|
+
.update(uniqueKey)
|
|
10
|
+
.digest('base64')
|
|
11
|
+
.replace(/(\+|\/|=)/g, '')
|
|
12
|
+
.slice(0, 15);
|
|
13
|
+
}
|
|
14
|
+
function generateId() {
|
|
15
|
+
return (0, node_crypto_1.randomUUID)();
|
|
16
|
+
}
|
|
17
|
+
function nowISO() {
|
|
18
|
+
return new Date();
|
|
19
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage identifier types (Apify-compatible names, so TypeScript actors importing
|
|
3
|
+
* them from 'apify' still compile after the apify → scrapely rewrite).
|
|
4
|
+
*/
|
|
5
|
+
/** Identifies a storage by its alias from the Actor's schema storages. */
|
|
6
|
+
export interface StorageAlias {
|
|
7
|
+
alias: string;
|
|
8
|
+
}
|
|
9
|
+
/** Identifies a storage by its platform ID. */
|
|
10
|
+
export interface StorageId {
|
|
11
|
+
id: string;
|
|
12
|
+
}
|
|
13
|
+
/** Identifies a storage by its name. */
|
|
14
|
+
export interface StorageName {
|
|
15
|
+
name: string;
|
|
16
|
+
}
|
|
17
|
+
/** A plain string (ID or name), `{ alias }`, `{ id }` or `{ name }`. */
|
|
18
|
+
export type StorageIdentifier = string | StorageAlias | StorageId | StorageName;
|
|
19
|
+
/** A plain string (ID or name), `{ id }` or `{ name }` (key-value stores and request queues). */
|
|
20
|
+
export type StorageIdentifierWithoutAlias = string | StorageId | StorageName;
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apify v2-compatible utility functions for scrapely.
|
|
3
|
+
* These adapt the Apify v2 API to Crawlee v3 internally.
|
|
4
|
+
*/
|
|
5
|
+
import type { CheerioAPI } from 'cheerio';
|
|
6
|
+
import type { RequestQueue } from 'crawlee';
|
|
7
|
+
/**
|
|
8
|
+
* Options for enqueueLinks (Apify v2 API compatible).
|
|
9
|
+
*/
|
|
10
|
+
export interface EnqueueLinksOptions {
|
|
11
|
+
/** Cheerio instance ($) - REQUIRED in Apify v2 */
|
|
12
|
+
$: CheerioAPI;
|
|
13
|
+
/** RequestQueue instance - REQUIRED in Apify v2 */
|
|
14
|
+
requestQueue: RequestQueue;
|
|
15
|
+
/** CSS selector to find links. Default: 'a[href]' */
|
|
16
|
+
selector?: string;
|
|
17
|
+
/** Base URL for resolving relative URLs */
|
|
18
|
+
baseUrl?: string;
|
|
19
|
+
/** Optional userData to merge into the queued requests */
|
|
20
|
+
userData?: Record<string, any>;
|
|
21
|
+
/** Optional transformation function for each URL */
|
|
22
|
+
transformRequestFunction?: (request: {
|
|
23
|
+
url: string;
|
|
24
|
+
userData?: Record<string, any>;
|
|
25
|
+
}) => {
|
|
26
|
+
url: string;
|
|
27
|
+
userData?: Record<string, any>;
|
|
28
|
+
} | null;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Enqueues links found on the page using a CSS selector.
|
|
32
|
+
*
|
|
33
|
+
* This is an Apify v2-compatible implementation that adapts to Crawlee v3 internally.
|
|
34
|
+
* It extracts URLs from the page using cheerio and adds them to the request queue.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* // Inside a CheerioCrawler requestHandler:
|
|
39
|
+
* await Apify.utils.enqueueLinks({
|
|
40
|
+
* $,
|
|
41
|
+
* requestQueue,
|
|
42
|
+
* selector: 'a[href*="/app/"]',
|
|
43
|
+
* baseUrl: new URL(request.url).origin,
|
|
44
|
+
* });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function enqueueLinks(options: EnqueueLinksOptions): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Utility namespace providing Apify v2-compatible helper functions.
|
|
50
|
+
* These adapt to Crawlee v3 APIs internally.
|
|
51
|
+
*/
|
|
52
|
+
export declare const utils: {
|
|
53
|
+
enqueueLinks: typeof enqueueLinks;
|
|
54
|
+
};
|