@detiq/crawler 0.1.5 → 0.1.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@detiq/crawler",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "private": false,
5
5
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
6
6
  "type": "module",
package/src/crawler.ts CHANGED
@@ -1303,7 +1303,8 @@ export async function crawlProject(opts: {
1303
1303
  }
1304
1304
  let checkpointSuccessCount = 0;
1305
1305
  let maxSeenTotal = 0; // track high-water mark so progress % never goes backwards
1306
- let lastEmittedPct = 20; // clamp so progress never decreases even when queue grows
1306
+ let lastEmittedPct = 5; // clamp so progress never decreases even when queue grows
1307
+ const crawlLoopStart = Date.now();
1307
1308
  const startUrl = page.url();
1308
1309
  // Resume from previous segment's pending queue if provided
1309
1310
  const resumeUrls = opts.resumeUrls ?? [];
@@ -2135,7 +2136,13 @@ export async function crawlProject(opts: {
2135
2136
 
2136
2137
  // Emit progress now — navigation is done and the page is stable.
2137
2138
  // This fires before the 30-90s parallel screenshot+AI phase, preventing a silent gap in the UI.
2138
- await onProgress?.(20, `Crawling ${visited.size} of ~${visited.size + queue.length} · ${(() => { try { return new URL(url).pathname || '/'; } catch { return '/'; } })()}`);
2139
+ // Phase 1 (link discovery): 5–20% over first 60s of wall-clock time so users see movement
2140
+ // before visited.size grows enough to drive the 20–80% phase.
2141
+ const elapsedDiscoverySec = (Date.now() - crawlLoopStart) / 1000;
2142
+ const discoveryPct = Math.round(5 + Math.min(15, (elapsedDiscoverySec / 60) * 15));
2143
+ const preCapturePct = Math.max(lastEmittedPct, discoveryPct);
2144
+ lastEmittedPct = preCapturePct;
2145
+ await onProgress?.(preCapturePct, `Crawling ${visited.size} of ~${visited.size + queue.length} · ${(() => { try { return new URL(url).pathname || '/'; } catch { return '/'; } })()}`);
2139
2146
 
2140
2147
  // P3a: Async content detection — get raw DOM element count before AI extraction.
2141
2148
  // The AI extraction takes 30-90s; any async-loaded content will be visible after it completes.
@@ -2145,6 +2152,7 @@ export async function crawlProject(opts: {
2145
2152
  ).catch(() => 0);
2146
2153
 
2147
2154
  await waitForScreenshotReady(page);
2155
+ await onProgress?.(lastEmittedPct, `Capturing ${(() => { try { return new URL(url).pathname || '/'; } catch { return '/'; } })()}`);
2148
2156
 
2149
2157
  // Parallelize: screenshot + element extraction + link discovery + AT capture (all work on current page state)
2150
2158
  const [, elements, nextLinks, atElements] = await Promise.all([
@@ -2644,9 +2652,10 @@ export async function crawlProject(opts: {
2644
2652
 
2645
2653
  discovered.push({ url, screenId: screen.id, elements: elements.length, elementFacts: elements, screenshotPath: shotPath, version: screen.version });
2646
2654
  // Progress: 20% baseline + up to 60% across discovered screens.
2647
- // maxSeenTotal is the denominator high-water mark; lastEmittedPct prevents
2648
- // the displayed % from going backwards when new link discoveries grow the queue.
2649
- maxSeenTotal = Math.max(maxSeenTotal, visited.size + queue.length);
2655
+ // Cap maxSeenTotal at maxScreens * 3 so unbounded link discovery does not
2656
+ // collapse the denominator and freeze progress in the 20-22% band.
2657
+ const effectiveMax = isFinite(maxScreens) ? Math.min(visited.size + queue.length, maxScreens * 3) : visited.size + queue.length;
2658
+ maxSeenTotal = Math.max(maxSeenTotal, effectiveMax);
2650
2659
  const rawPct = 20 + Math.round(Math.min(60, (visited.size / Math.max(1, maxSeenTotal)) * 60));
2651
2660
  const clampedPct = Math.max(lastEmittedPct, rawPct);
2652
2661
  lastEmittedPct = clampedPct;
@@ -4018,24 +4027,27 @@ async function waitForStableDOM(page: any, maxWait = 8_000, stabilityMs = 400):
4018
4027
  }
4019
4028
 
4020
4029
  async function waitForScreenshotReady(page: any): Promise<void> {
4030
+ const hardDeadline = Date.now() + 20_000;
4031
+ const withHardCap = <T>(p: Promise<T>): Promise<T | void> =>
4032
+ Promise.race([p, new Promise<void>((resolve) => setTimeout(resolve, Math.max(0, hardDeadline - Date.now())))]);
4021
4033
  try {
4022
- await page.waitForLoadState('domcontentloaded', { timeout: 5_000 }).catch(() => { });
4023
- await page.waitForLoadState('networkidle', { timeout: 12_000 }).catch(() => { });
4024
- await page.evaluate(() => {
4034
+ await withHardCap(page.waitForLoadState('domcontentloaded', { timeout: 5_000 }).catch(() => { }));
4035
+ await withHardCap(page.waitForLoadState('networkidle', { timeout: 12_000 }).catch(() => { }));
4036
+ await withHardCap(page.evaluate(() => {
4025
4037
  if (document.readyState === 'complete') return true;
4026
4038
  return new Promise<boolean>((resolve) => {
4027
4039
  const done = () => resolve(true);
4028
4040
  window.addEventListener('load', done, { once: true });
4029
4041
  setTimeout(done, 4_000);
4030
4042
  });
4031
- }).catch(() => { });
4032
- await page.evaluate(async () => {
4043
+ }).catch(() => { }));
4044
+ await withHardCap(page.evaluate(async () => {
4033
4045
  const fonts = (document as any).fonts;
4034
4046
  if (fonts?.ready) await fonts.ready;
4035
- }).catch(() => { });
4036
- await scrollToTriggerLazyLoad(page);
4037
- await waitForStableDOM(page, 10_000, 700);
4038
- await page.evaluate(async () => {
4047
+ }).catch(() => { }));
4048
+ await withHardCap(scrollToTriggerLazyLoad(page));
4049
+ await withHardCap(waitForStableDOM(page, 4_000, 700));
4050
+ await withHardCap(page.evaluate(async () => {
4039
4051
  const pending = Array.from(document.images).filter((img) => !img.complete);
4040
4052
  if (pending.length === 0) return;
4041
4053
  await Promise.race([
@@ -4045,9 +4057,9 @@ async function waitForScreenshotReady(page: any): Promise<void> {
4045
4057
  }))),
4046
4058
  new Promise<void>((resolve) => setTimeout(resolve, 4_000)),
4047
4059
  ]);
4048
- }).catch(() => { });
4049
- await waitForStableDOM(page, 4_000, 500);
4050
- await page.waitForTimeout(250).catch(() => { });
4060
+ }).catch(() => { }));
4061
+ await withHardCap(waitForStableDOM(page, 2_000, 500));
4062
+ await withHardCap(page.waitForTimeout(250).catch(() => { }));
4051
4063
  } catch { /* readiness is best-effort; never fail the crawl for a continuously mutating page */ }
4052
4064
  }
4053
4065