@nitpicker/crawler 0.15.0 → 0.16.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 (50) hide show
  1. package/lib/archive/archive.d.ts +39 -1
  2. package/lib/archive/archive.js +49 -0
  3. package/lib/archive/create-adjunct-tables.d.ts +3 -0
  4. package/lib/archive/create-adjunct-tables.js +42 -0
  5. package/lib/archive/database.d.ts +31 -1
  6. package/lib/archive/database.js +42 -0
  7. package/lib/archive/db-ops/dedupe-cap/accumulate-dedupe-cap-rejected-count.d.ts +18 -0
  8. package/lib/archive/db-ops/dedupe-cap/accumulate-dedupe-cap-rejected-count.js +23 -0
  9. package/lib/archive/db-ops/dedupe-cap/finalize-dedupe-cap-event.d.ts +12 -0
  10. package/lib/archive/db-ops/dedupe-cap/finalize-dedupe-cap-event.js +15 -0
  11. package/lib/archive/db-ops/dedupe-cap/insert-dedupe-cap-event.d.ts +14 -0
  12. package/lib/archive/db-ops/dedupe-cap/insert-dedupe-cap-event.js +30 -0
  13. package/lib/archive/db-ops/dedupe-cap/list-dedupe-cap-shape-keys.d.ts +21 -0
  14. package/lib/archive/db-ops/dedupe-cap/list-dedupe-cap-shape-keys.js +27 -0
  15. package/lib/archive/types.d.ts +13 -0
  16. package/lib/classify-error-kind.d.ts +1 -0
  17. package/lib/classify-error-kind.js +14 -0
  18. package/lib/crawler/assert-chrome-installed.d.ts +24 -0
  19. package/lib/crawler/assert-chrome-installed.js +43 -0
  20. package/lib/crawler/crawler.d.ts +12 -0
  21. package/lib/crawler/crawler.js +239 -29
  22. package/lib/crawler/decode-auth-credential.d.ts +29 -0
  23. package/lib/crawler/decode-auth-credential.js +39 -0
  24. package/lib/crawler/dedupe/compute-meta-signature.d.ts +30 -0
  25. package/lib/crawler/dedupe/compute-meta-signature.js +0 -0
  26. package/lib/crawler/dedupe/compute-shape-key.d.ts +37 -0
  27. package/lib/crawler/dedupe/compute-shape-key.js +56 -0
  28. package/lib/crawler/dedupe/dedupe-cap-tracker.d.ts +84 -0
  29. package/lib/crawler/dedupe/dedupe-cap-tracker.js +185 -0
  30. package/lib/crawler/dedupe/is-predicted-content-duplicate.d.ts +24 -0
  31. package/lib/crawler/dedupe/is-predicted-content-duplicate.js +26 -0
  32. package/lib/crawler/dedupe/is-shape-capped.d.ts +10 -0
  33. package/lib/crawler/dedupe/is-shape-capped.js +12 -0
  34. package/lib/crawler/dedupe/resolve-og-url-mismatch.d.ts +31 -0
  35. package/lib/crawler/dedupe/resolve-og-url-mismatch.js +40 -0
  36. package/lib/crawler/dedupe/types.d.ts +42 -0
  37. package/lib/crawler/dedupe/types.js +1 -0
  38. package/lib/crawler/fetch-destination.js +14 -2
  39. package/lib/crawler/generate-predicted-urls.d.ts +12 -0
  40. package/lib/crawler/generate-predicted-urls.js +33 -2
  41. package/lib/crawler/is-puppeteer-fallback-candidate.js +3 -0
  42. package/lib/crawler/types.d.ts +38 -0
  43. package/lib/crawler-orchestrator.d.ts +12 -0
  44. package/lib/crawler-orchestrator.js +106 -1
  45. package/lib/crawler.d.ts +1 -0
  46. package/lib/crawler.js +1 -0
  47. package/lib/permanent-error-kinds.d.ts +9 -4
  48. package/lib/permanent-error-kinds.js +10 -4
  49. package/lib/types.d.ts +2 -1
  50. package/package.json +2 -2
@@ -190,6 +190,29 @@ export interface CrawlerOptions extends Required<Pick<ParseURLOptions, 'disableQ
190
190
  * without touching the real network.
191
191
  */
192
192
  networkProbe: NetworkProbe | null;
193
+ /**
194
+ * Same-cluster soft-cap threshold (`--dedupe-cap`), or `null` to disable
195
+ * the feature entirely (the default). When set, {@link Crawler} stops
196
+ * enqueueing newly-discovered URLs whose shape (see `computeShapeKey`)
197
+ * has accumulated this many matching-signature observations (see
198
+ * `DedupeCapTracker`).
199
+ */
200
+ dedupeCap: number | null;
201
+ /**
202
+ * Hard cap on the number of distinct URL shapes the same-cluster soft
203
+ * cap tracks at once (`--dedupe-map-cap`); the least-recently-touched
204
+ * shape is evicted beyond this. Only relevant when {@link dedupeCap} is
205
+ * non-null.
206
+ */
207
+ dedupeMapCap: number;
208
+ /**
209
+ * Shape keys already confirmed capped in a prior session
210
+ * (persisted as `dedupe_cap_events.shape_key`), seeded into the
211
+ * tracker's sticky set so `--resume` / `--append` / `--retry-failed` /
212
+ * `--inventory` do not re-admit a trap this crawl already paid the cost
213
+ * of discovering once. Ignored when {@link dedupeCap} is `null`.
214
+ */
215
+ preloadedStickyShapeKeys: readonly string[];
193
216
  }
194
217
  /**
195
218
  * Inventory-mode runtime configuration. Passed from
@@ -436,6 +459,21 @@ export interface CrawlerEventTypes {
436
459
  /** Epoch ms the recovery probe first succeeded. */
437
460
  endedAt: number;
438
461
  };
462
+ /**
463
+ * Emitted the instant the opt-in same-cluster soft cap
464
+ * ({@link CrawlerOptions.dedupeCap}) confirms a URL shape as a trap (see
465
+ * `DedupeCapTracker`). The orchestrator persists this via
466
+ * `Archive.insertDedupeCapEvent` and must remember the returned row id so
467
+ * `rejected_count` can be finalized once at `crawlEnd` (`Crawler` itself
468
+ * never touches the archive).
469
+ */
470
+ dedupeCap: {
471
+ shapeKey: string;
472
+ sampleUrl: string;
473
+ bodyHash: Buffer;
474
+ effectiveThreshold: number;
475
+ observedCount: number;
476
+ };
439
477
  }
440
478
  /**
441
479
  * Tunables for `NetworkOutageDetector`.
@@ -69,6 +69,18 @@ interface CrawlConfig extends Config {
69
69
  * `options` so an E2E test can inject it via the public API.
70
70
  */
71
71
  networkProbe: NetworkProbe | null;
72
+ /** See {@link CrawlerOptions.dedupeCap}. `null`/omitted disables the feature. */
73
+ dedupeCap: number | null;
74
+ /** See {@link CrawlerOptions.dedupeMapCap}. Omitted falls through to `Crawler`'s own default. */
75
+ dedupeMapCap: number;
76
+ /**
77
+ * See {@link CrawlerOptions.preloadedStickyShapeKeys}. Set internally by
78
+ * the four resuming-session static methods
79
+ * (`append`/`inventory`/`retryFailed`/`resume`), each independently
80
+ * calling `archive.listDedupeCapShapeKeys()`; not part of the public
81
+ * options a caller of those methods passes directly.
82
+ */
83
+ preloadedStickyShapeKeys: readonly string[];
72
84
  }
73
85
  /**
74
86
  * Callback invoked after the CrawlerOrchestrator instance is fully initialized
@@ -64,6 +64,15 @@ export class CrawlerOrchestrator extends EventEmitter {
64
64
  #archive;
65
65
  /** The crawler engine that discovers and scrapes pages. */
66
66
  #crawler;
67
+ /**
68
+ * `dedupe_cap_events.id` for each shape confirmed capped this session, so
69
+ * `crawlEnd` can look up the right row to finalize with
70
+ * `Crawler#getDedupeCapRejections`'s counts. A `Map` (not a single
71
+ * scalar like {@link #openNetworkOutageId}) because, unlike a network
72
+ * outage, more than one shape can be capped simultaneously within one
73
+ * crawl.
74
+ */
75
+ #dedupeCapEventIds = new Map();
67
76
  /** Whether the crawl was started from a pre-defined URL list (non-recursive mode). */
68
77
  #fromList;
69
78
  /**
@@ -158,6 +167,13 @@ export class CrawlerOrchestrator extends EventEmitter {
158
167
  networkOutageHostThreshold: options?.networkOutageHostThreshold,
159
168
  networkOutageProbeIntervalMs: options?.networkOutageProbeIntervalMs,
160
169
  networkProbe: options?.networkProbe ?? null,
170
+ dedupeCap: options?.dedupeCap ?? null,
171
+ dedupeMapCap: options?.dedupeMapCap,
172
+ // Only the four resuming-session static methods
173
+ // (`append`/`inventory`/`retryFailed`/`resume`) pass this — a
174
+ // fresh `crawling()` has no archive history to seed from (see
175
+ // `CrawlConfig.preloadedStickyShapeKeys`'s JSDoc).
176
+ preloadedStickyShapeKeys: options?.preloadedStickyShapeKeys ?? [],
161
177
  });
162
178
  }
163
179
  /**
@@ -298,6 +314,23 @@ export class CrawlerOrchestrator extends EventEmitter {
298
314
  })
299
315
  .catch((error) => reject(error));
300
316
  });
317
+ this.#crawler.on('dedupeCap', ({ shapeKey, sampleUrl, bodyHash, effectiveThreshold, observedCount }) => {
318
+ crawlerLog('Dedupe cap reached: shapeKey=%s effectiveThreshold=%d observedCount=%d', shapeKey, effectiveThreshold, observedCount);
319
+ console.error(`[dedupe-cap] same-cluster trap confirmed: ${shapeKey} (sample: ${sampleUrl})`);
320
+ writeQueue
321
+ .enqueue(async () => {
322
+ const id = await this.#archive.insertDedupeCapEvent({
323
+ shapeKey,
324
+ sampleUrl,
325
+ bodyHash,
326
+ effectiveThreshold,
327
+ observedCount,
328
+ detectedAt: Date.now(),
329
+ });
330
+ this.#dedupeCapEventIds.set(shapeKey, id);
331
+ })
332
+ .catch((error) => reject(error));
333
+ });
301
334
  this.#crawler.on('response', ({ resource, source }) => {
302
335
  writeQueue
303
336
  .enqueue(() => this.#archive.setResources(resource, source))
@@ -314,6 +347,51 @@ export class CrawlerOrchestrator extends EventEmitter {
314
347
  .catch((error) => reject(error));
315
348
  });
316
349
  this.#crawler.on('crawlEnd', () => {
350
+ // Deferred to INSIDE a queued closure, not read synchronously
351
+ // here, for the same reason `networkOutageRecovered`'s handler
352
+ // defers reading `#openNetworkOutageId`: a `dedupeCap` event's
353
+ // INSERT closure may still be queued (not yet executed) at the
354
+ // instant `crawlEnd` fires. `WriteQueue` runs enqueued
355
+ // operations in submission order, so by the time THIS closure
356
+ // executes, every earlier-queued `dedupeCap` INSERT has
357
+ // already completed and `#dedupeCapEventIds` is reliably
358
+ // populated.
359
+ writeQueue
360
+ .enqueue(async () => {
361
+ const rejections = this.#crawler.getDedupeCapRejections();
362
+ // Finalize every shape capped THIS session (has an id in
363
+ // `#dedupeCapEventIds`), not just the ones with a nonzero
364
+ // rejection count — a shape that capped near the end of the
365
+ // crawl (or whose remaining anchors all happened to be
366
+ // discovered before it capped) never enters `rejections` at
367
+ // all, and would otherwise stay `rejected_count: NULL` forever
368
+ // despite the crawl completing normally, corrupting the "NULL
369
+ // means the crawl never reached crawlEnd" contract
370
+ // `list-dedupe-cap-events.ts` documents.
371
+ const shapeKeysToFinalize = new Set([
372
+ ...this.#dedupeCapEventIds.keys(),
373
+ ...rejections.keys(),
374
+ ]);
375
+ await Promise.all([...shapeKeysToFinalize].map((shapeKey) => {
376
+ const rejectedCount = rejections.get(shapeKey) ?? 0;
377
+ const id = this.#dedupeCapEventIds.get(shapeKey);
378
+ // A shape capped THIS session has an id here (the
379
+ // `dedupeCap` event always enqueues an INSERT before any
380
+ // rejection for that shape can be counted) and is
381
+ // finalized once via its row id. A shape with no id was
382
+ // never observed this session at all — it was preloaded
383
+ // into `DedupeCapTracker`'s sticky set from an EARLIER
384
+ // session's `dedupe_cap_events` row (see
385
+ // `CrawlConfig.preloadedStickyShapeKeys`'s JSDoc), so gate
386
+ // rejections still accumulate for it but no new row (and
387
+ // thus no id) is ever created. That earlier row's count is
388
+ // accumulated onto by shape_key instead of overwritten.
389
+ return id === undefined
390
+ ? this.#archive.accumulateDedupeCapRejectedCount(shapeKey, rejectedCount)
391
+ : this.#archive.finalizeDedupeCapEvent(id, rejectedCount);
392
+ }));
393
+ })
394
+ .catch((error) => reject(error));
317
395
  writeQueue
318
396
  .drain()
319
397
  .then(() => resolve())
@@ -521,9 +599,14 @@ export class CrawlerOrchestrator extends EventEmitter {
521
599
  scopeMap.set(parsed.hostname, [...existing, parsed]);
522
600
  }
523
601
  await archive.repromoteExternalPages(scopeMap, archived);
602
+ // Seed the sticky set from prior sessions' confirmed traps so
603
+ // `--append` does not pay the cost of re-discovering them (see
604
+ // `DedupeCapTracker`'s constructor JSDoc).
605
+ const preloadedStickyShapeKeys = await archive.listDedupeCapShapeKeys();
524
606
  const orchestrator = new CrawlerOrchestrator(archive, {
525
607
  ...mergedConfig,
526
608
  roots: mergedRoots,
609
+ preloadedStickyShapeKeys,
527
610
  });
528
611
  const { scraped, pending } = await archive.getCrawlingState();
529
612
  const resources = await archive.getResourceUrlList();
@@ -856,6 +939,16 @@ export class CrawlerOrchestrator extends EventEmitter {
856
939
  inventoryMode: { seedUrls: seedSet },
857
940
  };
858
941
  if (htmlSeeds.length > 0) {
942
+ // Seed the sticky set from prior sessions' confirmed traps
943
+ // so `--inventory` does not pay the cost of
944
+ // re-discovering them (see `DedupeCapTracker`'s
945
+ // constructor JSDoc). Scoped to this branch only,
946
+ // matching `#preloadDnsBurnedHostCache`'s scoping below —
947
+ // the fallback (non-HTML-only) branch never calls
948
+ // `orchestrator.crawling(...)`, so the tracker is never
949
+ // consulted there.
950
+ orchestratorOptions.preloadedStickyShapeKeys =
951
+ await archive.listDedupeCapShapeKeys();
859
952
  const orchestrator = new CrawlerOrchestrator(archive, orchestratorOptions);
860
953
  // Re-read pending *after* the pre-insert so the strict-
861
954
  // pending set includes the freshly inserted
@@ -1007,7 +1100,14 @@ export class CrawlerOrchestrator extends EventEmitter {
1007
1100
  log('Start retrying failed pages');
1008
1101
  log('Archive %s', absFilePath);
1009
1102
  log('Reset %d failed page(s)', resetUrls.length);
1010
- const orchestrator = new CrawlerOrchestrator(archive, config);
1103
+ // Seed the sticky set from prior sessions' confirmed traps so
1104
+ // `--retry-failed` does not pay the cost of re-discovering
1105
+ // them (see `DedupeCapTracker`'s constructor JSDoc).
1106
+ const preloadedStickyShapeKeys = await archive.listDedupeCapShapeKeys();
1107
+ const orchestrator = new CrawlerOrchestrator(archive, {
1108
+ ...config,
1109
+ preloadedStickyShapeKeys,
1110
+ });
1011
1111
  const { scraped, pending } = await archive.getCrawlingState();
1012
1112
  const resources = await archive.getResourceUrlList();
1013
1113
  const pagesScrapedOffset = await archive.getScrapedHtmlPageCount();
@@ -1056,9 +1156,14 @@ export class CrawlerOrchestrator extends EventEmitter {
1056
1156
  static async resume(stubPath, options, initializedCallback) {
1057
1157
  const archive = await Archive.resume(stubPath);
1058
1158
  const archivedConfig = await archive.getConfig();
1159
+ // Seed the sticky set from prior sessions' confirmed traps so
1160
+ // `--resume` does not pay the cost of re-discovering them (see
1161
+ // `DedupeCapTracker`'s constructor JSDoc).
1162
+ const preloadedStickyShapeKeys = await archive.listDedupeCapShapeKeys();
1059
1163
  const config = {
1060
1164
  ...archivedConfig,
1061
1165
  ...cleanObject(options),
1166
+ preloadedStickyShapeKeys,
1062
1167
  };
1063
1168
  const orchestrator = new CrawlerOrchestrator(archive, config);
1064
1169
  const _url = await archive.getUrl();
package/lib/crawler.d.ts CHANGED
@@ -46,6 +46,7 @@ export type { NetworkProbe } from './crawler/probe-network.js';
46
46
  export { probeNetwork } from './crawler/probe-network.js';
47
47
  export { computeOutageClampTimestamp } from './archive/db-ops/outages/compute-outage-clamp-timestamp.js';
48
48
  export { chooseProbeHost } from './crawler/choose-probe-host.js';
49
+ export { assertChromeIsInstalled } from './crawler/assert-chrome-installed.js';
49
50
  export { computeFileSha256 } from './utils/compute-file-sha256.js';
50
51
  export { populateEntityTables } from './archive/populate-entity-tables/populate-entities.js';
51
52
  export type { PageDomPathResolver } from './archive/populate-entity-tables/populate-image-items.js';
package/lib/crawler.js CHANGED
@@ -43,6 +43,7 @@ export { default as NetworkGate } from './crawler/network-gate.js';
43
43
  export { probeNetwork } from './crawler/probe-network.js';
44
44
  export { computeOutageClampTimestamp } from './archive/db-ops/outages/compute-outage-clamp-timestamp.js';
45
45
  export { chooseProbeHost } from './crawler/choose-probe-host.js';
46
+ export { assertChromeIsInstalled } from './crawler/assert-chrome-installed.js';
46
47
  export { computeFileSha256 } from './utils/compute-file-sha256.js';
47
48
  // 0.13 ref-table population (issue #191, epic #103). Exposed as the
48
49
  // public seam that the migration script (`scripts/migrate-to-0.13.mjs`)
@@ -7,11 +7,12 @@ import type { ErrorKind } from './types.js';
7
7
  * Used by `resetFailedPages` to exclude pages whose latest recorded error
8
8
  * falls in this set, so `--retry-failed` actually converges: without the
9
9
  * exclusion, NXDOMAIN / TLS mismatch / `ERR_BLOCKED_BY_CLIENT` /
10
- * `ECONNREFUSED` / HTTP parse-error pages would be reset to pending on every
11
- * iteration, the crawler would re-attempt them, they would fail again the
12
- * same way, and the retry-target count would stay constant forever.
10
+ * `ECONNREFUSED` / HTTP parse-error / redirect-loop pages would be reset to
11
+ * pending on every iteration, the crawler would re-attempt them, they would
12
+ * fail again the same way, and the retry-target count would stay constant
13
+ * forever.
13
14
  *
14
- * Why these five and not others:
15
+ * Why these six and not others:
15
16
  * - **dns** — `ENOTFOUND` / `ERR_NAME_NOT_RESOLVED` are authoritative DNS
16
17
  * answers; the host is gone (or never existed). EAI_AGAIN is split out as
17
18
  * `dns-transient` precisely so it is NOT in this set.
@@ -28,6 +29,10 @@ import type { ErrorKind } from './types.js';
28
29
  * the listener; either no process is listening on the port or its accept
29
30
  * queue rejected the connection. Either way the answer is final until the
30
31
  * server operator intervenes.
32
+ * - **redirect-loop** — `Maximum number of redirects exceeded` /
33
+ * `ERR_TOO_MANY_REDIRECTS` means the site's own redirect chain never
34
+ * terminates; the exact same chain is served on every future fetch until
35
+ * the site operator fixes it.
31
36
  *
32
37
  * Notably absent (intentionally retryable):
33
38
  * - `connection-reset` / `connection-timeout` — could be middlebox or
@@ -6,11 +6,12 @@
6
6
  * Used by `resetFailedPages` to exclude pages whose latest recorded error
7
7
  * falls in this set, so `--retry-failed` actually converges: without the
8
8
  * exclusion, NXDOMAIN / TLS mismatch / `ERR_BLOCKED_BY_CLIENT` /
9
- * `ECONNREFUSED` / HTTP parse-error pages would be reset to pending on every
10
- * iteration, the crawler would re-attempt them, they would fail again the
11
- * same way, and the retry-target count would stay constant forever.
9
+ * `ECONNREFUSED` / HTTP parse-error / redirect-loop pages would be reset to
10
+ * pending on every iteration, the crawler would re-attempt them, they would
11
+ * fail again the same way, and the retry-target count would stay constant
12
+ * forever.
12
13
  *
13
- * Why these five and not others:
14
+ * Why these six and not others:
14
15
  * - **dns** — `ENOTFOUND` / `ERR_NAME_NOT_RESOLVED` are authoritative DNS
15
16
  * answers; the host is gone (or never existed). EAI_AGAIN is split out as
16
17
  * `dns-transient` precisely so it is NOT in this set.
@@ -27,6 +28,10 @@
27
28
  * the listener; either no process is listening on the port or its accept
28
29
  * queue rejected the connection. Either way the answer is final until the
29
30
  * server operator intervenes.
31
+ * - **redirect-loop** — `Maximum number of redirects exceeded` /
32
+ * `ERR_TOO_MANY_REDIRECTS` means the site's own redirect chain never
33
+ * terminates; the exact same chain is served on every future fetch until
34
+ * the site operator fixes it.
30
35
  *
31
36
  * Notably absent (intentionally retryable):
32
37
  * - `connection-reset` / `connection-timeout` — could be middlebox or
@@ -45,4 +50,5 @@ export const PERMANENT_ERROR_KINDS = new Set([
45
50
  'client-blocked',
46
51
  'parse-error',
47
52
  'connection-refused',
53
+ 'redirect-loop',
48
54
  ]);
package/lib/types.d.ts CHANGED
@@ -66,6 +66,7 @@ export interface InventoryRunAggregates {
66
66
  * | `local-network` | **yes** | no | local machine's network is unreachable / changed (WiFi, sleep, ICMP-unreachable, …) |
67
67
  * | `parse-error` | mostly persistent | no | HTTP response could not be parsed (proxy, garbage, MITM) |
68
68
  * | `client-blocked` | persistent (per browser) | no | Chromium-side `ERR_BLOCKED_BY_*` family — the browser actively refused the request (ad/tracker heuristics, CSP, CORP, administrator block list, …) |
69
+ * | `redirect-loop` | no | no | the redirect chain exceeded `follow-redirects`' `maxRedirects` limit — the site's own redirect configuration never converges |
69
70
  * | `protocol` | yes | no | puppeteer protocol layer (frame detached, target closed, …) |
70
71
  * | `timeout` | yes | no | puppeteer navigation timeout or HEAD pre-flight race timeout (`Timeout: <url>`) |
71
72
  * | `unknown` | unknown | no | catch-all for messages no matcher recognised |
@@ -90,7 +91,7 @@ export interface InventoryRunAggregates {
90
91
  * entry), or a recoverable URL never reaching the puppeteer fallback (no
91
92
  * PUPPETEER_FALLBACK entry).
92
93
  */
93
- export type ErrorKind = 'dns' | 'dns-transient' | 'connection-refused' | 'connection-reset' | 'connection-timeout' | 'tls' | 'local-network' | 'parse-error' | 'client-blocked' | 'timeout' | 'protocol' | 'unknown';
94
+ export type ErrorKind = 'dns' | 'dns-transient' | 'connection-refused' | 'connection-reset' | 'connection-timeout' | 'tls' | 'local-network' | 'parse-error' | 'client-blocked' | 'redirect-loop' | 'timeout' | 'protocol' | 'unknown';
94
95
  /**
95
96
  * Event map for the `CrawlerOrchestrator` class.
96
97
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitpicker/crawler",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Web crawler engine with headless browser rendering and archive storage",
5
5
  "author": "D-ZERO",
6
6
  "license": "Apache-2.0",
@@ -48,5 +48,5 @@
48
48
  "@types/tar": "7.0.87",
49
49
  "@types/unzipper": "0.10.11"
50
50
  },
51
- "gitHead": "d1485df6e43375c6a44edfe07a0293fd39d7810d"
51
+ "gitHead": "bef8b6d48e3ca5167fee643d6aba8644a065df4a"
52
52
  }