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,114 @@
|
|
|
1
|
+
import type { DragonflyClient } from './dragonfly-client.js';
|
|
2
|
+
import type { UserPlan } from './ttl-manager.js';
|
|
3
|
+
/**
|
|
4
|
+
* Log configuration
|
|
5
|
+
*/
|
|
6
|
+
export interface LogConfig {
|
|
7
|
+
/** Maximum log size in bytes before truncation (default: 5MB) */
|
|
8
|
+
maxLogSize: number;
|
|
9
|
+
/** S3 bucket for long-term log storage */
|
|
10
|
+
s3Bucket?: string;
|
|
11
|
+
/** S3 region */
|
|
12
|
+
s3Region?: string;
|
|
13
|
+
/** S3 endpoint (for non-AWS S3-compatible storage) */
|
|
14
|
+
s3Endpoint?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Manages log storage in Dragonfly with optional S3 archival.
|
|
18
|
+
*
|
|
19
|
+
* Log lifecycle:
|
|
20
|
+
* 1. Logs are written to Dragonfly during execution (fast, real-time tailing)
|
|
21
|
+
* 2. Log size is capped at maxLogSize (trailing portion kept, truncated flag set)
|
|
22
|
+
* 3. After run completes, logs are uploaded to S3 for long-term storage
|
|
23
|
+
* 4. TTL in Redis is short (12h free, 7d paid) since S3 is the source of truth
|
|
24
|
+
*/
|
|
25
|
+
export declare class LogManager {
|
|
26
|
+
private client;
|
|
27
|
+
private config;
|
|
28
|
+
constructor(client: DragonflyClient, config?: Partial<LogConfig>);
|
|
29
|
+
/**
|
|
30
|
+
* Get the Redis key for a run's logs
|
|
31
|
+
*/
|
|
32
|
+
private getLogKey;
|
|
33
|
+
/**
|
|
34
|
+
* Get the Redis key for the truncation flag on a run's logs.
|
|
35
|
+
* Set to '1' the first time the log buffer overflows maxLogSize.
|
|
36
|
+
*/
|
|
37
|
+
private getTruncatedKey;
|
|
38
|
+
/**
|
|
39
|
+
* Advance a byte offset forward until it points to the start of a valid
|
|
40
|
+
* UTF-8 code-point boundary in `buf`. Continuation bytes (10xxxxxx) are
|
|
41
|
+
* skipped so we never cut a multibyte sequence in half.
|
|
42
|
+
*
|
|
43
|
+
* Returns the adjusted offset (always ≤ buf.length).
|
|
44
|
+
*/
|
|
45
|
+
private static alignUtf8Boundary;
|
|
46
|
+
/**
|
|
47
|
+
* Append log data to a run's log buffer.
|
|
48
|
+
*
|
|
49
|
+
* Implements trailing-only storage — if max size is exceeded, older data is
|
|
50
|
+
* truncated from the front. The cut point is aligned to a UTF-8 code-point
|
|
51
|
+
* boundary so the stored string is always valid UTF-8.
|
|
52
|
+
*
|
|
53
|
+
* A separate 'truncated' flag key is set the first time an overflow happens
|
|
54
|
+
* so callers can surface a "log was truncated" notice to the user.
|
|
55
|
+
*
|
|
56
|
+
* Fix (race condition): APPEND + STRLEN are batched in a single pipeline to
|
|
57
|
+
* reduce round-trips. The subsequent GETRANGE/SET truncation is unavoidably
|
|
58
|
+
* a second step, but at MVP scale log writes per run are effectively serial.
|
|
59
|
+
*/
|
|
60
|
+
appendLog(runId: string, data: string): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Get logs for a run
|
|
63
|
+
*/
|
|
64
|
+
getLogs(runId: string): Promise<string | null>;
|
|
65
|
+
/**
|
|
66
|
+
* Get log size in bytes
|
|
67
|
+
*/
|
|
68
|
+
getLogSize(runId: string): Promise<number>;
|
|
69
|
+
/**
|
|
70
|
+
* Set TTL on logs based on user plan.
|
|
71
|
+
* Should be called when run starts.
|
|
72
|
+
* Uses NX so a retry never resets an already-running countdown.
|
|
73
|
+
*/
|
|
74
|
+
setLogTtl(runId: string, plan: UserPlan): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Upload logs to S3 for long-term storage.
|
|
77
|
+
* Called after run completes.
|
|
78
|
+
*/
|
|
79
|
+
uploadToS3(runId: string, userId: string): Promise<string | null>;
|
|
80
|
+
/**
|
|
81
|
+
* Delete logs from Redis (after S3 upload or when no longer needed)
|
|
82
|
+
*/
|
|
83
|
+
deleteLogs(runId: string): Promise<void>;
|
|
84
|
+
/**
|
|
85
|
+
* Stream logs for real-time tailing.
|
|
86
|
+
* Returns an async generator that yields log chunks as they're written.
|
|
87
|
+
* Automatically stops when the run finishes (success, failed, timed_out, killed).
|
|
88
|
+
*
|
|
89
|
+
* Fix (offset invalidation): if a truncation has occurred since the last
|
|
90
|
+
* poll (i.e. currentLength < lastOffset), the offset is reset to 0 so we
|
|
91
|
+
* re-stream from the start of the new (shorter) buffer instead of silently
|
|
92
|
+
* dropping bytes.
|
|
93
|
+
*/
|
|
94
|
+
streamLogs(runId: string, options?: {
|
|
95
|
+
pollInterval?: number;
|
|
96
|
+
}): AsyncGenerator<string, void, unknown>;
|
|
97
|
+
/**
|
|
98
|
+
* Get log metadata (for API responses).
|
|
99
|
+
*
|
|
100
|
+
* `wasTruncated` replaces the previous `hasMore: false` which was always
|
|
101
|
+
* false and conveyed no useful information. When `wasTruncated` is true
|
|
102
|
+
* the caller should surface a notice like "Log was truncated — only the
|
|
103
|
+
* trailing portion is shown."
|
|
104
|
+
*/
|
|
105
|
+
getLogMetadata(runId: string): Promise<{
|
|
106
|
+
size: number;
|
|
107
|
+
maxSize: number;
|
|
108
|
+
wasTruncated: boolean;
|
|
109
|
+
}>;
|
|
110
|
+
/**
|
|
111
|
+
* Create LogConfig from environment variables
|
|
112
|
+
*/
|
|
113
|
+
static fromEnv(): LogConfig;
|
|
114
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LogManager = void 0;
|
|
4
|
+
const run_metadata_js_1 = require("./run-metadata.js");
|
|
5
|
+
const DEFAULT_LOG_CONFIG = {
|
|
6
|
+
maxLogSize: 5 * 1024 * 1024, // 5MB
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Manages log storage in Dragonfly with optional S3 archival.
|
|
10
|
+
*
|
|
11
|
+
* Log lifecycle:
|
|
12
|
+
* 1. Logs are written to Dragonfly during execution (fast, real-time tailing)
|
|
13
|
+
* 2. Log size is capped at maxLogSize (trailing portion kept, truncated flag set)
|
|
14
|
+
* 3. After run completes, logs are uploaded to S3 for long-term storage
|
|
15
|
+
* 4. TTL in Redis is short (12h free, 7d paid) since S3 is the source of truth
|
|
16
|
+
*/
|
|
17
|
+
class LogManager {
|
|
18
|
+
client;
|
|
19
|
+
config;
|
|
20
|
+
constructor(client, config) {
|
|
21
|
+
this.client = client;
|
|
22
|
+
this.config = { ...DEFAULT_LOG_CONFIG, ...config };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get the Redis key for a run's logs
|
|
26
|
+
*/
|
|
27
|
+
getLogKey(runId) {
|
|
28
|
+
return this.client.key('logs', runId);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Get the Redis key for the truncation flag on a run's logs.
|
|
32
|
+
* Set to '1' the first time the log buffer overflows maxLogSize.
|
|
33
|
+
*/
|
|
34
|
+
getTruncatedKey(runId) {
|
|
35
|
+
return this.client.key('logs', runId, 'truncated');
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Advance a byte offset forward until it points to the start of a valid
|
|
39
|
+
* UTF-8 code-point boundary in `buf`. Continuation bytes (10xxxxxx) are
|
|
40
|
+
* skipped so we never cut a multibyte sequence in half.
|
|
41
|
+
*
|
|
42
|
+
* Returns the adjusted offset (always ≤ buf.length).
|
|
43
|
+
*/
|
|
44
|
+
static alignUtf8Boundary(buf, offset) {
|
|
45
|
+
while (offset < buf.length && (buf[offset] & 0xc0) === 0x80) {
|
|
46
|
+
offset++;
|
|
47
|
+
}
|
|
48
|
+
return offset;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Append log data to a run's log buffer.
|
|
52
|
+
*
|
|
53
|
+
* Implements trailing-only storage — if max size is exceeded, older data is
|
|
54
|
+
* truncated from the front. The cut point is aligned to a UTF-8 code-point
|
|
55
|
+
* boundary so the stored string is always valid UTF-8.
|
|
56
|
+
*
|
|
57
|
+
* A separate 'truncated' flag key is set the first time an overflow happens
|
|
58
|
+
* so callers can surface a "log was truncated" notice to the user.
|
|
59
|
+
*
|
|
60
|
+
* Fix (race condition): APPEND + STRLEN are batched in a single pipeline to
|
|
61
|
+
* reduce round-trips. The subsequent GETRANGE/SET truncation is unavoidably
|
|
62
|
+
* a second step, but at MVP scale log writes per run are effectively serial.
|
|
63
|
+
*/
|
|
64
|
+
async appendLog(runId, data) {
|
|
65
|
+
if (data === '') {
|
|
66
|
+
// Redis APPEND of an empty string still creates the key — preserve
|
|
67
|
+
// that behaviour but skip the unnecessary pipeline call.
|
|
68
|
+
const key = this.getLogKey(runId);
|
|
69
|
+
await this.client.redis.append(key, '');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const key = this.getLogKey(runId);
|
|
73
|
+
const buffer = Buffer.from(data, 'utf-8');
|
|
74
|
+
// Batch APPEND + STRLEN into one pipeline to reduce round-trips
|
|
75
|
+
const pipeline = this.client.redis.pipeline();
|
|
76
|
+
pipeline.append(key, buffer);
|
|
77
|
+
pipeline.strlen(key);
|
|
78
|
+
const results = await pipeline.exec();
|
|
79
|
+
// results[1][1] is the STRLEN response
|
|
80
|
+
const currentLength = results?.[1]?.[1] ?? 0;
|
|
81
|
+
if (currentLength > this.config.maxLogSize) {
|
|
82
|
+
const rawExcess = currentLength - this.config.maxLogSize;
|
|
83
|
+
// Use getrangeBuffer to get the raw UTF-8 bytes without any ioredis
|
|
84
|
+
// string decoding. If we used getrange(), ioredis would UTF-8-decode
|
|
85
|
+
// the bytes into a JS string, and re-encoding with Buffer.from(s,'binary')
|
|
86
|
+
// would corrupt multi-byte sequences (latin-1 ≠ utf-8 for high bytes).
|
|
87
|
+
const trailingBuf = await this.client.redis.getrangeBuffer(key, rawExcess, -1);
|
|
88
|
+
// Advance past any UTF-8 continuation bytes (10xxxxxx) so we start at
|
|
89
|
+
// a valid code-point boundary and never store a broken sequence.
|
|
90
|
+
const alignedOffset = LogManager.alignUtf8Boundary(trailingBuf, 0);
|
|
91
|
+
const safeTrailing = trailingBuf.subarray(alignedOffset);
|
|
92
|
+
// Overwrite the key with only the safe trailing portion
|
|
93
|
+
await this.client.redis.set(key, safeTrailing);
|
|
94
|
+
// Mark log as truncated (NX — only set once; ignore if already set)
|
|
95
|
+
await this.client.redis.set(this.getTruncatedKey(runId), '1', 'NX');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get logs for a run
|
|
100
|
+
*/
|
|
101
|
+
async getLogs(runId) {
|
|
102
|
+
const key = this.getLogKey(runId);
|
|
103
|
+
return await this.client.redis.get(key);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Get log size in bytes
|
|
107
|
+
*/
|
|
108
|
+
async getLogSize(runId) {
|
|
109
|
+
const key = this.getLogKey(runId);
|
|
110
|
+
return await this.client.redis.strlen(key);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Set TTL on logs based on user plan.
|
|
114
|
+
* Should be called when run starts.
|
|
115
|
+
* Uses NX so a retry never resets an already-running countdown.
|
|
116
|
+
*/
|
|
117
|
+
async setLogTtl(runId, plan) {
|
|
118
|
+
const key = this.getLogKey(runId);
|
|
119
|
+
const ttl = this.client.ttlManager.getTtl('logs', plan);
|
|
120
|
+
if (ttl !== null && ttl > 0) {
|
|
121
|
+
await this.client.ttlManager.setExpiry(this.client.redis, [key], ttl);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Upload logs to S3 for long-term storage.
|
|
126
|
+
* Called after run completes.
|
|
127
|
+
*/
|
|
128
|
+
async uploadToS3(runId, userId) {
|
|
129
|
+
if (!this.config.s3Bucket) {
|
|
130
|
+
console.log(`[LogManager] S3 not configured, skipping upload for run ${runId}`);
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const logs = await this.getLogs(runId);
|
|
134
|
+
if (!logs) {
|
|
135
|
+
console.log(`[LogManager] No logs to upload for run ${runId}`);
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const s3Key = `logs/${userId}/${runId}.log`;
|
|
139
|
+
// Note: NOT DONE — wire up AWS SDK upload here
|
|
140
|
+
return s3Key;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Delete logs from Redis (after S3 upload or when no longer needed)
|
|
144
|
+
*/
|
|
145
|
+
async deleteLogs(runId) {
|
|
146
|
+
const pipeline = this.client.redis.pipeline();
|
|
147
|
+
pipeline.del(this.getLogKey(runId));
|
|
148
|
+
pipeline.del(this.getTruncatedKey(runId));
|
|
149
|
+
await pipeline.exec();
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Stream logs for real-time tailing.
|
|
153
|
+
* Returns an async generator that yields log chunks as they're written.
|
|
154
|
+
* Automatically stops when the run finishes (success, failed, timed_out, killed).
|
|
155
|
+
*
|
|
156
|
+
* Fix (offset invalidation): if a truncation has occurred since the last
|
|
157
|
+
* poll (i.e. currentLength < lastOffset), the offset is reset to 0 so we
|
|
158
|
+
* re-stream from the start of the new (shorter) buffer instead of silently
|
|
159
|
+
* dropping bytes.
|
|
160
|
+
*/
|
|
161
|
+
async *streamLogs(runId, options) {
|
|
162
|
+
const pollInterval = options?.pollInterval ?? 100;
|
|
163
|
+
const key = this.getLogKey(runId);
|
|
164
|
+
const metadataManager = new run_metadata_js_1.RunMetadataManager(this.client);
|
|
165
|
+
let lastOffset = 0;
|
|
166
|
+
const finishedStatuses = ['success', 'failed', 'timed_out', 'killed'];
|
|
167
|
+
while (true) {
|
|
168
|
+
// Check if run is still active
|
|
169
|
+
const metadata = await metadataManager.get(runId);
|
|
170
|
+
// If metadata is null the run doesn't exist (never created or already deleted).
|
|
171
|
+
// Treat this as a terminal condition — yield any buffered bytes and stop.
|
|
172
|
+
if (!metadata) {
|
|
173
|
+
const currentLength = await this.client.redis.strlen(key);
|
|
174
|
+
// Reset offset if the buffer was truncated underneath us
|
|
175
|
+
if (currentLength < lastOffset)
|
|
176
|
+
lastOffset = 0;
|
|
177
|
+
if (currentLength > lastOffset) {
|
|
178
|
+
const remaining = await this.client.redis.getrange(key, lastOffset, -1);
|
|
179
|
+
if (remaining)
|
|
180
|
+
yield remaining;
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (finishedStatuses.includes(metadata.status)) {
|
|
185
|
+
// Run finished — yield any remaining logs and stop
|
|
186
|
+
const currentLength = await this.client.redis.strlen(key);
|
|
187
|
+
// Reset offset if the buffer was truncated underneath us
|
|
188
|
+
if (currentLength < lastOffset)
|
|
189
|
+
lastOffset = 0;
|
|
190
|
+
if (currentLength > lastOffset) {
|
|
191
|
+
const remaining = await this.client.redis.getrange(key, lastOffset, -1);
|
|
192
|
+
if (remaining)
|
|
193
|
+
yield remaining;
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// Yield any new logs
|
|
198
|
+
const currentLength = await this.client.redis.strlen(key);
|
|
199
|
+
if (currentLength < lastOffset) {
|
|
200
|
+
// Buffer was truncated while we were streaming — reset and re-read
|
|
201
|
+
lastOffset = 0;
|
|
202
|
+
}
|
|
203
|
+
if (currentLength > lastOffset) {
|
|
204
|
+
const newData = await this.client.redis.getrange(key, lastOffset, -1);
|
|
205
|
+
if (newData) {
|
|
206
|
+
yield newData;
|
|
207
|
+
}
|
|
208
|
+
lastOffset = currentLength;
|
|
209
|
+
}
|
|
210
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Get log metadata (for API responses).
|
|
215
|
+
*
|
|
216
|
+
* `wasTruncated` replaces the previous `hasMore: false` which was always
|
|
217
|
+
* false and conveyed no useful information. When `wasTruncated` is true
|
|
218
|
+
* the caller should surface a notice like "Log was truncated — only the
|
|
219
|
+
* trailing portion is shown."
|
|
220
|
+
*/
|
|
221
|
+
async getLogMetadata(runId) {
|
|
222
|
+
const [size, truncatedFlag] = await Promise.all([
|
|
223
|
+
this.getLogSize(runId),
|
|
224
|
+
this.client.redis.get(this.getTruncatedKey(runId)),
|
|
225
|
+
]);
|
|
226
|
+
return {
|
|
227
|
+
size,
|
|
228
|
+
maxSize: this.config.maxLogSize,
|
|
229
|
+
wasTruncated: truncatedFlag === '1',
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Create LogConfig from environment variables
|
|
234
|
+
*/
|
|
235
|
+
static fromEnv() {
|
|
236
|
+
return {
|
|
237
|
+
maxLogSize: parseInt(process.env.SCRAPELY_LOG_MAX_SIZE ?? '', 10) || DEFAULT_LOG_CONFIG.maxLogSize,
|
|
238
|
+
s3Bucket: process.env.SCRAPELY_S3_LOG_BUCKET,
|
|
239
|
+
s3Region: process.env.SCRAPELY_S3_REGION || 'us-east-1',
|
|
240
|
+
s3Endpoint: process.env.SCRAPELY_S3_ENDPOINT,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
exports.LogManager = LogManager;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Configuration } from '../configuration.js';
|
|
2
|
+
import { ApiStorageClientFactory, type ApiStorageConfig } from './api-storage-client.js';
|
|
3
|
+
/**
|
|
4
|
+
* Configure API-based storage (for secure container execution).
|
|
5
|
+
* No DB credentials are exposed - only the user's API key.
|
|
6
|
+
* This is the ONLY way containers should access storage.
|
|
7
|
+
*/
|
|
8
|
+
export declare function setApiStorage(apiConfig: ApiStorageConfig, configuration?: Configuration): ApiStorageClientFactory;
|
|
9
|
+
export declare function getApiStorageFactory(): ApiStorageClientFactory | null;
|
|
10
|
+
export declare function isApiStorageConfigured(): boolean;
|
|
11
|
+
export declare function disconnectStorage(): void;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setApiStorage = setApiStorage;
|
|
4
|
+
exports.getApiStorageFactory = getApiStorageFactory;
|
|
5
|
+
exports.isApiStorageConfigured = isApiStorageConfigured;
|
|
6
|
+
exports.disconnectStorage = disconnectStorage;
|
|
7
|
+
const core_1 = require("@crawlee/core");
|
|
8
|
+
const configuration_js_1 = require("../configuration.js");
|
|
9
|
+
const api_storage_client_js_1 = require("./api-storage-client.js");
|
|
10
|
+
let _apiFactory = null;
|
|
11
|
+
/**
|
|
12
|
+
* Cache for auto-saved values. Map structure: storeId -> key -> value
|
|
13
|
+
* This allows getAutoSavedValue to return the same object reference,
|
|
14
|
+
* and persistState listeners can iterate and save all cached values.
|
|
15
|
+
*/
|
|
16
|
+
const autoSaveCache = new Map();
|
|
17
|
+
/**
|
|
18
|
+
* Track which stores have registered persistState listeners
|
|
19
|
+
*/
|
|
20
|
+
const persistStateRegistered = new Set();
|
|
21
|
+
/**
|
|
22
|
+
* Register a persistState listener for a store if not already registered.
|
|
23
|
+
* This listener saves all cached values for that store when persistState is emitted.
|
|
24
|
+
*/
|
|
25
|
+
function ensurePersistStateListener(storeId, config) {
|
|
26
|
+
if (persistStateRegistered.has(storeId)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const eventManager = config.getEventManager();
|
|
30
|
+
const persistStateIntervalMillis = config.get('persistStateIntervalMillis') ?? 60000;
|
|
31
|
+
// Use half the interval for timeout (matching Crawlee's approach)
|
|
32
|
+
const timeoutSecs = persistStateIntervalMillis / 2000;
|
|
33
|
+
eventManager.on('persistState', async () => {
|
|
34
|
+
const storeCache = autoSaveCache.get(storeId);
|
|
35
|
+
if (!storeCache)
|
|
36
|
+
return;
|
|
37
|
+
const factory = _apiFactory;
|
|
38
|
+
if (!factory)
|
|
39
|
+
return;
|
|
40
|
+
const kvClient = factory.keyValueStore(storeId);
|
|
41
|
+
const promises = [];
|
|
42
|
+
for (const [key, value] of storeCache) {
|
|
43
|
+
promises.push(kvClient.setRecord({ key, value }, { timeoutSecs, doNotRetryTimeouts: true }).catch((error) => {
|
|
44
|
+
console.warn(`[Scrapely] Failed to persist state value to ${key}:`, error.message);
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
await Promise.all(promises);
|
|
48
|
+
});
|
|
49
|
+
persistStateRegistered.add(storeId);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Configure API-based storage (for secure container execution).
|
|
53
|
+
* No DB credentials are exposed - only the user's API key.
|
|
54
|
+
* This is the ONLY way containers should access storage.
|
|
55
|
+
*/
|
|
56
|
+
function setApiStorage(apiConfig, configuration) {
|
|
57
|
+
_apiFactory = new api_storage_client_js_1.ApiStorageClientFactory(apiConfig);
|
|
58
|
+
const cfg = configuration ?? configuration_js_1.Configuration.getGlobalConfig();
|
|
59
|
+
cfg.useStorageClient(_apiFactory);
|
|
60
|
+
// Patch KeyValueStore to expose listKeys method
|
|
61
|
+
patchKeyValueStore();
|
|
62
|
+
return _apiFactory;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Patch KeyValueStore to expose listKeys, getAutoSavedValue, and getPublicUrl methods
|
|
66
|
+
* that delegate to the underlying client. forEachKey is left to Crawlee's native
|
|
67
|
+
* paginating implementation (via client.listKeys).
|
|
68
|
+
*/
|
|
69
|
+
function patchKeyValueStore() {
|
|
70
|
+
const origOpen = core_1.KeyValueStore.open.bind(core_1.KeyValueStore);
|
|
71
|
+
// Override the open method to return a patched store
|
|
72
|
+
core_1.KeyValueStore.open = async function (id) {
|
|
73
|
+
const store = await origOpen(id);
|
|
74
|
+
// Add listKeys if not already present
|
|
75
|
+
if (!store.listKeys) {
|
|
76
|
+
const storeId = store.id || store.name || 'default';
|
|
77
|
+
store.listKeys = async function (options) {
|
|
78
|
+
const factory = _apiFactory;
|
|
79
|
+
if (!factory) {
|
|
80
|
+
throw new Error('API storage client not initialized');
|
|
81
|
+
}
|
|
82
|
+
const kvClient = factory.keyValueStore(storeId);
|
|
83
|
+
return kvClient.listKeys(options);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// Always override keys() — Crawlee's built-in keys() is an async generator
|
|
87
|
+
// that only runs when iterated with `for await`. Actors that call `await kvs.keys()`
|
|
88
|
+
// without iterating never trigger billing. Replace it with a Promise-based version
|
|
89
|
+
// that calls _listKeys() immediately (and thus increments listCount for billing).
|
|
90
|
+
{
|
|
91
|
+
const storeId = store.id || store.name || 'default';
|
|
92
|
+
store.keys = async function (options) {
|
|
93
|
+
const factory = _apiFactory;
|
|
94
|
+
if (!factory) {
|
|
95
|
+
throw new Error('API storage client not initialized');
|
|
96
|
+
}
|
|
97
|
+
const kvClient = factory.keyValueStore(storeId);
|
|
98
|
+
return kvClient.keys(options);
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// Add getAutoSavedValue method - returns value with auto-save behavior
|
|
102
|
+
// This implementation matches Crawlee's approach:
|
|
103
|
+
// 1. Check cache first (return same object reference for subsequent calls)
|
|
104
|
+
// 2. Fetch value from storage if not cached
|
|
105
|
+
// 3. Cache the value (so mutations are tracked)
|
|
106
|
+
// 4. Register persistState listener to save cached values
|
|
107
|
+
{
|
|
108
|
+
const storeId = store.id || store.name || 'default';
|
|
109
|
+
const config = configuration_js_1.Configuration.getGlobalConfig();
|
|
110
|
+
store.getAutoSavedValue = async function (key, defaultValue) {
|
|
111
|
+
// Check if we have a cached value for this store/key
|
|
112
|
+
let storeCache = autoSaveCache.get(storeId);
|
|
113
|
+
if (storeCache?.has(key)) {
|
|
114
|
+
return storeCache.get(key);
|
|
115
|
+
}
|
|
116
|
+
// Fetch value from storage
|
|
117
|
+
const factory = _apiFactory;
|
|
118
|
+
if (!factory) {
|
|
119
|
+
throw new Error('API storage client not initialized');
|
|
120
|
+
}
|
|
121
|
+
const kvClient = factory.keyValueStore(storeId);
|
|
122
|
+
// Get the value (this stores defaultValue if key doesn't exist)
|
|
123
|
+
const value = await kvClient.getAutoSavedValue(key, defaultValue);
|
|
124
|
+
// Initialize store cache if needed
|
|
125
|
+
if (!storeCache) {
|
|
126
|
+
storeCache = new Map();
|
|
127
|
+
autoSaveCache.set(storeId, storeCache);
|
|
128
|
+
}
|
|
129
|
+
// Check cache again (race condition: another call might have cached it)
|
|
130
|
+
if (storeCache.has(key)) {
|
|
131
|
+
return storeCache.get(key);
|
|
132
|
+
}
|
|
133
|
+
// Cache the value - mutations to this object will be persisted
|
|
134
|
+
storeCache.set(key, value);
|
|
135
|
+
// Register persistState listener for this store
|
|
136
|
+
ensurePersistStateListener(storeId, config);
|
|
137
|
+
return value;
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// Add getPublicUrl method - returns public URL for a record
|
|
141
|
+
{
|
|
142
|
+
const storeId = store.id || store.name || 'default';
|
|
143
|
+
store.getPublicUrl = function (key) {
|
|
144
|
+
const factory = _apiFactory;
|
|
145
|
+
if (!factory) {
|
|
146
|
+
throw new Error('API storage client not initialized');
|
|
147
|
+
}
|
|
148
|
+
const kvClient = factory.keyValueStore(storeId);
|
|
149
|
+
return kvClient.getPublicUrl(key);
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return store;
|
|
153
|
+
};
|
|
154
|
+
// Add static getAutoSavedValue method - operates on default store
|
|
155
|
+
core_1.KeyValueStore.getAutoSavedValue = async function (key, defaultValue) {
|
|
156
|
+
const store = await core_1.KeyValueStore.open();
|
|
157
|
+
return store.getAutoSavedValue(key, defaultValue);
|
|
158
|
+
};
|
|
159
|
+
// Add static recordExists method - operates on default store
|
|
160
|
+
core_1.KeyValueStore.recordExists = async function (key) {
|
|
161
|
+
const store = await core_1.KeyValueStore.open();
|
|
162
|
+
return store.recordExists(key);
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function getApiStorageFactory() {
|
|
166
|
+
return _apiFactory;
|
|
167
|
+
}
|
|
168
|
+
function isApiStorageConfigured() {
|
|
169
|
+
return _apiFactory !== null;
|
|
170
|
+
}
|
|
171
|
+
function disconnectStorage() {
|
|
172
|
+
// API storage doesn't need explicit disconnect
|
|
173
|
+
_apiFactory = null;
|
|
174
|
+
// Clear caches and listener registrations
|
|
175
|
+
autoSaveCache.clear();
|
|
176
|
+
persistStateRegistered.clear();
|
|
177
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { RequestQueueClient, RequestQueueInfo, QueueHead, QueueOperationInfo, RequestSchema, UpdateRequestSchema, RequestOptions, BatchAddRequestsResult, ListOptions, ListAndLockOptions, ListAndLockHeadResult, ProlongRequestLockOptions, ProlongRequestLockResult, DeleteRequestLockOptions } from '@crawlee/types';
|
|
2
|
+
export type { RequestQueueRequest } from './rq-list-requests.js';
|
|
3
|
+
/** Options for listRequests endpoint */
|
|
4
|
+
export interface ListRequestsOptions {
|
|
5
|
+
limit?: number;
|
|
6
|
+
cursor?: string;
|
|
7
|
+
exclusiveStartId?: string;
|
|
8
|
+
filter?: ('locked' | 'pending')[];
|
|
9
|
+
clientKey?: string;
|
|
10
|
+
}
|
|
11
|
+
/** Result from listRequests endpoint */
|
|
12
|
+
export interface ListRequestsResult {
|
|
13
|
+
items: RequestQueueRequest[];
|
|
14
|
+
count: number;
|
|
15
|
+
limit: number;
|
|
16
|
+
exclusiveStartId?: string;
|
|
17
|
+
cursor?: string;
|
|
18
|
+
nextCursor?: string;
|
|
19
|
+
}
|
|
20
|
+
import type { DragonflyClient } from './dragonfly-client.js';
|
|
21
|
+
import type { RequestQueueOptions } from '@crawlee/types';
|
|
22
|
+
import { type RequestQueueRequest } from './rq-list-requests.js';
|
|
23
|
+
export declare class DragonflyRequestQueueClient implements RequestQueueClient {
|
|
24
|
+
private client;
|
|
25
|
+
private id;
|
|
26
|
+
private infoKey;
|
|
27
|
+
private requestsKey;
|
|
28
|
+
private pendingKey;
|
|
29
|
+
private lockedKey;
|
|
30
|
+
private lockOwnersKey;
|
|
31
|
+
private readonly clientKey;
|
|
32
|
+
constructor(client: DragonflyClient, id: string, options?: RequestQueueOptions);
|
|
33
|
+
private estimateStoredRequestBytes;
|
|
34
|
+
/**
|
|
35
|
+
* Track a write operation for billing.
|
|
36
|
+
*/
|
|
37
|
+
private trackWrite;
|
|
38
|
+
/**
|
|
39
|
+
* Track a read operation for billing.
|
|
40
|
+
*/
|
|
41
|
+
private trackRead;
|
|
42
|
+
/**
|
|
43
|
+
* Track storage bytes for billing.
|
|
44
|
+
*/
|
|
45
|
+
private trackStorageBytes;
|
|
46
|
+
/**
|
|
47
|
+
* Track clientKey access for hadMultipleClients (per Apify spec).
|
|
48
|
+
* Returns the current hadMultipleClients flag after update.
|
|
49
|
+
*/
|
|
50
|
+
private trackClientKey;
|
|
51
|
+
get(): Promise<RequestQueueInfo | undefined>;
|
|
52
|
+
update(newFields: {
|
|
53
|
+
name?: string;
|
|
54
|
+
}): Promise<Partial<RequestQueueInfo> | undefined>;
|
|
55
|
+
delete(): Promise<void>;
|
|
56
|
+
addRequest(request: RequestSchema, options?: RequestOptions): Promise<QueueOperationInfo>;
|
|
57
|
+
batchAddRequests(requests: RequestSchema[], options?: RequestOptions): Promise<BatchAddRequestsResult>;
|
|
58
|
+
getRequest(id: string): Promise<RequestOptions | undefined>;
|
|
59
|
+
updateRequest(request: UpdateRequestSchema, options?: RequestOptions): Promise<QueueOperationInfo>;
|
|
60
|
+
deleteRequest(id: string): Promise<void>;
|
|
61
|
+
listHead(options?: ListOptions): Promise<QueueHead>;
|
|
62
|
+
/**
|
|
63
|
+
* List all requests in the queue with optional filtering and pagination.
|
|
64
|
+
* Supports cursor-based pagination (preferred) and deprecated exclusiveStartId.
|
|
65
|
+
* @param options.limit - Max number of requests to return (default 100, max 10000)
|
|
66
|
+
* @param options.cursor - Base64-encoded cursor from previous response.
|
|
67
|
+
* If the cursor's request was deleted, listing resumes just past its
|
|
68
|
+
* sorted position (it never restarts from the first page).
|
|
69
|
+
* @param options.exclusiveStartId - DEPRECATED: Skip requests up to this ID
|
|
70
|
+
* @param options.filter - Filter by state: 'locked' or 'pending'
|
|
71
|
+
* @param options.clientKey - Client identifier for hadMultipleClients tracking
|
|
72
|
+
*/
|
|
73
|
+
listRequests(options?: ListRequestsOptions): Promise<ListRequestsResult>;
|
|
74
|
+
listAndLockHead(options: ListAndLockOptions & {
|
|
75
|
+
clientKey?: string;
|
|
76
|
+
}): Promise<ListAndLockHeadResult>;
|
|
77
|
+
prolongRequestLock(id: string, options: ProlongRequestLockOptions): Promise<ProlongRequestLockResult>;
|
|
78
|
+
deleteRequestLock(id: string, options?: DeleteRequestLockOptions): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Unlock all requests locked by a specific clientKey.
|
|
81
|
+
* This is useful for batch unlocking when a client is done processing.
|
|
82
|
+
* @param options.clientKey - The client key to unlock requests for
|
|
83
|
+
* @param options.forefront - If true, move unlocked requests to the front of the queue
|
|
84
|
+
* @param options.runId - Optional run ID to only unlock requests for a specific run
|
|
85
|
+
*/
|
|
86
|
+
unlockRequests(options: {
|
|
87
|
+
clientKey: string;
|
|
88
|
+
forefront?: boolean;
|
|
89
|
+
runId?: string;
|
|
90
|
+
}): Promise<{
|
|
91
|
+
unlockedCount: number;
|
|
92
|
+
}>;
|
|
93
|
+
}
|