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,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BillingTracker = exports.LogManager = exports.RunMetadataManager = exports.DragonflyRequestQueueCollectionClient = exports.DragonflyKeyValueStoreCollectionClient = exports.DragonflyDatasetCollectionClient = exports.DragonflyRequestQueueClient = exports.DragonflyKeyValueStoreClient = exports.DragonflyDatasetClient = exports.TtlManager = exports.DragonflyClient = void 0;
|
|
4
|
+
var dragonfly_client_js_1 = require("./dragonfly-client.js");
|
|
5
|
+
Object.defineProperty(exports, "DragonflyClient", { enumerable: true, get: function () { return dragonfly_client_js_1.DragonflyClient; } });
|
|
6
|
+
var ttl_manager_js_1 = require("./ttl-manager.js");
|
|
7
|
+
Object.defineProperty(exports, "TtlManager", { enumerable: true, get: function () { return ttl_manager_js_1.TtlManager; } });
|
|
8
|
+
var dataset_client_js_1 = require("./dataset-client.js");
|
|
9
|
+
Object.defineProperty(exports, "DragonflyDatasetClient", { enumerable: true, get: function () { return dataset_client_js_1.DragonflyDatasetClient; } });
|
|
10
|
+
var key_value_store_client_js_1 = require("./key-value-store-client.js");
|
|
11
|
+
Object.defineProperty(exports, "DragonflyKeyValueStoreClient", { enumerable: true, get: function () { return key_value_store_client_js_1.DragonflyKeyValueStoreClient; } });
|
|
12
|
+
var request_queue_client_js_1 = require("./request-queue-client.js");
|
|
13
|
+
Object.defineProperty(exports, "DragonflyRequestQueueClient", { enumerable: true, get: function () { return request_queue_client_js_1.DragonflyRequestQueueClient; } });
|
|
14
|
+
var dataset_collection_client_js_1 = require("./dataset-collection-client.js");
|
|
15
|
+
Object.defineProperty(exports, "DragonflyDatasetCollectionClient", { enumerable: true, get: function () { return dataset_collection_client_js_1.DragonflyDatasetCollectionClient; } });
|
|
16
|
+
var kv_store_collection_client_js_1 = require("./kv-store-collection-client.js");
|
|
17
|
+
Object.defineProperty(exports, "DragonflyKeyValueStoreCollectionClient", { enumerable: true, get: function () { return kv_store_collection_client_js_1.DragonflyKeyValueStoreCollectionClient; } });
|
|
18
|
+
var rq_collection_client_js_1 = require("./rq-collection-client.js");
|
|
19
|
+
Object.defineProperty(exports, "DragonflyRequestQueueCollectionClient", { enumerable: true, get: function () { return rq_collection_client_js_1.DragonflyRequestQueueCollectionClient; } });
|
|
20
|
+
var run_metadata_js_1 = require("./run-metadata.js");
|
|
21
|
+
Object.defineProperty(exports, "RunMetadataManager", { enumerable: true, get: function () { return run_metadata_js_1.RunMetadataManager; } });
|
|
22
|
+
var log_manager_js_1 = require("./log-manager.js");
|
|
23
|
+
Object.defineProperty(exports, "LogManager", { enumerable: true, get: function () { return log_manager_js_1.LogManager; } });
|
|
24
|
+
var billing_tracker_js_1 = require("./billing-tracker.js");
|
|
25
|
+
Object.defineProperty(exports, "BillingTracker", { enumerable: true, get: function () { return billing_tracker_js_1.BillingTracker; } });
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { KeyValueStoreClient, KeyValueStoreInfo, KeyValueStoreRecord, KeyValueStoreClientGetRecordOptions, KeyValueStoreClientListOptions, KeyValueStoreClientUpdateOptions, KeyValueStoreClientListData, KeyValueStoreItemData, KeyValueStoreRecordOptions } from '@crawlee/types';
|
|
2
|
+
import type { DragonflyClient } from './dragonfly-client.js';
|
|
3
|
+
/**
|
|
4
|
+
* Type for the iteratee function used in forEachKey
|
|
5
|
+
*/
|
|
6
|
+
export type KeyConsumer = (key: string, index: number, info: {
|
|
7
|
+
size: number;
|
|
8
|
+
}) => Promise<void> | void;
|
|
9
|
+
/**
|
|
10
|
+
* Options for forEachKey iteration
|
|
11
|
+
*/
|
|
12
|
+
export interface ForEachKeyOptions extends KeyValueStoreClientListOptions {
|
|
13
|
+
}
|
|
14
|
+
export declare class DragonflyKeyValueStoreClient implements KeyValueStoreClient {
|
|
15
|
+
private client;
|
|
16
|
+
private id;
|
|
17
|
+
private infoKey;
|
|
18
|
+
private recordsKey;
|
|
19
|
+
private metaKey;
|
|
20
|
+
constructor(client: DragonflyClient, id: string);
|
|
21
|
+
private estimateStoredRecordBytes;
|
|
22
|
+
get(): Promise<KeyValueStoreInfo | undefined>;
|
|
23
|
+
update(newFields: KeyValueStoreClientUpdateOptions): Promise<Partial<KeyValueStoreInfo>>;
|
|
24
|
+
delete(): Promise<void>;
|
|
25
|
+
recordExists(key: string): Promise<boolean>;
|
|
26
|
+
getRecord(key: string, options?: KeyValueStoreClientGetRecordOptions): Promise<KeyValueStoreRecord | undefined>;
|
|
27
|
+
setRecord(record: KeyValueStoreRecord, _options?: KeyValueStoreRecordOptions, billingContext?: {
|
|
28
|
+
runId: string;
|
|
29
|
+
userId: string;
|
|
30
|
+
}): Promise<void>;
|
|
31
|
+
deleteRecord(key: string): Promise<void>;
|
|
32
|
+
listKeys(options?: KeyValueStoreClientListOptions): Partial<AsyncIterable<KeyValueStoreItemData>> & Promise<KeyValueStoreClientListData>;
|
|
33
|
+
private _listKeys;
|
|
34
|
+
keys(options?: KeyValueStoreClientListOptions): AsyncIterable<string> & Promise<KeyValueStoreClientListData>;
|
|
35
|
+
values(options?: KeyValueStoreClientListOptions): AsyncIterable<unknown> & Promise<unknown[]>;
|
|
36
|
+
entries(options?: KeyValueStoreClientListOptions): AsyncIterable<[string, unknown]> & Promise<[string, unknown][]>;
|
|
37
|
+
/**
|
|
38
|
+
* Iterates over all keys in the key-value store, calling the iteratee function for each key.
|
|
39
|
+
* Similar to Apify's KeyValueStore.forEachKey() method.
|
|
40
|
+
*/
|
|
41
|
+
forEachKey(iteratee: KeyConsumer, options?: ForEachKeyOptions): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Gets a value from the key-value store that is automatically saved when modified.
|
|
44
|
+
* If the key doesn't exist, returns the defaultValue and stores it for future use.
|
|
45
|
+
* Similar to Apify's KeyValueStore.getAutoSavedValue() method.
|
|
46
|
+
*
|
|
47
|
+
* @param key - The key of the record to get
|
|
48
|
+
* @param defaultValue - The default value to return and store if the key doesn't exist
|
|
49
|
+
* @returns The stored value or the defaultValue
|
|
50
|
+
*/
|
|
51
|
+
getAutoSavedValue<T>(key: string, defaultValue: T): Promise<T>;
|
|
52
|
+
/**
|
|
53
|
+
* Returns a URL for the given key that may be used to publicly access the value
|
|
54
|
+
* in the remote key-value store.
|
|
55
|
+
* Similar to Apify's KeyValueStore.getPublicUrl() method.
|
|
56
|
+
*
|
|
57
|
+
* @param key - The key of the record to get the public URL for
|
|
58
|
+
* @returns A public URL for accessing the record
|
|
59
|
+
*/
|
|
60
|
+
getPublicUrl(key: string): string;
|
|
61
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DragonflyKeyValueStoreClient = void 0;
|
|
4
|
+
const node_stream_1 = require("node:stream");
|
|
5
|
+
const utils_js_1 = require("./utils.js");
|
|
6
|
+
const billing_tracker_js_1 = require("./billing-tracker.js");
|
|
7
|
+
const configuration_js_1 = require("../configuration.js");
|
|
8
|
+
const kvs_iteration_js_1 = require("./kvs-iteration.js");
|
|
9
|
+
function makeAsyncIterableListKeys(promiseFn) {
|
|
10
|
+
const promise = promiseFn();
|
|
11
|
+
const iterable = {
|
|
12
|
+
[Symbol.asyncIterator]: () => {
|
|
13
|
+
let items = null;
|
|
14
|
+
let index = 0;
|
|
15
|
+
return {
|
|
16
|
+
async next() {
|
|
17
|
+
if (!items)
|
|
18
|
+
items = (await promise).items;
|
|
19
|
+
if (index >= items.length)
|
|
20
|
+
return { done: true, value: undefined };
|
|
21
|
+
return { done: false, value: items[index++] };
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
then: promise.then.bind(promise),
|
|
26
|
+
catch: promise.catch.bind(promise),
|
|
27
|
+
finally: promise.finally.bind(promise),
|
|
28
|
+
[Symbol.toStringTag]: 'AsyncIterablePromise',
|
|
29
|
+
};
|
|
30
|
+
return iterable;
|
|
31
|
+
}
|
|
32
|
+
class DragonflyKeyValueStoreClient {
|
|
33
|
+
client;
|
|
34
|
+
id;
|
|
35
|
+
infoKey;
|
|
36
|
+
recordsKey;
|
|
37
|
+
metaKey;
|
|
38
|
+
constructor(client, id) {
|
|
39
|
+
this.client = client;
|
|
40
|
+
this.id = id;
|
|
41
|
+
this.infoKey = client.key('kvs', id, 'info');
|
|
42
|
+
this.recordsKey = client.key('kvs', id, 'records');
|
|
43
|
+
this.metaKey = client.key('kvs', id, 'meta');
|
|
44
|
+
}
|
|
45
|
+
estimateStoredRecordBytes(key, stored, contentType) {
|
|
46
|
+
if (stored === null)
|
|
47
|
+
return 0;
|
|
48
|
+
if (contentType === 'application/octet-stream') {
|
|
49
|
+
return billing_tracker_js_1.BillingTracker.estimateRecordBytes(key, Buffer.from(stored, 'base64'));
|
|
50
|
+
}
|
|
51
|
+
return billing_tracker_js_1.BillingTracker.estimateRecordBytes(key, stored);
|
|
52
|
+
}
|
|
53
|
+
async get() {
|
|
54
|
+
const info = await this.client.redis.hgetall(this.infoKey);
|
|
55
|
+
if (!info || !info.id)
|
|
56
|
+
return undefined;
|
|
57
|
+
return {
|
|
58
|
+
id: info.id,
|
|
59
|
+
name: info.name || undefined,
|
|
60
|
+
userId: info.userId,
|
|
61
|
+
createdAt: new Date(info.createdAt),
|
|
62
|
+
modifiedAt: new Date(info.modifiedAt),
|
|
63
|
+
accessedAt: new Date(info.accessedAt),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async update(newFields) {
|
|
67
|
+
if (newFields.name !== undefined) {
|
|
68
|
+
await this.client.redis.hset(this.infoKey, 'name', newFields.name);
|
|
69
|
+
const indexKey = this.client.key('kvs:index');
|
|
70
|
+
const raw = await this.client.redis.hget(indexKey, `id:${this.id}`);
|
|
71
|
+
if (raw) {
|
|
72
|
+
const data = JSON.parse(raw);
|
|
73
|
+
if (data.name)
|
|
74
|
+
await this.client.redis.hdel(indexKey, `name:${data.name}`);
|
|
75
|
+
data.name = newFields.name;
|
|
76
|
+
await this.client.redis.hset(indexKey, `id:${this.id}`, JSON.stringify(data));
|
|
77
|
+
await this.client.redis.hset(indexKey, `name:${newFields.name}`, JSON.stringify(data));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
await this.client.redis.hset(this.infoKey, 'modifiedAt', (0, utils_js_1.nowISO)().toISOString());
|
|
81
|
+
return { name: newFields.name };
|
|
82
|
+
}
|
|
83
|
+
async delete() {
|
|
84
|
+
const info = await this.client.redis.hgetall(this.infoKey);
|
|
85
|
+
await this.client.redis.del(this.infoKey, this.recordsKey, this.metaKey);
|
|
86
|
+
const indexKey = this.client.key('kvs:index');
|
|
87
|
+
await this.client.redis.hdel(indexKey, `id:${this.id}`);
|
|
88
|
+
if (info?.name)
|
|
89
|
+
await this.client.redis.hdel(indexKey, `name:${info.name}`);
|
|
90
|
+
}
|
|
91
|
+
async recordExists(key) {
|
|
92
|
+
return (await this.client.redis.hexists(this.recordsKey, key)) === 1;
|
|
93
|
+
}
|
|
94
|
+
async getRecord(key, options) {
|
|
95
|
+
const raw = await this.client.redis.hget(this.recordsKey, key);
|
|
96
|
+
if (raw === null)
|
|
97
|
+
return undefined;
|
|
98
|
+
// Track read for billing - increment both user-level and info hash counters
|
|
99
|
+
if (this.client.billingContext) {
|
|
100
|
+
const billingTracker = new billing_tracker_js_1.BillingTracker(this.client);
|
|
101
|
+
await billingTracker.trackRead(this.client.billingContext.userId, 1);
|
|
102
|
+
await this.client.redis.hincrby(this.infoKey, 'readCount', 1);
|
|
103
|
+
// Track internal data transfer (bytes returned to actor)
|
|
104
|
+
const bytesTransferred = Buffer.byteLength(raw, 'utf8');
|
|
105
|
+
await billingTracker.trackInternalTransfer(this.client.billingContext.runId, this.client.billingContext.userId, bytesTransferred);
|
|
106
|
+
// Register this store as accessed for per-run aggregation
|
|
107
|
+
await this.client.redis.sadd(this.client.key('run', this.client.billingContext.runId, 'kvs_accessed'), this.id);
|
|
108
|
+
}
|
|
109
|
+
const meta = await this.client.redis.hget(this.metaKey, key);
|
|
110
|
+
const contentType = meta ? JSON.parse(meta).contentType : undefined;
|
|
111
|
+
const isBinary = contentType === 'application/octet-stream';
|
|
112
|
+
let value;
|
|
113
|
+
if (isBinary) {
|
|
114
|
+
value = Buffer.from(raw, 'base64');
|
|
115
|
+
}
|
|
116
|
+
else if (options?.buffer) {
|
|
117
|
+
value = Buffer.from(raw);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
try {
|
|
121
|
+
value = JSON.parse(raw);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
value = raw;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (options?.stream) {
|
|
128
|
+
const buf = Buffer.isBuffer(value) ? value : Buffer.from(typeof value === 'string' ? value : JSON.stringify(value));
|
|
129
|
+
value = node_stream_1.Readable.from(buf);
|
|
130
|
+
}
|
|
131
|
+
return { key, value, contentType };
|
|
132
|
+
}
|
|
133
|
+
async setRecord(record, _options, billingContext) {
|
|
134
|
+
const { key, value, contentType } = record;
|
|
135
|
+
let stored;
|
|
136
|
+
let billingValue = value; // Use for billing estimation (may differ from original if stream)
|
|
137
|
+
// Handle Readable streams (e.g., fs.createReadStream) - Apify compatibility
|
|
138
|
+
// Streams are objects with a pipe method. We need to buffer them.
|
|
139
|
+
if (value && typeof value === 'object' && typeof value.pipe === 'function') {
|
|
140
|
+
const chunks = [];
|
|
141
|
+
for await (const chunk of value) {
|
|
142
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
143
|
+
}
|
|
144
|
+
const buffered = Buffer.concat(chunks);
|
|
145
|
+
stored = buffered.toString('base64');
|
|
146
|
+
billingValue = buffered; // Use buffered value for billing estimation
|
|
147
|
+
}
|
|
148
|
+
else if (Buffer.isBuffer(value)) {
|
|
149
|
+
stored = value.toString('base64');
|
|
150
|
+
}
|
|
151
|
+
else if (typeof value === 'string') {
|
|
152
|
+
stored = value;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
stored = JSON.stringify(value);
|
|
156
|
+
}
|
|
157
|
+
const [existingRaw, existingMetaRaw] = await Promise.all([
|
|
158
|
+
this.client.redis.hget(this.recordsKey, key),
|
|
159
|
+
this.client.redis.hget(this.metaKey, key),
|
|
160
|
+
]);
|
|
161
|
+
const existingContentType = existingMetaRaw ? JSON.parse(existingMetaRaw).contentType : undefined;
|
|
162
|
+
const previousBytes = this.estimateStoredRecordBytes(key, existingRaw, existingContentType);
|
|
163
|
+
const nextBytes = billing_tracker_js_1.BillingTracker.estimateRecordBytes(key, billingValue);
|
|
164
|
+
const deltaBytes = nextBytes - previousBytes;
|
|
165
|
+
const pipeline = this.client.redis.pipeline();
|
|
166
|
+
pipeline.hset(this.recordsKey, key, stored);
|
|
167
|
+
if (contentType) {
|
|
168
|
+
pipeline.hset(this.metaKey, key, JSON.stringify({ contentType }));
|
|
169
|
+
}
|
|
170
|
+
pipeline.hset(this.infoKey, 'modifiedAt', (0, utils_js_1.nowISO)().toISOString());
|
|
171
|
+
await pipeline.exec();
|
|
172
|
+
// Track billing using client's billing context
|
|
173
|
+
if (this.client.billingContext) {
|
|
174
|
+
const billingTracker = new billing_tracker_js_1.BillingTracker(this.client);
|
|
175
|
+
if (deltaBytes !== 0) {
|
|
176
|
+
await billingTracker.trackStorageBytes(this.client.billingContext.runId, this.client.billingContext.userId, deltaBytes);
|
|
177
|
+
}
|
|
178
|
+
await billingTracker.trackWrite(this.client.billingContext.userId);
|
|
179
|
+
await this.client.redis.hincrby(this.infoKey, 'writeCount', 1);
|
|
180
|
+
// Track internal data transfer (bytes written to storage)
|
|
181
|
+
await billingTracker.trackInternalTransfer(this.client.billingContext.runId, this.client.billingContext.userId, nextBytes);
|
|
182
|
+
// Register this store as accessed for per-run aggregation
|
|
183
|
+
await this.client.redis.sadd(this.client.key('run', this.client.billingContext.runId, 'kvs_accessed'), this.id);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async deleteRecord(key) {
|
|
187
|
+
const [existingRaw, existingMetaRaw] = await Promise.all([
|
|
188
|
+
this.client.redis.hget(this.recordsKey, key),
|
|
189
|
+
this.client.redis.hget(this.metaKey, key),
|
|
190
|
+
]);
|
|
191
|
+
const pipeline = this.client.redis.pipeline();
|
|
192
|
+
pipeline.hdel(this.recordsKey, key);
|
|
193
|
+
pipeline.hdel(this.metaKey, key);
|
|
194
|
+
pipeline.hset(this.infoKey, 'modifiedAt', (0, utils_js_1.nowISO)().toISOString());
|
|
195
|
+
await pipeline.exec();
|
|
196
|
+
if (this.client.billingContext && existingRaw !== null) {
|
|
197
|
+
const billingTracker = new billing_tracker_js_1.BillingTracker(this.client);
|
|
198
|
+
const existingContentType = existingMetaRaw ? JSON.parse(existingMetaRaw).contentType : undefined;
|
|
199
|
+
const releasedBytes = this.estimateStoredRecordBytes(key, existingRaw, existingContentType);
|
|
200
|
+
if (releasedBytes > 0) {
|
|
201
|
+
await billingTracker.trackStorageBytes(this.client.billingContext.runId, this.client.billingContext.userId, -releasedBytes);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
listKeys(options) {
|
|
206
|
+
return makeAsyncIterableListKeys(() => this._listKeys(options));
|
|
207
|
+
}
|
|
208
|
+
async _listKeys(options) {
|
|
209
|
+
// HKEYS returns only field names; HGETALL would transfer every record's
|
|
210
|
+
// value (including base64-encoded blobs) just to discard them here.
|
|
211
|
+
let allKeys = await this.client.redis.hkeys(this.recordsKey);
|
|
212
|
+
if (options?.prefix) {
|
|
213
|
+
allKeys = allKeys.filter(k => k.startsWith(options.prefix));
|
|
214
|
+
}
|
|
215
|
+
if (options?.collection) {
|
|
216
|
+
const info = await this.client.redis.hgetall(this.infoKey);
|
|
217
|
+
allKeys = (0, kvs_iteration_js_1.filterKeysByCollection)(allKeys, info.schema, options.collection);
|
|
218
|
+
}
|
|
219
|
+
allKeys.sort();
|
|
220
|
+
if (options?.exclusiveStartKey) {
|
|
221
|
+
// Keys are sorted, so keep everything strictly after the start key.
|
|
222
|
+
// Comparing (instead of indexOf) also handles the start key having been
|
|
223
|
+
// deleted between pages, which previously restarted the page from the
|
|
224
|
+
// beginning and could make paginating iterators loop forever.
|
|
225
|
+
allKeys = allKeys.filter((k) => k > options.exclusiveStartKey);
|
|
226
|
+
}
|
|
227
|
+
const limit = options?.limit ?? allKeys.length;
|
|
228
|
+
const isTruncated = allKeys.length > limit;
|
|
229
|
+
const sliced = allKeys.slice(0, limit);
|
|
230
|
+
// Fetch value lengths only (HSTRLEN) in one pipelined round trip;
|
|
231
|
+
// this avoids transferring each record's full value just to compute its size.
|
|
232
|
+
const pipeline = this.client.redis.pipeline();
|
|
233
|
+
for (const key of sliced)
|
|
234
|
+
pipeline.hstrlen(this.recordsKey, key);
|
|
235
|
+
const results = await pipeline.exec();
|
|
236
|
+
const items = sliced.map((key, i) => ({
|
|
237
|
+
key,
|
|
238
|
+
size: results?.[i]?.[1] ?? 0,
|
|
239
|
+
}));
|
|
240
|
+
// Track list operation for billing
|
|
241
|
+
if (this.client.billingContext) {
|
|
242
|
+
const billingTracker = new billing_tracker_js_1.BillingTracker(this.client);
|
|
243
|
+
await billingTracker.trackList(this.client.billingContext.userId, 1);
|
|
244
|
+
// Increment the per-store listCount so the worker can tally KVS list operations
|
|
245
|
+
await this.client.redis.hincrby(this.infoKey, 'listCount', 1);
|
|
246
|
+
// Register this store as accessed for per-run aggregation (always, even if empty)
|
|
247
|
+
await this.client.redis.sadd(this.client.key('run', this.client.billingContext.runId, 'kvs_accessed'), this.id);
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
count: items.length,
|
|
251
|
+
limit,
|
|
252
|
+
exclusiveStartKey: options?.exclusiveStartKey,
|
|
253
|
+
isTruncated,
|
|
254
|
+
nextExclusiveStartKey: isTruncated ? sliced[sliced.length - 1] : undefined,
|
|
255
|
+
items,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
keys(options) {
|
|
259
|
+
return (0, kvs_iteration_js_1.makePaginatingKeysIterable)((opts) => this._listKeys(opts), options);
|
|
260
|
+
}
|
|
261
|
+
values(options) {
|
|
262
|
+
return (0, kvs_iteration_js_1.makePaginatingValuesIterable)((opts) => this._listKeys(opts), (key) => this.getRecord(key), options);
|
|
263
|
+
}
|
|
264
|
+
entries(options) {
|
|
265
|
+
return (0, kvs_iteration_js_1.makePaginatingEntriesIterable)((opts) => this._listKeys(opts), (key) => this.getRecord(key), options);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Iterates over all keys in the key-value store, calling the iteratee function for each key.
|
|
269
|
+
* Similar to Apify's KeyValueStore.forEachKey() method.
|
|
270
|
+
*/
|
|
271
|
+
async forEachKey(iteratee, options) {
|
|
272
|
+
return (0, kvs_iteration_js_1.forEachKeyPaginated)((opts) => this._listKeys(opts), iteratee, options);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Gets a value from the key-value store that is automatically saved when modified.
|
|
276
|
+
* If the key doesn't exist, returns the defaultValue and stores it for future use.
|
|
277
|
+
* Similar to Apify's KeyValueStore.getAutoSavedValue() method.
|
|
278
|
+
*
|
|
279
|
+
* @param key - The key of the record to get
|
|
280
|
+
* @param defaultValue - The default value to return and store if the key doesn't exist
|
|
281
|
+
* @returns The stored value or the defaultValue
|
|
282
|
+
*/
|
|
283
|
+
async getAutoSavedValue(key, defaultValue) {
|
|
284
|
+
const record = await this.getRecord(key);
|
|
285
|
+
if (record === undefined) {
|
|
286
|
+
// Store the default value and return it
|
|
287
|
+
await this.setRecord({ key, value: defaultValue });
|
|
288
|
+
return defaultValue;
|
|
289
|
+
}
|
|
290
|
+
return record.value;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Returns a URL for the given key that may be used to publicly access the value
|
|
294
|
+
* in the remote key-value store.
|
|
295
|
+
* Similar to Apify's KeyValueStore.getPublicUrl() method.
|
|
296
|
+
*
|
|
297
|
+
* @param key - The key of the record to get the public URL for
|
|
298
|
+
* @returns A public URL for accessing the record
|
|
299
|
+
*/
|
|
300
|
+
getPublicUrl(key) {
|
|
301
|
+
const config = configuration_js_1.Configuration.getGlobalConfig();
|
|
302
|
+
const baseUrl = config.get('apiPublicBaseUrl') || config.get('apiBaseUrl') || 'https://api-staging.scrape.ly';
|
|
303
|
+
return `${baseUrl}/v2/key-value-stores/${this.id}/records/${key}`;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
exports.DragonflyKeyValueStoreClient = DragonflyKeyValueStoreClient;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { KeyValueStoreCollectionClient, KeyValueStoreInfo, PaginatedList } from '@crawlee/types';
|
|
2
|
+
import type { DragonflyClient } from './dragonfly-client.js';
|
|
3
|
+
export declare class DragonflyKeyValueStoreCollectionClient implements KeyValueStoreCollectionClient {
|
|
4
|
+
private client;
|
|
5
|
+
constructor(client: DragonflyClient);
|
|
6
|
+
getOrCreate(name?: string): Promise<KeyValueStoreInfo>;
|
|
7
|
+
list(): Promise<PaginatedList<KeyValueStoreInfo>>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DragonflyKeyValueStoreCollectionClient = void 0;
|
|
4
|
+
const utils_js_1 = require("./utils.js");
|
|
5
|
+
class DragonflyKeyValueStoreCollectionClient {
|
|
6
|
+
client;
|
|
7
|
+
constructor(client) {
|
|
8
|
+
this.client = client;
|
|
9
|
+
}
|
|
10
|
+
async getOrCreate(name) {
|
|
11
|
+
const indexKey = this.client.key('kvs:index');
|
|
12
|
+
if (name) {
|
|
13
|
+
const existing = await this.client.redis.hget(indexKey, `name:${name}`);
|
|
14
|
+
if (existing)
|
|
15
|
+
return JSON.parse(existing);
|
|
16
|
+
}
|
|
17
|
+
const id = (0, utils_js_1.generateId)();
|
|
18
|
+
const now = (0, utils_js_1.nowISO)();
|
|
19
|
+
const data = {
|
|
20
|
+
id,
|
|
21
|
+
name,
|
|
22
|
+
userId: '1',
|
|
23
|
+
createdAt: now,
|
|
24
|
+
modifiedAt: now,
|
|
25
|
+
accessedAt: now,
|
|
26
|
+
};
|
|
27
|
+
await this.client.redis.hset(indexKey, `id:${id}`, JSON.stringify(data));
|
|
28
|
+
if (name)
|
|
29
|
+
await this.client.redis.hset(indexKey, `name:${name}`, JSON.stringify(data));
|
|
30
|
+
const infoKey = this.client.key('kvs', id, 'info');
|
|
31
|
+
const recordsKey = this.client.key('kvs', id, 'records');
|
|
32
|
+
const metaKey = this.client.key('kvs', id, 'meta');
|
|
33
|
+
await this.client.redis.hset(infoKey, {
|
|
34
|
+
id,
|
|
35
|
+
name: name ?? '',
|
|
36
|
+
userId: '1',
|
|
37
|
+
createdAt: now.toISOString(),
|
|
38
|
+
modifiedAt: now.toISOString(),
|
|
39
|
+
accessedAt: now.toISOString(),
|
|
40
|
+
});
|
|
41
|
+
// Set TTL for unnamed KV stores (named stores have indefinite retention)
|
|
42
|
+
if (!name) {
|
|
43
|
+
const ttl = this.client.ttlManager.getTtl('kv', this.client.userPlan, false);
|
|
44
|
+
if (ttl) {
|
|
45
|
+
await this.client.ttlManager.setExpiry(this.client.redis, [infoKey, recordsKey, metaKey], ttl);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return data;
|
|
49
|
+
}
|
|
50
|
+
async list() {
|
|
51
|
+
const indexKey = this.client.key('kvs:index');
|
|
52
|
+
const all = await this.client.redis.hgetall(indexKey);
|
|
53
|
+
const items = [];
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
for (const [key, val] of Object.entries(all)) {
|
|
56
|
+
if (!key.startsWith('id:'))
|
|
57
|
+
continue;
|
|
58
|
+
const parsed = JSON.parse(val);
|
|
59
|
+
if (seen.has(parsed.id))
|
|
60
|
+
continue;
|
|
61
|
+
seen.add(parsed.id);
|
|
62
|
+
const infoKey = this.client.key('kvs', parsed.id, 'info');
|
|
63
|
+
// Skip and clean up stale index entries whose keys have expired via TTL
|
|
64
|
+
const exists = await this.client.redis.exists(infoKey);
|
|
65
|
+
if (!exists) {
|
|
66
|
+
await this.client.redis.hdel(indexKey, `id:${parsed.id}`);
|
|
67
|
+
if (parsed.name)
|
|
68
|
+
await this.client.redis.hdel(indexKey, `name:${parsed.name}`);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
items.push(parsed);
|
|
72
|
+
}
|
|
73
|
+
return { total: items.length, count: items.length, offset: 0, limit: items.length, items };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
exports.DragonflyKeyValueStoreCollectionClient = DragonflyKeyValueStoreCollectionClient;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { KeyValueStoreClientListData, KeyValueStoreClientListOptions, KeyValueStoreItemData } from '@crawlee/types';
|
|
2
|
+
export type ListKeysFn = (options?: KeyValueStoreClientListOptions) => Promise<KeyValueStoreClientListData>;
|
|
3
|
+
/**
|
|
4
|
+
* Filter keys by Apify-style collection config from a KVS schema.
|
|
5
|
+
*/
|
|
6
|
+
export declare function filterKeysByCollection(allKeys: string[], schemaJson: string | undefined, collection: string): string[];
|
|
7
|
+
export declare function paginateKeyItems(listKeys: ListKeysFn, options?: KeyValueStoreClientListOptions): AsyncGenerator<KeyValueStoreItemData>;
|
|
8
|
+
export declare function forEachKeyPaginated(listKeys: ListKeysFn, iteratee: (key: string, index: number, info: {
|
|
9
|
+
size: number;
|
|
10
|
+
}) => Promise<void> | void, options?: KeyValueStoreClientListOptions, startIndex?: number): Promise<void>;
|
|
11
|
+
export declare function makePaginatingKeysIterable(listKeys: ListKeysFn, options?: KeyValueStoreClientListOptions): AsyncIterable<string> & Promise<KeyValueStoreClientListData>;
|
|
12
|
+
export declare function makePaginatingValuesIterable(listKeys: ListKeysFn, getRecord: (key: string) => Promise<{
|
|
13
|
+
value: unknown;
|
|
14
|
+
} | undefined>, options?: KeyValueStoreClientListOptions): AsyncIterable<unknown> & Promise<unknown[]>;
|
|
15
|
+
export declare function makePaginatingEntriesIterable(listKeys: ListKeysFn, getRecord: (key: string) => Promise<{
|
|
16
|
+
value: unknown;
|
|
17
|
+
} | undefined>, options?: KeyValueStoreClientListOptions): AsyncIterable<[string, unknown]> & Promise<[string, unknown][]>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.filterKeysByCollection = filterKeysByCollection;
|
|
4
|
+
exports.paginateKeyItems = paginateKeyItems;
|
|
5
|
+
exports.forEachKeyPaginated = forEachKeyPaginated;
|
|
6
|
+
exports.makePaginatingKeysIterable = makePaginatingKeysIterable;
|
|
7
|
+
exports.makePaginatingValuesIterable = makePaginatingValuesIterable;
|
|
8
|
+
exports.makePaginatingEntriesIterable = makePaginatingEntriesIterable;
|
|
9
|
+
/**
|
|
10
|
+
* Filter keys by Apify-style collection config from a KVS schema.
|
|
11
|
+
*/
|
|
12
|
+
function filterKeysByCollection(allKeys, schemaJson, collection) {
|
|
13
|
+
if (!schemaJson) {
|
|
14
|
+
throw new Error('Key-value store schema is not configured for collection filtering.');
|
|
15
|
+
}
|
|
16
|
+
let schema;
|
|
17
|
+
try {
|
|
18
|
+
schema = JSON.parse(schemaJson);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw new Error('Key-value store schema is invalid JSON.');
|
|
22
|
+
}
|
|
23
|
+
const cfg = schema?.collections?.[collection];
|
|
24
|
+
if (!cfg) {
|
|
25
|
+
throw new Error(`Collection '${collection}' is not defined in the key-value store schema.`);
|
|
26
|
+
}
|
|
27
|
+
if (cfg.keyPrefix && typeof cfg.keyPrefix === 'string') {
|
|
28
|
+
return allKeys.filter((k) => k.startsWith(cfg.keyPrefix));
|
|
29
|
+
}
|
|
30
|
+
if (cfg.key && typeof cfg.key === 'string') {
|
|
31
|
+
return allKeys.filter((k) => k === cfg.key);
|
|
32
|
+
}
|
|
33
|
+
if (Array.isArray(cfg.keys)) {
|
|
34
|
+
const set = new Set(cfg.keys.map(String));
|
|
35
|
+
return allKeys.filter((k) => set.has(k));
|
|
36
|
+
}
|
|
37
|
+
return allKeys;
|
|
38
|
+
}
|
|
39
|
+
async function* paginateKeyItems(listKeys, options = {}) {
|
|
40
|
+
let exclusiveStartKey = options.exclusiveStartKey;
|
|
41
|
+
const { prefix, collection, limit } = options;
|
|
42
|
+
for (;;) {
|
|
43
|
+
const response = await listKeys({ prefix, collection, limit, exclusiveStartKey });
|
|
44
|
+
for (const item of response.items) {
|
|
45
|
+
yield item;
|
|
46
|
+
}
|
|
47
|
+
if (!response.isTruncated || !response.nextExclusiveStartKey) {
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
exclusiveStartKey = response.nextExclusiveStartKey;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function forEachKeyPaginated(listKeys, iteratee, options = {}, startIndex = 0) {
|
|
54
|
+
let index = startIndex;
|
|
55
|
+
let exclusiveStartKey = options.exclusiveStartKey;
|
|
56
|
+
const { prefix, collection, limit } = options;
|
|
57
|
+
for (;;) {
|
|
58
|
+
const response = await listKeys({ prefix, collection, limit, exclusiveStartKey });
|
|
59
|
+
for (const item of response.items) {
|
|
60
|
+
await iteratee(item.key, index++, { size: item.size });
|
|
61
|
+
}
|
|
62
|
+
if (!response.isTruncated || !response.nextExclusiveStartKey) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
exclusiveStartKey = response.nextExclusiveStartKey;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function makeHybridIterable(iterate, promise) {
|
|
69
|
+
// A native Promise has no own enumerable properties (then/catch/finally live on
|
|
70
|
+
// Promise.prototype), so Object.assign(iterable, promise) copies nothing and the
|
|
71
|
+
// result is not thenable — awaiting it resolved to the iterable object itself.
|
|
72
|
+
// Explicitly bind the promise methods instead (same pattern as
|
|
73
|
+
// makeAsyncIterableListKeys in key-value-store-client.ts).
|
|
74
|
+
const iterable = {
|
|
75
|
+
[Symbol.asyncIterator]: iterate,
|
|
76
|
+
then: promise.then.bind(promise),
|
|
77
|
+
catch: promise.catch.bind(promise),
|
|
78
|
+
finally: promise.finally.bind(promise),
|
|
79
|
+
[Symbol.toStringTag]: 'AsyncIterablePromise',
|
|
80
|
+
};
|
|
81
|
+
return iterable;
|
|
82
|
+
}
|
|
83
|
+
function makePaginatingKeysIterable(listKeys, options) {
|
|
84
|
+
const opts = options ?? {};
|
|
85
|
+
return makeHybridIterable(() => (async function* () {
|
|
86
|
+
for await (const item of paginateKeyItems(listKeys, opts)) {
|
|
87
|
+
yield item.key;
|
|
88
|
+
}
|
|
89
|
+
})(), listKeys(opts));
|
|
90
|
+
}
|
|
91
|
+
function makePaginatingValuesIterable(listKeys, getRecord, options) {
|
|
92
|
+
const opts = options ?? {};
|
|
93
|
+
return makeHybridIterable(() => (async function* () {
|
|
94
|
+
for await (const item of paginateKeyItems(listKeys, opts)) {
|
|
95
|
+
const record = await getRecord(item.key);
|
|
96
|
+
if (record)
|
|
97
|
+
yield record.value;
|
|
98
|
+
}
|
|
99
|
+
})(), (async () => {
|
|
100
|
+
const values = [];
|
|
101
|
+
for await (const item of paginateKeyItems(listKeys, opts)) {
|
|
102
|
+
const record = await getRecord(item.key);
|
|
103
|
+
if (record)
|
|
104
|
+
values.push(record.value);
|
|
105
|
+
}
|
|
106
|
+
return values;
|
|
107
|
+
})());
|
|
108
|
+
}
|
|
109
|
+
function makePaginatingEntriesIterable(listKeys, getRecord, options) {
|
|
110
|
+
const opts = options ?? {};
|
|
111
|
+
return makeHybridIterable(() => (async function* () {
|
|
112
|
+
for await (const item of paginateKeyItems(listKeys, opts)) {
|
|
113
|
+
const record = await getRecord(item.key);
|
|
114
|
+
if (record)
|
|
115
|
+
yield [item.key, record.value];
|
|
116
|
+
}
|
|
117
|
+
})(), (async () => {
|
|
118
|
+
const entries = [];
|
|
119
|
+
for await (const item of paginateKeyItems(listKeys, opts)) {
|
|
120
|
+
const record = await getRecord(item.key);
|
|
121
|
+
if (record)
|
|
122
|
+
entries.push([item.key, record.value]);
|
|
123
|
+
}
|
|
124
|
+
return entries;
|
|
125
|
+
})());
|
|
126
|
+
}
|