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.
Files changed (86) hide show
  1. package/README.md +119 -1
  2. package/dist/actor.d.ts +703 -0
  3. package/dist/actor.js +1441 -0
  4. package/dist/browser-configuration.d.ts +58 -0
  5. package/dist/browser-configuration.js +128 -0
  6. package/dist/browser-locations.d.ts +5 -0
  7. package/dist/browser-locations.js +5469 -0
  8. package/dist/charging.d.ts +153 -0
  9. package/dist/charging.js +782 -0
  10. package/dist/charging_client.d.ts +18 -0
  11. package/dist/charging_client.js +49 -0
  12. package/dist/configuration.d.ts +121 -0
  13. package/dist/configuration.js +128 -0
  14. package/dist/crypto/secrets.d.ts +44 -0
  15. package/dist/crypto/secrets.js +164 -0
  16. package/dist/events/events-client.d.ts +59 -0
  17. package/dist/events/events-client.js +120 -0
  18. package/dist/events/scrapely-event-manager.d.ts +13 -0
  19. package/dist/events/scrapely-event-manager.js +61 -0
  20. package/dist/index.d.ts +29 -0
  21. package/dist/index.js +95 -0
  22. package/dist/log-env.d.ts +1 -0
  23. package/dist/log-env.js +12 -0
  24. package/dist/manifest.d.ts +30 -0
  25. package/dist/manifest.js +47 -0
  26. package/dist/patch-browser-pool.d.ts +66 -0
  27. package/dist/patch-browser-pool.js +827 -0
  28. package/dist/proxy-configuration.d.ts +118 -0
  29. package/dist/proxy-configuration.js +88 -0
  30. package/dist/request-list.d.ts +57 -0
  31. package/dist/request-list.js +79 -0
  32. package/dist/runner/docker-manager.d.ts +88 -0
  33. package/dist/runner/docker-manager.js +283 -0
  34. package/dist/runner/index.d.ts +2 -0
  35. package/dist/runner/index.js +7 -0
  36. package/dist/runner/timeout-manager.d.ts +43 -0
  37. package/dist/runner/timeout-manager.js +90 -0
  38. package/dist/scrapely-proxy-configuration.d.ts +99 -0
  39. package/dist/scrapely-proxy-configuration.js +559 -0
  40. package/dist/storage/api-storage-client.d.ts +199 -0
  41. package/dist/storage/api-storage-client.js +910 -0
  42. package/dist/storage/billing-tracker.d.ts +163 -0
  43. package/dist/storage/billing-tracker.js +260 -0
  44. package/dist/storage/dataset-client.d.ts +43 -0
  45. package/dist/storage/dataset-client.js +232 -0
  46. package/dist/storage/dataset-collection-client.d.ts +8 -0
  47. package/dist/storage/dataset-collection-client.js +75 -0
  48. package/dist/storage/dataset-push-script.d.ts +18 -0
  49. package/dist/storage/dataset-push-script.js +38 -0
  50. package/dist/storage/dragonfly-client.d.ts +94 -0
  51. package/dist/storage/dragonfly-client.js +167 -0
  52. package/dist/storage/index.d.ts +11 -0
  53. package/dist/storage/index.js +25 -0
  54. package/dist/storage/key-value-store-client.d.ts +61 -0
  55. package/dist/storage/key-value-store-client.js +306 -0
  56. package/dist/storage/kv-store-collection-client.d.ts +8 -0
  57. package/dist/storage/kv-store-collection-client.js +76 -0
  58. package/dist/storage/kvs-iteration.d.ts +17 -0
  59. package/dist/storage/kvs-iteration.js +126 -0
  60. package/dist/storage/log-manager.d.ts +114 -0
  61. package/dist/storage/log-manager.js +244 -0
  62. package/dist/storage/patch-storage.d.ts +11 -0
  63. package/dist/storage/patch-storage.js +177 -0
  64. package/dist/storage/request-queue-client.d.ts +93 -0
  65. package/dist/storage/request-queue-client.js +522 -0
  66. package/dist/storage/rq-collection-client.d.ts +8 -0
  67. package/dist/storage/rq-collection-client.js +89 -0
  68. package/dist/storage/rq-list-requests.d.ts +59 -0
  69. package/dist/storage/rq-list-requests.js +114 -0
  70. package/dist/storage/rq-lock-script.d.ts +56 -0
  71. package/dist/storage/rq-lock-script.js +127 -0
  72. package/dist/storage/run-metadata.d.ts +98 -0
  73. package/dist/storage/run-metadata.js +256 -0
  74. package/dist/storage/s3-client.d.ts +103 -0
  75. package/dist/storage/s3-client.js +302 -0
  76. package/dist/storage/ttl-manager.d.ts +70 -0
  77. package/dist/storage/ttl-manager.js +115 -0
  78. package/dist/storage/utils.d.ts +3 -0
  79. package/dist/storage/utils.js +19 -0
  80. package/dist/storage-identifiers.d.ts +20 -0
  81. package/dist/storage-identifiers.js +6 -0
  82. package/dist/utils.d.ts +54 -0
  83. package/dist/utils.js +63 -0
  84. package/dist/validator.d.ts +24 -0
  85. package/dist/validator.js +158 -0
  86. 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
+ }