@detiq/crawler 1.1.19 → 1.1.21

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/dist/job-queue.js CHANGED
@@ -113,6 +113,19 @@ async function claimNextQueuedJob() {
113
113
  return rows[0] ?? null;
114
114
  }
115
115
  async function requeueStaleRunningJobs() {
116
+ const failedRows = await prismaAdmin.$queryRaw`
117
+ UPDATE "PlatformJob"
118
+ SET status = 'FAILED',
119
+ "finishedAt" = NOW(),
120
+ error = 'API process was restarted while this job was running — please retry from the UI.'
121
+ WHERE status = 'RUNNING'
122
+ AND type IN ('GENERATE_SCRIPT', 'GENERATE_DATA', 'GENERATE_TESTS', 'GENERATE_TEST_PLAN')
123
+ AND COALESCE("heartbeatAt", "startedAt") < NOW() - (${STALE_JOB_TIMEOUT_MINUTES} * interval '1 minute')
124
+ RETURNING id
125
+ `;
126
+ if (failedRows.length > 0) {
127
+ logger.warn({ jobIds: failedRows.map((r) => r.id), staleTimeoutMinutes: STALE_JOB_TIMEOUT_MINUTES }, "[job-worker] failed stale API-in-process job(s) \u2014 API restarted while running");
128
+ }
116
129
  const rows = await prismaAdmin.$queryRaw`
117
130
  UPDATE "PlatformJob"
118
131
  SET status = 'QUEUED',
@@ -124,6 +137,7 @@ async function requeueStaleRunningJobs() {
124
137
  true
125
138
  )
126
139
  WHERE status = 'RUNNING'
140
+ AND type NOT IN ('GENERATE_SCRIPT', 'GENERATE_DATA', 'GENERATE_TESTS', 'GENERATE_TEST_PLAN')
127
141
  AND COALESCE("heartbeatAt", "startedAt") < NOW() - (${STALE_JOB_TIMEOUT_MINUTES} * interval '1 minute')
128
142
  RETURNING id
129
143
  `;
@@ -469,8 +483,27 @@ async function processJob(jobId, config, opts) {
469
483
  }, onProgress);
470
484
  jobCheckpoints.delete(jobId);
471
485
  const pendingUrls = result?.pendingUrls ?? [];
486
+ let nextJobId;
472
487
  if (pendingUrls.length > 0) {
473
- logger.info({ pendingUrls: pendingUrls.length }, "Crawl finished with pending URLs \u2014 not auto-queuing (segmented crawls disabled)");
488
+ const visitedForSkip = result.visitedUrls ?? (result.discovered ?? []).filter((d) => !!d.screenId && !!d.url).map((d) => {
489
+ try {
490
+ const _u = new URL(d.url);
491
+ _u.hash = "";
492
+ return _u.href;
493
+ } catch {
494
+ return d.url;
495
+ }
496
+ });
497
+ const reason = result.wasTimeout ? "timeout" : "segment-limit";
498
+ logger.info({ pendingUrls: pendingUrls.length, visited: visitedForSkip.length, reason }, "[crawl] auto-enqueuing continuation job");
499
+ nextJobId = await enqueueJob("CRAWL", {
500
+ ...config,
501
+ startUrls: pendingUrls,
502
+ skipUrls: [...config.skipUrls ?? [], ...visitedForSkip]
503
+ }).catch((autoErr) => {
504
+ logger.warn({ err: autoErr?.message }, "[crawl] auto-segment enqueue failed (non-fatal)");
505
+ return void 0;
506
+ });
474
507
  }
475
508
  await withTenant(
476
509
  tenantId,
@@ -478,7 +511,7 @@ async function processJob(jobId, config, opts) {
478
511
  where: { id: jobId },
479
512
  data: {
480
513
  status: "DONE",
481
- result: { ...result },
514
+ result: { ...result, ...nextJobId ? { nextJobId } : {} },
482
515
  progress: 100,
483
516
  finishedAt: /* @__PURE__ */ new Date()
484
517
  }
@@ -586,7 +619,7 @@ async function processJob(jobId, config, opts) {
586
619
  try {
587
620
  const key = buildArtifactKey(tenantId, projectId, "dom-snapshots", `${screen.id}.json`, tenantStorageCfg.prefix);
588
621
  const cloudUrl = await uploadFileToTenantStorage(tenantStorageCfg, domPath, key, "application/json");
589
- await AppBrain.updateScreenDomSnapshot(tenantId, screen.id, cloudUrl);
622
+ await AppBrain.updateScreenDomSnapshot(tenantId, screen.id, domPath, cloudUrl);
590
623
  domUploaded++;
591
624
  } catch (err) {
592
625
  logger.warn({ screenId: screen.id, err }, "Failed to upload DOM snapshot to cloud storage");
@@ -647,8 +680,18 @@ async function processJob(jobId, config, opts) {
647
680
  clearInterval(heartbeatTimer);
648
681
  runningControllers.delete(jobId);
649
682
  const aborted = String(err).includes("CRAWL_ABORTED");
683
+ const authFailure = !aborted && (String(err).includes("AUTH_FAILURE") || String(err).includes("SESSION_EXPIRED"));
650
684
  const checkpoint = jobCheckpoints.get(jobId);
651
685
  jobCheckpoints.delete(jobId);
686
+ if (authFailure && (checkpoint?.pendingQueue?.length ?? 0) > 0) {
687
+ const pendingAfterAuth = checkpoint.pendingQueue;
688
+ logger.info({ pending: pendingAfterAuth.length }, "[crawl] auth failure mid-crawl \u2014 auto-enqueuing continuation with pending URLs");
689
+ enqueueJob("CRAWL", {
690
+ ...config,
691
+ startUrls: pendingAfterAuth,
692
+ skipUrls: [...config.skipUrls ?? [], ...checkpoint.visitedUrls ?? []]
693
+ }).catch((enqErr) => logger.warn({ err: enqErr?.message }, "[crawl] auth-failure auto-segment enqueue failed (non-fatal)"));
694
+ }
652
695
  await withTenant(
653
696
  tenantId,
654
697
  (tx) => tx.platformJob.update({