@crawlee/core 4.0.0-beta.98 → 4.0.0-rc.0

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 (109) hide show
  1. package/autoscaling/autoscaled_pool.d.ts +3 -21
  2. package/autoscaling/autoscaled_pool.js +105 -97
  3. package/autoscaling/concurrency_system.d.ts +7 -22
  4. package/autoscaling/concurrency_system.js +112 -100
  5. package/autoscaling/cpu_load_signal.d.ts +1 -2
  6. package/autoscaling/cpu_load_signal.js +11 -10
  7. package/autoscaling/event_loop_load_signal.d.ts +1 -4
  8. package/autoscaling/event_loop_load_signal.js +18 -18
  9. package/autoscaling/index.d.ts +1 -1
  10. package/autoscaling/index.js +1 -1
  11. package/autoscaling/load_signal.d.ts +8 -7
  12. package/autoscaling/load_signal.js +14 -12
  13. package/autoscaling/memory_load_signal.d.ts +3 -12
  14. package/autoscaling/memory_load_signal.js +41 -41
  15. package/autoscaling/snapshotter.d.ts +7 -10
  16. package/autoscaling/snapshotter.js +18 -18
  17. package/autoscaling/{client_load_signal.d.ts → storage_backend_load_signal.d.ts} +14 -17
  18. package/autoscaling/{client_load_signal.js → storage_backend_load_signal.js} +27 -27
  19. package/autoscaling/system_status.d.ts +9 -11
  20. package/autoscaling/system_status.js +13 -13
  21. package/configuration.d.ts +16 -16
  22. package/configuration.js +6 -6
  23. package/crawlers/context_pipeline.js +6 -6
  24. package/crawlers/crawler_commons.d.ts +14 -116
  25. package/crawlers/crawler_commons.js +1 -107
  26. package/crawlers/error_snapshotter.d.ts +2 -4
  27. package/crawlers/error_snapshotter.js +5 -5
  28. package/crawlers/index.d.ts +1 -1
  29. package/crawlers/index.js +0 -1
  30. package/crawlers/statistics.d.ts +164 -45
  31. package/crawlers/statistics.js +339 -153
  32. package/debug.js +4 -4
  33. package/enqueue_links/enqueue_links.d.ts +51 -159
  34. package/enqueue_links/enqueue_links.js +38 -231
  35. package/enqueue_links/shared.d.ts +34 -24
  36. package/enqueue_links/shared.js +54 -36
  37. package/errors.d.ts +43 -12
  38. package/errors.js +48 -9
  39. package/events/event_manager.d.ts +2 -2
  40. package/events/event_manager.js +7 -6
  41. package/events/local_event_manager.d.ts +1 -1
  42. package/events/local_event_manager.js +5 -5
  43. package/http.d.ts +9 -0
  44. package/http.js +28 -0
  45. package/index.d.ts +1 -1
  46. package/index.js +1 -1
  47. package/log.js +5 -1
  48. package/memory-storage/memory-storage.d.ts +3 -11
  49. package/memory-storage/memory-storage.js +42 -48
  50. package/memory-storage/resource-clients/dataset.d.ts +1 -1
  51. package/memory-storage/resource-clients/dataset.js +8 -13
  52. package/memory-storage/resource-clients/key-value-store.d.ts +1 -1
  53. package/memory-storage/resource-clients/key-value-store.js +36 -38
  54. package/memory-storage/resource-clients/request-queue.d.ts +4 -23
  55. package/memory-storage/resource-clients/request-queue.js +68 -80
  56. package/owned_or_injected.d.ts +1 -3
  57. package/owned_or_injected.js +17 -17
  58. package/package.json +8 -9
  59. package/proxy_configuration.d.ts +1 -3
  60. package/proxy_configuration.js +18 -14
  61. package/recoverable_state.d.ts +80 -45
  62. package/recoverable_state.js +163 -75
  63. package/request.d.ts +3 -4
  64. package/request.js +57 -44
  65. package/router.d.ts +6 -10
  66. package/router.js +23 -23
  67. package/serialization.js +14 -13
  68. package/service_locator.d.ts +19 -10
  69. package/service_locator.js +57 -48
  70. package/session_pool/session.d.ts +3 -14
  71. package/session_pool/session.js +76 -74
  72. package/session_pool/session_pool.d.ts +6 -12
  73. package/session_pool/session_pool.js +85 -77
  74. package/storages/batched_adds.d.ts +37 -0
  75. package/storages/batched_adds.js +73 -0
  76. package/storages/dataset.d.ts +12 -1
  77. package/storages/dataset.js +133 -32
  78. package/storages/index.d.ts +2 -1
  79. package/storages/index.js +2 -1
  80. package/storages/key_value_store.d.ts +19 -4
  81. package/storages/key_value_store.js +204 -75
  82. package/storages/request_dedup_cache.d.ts +1 -2
  83. package/storages/request_dedup_cache.js +9 -9
  84. package/storages/request_list.d.ts +4 -23
  85. package/storages/request_list.js +103 -96
  86. package/storages/request_loader.d.ts +5 -0
  87. package/storages/request_manager_tandem.d.ts +1 -10
  88. package/storages/request_manager_tandem.js +27 -27
  89. package/storages/request_queue.d.ts +21 -21
  90. package/storages/request_queue.js +318 -177
  91. package/storages/sitemap_request_loader.d.ts +22 -70
  92. package/storages/sitemap_request_loader.js +122 -116
  93. package/storages/storage_instance_manager.d.ts +1 -4
  94. package/storages/storage_instance_manager.js +17 -19
  95. package/storages/storage_stats.d.ts +1 -1
  96. package/storages/storage_stats.js +4 -4
  97. package/storages/throttling_request_manager.d.ts +239 -0
  98. package/storages/throttling_request_manager.js +646 -0
  99. package/storages/transaction.d.ts +252 -0
  100. package/storages/transaction.js +251 -0
  101. package/storages/utils.d.ts +6 -3
  102. package/storages/utils.js +11 -2
  103. package/system-info/runtime.js +7 -7
  104. package/url.d.ts +9 -0
  105. package/url.js +11 -0
  106. package/validators.d.ts +22 -25
  107. package/validators.js +13 -25
  108. package/storages/access_checking.d.ts +0 -12
  109. package/storages/access_checking.js +0 -17
@@ -1,11 +1,29 @@
1
1
  import { AsyncQueue } from '@sapphire/async-queue';
2
- import ow from 'ow';
2
+ import { z } from 'zod';
3
+ import { EventType } from '../events/event_manager.js';
3
4
  import { serviceLocator } from '../service_locator.js';
4
5
  import { KeyValueStore } from '../storages/key_value_store.js';
6
+ import { parseArgument, schemas, validators } from '../validators.js';
5
7
  import { MAX_POOL_SIZE, PERSIST_STATE_KEY } from './consts.js';
6
8
  import { createDefaultSessionFingerprint } from './fingerprint.js';
7
9
  import { Session } from './session.js';
8
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.
@@ -60,53 +78,41 @@ const SESSION_REUSE_STRATEGIES = ['random', 'round-robin', 'use-until-failure'];
60
78
  * @category Scaling
61
79
  */
62
80
  export class SessionPool {
63
- static nextId = 0;
81
+ static #nextId = 0;
64
82
  id;
65
- log;
83
+ #log;
84
+ #sessions = [];
85
+ // kept as TS-private: session_pool tests read/override the members below directly
66
86
  maxPoolSize;
67
87
  createSessionFunction;
68
88
  keyValueStore;
69
- sessions = [];
70
89
  sessionMap = new Map();
71
90
  sessionOptions;
72
91
  persistStateKeyValueStoreId;
73
92
  persistStateKey;
74
- listener;
75
- events;
76
- persistenceOptions;
77
- sessionReuseStrategy;
78
- initPromise;
79
- queue = new AsyncQueue();
80
- roundRobinIndex = 0;
93
+ #listener;
94
+ #events;
95
+ #persistenceOptions;
96
+ #sessionReuseStrategy;
97
+ #initPromise;
98
+ #queue = new AsyncQueue();
99
+ #roundRobinIndex = 0;
81
100
  constructor(options = {}) {
82
- ow(options, ow.object.exactShape({
83
- id: ow.optional.any(ow.number, ow.string),
84
- maxPoolSize: ow.optional.number,
85
- persistStateKeyValueStoreId: ow.optional.string,
86
- persistStateKey: ow.optional.string,
87
- createSessionFunction: ow.optional.function,
88
- sessionOptions: ow.optional.object,
89
- log: ow.optional.object,
90
- persistenceOptions: ow.optional.object,
91
- sessionReuseStrategy: ow.optional.string.oneOf([...SESSION_REUSE_STRATEGIES]),
92
- }));
93
- const { id, maxPoolSize = MAX_POOL_SIZE, persistStateKeyValueStoreId, persistStateKey, createSessionFunction, sessionOptions = {}, log = serviceLocator.getLogger(), persistenceOptions = {
94
- enable: true,
95
- }, sessionReuseStrategy = 'random', } = options;
96
- this.id = id != null ? String(id) : String(SessionPool.nextId++);
97
- this.sessionReuseStrategy = sessionReuseStrategy;
98
- this.events = serviceLocator.getEventManager();
99
- this.log = log.child({ prefix: 'SessionPool' });
100
- this.persistenceOptions = persistenceOptions;
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;
101
107
  // Pool Configuration
102
108
  this.maxPoolSize = maxPoolSize;
103
109
  this.createSessionFunction = createSessionFunction || this.defaultCreateSessionFunction;
104
110
  // Session configuration. The pool-scoped logger is merged into per-call sessionOptions inside
105
- // `_invokeCreateSessionFunction`, so every Session inherits it without custom createSessionFunctions
111
+ // `invokeCreateSessionFunction`, so every Session inherits it without custom createSessionFunctions
106
112
  // having to know about it.
107
113
  this.sessionOptions = {
108
114
  ...sessionOptions,
109
- log: this.log,
115
+ log: this.#log,
110
116
  };
111
117
  // Session keyValueStore
112
118
  this.persistStateKeyValueStoreId = persistStateKeyValueStoreId;
@@ -117,39 +123,39 @@ export class SessionPool {
117
123
  */
118
124
  async usableSessionsCount() {
119
125
  await this.ensureInitialized();
120
- return this.sessions.filter((session) => session.isUsable()).length;
126
+ return this.#sessions.filter((session) => session.isUsable()).length;
121
127
  }
122
128
  /**
123
129
  * Gets count of retired sessions in the pool.
124
130
  */
125
131
  async retiredSessionsCount() {
126
132
  await this.ensureInitialized();
127
- return this.sessions.filter((session) => !session.isUsable()).length;
133
+ return this.#sessions.filter((session) => !session.isUsable()).length;
128
134
  }
129
135
  /**
130
136
  * Starts periodic state persistence and potentially loads SessionPool state from {@link KeyValueStore}.
131
137
  * Called automatically on first use of any public method.
132
138
  */
133
139
  async ensureInitialized() {
134
- if (!this.initPromise) {
135
- this.initPromise = this.setupPool();
140
+ if (!this.#initPromise) {
141
+ this.#initPromise = this.setupPool();
136
142
  }
137
- return this.initPromise;
143
+ return this.#initPromise;
138
144
  }
139
145
  async setupPool() {
140
- if (!this.persistenceOptions.enable) {
146
+ if (!this.#persistenceOptions.enable) {
141
147
  return;
142
148
  }
143
149
  this.keyValueStore = await KeyValueStore.open(this.persistStateKeyValueStoreId ? { id: this.persistStateKeyValueStoreId } : null, {
144
150
  configuration: serviceLocator.getConfiguration(),
145
151
  });
146
152
  if (!this.persistStateKeyValueStoreId) {
147
- 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}`);
148
154
  }
149
155
  // in case of migration happened and SessionPool state should be restored from the keyValueStore.
150
156
  await this.maybeLoadSessionPool();
151
- this.listener = this.persistState.bind(this);
152
- this.events.on("persistState" /* EventType.PERSIST_STATE */, this.listener);
157
+ this.#listener = this.persistState.bind(this);
158
+ this.#events.on(EventType.PERSIST_STATE, this.#listener);
153
159
  }
154
160
  /**
155
161
  * Adds a new session to the session pool. The pool automatically creates sessions up to the maximum size of the pool,
@@ -169,8 +175,8 @@ export class SessionPool {
169
175
  if (!this.hasSpaceForSession()) {
170
176
  this.removeRetiredSessions();
171
177
  }
172
- const newSession = options instanceof Session ? options : await this._invokeCreateSessionFunction(options);
173
- this.log.debug(`Adding new Session - ${newSession.id}`);
178
+ const newSession = options instanceof Session ? options : await this.invokeCreateSessionFunction(options);
179
+ this.#log.debug(`Adding new Session - ${newSession.id}`);
174
180
  this.registerSession(newSession);
175
181
  }
176
182
  /**
@@ -181,7 +187,7 @@ export class SessionPool {
181
187
  */
182
188
  async newSession(sessionOptions) {
183
189
  await this.ensureInitialized();
184
- const newSession = await this._invokeCreateSessionFunction(sessionOptions);
190
+ const newSession = await this.invokeCreateSessionFunction(sessionOptions);
185
191
  this.registerSession(newSession);
186
192
  return newSession;
187
193
  }
@@ -194,7 +200,7 @@ export class SessionPool {
194
200
  */
195
201
  async getSession(sessionId) {
196
202
  await this.ensureInitialized();
197
- await this.queue.wait();
203
+ await this.#queue.wait();
198
204
  try {
199
205
  if (sessionId) {
200
206
  const session = this.sessionMap.get(sessionId);
@@ -212,14 +218,14 @@ export class SessionPool {
212
218
  return await this.createSession();
213
219
  }
214
220
  finally {
215
- this.queue.shift();
221
+ this.#queue.shift();
216
222
  }
217
223
  }
218
224
  /**
219
225
  * @param options - Override the persistence options provided in the constructor
220
226
  */
221
227
  async resetStore(options) {
222
- if (!this.persistenceOptions.enable && !options?.enable) {
228
+ if (!this.#persistenceOptions.enable && !options?.enable) {
223
229
  return;
224
230
  }
225
231
  await this.ensureInitialized();
@@ -234,7 +240,7 @@ export class SessionPool {
234
240
  return {
235
241
  usableSessionsCount: await this.usableSessionsCount(),
236
242
  retiredSessionsCount: await this.retiredSessionsCount(),
237
- sessions: this.sessions.map((session) => session.getState()),
243
+ sessions: this.#sessions.map((session) => session.getState()),
238
244
  };
239
245
  }
240
246
  /**
@@ -243,40 +249,43 @@ export class SessionPool {
243
249
  * @param options - Override the persistence options provided in the constructor
244
250
  */
245
251
  async persistState(options) {
246
- if (!this.persistenceOptions.enable && !options?.enable) {
252
+ if (!this.#persistenceOptions.enable && !options?.enable) {
247
253
  return;
248
254
  }
249
255
  await this.ensureInitialized();
250
- this.log.debug('Persisting state', {
256
+ this.#log.debug('Persisting state', {
251
257
  persistStateKeyValueStoreId: this.persistStateKeyValueStoreId,
252
258
  persistStateKey: this.persistStateKey,
253
259
  });
254
260
  await this.keyValueStore
255
261
  ?.setValue(this.persistStateKey, await this.getState())
256
- .catch((error) => this.log.warning(`Failed to persist the session pool stats to ${this.persistStateKey}`, { error }));
262
+ .catch((error) => this.#log.warning(`Failed to persist the session pool stats to ${this.persistStateKey}`, { error }));
257
263
  }
258
264
  /**
259
265
  * Removes listener from `persistState` event.
260
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.
261
268
  */
262
- async teardown() {
263
- if (!this.initPromise)
269
+ async teardown({ persistState = true } = {}) {
270
+ if (!this.#initPromise)
264
271
  return;
265
272
  await this.ensureInitialized();
266
- if (this.listener) {
267
- this.events.off("persistState" /* EventType.PERSIST_STATE */, this.listener);
273
+ if (this.#listener) {
274
+ this.#events.off(EventType.PERSIST_STATE, this.#listener);
275
+ }
276
+ if (persistState) {
277
+ await this.persistState();
268
278
  }
269
- await this.persistState();
270
279
  }
271
280
  /**
272
281
  * Removes retired `Session` instances from `SessionPool`.
273
282
  */
274
283
  removeRetiredSessions() {
275
- this.sessions = this.sessions.filter((storedSession) => {
284
+ this.#sessions = this.#sessions.filter((storedSession) => {
276
285
  if (storedSession.isUsable())
277
286
  return true;
278
287
  this.sessionMap.delete(storedSession.id);
279
- this.log.debug(`Removed Session - ${storedSession.id}`);
288
+ this.#log.debug(`Removed Session - ${storedSession.id}`);
280
289
  return false;
281
290
  });
282
291
  }
@@ -285,14 +294,14 @@ export class SessionPool {
285
294
  * @param newSession `Session` instance to be added.
286
295
  */
287
296
  registerSession(newSession) {
288
- this.sessions.push(newSession);
297
+ this.#sessions.push(newSession);
289
298
  this.sessionMap.set(newSession.id, newSession);
290
299
  }
291
300
  /**
292
301
  * Gets random index.
293
302
  */
294
303
  getRandomIndex() {
295
- return Math.floor(Math.random() * this.sessions.length);
304
+ return Math.floor(Math.random() * this.#sessions.length);
296
305
  }
297
306
  /**
298
307
  * Creates new session without any extra behavior.
@@ -301,8 +310,7 @@ export class SessionPool {
301
310
  * @returns New session.
302
311
  */
303
312
  async defaultCreateSessionFunction(options = {}) {
304
- ow(options, ow.object.exactShape({ sessionOptions: ow.optional.object }));
305
- const { sessionOptions = {} } = options;
313
+ const { sessionOptions } = parseArgument(options, createSessionOptionsSchema);
306
314
  return new Session(sessionOptions);
307
315
  }
308
316
  /**
@@ -315,7 +323,7 @@ export class SessionPool {
315
323
  * through `maybeLoadSessionPool` naturally wins because it arrives in
316
324
  * `perCallOptions`.
317
325
  */
318
- async _invokeCreateSessionFunction(perCallOptions) {
326
+ async invokeCreateSessionFunction(perCallOptions) {
319
327
  const sessionOptions = {
320
328
  fingerprint: createDefaultSessionFingerprint(),
321
329
  ...this.sessionOptions,
@@ -328,35 +336,35 @@ export class SessionPool {
328
336
  * @returns Newly created `Session` instance.
329
337
  */
330
338
  async createSession() {
331
- const newSession = await this._invokeCreateSessionFunction();
339
+ const newSession = await this.invokeCreateSessionFunction();
332
340
  this.registerSession(newSession);
333
- this.log.debug(`Created new Session - ${newSession.id}`);
341
+ this.#log.debug(`Created new Session - ${newSession.id}`);
334
342
  return newSession;
335
343
  }
336
344
  /**
337
345
  * Decides whether there is enough space for creating new session.
338
346
  */
339
347
  hasSpaceForSession() {
340
- return this.sessions.length < this.maxPoolSize;
348
+ return this.#sessions.length < this.maxPoolSize;
341
349
  }
342
350
  /**
343
351
  * Picks a session from the `SessionPool` according to the configured `sessionReuseStrategy`.
344
352
  * Returns `undefined` when no session should be reused and a new one should be created instead.
345
353
  */
346
354
  pickSession() {
347
- if (this.sessionReuseStrategy !== 'use-until-failure' && this.hasSpaceForSession())
355
+ if (this.#sessionReuseStrategy !== 'use-until-failure' && this.hasSpaceForSession())
348
356
  return undefined;
349
- if (this.sessionReuseStrategy === 'use-until-failure') {
350
- return this.sessions.find((session) => session.isUsable());
357
+ if (this.#sessionReuseStrategy === 'use-until-failure') {
358
+ return this.#sessions.find((session) => session.isUsable());
351
359
  }
352
360
  let picked;
353
- if (this.sessionReuseStrategy === 'round-robin') {
354
- const index = this.roundRobinIndex % this.sessions.length;
355
- this.roundRobinIndex = index + 1;
356
- picked = this.sessions[index];
361
+ if (this.#sessionReuseStrategy === 'round-robin') {
362
+ const index = this.#roundRobinIndex % this.#sessions.length;
363
+ this.#roundRobinIndex = index + 1;
364
+ picked = this.#sessions[index];
357
365
  }
358
366
  else {
359
- picked = this.sessions[this.getRandomIndex()];
367
+ picked = this.#sessions[this.getRandomIndex()];
360
368
  }
361
369
  return picked.isUsable() ? picked : undefined;
362
370
  }
@@ -369,18 +377,18 @@ export class SessionPool {
369
377
  if (!loadedSessionPool)
370
378
  return;
371
379
  // Invalidate old sessions and load active sessions only
372
- this.log.debug('Recreating state from KeyValueStore', {
380
+ this.#log.debug('Recreating state from KeyValueStore', {
373
381
  persistStateKeyValueStoreId: this.persistStateKeyValueStoreId,
374
382
  persistStateKey: this.persistStateKey,
375
383
  });
376
384
  for (const sessionObject of loadedSessionPool.sessions) {
377
385
  sessionObject.createdAt = new Date(sessionObject.createdAt);
378
386
  sessionObject.expiresAt = new Date(sessionObject.expiresAt);
379
- const recreatedSession = await this._invokeCreateSessionFunction(sessionObject);
387
+ const recreatedSession = await this.invokeCreateSessionFunction(sessionObject);
380
388
  if (recreatedSession.isUsable()) {
381
389
  this.registerSession(recreatedSession);
382
390
  }
383
391
  }
384
- this.log.debug(`${this.sessions.length} active sessions loaded from KeyValueStore`);
392
+ this.#log.debug(`${this.#sessions.length} active sessions loaded from KeyValueStore`);
385
393
  }
386
394
  }
@@ -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
+ }
@@ -1,6 +1,7 @@
1
1
  import type { Awaitable, DatasetBackend, DatasetInfo, Dictionary } from '@crawlee/types';
2
2
  import { Configuration } from '../configuration.js';
3
3
  import type { CrawleeLogger } from '../log.js';
4
+ import type { JournalEntry } from './transaction.js';
4
5
  import type { DatasetStats } from './storage_stats.js';
5
6
  import type { StorageOpenOptions } from './utils.js';
6
7
  import type { StorageIdentifier } from './storage_instance_manager.js';
@@ -139,12 +140,12 @@ export interface DatasetExportToOptions extends DatasetExportOptions {
139
140
  * @category Result Stores
140
141
  */
141
142
  export declare class Dataset<Data extends Dictionary = Dictionary> {
143
+ #private;
142
144
  readonly configuration: Configuration;
143
145
  id: string;
144
146
  name?: string;
145
147
  backend: DatasetBackend<Data>;
146
148
  log: CrawleeLogger;
147
- private readonly statsTracker;
148
149
  /**
149
150
  * @internal
150
151
  */
@@ -170,6 +171,16 @@ export declare class Dataset<Data extends Dictionary = Dictionary> {
170
171
  * Returns {@link DatasetContent} object holding the items in the dataset based on the provided parameters.
171
172
  */
172
173
  getData(options?: DatasetDataOptions): Promise<DatasetContent<Data>>;
174
+ /**
175
+ * The single transaction-aware page read all dataset read paths go through — both `getData()` and
176
+ * the private `fetchPages()`. Returns the real page concatenated with the current transaction's
177
+ * buffered items, with `offset` / `limit` / `desc` windowing applied across the concatenation.
178
+ */
179
+ private readPage;
180
+ /** The active transaction's buffered writes to this dataset, derived from its journal. */
181
+ private bufferedJournalEntries;
182
+ /** @internal */
183
+ commitJournalEntries(entries: JournalEntry[]): Promise<void>;
173
184
  /**
174
185
  * Returns all the data from the dataset. This will iterate through the whole dataset
175
186
  * via the `listItems()` client method, which gives you only paginated results.