@crawlee/core 4.0.0-beta.13 → 4.0.0-beta.131

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 (280) hide show
  1. package/README.md +17 -13
  2. package/autoscaling/autoscaled_pool.d.ts +67 -172
  3. package/autoscaling/autoscaled_pool.js +182 -329
  4. package/autoscaling/concurrency_system.d.ts +268 -0
  5. package/autoscaling/concurrency_system.js +362 -0
  6. package/autoscaling/cpu_load_signal.d.ts +43 -0
  7. package/autoscaling/cpu_load_signal.js +47 -0
  8. package/autoscaling/event_loop_load_signal.d.ts +51 -0
  9. package/autoscaling/event_loop_load_signal.js +60 -0
  10. package/autoscaling/index.d.ts +6 -1
  11. package/autoscaling/index.js +6 -1
  12. package/autoscaling/load_signal.d.ts +100 -0
  13. package/autoscaling/load_signal.js +105 -0
  14. package/autoscaling/memory_load_signal.d.ts +47 -0
  15. package/autoscaling/memory_load_signal.js +106 -0
  16. package/autoscaling/snapshotter.d.ts +58 -163
  17. package/autoscaling/snapshotter.js +45 -263
  18. package/autoscaling/storage_backend_load_signal.d.ts +56 -0
  19. package/autoscaling/storage_backend_load_signal.js +73 -0
  20. package/autoscaling/system_status.d.ts +67 -89
  21. package/autoscaling/system_status.js +92 -122
  22. package/autoscaling/weighted_avg.d.ts +5 -0
  23. package/autoscaling/weighted_avg.js +14 -0
  24. package/byte_utils.d.ts +17 -0
  25. package/byte_utils.js +42 -0
  26. package/configuration.d.ts +96 -223
  27. package/configuration.js +170 -222
  28. package/cookie_utils.d.ts +3 -2
  29. package/cookie_utils.js +18 -7
  30. package/crawlers/context_pipeline.d.ts +10 -1
  31. package/crawlers/context_pipeline.js +31 -8
  32. package/crawlers/crawler_commons.d.ts +90 -129
  33. package/crawlers/crawler_commons.js +1 -116
  34. package/crawlers/error_snapshotter.d.ts +2 -5
  35. package/crawlers/error_snapshotter.js +7 -8
  36. package/crawlers/error_tracker.d.ts +0 -1
  37. package/crawlers/error_tracker.js +0 -1
  38. package/crawlers/index.d.ts +1 -3
  39. package/crawlers/index.js +0 -3
  40. package/crawlers/internals/types.d.ts +0 -1
  41. package/crawlers/internals/types.js +0 -1
  42. package/crawlers/statistics.d.ts +187 -63
  43. package/crawlers/statistics.js +354 -164
  44. package/debug.d.ts +36 -0
  45. package/debug.js +70 -0
  46. package/enqueue_links/enqueue_links.d.ts +68 -154
  47. package/enqueue_links/enqueue_links.js +38 -210
  48. package/enqueue_links/index.d.ts +0 -1
  49. package/enqueue_links/index.js +0 -1
  50. package/enqueue_links/shared.d.ts +52 -26
  51. package/enqueue_links/shared.js +115 -66
  52. package/errors.d.ts +72 -4
  53. package/errors.js +89 -5
  54. package/events/event_manager.d.ts +35 -9
  55. package/events/event_manager.js +12 -13
  56. package/events/index.d.ts +0 -1
  57. package/events/index.js +0 -1
  58. package/events/local_event_manager.d.ts +15 -3
  59. package/events/local_event_manager.js +39 -13
  60. package/http.d.ts +9 -0
  61. package/http.js +28 -0
  62. package/index.d.ts +7 -4
  63. package/index.js +6 -3
  64. package/iterables.d.ts +79 -0
  65. package/iterables.js +134 -0
  66. package/log.d.ts +82 -3
  67. package/log.js +106 -1
  68. package/memory-storage/consts.d.ts +4 -0
  69. package/memory-storage/consts.js +4 -0
  70. package/memory-storage/index.d.ts +1 -0
  71. package/memory-storage/index.js +1 -0
  72. package/memory-storage/memory-storage.d.ts +38 -0
  73. package/memory-storage/memory-storage.js +130 -0
  74. package/memory-storage/resource-clients/common/base-client.d.ts +4 -0
  75. package/memory-storage/resource-clients/common/base-client.js +6 -0
  76. package/memory-storage/resource-clients/dataset.d.ts +40 -0
  77. package/memory-storage/resource-clients/dataset.js +108 -0
  78. package/memory-storage/resource-clients/key-value-store.d.ts +63 -0
  79. package/memory-storage/resource-clients/key-value-store.js +201 -0
  80. package/memory-storage/resource-clients/request-queue.d.ts +77 -0
  81. package/memory-storage/resource-clients/request-queue.js +409 -0
  82. package/memory-storage/utils.d.ts +16 -0
  83. package/memory-storage/utils.js +41 -0
  84. package/owned_or_injected.d.ts +58 -0
  85. package/owned_or_injected.js +98 -0
  86. package/package.json +14 -13
  87. package/proxy_configuration.d.ts +24 -132
  88. package/proxy_configuration.js +32 -147
  89. package/recoverable_state.d.ts +155 -0
  90. package/recoverable_state.js +231 -0
  91. package/request.d.ts +80 -16
  92. package/request.js +147 -68
  93. package/router.d.ts +193 -21
  94. package/router.js +188 -43
  95. package/serialization.d.ts +0 -1
  96. package/serialization.js +15 -15
  97. package/service_locator.d.ts +165 -0
  98. package/service_locator.js +253 -0
  99. package/session_pool/consts.d.ts +1 -2
  100. package/session_pool/consts.js +1 -2
  101. package/session_pool/errors.d.ts +0 -1
  102. package/session_pool/errors.js +0 -1
  103. package/session_pool/fingerprint.d.ts +9 -0
  104. package/session_pool/fingerprint.js +30 -0
  105. package/session_pool/index.d.ts +0 -2
  106. package/session_pool/index.js +0 -2
  107. package/session_pool/session.d.ts +35 -88
  108. package/session_pool/session.js +101 -159
  109. package/session_pool/session_pool.d.ts +73 -91
  110. package/session_pool/session_pool.js +172 -165
  111. package/storages/batched_adds.d.ts +37 -0
  112. package/storages/batched_adds.js +73 -0
  113. package/storages/dataset.d.ts +114 -54
  114. package/storages/dataset.js +291 -148
  115. package/storages/index.d.ts +10 -8
  116. package/storages/index.js +8 -8
  117. package/storages/key_value_store.d.ts +185 -42
  118. package/storages/key_value_store.js +445 -169
  119. package/storages/key_value_store_codec.d.ts +32 -0
  120. package/storages/key_value_store_codec.js +113 -0
  121. package/storages/request_dedup_cache.d.ts +22 -0
  122. package/storages/request_dedup_cache.js +48 -0
  123. package/storages/request_list.d.ts +52 -115
  124. package/storages/request_list.js +183 -152
  125. package/storages/request_loader.d.ts +101 -0
  126. package/storages/request_loader.js +1 -0
  127. package/storages/request_manager.d.ts +33 -0
  128. package/storages/request_manager.js +1 -0
  129. package/storages/request_manager_tandem.d.ts +97 -0
  130. package/storages/request_manager_tandem.js +197 -0
  131. package/storages/request_queue.d.ts +290 -47
  132. package/storages/request_queue.js +762 -216
  133. package/storages/{sitemap_request_list.d.ts → sitemap_request_loader.d.ts} +45 -89
  134. package/storages/sitemap_request_loader.js +438 -0
  135. package/storages/storage_instance_manager.d.ts +87 -0
  136. package/storages/storage_instance_manager.js +256 -0
  137. package/storages/storage_stats.d.ts +48 -0
  138. package/storages/storage_stats.js +29 -0
  139. package/storages/throttling_request_manager.d.ts +239 -0
  140. package/storages/throttling_request_manager.js +646 -0
  141. package/storages/transaction.d.ts +252 -0
  142. package/storages/transaction.js +251 -0
  143. package/storages/utils.d.ts +59 -11
  144. package/storages/utils.js +75 -15
  145. package/system-info/cpu-info.d.ts +67 -0
  146. package/system-info/cpu-info.js +216 -0
  147. package/system-info/memory-info.d.ts +31 -0
  148. package/system-info/memory-info.js +115 -0
  149. package/system-info/ps-tree.d.ts +17 -0
  150. package/system-info/ps-tree.js +144 -0
  151. package/system-info/runtime.d.ts +14 -0
  152. package/system-info/runtime.js +80 -0
  153. package/typedefs.d.ts +0 -6
  154. package/typedefs.js +0 -1
  155. package/url.d.ts +9 -0
  156. package/url.js +11 -0
  157. package/validators.d.ts +22 -18
  158. package/validators.js +13 -18
  159. package/autoscaling/autoscaled_pool.d.ts.map +0 -1
  160. package/autoscaling/autoscaled_pool.js.map +0 -1
  161. package/autoscaling/index.d.ts.map +0 -1
  162. package/autoscaling/index.js.map +0 -1
  163. package/autoscaling/snapshotter.d.ts.map +0 -1
  164. package/autoscaling/snapshotter.js.map +0 -1
  165. package/autoscaling/system_status.d.ts.map +0 -1
  166. package/autoscaling/system_status.js.map +0 -1
  167. package/configuration.d.ts.map +0 -1
  168. package/configuration.js.map +0 -1
  169. package/cookie_utils.d.ts.map +0 -1
  170. package/cookie_utils.js.map +0 -1
  171. package/crawlers/context_pipeline.d.ts.map +0 -1
  172. package/crawlers/context_pipeline.js.map +0 -1
  173. package/crawlers/crawler_commons.d.ts.map +0 -1
  174. package/crawlers/crawler_commons.js.map +0 -1
  175. package/crawlers/crawler_utils.d.ts +0 -10
  176. package/crawlers/crawler_utils.d.ts.map +0 -1
  177. package/crawlers/crawler_utils.js +0 -12
  178. package/crawlers/crawler_utils.js.map +0 -1
  179. package/crawlers/error_snapshotter.d.ts.map +0 -1
  180. package/crawlers/error_snapshotter.js.map +0 -1
  181. package/crawlers/error_tracker.d.ts.map +0 -1
  182. package/crawlers/error_tracker.js.map +0 -1
  183. package/crawlers/index.d.ts.map +0 -1
  184. package/crawlers/index.js.map +0 -1
  185. package/crawlers/internals/types.d.ts.map +0 -1
  186. package/crawlers/internals/types.js.map +0 -1
  187. package/crawlers/statistics.d.ts.map +0 -1
  188. package/crawlers/statistics.js.map +0 -1
  189. package/enqueue_links/enqueue_links.d.ts.map +0 -1
  190. package/enqueue_links/enqueue_links.js.map +0 -1
  191. package/enqueue_links/index.d.ts.map +0 -1
  192. package/enqueue_links/index.js.map +0 -1
  193. package/enqueue_links/shared.d.ts.map +0 -1
  194. package/enqueue_links/shared.js.map +0 -1
  195. package/errors.d.ts.map +0 -1
  196. package/errors.js.map +0 -1
  197. package/events/event_manager.d.ts.map +0 -1
  198. package/events/event_manager.js.map +0 -1
  199. package/events/index.d.ts.map +0 -1
  200. package/events/index.js.map +0 -1
  201. package/events/local_event_manager.d.ts.map +0 -1
  202. package/events/local_event_manager.js.map +0 -1
  203. package/http_clients/base-http-client.d.ts +0 -140
  204. package/http_clients/base-http-client.d.ts.map +0 -1
  205. package/http_clients/base-http-client.js +0 -40
  206. package/http_clients/base-http-client.js.map +0 -1
  207. package/http_clients/form-data-like.d.ts +0 -67
  208. package/http_clients/form-data-like.d.ts.map +0 -1
  209. package/http_clients/form-data-like.js +0 -5
  210. package/http_clients/form-data-like.js.map +0 -1
  211. package/http_clients/got-scraping-http-client.d.ts +0 -20
  212. package/http_clients/got-scraping-http-client.d.ts.map +0 -1
  213. package/http_clients/got-scraping-http-client.js +0 -82
  214. package/http_clients/got-scraping-http-client.js.map +0 -1
  215. package/http_clients/index.d.ts +0 -3
  216. package/http_clients/index.d.ts.map +0 -1
  217. package/http_clients/index.js +0 -3
  218. package/http_clients/index.js.map +0 -1
  219. package/index.d.ts.map +0 -1
  220. package/index.js.map +0 -1
  221. package/log.d.ts.map +0 -1
  222. package/log.js.map +0 -1
  223. package/proxy_configuration.d.ts.map +0 -1
  224. package/proxy_configuration.js.map +0 -1
  225. package/request.d.ts.map +0 -1
  226. package/request.js.map +0 -1
  227. package/router.d.ts.map +0 -1
  228. package/router.js.map +0 -1
  229. package/serialization.d.ts.map +0 -1
  230. package/serialization.js.map +0 -1
  231. package/session_pool/consts.d.ts.map +0 -1
  232. package/session_pool/consts.js.map +0 -1
  233. package/session_pool/errors.d.ts.map +0 -1
  234. package/session_pool/errors.js.map +0 -1
  235. package/session_pool/events.d.ts +0 -3
  236. package/session_pool/events.d.ts.map +0 -1
  237. package/session_pool/events.js +0 -3
  238. package/session_pool/events.js.map +0 -1
  239. package/session_pool/index.d.ts.map +0 -1
  240. package/session_pool/index.js.map +0 -1
  241. package/session_pool/session.d.ts.map +0 -1
  242. package/session_pool/session.js.map +0 -1
  243. package/session_pool/session_pool.d.ts.map +0 -1
  244. package/session_pool/session_pool.js.map +0 -1
  245. package/storages/access_checking.d.ts +0 -13
  246. package/storages/access_checking.d.ts.map +0 -1
  247. package/storages/access_checking.js +0 -14
  248. package/storages/access_checking.js.map +0 -1
  249. package/storages/dataset.d.ts.map +0 -1
  250. package/storages/dataset.js.map +0 -1
  251. package/storages/index.d.ts.map +0 -1
  252. package/storages/index.js.map +0 -1
  253. package/storages/key_value_store.d.ts.map +0 -1
  254. package/storages/key_value_store.js.map +0 -1
  255. package/storages/request_list.d.ts.map +0 -1
  256. package/storages/request_list.js.map +0 -1
  257. package/storages/request_provider.d.ts +0 -308
  258. package/storages/request_provider.d.ts.map +0 -1
  259. package/storages/request_provider.js +0 -555
  260. package/storages/request_provider.js.map +0 -1
  261. package/storages/request_queue.d.ts.map +0 -1
  262. package/storages/request_queue.js.map +0 -1
  263. package/storages/request_queue_v2.d.ts +0 -87
  264. package/storages/request_queue_v2.d.ts.map +0 -1
  265. package/storages/request_queue_v2.js +0 -438
  266. package/storages/request_queue_v2.js.map +0 -1
  267. package/storages/sitemap_request_list.d.ts.map +0 -1
  268. package/storages/sitemap_request_list.js +0 -430
  269. package/storages/sitemap_request_list.js.map +0 -1
  270. package/storages/storage_manager.d.ts +0 -58
  271. package/storages/storage_manager.d.ts.map +0 -1
  272. package/storages/storage_manager.js +0 -105
  273. package/storages/storage_manager.js.map +0 -1
  274. package/storages/utils.d.ts.map +0 -1
  275. package/storages/utils.js.map +0 -1
  276. package/tsconfig.build.tsbuildinfo +0 -1
  277. package/typedefs.d.ts.map +0 -1
  278. package/typedefs.js.map +0 -1
  279. package/validators.d.ts.map +0 -1
  280. package/validators.js.map +0 -1
@@ -1,31 +1,37 @@
1
- import { EventEmitter } from 'node:events';
2
1
  import { AsyncQueue } from '@sapphire/async-queue';
3
- import ow from 'ow';
4
- import { Configuration } from '../configuration.js';
5
- import { log as defaultLog } from '../log.js';
2
+ import { z } from 'zod';
3
+ import { EventType } from '../events/event_manager.js';
4
+ import { serviceLocator } from '../service_locator.js';
6
5
  import { KeyValueStore } from '../storages/key_value_store.js';
7
- import { BLOCKED_STATUS_CODES, MAX_POOL_SIZE, PERSIST_STATE_KEY } from './consts.js';
6
+ import { parseArgument, schemas, validators } from '../validators.js';
7
+ import { MAX_POOL_SIZE, PERSIST_STATE_KEY } from './consts.js';
8
+ import { createDefaultSessionFingerprint } from './fingerprint.js';
8
9
  import { Session } from './session.js';
10
+ const SESSION_REUSE_STRATEGIES = ['random', 'round-robin', 'use-until-failure'];
11
+ // `schemas.anyObject` passes values through by reference (object schemas return a pruned plain
12
+ // copy), so class instances like loggers keep their prototype.
13
+ const sessionPoolOptionsSchema = z.strictObject({
14
+ id: z.union([schemas.anyNumber, z.string()]).optional(),
15
+ maxPoolSize: schemas.anyNumber.default(MAX_POOL_SIZE),
16
+ persistStateKeyValueStoreId: z.string().optional(),
17
+ persistStateKey: z.string().optional(),
18
+ createSessionFunction: schemas.anyFunction.optional(),
19
+ sessionOptions: schemas.anyObject.default(() => ({})),
20
+ log: validators.logger.default(() => serviceLocator.getLogger()),
21
+ persistenceOptions: schemas.anyObject.default(() => ({ enable: true })),
22
+ sessionReuseStrategy: z.enum(SESSION_REUSE_STRATEGIES).default('random'),
23
+ });
24
+ const createSessionOptionsSchema = z.strictObject({
25
+ sessionOptions: schemas.anyObject.default(() => ({})),
26
+ });
9
27
  /**
10
28
  * Handles the rotation, creation and persistence of user-like sessions.
11
29
  * Creates a pool of {@link Session} instances, that are randomly rotated.
12
30
  * When some session is marked as blocked, it is removed and new one is created instead (the pool never returns an unusable session).
13
31
  * Learn more in the {@doclink guides/session-management | Session management guide}.
14
32
  *
15
- * You can create one by calling the {@link SessionPool.open} function.
16
- *
17
- * Session pool is already integrated into crawlers, and it can significantly improve your scraper
18
- * performance with just 2 lines of code.
19
- *
20
- * **Example usage:**
21
- *
22
- * ```javascript
23
- * const crawler = new CheerioCrawler({
24
- * useSessionPool: true,
25
- * persistCookiesPerSession: true,
26
- * // ...
27
- * })
28
- * ```
33
+ * Session pool is already integrated into crawlers and is always active.
34
+ * All public methods are lazy-initialized — the pool initializes itself on first use.
29
35
  *
30
36
  * You can configure the pool with many options. See the {@link SessionPoolOptions}.
31
37
  * Session pool is by default persisted in default {@link KeyValueStore}.
@@ -35,7 +41,7 @@ import { Session } from './session.js';
35
41
  * **Advanced usage:**
36
42
  *
37
43
  * ```javascript
38
- * const sessionPool = await SessionPool.open({
44
+ * const sessionPool = new SessionPool({
39
45
  * maxPoolSize: 25,
40
46
  * sessionOptions:{
41
47
  * maxAgeSecs: 10,
@@ -71,94 +77,85 @@ import { Session } from './session.js';
71
77
  *
72
78
  * @category Scaling
73
79
  */
74
- export class SessionPool extends EventEmitter {
75
- config;
76
- log;
80
+ export class SessionPool {
81
+ static #nextId = 0;
82
+ id;
83
+ #log;
84
+ #sessions = [];
85
+ // kept as TS-private: session_pool tests read/override the members below directly
77
86
  maxPoolSize;
78
87
  createSessionFunction;
79
88
  keyValueStore;
80
- sessions = [];
81
89
  sessionMap = new Map();
82
90
  sessionOptions;
83
91
  persistStateKeyValueStoreId;
84
92
  persistStateKey;
85
- _listener;
86
- events;
87
- blockedStatusCodes;
88
- persistenceOptions;
89
- isInitialized = false;
90
- queue = new AsyncQueue();
91
- /**
92
- * @internal
93
- */
94
- constructor(options = {}, config = Configuration.getGlobalConfig()) {
95
- super();
96
- this.config = config;
97
- ow(options, ow.object.exactShape({
98
- maxPoolSize: ow.optional.number,
99
- persistStateKeyValueStoreId: ow.optional.string,
100
- persistStateKey: ow.optional.string,
101
- createSessionFunction: ow.optional.function,
102
- sessionOptions: ow.optional.object,
103
- blockedStatusCodes: ow.optional.array.ofType(ow.number),
104
- log: ow.optional.object,
105
- persistenceOptions: ow.optional.object,
106
- }));
107
- const { maxPoolSize = MAX_POOL_SIZE, persistStateKeyValueStoreId, persistStateKey = PERSIST_STATE_KEY, createSessionFunction, sessionOptions = {}, blockedStatusCodes = BLOCKED_STATUS_CODES, log = defaultLog, persistenceOptions = {
108
- enable: true,
109
- }, } = options;
110
- this.config = config;
111
- this.blockedStatusCodes = blockedStatusCodes;
112
- this.events = config.getEventManager();
113
- this.log = log.child({ prefix: 'SessionPool' });
114
- this.persistenceOptions = persistenceOptions;
93
+ #listener;
94
+ #events;
95
+ #persistenceOptions;
96
+ #sessionReuseStrategy;
97
+ #initPromise;
98
+ #queue = new AsyncQueue();
99
+ #roundRobinIndex = 0;
100
+ constructor(options = {}) {
101
+ const { id, maxPoolSize, persistStateKeyValueStoreId, persistStateKey, createSessionFunction, sessionOptions, log, persistenceOptions, sessionReuseStrategy, } = parseArgument(options, sessionPoolOptionsSchema);
102
+ this.id = id != null ? String(id) : String(SessionPool.#nextId++);
103
+ this.#sessionReuseStrategy = sessionReuseStrategy;
104
+ this.#events = serviceLocator.getEventManager();
105
+ this.#log = log.child({ prefix: 'SessionPool' });
106
+ this.#persistenceOptions = persistenceOptions;
115
107
  // Pool Configuration
116
108
  this.maxPoolSize = maxPoolSize;
117
- this.createSessionFunction = createSessionFunction || this._defaultCreateSessionFunction;
118
- // Session configuration
109
+ this.createSessionFunction = createSessionFunction || this.defaultCreateSessionFunction;
110
+ // Session configuration. The pool-scoped logger is merged into per-call sessionOptions inside
111
+ // `invokeCreateSessionFunction`, so every Session inherits it without custom createSessionFunctions
112
+ // having to know about it.
119
113
  this.sessionOptions = {
120
114
  ...sessionOptions,
121
- // the log needs to propagate to createSessionFunction as in "new Session({ ...sessionPool.sessionOptions })"
122
- // and can't go inside _defaultCreateSessionFunction
123
- log: this.log,
115
+ log: this.#log,
124
116
  };
125
117
  // Session keyValueStore
126
118
  this.persistStateKeyValueStoreId = persistStateKeyValueStoreId;
127
- this.persistStateKey = persistStateKey;
119
+ this.persistStateKey = persistStateKey ?? `${PERSIST_STATE_KEY}_${this.id}`;
128
120
  }
129
121
  /**
130
122
  * Gets count of usable sessions in the pool.
131
123
  */
132
- get usableSessionsCount() {
133
- return this.sessions.filter((session) => session.isUsable()).length;
124
+ async usableSessionsCount() {
125
+ await this.ensureInitialized();
126
+ return this.#sessions.filter((session) => session.isUsable()).length;
134
127
  }
135
128
  /**
136
129
  * Gets count of retired sessions in the pool.
137
130
  */
138
- get retiredSessionsCount() {
139
- return this.sessions.filter((session) => !session.isUsable()).length;
131
+ async retiredSessionsCount() {
132
+ await this.ensureInitialized();
133
+ return this.#sessions.filter((session) => !session.isUsable()).length;
140
134
  }
141
135
  /**
142
136
  * Starts periodic state persistence and potentially loads SessionPool state from {@link KeyValueStore}.
143
- * It is called automatically by the {@link SessionPool.open} function.
137
+ * Called automatically on first use of any public method.
144
138
  */
145
- async initialize() {
146
- if (this.isInitialized) {
147
- return;
139
+ async ensureInitialized() {
140
+ if (!this.#initPromise) {
141
+ this.#initPromise = this.setupPool();
148
142
  }
149
- this.keyValueStore = await KeyValueStore.open(this.persistStateKeyValueStoreId, { config: this.config });
150
- if (!this.persistenceOptions.enable) {
151
- this.isInitialized = true;
143
+ return this.#initPromise;
144
+ }
145
+ async setupPool() {
146
+ if (!this.#persistenceOptions.enable) {
152
147
  return;
153
148
  }
149
+ this.keyValueStore = await KeyValueStore.open(this.persistStateKeyValueStoreId ? { id: this.persistStateKeyValueStoreId } : null, {
150
+ configuration: serviceLocator.getConfiguration(),
151
+ });
154
152
  if (!this.persistStateKeyValueStoreId) {
155
- this.log.debug(`No 'persistStateKeyValueStoreId' options specified, this session pool's data has been saved in the KeyValueStore with the id: ${this.keyValueStore.id}`);
153
+ this.#log.debug(`No 'persistStateKeyValueStoreId' options specified, this session pool's data has been saved in the KeyValueStore with the id: ${this.keyValueStore.id}`);
156
154
  }
157
155
  // in case of migration happened and SessionPool state should be restored from the keyValueStore.
158
- await this._maybeLoadSessionPool();
159
- this._listener = this.persistState.bind(this);
160
- this.events.on("persistState" /* EventType.PERSIST_STATE */, this._listener);
161
- this.isInitialized = true;
156
+ await this.maybeLoadSessionPool();
157
+ this.#listener = this.persistState.bind(this);
158
+ this.#events.on(EventType.PERSIST_STATE, this.#listener);
162
159
  }
163
160
  /**
164
161
  * Adds a new session to the session pool. The pool automatically creates sessions up to the maximum size of the pool,
@@ -167,7 +164,7 @@ export class SessionPool extends EventEmitter {
167
164
  * @param [options] The configuration options for the session being added to the session pool.
168
165
  */
169
166
  async addSession(options = {}) {
170
- this._throwIfNotInitialized();
167
+ await this.ensureInitialized();
171
168
  const { id } = options;
172
169
  if (id) {
173
170
  const sessionExists = this.sessionMap.has(id);
@@ -175,12 +172,12 @@ export class SessionPool extends EventEmitter {
175
172
  throw new Error(`Cannot add session with id '${id}' as it already exists in the pool`);
176
173
  }
177
174
  }
178
- if (!this._hasSpaceForSession()) {
179
- this._removeRetiredSessions();
175
+ if (!this.hasSpaceForSession()) {
176
+ this.removeRetiredSessions();
180
177
  }
181
- const newSession = options instanceof Session ? options : await this.createSessionFunction(this, { sessionOptions: options });
182
- this.log.debug(`Adding new Session - ${newSession.id}`);
183
- this._addSession(newSession);
178
+ const newSession = options instanceof Session ? options : await this.invokeCreateSessionFunction(options);
179
+ this.#log.debug(`Adding new Session - ${newSession.id}`);
180
+ this.registerSession(newSession);
184
181
  }
185
182
  /**
186
183
  * Adds a new session to the session pool. The pool automatically creates sessions up to the maximum size of the pool,
@@ -189,9 +186,9 @@ export class SessionPool extends EventEmitter {
189
186
  * @param [options] The configuration options for the session being added to the session pool.
190
187
  */
191
188
  async newSession(sessionOptions) {
192
- this._throwIfNotInitialized();
193
- const newSession = await this.createSessionFunction(this, { sessionOptions });
194
- this._addSession(newSession);
189
+ await this.ensureInitialized();
190
+ const newSession = await this.invokeCreateSessionFunction(sessionOptions);
191
+ this.registerSession(newSession);
195
192
  return newSession;
196
193
  }
197
194
  /**
@@ -202,47 +199,48 @@ export class SessionPool extends EventEmitter {
202
199
  * @param [sessionId] If provided, it returns the usable session with this id, `undefined` otherwise.
203
200
  */
204
201
  async getSession(sessionId) {
205
- await this.queue.wait();
202
+ await this.ensureInitialized();
203
+ await this.#queue.wait();
206
204
  try {
207
- this._throwIfNotInitialized();
208
205
  if (sessionId) {
209
206
  const session = this.sessionMap.get(sessionId);
210
- if (session && session.isUsable())
207
+ if (session?.isUsable())
211
208
  return session;
212
209
  return undefined;
213
210
  }
214
- if (this._hasSpaceForSession()) {
215
- return await this._createSession();
216
- }
217
- const pickedSession = this._pickSession();
218
- if (pickedSession.isUsable()) {
211
+ const pickedSession = this.pickSession();
212
+ if (pickedSession)
219
213
  return pickedSession;
214
+ if (this.hasSpaceForSession()) {
215
+ return await this.createSession();
220
216
  }
221
- this._removeRetiredSessions();
222
- return await this._createSession();
217
+ this.removeRetiredSessions();
218
+ return await this.createSession();
223
219
  }
224
220
  finally {
225
- this.queue.shift();
221
+ this.#queue.shift();
226
222
  }
227
223
  }
228
224
  /**
229
225
  * @param options - Override the persistence options provided in the constructor
230
226
  */
231
227
  async resetStore(options) {
232
- if (!this.persistenceOptions.enable && !options?.enable) {
228
+ if (!this.#persistenceOptions.enable && !options?.enable) {
233
229
  return;
234
230
  }
231
+ await this.ensureInitialized();
235
232
  await this.keyValueStore?.setValue(this.persistStateKey, null);
236
233
  }
237
234
  /**
238
235
  * Returns an object representing the internal state of the `SessionPool` instance.
239
236
  * Note that the object's fields can change in future releases.
240
237
  */
241
- getState() {
238
+ async getState() {
239
+ await this.ensureInitialized();
242
240
  return {
243
- usableSessionsCount: this.usableSessionsCount,
244
- retiredSessionsCount: this.retiredSessionsCount,
245
- sessions: this.sessions.map((session) => session.getState()),
241
+ usableSessionsCount: await this.usableSessionsCount(),
242
+ retiredSessionsCount: await this.retiredSessionsCount(),
243
+ sessions: this.#sessions.map((session) => session.getState()),
246
244
  };
247
245
  }
248
246
  /**
@@ -251,47 +249,43 @@ export class SessionPool extends EventEmitter {
251
249
  * @param options - Override the persistence options provided in the constructor
252
250
  */
253
251
  async persistState(options) {
254
- if (!this.persistenceOptions.enable && !options?.enable) {
252
+ if (!this.#persistenceOptions.enable && !options?.enable) {
255
253
  return;
256
254
  }
257
- this.log.debug('Persisting state', {
255
+ await this.ensureInitialized();
256
+ this.#log.debug('Persisting state', {
258
257
  persistStateKeyValueStoreId: this.persistStateKeyValueStoreId,
259
258
  persistStateKey: this.persistStateKey,
260
259
  });
261
- // use half the interval of `persistState` to avoid race conditions
262
- const persistStateIntervalMillis = this.config.get('persistStateIntervalMillis');
263
- const timeoutSecs = persistStateIntervalMillis / 2_000;
264
260
  await this.keyValueStore
265
- .setValue(this.persistStateKey, this.getState(), {
266
- timeoutSecs,
267
- doNotRetryTimeouts: true,
268
- })
269
- .catch((error) => this.log.warning(`Failed to persist the session pool stats to ${this.persistStateKey}`, { error }));
261
+ ?.setValue(this.persistStateKey, await this.getState())
262
+ .catch((error) => this.#log.warning(`Failed to persist the session pool stats to ${this.persistStateKey}`, { error }));
270
263
  }
271
264
  /**
272
265
  * Removes listener from `persistState` event.
273
266
  * This function should be called after you are done with using the `SessionPool` instance.
267
+ * @param options - Set `persistState` to false when the final state was already persisted by the event manager.
274
268
  */
275
- async teardown() {
276
- this.events.off("persistState" /* EventType.PERSIST_STATE */, this._listener);
277
- await this.persistState();
278
- }
279
- /**
280
- * SessionPool should not work before initialization.
281
- */
282
- _throwIfNotInitialized() {
283
- if (!this.isInitialized)
284
- throw new Error('SessionPool is not initialized.');
269
+ async teardown({ persistState = true } = {}) {
270
+ if (!this.#initPromise)
271
+ return;
272
+ await this.ensureInitialized();
273
+ if (this.#listener) {
274
+ this.#events.off(EventType.PERSIST_STATE, this.#listener);
275
+ }
276
+ if (persistState) {
277
+ await this.persistState();
278
+ }
285
279
  }
286
280
  /**
287
281
  * Removes retired `Session` instances from `SessionPool`.
288
282
  */
289
- _removeRetiredSessions() {
290
- this.sessions = this.sessions.filter((storedSession) => {
283
+ removeRetiredSessions() {
284
+ this.#sessions = this.#sessions.filter((storedSession) => {
291
285
  if (storedSession.isUsable())
292
286
  return true;
293
287
  this.sessionMap.delete(storedSession.id);
294
- this.log.debug(`Removed Session - ${storedSession.id}`);
288
+ this.#log.debug(`Removed Session - ${storedSession.id}`);
295
289
  return false;
296
290
  });
297
291
  }
@@ -299,89 +293,102 @@ export class SessionPool extends EventEmitter {
299
293
  * Adds `Session` instance to `SessionPool`.
300
294
  * @param newSession `Session` instance to be added.
301
295
  */
302
- _addSession(newSession) {
303
- this.sessions.push(newSession);
296
+ registerSession(newSession) {
297
+ this.#sessions.push(newSession);
304
298
  this.sessionMap.set(newSession.id, newSession);
305
299
  }
306
300
  /**
307
301
  * Gets random index.
308
302
  */
309
- _getRandomIndex() {
310
- return Math.floor(Math.random() * this.sessions.length);
303
+ getRandomIndex() {
304
+ return Math.floor(Math.random() * this.#sessions.length);
311
305
  }
312
306
  /**
313
307
  * Creates new session without any extra behavior.
314
- * @param sessionPool
315
308
  * @param [options]
316
309
  * @param [options.sessionOptions] The configuration options for the session being created.
317
310
  * @returns New session.
318
311
  */
319
- async _defaultCreateSessionFunction(sessionPool, options = {}) {
320
- ow(options, ow.object.exactShape({ sessionOptions: ow.optional.object }));
321
- const { sessionOptions = {} } = options;
322
- return new Session({
312
+ async defaultCreateSessionFunction(options = {}) {
313
+ const { sessionOptions } = parseArgument(options, createSessionOptionsSchema);
314
+ return new Session(sessionOptions);
315
+ }
316
+ /**
317
+ * Invokes `createSessionFunction` with `sessionOptions` already merged from pool-wide defaults and
318
+ * the supplied per-call overrides, so custom implementations don't need to spread `pool.sessionOptions` themselves.
319
+ *
320
+ * A default {@link SessionFingerprint} is generated up front (host OS as
321
+ * `platform`, a random valid `browser`/`device` for that platform). Pool-wide
322
+ * and per-call options override it, and a persisted fingerprint coming
323
+ * through `maybeLoadSessionPool` naturally wins because it arrives in
324
+ * `perCallOptions`.
325
+ */
326
+ async invokeCreateSessionFunction(perCallOptions) {
327
+ const sessionOptions = {
328
+ fingerprint: createDefaultSessionFingerprint(),
323
329
  ...this.sessionOptions,
324
- ...sessionOptions,
325
- sessionPool,
326
- });
330
+ ...perCallOptions,
331
+ };
332
+ return this.createSessionFunction({ sessionOptions });
327
333
  }
328
334
  /**
329
335
  * Creates new session and adds it to the pool.
330
336
  * @returns Newly created `Session` instance.
331
337
  */
332
- async _createSession() {
333
- const newSession = await this.createSessionFunction(this);
334
- this._addSession(newSession);
335
- this.log.debug(`Created new Session - ${newSession.id}`);
338
+ async createSession() {
339
+ const newSession = await this.invokeCreateSessionFunction();
340
+ this.registerSession(newSession);
341
+ this.#log.debug(`Created new Session - ${newSession.id}`);
336
342
  return newSession;
337
343
  }
338
344
  /**
339
345
  * Decides whether there is enough space for creating new session.
340
346
  */
341
- _hasSpaceForSession() {
342
- return this.sessions.length < this.maxPoolSize;
347
+ hasSpaceForSession() {
348
+ return this.#sessions.length < this.maxPoolSize;
343
349
  }
344
350
  /**
345
- * Picks random session from the `SessionPool`.
346
- * @returns Picked `Session`.
351
+ * Picks a session from the `SessionPool` according to the configured `sessionReuseStrategy`.
352
+ * Returns `undefined` when no session should be reused and a new one should be created instead.
347
353
  */
348
- _pickSession() {
349
- return this.sessions[this._getRandomIndex()]; // Or maybe we should let the developer to customize the picking algorithm
354
+ pickSession() {
355
+ if (this.#sessionReuseStrategy !== 'use-until-failure' && this.hasSpaceForSession())
356
+ return undefined;
357
+ if (this.#sessionReuseStrategy === 'use-until-failure') {
358
+ return this.#sessions.find((session) => session.isUsable());
359
+ }
360
+ let picked;
361
+ if (this.#sessionReuseStrategy === 'round-robin') {
362
+ const index = this.#roundRobinIndex % this.#sessions.length;
363
+ this.#roundRobinIndex = index + 1;
364
+ picked = this.#sessions[index];
365
+ }
366
+ else {
367
+ picked = this.#sessions[this.getRandomIndex()];
368
+ }
369
+ return picked.isUsable() ? picked : undefined;
350
370
  }
351
371
  /**
352
372
  * Potentially loads `SessionPool`.
353
373
  * If the state was persisted it loads the `SessionPool` from the persisted state.
354
374
  */
355
- async _maybeLoadSessionPool() {
356
- const loadedSessionPool = await this.keyValueStore.getValue(this.persistStateKey);
375
+ async maybeLoadSessionPool() {
376
+ const loadedSessionPool = await this.keyValueStore?.getValue(this.persistStateKey);
357
377
  if (!loadedSessionPool)
358
378
  return;
359
379
  // Invalidate old sessions and load active sessions only
360
- this.log.debug('Recreating state from KeyValueStore', {
380
+ this.#log.debug('Recreating state from KeyValueStore', {
361
381
  persistStateKeyValueStoreId: this.persistStateKeyValueStoreId,
362
382
  persistStateKey: this.persistStateKey,
363
383
  });
364
384
  for (const sessionObject of loadedSessionPool.sessions) {
365
- sessionObject.sessionPool = this;
366
385
  sessionObject.createdAt = new Date(sessionObject.createdAt);
367
386
  sessionObject.expiresAt = new Date(sessionObject.expiresAt);
368
- const recreatedSession = await this.createSessionFunction(this, { sessionOptions: sessionObject });
387
+ const recreatedSession = await this.invokeCreateSessionFunction(sessionObject);
369
388
  if (recreatedSession.isUsable()) {
370
- this._addSession(recreatedSession);
389
+ this.registerSession(recreatedSession);
371
390
  }
372
391
  }
373
- this.log.debug(`${this.usableSessionsCount} active sessions loaded from KeyValueStore`);
374
- }
375
- /**
376
- * Opens a SessionPool and returns a promise resolving to an instance
377
- * of the {@link SessionPool} class that is already initialized.
378
- *
379
- * For more details and code examples, see the {@link SessionPool} class.
380
- */
381
- static async open(options, config) {
382
- const sessionPool = new SessionPool(options, config);
383
- await sessionPool.initialize();
384
- return sessionPool;
392
+ this.#log.debug(`${this.#sessions.length} active sessions loaded from KeyValueStore`);
385
393
  }
386
394
  }
387
- //# sourceMappingURL=session_pool.js.map
@@ -0,0 +1,37 @@
1
+ import type { ProcessedRequest } from '@crawlee/types';
2
+ import type { Source } from '../request.js';
3
+ import type { AddRequestsBatchedResult } from './request_queue.js';
4
+ export interface DrainRequestBatchesOptions<TItem extends Source> {
5
+ /**
6
+ * The requests to add, already normalized by the caller. Consumed lazily: an unbounded or expensive
7
+ * iterable is only pulled from as far as the batching (and any `maxNewRequests` budget) requires.
8
+ */
9
+ items: AsyncGenerator<TItem>;
10
+ batchSize: number;
11
+ waitBetweenBatchesMillis: number;
12
+ waitForAllRequestsToBeAdded: boolean;
13
+ maxNewRequests?: number;
14
+ /**
15
+ * Adds a single chunk and reports what it processed.
16
+ *
17
+ * @param isInitial Whether this is the first chunk, which is added before this function returns. Later
18
+ * chunks land in the background, which is why some callers cache only the first.
19
+ */
20
+ processChunk: (chunk: TItem[], isInitial: boolean) => Promise<ProcessedRequest[]>;
21
+ /**
22
+ * Called with the promise covering every chunk after the first, so the caller can keep its own
23
+ * `isFinished` honest while batches are still landing.
24
+ */
25
+ trackBackgroundBatches?: (batches: Promise<unknown>) => void;
26
+ }
27
+ /**
28
+ * Drives the chunk-by-chunk half of `addRequestsBatched`: the first chunk is added before returning and the
29
+ * rest continue in the background, paced by `waitBetweenBatchesMillis`.
30
+ *
31
+ * Callers differ only in how a chunk is added and how the input is normalized, so that is all
32
+ * {@link DrainRequestBatchesOptions} asks for - the budget arithmetic, the lazy chunking, the
33
+ * over-limit reporting and the transaction handling are identical for everyone and live here. In
34
+ * particular, every caller has to keep its background chunks out of a transaction they will outlive,
35
+ * so that is read from the ambient transaction rather than asked of the caller.
36
+ */
37
+ export declare function drainRequestBatches<TItem extends Source>(options: DrainRequestBatchesOptions<TItem>): Promise<AddRequestsBatchedResult>;
@@ -0,0 +1,73 @@
1
+ import { setTimeout as sleep } from 'node:timers/promises';
2
+ import { chunkedAsyncIterable, peekableAsyncIterable } from '../iterables.js';
3
+ import { activeStorageTransaction, withDirectStorageAccess } from './transaction.js';
4
+ /**
5
+ * Drives the chunk-by-chunk half of `addRequestsBatched`: the first chunk is added before returning and the
6
+ * rest continue in the background, paced by `waitBetweenBatchesMillis`.
7
+ *
8
+ * Callers differ only in how a chunk is added and how the input is normalized, so that is all
9
+ * {@link DrainRequestBatchesOptions} asks for - the budget arithmetic, the lazy chunking, the
10
+ * over-limit reporting and the transaction handling are identical for everyone and live here. In
11
+ * particular, every caller has to keep its background chunks out of a transaction they will outlive,
12
+ * so that is read from the ambient transaction rather than asked of the caller.
13
+ */
14
+ export async function drainRequestBatches(options) {
15
+ const { items, batchSize, waitBetweenBatchesMillis, waitForAllRequestsToBeAdded, maxNewRequests, processChunk, trackBackgroundBatches, } = options;
16
+ const deferred = activeStorageTransaction()?.policy.requestQueue === 'deferred';
17
+ let remainingBudget = maxNewRequests ?? Infinity;
18
+ const requestsOverLimit = [];
19
+ // Never hand a chunk more than the budget allows, so an over-large final batch cannot overshoot.
20
+ const effectiveChunkSize = maxNewRequests !== undefined ? () => Math.min(batchSize, remainingBudget) : batchSize;
21
+ const chunks = peekableAsyncIterable(chunkedAsyncIterable(items, effectiveChunkSize));
22
+ const chunksIterator = chunks[Symbol.asyncIterator]();
23
+ const addChunk = async (chunk, isInitial) => {
24
+ const processedRequests = await processChunk(chunk, isInitial);
25
+ if (maxNewRequests !== undefined) {
26
+ remainingBudget -= processedRequests.filter((request) => !request.wasAlreadyPresent).length;
27
+ }
28
+ return processedRequests;
29
+ };
30
+ const buildResult = async (addedRequests, waitForAll) => {
31
+ if (maxNewRequests !== undefined) {
32
+ // `chunkedAsyncIterable` stops pulling once the budget-derived chunk size hits zero, so whatever
33
+ // is left is still sitting in `items` rather than in a chunk we have seen.
34
+ for await (const item of items) {
35
+ requestsOverLimit.push(item);
36
+ }
37
+ }
38
+ return { addedRequests, waitForAllRequestsToBeAdded: waitForAll, requestsOverLimit };
39
+ };
40
+ const initialChunk = await chunksIterator.peek();
41
+ if (initialChunk === undefined) {
42
+ return buildResult([], Promise.resolve([]));
43
+ }
44
+ const addedRequests = await addChunk(initialChunk, true);
45
+ await chunksIterator.next();
46
+ if ((await chunksIterator.peek()) === undefined) {
47
+ return buildResult(addedRequests, Promise.resolve([]));
48
+ }
49
+ const processRemainingChunks = async () => {
50
+ const added = [];
51
+ for await (const chunk of chunks) {
52
+ added.push(...(await addChunk(chunk, false)));
53
+ // Under `deferred` no chunk performs backend I/O, so pacing them would only stall the handler.
54
+ await sleep(deferred ? 0 : waitBetweenBatchesMillis);
55
+ }
56
+ return added;
57
+ };
58
+ // With a budget we must drain everything before we can report what went over it; under `deferred` a
59
+ // writer that finishes after commit would have nowhere to put its journal entries.
60
+ const awaitsRemainder = waitForAllRequestsToBeAdded || maxNewRequests !== undefined || deferred;
61
+ // An un-awaited writer outlives the transaction scope it inherits, so it must not record into a
62
+ // transaction that may already be closed. It writes directly - its write-through additions were never
63
+ // going to be rolled back anyway - which means the requests it adds are not journaled.
64
+ // See `StorageTransactionView.enqueuedUrls`.
65
+ const remainder = awaitsRemainder ? processRemainingChunks() : withDirectStorageAccess(processRemainingChunks);
66
+ // The caller is not obliged to await `remainder`, so give it a handler of its own - an unhandled
67
+ // rejection here would otherwise take the process down.
68
+ trackBackgroundBatches?.(remainder.catch(() => { }));
69
+ if (awaitsRemainder) {
70
+ addedRequests.push(...(await remainder));
71
+ }
72
+ return buildResult(addedRequests, remainder);
73
+ }