@crawlee/basic 3.17.1-beta.7 → 3.17.1-beta.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.mjs CHANGED
@@ -48,6 +48,7 @@ export const RequestQueue = mod.RequestQueue;
48
48
  export const RequestQueueV1 = mod.RequestQueueV1;
49
49
  export const RequestQueueV2 = mod.RequestQueueV2;
50
50
  export const RequestState = mod.RequestState;
51
+ export const RequestValidationError = mod.RequestValidationError;
51
52
  export const RetryRequestError = mod.RetryRequestError;
52
53
  export const Router = mod.Router;
53
54
  export const STATE_PERSISTENCE_KEY = mod.STATE_PERSISTENCE_KEY;
@@ -76,6 +77,7 @@ export const createDeserialize = mod.createDeserialize;
76
77
  export const createEventLoopLoadSignal = mod.createEventLoopLoadSignal;
77
78
  export const createRequestOptions = mod.createRequestOptions;
78
79
  export const createRequests = mod.createRequests;
80
+ export const defaultRoute = mod.defaultRoute;
79
81
  export const deserializeArray = mod.deserializeArray;
80
82
  export const enqueueLinks = mod.enqueueLinks;
81
83
  export const evaluateLoadSignalSample = mod.evaluateLoadSignalSample;
@@ -96,5 +98,6 @@ export const tryAbsoluteURL = mod.tryAbsoluteURL;
96
98
  export const updateEnqueueLinksPatternCache = mod.updateEnqueueLinksPatternCache;
97
99
  export const useState = mod.useState;
98
100
  export const validateGlobPattern = mod.validateGlobPattern;
101
+ export const validateUserData = mod.validateUserData;
99
102
  export const validators = mod.validators;
100
103
  export const withCheckedStorageAccess = mod.withCheckedStorageAccess;
@@ -436,7 +436,7 @@ export declare class BasicCrawler<Context extends CrawlingContext = BasicCrawlin
436
436
  userAgent?: string;
437
437
  };
438
438
  protected onSkippedRequest?: SkippedRequestCallback;
439
- private _closeEvents?;
439
+ private _ownsEventManager;
440
440
  private loggedPerRun;
441
441
  private experiments;
442
442
  private readonly robotsTxtFileCache;
@@ -545,6 +545,24 @@ export declare class BasicCrawler<Context extends CrawlingContext = BasicCrawlin
545
545
  */
546
546
  stop(reason?: string): void;
547
547
  getRequestQueue(): Promise<RequestProvider>;
548
+ /**
549
+ * The request handler exactly as the user supplied it — a {@link Router} when one is in use, whether it
550
+ * was passed as `requestHandler` or auto-wired from {@link BasicCrawler.router|`crawler.router`}.
551
+ *
552
+ * Router-aware features read per-label metadata off this handler (currently the `userData` schema map), so
553
+ * it must resolve to the *unwrapped* handler. Subclasses that hand a wrapper to `BasicCrawler` instead of
554
+ * the user's own function — {@link BrowserCrawler} and its descendants do — have to override this, or
555
+ * those features silently no-op against the wrapper.
556
+ */
557
+ protected get userRequestHandler(): RequestHandler<Context>;
558
+ /**
559
+ * Validates a request source's `userData` against the {@link RouteSchemas|Standard Schema} registered
560
+ * for its label on the crawler's schema-router (if any), throwing a {@link RequestValidationError} on
561
+ * mismatch. A no-op when the user's request handler is not a schema-router, or no schema is registered for
562
+ * the request's label. Applied by the crawler on the add paths it owns — `crawler.addRequests`,
563
+ * `crawler.run`, `context.addRequests` and `context.enqueueLinks`.
564
+ */
565
+ protected validateRequestUserData(source: Source | string): Promise<void>;
548
566
  useState<State extends Dictionary = Dictionary>(defaultValue?: State): Promise<State>;
549
567
  protected get pendingRequestCountApproximation(): number;
550
568
  protected calculateEnqueuedRequestLimit(explicitLimit?: number): number | undefined;
@@ -719,5 +737,6 @@ interface HandlePropertyNameChangeData<New, Old> {
719
737
  * await crawler.run();
720
738
  * ```
721
739
  */
722
- export declare function createBasicRouter<Context extends BasicCrawlingContext = BasicCrawlingContext, UserData extends Dictionary = GetUserDataFromRequest<Context['request']>>(routes?: RouterRoutes<Context, UserData>): RouterHandler<Context>;
740
+ export declare function createBasicRouter<Context extends BasicCrawlingContext = BasicCrawlingContext, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>>(routes?: RouterRoutes<Context, Routes>): RouterHandler<Context, Routes>;
741
+ export declare function createBasicRouter<Context extends BasicCrawlingContext = BasicCrawlingContext, UserData extends Dictionary = GetUserDataFromRequest<Context['request']>>(routes?: RouterRoutes<Context, Record<string, UserData>>): RouterHandler<Context, Record<string, UserData>>;
723
742
  export {};
@@ -336,11 +336,11 @@ class BasicCrawler {
336
336
  writable: true,
337
337
  value: void 0
338
338
  });
339
- Object.defineProperty(this, "_closeEvents", {
339
+ Object.defineProperty(this, "_ownsEventManager", {
340
340
  enumerable: true,
341
341
  configurable: true,
342
342
  writable: true,
343
- value: void 0
343
+ value: false
344
344
  });
345
345
  Object.defineProperty(this, "loggedPerRun", {
346
346
  enumerable: true,
@@ -655,8 +655,8 @@ class BasicCrawler {
655
655
  await this.autoscaledPool.run();
656
656
  }
657
657
  finally {
658
- await this.teardown();
659
658
  await this.stats.stopCapturing();
659
+ await this.teardown();
660
660
  process.off('SIGINT', sigintHandler);
661
661
  this.events.off("migrating" /* EventType.MIGRATING */, boundPauseOnMigration);
662
662
  this.events.off("aborting" /* EventType.ABORTING */, boundPauseOnMigration);
@@ -688,8 +688,12 @@ class BasicCrawler {
688
688
  finished = true;
689
689
  }
690
690
  periodicLogger.stop();
691
- // Don't await, we don't want to block the execution
692
- void this.setStatusMessage(`Finished! Total ${this.stats.state.requestsFinished + this.stats.state.requestsFailed} requests: ${this.stats.state.requestsFinished} succeeded, ${this.stats.state.requestsFailed} failed.`, { isStatusMessageTerminal: true, level: 'INFO' });
691
+ // Give the event loop a single tick to flush the HTTP
692
+ // 1ms is enough because we already have a keep-alive connection to the API
693
+ await Promise.race([
694
+ this.setStatusMessage(`Finished! Total ${this.stats.state.requestsFinished + this.stats.state.requestsFailed} requests: ${this.stats.state.requestsFinished} succeeded, ${this.stats.state.requestsFailed} failed.`, { isStatusMessageTerminal: true, level: 'INFO' }),
695
+ (0, utils_1.sleep)(1),
696
+ ]);
693
697
  this.running = false;
694
698
  this.hasFinishedBefore = true;
695
699
  }
@@ -725,6 +729,44 @@ class BasicCrawler {
725
729
  }
726
730
  return this.requestQueue;
727
731
  }
732
+ /**
733
+ * The request handler exactly as the user supplied it — a {@link Router} when one is in use, whether it
734
+ * was passed as `requestHandler` or auto-wired from {@link BasicCrawler.router|`crawler.router`}.
735
+ *
736
+ * Router-aware features read per-label metadata off this handler (currently the `userData` schema map), so
737
+ * it must resolve to the *unwrapped* handler. Subclasses that hand a wrapper to `BasicCrawler` instead of
738
+ * the user's own function — {@link BrowserCrawler} and its descendants do — have to override this, or
739
+ * those features silently no-op against the wrapper.
740
+ */
741
+ get userRequestHandler() {
742
+ return this.requestHandler;
743
+ }
744
+ /**
745
+ * Validates a request source's `userData` against the {@link RouteSchemas|Standard Schema} registered
746
+ * for its label on the crawler's schema-router (if any), throwing a {@link RequestValidationError} on
747
+ * mismatch. A no-op when the user's request handler is not a schema-router, or no schema is registered for
748
+ * the request's label. Applied by the crawler on the add paths it owns — `crawler.addRequests`,
749
+ * `crawler.run`, `context.addRequests` and `context.enqueueLinks`.
750
+ */
751
+ async validateRequestUserData(source) {
752
+ if (typeof source === 'string') {
753
+ return;
754
+ }
755
+ const getSchema = this.userRequestHandler.getSchema;
756
+ if (typeof getSchema !== 'function') {
757
+ return;
758
+ }
759
+ // Resolve the label via its public accessors only — the top-level `label` of a `RequestOptions` or the
760
+ // `Request.label` getter — rather than reaching into `userData`, where the request happens to store it.
761
+ const target = source;
762
+ const schema = getSchema(target.label);
763
+ if (!schema) {
764
+ return;
765
+ }
766
+ // Store the parsed value rather than the raw input, so the queue holds the same coerced `userData` the
767
+ // handler will see. Assigning through a `Request` instance's setter keeps its internal `__crawlee` meta.
768
+ target.userData = await (0, core_1.validateUserData)(target.label, schema, target.userData ?? {});
769
+ }
728
770
  async useState(defaultValue = {}) {
729
771
  const kvs = await core_1.KeyValueStore.open(null, { config: this.config });
730
772
  return kvs.getAutoSavedValue(BasicCrawler.CRAWLEE_STATE_KEY, defaultValue);
@@ -773,6 +815,7 @@ class BasicCrawler {
773
815
  const skippedBecauseOfMaxCrawlDepth = new Set();
774
816
  const isAllowedBasedOnRobotsTxtFile = this.isAllowedBasedOnRobotsTxtFile.bind(this);
775
817
  const maxCrawlDepth = this.maxCrawlDepth;
818
+ const validateRequestUserData = this.validateRequestUserData.bind(this);
776
819
  (0, ow_1.default)(requests, ow_1.default.object
777
820
  .is((value) => (0, utils_1.isIterable)(value) || (0, utils_1.isAsyncIterable)(value))
778
821
  .message((value) => `Expected an iterable or async iterable, got ${(0, utils_1.getObjectType)(value)}`));
@@ -784,6 +827,7 @@ class BasicCrawler {
784
827
  continue;
785
828
  }
786
829
  if (await isAllowedBasedOnRobotsTxtFile(url)) {
830
+ await validateRequestUserData(request);
787
831
  yield request;
788
832
  }
789
833
  else {
@@ -888,7 +932,7 @@ class BasicCrawler {
888
932
  async _init() {
889
933
  if (!this.events.isInitialized()) {
890
934
  await this.events.init();
891
- this._closeEvents = true;
935
+ this._ownsEventManager = true;
892
936
  }
893
937
  this.autoscaledPool = new core_1.AutoscaledPool(this.autoscaledPoolOptions, this.config);
894
938
  if (this.useSessionPool) {
@@ -1173,6 +1217,9 @@ class BasicCrawler {
1173
1217
  }
1174
1218
  await this.handleSkippedRequest(skippedOptions);
1175
1219
  };
1220
+ // `enqueueLinks` applies `options.label`/`options.userData` to every newly enqueued request, so a single
1221
+ // validation against the label's schema covers them all (a no-op unless the router declares a schema).
1222
+ await this.validateRequestUserData({ label: options.label, userData: options.userData });
1176
1223
  return (0, core_1.enqueueLinks)({
1177
1224
  requestQueue,
1178
1225
  robotsTxtFile: await this.getRobotsTxtFileForUrl(request.url),
@@ -1359,11 +1406,16 @@ class BasicCrawler {
1359
1406
  * To stop the crawler gracefully (waiting for all running requests to finish), use {@link BasicCrawler.stop|`crawler.stop()`} instead.
1360
1407
  */
1361
1408
  async teardown() {
1362
- this.events.emit("persistState" /* EventType.PERSIST_STATE */, { isMigrating: false });
1409
+ // When this crawler initialized the event manager, its close() call emits
1410
+ // the final persistence event after the crawler-specific state has been
1411
+ // saved. External event managers still need an explicit event here.
1412
+ if (!this._ownsEventManager) {
1413
+ this.events.emit("persistState" /* EventType.PERSIST_STATE */, { isMigrating: false });
1414
+ }
1363
1415
  if (this.useSessionPool) {
1364
- await this.sessionPool.teardown();
1416
+ await this.sessionPool.teardown({ persistState: this._ownsEventManager });
1365
1417
  }
1366
- if (this._closeEvents) {
1418
+ if (this._ownsEventManager) {
1367
1419
  await this.events.close();
1368
1420
  }
1369
1421
  await this.autoscaledPool?.abort();
@@ -1502,30 +1554,6 @@ Object.defineProperty(BasicCrawler, "optionsShape", {
1502
1554
  statisticsOptions: ow_1.default.optional.object,
1503
1555
  }
1504
1556
  });
1505
- /**
1506
- * Creates new {@link Router} instance that works based on request labels.
1507
- * This instance can then serve as a {@link BasicCrawlerOptions.requestHandler|`requestHandler`} of our {@link BasicCrawler}.
1508
- * Defaults to the {@link BasicCrawlingContext}.
1509
- *
1510
- * > Serves as a shortcut for using `Router.create<BasicCrawlingContext>()`.
1511
- *
1512
- * ```ts
1513
- * import { BasicCrawler, createBasicRouter } from 'crawlee';
1514
- *
1515
- * const router = createBasicRouter();
1516
- * router.addHandler('label-a', async (ctx) => {
1517
- * ctx.log.info('...');
1518
- * });
1519
- * router.addDefaultHandler(async (ctx) => {
1520
- * ctx.log.info('...');
1521
- * });
1522
- *
1523
- * const crawler = new BasicCrawler({
1524
- * requestHandler: router,
1525
- * });
1526
- * await crawler.run();
1527
- * ```
1528
- */
1529
1557
  function createBasicRouter(routes) {
1530
1558
  return core_1.Router.create(routes);
1531
1559
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlee/basic",
3
- "version": "3.17.1-beta.7",
3
+ "version": "3.17.1-beta.70",
4
4
  "description": "The scalable web crawling and scraping library for JavaScript/Node.js. Enables development of data extraction and web automation jobs (not only) with headless Chrome and Puppeteer.",
5
5
  "engines": {
6
6
  "node": ">=16.0.0"
@@ -45,12 +45,13 @@
45
45
  "access": "public"
46
46
  },
47
47
  "dependencies": {
48
+ "@apify/datastructures": "^2.0.0",
48
49
  "@apify/log": "^2.4.0",
49
- "@apify/timeout": "^0.3.0",
50
+ "@apify/timeout": "^0.4.0",
50
51
  "@apify/utilities": "^2.7.10",
51
- "@crawlee/core": "3.17.1-beta.7",
52
- "@crawlee/types": "3.17.1-beta.7",
53
- "@crawlee/utils": "3.17.1-beta.7",
52
+ "@crawlee/core": "3.17.1-beta.70",
53
+ "@crawlee/types": "3.17.1-beta.70",
54
+ "@crawlee/utils": "3.17.1-beta.70",
54
55
  "csv-stringify": "^6.2.0",
55
56
  "fs-extra": "^11.0.0",
56
57
  "got-scraping": "^4.2.1",
@@ -66,5 +67,5 @@
66
67
  }
67
68
  }
68
69
  },
69
- "gitHead": "541c9687ee7f8421189d489ea817aaa08cb9b4ec"
70
+ "gitHead": "7bd31d6a8845a46afae420540bf6da00c8922379"
70
71
  }