@detiq/crawler 1.1.18 → 1.1.20

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({
@@ -2076,7 +2119,7 @@ jobDispatchTable = {
2076
2119
  BATCH_GENERATE_ALIGNED: processBatchGenerateAlignedJob,
2077
2120
  CREATE_PR: processCreatePRJob,
2078
2121
  NEW_PAGES_PIPELINE: processNewPagesPipelineJob,
2079
- // GENERATE_TESTS is processed in-process by the API (never QUEUED).
2122
+ // The following job types are processed in-process by the API (never QUEUED).
2080
2123
  // If the worker claims one, the API crashed after creating it — mark failed so it's visible.
2081
2124
  GENERATE_TESTS: async (jobId, _config) => {
2082
2125
  logger.error({ jobId }, "[job-worker] GENERATE_TESTS claimed by worker \u2014 API did not process it in-process");
@@ -2085,6 +2128,30 @@ jobDispatchTable = {
2085
2128
  data: { status: "FAILED", error: "Test generation was not completed by the API \u2014 please retry from the UI.", finishedAt: /* @__PURE__ */ new Date() }
2086
2129
  }).catch(() => {
2087
2130
  });
2131
+ },
2132
+ GENERATE_DATA: async (jobId, _config) => {
2133
+ logger.error({ jobId }, "[job-worker] GENERATE_DATA claimed by worker \u2014 API did not process it in-process");
2134
+ await prismaAdmin.platformJob.update({
2135
+ where: { id: jobId },
2136
+ data: { status: "FAILED", error: "Data generation was not completed by the API \u2014 please retry from the UI.", finishedAt: /* @__PURE__ */ new Date() }
2137
+ }).catch(() => {
2138
+ });
2139
+ },
2140
+ GENERATE_SCRIPT: async (jobId, _config) => {
2141
+ logger.error({ jobId }, "[job-worker] GENERATE_SCRIPT claimed by worker \u2014 API did not process it in-process");
2142
+ await prismaAdmin.platformJob.update({
2143
+ where: { id: jobId },
2144
+ data: { status: "FAILED", error: "Script generation was not completed by the API \u2014 please retry from the UI.", finishedAt: /* @__PURE__ */ new Date() }
2145
+ }).catch(() => {
2146
+ });
2147
+ },
2148
+ GENERATE_TEST_PLAN: async (jobId, _config) => {
2149
+ logger.error({ jobId }, "[job-worker] GENERATE_TEST_PLAN claimed by worker \u2014 API did not process it in-process");
2150
+ await prismaAdmin.platformJob.update({
2151
+ where: { id: jobId },
2152
+ data: { status: "FAILED", error: "Test plan generation was not completed by the API \u2014 please retry from the UI.", finishedAt: /* @__PURE__ */ new Date() }
2153
+ }).catch(() => {
2154
+ });
2088
2155
  }
2089
2156
  };
2090
2157
  async function processRepoScanJob(jobId, config) {