@prenta/core 8.0.0 → 8.1.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/api/routes/scheduling.d.ts.map +1 -1
  3. package/dist/api/routes/scheduling.js +6 -47
  4. package/dist/api/routes/scheduling.js.map +1 -1
  5. package/dist/content-health/cron.d.ts +16 -0
  6. package/dist/content-health/cron.d.ts.map +1 -1
  7. package/dist/content-health/cron.js +18 -4
  8. package/dist/content-health/cron.js.map +1 -1
  9. package/dist/db/model-types.d.ts +15 -0
  10. package/dist/db/model-types.d.ts.map +1 -1
  11. package/dist/generated/browser.d.ts +8 -0
  12. package/dist/generated/client.d.ts +8 -0
  13. package/dist/generated/internal/class.d.ts +11 -0
  14. package/dist/generated/internal/class.js +4 -4
  15. package/dist/generated/internal/prismaNamespace.d.ts +99 -1
  16. package/dist/generated/internal/prismaNamespace.js +22 -1
  17. package/dist/generated/internal/prismaNamespaceBrowser.d.ts +23 -0
  18. package/dist/generated/internal/prismaNamespaceBrowser.js +22 -1
  19. package/dist/generated/models/Job.d.ts +1243 -0
  20. package/dist/generated/models/Job.js +1 -0
  21. package/dist/generated/models.d.ts +1 -0
  22. package/dist/jobs/handlers.d.ts +4 -0
  23. package/dist/jobs/handlers.d.ts.map +1 -0
  24. package/dist/jobs/handlers.js +95 -0
  25. package/dist/jobs/handlers.js.map +1 -0
  26. package/dist/jobs/memory-store.d.ts +13 -0
  27. package/dist/jobs/memory-store.d.ts.map +1 -0
  28. package/dist/jobs/memory-store.js +55 -0
  29. package/dist/jobs/memory-store.js.map +1 -0
  30. package/dist/jobs/prisma-store.d.ts +13 -0
  31. package/dist/jobs/prisma-store.d.ts.map +1 -0
  32. package/dist/jobs/prisma-store.js +113 -0
  33. package/dist/jobs/prisma-store.js.map +1 -0
  34. package/dist/jobs/queue.d.ts +17 -0
  35. package/dist/jobs/queue.d.ts.map +1 -0
  36. package/dist/jobs/queue.js +134 -0
  37. package/dist/jobs/queue.js.map +1 -0
  38. package/dist/jobs/routes.d.ts +10 -0
  39. package/dist/jobs/routes.d.ts.map +1 -0
  40. package/dist/jobs/routes.js +61 -0
  41. package/dist/jobs/routes.js.map +1 -0
  42. package/dist/jobs/seo-scan-tick.d.ts +12 -0
  43. package/dist/jobs/seo-scan-tick.d.ts.map +1 -0
  44. package/dist/jobs/seo-scan-tick.js +41 -0
  45. package/dist/jobs/seo-scan-tick.js.map +1 -0
  46. package/dist/jobs/types.d.ts +52 -0
  47. package/dist/jobs/types.d.ts.map +1 -0
  48. package/dist/jobs/types.js +2 -0
  49. package/dist/jobs/types.js.map +1 -0
  50. package/package.json +1 -1
  51. package/prisma/migrations/0022_jobs/migration.sql +20 -0
  52. package/prisma/schema.prisma +22 -0
@@ -0,0 +1 @@
1
+ export {};
@@ -37,4 +37,5 @@ export type * from './models/ContentIssue.js';
37
37
  export type * from './models/DocumentCRDT.js';
38
38
  export type * from './models/DocumentComment.js';
39
39
  export type * from './models/DocumentNotification.js';
40
+ export type * from './models/Job.js';
40
41
  export type * from './commonInputTypes.js';
@@ -0,0 +1,4 @@
1
+ import type { PrentaDB } from '../db/client-types.js';
2
+ import type { JobHandler } from './types.js';
3
+ export declare function createJobHandlers(db: PrentaDB): Record<string, JobHandler>;
4
+ //# sourceMappingURL=handlers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/jobs/handlers.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AAErD,OAAO,KAAK,EAAE,UAAU,EAAe,MAAM,YAAY,CAAA;AAgBzD,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CA8E1E"}
@@ -0,0 +1,95 @@
1
+ import { processContentHealthScan, resolveUnseenContentIssues } from '../content-health/cron.js';
2
+ import { getPrentaConfig } from '../config/runtime.js';
3
+ import { runSeoScanTick } from './seo-scan-tick.js';
4
+ /** Documents per content-health slice. Small enough to finish inside the worker budget. */
5
+ const CONTENT_HEALTH_SLICE = 25;
6
+ function siteOrigin() {
7
+ const config = getPrentaConfig();
8
+ return config?.site?.url ?? config?.seo?.siteUrl ?? process.env.NEXT_PUBLIC_SITE_URL ?? null;
9
+ }
10
+ function stringList(value) {
11
+ return Array.isArray(value)
12
+ ? value.filter((item) => typeof item === 'string')
13
+ : [];
14
+ }
15
+ export function createJobHandlers(db) {
16
+ return {
17
+ 'seo-scan': async () => {
18
+ const tick = await runSeoScanTick(db);
19
+ const failed = typeof tick.audit.error === 'string';
20
+ if (failed)
21
+ throw new Error(tick.audit.error);
22
+ const message = typeof tick.audit.skipped === 'string'
23
+ ? tick.audit.skipped
24
+ : `Scanned ${String(tick.audit.pagesScanned ?? 0)} pages`;
25
+ return {
26
+ done: true,
27
+ progress: { completed: 1, total: 1, message },
28
+ };
29
+ },
30
+ 'content-health': async (job) => {
31
+ const seen = new Set(stringList(job.payload.seen));
32
+ const scannedBefore = typeof job.payload.scanned === 'number' ? job.payload.scanned : 0;
33
+ const cursor = typeof job.payload.cursor === 'string' ? job.payload.cursor : undefined;
34
+ const result = await processContentHealthScan(db, {
35
+ maxDocuments: CONTENT_HEALTH_SLICE,
36
+ startCursor: cursor,
37
+ seen,
38
+ prune: false,
39
+ siteOrigin: siteOrigin(),
40
+ });
41
+ const scanned = scannedBefore + result.scanned;
42
+ const progress = {
43
+ completed: scanned,
44
+ total: result.truncated ? scanned + 1 : scanned,
45
+ message: result.truncated
46
+ ? `Scanned ${scanned} documents, more remain`
47
+ : `Scanned ${scanned} documents`,
48
+ };
49
+ if (result.truncated && result.cursor) {
50
+ return {
51
+ done: false,
52
+ progress,
53
+ payload: {
54
+ ...job.payload,
55
+ cursor: result.cursor,
56
+ seen: result.seenKeys,
57
+ scanned,
58
+ },
59
+ };
60
+ }
61
+ const resolved = await resolveUnseenContentIssues(db, result.seenKeys);
62
+ return {
63
+ done: true,
64
+ progress: {
65
+ ...progress,
66
+ message: `Scanned ${scanned} documents, resolved ${resolved}`,
67
+ },
68
+ };
69
+ },
70
+ 'batch-ai': async (job) => {
71
+ const items = Array.isArray(job.payload.items) ? job.payload.items : [];
72
+ const index = typeof job.payload.index === 'number' ? job.payload.index : 0;
73
+ if (index >= items.length) {
74
+ return {
75
+ done: true,
76
+ progress: { completed: items.length, total: items.length, message: 'Batch finished' },
77
+ };
78
+ }
79
+ const next = index + 1;
80
+ const progress = {
81
+ completed: next,
82
+ total: items.length,
83
+ message: `Processed ${next} of ${items.length}`,
84
+ };
85
+ if (next >= items.length)
86
+ return { done: true, progress };
87
+ return {
88
+ done: false,
89
+ progress,
90
+ payload: { ...job.payload, index: next },
91
+ };
92
+ },
93
+ };
94
+ }
95
+ //# sourceMappingURL=handlers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handlers.js","sourceRoot":"","sources":["../../src/jobs/handlers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { JobRecord, JobStore } from './types.js';
2
+ /** In-process store for the queue tests. Production uses {@link PrismaJobStore}. */
3
+ export declare class MemoryJobStore implements JobStore {
4
+ private readonly jobs;
5
+ insert(job: JobRecord): Promise<void>;
6
+ findActive(type: string): Promise<JobRecord | null>;
7
+ claimNext(now: Date): Promise<JobRecord | null>;
8
+ findStaleRunning(cutoff: Date): Promise<JobRecord[]>;
9
+ save(job: JobRecord): Promise<void>;
10
+ list(limit: number): Promise<JobRecord[]>;
11
+ private clone;
12
+ }
13
+ //# sourceMappingURL=memory-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../../src/jobs/memory-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAErD,oFAAoF;AACpF,qBAAa,cAAe,YAAW,QAAQ;IAC7C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA+B;IAE9C,MAAM,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IASnD,SAAS,CAAC,GAAG,EAAE,IAAI,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAe/C,gBAAgB,CAAC,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAWpD,IAAI,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAInC,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAO/C,OAAO,CAAC,KAAK;CAWd"}
@@ -0,0 +1,55 @@
1
+ /** In-process store for the queue tests. Production uses {@link PrismaJobStore}. */
2
+ export class MemoryJobStore {
3
+ jobs = new Map();
4
+ async insert(job) {
5
+ this.jobs.set(job.id, { ...job, payload: { ...job.payload }, progress: { ...job.progress } });
6
+ }
7
+ async findActive(type) {
8
+ for (const job of this.jobs.values()) {
9
+ if (job.type === type && (job.status === 'queued' || job.status === 'running')) {
10
+ return this.clone(job);
11
+ }
12
+ }
13
+ return null;
14
+ }
15
+ async claimNext(now) {
16
+ const due = [...this.jobs.values()]
17
+ .filter((job) => job.status === 'queued' && job.runAt.getTime() <= now.getTime())
18
+ .sort((a, b) => a.runAt.getTime() - b.runAt.getTime() || a.createdAt.getTime() - b.createdAt.getTime());
19
+ const job = due[0];
20
+ if (!job)
21
+ return null;
22
+ job.status = 'running';
23
+ job.lockedAt = now;
24
+ job.updatedAt = now;
25
+ return this.clone(job);
26
+ }
27
+ async findStaleRunning(cutoff) {
28
+ return [...this.jobs.values()]
29
+ .filter((job) => job.status === 'running' &&
30
+ job.lockedAt !== null &&
31
+ job.lockedAt.getTime() < cutoff.getTime())
32
+ .map((job) => this.clone(job));
33
+ }
34
+ async save(job) {
35
+ this.jobs.set(job.id, { ...job, payload: { ...job.payload }, progress: { ...job.progress } });
36
+ }
37
+ async list(limit) {
38
+ return [...this.jobs.values()]
39
+ .sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())
40
+ .slice(0, limit)
41
+ .map((job) => this.clone(job));
42
+ }
43
+ clone(job) {
44
+ return {
45
+ ...job,
46
+ payload: { ...job.payload },
47
+ progress: { ...job.progress },
48
+ runAt: new Date(job.runAt),
49
+ lockedAt: job.lockedAt ? new Date(job.lockedAt) : null,
50
+ createdAt: new Date(job.createdAt),
51
+ updatedAt: new Date(job.updatedAt),
52
+ };
53
+ }
54
+ }
55
+ //# sourceMappingURL=memory-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-store.js","sourceRoot":"","sources":["../../src/jobs/memory-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { PrentaDB } from '../db/client-types.js';
2
+ import type { JobRecord, JobStore } from './types.js';
3
+ export declare class PrismaJobStore implements JobStore {
4
+ private readonly db;
5
+ constructor(db: PrentaDB);
6
+ insert(job: JobRecord): Promise<void>;
7
+ findActive(type: string): Promise<JobRecord | null>;
8
+ claimNext(now: Date): Promise<JobRecord | null>;
9
+ findStaleRunning(cutoff: Date): Promise<JobRecord[]>;
10
+ save(job: JobRecord): Promise<void>;
11
+ list(limit: number): Promise<JobRecord[]>;
12
+ }
13
+ //# sourceMappingURL=prisma-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prisma-store.d.ts","sourceRoot":"","sources":["../../src/jobs/prisma-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,KAAK,EAAe,SAAS,EAAa,QAAQ,EAAE,MAAM,YAAY,CAAA;AAqD7E,qBAAa,cAAe,YAAW,QAAQ;IACjC,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,QAAQ;IAEnC,MAAM,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBrC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAQnD,SAAS,CAAC,GAAG,EAAE,IAAI,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAc/C,gBAAgB,CAAC,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAQpD,IAAI,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBnC,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;CAOhD"}
@@ -0,0 +1,113 @@
1
+ function asRecord(value) {
2
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
3
+ return value;
4
+ }
5
+ return {};
6
+ }
7
+ function asProgress(value) {
8
+ const record = asRecord(value);
9
+ return {
10
+ completed: typeof record.completed === 'number' ? record.completed : 0,
11
+ total: typeof record.total === 'number' ? record.total : 0,
12
+ ...(typeof record.message === 'string' ? { message: record.message } : {}),
13
+ };
14
+ }
15
+ function asStatus(value) {
16
+ if (value === 'running' || value === 'succeeded' || value === 'dead')
17
+ return value;
18
+ return 'queued';
19
+ }
20
+ function toRecord(row) {
21
+ return {
22
+ id: row.id,
23
+ type: row.type,
24
+ payload: asRecord(row.payload),
25
+ status: asStatus(row.status),
26
+ attempts: row.attempts,
27
+ maxAttempts: row.maxAttempts,
28
+ runAt: row.runAt,
29
+ lockedAt: row.lockedAt,
30
+ lastError: row.lastError,
31
+ progress: asProgress(row.progress),
32
+ createdAt: row.createdAt,
33
+ updatedAt: row.updatedAt,
34
+ };
35
+ }
36
+ export class PrismaJobStore {
37
+ db;
38
+ constructor(db) {
39
+ this.db = db;
40
+ }
41
+ async insert(job) {
42
+ await this.db.job.create({
43
+ data: {
44
+ id: job.id,
45
+ type: job.type,
46
+ payload: job.payload,
47
+ status: job.status,
48
+ attempts: job.attempts,
49
+ maxAttempts: job.maxAttempts,
50
+ runAt: job.runAt,
51
+ lockedAt: job.lockedAt,
52
+ lastError: job.lastError,
53
+ progress: job.progress,
54
+ createdAt: job.createdAt,
55
+ updatedAt: job.updatedAt,
56
+ },
57
+ });
58
+ }
59
+ async findActive(type) {
60
+ const row = await this.db.job.findFirst({
61
+ where: { type, status: { in: ['queued', 'running'] } },
62
+ orderBy: { createdAt: 'asc' },
63
+ });
64
+ return row ? toRecord(row) : null;
65
+ }
66
+ async claimNext(now) {
67
+ const row = await this.db.job.findFirst({
68
+ where: { status: 'queued', runAt: { lte: now } },
69
+ orderBy: [{ runAt: 'asc' }, { createdAt: 'asc' }],
70
+ });
71
+ if (!row)
72
+ return null;
73
+ const claimed = await this.db.job.updateMany({
74
+ where: { id: row.id, status: 'queued' },
75
+ data: { status: 'running', lockedAt: now, updatedAt: now },
76
+ });
77
+ if (claimed.count !== 1)
78
+ return null;
79
+ return toRecord({ ...row, status: 'running', lockedAt: now, updatedAt: now });
80
+ }
81
+ async findStaleRunning(cutoff) {
82
+ const rows = await this.db.job.findMany({
83
+ where: { status: 'running', lockedAt: { lt: cutoff } },
84
+ orderBy: { lockedAt: 'asc' },
85
+ });
86
+ return rows.map((row) => toRecord(row));
87
+ }
88
+ async save(job) {
89
+ await this.db.job.update({
90
+ where: { id: job.id },
91
+ data: {
92
+ type: job.type,
93
+ payload: job.payload,
94
+ status: job.status,
95
+ attempts: job.attempts,
96
+ maxAttempts: job.maxAttempts,
97
+ runAt: job.runAt,
98
+ lockedAt: job.lockedAt,
99
+ lastError: job.lastError,
100
+ progress: job.progress,
101
+ updatedAt: job.updatedAt,
102
+ },
103
+ });
104
+ }
105
+ async list(limit) {
106
+ const rows = await this.db.job.findMany({
107
+ orderBy: { updatedAt: 'desc' },
108
+ take: limit,
109
+ });
110
+ return rows.map((row) => toRecord(row));
111
+ }
112
+ }
113
+ //# sourceMappingURL=prisma-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prisma-store.js","sourceRoot":"","sources":["../../src/jobs/prisma-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type { JobHandler, JobRecord, JobStore, QueueTickResult } from './types.js';
2
+ export declare function enqueueJob(store: JobStore, input: {
3
+ type: string;
4
+ payload?: Record<string, unknown>;
5
+ maxAttempts?: number;
6
+ now?: Date;
7
+ dedupe?: boolean;
8
+ }): Promise<JobRecord>;
9
+ export declare function processJobQueue(options: {
10
+ store: JobStore;
11
+ handlers: Record<string, JobHandler>;
12
+ now?: Date;
13
+ budgetMs?: number;
14
+ staleLockMs?: number;
15
+ clock?: () => Date;
16
+ }): Promise<QueueTickResult>;
17
+ //# sourceMappingURL=queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue.d.ts","sourceRoot":"","sources":["../../src/jobs/queue.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAe,SAAS,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAiB/F,wBAAsB,UAAU,CAC9B,KAAK,EAAE,QAAQ,EACf,KAAK,EAAE;IACL,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACjC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,GAAG,CAAC,EAAE,IAAI,CAAA;IACV,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,GACA,OAAO,CAAC,SAAS,CAAC,CAsBpB;AAkED,wBAAsB,eAAe,CAAC,OAAO,EAAE;IAC7C,KAAK,EAAE,QAAQ,CAAA;IACf,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;IACpC,GAAG,CAAC,EAAE,IAAI,CAAA;IACV,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,IAAI,CAAA;CACnB,GAAG,OAAO,CAAC,eAAe,CAAC,CAuD3B"}
@@ -0,0 +1,134 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ const BACKOFF_BASE_MS = 30_000;
3
+ const BACKOFF_CAP_MS = 15 * 60_000;
4
+ const DEFAULT_MAX_ATTEMPTS = 3;
5
+ const DEFAULT_BUDGET_MS = 20_000;
6
+ const DEFAULT_STALE_LOCK_MS = 10 * 60_000;
7
+ function emptyProgress() {
8
+ return { completed: 0, total: 0 };
9
+ }
10
+ function backoffMs(attempts) {
11
+ const exp = Math.min(Math.max(attempts - 1, 0), 8);
12
+ return Math.min(BACKOFF_CAP_MS, BACKOFF_BASE_MS * 2 ** exp);
13
+ }
14
+ export async function enqueueJob(store, input) {
15
+ const now = input.now ?? new Date();
16
+ if (input.dedupe !== false) {
17
+ const active = await store.findActive(input.type);
18
+ if (active)
19
+ return active;
20
+ }
21
+ const job = {
22
+ id: randomUUID(),
23
+ type: input.type,
24
+ payload: input.payload ?? {},
25
+ status: 'queued',
26
+ attempts: 0,
27
+ maxAttempts: input.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
28
+ runAt: now,
29
+ lockedAt: null,
30
+ lastError: null,
31
+ progress: emptyProgress(),
32
+ createdAt: now,
33
+ updatedAt: now,
34
+ };
35
+ await store.insert(job);
36
+ return job;
37
+ }
38
+ /** Count a failure. Returns the row to queued with backoff, or dead-letters it. */
39
+ async function failJob(store, job, error, now) {
40
+ const attempts = job.attempts + 1;
41
+ const dead = attempts >= job.maxAttempts;
42
+ const next = {
43
+ ...job,
44
+ attempts,
45
+ status: dead ? 'dead' : 'queued',
46
+ runAt: dead ? job.runAt : new Date(now.getTime() + backoffMs(attempts)),
47
+ lockedAt: null,
48
+ lastError: error,
49
+ updatedAt: now,
50
+ };
51
+ await store.save(next);
52
+ return next;
53
+ }
54
+ /** A voluntary slice. Attempts stay put so a long job is not punished for yielding. */
55
+ async function yieldJob(store, job, progress, payload, now) {
56
+ const next = {
57
+ ...job,
58
+ status: 'queued',
59
+ runAt: now,
60
+ lockedAt: null,
61
+ progress,
62
+ payload,
63
+ updatedAt: now,
64
+ };
65
+ await store.save(next);
66
+ return next;
67
+ }
68
+ async function completeJob(store, job, progress, now) {
69
+ const next = {
70
+ ...job,
71
+ status: 'succeeded',
72
+ lockedAt: null,
73
+ progress: progress ?? {
74
+ completed: job.progress.total || 1,
75
+ total: job.progress.total || 1,
76
+ message: job.progress.message,
77
+ },
78
+ updatedAt: now,
79
+ };
80
+ await store.save(next);
81
+ return next;
82
+ }
83
+ export async function processJobQueue(options) {
84
+ const clock = options.clock ?? (() => new Date());
85
+ const started = options.now ?? clock();
86
+ const budgetMs = options.budgetMs ?? DEFAULT_BUDGET_MS;
87
+ const deadline = new Date(started.getTime() + budgetMs);
88
+ const staleLockMs = options.staleLockMs ?? DEFAULT_STALE_LOCK_MS;
89
+ const result = { succeeded: 0, continued: 0, retried: 0, dead: 0 };
90
+ const cutoff = new Date(started.getTime() - staleLockMs);
91
+ for (const stale of await options.store.findStaleRunning(cutoff)) {
92
+ const failed = await failJob(options.store, stale, 'Worker stopped before the job finished', started);
93
+ if (failed.status === 'dead')
94
+ result.dead++;
95
+ else
96
+ result.retried++;
97
+ }
98
+ while (clock().getTime() < deadline.getTime()) {
99
+ const now = clock();
100
+ const job = await options.store.claimNext(now);
101
+ if (!job)
102
+ break;
103
+ const handler = options.handlers[job.type];
104
+ if (!handler) {
105
+ const failed = await failJob(options.store, job, `Unknown job type: ${job.type}`, now);
106
+ if (failed.status === 'dead')
107
+ result.dead++;
108
+ else
109
+ result.retried++;
110
+ continue;
111
+ }
112
+ try {
113
+ const outcome = await handler(job, { now, deadline });
114
+ if (outcome.done) {
115
+ await completeJob(options.store, job, outcome.progress, clock());
116
+ result.succeeded++;
117
+ }
118
+ else {
119
+ await yieldJob(options.store, job, outcome.progress, outcome.payload ?? job.payload, clock());
120
+ result.continued++;
121
+ }
122
+ }
123
+ catch (err) {
124
+ const message = err instanceof Error ? err.message : String(err);
125
+ const failed = await failJob(options.store, job, message, clock());
126
+ if (failed.status === 'dead')
127
+ result.dead++;
128
+ else
129
+ result.retried++;
130
+ }
131
+ }
132
+ return result;
133
+ }
134
+ //# sourceMappingURL=queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue.js","sourceRoot":"","sources":["../../src/jobs/queue.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { ApiRouter } from '../api/router.js';
2
+ import { guardedDb } from '../api/guarded-db.js';
3
+ export declare function registerJobRoutes(router: ApiRouter): void;
4
+ /** Queue a cron's work and return immediately. An active job of the same type is reused. */
5
+ export declare function enqueueCronJob(db: ReturnType<typeof guardedDb>, type: string): Promise<{
6
+ queued: true;
7
+ jobId: string;
8
+ type: string;
9
+ }>;
10
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/jobs/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAEjD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AAwBhD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAgCzD;AAED,4FAA4F;AAC5F,wBAAsB,cAAc,CAClC,EAAE,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,EAChC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAGxD"}
@@ -0,0 +1,61 @@
1
+ import { isAuthorizedCronRequest } from '../cron/index.js';
2
+ import { guardedDb } from '../api/guarded-db.js';
3
+ import { errorResponse, internalError, json, requireAuth } from '../api/route-helpers.js';
4
+ import { createJobHandlers } from './handlers.js';
5
+ import { PrismaJobStore } from './prisma-store.js';
6
+ import { enqueueJob, processJobQueue } from './queue.js';
7
+ const WORKER_BUDGET_MS = 20_000;
8
+ function serializeJob(job) {
9
+ return {
10
+ id: job.id,
11
+ type: job.type,
12
+ status: job.status,
13
+ attempts: job.attempts,
14
+ maxAttempts: job.maxAttempts,
15
+ lastError: job.lastError,
16
+ progress: job.progress,
17
+ runAt: job.runAt.toISOString(),
18
+ createdAt: job.createdAt.toISOString(),
19
+ updatedAt: job.updatedAt.toISOString(),
20
+ };
21
+ }
22
+ export function registerJobRoutes(router) {
23
+ const db = guardedDb;
24
+ const cronJobs = async (request) => {
25
+ try {
26
+ if (!isAuthorizedCronRequest(request.headers.get('authorization'))) {
27
+ return errorResponse('Unauthorized', 401);
28
+ }
29
+ const client = db();
30
+ const summary = await processJobQueue({
31
+ store: new PrismaJobStore(client),
32
+ handlers: createJobHandlers(client),
33
+ budgetMs: WORKER_BUDGET_MS,
34
+ });
35
+ return json({ data: summary });
36
+ }
37
+ catch (err) {
38
+ return internalError(err, 'cron jobs');
39
+ }
40
+ };
41
+ router.get('/cron/jobs', cronJobs);
42
+ router.post('/cron/jobs', cronJobs);
43
+ router.get('/jobs', async (request) => {
44
+ try {
45
+ const auth = await requireAuth(request);
46
+ if (auth.error)
47
+ return auth.error;
48
+ const jobs = await new PrismaJobStore(db()).list(50);
49
+ return json({ data: { jobs: jobs.map(serializeJob) } });
50
+ }
51
+ catch (err) {
52
+ return internalError(err, 'list jobs');
53
+ }
54
+ });
55
+ }
56
+ /** Queue a cron's work and return immediately. An active job of the same type is reused. */
57
+ export async function enqueueCronJob(db, type) {
58
+ const job = await enqueueJob(new PrismaJobStore(db), { type });
59
+ return { queued: true, jobId: job.id, type: job.type };
60
+ }
61
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/jobs/routes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import { type DigestSendResult } from '../seo/digest.js';
2
+ import type { PrentaDB } from '../db/client-types.js';
3
+ /**
4
+ * One SEO audit + digest attempt. Failures stay in the result so a retry
5
+ * policy can see them, and a thrown audit error does not discard the digest.
6
+ */
7
+ export declare function runSeoScanTick(db: PrentaDB): Promise<{
8
+ swept: number;
9
+ audit: Record<string, unknown>;
10
+ digest: DigestSendResult;
11
+ }>;
12
+ //# sourceMappingURL=seo-scan-tick.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seo-scan-tick.d.ts","sourceRoot":"","sources":["../../src/jobs/seo-scan-tick.ts"],"names":[],"mappings":"AAMA,OAAO,EAAsB,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAE5E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AAErD;;;GAGG;AACH,wBAAsB,cAAc,CAAC,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC;IAC1D,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,MAAM,EAAE,gBAAgB,CAAA;CACzB,CAAC,CAgCD"}