@crawlee/basic 4.0.0-beta.165 → 4.0.0-beta.167
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/internals/basic-crawler.d.ts +16 -7
- package/internals/basic-crawler.js +112 -111
- package/internals/crawler-run.d.ts +74 -0
- package/internals/crawler-run.js +142 -0
- package/package.json +7 -7
|
@@ -398,13 +398,15 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
398
398
|
* {@link ISessionPool} so custom implementations can be plugged in via the `sessionPool` constructor option.
|
|
399
399
|
*/
|
|
400
400
|
get sessionPool(): ISessionPool;
|
|
401
|
+
/** Whether a {@link BasicCrawler.run|`run()`} is in progress on this instance. */
|
|
402
|
+
get running(): boolean;
|
|
401
403
|
/**
|
|
402
|
-
* The concurrency governor
|
|
404
|
+
* The concurrency governor the run in progress is booking its requests against — either the
|
|
403
405
|
* {@link BasicCrawlerOptions.concurrencySystem|`concurrencySystem`} that was injected, or the default the
|
|
404
406
|
* crawler built for itself. Read it for telemetry: `desiredConcurrency`, `currentConcurrency`, `isRunning`.
|
|
405
407
|
*
|
|
406
|
-
* > *NOTE:* `undefined`
|
|
407
|
-
*
|
|
408
|
+
* > *NOTE:* `undefined` outside a run, and a crawler-owned default is rebuilt for every run — so read it
|
|
409
|
+
* during a run rather than caching it across runs.
|
|
408
410
|
*
|
|
409
411
|
* {@link IConcurrencySystem} is deliberately read-only. Tuning concurrency *while a crawl is running* means
|
|
410
412
|
* owning the instance: build a {@link ConcurrencySystem} yourself and inject it, then set
|
|
@@ -433,7 +435,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
433
435
|
request: Request;
|
|
434
436
|
}, CrawlingContext>;
|
|
435
437
|
get contextPipeline(): ContextPipeline<CrawlingContext, ExtendedContext>;
|
|
436
|
-
running: boolean;
|
|
437
438
|
/** Whether a `run()` on this instance has already finished - a repeated one continues where it left off. */
|
|
438
439
|
get hasFinishedBefore(): boolean;
|
|
439
440
|
get log(): CrawleeLogger;
|
|
@@ -616,7 +617,6 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
616
617
|
*/
|
|
617
618
|
private validateRequestUserData;
|
|
618
619
|
useState<State extends Dictionary = Dictionary>(defaultValue?: State): Promise<State>;
|
|
619
|
-
private logOncePerRun;
|
|
620
620
|
/**
|
|
621
621
|
* Adds requests to the queue in batches. By default, it will resolve after the initial batch is added, and continue
|
|
622
622
|
* adding the rest in background. You can configure the batch size via `batchSize` option and the sleep time in between
|
|
@@ -751,13 +751,22 @@ export declare class BasicCrawler<Context extends CrawlingContext = CrawlingCont
|
|
|
751
751
|
private errorAbsolvesSession;
|
|
752
752
|
private canRequestBeRetried;
|
|
753
753
|
/**
|
|
754
|
-
*
|
|
754
|
+
* Ends the run in progress immediately, without waiting for the requests already in flight.
|
|
755
755
|
*
|
|
756
|
-
* This
|
|
756
|
+
* This runs at the end of every {@link BasicCrawler.run|`run()`}, so what it releases is per-run only, and
|
|
757
|
+
* a repeated `run()` has to find everything it needs either intact or re-establishable. What outlives a run is
|
|
758
|
+
* released by {@link BasicCrawler.destroy|`destroy()`} instead.
|
|
757
759
|
*
|
|
758
760
|
* To stop the crawler gracefully (waiting for all running requests to finish), use {@link BasicCrawler.stop|`crawler.stop()`} instead.
|
|
759
761
|
*/
|
|
760
762
|
teardown(): Promise<void>;
|
|
763
|
+
/**
|
|
764
|
+
* Releases what the crawler owns beyond a single run — in the browser crawlers, the browser pool. A finished
|
|
765
|
+
* `run()` has already released everything a run owns, so this is only needed before dropping a crawler you
|
|
766
|
+
* could otherwise have run again.
|
|
767
|
+
*/
|
|
768
|
+
destroy(): Promise<void>;
|
|
769
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
761
770
|
protected getCookieHeaderFromRequest(request: Request): string;
|
|
762
771
|
private requestMatchesEnqueueStrategy;
|
|
763
772
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
|
-
import { applyRequestTransform,
|
|
3
|
+
import { applyRequestTransform, bindMethodsToServiceLocator, BLOCKED_STATUS_CODES, buildEnqueueStrategyPatterns, ConcurrencySystem, Configuration, constructUrlPatternObjects, ContextPipeline, ContextPipelineCleanupError, ContextPipelineInitializationError, ContextPipelineInterruptedError, createRequestOptions, createSkippedRequestArgs, createStorageTransaction, Request, CriticalError, currentStorageTransaction, Dataset, EnqueueStrategy, EventManager, EventType, filterRequestOptionsByPatterns, getObjectType, KeyValueStore, log, mergeCookies, MissingSessionError, NavigationSkippedError, NonRetryableError, OwnedOrInjected, PersistentRateLimitError, purgeDefaultStorages, RequestHandlerError, parseRetryAfterHeader, RequestThrottledError, RequestManagerTandem, RequestQueue, RequestState, RetryRequestError, Router, ServiceLocator, serviceLocator, Session, SessionError, SessionPool, Statistics, ThrottlingRequestManager, validateUserData, validators, withDirectStorageAccess, } from '@crawlee/core';
|
|
4
4
|
import { BaseHttpClient, FetchHttpClient } from '@crawlee/http-client';
|
|
5
5
|
import { isAsyncIterable, isIterable, parseArgument, ROTATE_PROXY_ERRORS, schemas } from '@crawlee/utils/internal';
|
|
6
6
|
import { RobotsTxtFile } from '@crawlee/utils';
|
|
@@ -9,6 +9,7 @@ import { z } from 'zod';
|
|
|
9
9
|
import { LruCache } from '@apify/datastructures';
|
|
10
10
|
import { addTimeoutToPromise, extendTimeout, storage as timeoutStorage, TimeoutError, tryCancel } from '@apify/timeout';
|
|
11
11
|
import { cryptoRandomObjectId } from '@apify/utilities';
|
|
12
|
+
import { CrawlerRun } from './crawler-run.js';
|
|
12
13
|
import { extendTimeoutKey, navigationDeadlineKey, raceWithTimeout, timeoutExpiredKey, } from './request-timeout.js';
|
|
13
14
|
import { createSendRequest } from './send-request.js';
|
|
14
15
|
class LazyDefaultHttpClient extends BaseHttpClient {
|
|
@@ -120,35 +121,36 @@ export class BasicCrawler {
|
|
|
120
121
|
* the concurrency shortcuts folded in (owned, so the crawler starts and stops it).
|
|
121
122
|
*/
|
|
122
123
|
#resolveConcurrencySystem;
|
|
123
|
-
/**
|
|
124
|
-
#
|
|
124
|
+
/** The last run started on this instance. It outlives the run, so anything that drives one goes through `#liveRun`. */
|
|
125
|
+
#run;
|
|
126
|
+
#concurrencySystem;
|
|
127
|
+
/** The run in progress, if any — a finished run is history and cannot be driven. */
|
|
128
|
+
get #liveRun() {
|
|
129
|
+
return this.#run?.isLive === true ? this.#run : undefined;
|
|
130
|
+
}
|
|
131
|
+
/** The run whose task loop is dispatching, if any — only that one can be paused and resumed. */
|
|
132
|
+
get #dispatchingRun() {
|
|
133
|
+
return this.#liveRun?.isDispatching === true ? this.#liveRun : undefined;
|
|
134
|
+
}
|
|
135
|
+
/** Whether a {@link BasicCrawler.run|`run()`} is in progress on this instance. */
|
|
136
|
+
get running() {
|
|
137
|
+
return this.#liveRun !== undefined;
|
|
138
|
+
}
|
|
125
139
|
/**
|
|
126
|
-
* The concurrency governor
|
|
140
|
+
* The concurrency governor the run in progress is booking its requests against — either the
|
|
127
141
|
* {@link BasicCrawlerOptions.concurrencySystem|`concurrencySystem`} that was injected, or the default the
|
|
128
142
|
* crawler built for itself. Read it for telemetry: `desiredConcurrency`, `currentConcurrency`, `isRunning`.
|
|
129
143
|
*
|
|
130
|
-
* > *NOTE:* `undefined`
|
|
131
|
-
*
|
|
144
|
+
* > *NOTE:* `undefined` outside a run, and a crawler-owned default is rebuilt for every run — so read it
|
|
145
|
+
* during a run rather than caching it across runs.
|
|
132
146
|
*
|
|
133
147
|
* {@link IConcurrencySystem} is deliberately read-only. Tuning concurrency *while a crawl is running* means
|
|
134
148
|
* owning the instance: build a {@link ConcurrencySystem} yourself and inject it, then set
|
|
135
149
|
* `minConcurrency`/`maxConcurrency`/`desiredConcurrency` on your own reference.
|
|
136
150
|
*/
|
|
137
151
|
get concurrencySystem() {
|
|
138
|
-
return this.#
|
|
152
|
+
return this.#concurrencySystem?.value;
|
|
139
153
|
}
|
|
140
|
-
/**
|
|
141
|
-
* The task loop that dispatches this run's requests. Private on purpose — it is a bare parallel task runner with
|
|
142
|
-
* no configuration left of its own (see {@link ConcurrencySystem}), and everything a caller legitimately did
|
|
143
|
-
* with it now has a crawler-level counterpart: {@link BasicCrawler.pause|`pause()`},
|
|
144
|
-
* {@link BasicCrawler.resume|`resume()`}, {@link BasicCrawler.teardown|`teardown()`} and
|
|
145
|
-
* {@link BasicCrawler.concurrencySystem|`concurrencySystem`}.
|
|
146
|
-
*/
|
|
147
|
-
#autoscaledPool;
|
|
148
|
-
/** A pending nudge of the task loop, armed when the request manager announces when it will have work again. */
|
|
149
|
-
#taskLoopWakeTimer;
|
|
150
|
-
/** When the pending wake-up is due, so an earlier one can replace a later one. */
|
|
151
|
-
#taskLoopWakeAt = 0;
|
|
152
154
|
/**
|
|
153
155
|
* A reference to the underlying {@link IProxyConfiguration} instance that manages the crawler's proxies.
|
|
154
156
|
* Only available if used by the crawler.
|
|
@@ -181,9 +183,7 @@ export class BasicCrawler {
|
|
|
181
183
|
}
|
|
182
184
|
return this.#contextPipeline;
|
|
183
185
|
}
|
|
184
|
-
running = false;
|
|
185
186
|
#hasFinishedBefore = false;
|
|
186
|
-
#unexpectedStop = false;
|
|
187
187
|
/** Whether a `run()` on this instance has already finished - a repeated one continues where it left off. */
|
|
188
188
|
get hasFinishedBefore() {
|
|
189
189
|
return this.#hasFinishedBefore;
|
|
@@ -223,8 +223,6 @@ export class BasicCrawler {
|
|
|
223
223
|
/** The resolved per-storage-type write policy overrides forwarded to each request's transaction. */
|
|
224
224
|
#storageWritePolicy;
|
|
225
225
|
#onSkippedRequest;
|
|
226
|
-
#closeEvents;
|
|
227
|
-
#loggedPerRun = new Set();
|
|
228
226
|
#robotsTxtFileCache;
|
|
229
227
|
#identity;
|
|
230
228
|
#contextPipelineOptions;
|
|
@@ -497,12 +495,12 @@ export class BasicCrawler {
|
|
|
497
495
|
},
|
|
498
496
|
isTaskReadyFunction: async () => {
|
|
499
497
|
if (isMaxPagesExceeded()) {
|
|
500
|
-
this
|
|
498
|
+
this.#liveRun?.logOnce('shuttingDown', 'Crawler reached the maxRequestsPerCrawl limit of ' +
|
|
501
499
|
`${this.#maxRequestsPerCrawl} requests and will shut down soon. Requests that are in progress will be allowed to finish.`);
|
|
502
500
|
return false;
|
|
503
501
|
}
|
|
504
|
-
if (this.#
|
|
505
|
-
this
|
|
502
|
+
if (this.#run?.stopRequested) {
|
|
503
|
+
this.#liveRun?.logOnce('shuttingDown', 'No new requests are allowed because the `stop()` method has been called. ' +
|
|
506
504
|
'Ongoing requests will be allowed to complete.');
|
|
507
505
|
return false;
|
|
508
506
|
}
|
|
@@ -515,7 +513,7 @@ export class BasicCrawler {
|
|
|
515
513
|
`In total, the crawler processed ${this.handledRequestsCount} requests and will shut down.`);
|
|
516
514
|
return true;
|
|
517
515
|
}
|
|
518
|
-
if (this.#
|
|
516
|
+
if (this.#run?.stopRequested) {
|
|
519
517
|
this.log.info('The crawler has finished all the remaining ongoing requests and will shut down now.');
|
|
520
518
|
return true;
|
|
521
519
|
}
|
|
@@ -619,6 +617,15 @@ export class BasicCrawler {
|
|
|
619
617
|
registerDeferredCleanup: (cleanup) => {
|
|
620
618
|
deferredCleanup.push(cleanup);
|
|
621
619
|
},
|
|
620
|
+
afterStorageCommit: (callback) => {
|
|
621
|
+
const transaction = currentStorageTransaction();
|
|
622
|
+
if (transaction === undefined) {
|
|
623
|
+
throw new Error('afterStorageCommit() needs an active storage transaction, and there is none. ' +
|
|
624
|
+
"With `transactionalStorage: false` the request's writes are applied as they are " +
|
|
625
|
+
'made, so they throw at the call site instead of at commit time.');
|
|
626
|
+
}
|
|
627
|
+
transaction.afterCommit(callback);
|
|
628
|
+
},
|
|
622
629
|
extendTimeout: (secs) => {
|
|
623
630
|
const extraMillis = secs * 1000;
|
|
624
631
|
// the current `addTimeoutToPromise` window (the request handler, or a navigation hook)...
|
|
@@ -804,18 +811,29 @@ export class BasicCrawler {
|
|
|
804
811
|
await pool.resetStore();
|
|
805
812
|
});
|
|
806
813
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
+
// An owned governor is rebuilt for every run, so it always starts from a clean slate — stale
|
|
815
|
+
// resource snapshots or a previous run's scaled desired concurrency would otherwise distort this
|
|
816
|
+
// run's scaling. An injected one is long-lived and its lifecycle belongs to the caller.
|
|
817
|
+
this.#concurrencySystem = this.#resolveConcurrencySystem();
|
|
818
|
+
const run = new CrawlerRun({
|
|
819
|
+
log: this.log,
|
|
820
|
+
concurrencySystem: this.#concurrencySystem.value,
|
|
821
|
+
taskLoopOptions: this.#taskLoopOptions,
|
|
822
|
+
consumer: this.#identity,
|
|
814
823
|
});
|
|
815
|
-
|
|
816
|
-
await this.addRequests(requests, options);
|
|
817
|
-
}
|
|
824
|
+
this.#run = run;
|
|
818
825
|
try {
|
|
826
|
+
await purgeDefaultStorages({
|
|
827
|
+
onlyPurgeOnce: true,
|
|
828
|
+
storageBackend: serviceLocator.getStorageBackend(),
|
|
829
|
+
configuration: serviceLocator.getConfiguration(),
|
|
830
|
+
});
|
|
831
|
+
if (requests) {
|
|
832
|
+
await this.addRequests(requests, options);
|
|
833
|
+
}
|
|
834
|
+
// An injected governor is the caller's to run; an owned one is started here and stopped again
|
|
835
|
+
// below if the startup fails. Either way the task loop rejects one that is not running.
|
|
836
|
+
await this.#concurrencySystem.ifOwned((system) => system.start());
|
|
819
837
|
await this.init();
|
|
820
838
|
await this.statistics.startCapturing();
|
|
821
839
|
}
|
|
@@ -825,7 +843,8 @@ export class BasicCrawler {
|
|
|
825
843
|
this.log.exception(teardownError, 'Cleaning up after a failed crawler startup failed.');
|
|
826
844
|
});
|
|
827
845
|
// The run never began, so let the instance be run again instead of leaving it wedged as `running`.
|
|
828
|
-
|
|
846
|
+
await run.finish();
|
|
847
|
+
await this.#concurrencySystem.ifOwned((system) => system.stop());
|
|
829
848
|
throw error;
|
|
830
849
|
}
|
|
831
850
|
const periodicLogger = this.getPeriodicLogger();
|
|
@@ -833,7 +852,7 @@ export class BasicCrawler {
|
|
|
833
852
|
const sigintHandler = async () => {
|
|
834
853
|
this.log.warning('Pausing... Press CTRL+C again to force exit. To resume, do: CRAWLEE_PURGE_ON_START=0 npm start');
|
|
835
854
|
await this.pauseOnMigration();
|
|
836
|
-
await
|
|
855
|
+
await run.abort();
|
|
837
856
|
};
|
|
838
857
|
// Attach a listener to handle migration and aborting events gracefully.
|
|
839
858
|
const boundPauseOnMigration = this.pauseOnMigration.bind(this);
|
|
@@ -843,7 +862,7 @@ export class BasicCrawler {
|
|
|
843
862
|
eventManager.on(EventType.ABORTING, boundPauseOnMigration);
|
|
844
863
|
let stats = {};
|
|
845
864
|
try {
|
|
846
|
-
await
|
|
865
|
+
await run.dispatchRequests();
|
|
847
866
|
}
|
|
848
867
|
finally {
|
|
849
868
|
await this.statistics.stopCapturing();
|
|
@@ -896,7 +915,8 @@ export class BasicCrawler {
|
|
|
896
915
|
}
|
|
897
916
|
periodicLogger.stop();
|
|
898
917
|
this.setStatusMessage(`Finished! Total ${this.statistics.state.requestsFinished + this.statistics.state.requestsFailed} requests: ${this.statistics.state.requestsFinished} succeeded, ${this.statistics.state.requestsFailed} failed.`, { isStatusMessageTerminal: true, level: 'INFO' });
|
|
899
|
-
|
|
918
|
+
await run.finish();
|
|
919
|
+
await this.#concurrencySystem.ifOwned((system) => system.stop());
|
|
900
920
|
this.#hasFinishedBefore = true;
|
|
901
921
|
}
|
|
902
922
|
return stats;
|
|
@@ -910,11 +930,12 @@ export class BasicCrawler {
|
|
|
910
930
|
* To stop the crawler immediately, use {@link BasicCrawler.teardown|`crawler.teardown()`} instead.
|
|
911
931
|
*/
|
|
912
932
|
stop(reason = 'The crawler has been gracefully stopped.') {
|
|
913
|
-
|
|
933
|
+
const run = this.#liveRun;
|
|
934
|
+
if (run === undefined) {
|
|
935
|
+
this.log.warning('Cannot stop a crawler that is not running.');
|
|
914
936
|
return;
|
|
915
937
|
}
|
|
916
|
-
|
|
917
|
-
this.#unexpectedStop = true;
|
|
938
|
+
run.stop(reason);
|
|
918
939
|
}
|
|
919
940
|
/**
|
|
920
941
|
* Stops dispatching new requests, letting the in-progress ones finish. Resolves once they have settled, or rejects
|
|
@@ -925,22 +946,24 @@ export class BasicCrawler {
|
|
|
925
946
|
* throughout, since a shared one may still be serving other crawlers.
|
|
926
947
|
*/
|
|
927
948
|
async pause(timeoutSecs) {
|
|
928
|
-
|
|
949
|
+
const run = this.#dispatchingRun;
|
|
950
|
+
if (run === undefined) {
|
|
929
951
|
this.log.warning('Cannot pause a crawler that is not running.');
|
|
930
952
|
return;
|
|
931
953
|
}
|
|
932
|
-
await
|
|
954
|
+
await run.pause(timeoutSecs);
|
|
933
955
|
}
|
|
934
956
|
/**
|
|
935
957
|
* Resumes a run suspended with {@link BasicCrawler.pause|`pause()`}, letting the crawler dispatch requests
|
|
936
958
|
* again. A no-op on a crawler that is not paused.
|
|
937
959
|
*/
|
|
938
960
|
resume() {
|
|
939
|
-
|
|
961
|
+
const run = this.#dispatchingRun;
|
|
962
|
+
if (run === undefined) {
|
|
940
963
|
this.log.warning('Cannot resume a crawler that is not running.');
|
|
941
964
|
return;
|
|
942
965
|
}
|
|
943
|
-
|
|
966
|
+
run.resume();
|
|
944
967
|
}
|
|
945
968
|
/**
|
|
946
969
|
* Returns the crawler's {@link IRequestManager|request manager}, opening the default {@link RequestQueue}
|
|
@@ -1048,22 +1071,18 @@ export class BasicCrawler {
|
|
|
1048
1071
|
// A skipped request is a *successful* outcome, but the interrupt still unwinds through the
|
|
1049
1072
|
// transaction scope, which rolls back - so the skip bookkeeping must write directly.
|
|
1050
1073
|
await withDirectStorageAccess(async () => {
|
|
1074
|
+
// Enqueueing happens outside a run too - `addRequests()` before the first `run()` - so these dedupe
|
|
1075
|
+
// against the logger for the crawler's lifetime rather than per run.
|
|
1051
1076
|
if (options.reason === 'limit') {
|
|
1052
|
-
this.
|
|
1053
|
-
`${this.#maxRequestsPerCrawl} requests and no further requests will be added
|
|
1077
|
+
this.log.info('The number of requests enqueued by the crawler reached the maxRequestsPerCrawl limit of ' +
|
|
1078
|
+
`${this.#maxRequestsPerCrawl} requests and no further requests will be added.`, undefined, { once: true });
|
|
1054
1079
|
}
|
|
1055
1080
|
if (options.reason === 'depth') {
|
|
1056
|
-
this.
|
|
1081
|
+
this.log.info(`The crawler reached the maxCrawlDepth limit of ${this.#maxCrawlDepth} and no further requests will be enqueued.`, undefined, { once: true });
|
|
1057
1082
|
}
|
|
1058
1083
|
await this.#onSkippedRequest?.(options);
|
|
1059
1084
|
});
|
|
1060
1085
|
}
|
|
1061
|
-
logOncePerRun(key, message, level = 'info') {
|
|
1062
|
-
if (!this.#loggedPerRun.has(key)) {
|
|
1063
|
-
this.log[level](message);
|
|
1064
|
-
this.#loggedPerRun.add(key);
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
1086
|
/**
|
|
1068
1087
|
* Adds requests to the queue in batches. By default, it will resolve after the initial batch is added, and continue
|
|
1069
1088
|
* adding the rest in background. You can configure the batch size via `batchSize` option and the sleep time in between
|
|
@@ -1268,10 +1287,14 @@ export class BasicCrawler {
|
|
|
1268
1287
|
* Initializes the crawler.
|
|
1269
1288
|
*/
|
|
1270
1289
|
async init() {
|
|
1290
|
+
const run = this.#liveRun;
|
|
1291
|
+
if (run === undefined) {
|
|
1292
|
+
throw new Error('There is no crawl in progress - `run()` is the only thing that starts one.');
|
|
1293
|
+
}
|
|
1271
1294
|
const eventManager = serviceLocator.getEventManager();
|
|
1272
1295
|
if (!eventManager.isInitialized()) {
|
|
1273
1296
|
await eventManager.init();
|
|
1274
|
-
|
|
1297
|
+
run.ownsEventManager = true;
|
|
1275
1298
|
}
|
|
1276
1299
|
// Warn once at startup if the internal timeout is shorter than the phases it is meant to outlast. It is
|
|
1277
1300
|
// floored per request so it will not actually cut them short, but the configured value is then effectively
|
|
@@ -1283,16 +1306,6 @@ export class BasicCrawler {
|
|
|
1283
1306
|
`and request handler timeouts combined (${phasesMillis / 1000}s); it will be raised per request ` +
|
|
1284
1307
|
`so it does not cut them short.`);
|
|
1285
1308
|
}
|
|
1286
|
-
// An owned governor is rebuilt (and started) for every run, so it always starts from a clean slate — stale
|
|
1287
|
-
// resource snapshots or a previous run's scaled desired concurrency would otherwise distort this run's
|
|
1288
|
-
// scaling. An injected one is long-lived and its lifecycle belongs to the caller.
|
|
1289
|
-
this.#concurrencySystemDep = this.#resolveConcurrencySystem();
|
|
1290
|
-
await this.#concurrencySystemDep.ifOwned((system) => system.start());
|
|
1291
|
-
this.#autoscaledPool = new AutoscaledPool({
|
|
1292
|
-
...this.#taskLoopOptions,
|
|
1293
|
-
concurrencySystem: this.#concurrencySystemDep.value,
|
|
1294
|
-
consumer: this.#identity,
|
|
1295
|
-
});
|
|
1296
1309
|
await this.getRequestManager();
|
|
1297
1310
|
}
|
|
1298
1311
|
/**
|
|
@@ -1415,10 +1428,10 @@ export class BasicCrawler {
|
|
|
1415
1428
|
return true;
|
|
1416
1429
|
}
|
|
1417
1430
|
const domain = hostnameOrUrl(url);
|
|
1418
|
-
this.
|
|
1431
|
+
this.log.warningOnce(`"${domain}" responded with HTTP 429 (Too Many Requests), but the crawler's request manager does not ` +
|
|
1419
1432
|
'pace that domain, so the response is handled like any other, with no per-domain delay. Set ' +
|
|
1420
1433
|
`\`sameDomainDelaySecs\`, or pass a \`ThrottlingRequestManager\` covering "${domain}" as ` +
|
|
1421
|
-
'`requestManager`, to honour `Retry-After` and apply exponential backoff instead.'
|
|
1434
|
+
'`requestManager`, to honour `Retry-After` and apply exponential backoff instead.');
|
|
1422
1435
|
return false;
|
|
1423
1436
|
}
|
|
1424
1437
|
/** Hands a robots.txt `Crawl-delay` to the request manager, warning if it will not be honoured. */
|
|
@@ -1434,10 +1447,10 @@ export class BasicCrawler {
|
|
|
1434
1447
|
return;
|
|
1435
1448
|
}
|
|
1436
1449
|
const domain = hostnameOrUrl(url);
|
|
1437
|
-
this.
|
|
1450
|
+
this.log.warningOnce(`robots.txt for "${domain}" defines a crawl-delay of ${delaySeconds}s, but the crawler's request ` +
|
|
1438
1451
|
'manager does not pace that domain, so its requests will not be paced. Set ' +
|
|
1439
1452
|
`\`sameDomainDelaySecs\`, or pass a \`ThrottlingRequestManager\` covering "${domain}" as ` +
|
|
1440
|
-
'`requestManager`.'
|
|
1453
|
+
'`requestManager`.');
|
|
1441
1454
|
}
|
|
1442
1455
|
async getRobotsTxtFileForUrl(url) {
|
|
1443
1456
|
if (!this.#respectRobotsTxtFile) {
|
|
@@ -1459,9 +1472,9 @@ export class BasicCrawler {
|
|
|
1459
1472
|
}
|
|
1460
1473
|
}
|
|
1461
1474
|
async pauseOnMigration() {
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
await
|
|
1475
|
+
const run = this.#dispatchingRun;
|
|
1476
|
+
if (run) {
|
|
1477
|
+
await run.pause(SAFE_MIGRATION_WAIT_MILLIS).catch((err) => {
|
|
1465
1478
|
if (err.message.includes('running tasks did not finish')) {
|
|
1466
1479
|
this.log.error('The crawler was paused due to migration to another host, ' +
|
|
1467
1480
|
"but some requests did not finish in time. Those requests' results may be duplicated.");
|
|
@@ -1615,36 +1628,10 @@ export class BasicCrawler {
|
|
|
1615
1628
|
}
|
|
1616
1629
|
const state = await this.requestManager.checkReadiness();
|
|
1617
1630
|
if (state.status === 'waiting' && state.readyAt !== undefined) {
|
|
1618
|
-
this.#
|
|
1631
|
+
this.#run?.scheduleWake(state.readyAt);
|
|
1619
1632
|
}
|
|
1620
1633
|
return state.status === 'ready';
|
|
1621
1634
|
}
|
|
1622
|
-
/**
|
|
1623
|
-
* Nudges the task loop at `readyAt`, on a single timer that only an earlier one replaces. The pool polls anyway
|
|
1624
|
-
* every `maybeRunIntervalSecs` (0.5s by default), so this only shortens the wait - hence one timer rather than
|
|
1625
|
-
* one per probe, and `unref`'d so it never keeps the process alive.
|
|
1626
|
-
*/
|
|
1627
|
-
#scheduleTaskLoopWake(readyAt) {
|
|
1628
|
-
if (this.#taskLoopWakeTimer !== undefined) {
|
|
1629
|
-
if (this.#taskLoopWakeAt <= readyAt) {
|
|
1630
|
-
return;
|
|
1631
|
-
}
|
|
1632
|
-
clearTimeout(this.#taskLoopWakeTimer);
|
|
1633
|
-
}
|
|
1634
|
-
this.#taskLoopWakeAt = readyAt;
|
|
1635
|
-
this.#taskLoopWakeTimer = setTimeout(() => {
|
|
1636
|
-
this.#taskLoopWakeTimer = undefined;
|
|
1637
|
-
void this.#autoscaledPool?.notify();
|
|
1638
|
-
}, Math.max(0, readyAt - Date.now()));
|
|
1639
|
-
this.#taskLoopWakeTimer.unref();
|
|
1640
|
-
}
|
|
1641
|
-
/** Drops a pending task-loop wake-up, so a finished run leaves no timer behind. */
|
|
1642
|
-
#clearTaskLoopWake() {
|
|
1643
|
-
if (this.#taskLoopWakeTimer !== undefined) {
|
|
1644
|
-
clearTimeout(this.#taskLoopWakeTimer);
|
|
1645
|
-
this.#taskLoopWakeTimer = undefined;
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
1635
|
/**
|
|
1649
1636
|
* Unwraps errors thrown by the context pipeline to get the actual user error.
|
|
1650
1637
|
* RequestHandlerError and ContextPipelineInitializationError wrap the actual error.
|
|
@@ -1770,28 +1757,42 @@ export class BasicCrawler {
|
|
|
1770
1757
|
return request.retryCount < maxRequestRetries;
|
|
1771
1758
|
}
|
|
1772
1759
|
/**
|
|
1773
|
-
*
|
|
1760
|
+
* Ends the run in progress immediately, without waiting for the requests already in flight.
|
|
1774
1761
|
*
|
|
1775
|
-
* This
|
|
1762
|
+
* This runs at the end of every {@link BasicCrawler.run|`run()`}, so what it releases is per-run only, and
|
|
1763
|
+
* a repeated `run()` has to find everything it needs either intact or re-establishable. What outlives a run is
|
|
1764
|
+
* released by {@link BasicCrawler.destroy|`destroy()`} instead.
|
|
1776
1765
|
*
|
|
1777
1766
|
* To stop the crawler gracefully (waiting for all running requests to finish), use {@link BasicCrawler.stop|`crawler.stop()`} instead.
|
|
1778
1767
|
*/
|
|
1779
1768
|
async teardown() {
|
|
1769
|
+
const eventManager = serviceLocator.getEventManager();
|
|
1770
|
+
const ownsEventManager = this.#run?.ownsEventManager === true;
|
|
1780
1771
|
// First, so that the pool writes its final state once - its teardown stops it listening before the
|
|
1781
1772
|
// persistence event below goes out.
|
|
1782
1773
|
await this.#sessionPoolDep.ifOwned(async (pool) => pool.teardown());
|
|
1783
1774
|
// When this crawler initialized the event manager, its close() call emits
|
|
1784
1775
|
// the final persistence event after the crawler-specific state has been
|
|
1785
1776
|
// saved. External event managers still need an explicit event here.
|
|
1786
|
-
if (!
|
|
1787
|
-
|
|
1777
|
+
if (!ownsEventManager) {
|
|
1778
|
+
eventManager.emit(EventType.PERSIST_STATE, { isMigrating: false });
|
|
1788
1779
|
}
|
|
1789
|
-
if (
|
|
1790
|
-
await
|
|
1780
|
+
if (ownsEventManager) {
|
|
1781
|
+
await eventManager.close();
|
|
1791
1782
|
}
|
|
1792
|
-
this.#
|
|
1793
|
-
|
|
1794
|
-
|
|
1783
|
+
await this.#run?.end();
|
|
1784
|
+
}
|
|
1785
|
+
/**
|
|
1786
|
+
* Releases what the crawler owns beyond a single run — in the browser crawlers, the browser pool. A finished
|
|
1787
|
+
* `run()` has already released everything a run owns, so this is only needed before dropping a crawler you
|
|
1788
|
+
* could otherwise have run again.
|
|
1789
|
+
*/
|
|
1790
|
+
async destroy() {
|
|
1791
|
+
// An abandoned run - one whose `run()` never returned - would otherwise keep dispatching requests.
|
|
1792
|
+
await this.#run?.end();
|
|
1793
|
+
}
|
|
1794
|
+
async [Symbol.asyncDispose]() {
|
|
1795
|
+
await this.destroy();
|
|
1795
1796
|
}
|
|
1796
1797
|
getCookieHeaderFromRequest(request) {
|
|
1797
1798
|
if (request.headers?.Cookie && request.headers?.cookie) {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { AutoscaledPoolOptions, CrawleeLogger, IConcurrencySystem } from '@crawlee/core';
|
|
2
|
+
/** Everything a run needs to exist. Whatever a crawler resolves per run is resolved before the run is built. */
|
|
3
|
+
export interface CrawlerRunSetup {
|
|
4
|
+
log: CrawleeLogger;
|
|
5
|
+
/** The governor this run books its requests against, started and stopped by whoever owns it. */
|
|
6
|
+
concurrencySystem: IConcurrencySystem;
|
|
7
|
+
taskLoopOptions: Omit<AutoscaledPoolOptions, 'concurrencySystem' | 'consumer'>;
|
|
8
|
+
consumer: AutoscaledPoolOptions['consumer'];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Everything that belongs to a single {@link BasicCrawler.run|`crawler.run()`} — the task loop dispatching its
|
|
12
|
+
* requests, the concurrency governor those requests are booked against, and the bookkeeping that must not survive
|
|
13
|
+
* into a later run.
|
|
14
|
+
*
|
|
15
|
+
* The crawler builds a fresh one for every run, which is what keeps per-run state from leaking across runs: there
|
|
16
|
+
* is no reset checklist to keep in sync. A finished run stays readable as the history of the last crawl, so
|
|
17
|
+
* liveness is asked about explicitly (`isLive`) rather than inferred from presence.
|
|
18
|
+
*
|
|
19
|
+
* The lifecycle is `dispatchRequests()` and `finish()`. Everything a run owns exists from construction, so there
|
|
20
|
+
* is no half-built state to guard against in between.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
export declare class CrawlerRun {
|
|
25
|
+
#private;
|
|
26
|
+
/** Whether this run initialized the ambient event manager, and is therefore the one that closes it. */
|
|
27
|
+
ownsEventManager: boolean;
|
|
28
|
+
constructor(setup: CrawlerRunSetup);
|
|
29
|
+
/**
|
|
30
|
+
* Runs the task loop until the crawl is over or {@link CrawlerRun.abort|aborted}. The governor has to be
|
|
31
|
+
* running by now — the task loop refuses to book anything against one that is not.
|
|
32
|
+
*/
|
|
33
|
+
dispatchRequests(): Promise<void>;
|
|
34
|
+
/** Whether the `run()` call this belongs to is still executing. */
|
|
35
|
+
get isLive(): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Whether the task loop is dispatching requests. Pausing one that has not started would not suspend a crawl,
|
|
38
|
+
* it would keep it from ever starting.
|
|
39
|
+
*/
|
|
40
|
+
get isDispatching(): boolean;
|
|
41
|
+
/** Whether a graceful shutdown of this run has been requested. */
|
|
42
|
+
get stopRequested(): boolean;
|
|
43
|
+
/** Asks the run to stop taking new requests. Only the first call logs `reason`. */
|
|
44
|
+
stop(reason: string): void;
|
|
45
|
+
/** Stops dispatching new requests, resolving once the ones in flight have settled. */
|
|
46
|
+
pause(timeoutSecs?: number): Promise<void>;
|
|
47
|
+
/** Resumes dispatching after a {@link CrawlerRun.pause|`pause()`}. */
|
|
48
|
+
resume(): void;
|
|
49
|
+
/** Ends the crawl without waiting for the requests in flight, so a pending `crawl()` resolves. */
|
|
50
|
+
abort(): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Logs `message` the first time this run asks for it under `key`. Keyed rather than deduped by text, so that
|
|
53
|
+
* two messages describing the same shutdown say it once between them.
|
|
54
|
+
*/
|
|
55
|
+
logOnce(key: string, message: string): void;
|
|
56
|
+
/**
|
|
57
|
+
* Nudges the task loop at `readyAt`, when the request manager says it will have work again. The loop polls on
|
|
58
|
+
* its own anyway, so this only shortens the wait — hence a single timer that only an earlier wake-up replaces,
|
|
59
|
+
* `unref`'d so it never keeps the process alive. A run that has ended arms nothing.
|
|
60
|
+
*/
|
|
61
|
+
scheduleWake(readyAt: number): void;
|
|
62
|
+
/**
|
|
63
|
+
* Ends the run: drops a pending wake-up and aborts the task loop. Idempotent, so aborting a run through
|
|
64
|
+
* `teardown()` and then finishing it does the work once. The governor is left alone — it belongs to whoever
|
|
65
|
+
* started it.
|
|
66
|
+
*/
|
|
67
|
+
end(): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Ends the run and marks it as history: the crawler stops reporting itself as running, and what is left is
|
|
70
|
+
* readable but no longer drivable. Ending here too means a subclass `teardown()` that forgets `super` cannot
|
|
71
|
+
* leave the task loop dispatching.
|
|
72
|
+
*/
|
|
73
|
+
finish(): Promise<void>;
|
|
74
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { AutoscaledPool } from '@crawlee/core';
|
|
2
|
+
/**
|
|
3
|
+
* Everything that belongs to a single {@link BasicCrawler.run|`crawler.run()`} — the task loop dispatching its
|
|
4
|
+
* requests, the concurrency governor those requests are booked against, and the bookkeeping that must not survive
|
|
5
|
+
* into a later run.
|
|
6
|
+
*
|
|
7
|
+
* The crawler builds a fresh one for every run, which is what keeps per-run state from leaking across runs: there
|
|
8
|
+
* is no reset checklist to keep in sync. A finished run stays readable as the history of the last crawl, so
|
|
9
|
+
* liveness is asked about explicitly (`isLive`) rather than inferred from presence.
|
|
10
|
+
*
|
|
11
|
+
* The lifecycle is `dispatchRequests()` and `finish()`. Everything a run owns exists from construction, so there
|
|
12
|
+
* is no half-built state to guard against in between.
|
|
13
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
export class CrawlerRun {
|
|
17
|
+
#log;
|
|
18
|
+
#autoscaledPool;
|
|
19
|
+
#loggedOnce = new Set();
|
|
20
|
+
#stopRequested = false;
|
|
21
|
+
#dispatching = false;
|
|
22
|
+
#ended = false;
|
|
23
|
+
#finished = false;
|
|
24
|
+
#wakeTimer;
|
|
25
|
+
#wakeAt = 0;
|
|
26
|
+
/** Whether this run initialized the ambient event manager, and is therefore the one that closes it. */
|
|
27
|
+
ownsEventManager = false;
|
|
28
|
+
constructor(setup) {
|
|
29
|
+
this.#log = setup.log;
|
|
30
|
+
this.#autoscaledPool = new AutoscaledPool({
|
|
31
|
+
...setup.taskLoopOptions,
|
|
32
|
+
concurrencySystem: setup.concurrencySystem,
|
|
33
|
+
consumer: setup.consumer,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Runs the task loop until the crawl is over or {@link CrawlerRun.abort|aborted}. The governor has to be
|
|
38
|
+
* running by now — the task loop refuses to book anything against one that is not.
|
|
39
|
+
*/
|
|
40
|
+
async dispatchRequests() {
|
|
41
|
+
this.#dispatching = true;
|
|
42
|
+
try {
|
|
43
|
+
await this.#autoscaledPool.run();
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
this.#dispatching = false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Whether the `run()` call this belongs to is still executing. */
|
|
50
|
+
get isLive() {
|
|
51
|
+
return !this.#finished;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Whether the task loop is dispatching requests. Pausing one that has not started would not suspend a crawl,
|
|
55
|
+
* it would keep it from ever starting.
|
|
56
|
+
*/
|
|
57
|
+
get isDispatching() {
|
|
58
|
+
return this.#dispatching;
|
|
59
|
+
}
|
|
60
|
+
/** Whether a graceful shutdown of this run has been requested. */
|
|
61
|
+
get stopRequested() {
|
|
62
|
+
return this.#stopRequested;
|
|
63
|
+
}
|
|
64
|
+
/** Asks the run to stop taking new requests. Only the first call logs `reason`. */
|
|
65
|
+
stop(reason) {
|
|
66
|
+
if (this.#stopRequested) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
this.#log.info(reason);
|
|
70
|
+
this.#stopRequested = true;
|
|
71
|
+
}
|
|
72
|
+
/** Stops dispatching new requests, resolving once the ones in flight have settled. */
|
|
73
|
+
async pause(timeoutSecs) {
|
|
74
|
+
await this.#autoscaledPool.pause(timeoutSecs);
|
|
75
|
+
}
|
|
76
|
+
/** Resumes dispatching after a {@link CrawlerRun.pause|`pause()`}. */
|
|
77
|
+
resume() {
|
|
78
|
+
this.#autoscaledPool.resume();
|
|
79
|
+
}
|
|
80
|
+
/** Ends the crawl without waiting for the requests in flight, so a pending `crawl()` resolves. */
|
|
81
|
+
async abort() {
|
|
82
|
+
await this.#autoscaledPool.abort();
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Logs `message` the first time this run asks for it under `key`. Keyed rather than deduped by text, so that
|
|
86
|
+
* two messages describing the same shutdown say it once between them.
|
|
87
|
+
*/
|
|
88
|
+
logOnce(key, message) {
|
|
89
|
+
if (this.#loggedOnce.has(key)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
this.#log.info(message);
|
|
93
|
+
this.#loggedOnce.add(key);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Nudges the task loop at `readyAt`, when the request manager says it will have work again. The loop polls on
|
|
97
|
+
* its own anyway, so this only shortens the wait — hence a single timer that only an earlier wake-up replaces,
|
|
98
|
+
* `unref`'d so it never keeps the process alive. A run that has ended arms nothing.
|
|
99
|
+
*/
|
|
100
|
+
scheduleWake(readyAt) {
|
|
101
|
+
if (this.#ended) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (this.#wakeTimer !== undefined) {
|
|
105
|
+
if (this.#wakeAt <= readyAt) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
clearTimeout(this.#wakeTimer);
|
|
109
|
+
}
|
|
110
|
+
this.#wakeAt = readyAt;
|
|
111
|
+
this.#wakeTimer = setTimeout(() => {
|
|
112
|
+
this.#wakeTimer = undefined;
|
|
113
|
+
void this.#autoscaledPool.notify();
|
|
114
|
+
}, Math.max(0, readyAt - Date.now()));
|
|
115
|
+
this.#wakeTimer.unref();
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Ends the run: drops a pending wake-up and aborts the task loop. Idempotent, so aborting a run through
|
|
119
|
+
* `teardown()` and then finishing it does the work once. The governor is left alone — it belongs to whoever
|
|
120
|
+
* started it.
|
|
121
|
+
*/
|
|
122
|
+
async end() {
|
|
123
|
+
if (this.#ended) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
this.#ended = true;
|
|
127
|
+
if (this.#wakeTimer !== undefined) {
|
|
128
|
+
clearTimeout(this.#wakeTimer);
|
|
129
|
+
this.#wakeTimer = undefined;
|
|
130
|
+
}
|
|
131
|
+
await this.#autoscaledPool.abort();
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Ends the run and marks it as history: the crawler stops reporting itself as running, and what is left is
|
|
135
|
+
* readable but no longer drivable. Ending here too means a subclass `teardown()` that forgets `super` cannot
|
|
136
|
+
* leave the task loop dispatching.
|
|
137
|
+
*/
|
|
138
|
+
async finish() {
|
|
139
|
+
await this.end();
|
|
140
|
+
this.#finished = true;
|
|
141
|
+
}
|
|
142
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/basic",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.167",
|
|
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": ">=22.0.0"
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"@apify/datastructures": "^3.0.1",
|
|
43
43
|
"@apify/timeout": "^1.0.1",
|
|
44
44
|
"@apify/utilities": "^3.0.1",
|
|
45
|
-
"@crawlee/core": "4.0.0-beta.
|
|
46
|
-
"@crawlee/http-client": "4.0.0-beta.
|
|
47
|
-
"@crawlee/types": "4.0.0-beta.
|
|
48
|
-
"@crawlee/utils": "4.0.0-beta.
|
|
45
|
+
"@crawlee/core": "4.0.0-beta.167",
|
|
46
|
+
"@crawlee/http-client": "4.0.0-beta.167",
|
|
47
|
+
"@crawlee/types": "4.0.0-beta.167",
|
|
48
|
+
"@crawlee/utils": "4.0.0-beta.167",
|
|
49
49
|
"csv-stringify": "^6.5.2",
|
|
50
50
|
"tldts": "^7.0.6",
|
|
51
51
|
"tslib": "^2.8.1",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"zod": "^4.5.4"
|
|
54
54
|
},
|
|
55
55
|
"optionalDependencies": {
|
|
56
|
-
"@crawlee/impit-client": "4.0.0-beta.
|
|
56
|
+
"@crawlee/impit-client": "4.0.0-beta.167"
|
|
57
57
|
},
|
|
58
58
|
"lerna": {
|
|
59
59
|
"command": {
|
|
@@ -62,5 +62,5 @@
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
},
|
|
65
|
-
"gitHead": "
|
|
65
|
+
"gitHead": "8b26080bf07dc602aa0970e12fb66e7e9cba509b"
|
|
66
66
|
}
|