doc2vec 2.2.0 → 2.3.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.
@@ -247,83 +247,121 @@ class ContentProcessor {
247
247
  let pdfProcessedCount = 0;
248
248
  let errorCount = 0;
249
249
  let hasNetworkErrors = false;
250
- while (queue.length > 0) {
251
- const url = queue.shift();
252
- if (!url)
253
- continue;
254
- const normalizedUrl = utils_1.Utils.normalizeUrl(url);
255
- if (visitedUrls.has(normalizedUrl))
256
- continue;
257
- visitedUrls.add(normalizedUrl);
258
- if (!utils_1.Utils.shouldProcessUrl(url)) {
259
- logger.debug(`Skipping URL with unsupported extension: ${url}`);
260
- skippedCount++;
261
- continue;
250
+ // Launch a single browser instance and reuse one page (tab) for all URLs
251
+ let browser = null;
252
+ let page = null;
253
+ const launchBrowser = async () => {
254
+ let executablePath = process.env.PUPPETEER_EXECUTABLE_PATH;
255
+ if (!executablePath) {
256
+ if (fs.existsSync('/usr/bin/chromium')) {
257
+ executablePath = '/usr/bin/chromium';
258
+ }
259
+ else if (fs.existsSync('/usr/bin/chromium-browser')) {
260
+ executablePath = '/usr/bin/chromium-browser';
261
+ }
262
262
  }
263
- try {
264
- logger.info(`Crawling: ${url}`);
265
- const sources = referrers.get(url) ?? new Set([baseUrl]);
266
- const content = await this.processPage(url, sourceConfig, (reportedUrl, status) => {
267
- if (status === 404) {
268
- for (const source of sources) {
269
- addBrokenLink(source, reportedUrl);
263
+ const b = await puppeteer_1.default.launch({
264
+ executablePath,
265
+ args: ['--no-sandbox', '--disable-setuid-sandbox'],
266
+ });
267
+ const p = await b.newPage();
268
+ return { browser: b, page: p };
269
+ };
270
+ const ensureBrowser = async () => {
271
+ if (!browser || !browser.isConnected()) {
272
+ logger.info(browser ? 'Browser disconnected, relaunching...' : 'Launching browser...');
273
+ const launched = await launchBrowser();
274
+ browser = launched.browser;
275
+ page = launched.page;
276
+ }
277
+ return page;
278
+ };
279
+ try {
280
+ while (queue.length > 0) {
281
+ const url = queue.shift();
282
+ if (!url)
283
+ continue;
284
+ const normalizedUrl = utils_1.Utils.normalizeUrl(url);
285
+ if (visitedUrls.has(normalizedUrl))
286
+ continue;
287
+ visitedUrls.add(normalizedUrl);
288
+ if (!utils_1.Utils.shouldProcessUrl(url)) {
289
+ logger.debug(`Skipping URL with unsupported extension: ${url}`);
290
+ skippedCount++;
291
+ continue;
292
+ }
293
+ try {
294
+ logger.info(`Crawling: ${url}`);
295
+ const sources = referrers.get(url) ?? new Set([baseUrl]);
296
+ // For HTML pages, ensure the browser is running and pass the shared page
297
+ // For PDFs, processPage handles them without Puppeteer
298
+ const currentPage = utils_1.Utils.isPdfUrl(url) ? undefined : await ensureBrowser();
299
+ const result = await this.processPage(url, sourceConfig, (reportedUrl, status) => {
300
+ if (status === 404) {
301
+ for (const source of sources) {
302
+ addBrokenLink(source, reportedUrl);
303
+ }
304
+ }
305
+ }, currentPage);
306
+ if (result.content !== null) {
307
+ await processPageContent(url, result.content);
308
+ if (utils_1.Utils.isPdfUrl(url)) {
309
+ pdfProcessedCount++;
310
+ }
311
+ else {
312
+ processedCount++;
270
313
  }
271
- }
272
- });
273
- if (content !== null) {
274
- await processPageContent(url, content);
275
- if (utils_1.Utils.isPdfUrl(url)) {
276
- pdfProcessedCount++;
277
314
  }
278
315
  else {
279
- processedCount++;
316
+ skippedSizeCount++;
280
317
  }
281
- }
282
- else {
283
- skippedSizeCount++;
284
- }
285
- // Only try to extract links from HTML pages, not PDFs
286
- if (!utils_1.Utils.isPdfUrl(url)) {
287
- const response = await axios_1.default.get(url);
288
- const $ = (0, cheerio_1.load)(response.data);
289
- const pageUrlForLinks = response?.request?.res?.responseUrl || url;
290
- logger.debug(`Finding links on page ${url}`);
291
- let newLinksFound = 0;
292
- $('a[href]').each((_, element) => {
293
- const href = $(element).attr('href');
294
- if (!href || href.startsWith('#') || href.startsWith('mailto:'))
295
- return;
296
- const fullUrl = utils_1.Utils.buildUrl(href, pageUrlForLinks);
297
- if (fullUrl.startsWith(sourceConfig.url)) {
298
- addReferrer(fullUrl, pageUrlForLinks);
299
- if (!visitedUrls.has(utils_1.Utils.normalizeUrl(fullUrl))) {
300
- if (!queue.includes(fullUrl)) {
301
- queue.push(fullUrl);
302
- newLinksFound++;
318
+ // Use links extracted from the full rendered DOM by processPage
319
+ // (no separate axios request needed)
320
+ if (result.links.length > 0) {
321
+ const pageUrlForLinks = result.finalUrl || url;
322
+ logger.debug(`Finding links on page ${url}`);
323
+ let newLinksFound = 0;
324
+ for (const href of result.links) {
325
+ const fullUrl = utils_1.Utils.buildUrl(href, pageUrlForLinks);
326
+ if (fullUrl.startsWith(sourceConfig.url)) {
327
+ addReferrer(fullUrl, pageUrlForLinks);
328
+ if (!visitedUrls.has(utils_1.Utils.normalizeUrl(fullUrl))) {
329
+ if (!queue.includes(fullUrl)) {
330
+ queue.push(fullUrl);
331
+ newLinksFound++;
332
+ }
303
333
  }
304
334
  }
305
335
  }
306
- });
307
- logger.debug(`Found ${newLinksFound} new links on ${url}`);
308
- }
309
- }
310
- catch (error) {
311
- logger.error(`Failed during processing or link discovery for ${url}:`, error);
312
- errorCount++;
313
- const status = this.getHttpStatus(error);
314
- if (status === 404) {
315
- const sources = referrers.get(url) ?? new Set([baseUrl]);
316
- for (const source of sources) {
317
- addBrokenLink(source, url);
336
+ logger.debug(`Found ${newLinksFound} new links on ${url}`);
318
337
  }
319
338
  }
320
- // Check if this is a network error (DNS resolution, connection issues, etc.)
321
- if (this.isNetworkError(error)) {
322
- hasNetworkErrors = true;
323
- logger.warn(`Network error detected for ${url}, this may affect cleanup decisions`);
339
+ catch (error) {
340
+ logger.error(`Failed during processing or link discovery for ${url}:`, error);
341
+ errorCount++;
342
+ const status = this.getHttpStatus(error);
343
+ if (status === 404) {
344
+ const sources = referrers.get(url) ?? new Set([baseUrl]);
345
+ for (const source of sources) {
346
+ addBrokenLink(source, url);
347
+ }
348
+ }
349
+ // Check if this is a network error (DNS resolution, connection issues, etc.)
350
+ if (this.isNetworkError(error)) {
351
+ hasNetworkErrors = true;
352
+ logger.warn(`Network error detected for ${url}, this may affect cleanup decisions`);
353
+ }
324
354
  }
325
355
  }
326
356
  }
357
+ finally {
358
+ // Close the shared browser instance when the crawl is done
359
+ const browserToClose = browser;
360
+ if (browserToClose && browserToClose.isConnected()) {
361
+ await browserToClose.close();
362
+ logger.debug('Shared browser closed after crawl completed');
363
+ }
364
+ }
327
365
  logger.info(`Crawl completed. HTML Pages: ${processedCount}, PDFs: ${pdfProcessedCount}, Skipped (Extension): ${skippedCount}, Skipped (Size): ${skippedSizeCount}, Errors: ${errorCount}`);
328
366
  if (hasNetworkErrors) {
329
367
  logger.warn('Network errors were encountered during crawling. Cleanup may be skipped to avoid removing valid chunks.');
@@ -369,7 +407,7 @@ class ContentProcessor {
369
407
  }
370
408
  return false;
371
409
  }
372
- async processPage(url, sourceConfig, onHttpStatus) {
410
+ async processPage(url, sourceConfig, onHttpStatus, existingPage) {
373
411
  const logger = this.logger.child('page-processor');
374
412
  logger.debug(`Processing content from ${url}`);
375
413
  // Check if this is a PDF URL
@@ -380,9 +418,9 @@ class ContentProcessor {
380
418
  // Check size limit for PDF content
381
419
  if (markdown.length > sourceConfig.max_size) {
382
420
  logger.warn(`PDF content (${markdown.length} chars) exceeds max size (${sourceConfig.max_size}). Skipping ${url}.`);
383
- return null;
421
+ return { content: null, links: [], finalUrl: url };
384
422
  }
385
- return markdown;
423
+ return { content: markdown, links: [], finalUrl: url };
386
424
  }
387
425
  catch (error) {
388
426
  const status = this.getHttpStatus(error);
@@ -393,27 +431,35 @@ class ContentProcessor {
393
431
  throw error;
394
432
  }
395
433
  logger.error(`Failed to process PDF ${url}:`, error);
396
- return null;
434
+ return { content: null, links: [], finalUrl: url };
397
435
  }
398
436
  }
399
- // Original HTML page processing logic
437
+ // HTML page processing logic
438
+ // If an existing page (tab) is provided, reuse it; otherwise launch a standalone browser
400
439
  let browser = null;
440
+ let page;
441
+ const ownsTheBrowser = !existingPage;
401
442
  try {
402
- // Use system Chromium if available (for Docker environments)
403
- let executablePath = process.env.PUPPETEER_EXECUTABLE_PATH;
404
- if (!executablePath) {
405
- if (fs.existsSync('/usr/bin/chromium')) {
406
- executablePath = '/usr/bin/chromium';
407
- }
408
- else if (fs.existsSync('/usr/bin/chromium-browser')) {
409
- executablePath = '/usr/bin/chromium-browser';
443
+ if (existingPage) {
444
+ page = existingPage;
445
+ }
446
+ else {
447
+ // Standalone mode: launch a browser for this single page
448
+ let executablePath = process.env.PUPPETEER_EXECUTABLE_PATH;
449
+ if (!executablePath) {
450
+ if (fs.existsSync('/usr/bin/chromium')) {
451
+ executablePath = '/usr/bin/chromium';
452
+ }
453
+ else if (fs.existsSync('/usr/bin/chromium-browser')) {
454
+ executablePath = '/usr/bin/chromium-browser';
455
+ }
410
456
  }
457
+ browser = await puppeteer_1.default.launch({
458
+ executablePath,
459
+ args: ['--no-sandbox', '--disable-setuid-sandbox'],
460
+ });
461
+ page = await browser.newPage();
411
462
  }
412
- browser = await puppeteer_1.default.launch({
413
- executablePath,
414
- args: ['--no-sandbox', '--disable-setuid-sandbox'],
415
- });
416
- const page = await browser.newPage();
417
463
  logger.debug(`Navigating to ${url}`);
418
464
  const response = await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
419
465
  const status = response?.status();
@@ -422,8 +468,24 @@ class ContentProcessor {
422
468
  error.status = status;
423
469
  throw error;
424
470
  }
471
+ // Get the final URL after any redirects
472
+ const finalUrl = page.url();
473
+ // Extract ALL links from the full rendered DOM before any content filtering
474
+ // This searches the entire document, not just the main content area
475
+ const links = await page.evaluate(() => {
476
+ const anchors = document.querySelectorAll('a[href]');
477
+ const hrefs = [];
478
+ anchors.forEach(a => {
479
+ const href = a.getAttribute('href');
480
+ if (href && !href.startsWith('#') && !href.startsWith('mailto:')) {
481
+ hrefs.push(href);
482
+ }
483
+ });
484
+ return hrefs;
485
+ });
486
+ logger.debug(`Extracted ${links.length} links from full DOM of ${url}`);
425
487
  const htmlContent = await page.evaluate(() => {
426
- // 💡 Try specific content selectors first, then fall back to broader ones
488
+ // Try specific content selectors first, then fall back to broader ones
427
489
  const mainContentElement = document.querySelector('.docs-content') || // Common docs pattern
428
490
  document.querySelector('.doc-content') || // Alternative docs pattern
429
491
  document.querySelector('.markdown-body') || // GitHub-style
@@ -435,8 +497,7 @@ class ContentProcessor {
435
497
  });
436
498
  if (htmlContent.length > sourceConfig.max_size) {
437
499
  logger.warn(`Raw HTML content (${htmlContent.length} chars) exceeds max size (${sourceConfig.max_size}). Skipping detailed processing for ${url}.`);
438
- await browser.close();
439
- return null;
500
+ return { content: null, links, finalUrl };
440
501
  }
441
502
  logger.debug(`Got HTML content (${htmlContent.length} chars), creating DOM`);
442
503
  const dom = new jsdom_1.JSDOM(htmlContent);
@@ -446,7 +507,7 @@ class ContentProcessor {
446
507
  pre.setAttribute('data-readable-content-score', '100');
447
508
  this.markCodeParents(pre.parentElement);
448
509
  });
449
- // 💡 Extract H1s BEFORE Readability - it often strips them as "chrome"
510
+ // Extract H1s BEFORE Readability - it often strips them as "chrome"
450
511
  // We'll inject them back after Readability processing
451
512
  const h1Elements = document.querySelectorAll('h1');
452
513
  const extractedH1s = [];
@@ -468,8 +529,7 @@ class ContentProcessor {
468
529
  const article = reader.parse();
469
530
  if (!article) {
470
531
  logger.warn(`Failed to parse article content with Readability for ${url}`);
471
- await browser.close();
472
- return null;
532
+ return { content: null, links, finalUrl };
473
533
  }
474
534
  // Debug: Log what Readability extracted
475
535
  logger.debug(`[Readability Debug] article.title: "${article.title}"`);
@@ -478,7 +538,7 @@ class ContentProcessor {
478
538
  logger.debug(`[Readability Debug] Contains H1 tag: ${article.content?.includes('<h1')}`);
479
539
  logger.debug(`[Readability Debug] Contains H2 tag: ${article.content?.includes('<h2')}`);
480
540
  logger.debug(`[Readability Debug] Contains original-h1 class: ${article.content?.includes('original-h1')}`);
481
- // 💡 Restore H1s: find elements with our marker class and convert back from H2
541
+ // Restore H1s: find elements with our marker class and convert back from H2
482
542
  const articleDom = new jsdom_1.JSDOM(article.content);
483
543
  const articleDoc = articleDom.window.document;
484
544
  const originalH1Elements = articleDoc.querySelectorAll('.original-h1');
@@ -515,7 +575,7 @@ class ContentProcessor {
515
575
  });
516
576
  logger.debug(`Converting HTML to Markdown`);
517
577
  let markdown = this.turndownService.turndown(cleanHtml);
518
- // 💡 Inject extracted H1s back if they're not in the markdown
578
+ // Inject extracted H1s back if they're not in the markdown
519
579
  // Readability often strips them as "page chrome"
520
580
  // Use article.title as fallback if no H1 was extracted
521
581
  const pageTitle = extractedH1s.length > 0 ? extractedH1s[0] : (article.title?.trim() || '');
@@ -535,7 +595,7 @@ class ContentProcessor {
535
595
  }
536
596
  }
537
597
  logger.debug(`Markdown conversion complete (${markdown.length} chars)`);
538
- return markdown;
598
+ return { content: markdown, links, finalUrl };
539
599
  }
540
600
  catch (error) {
541
601
  const status = this.getHttpStatus(error);
@@ -546,10 +606,11 @@ class ContentProcessor {
546
606
  throw error;
547
607
  }
548
608
  logger.error(`Error processing page ${url}:`, error);
549
- return null;
609
+ return { content: null, links: [], finalUrl: url };
550
610
  }
551
611
  finally {
552
- if (browser && browser.isConnected()) {
612
+ // Only close the browser if we launched it ourselves (standalone mode)
613
+ if (ownsTheBrowser && browser && browser.isConnected()) {
553
614
  await browser.close();
554
615
  logger.debug(`Browser closed for ${url}`);
555
616
  }
package/dist/database.js CHANGED
@@ -374,10 +374,10 @@ class DatabaseManager {
374
374
  chunk.metadata.version
375
375
  ];
376
376
  if (hasBranchColumn) {
377
- insertValues.push(chunk.metadata.branch ?? null);
377
+ insertValues.push(chunk.metadata.branch ?? '');
378
378
  }
379
379
  if (hasRepoColumn) {
380
- insertValues.push(chunk.metadata.repo ?? null);
380
+ insertValues.push(chunk.metadata.repo ?? '');
381
381
  }
382
382
  insertValues.push(JSON.stringify(chunk.metadata.heading_hierarchy), chunk.metadata.section, chunk.metadata.chunk_id, chunk.content, chunk.metadata.url, hash, chunkIndex, totalChunks);
383
383
  insertStmt.run(...insertValues);
@@ -389,10 +389,10 @@ class DatabaseManager {
389
389
  chunk.metadata.version
390
390
  ];
391
391
  if (hasBranchColumn) {
392
- updateValues.push(chunk.metadata.branch ?? null);
392
+ updateValues.push(chunk.metadata.branch ?? '');
393
393
  }
394
394
  if (hasRepoColumn) {
395
- updateValues.push(chunk.metadata.repo ?? null);
395
+ updateValues.push(chunk.metadata.repo ?? '');
396
396
  }
397
397
  updateValues.push(JSON.stringify(chunk.metadata.heading_hierarchy), chunk.metadata.section, chunk.content, chunk.metadata.url, hash, chunkIndex, totalChunks, chunk.metadata.chunk_id);
398
398
  updateStmt.run(...updateValues);
package/dist/doc2vec.js CHANGED
@@ -402,7 +402,7 @@ class Doc2Vec {
402
402
  const chunks = await this.contentProcessor.chunkMarkdown(content, config, url);
403
403
  logger.info(`Created ${chunks.length} chunks`);
404
404
  if (chunks.length > 0) {
405
- const chunkProgress = logger.progress(`Embedding chunks for ${url}`, chunks.length);
405
+ const chunkProgress = logger.progress(`Processing chunks for ${url}`, chunks.length);
406
406
  for (let i = 0; i < chunks.length; i++) {
407
407
  const chunk = chunks[i];
408
408
  validChunkIds.add(chunk.metadata.chunk_id);
@@ -561,7 +561,7 @@ class Doc2Vec {
561
561
  const chunks = await this.contentProcessor.chunkMarkdown(content, config, fileUrl);
562
562
  logger.info(`Created ${chunks.length} chunks`);
563
563
  if (chunks.length > 0) {
564
- const chunkProgress = logger.progress(`Embedding chunks for ${filePath}`, chunks.length);
564
+ const chunkProgress = logger.progress(`Processing chunks for ${filePath}`, chunks.length);
565
565
  for (let i = 0; i < chunks.length; i++) {
566
566
  const chunk = chunks[i];
567
567
  validChunkIds.add(chunk.metadata.chunk_id);
@@ -729,7 +729,7 @@ class Doc2Vec {
729
729
  const chunks = await this.contentProcessor.chunkCode(content, config, fileUrl, relativePath || filePath, repoBranch || config.branch, config.repo);
730
730
  logger.info(`Created ${chunks.length} chunks`);
731
731
  if (chunks.length > 0) {
732
- const chunkProgress = logger.progress(`Embedding chunks for ${relativePath || filePath}`, chunks.length);
732
+ const chunkProgress = logger.progress(`Processing chunks for ${relativePath || filePath}`, chunks.length);
733
733
  for (let i = 0; i < chunks.length; i++) {
734
734
  const chunk = chunks[i];
735
735
  validChunkIds.add(chunk.metadata.chunk_id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doc2vec",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "type": "commonjs",
5
5
  "description": "",
6
6
  "main": "dist/doc2vec.js",