@crawlee/core 4.0.0-rc.0 → 4.0.0-rc.1

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 (122) hide show
  1. package/README.md +1 -1
  2. package/configuration.d.ts +15 -46
  3. package/configuration.js +8 -20
  4. package/errors.d.ts +12 -53
  5. package/errors.js +13 -66
  6. package/events/index.d.ts +1 -0
  7. package/events/local_event_manager.d.ts +0 -7
  8. package/events/local_event_manager.js +8 -8
  9. package/events/system_info.d.ts +38 -0
  10. package/index.d.ts +2 -8
  11. package/index.js +4 -8
  12. package/internal.d.ts +8 -0
  13. package/internal.js +9 -0
  14. package/log.d.ts +10 -11
  15. package/log.js +53 -25
  16. package/memory-storage/memory-storage.d.ts +12 -7
  17. package/memory-storage/memory-storage.js +45 -17
  18. package/memory-storage/resource-clients/dataset.d.ts +0 -5
  19. package/memory-storage/resource-clients/dataset.js +17 -20
  20. package/memory-storage/resource-clients/key-value-store.d.ts +0 -9
  21. package/memory-storage/resource-clients/key-value-store.js +11 -33
  22. package/memory-storage/resource-clients/request-queue.d.ts +0 -22
  23. package/memory-storage/resource-clients/request-queue.js +57 -53
  24. package/package.json +16 -18
  25. package/proxy_configuration.d.ts +20 -23
  26. package/proxy_configuration.js +18 -12
  27. package/recoverable_state.d.ts +28 -6
  28. package/recoverable_state.js +51 -14
  29. package/request.d.ts +18 -104
  30. package/request.js +41 -220
  31. package/serialization.js +3 -3
  32. package/service_locator.d.ts +3 -0
  33. package/service_locator.js +2 -0
  34. package/storages/dataset.d.ts +9 -15
  35. package/storages/dataset.js +22 -14
  36. package/storages/index.d.ts +3 -4
  37. package/storages/index.js +1 -4
  38. package/storages/key_value_store.d.ts +12 -46
  39. package/storages/key_value_store.js +31 -47
  40. package/storages/key_value_store_codec.js +6 -11
  41. package/storages/request_dedup_cache.d.ts +0 -2
  42. package/storages/request_dedup_cache.js +8 -8
  43. package/storages/request_list.d.ts +6 -82
  44. package/storages/request_list.js +175 -179
  45. package/storages/request_loader.d.ts +48 -22
  46. package/storages/request_loader.js +36 -1
  47. package/storages/request_manager.d.ts +86 -0
  48. package/storages/request_manager_tandem.d.ts +13 -28
  49. package/storages/request_manager_tandem.js +46 -43
  50. package/storages/request_queue.d.ts +19 -49
  51. package/storages/request_queue.js +92 -88
  52. package/storages/storage_instance_manager.d.ts +1 -2
  53. package/storages/storage_instance_manager.js +4 -4
  54. package/storages/transaction.d.ts +27 -9
  55. package/storages/transaction.js +56 -11
  56. package/storages/utils.d.ts +2 -2
  57. package/validators.d.ts +3 -2
  58. package/validators.js +3 -2
  59. package/autoscaling/autoscaled_pool.d.ts +0 -195
  60. package/autoscaling/autoscaled_pool.js +0 -386
  61. package/autoscaling/concurrency_system.d.ts +0 -268
  62. package/autoscaling/concurrency_system.js +0 -362
  63. package/autoscaling/cpu_load_signal.d.ts +0 -43
  64. package/autoscaling/cpu_load_signal.js +0 -47
  65. package/autoscaling/event_loop_load_signal.d.ts +0 -51
  66. package/autoscaling/event_loop_load_signal.js +0 -60
  67. package/autoscaling/index.d.ts +0 -9
  68. package/autoscaling/index.js +0 -9
  69. package/autoscaling/load_signal.d.ts +0 -100
  70. package/autoscaling/load_signal.js +0 -105
  71. package/autoscaling/memory_load_signal.d.ts +0 -47
  72. package/autoscaling/memory_load_signal.js +0 -106
  73. package/autoscaling/snapshotter.d.ts +0 -84
  74. package/autoscaling/snapshotter.js +0 -67
  75. package/autoscaling/storage_backend_load_signal.d.ts +0 -56
  76. package/autoscaling/storage_backend_load_signal.js +0 -73
  77. package/autoscaling/system_status.d.ts +0 -159
  78. package/autoscaling/system_status.js +0 -139
  79. package/autoscaling/weighted_avg.d.ts +0 -5
  80. package/autoscaling/weighted_avg.js +0 -14
  81. package/cookie_utils.d.ts +0 -44
  82. package/cookie_utils.js +0 -122
  83. package/crawlers/context_pipeline.d.ts +0 -70
  84. package/crawlers/context_pipeline.js +0 -122
  85. package/crawlers/crawler_commons.d.ts +0 -159
  86. package/crawlers/error_snapshotter.d.ts +0 -57
  87. package/crawlers/error_snapshotter.js +0 -117
  88. package/crawlers/error_tracker.d.ts +0 -54
  89. package/crawlers/error_tracker.js +0 -308
  90. package/crawlers/index.d.ts +0 -5
  91. package/crawlers/index.js +0 -4
  92. package/crawlers/internals/types.d.ts +0 -7
  93. package/crawlers/internals/types.js +0 -1
  94. package/crawlers/statistics.d.ts +0 -328
  95. package/crawlers/statistics.js +0 -536
  96. package/enqueue_links/enqueue_links.d.ts +0 -156
  97. package/enqueue_links/enqueue_links.js +0 -78
  98. package/enqueue_links/index.d.ts +0 -2
  99. package/enqueue_links/index.js +0 -2
  100. package/enqueue_links/shared.d.ts +0 -93
  101. package/enqueue_links/shared.js +0 -239
  102. package/http.d.ts +0 -9
  103. package/http.js +0 -28
  104. package/router.d.ts +0 -306
  105. package/router.js +0 -309
  106. package/session_pool/consts.d.ts +0 -3
  107. package/session_pool/consts.js +0 -3
  108. package/session_pool/errors.d.ts +0 -7
  109. package/session_pool/errors.js +0 -11
  110. package/session_pool/fingerprint.d.ts +0 -9
  111. package/session_pool/fingerprint.js +0 -30
  112. package/session_pool/index.d.ts +0 -4
  113. package/session_pool/index.js +0 -4
  114. package/session_pool/session.d.ts +0 -150
  115. package/session_pool/session.js +0 -220
  116. package/session_pool/session_pool.d.ts +0 -240
  117. package/session_pool/session_pool.js +0 -394
  118. package/storages/sitemap_request_loader.d.ts +0 -201
  119. package/storages/sitemap_request_loader.js +0 -438
  120. package/storages/throttling_request_manager.d.ts +0 -239
  121. package/storages/throttling_request_manager.js +0 -646
  122. /package/{crawlers/crawler_commons.js → events/system_info.js} +0 -0
package/log.js CHANGED
@@ -30,23 +30,43 @@ import log, { Log, Logger, LoggerJson, LoggerText, LogLevel } from '@apify/log';
30
30
  * ```
31
31
  */
32
32
  export class BaseCrawleeLogger {
33
- // kept as TS-private: the adaptive crawler's log proxy calls non-intercepted methods with `this === proxy`,
34
- // where `#`-field access would throw at runtime
35
- options;
36
- warningsLogged = new Set();
33
+ // Note: If wrapping logger in a Proxy, unbound methods calling #-fields throw TypeError
34
+ // unless bound to the target (see createLogProxy in adaptive-playwright-crawler.ts).
35
+ #options;
36
+ #loggedOnce = new Set();
37
37
  constructor(options = {}) {
38
- this.options = options;
38
+ this.#options = options;
39
39
  }
40
40
  getOptions() {
41
- return this.options;
41
+ return this.#options;
42
42
  }
43
43
  setOptions(options) {
44
- this.options = { ...this.options, ...options };
44
+ this.#options = { ...this.#options, ...options };
45
45
  }
46
46
  child(options) {
47
47
  return this.createChild(options);
48
48
  }
49
- error(message, data) {
49
+ /**
50
+ * Whether `once` suppresses this message. Keyed by level as well as text, so that a message logged once as
51
+ * `info` does not swallow the same text logged as a `warning`.
52
+ *
53
+ * Handled here rather than in {@link BaseCrawleeLogger.logWithLevel} so that every logger gets it, instead
54
+ * of each implementation having to dedupe for itself.
55
+ */
56
+ #suppressedAsRepeat(level, message, options) {
57
+ if (!options?.once) {
58
+ return false;
59
+ }
60
+ const key = `${level}:${message}`;
61
+ if (this.#loggedOnce.has(key)) {
62
+ return true;
63
+ }
64
+ this.#loggedOnce.add(key);
65
+ return false;
66
+ }
67
+ error(message, data, options) {
68
+ if (this.#suppressedAsRepeat(LogLevel.ERROR, message, options))
69
+ return;
50
70
  this.logWithLevel(LogLevel.ERROR, message, data);
51
71
  }
52
72
  exception(exception, message, data) {
@@ -56,25 +76,35 @@ export class BaseCrawleeLogger {
56
76
  exception,
57
77
  });
58
78
  }
59
- softFail(message, data) {
79
+ softFail(message, data, options) {
80
+ if (this.#suppressedAsRepeat(LogLevel.SOFT_FAIL, message, options))
81
+ return;
60
82
  this.logWithLevel(LogLevel.SOFT_FAIL, message, data);
61
83
  }
62
- warning(message, data) {
84
+ warning(message, data, options) {
85
+ if (this.#suppressedAsRepeat(LogLevel.WARNING, message, options))
86
+ return;
63
87
  this.logWithLevel(LogLevel.WARNING, message, data);
64
88
  }
65
89
  warningOnce(message) {
66
- if (!this.warningsLogged.has(message)) {
67
- this.warningsLogged.add(message);
68
- this.warning(message);
69
- }
70
- }
71
- info(message, data) {
90
+ // Gates before delegating, so that a suppressed repeat is not even a `warning()` call.
91
+ if (this.#suppressedAsRepeat(LogLevel.WARNING, message, { once: true }))
92
+ return;
93
+ this.warning(message);
94
+ }
95
+ info(message, data, options) {
96
+ if (this.#suppressedAsRepeat(LogLevel.INFO, message, options))
97
+ return;
72
98
  this.logWithLevel(LogLevel.INFO, message, data);
73
99
  }
74
- debug(message, data) {
100
+ debug(message, data, options) {
101
+ if (this.#suppressedAsRepeat(LogLevel.DEBUG, message, options))
102
+ return;
75
103
  this.logWithLevel(LogLevel.DEBUG, message, data);
76
104
  }
77
- perf(message, data) {
105
+ perf(message, data, options) {
106
+ if (this.#suppressedAsRepeat(LogLevel.PERF, message, options))
107
+ return;
78
108
  this.logWithLevel(LogLevel.PERF, `[PERF] ${message}`, data);
79
109
  }
80
110
  deprecated(message) {
@@ -88,18 +118,16 @@ export class BaseCrawleeLogger {
88
118
  * Users who want to use a different logging library should implement {@link BaseCrawleeLogger} directly.
89
119
  */
90
120
  export class ApifyLogAdapter extends BaseCrawleeLogger {
91
- apifyLog;
92
- constructor(
93
- // kept as a TS-private parameter property: reached through the adaptive crawler's log proxy, see above
94
- apifyLog, options) {
121
+ #apifyLog;
122
+ constructor(apifyLog, options) {
95
123
  super(options ?? {});
96
- this.apifyLog = apifyLog;
124
+ this.#apifyLog = apifyLog;
97
125
  }
98
126
  logWithLevel(level, message, data) {
99
- this.apifyLog.internal(level, message, data);
127
+ this.#apifyLog.internal(level, message, data);
100
128
  }
101
129
  createChild(options) {
102
- return new ApifyLogAdapter(this.apifyLog.child({ prefix: options.prefix ?? null }), {
130
+ return new ApifyLogAdapter(this.#apifyLog.child({ prefix: options.prefix ?? null }), {
103
131
  ...this.getOptions(),
104
132
  ...options,
105
133
  });
@@ -1,8 +1,5 @@
1
1
  import type * as storage from '@crawlee/types';
2
2
  import type { CrawleeLogger } from '@crawlee/types';
3
- import { DatasetBackend } from './resource-clients/dataset.js';
4
- import { KeyValueStoreBackend } from './resource-clients/key-value-store.js';
5
- import { RequestQueueBackend } from './resource-clients/request-queue.js';
6
3
  export interface MemoryStorageOptions {
7
4
  /**
8
5
  * Optional logger for MemoryStorageBackend warnings.
@@ -12,18 +9,26 @@ export interface MemoryStorageOptions {
12
9
  export declare class MemoryStorageBackend implements storage.StorageBackend {
13
10
  #private;
14
11
  readonly logger?: CrawleeLogger;
15
- readonly keyValueStoreBackendCache: KeyValueStoreBackend[];
16
- readonly datasetBackendCache: DatasetBackend[];
17
- readonly requestQueueBackendCache: RequestQueueBackend[];
18
12
  constructor(options?: MemoryStorageOptions);
19
13
  /**
20
14
  * Return a per-instance unique cache key so that distinct `MemoryStorageBackend` instances get separate
21
15
  * cache partitions in the storage backend cache.
22
16
  */
23
17
  getStorageBackendCacheKey(): string;
18
+ /**
19
+ * Evict a cached backend so that a dropped storage is no longer resolved by `createXBackend`,
20
+ * reported by `storageExists` or visited by `purge`. Returns whether the backend was cached, which
21
+ * tells the caller whether it still owns in-memory state worth clearing.
22
+ *
23
+ * The resource clients own their own entry's lifetime but must not reach into the caches directly.
24
+ * Because a client is only ever constructed by `createXBackend`, which caches it immediately, the
25
+ * entry matching `id` is always the caller itself.
26
+ * @internal
27
+ */
28
+ evictBackend(type: 'Dataset' | 'KeyValueStore' | 'RequestQueue', id: string): boolean;
24
29
  createDatasetBackend(options?: storage.StorageIdentifier): Promise<storage.DatasetBackend>;
25
30
  createKeyValueStoreBackend(options?: storage.StorageIdentifier): Promise<storage.KeyValueStoreBackend>;
26
- createRequestQueueBackend(options?: storage.StorageIdentifier): Promise<RequestQueueBackend>;
31
+ createRequestQueueBackend(options?: storage.StorageIdentifier): Promise<storage.RequestQueueBackend>;
27
32
  storageExists(id: string, type: 'Dataset' | 'KeyValueStore' | 'RequestQueue'): Promise<boolean>;
28
33
  /**
29
34
  * Cleans up the run-scoped storages before the run starts. For the in-memory storage this simply
@@ -11,9 +11,9 @@ export class MemoryStorageBackend {
11
11
  * cache by storage directory: two distinct `MemoryStorageBackend` instances must not share cached backends.
12
12
  */
13
13
  #instanceCacheKey = `MemoryStorageBackend:${randomUUID()}`;
14
- keyValueStoreBackendCache = [];
15
- datasetBackendCache = [];
16
- requestQueueBackendCache = [];
14
+ #keyValueStoreBackendCache = [];
15
+ #datasetBackendCache = [];
16
+ #requestQueueBackendCache = [];
17
17
  constructor(options = {}) {
18
18
  this.logger = options.logger;
19
19
  }
@@ -24,6 +24,36 @@ export class MemoryStorageBackend {
24
24
  getStorageBackendCacheKey() {
25
25
  return this.#instanceCacheKey;
26
26
  }
27
+ /**
28
+ * Evict a cached backend so that a dropped storage is no longer resolved by `createXBackend`,
29
+ * reported by `storageExists` or visited by `purge`. Returns whether the backend was cached, which
30
+ * tells the caller whether it still owns in-memory state worth clearing.
31
+ *
32
+ * The resource clients own their own entry's lifetime but must not reach into the caches directly.
33
+ * Because a client is only ever constructed by `createXBackend`, which caches it immediately, the
34
+ * entry matching `id` is always the caller itself.
35
+ * @internal
36
+ */
37
+ evictBackend(type, id) {
38
+ let cache;
39
+ switch (type) {
40
+ case 'Dataset':
41
+ cache = this.#datasetBackendCache;
42
+ break;
43
+ case 'KeyValueStore':
44
+ cache = this.#keyValueStoreBackendCache;
45
+ break;
46
+ case 'RequestQueue':
47
+ cache = this.#requestQueueBackendCache;
48
+ break;
49
+ }
50
+ const index = cache.findIndex((entry) => entry.id === id);
51
+ if (index === -1) {
52
+ return false;
53
+ }
54
+ cache.splice(index, 1);
55
+ return true;
56
+ }
27
57
  static #resolveStorageKey(options) {
28
58
  // No identifier at all means the default storage, which is opened under the reserved alias —
29
59
  // same rule as `resolveStorageIdentifier` in the storage frontends, so that a backend used
@@ -37,7 +67,7 @@ export class MemoryStorageBackend {
37
67
  }
38
68
  async createDatasetBackend(options = {}) {
39
69
  const { isAlias, cacheKey } = MemoryStorageBackend.#resolveStorageKey(options);
40
- const found = this.datasetBackendCache.find((store) => store.id === cacheKey ||
70
+ const found = this.#datasetBackendCache.find((store) => store.id === cacheKey ||
41
71
  store.name?.toLowerCase() === cacheKey.toLowerCase() ||
42
72
  store.cacheKey.toLowerCase() === cacheKey.toLowerCase());
43
73
  if (found) {
@@ -48,12 +78,12 @@ export class MemoryStorageBackend {
48
78
  cacheKey,
49
79
  storageBackend: this,
50
80
  });
51
- this.datasetBackendCache.push(newStore);
81
+ this.#datasetBackendCache.push(newStore);
52
82
  return newStore;
53
83
  }
54
84
  async createKeyValueStoreBackend(options = {}) {
55
85
  const { isAlias, cacheKey } = MemoryStorageBackend.#resolveStorageKey(options);
56
- const found = this.keyValueStoreBackendCache.find((store) => store.id === cacheKey ||
86
+ const found = this.#keyValueStoreBackendCache.find((store) => store.id === cacheKey ||
57
87
  store.name?.toLowerCase() === cacheKey.toLowerCase() ||
58
88
  store.cacheKey.toLowerCase() === cacheKey.toLowerCase());
59
89
  if (found) {
@@ -64,12 +94,12 @@ export class MemoryStorageBackend {
64
94
  cacheKey,
65
95
  storageBackend: this,
66
96
  });
67
- this.keyValueStoreBackendCache.push(newStore);
97
+ this.#keyValueStoreBackendCache.push(newStore);
68
98
  return newStore;
69
99
  }
70
100
  async createRequestQueueBackend(options = {}) {
71
101
  const { isAlias, cacheKey } = MemoryStorageBackend.#resolveStorageKey(options);
72
- const found = this.requestQueueBackendCache.find((queue) => queue.id === cacheKey ||
102
+ const found = this.#requestQueueBackendCache.find((queue) => queue.id === cacheKey ||
73
103
  queue.name?.toLowerCase() === cacheKey.toLowerCase() ||
74
104
  queue.cacheKey.toLowerCase() === cacheKey.toLowerCase());
75
105
  if (found) {
@@ -80,20 +110,20 @@ export class MemoryStorageBackend {
80
110
  cacheKey,
81
111
  storageBackend: this,
82
112
  });
83
- this.requestQueueBackendCache.push(newStore);
113
+ this.#requestQueueBackendCache.push(newStore);
84
114
  return newStore;
85
115
  }
86
116
  async storageExists(id, type) {
87
117
  let backends;
88
118
  switch (type) {
89
119
  case 'Dataset':
90
- backends = this.datasetBackendCache;
120
+ backends = this.#datasetBackendCache;
91
121
  break;
92
122
  case 'KeyValueStore':
93
- backends = this.keyValueStoreBackendCache;
123
+ backends = this.#keyValueStoreBackendCache;
94
124
  break;
95
125
  case 'RequestQueue':
96
- backends = this.requestQueueBackendCache;
126
+ backends = this.#requestQueueBackendCache;
97
127
  break;
98
128
  default:
99
129
  return false;
@@ -109,15 +139,13 @@ export class MemoryStorageBackend {
109
139
  // `#resolveStorageKey` leaves `name` unset for the default and alias-keyed storages, which is what
110
140
  // marks them as run-scoped. `'default'` is the exception — it collapses onto the default storage.
111
141
  const isRunScoped = (store) => store.name === undefined || store.name === 'default';
112
- const isDefault = (store) => store.name === 'default' || store.cacheKey === 'default';
113
142
  const purgeRunScoped = async (cache, purgeStore) => {
114
143
  await Promise.all(cache.filter(isRunScoped).map(async (store) => purgeStore(store)));
115
144
  };
116
145
  await Promise.all([
117
- // Only the default store holds the run input, so it is the only one that keeps `INPUT`.
118
- purgeRunScoped(this.keyValueStoreBackendCache, async (store) => isDefault(store) ? store.purgeExceptInput() : store.purge()),
119
- purgeRunScoped(this.datasetBackendCache, async (store) => store.purge()),
120
- purgeRunScoped(this.requestQueueBackendCache, async (store) => store.purge()),
146
+ purgeRunScoped(this.#keyValueStoreBackendCache, async (store) => store.purge()),
147
+ purgeRunScoped(this.#datasetBackendCache, async (store) => store.purge()),
148
+ purgeRunScoped(this.#requestQueueBackendCache, async (store) => store.purge()),
121
149
  ]);
122
150
  }
123
151
  /**
@@ -25,16 +25,11 @@ export declare class DatasetBackend<Data extends Dictionary = Dictionary> extend
25
25
  accessedAt: Date;
26
26
  modifiedAt: Date;
27
27
  itemCount: number;
28
- private readonly storageBackend;
29
28
  constructor(options: DatasetBackendOptions);
30
29
  getMetadata(): Promise<storage.DatasetInfo>;
31
30
  drop(): Promise<void>;
32
31
  purge(): Promise<void>;
33
32
  getData(options?: storage.DatasetBackendListOptions): Promise<storage.PaginatedList<Data>>;
34
- private getDataPage;
35
33
  pushData(items: Data[]): Promise<void>;
36
34
  toDatasetInfo(): storage.DatasetInfo;
37
- private generateLocalEntryName;
38
- private getStartAndEndIndexes;
39
- private updateTimestamps;
40
35
  }
@@ -23,48 +23,45 @@ export class DatasetBackend extends BaseClient {
23
23
  modifiedAt = new Date();
24
24
  itemCount = 0;
25
25
  #datasetEntries = new Map();
26
- // kept as TS-private: storage-backend tests read this field at runtime
27
- storageBackend;
26
+ #storageBackend;
28
27
  constructor(options) {
29
28
  super(options.id ?? randomUUID());
30
29
  this.name = options.name;
31
30
  this.cacheKey = options.cacheKey ?? this.name ?? this.id;
32
- this.storageBackend = options.storageBackend;
31
+ this.#storageBackend = options.storageBackend;
33
32
  }
34
33
  async getMetadata() {
35
- this.updateTimestamps(false);
34
+ this.#updateTimestamps(false);
36
35
  return this.toDatasetInfo();
37
36
  }
38
37
  async drop() {
39
- const storeIndex = this.storageBackend.datasetBackendCache.findIndex((store) => store.id === this.id);
40
- if (storeIndex !== -1) {
41
- const [oldBackend] = this.storageBackend.datasetBackendCache.splice(storeIndex, 1);
42
- oldBackend.itemCount = 0;
43
- oldBackend.#datasetEntries.clear();
38
+ if (this.#storageBackend.evictBackend('Dataset', this.id)) {
39
+ this.itemCount = 0;
40
+ this.#datasetEntries.clear();
44
41
  }
45
42
  }
46
43
  async purge() {
47
44
  this.itemCount = 0;
48
45
  this.#datasetEntries.clear();
49
- this.updateTimestamps(true);
46
+ this.#updateTimestamps(true);
50
47
  }
51
48
  getData(options = {}) {
52
49
  const { desc, limit, offset } = parseArgument(options, schemas.datasetListItemsOptions);
53
- return this.getDataPage({
50
+ return this.#getDataPage({
54
51
  desc,
55
52
  offset: offset ?? 0,
56
53
  limit: Math.min(limit ?? LIST_ITEMS_LIMIT, LIST_ITEMS_LIMIT),
57
54
  });
58
55
  }
59
- async getDataPage(options = {}) {
56
+ async #getDataPage(options = {}) {
60
57
  const { limit = LIST_ITEMS_LIMIT, offset = 0, desc } = options;
61
- const [start, end] = this.getStartAndEndIndexes(desc ? Math.max(this.itemCount - offset - limit, 0) : offset, limit);
58
+ const [start, end] = this.#getStartAndEndIndexes(desc ? Math.max(this.itemCount - offset - limit, 0) : offset, limit);
62
59
  const items = [];
63
60
  for (let idx = start; idx < end; idx++) {
64
- const entryNumber = this.generateLocalEntryName(idx);
61
+ const entryNumber = this.#generateLocalEntryName(idx);
65
62
  items.push(this.#datasetEntries.get(entryNumber));
66
63
  }
67
- this.updateTimestamps(false);
64
+ this.#updateTimestamps(false);
68
65
  return {
69
66
  count: items.length,
70
67
  desc: desc ?? false,
@@ -76,10 +73,10 @@ export class DatasetBackend extends BaseClient {
76
73
  }
77
74
  async pushData(items) {
78
75
  for (const entry of items) {
79
- const idx = this.generateLocalEntryName(++this.itemCount);
76
+ const idx = this.#generateLocalEntryName(++this.itemCount);
80
77
  this.#datasetEntries.set(idx, JSON.parse(JSON.stringify(entry)));
81
78
  }
82
- this.updateTimestamps(true);
79
+ this.#updateTimestamps(true);
83
80
  }
84
81
  toDatasetInfo() {
85
82
  return {
@@ -91,15 +88,15 @@ export class DatasetBackend extends BaseClient {
91
88
  name: this.name,
92
89
  };
93
90
  }
94
- generateLocalEntryName(idx) {
91
+ #generateLocalEntryName(idx) {
95
92
  return idx.toString().padStart(LOCAL_ENTRY_NAME_DIGITS, '0');
96
93
  }
97
- getStartAndEndIndexes(offset, limit = this.itemCount) {
94
+ #getStartAndEndIndexes(offset, limit = this.itemCount) {
98
95
  const start = offset + 1;
99
96
  const end = Math.min(offset + limit, this.itemCount) + 1;
100
97
  return [start, end];
101
98
  }
102
- updateTimestamps(hasBeenModified) {
99
+ #updateTimestamps(hasBeenModified) {
103
100
  this.accessedAt = new Date();
104
101
  if (hasBeenModified) {
105
102
  this.modifiedAt = new Date();
@@ -29,18 +29,10 @@ export declare class KeyValueStoreBackend extends BaseClient implements storage.
29
29
  createdAt: Date;
30
30
  accessedAt: Date;
31
31
  modifiedAt: Date;
32
- private readonly storageBackend;
33
32
  constructor(options: KeyValueStoreBackendOptions);
34
33
  getMetadata(): Promise<storage.KeyValueStoreInfo>;
35
34
  drop(): Promise<void>;
36
35
  purge(): Promise<void>;
37
- /**
38
- * Purges every record except the run's input. Used by {@link MemoryStorageBackend.purge} for the
39
- * default key-value store, mirroring `FileSystemStorageBackend`, which preserves `INPUT` (and its
40
- * extension variants) when purging the default store. The in-memory key has no extension, so we
41
- * preserve the bare `INPUT` key only.
42
- */
43
- purgeExceptInput(): Promise<void>;
44
36
  listKeys(options?: storage.KeyValueStoreListKeysOptions): Promise<storage.KeyValueStoreListKeysResult>;
45
37
  /**
46
38
  * In-memory records are not file-backed, so there is no public file URL to return.
@@ -59,5 +51,4 @@ export declare class KeyValueStoreBackend extends BaseClient implements storage.
59
51
  setValue(record: storage.KeyValueStoreInputRecord): Promise<void>;
60
52
  deleteValue(key: string): Promise<void>;
61
53
  toKeyValueStoreInfo(): storage.KeyValueStoreInfo;
62
- private updateTimestamps;
63
54
  }
@@ -20,11 +20,6 @@ const inputRecordSchema = z.object({
20
20
  ]),
21
21
  contentType: z.string().min(1).optional(),
22
22
  });
23
- /**
24
- * Key under which a run's input is stored in the default key-value store. Matches Crawlee's default
25
- * `inputKey` (`CRAWLEE_INPUT_KEY`) and the `INPUT` files `FileSystemStorageBackend` preserves on purge.
26
- */
27
- const KEY_VALUE_STORE_INPUT_KEY = 'INPUT';
28
23
  export class KeyValueStoreBackend extends BaseClient {
29
24
  name;
30
25
  /**
@@ -36,42 +31,25 @@ export class KeyValueStoreBackend extends BaseClient {
36
31
  accessedAt = new Date();
37
32
  modifiedAt = new Date();
38
33
  #keyValueEntries = new Map();
39
- // kept as TS-private: storage-backend tests read this field at runtime
40
- storageBackend;
34
+ #storageBackend;
41
35
  constructor(options) {
42
36
  super(options.id ?? randomUUID());
43
37
  this.name = options.name;
44
38
  this.cacheKey = options.cacheKey ?? this.name ?? this.id;
45
- this.storageBackend = options.storageBackend;
39
+ this.#storageBackend = options.storageBackend;
46
40
  }
47
41
  async getMetadata() {
48
- this.updateTimestamps(false);
42
+ this.#updateTimestamps(false);
49
43
  return this.toKeyValueStoreInfo();
50
44
  }
51
45
  async drop() {
52
- const storeIndex = this.storageBackend.keyValueStoreBackendCache.findIndex((store) => store.id === this.id);
53
- if (storeIndex !== -1) {
54
- const [oldBackend] = this.storageBackend.keyValueStoreBackendCache.splice(storeIndex, 1);
55
- oldBackend.#keyValueEntries.clear();
46
+ if (this.#storageBackend.evictBackend('KeyValueStore', this.id)) {
47
+ this.#keyValueEntries.clear();
56
48
  }
57
49
  }
58
50
  async purge() {
59
51
  this.#keyValueEntries.clear();
60
- this.updateTimestamps(true);
61
- }
62
- /**
63
- * Purges every record except the run's input. Used by {@link MemoryStorageBackend.purge} for the
64
- * default key-value store, mirroring `FileSystemStorageBackend`, which preserves `INPUT` (and its
65
- * extension variants) when purging the default store. The in-memory key has no extension, so we
66
- * preserve the bare `INPUT` key only.
67
- */
68
- async purgeExceptInput() {
69
- for (const key of this.#keyValueEntries.keys()) {
70
- if (key !== KEY_VALUE_STORE_INPUT_KEY) {
71
- this.#keyValueEntries.delete(key);
72
- }
73
- }
74
- this.updateTimestamps(true);
52
+ this.#updateTimestamps(true);
75
53
  }
76
54
  async listKeys(options = {}) {
77
55
  const { prefix, exclusiveStartKey, limit } = parseArgument(options, schemas.keyValueStoreListKeysOptions);
@@ -98,7 +76,7 @@ export class KeyValueStoreBackend extends BaseClient {
98
76
  const isTruncated = limit !== undefined && filteredItems.length > limit;
99
77
  const pageItems = isTruncated ? filteredItems.slice(0, limit) : filteredItems;
100
78
  const nextExclusiveStartKey = isTruncated ? pageItems[pageItems.length - 1].key : undefined;
101
- this.updateTimestamps(false);
79
+ this.#updateTimestamps(false);
102
80
  return {
103
81
  items: pageItems,
104
82
  count: pageItems.length,
@@ -142,7 +120,7 @@ export class KeyValueStoreBackend extends BaseClient {
142
120
  // frontend treats it as "no content type" rather than a bogus value.
143
121
  contentType: entry.contentType ?? (mime.contentType(entry.extension) || undefined),
144
122
  };
145
- this.updateTimestamps(false);
123
+ this.#updateTimestamps(false);
146
124
  return record;
147
125
  }
148
126
  async setValue(record) {
@@ -174,13 +152,13 @@ export class KeyValueStoreBackend extends BaseClient {
174
152
  contentType,
175
153
  };
176
154
  this.#keyValueEntries.set(key, normalizedRecord);
177
- this.updateTimestamps(true);
155
+ this.#updateTimestamps(true);
178
156
  }
179
157
  async deleteValue(key) {
180
158
  parseArgument(key, keySchema);
181
159
  if (this.#keyValueEntries.has(key)) {
182
160
  this.#keyValueEntries.delete(key);
183
- this.updateTimestamps(true);
161
+ this.#updateTimestamps(true);
184
162
  }
185
163
  }
186
164
  toKeyValueStoreInfo() {
@@ -192,7 +170,7 @@ export class KeyValueStoreBackend extends BaseClient {
192
170
  modifiedAt: this.modifiedAt,
193
171
  };
194
172
  }
195
- updateTimestamps(hasBeenModified) {
173
+ #updateTimestamps(hasBeenModified) {
196
174
  this.accessedAt = new Date();
197
175
  if (hasBeenModified) {
198
176
  this.modifiedAt = new Date();
@@ -34,28 +34,10 @@ export declare class RequestQueueBackend extends BaseClient implements storage.R
34
34
  modifiedAt: Date;
35
35
  handledRequestCount: number;
36
36
  pendingRequestCount: number;
37
- private readonly storageBackend;
38
37
  constructor(options: RequestQueueBackendOptions);
39
38
  getMetadata(): Promise<storage.RequestQueueInfo>;
40
39
  drop(): Promise<void>;
41
40
  purge(): Promise<void>;
42
- private requestKeyIterator;
43
- /**
44
- * Scans the queue and returns the pending head — requests that are neither handled nor currently
45
- * in progress — ordered by `orderNo`, deduplicated.
46
- *
47
- * When `detectInProgressRequests` is set, the result also carries an `hasInProgressRequests` flag
48
- * telling whether any unhandled-but-in-progress request was skipped along the way. It lets
49
- * {@link isFinished} distinguish "no work left at all" from "work remains, but it is currently being
50
- * processed". Without it, a consumer with concurrency could consider the queue finished and shut the
51
- * crawler down while it is still handling the last requests.
52
- *
53
- * Computing the flag is expensive: because an in-progress request may sit anywhere in the queue, it
54
- * forces a scan of every pending entry even when only `limit` items are wanted. Callers that only
55
- * need the head (e.g. {@link fetchNextRequest}, {@link isEmpty}) leave it off so the scan can stop as
56
- * soon as the page is filled, keeping those calls O(head) instead of O(N).
57
- */
58
- private listPendingHead;
59
41
  fetchNextRequest(): Promise<storage.UpdateRequestSchema | undefined>;
60
42
  addBatchOfRequests(requests: storage.RequestSchema[], options?: storage.RequestQueueOperationOptions): Promise<storage.BatchAddRequestsResult>;
61
43
  getRequest(uniqueKey: string): Promise<storage.UpdateRequestSchema | undefined>;
@@ -70,8 +52,4 @@ export declare class RequestQueueBackend extends BaseClient implements storage.R
70
52
  */
71
53
  listItems(): Promise<storage.UpdateRequestSchema[]>;
72
54
  toRequestQueueInfo(): storage.RequestQueueInfo;
73
- private updateTimestamps;
74
- private jsonToRequest;
75
- private createInternalRequest;
76
- private calculateOrderNo;
77
55
  }