@detiq/crawler 1.1.17 → 1.1.19
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 +44 -12
- package/dist/job-queue.js.map +2 -2
- package/package.json +1 -1
- package/src/job-queue.ts +61 -14
package/package.json
CHANGED
package/src/job-queue.ts
CHANGED
|
@@ -240,8 +240,25 @@ export async function claimNextQueuedJob(): Promise<ClaimedJob | null> {
|
|
|
240
240
|
|
|
241
241
|
/** Requeues RUNNING jobs whose heartbeat has gone stale — the worker that claimed them
|
|
242
242
|
* died (crash, restart, OOM) without ever marking them DONE/FAILED. Idempotent and
|
|
243
|
-
* safe to run from every worker process; only affects rows past the timeout.
|
|
243
|
+
* safe to run from every worker process; only affects rows past the timeout.
|
|
244
|
+
* API-in-process job types (GENERATE_SCRIPT etc.) are failed directly — requeueing them
|
|
245
|
+
* would cause the worker to claim and fail them with a confusing "claimed by worker" error. */
|
|
244
246
|
export async function requeueStaleRunningJobs(): Promise<void> {
|
|
247
|
+
// Directly fail stale API-in-process jobs — worker cannot process these types.
|
|
248
|
+
const failedRows = await prismaAdmin.$queryRaw<Array<{ id: string }>>`
|
|
249
|
+
UPDATE "PlatformJob"
|
|
250
|
+
SET status = 'FAILED',
|
|
251
|
+
"finishedAt" = NOW(),
|
|
252
|
+
error = 'API process was restarted while this job was running — please retry from the UI.'
|
|
253
|
+
WHERE status = 'RUNNING'
|
|
254
|
+
AND type IN ('GENERATE_SCRIPT', 'GENERATE_DATA', 'GENERATE_TESTS', 'GENERATE_TEST_PLAN')
|
|
255
|
+
AND COALESCE("heartbeatAt", "startedAt") < NOW() - (${STALE_JOB_TIMEOUT_MINUTES} * interval '1 minute')
|
|
256
|
+
RETURNING id
|
|
257
|
+
`;
|
|
258
|
+
if (failedRows.length > 0) {
|
|
259
|
+
logger.warn({ jobIds: failedRows.map((r: any) => r.id), staleTimeoutMinutes: STALE_JOB_TIMEOUT_MINUTES }, '[job-worker] failed stale API-in-process job(s) — API restarted while running');
|
|
260
|
+
}
|
|
261
|
+
|
|
245
262
|
const rows = await prismaAdmin.$queryRaw<Array<{ id: string }>>`
|
|
246
263
|
UPDATE "PlatformJob"
|
|
247
264
|
SET status = 'QUEUED',
|
|
@@ -253,6 +270,7 @@ export async function requeueStaleRunningJobs(): Promise<void> {
|
|
|
253
270
|
true
|
|
254
271
|
)
|
|
255
272
|
WHERE status = 'RUNNING'
|
|
273
|
+
AND type NOT IN ('GENERATE_SCRIPT', 'GENERATE_DATA', 'GENERATE_TESTS', 'GENERATE_TEST_PLAN')
|
|
256
274
|
AND COALESCE("heartbeatAt", "startedAt") < NOW() - (${STALE_JOB_TIMEOUT_MINUTES} * interval '1 minute')
|
|
257
275
|
RETURNING id
|
|
258
276
|
`;
|
|
@@ -1064,19 +1082,25 @@ async function processFigmaImportJob(jobId: string, config: any) {
|
|
|
1064
1082
|
tx.platformJob.update({ where: { id: jobId }, data: { status: 'RUNNING', startedAt: new Date() } })
|
|
1065
1083
|
);
|
|
1066
1084
|
try {
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
const cfg = connector.config as Record<string, string>;
|
|
1073
|
-
const accessToken = cfg.accessToken_enc ? decrypt(cfg.accessToken_enc) : '';
|
|
1074
|
-
if (!accessToken) throw new Error('Figma access token missing. Edit the Figma connector and add your Personal Access Token.');
|
|
1075
|
-
|
|
1076
|
-
const resolvedFileKey = fileKey ?? cfg.fileKey ?? '';
|
|
1085
|
+
// Prefer OAuth token (connected via Figma OAuth flow), fall back to PAT connector.
|
|
1086
|
+
const oauthConn = await AppBrain.getOAuthConnection(tenantId, projectId, 'FIGMA');
|
|
1087
|
+
let creds: figma.FigmaCredentials;
|
|
1088
|
+
const resolvedFileKey = fileKey ?? '';
|
|
1077
1089
|
if (!resolvedFileKey) throw new Error('No Figma file key. Paste the full Figma file URL as the App URL.');
|
|
1078
1090
|
|
|
1079
|
-
|
|
1091
|
+
if (oauthConn?.accessToken && oauthConn.status === 'CONNECTED') {
|
|
1092
|
+
// OAuth token must use Authorization: Bearer header (oauthToken field), not X-Figma-Token
|
|
1093
|
+
creds = { accessToken: '', oauthToken: oauthConn.accessToken, fileKey: resolvedFileKey };
|
|
1094
|
+
} else {
|
|
1095
|
+
const connector = await withTenant(tenantId, (tx) =>
|
|
1096
|
+
(tx as any).connector.findFirst({ where: { projectId, kind: 'FIGMA', tenantId } })
|
|
1097
|
+
) as any;
|
|
1098
|
+
if (!connector) throw new Error('Figma connector not configured. Add your Figma token in Project Settings → Integrations.');
|
|
1099
|
+
const cfg = connector.config as Record<string, string>;
|
|
1100
|
+
const pat = cfg.accessToken_enc ? decrypt(cfg.accessToken_enc) : '';
|
|
1101
|
+
if (!pat) throw new Error('Figma access token missing. Edit the Figma connector and add your Personal Access Token.');
|
|
1102
|
+
creds = { accessToken: pat, fileKey: resolvedFileKey };
|
|
1103
|
+
}
|
|
1080
1104
|
|
|
1081
1105
|
await updateJobProgress(tenantId, jobId, 5, 'Listing frames from Figma file…');
|
|
1082
1106
|
const frames = await figma.listScreens(creds, resolvedFileKey);
|
|
@@ -1118,7 +1142,9 @@ async function processFigmaImportJob(jobId: string, config: any) {
|
|
|
1118
1142
|
console.warn('[figma-import] cloud upload failed:', e);
|
|
1119
1143
|
}
|
|
1120
1144
|
}
|
|
1121
|
-
|
|
1145
|
+
const w = (frame as any).boundingBox?.width ?? 1440;
|
|
1146
|
+
const viewport = w <= 480 ? 'MOBILE_PORTRAIT' : w <= 1024 ? 'TABLET' : 'DESKTOP';
|
|
1147
|
+
await AppBrain.saveScreenshot(tenantId, projectId, screen.id, storagePath, 1, viewport, cloudUrl);
|
|
1122
1148
|
}
|
|
1123
1149
|
}
|
|
1124
1150
|
|
|
@@ -2502,7 +2528,7 @@ jobDispatchTable = {
|
|
|
2502
2528
|
BATCH_GENERATE_ALIGNED: processBatchGenerateAlignedJob,
|
|
2503
2529
|
CREATE_PR: processCreatePRJob,
|
|
2504
2530
|
NEW_PAGES_PIPELINE: processNewPagesPipelineJob,
|
|
2505
|
-
//
|
|
2531
|
+
// The following job types are processed in-process by the API (never QUEUED).
|
|
2506
2532
|
// If the worker claims one, the API crashed after creating it — mark failed so it's visible.
|
|
2507
2533
|
GENERATE_TESTS: async (jobId: string, _config: any) => {
|
|
2508
2534
|
logger.error({ jobId }, '[job-worker] GENERATE_TESTS claimed by worker — API did not process it in-process');
|
|
@@ -2511,6 +2537,27 @@ jobDispatchTable = {
|
|
|
2511
2537
|
data: { status: 'FAILED', error: 'Test generation was not completed by the API — please retry from the UI.', finishedAt: new Date() },
|
|
2512
2538
|
}).catch(() => {});
|
|
2513
2539
|
},
|
|
2540
|
+
GENERATE_DATA: async (jobId: string, _config: any) => {
|
|
2541
|
+
logger.error({ jobId }, '[job-worker] GENERATE_DATA claimed by worker — API did not process it in-process');
|
|
2542
|
+
await prismaAdmin.platformJob.update({
|
|
2543
|
+
where: { id: jobId },
|
|
2544
|
+
data: { status: 'FAILED', error: 'Data generation was not completed by the API — please retry from the UI.', finishedAt: new Date() },
|
|
2545
|
+
}).catch(() => {});
|
|
2546
|
+
},
|
|
2547
|
+
GENERATE_SCRIPT: async (jobId: string, _config: any) => {
|
|
2548
|
+
logger.error({ jobId }, '[job-worker] GENERATE_SCRIPT claimed by worker — API did not process it in-process');
|
|
2549
|
+
await prismaAdmin.platformJob.update({
|
|
2550
|
+
where: { id: jobId },
|
|
2551
|
+
data: { status: 'FAILED', error: 'Script generation was not completed by the API — please retry from the UI.', finishedAt: new Date() },
|
|
2552
|
+
}).catch(() => {});
|
|
2553
|
+
},
|
|
2554
|
+
GENERATE_TEST_PLAN: async (jobId: string, _config: any) => {
|
|
2555
|
+
logger.error({ jobId }, '[job-worker] GENERATE_TEST_PLAN claimed by worker — API did not process it in-process');
|
|
2556
|
+
await prismaAdmin.platformJob.update({
|
|
2557
|
+
where: { id: jobId },
|
|
2558
|
+
data: { status: 'FAILED', error: 'Test plan generation was not completed by the API — please retry from the UI.', finishedAt: new Date() },
|
|
2559
|
+
}).catch(() => {});
|
|
2560
|
+
},
|
|
2514
2561
|
};
|
|
2515
2562
|
|
|
2516
2563
|
async function processRepoScanJob(jobId: string, config: any) {
|