@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/auth.js +12 -2
- package/dist/auth.js.map +2 -2
- package/dist/crawler.js +370 -155
- package/dist/crawler.js.map +2 -2
- package/dist/job-queue.js +71 -4
- package/dist/job-queue.js.map +2 -2
- package/dist/native-crawler.js +6 -6
- package/dist/native-crawler.js.map +2 -2
- package/package.json +1 -1
- package/src/auth.ts +13 -1
- package/src/crawler.ts +307 -60
- package/src/job-queue.ts +71 -5
- package/src/native-crawler.ts +6 -4
- package/types/crawler.d.ts +4 -0
- package/types/job-queue.d.ts +3 -1
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
|
-
|
|
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
|
-
//
|
|
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) {
|