@agentsean/scheduler 1.0.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.
@@ -0,0 +1,106 @@
1
+ /** GSC insights for the Search performance screen. Default metric is clicks. */
2
+ const IMPRESSIONS_BUG_BEGIN = "2025-05-13";
3
+ const IMPRESSIONS_BUG_END = "2026-04-27";
4
+ export function impressionsWindowContaminated(dates) {
5
+ return dates.some((d) => d >= IMPRESSIONS_BUG_BEGIN && d <= IMPRESSIONS_BUG_END);
6
+ }
7
+ export function computeGscInsights(opts) {
8
+ const now = opts.now ?? new Date();
9
+ const today = now.toISOString().slice(0, 10);
10
+ const weekAgo = new Date(now.getTime() - 7 * 86400000).toISOString().slice(0, 10);
11
+ const twoWeeks = new Date(now.getTime() - 14 * 86400000).toISOString().slice(0, 10);
12
+ const pageAgg = new Map();
13
+ for (const row of opts.pages) {
14
+ if (row.date < weekAgo || row.date > today)
15
+ continue;
16
+ const cur = pageAgg.get(row.page) ?? { clicks: 0, position: 0, n: 0 };
17
+ cur.clicks += row.clicks;
18
+ if (row.position !== null) {
19
+ cur.position += row.position;
20
+ cur.n++;
21
+ }
22
+ pageAgg.set(row.page, cur);
23
+ }
24
+ const strikingDistance = [...pageAgg.entries()]
25
+ .map(([page, v]) => ({
26
+ page,
27
+ clicks: v.clicks,
28
+ position: v.n ? v.position / v.n : 0,
29
+ }))
30
+ .filter((r) => r.position >= 8 && r.position <= 20)
31
+ .toSorted((a, b) => b.clicks - a.clicks)
32
+ .slice(0, 25);
33
+ const currentClicks = opts.days
34
+ .filter((d) => d.date > weekAgo && d.date <= today)
35
+ .reduce((s, d) => s + d.clicks, 0);
36
+ const previousClicks = opts.days
37
+ .filter((d) => d.date > twoWeeks && d.date <= weekAgo)
38
+ .reduce((s, d) => s + d.clicks, 0);
39
+ const delta = currentClicks - previousClicks;
40
+ const deltaPct = previousClicks > 0 ? delta / previousClicks : null;
41
+ const queryAgg = new Map();
42
+ for (const row of opts.queries) {
43
+ if (row.date < weekAgo)
44
+ continue;
45
+ const cur = queryAgg.get(row.query) ?? { clicks: 0, impressions: 0 };
46
+ cur.clicks += row.clicks;
47
+ cur.impressions += row.impressions;
48
+ queryAgg.set(row.query, cur);
49
+ }
50
+ const ctrOutliers = [...queryAgg.entries()]
51
+ .map(([query, v]) => ({
52
+ query,
53
+ clicks: v.clicks,
54
+ impressions: v.impressions,
55
+ ctr: v.impressions > 0 ? v.clicks / v.impressions : 0,
56
+ }))
57
+ .filter((r) => r.impressions >= 100 && r.ctr < 0.02 && r.clicks < 5)
58
+ .toSorted((a, b) => b.impressions - a.impressions)
59
+ .slice(0, 25);
60
+ const terms = opts.brandTerms.map((t) => t.toLowerCase()).filter(Boolean);
61
+ let brandClicks = 0;
62
+ let nonBrandClicks = 0;
63
+ for (const [query, v] of queryAgg) {
64
+ const q = query.toLowerCase();
65
+ if (terms.some((t) => q.includes(t)))
66
+ brandClicks += v.clicks;
67
+ else
68
+ nonBrandClicks += v.clicks;
69
+ }
70
+ const total = brandClicks + nonBrandClicks;
71
+ const dates = [
72
+ ...opts.days.map((d) => d.date),
73
+ ...opts.pages.map((p) => p.date),
74
+ ...opts.queries.map((q) => q.date),
75
+ ];
76
+ return {
77
+ metric: "clicks",
78
+ impressionsContaminated: impressionsWindowContaminated(dates),
79
+ strikingDistance,
80
+ decay: {
81
+ previousClicks,
82
+ currentClicks,
83
+ delta,
84
+ deltaPct,
85
+ },
86
+ cannibalization: [],
87
+ cannibalizationNote: "Query×page GSC pull is required for cannibalization; weekly query and page totals are stored separately.",
88
+ ctrOutliers,
89
+ brand: {
90
+ brandClicks,
91
+ nonBrandClicks,
92
+ brandShare: total > 0 ? brandClicks / total : null,
93
+ },
94
+ };
95
+ }
96
+ export function brandTermsFromOrigin(origin) {
97
+ try {
98
+ const host = new URL(origin).hostname.replace(/^www\./, "");
99
+ const head = host.split(".")[0] ?? host;
100
+ return head.length >= 3 ? [head] : [];
101
+ }
102
+ catch {
103
+ return [];
104
+ }
105
+ }
106
+ //# sourceMappingURL=insights.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"insights.js","sourceRoot":"","sources":["../src/insights.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import type { JobKind, JobQueue } from "./types.js";
2
+ /**
3
+ * Duck-typed pg-boss surface. Hosted tier constructs `new PgBoss(DATABASE_URL)`
4
+ * and passes it here. Local installs never import pg-boss — they use the jobs table.
5
+ */
6
+ export type PgBossLike = {
7
+ send(name: string, data: object, options?: {
8
+ singletonKey?: string;
9
+ startAfter?: number | Date | string;
10
+ }): Promise<string | null>;
11
+ fetch(name: string, batchSize?: number): Promise<Array<{
12
+ id: string;
13
+ data: object;
14
+ retrycount?: number;
15
+ }> | null>;
16
+ complete(id: string, data?: object): Promise<void>;
17
+ fail(id: string, data?: object | Error): Promise<void>;
18
+ cancel(id: string): Promise<void>;
19
+ };
20
+ /**
21
+ * pg-boss adapter behind the same JobQueue interface as the SQLite queue.
22
+ */
23
+ export declare function createPgBossQueue(boss: PgBossLike, options?: {
24
+ now?: (() => Date) | undefined;
25
+ }): JobQueue;
26
+ export declare function assertJobKind(kind: string): JobKind;
27
+ //# sourceMappingURL=pg-boss.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pg-boss.d.ts","sourceRoot":"","sources":["../src/pg-boss.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,OAAO,EAEP,QAAQ,EAET,MAAM,YAAY,CAAC;AAIpB;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,CACF,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAAA;KAAE,GACvE,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1B,KAAK,CACH,IAAI,EAAE,MAAM,EACZ,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,IAAI,CAAC,CAAC;IAC5E,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnC,CAAC;AASF;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,UAAU,EAChB,OAAO,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAA;CAAE,GAC3C,QAAQ,CAmIV;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGnD"}
@@ -0,0 +1,134 @@
1
+ import { JOB_KINDS } from "./types.js";
2
+ import { isJobKind } from "./cadence.js";
3
+ /**
4
+ * pg-boss adapter behind the same JobQueue interface as the SQLite queue.
5
+ */
6
+ export function createPgBossQueue(boss, options) {
7
+ const clock = () => options?.now?.() ?? new Date();
8
+ const byId = new Map();
9
+ const stamp = (kind, row, status) => {
10
+ const data = row.data;
11
+ const job = {
12
+ id: row.id,
13
+ siteId: data.siteId ?? null,
14
+ kind,
15
+ status,
16
+ idempotencyKey: data.idempotencyKey ?? row.id,
17
+ payload: data.payload ?? {},
18
+ attempts: row.retrycount ?? 0,
19
+ runAt: clock().toISOString(),
20
+ heartbeatAt: clock().toISOString(),
21
+ error: null,
22
+ createdAt: data.createdAt ?? clock().toISOString(),
23
+ completedAt: null,
24
+ };
25
+ byId.set(job.id, job);
26
+ return job;
27
+ };
28
+ return {
29
+ async enqueue(input) {
30
+ const createdAt = clock().toISOString();
31
+ const data = {
32
+ siteId: input.siteId ?? null,
33
+ idempotencyKey: input.idempotencyKey,
34
+ payload: input.payload ?? {},
35
+ createdAt,
36
+ };
37
+ const startAfter = input.runAt
38
+ ? input.runAt instanceof Date
39
+ ? input.runAt
40
+ : new Date(input.runAt)
41
+ : undefined;
42
+ const id = (await boss.send(input.kind, data, {
43
+ singletonKey: input.idempotencyKey,
44
+ ...(startAfter ? { startAfter } : {}),
45
+ })) ?? input.idempotencyKey;
46
+ const job = {
47
+ id,
48
+ siteId: input.siteId ?? null,
49
+ kind: input.kind,
50
+ status: "queued",
51
+ idempotencyKey: input.idempotencyKey,
52
+ payload: input.payload ?? {},
53
+ attempts: 0,
54
+ runAt: startAfter ? startAfter.toISOString() : createdAt,
55
+ heartbeatAt: null,
56
+ error: null,
57
+ createdAt,
58
+ completedAt: null,
59
+ };
60
+ byId.set(id, job);
61
+ return job;
62
+ },
63
+ async claimDue(limit) {
64
+ const out = [];
65
+ for (const kind of JOB_KINDS) {
66
+ if (out.length >= limit)
67
+ break;
68
+ const batch = (await boss.fetch(kind, limit - out.length)) ?? [];
69
+ for (const row of batch)
70
+ out.push(stamp(kind, row, "running"));
71
+ }
72
+ return out;
73
+ },
74
+ async heartbeat(id) {
75
+ const job = byId.get(id);
76
+ if (job)
77
+ byId.set(id, { ...job, heartbeatAt: clock().toISOString() });
78
+ },
79
+ async checkpoint(id, payload) {
80
+ const job = byId.get(id);
81
+ if (job) {
82
+ byId.set(id, {
83
+ ...job,
84
+ payload: { ...job.payload, ...payload },
85
+ heartbeatAt: clock().toISOString(),
86
+ });
87
+ }
88
+ },
89
+ async complete(id, result) {
90
+ await boss.complete(id, result ? { result } : undefined);
91
+ const job = byId.get(id);
92
+ if (job) {
93
+ byId.set(id, {
94
+ ...job,
95
+ status: "completed",
96
+ completedAt: clock().toISOString(),
97
+ });
98
+ }
99
+ },
100
+ async fail(id, error) {
101
+ const err = error instanceof Error ? error : new Error(String(error));
102
+ await boss.fail(id, err);
103
+ const job = byId.get(id);
104
+ if (job)
105
+ byId.set(id, { ...job, status: "queued", error: err.message });
106
+ },
107
+ async recoverStale() {
108
+ return 0;
109
+ },
110
+ async cancel(id) {
111
+ await boss.cancel(id);
112
+ const job = byId.get(id);
113
+ if (job)
114
+ byId.set(id, { ...job, status: "cancelled" });
115
+ },
116
+ async get(id) {
117
+ return byId.get(id) ?? null;
118
+ },
119
+ async list(filter) {
120
+ let rows = [...byId.values()];
121
+ if (filter?.siteId)
122
+ rows = rows.filter((j) => j.siteId === filter.siteId);
123
+ if (filter?.status)
124
+ rows = rows.filter((j) => j.status === filter.status);
125
+ return rows;
126
+ },
127
+ };
128
+ }
129
+ export function assertJobKind(kind) {
130
+ if (!isJobKind(kind))
131
+ throw new Error(`unknown job kind ${kind}`);
132
+ return kind;
133
+ }
134
+ //# sourceMappingURL=pg-boss.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pg-boss.js","sourceRoot":"","sources":["../src/pg-boss.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import { type SqliteDatabase } from "@agentsean/db";
2
+ import { type JobHandler, type JobKind, type JobQueue } from "./types.js";
3
+ export type TickResult = {
4
+ recovered: number;
5
+ scheduled: number;
6
+ ran: number;
7
+ errors: string[];
8
+ };
9
+ export declare function ensureCadences(queue: JobQueue, db: SqliteDatabase, now: Date): Promise<number>;
10
+ export declare function runDue(queue: JobQueue, handlers: Partial<Record<JobKind, JobHandler>>, opts: {
11
+ halted: boolean;
12
+ now: Date;
13
+ limit?: number | undefined;
14
+ }): Promise<{
15
+ ran: number;
16
+ errors: string[];
17
+ }>;
18
+ export declare function tick(queue: JobQueue, handlers: Partial<Record<JobKind, JobHandler>>, opts: {
19
+ db: SqliteDatabase;
20
+ halted: boolean;
21
+ now: Date;
22
+ limit?: number | undefined;
23
+ }): Promise<TickResult>;
24
+ export declare function startLoop(queue: JobQueue, handlers: Partial<Record<JobKind, JobHandler>>, opts: {
25
+ db: SqliteDatabase;
26
+ halted: () => boolean;
27
+ now?: (() => Date) | undefined;
28
+ intervalMs?: number | undefined;
29
+ onTick?: ((result: TickResult) => void) | undefined;
30
+ }): () => Promise<void>;
31
+ //# sourceMappingURL=runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAG3D,OAAO,EAAa,KAAK,UAAU,EAAE,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAErF,MAAM,MAAM,UAAU,GAAG;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAEF,wBAAsB,cAAc,CAClC,KAAK,EAAE,QAAQ,EACf,EAAE,EAAE,cAAc,EAClB,GAAG,EAAE,IAAI,GACR,OAAO,CAAC,MAAM,CAAC,CAuBjB;AAED,wBAAsB,MAAM,CAC1B,KAAK,EAAE,QAAQ,EACf,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC,EAC9C,IAAI,EAAE;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GAC/D,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CA6B5C;AAED,wBAAsB,IAAI,CACxB,KAAK,EAAE,QAAQ,EACf,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC,EAC9C,IAAI,EAAE;IAAE,EAAE,EAAE,cAAc,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GACnF,OAAO,CAAC,UAAU,CAAC,CAKrB;AAED,wBAAgB,SAAS,CACvB,KAAK,EAAE,QAAQ,EACf,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC,EAC9C,IAAI,EAAE;IACJ,EAAE,EAAE,cAAc,CAAC;IACnB,MAAM,EAAE,MAAM,OAAO,CAAC;IACtB,GAAG,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,MAAM,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;CACrD,GACA,MAAM,OAAO,CAAC,IAAI,CAAC,CAuBrB"}
package/dist/runner.js ADDED
@@ -0,0 +1,90 @@
1
+ import { sites } from "@agentsean/db";
2
+ import { rankCadenceForTenant, tenantIdForSite } from "@agentsean/hosted";
3
+ import { CADENCES, DAY_MS, WEEK_MS, idempotencyKey } from "./cadence.js";
4
+ import { JOB_KINDS } from "./types.js";
5
+ export async function ensureCadences(queue, db, now) {
6
+ const rows = db.select().from(sites).all();
7
+ let n = 0;
8
+ for (const site of rows) {
9
+ const tenantId = tenantIdForSite(db, site.id);
10
+ const rankMs = tenantId && rankCadenceForTenant(db, tenantId) === "daily" ? DAY_MS : WEEK_MS;
11
+ for (const kind of JOB_KINDS) {
12
+ const cadenceMs = kind === "rank_check" ? rankMs : CADENCES[kind].everyMs;
13
+ const key = idempotencyKey(site.id, kind, now);
14
+ const job = await queue.enqueue({
15
+ siteId: site.id,
16
+ kind,
17
+ idempotencyKey: key,
18
+ payload: { origin: site.origin, cadenceMs },
19
+ runAt: now,
20
+ });
21
+ if (job.createdAt && new Date(job.createdAt).getTime() >= now.getTime() - 2000) {
22
+ n++;
23
+ }
24
+ }
25
+ }
26
+ return n;
27
+ }
28
+ export async function runDue(queue, handlers, opts) {
29
+ const claimed = await queue.claimDue(opts.limit ?? 5);
30
+ const errors = [];
31
+ for (const job of claimed) {
32
+ const handler = handlers[job.kind];
33
+ if (!handler) {
34
+ await queue.fail(job.id, new Error(`no handler for ${job.kind}`));
35
+ errors.push(`${job.id}: no handler for ${job.kind}`);
36
+ continue;
37
+ }
38
+ try {
39
+ const result = await handler(job, {
40
+ now: opts.now,
41
+ halted: opts.halted,
42
+ heartbeat: () => {
43
+ void queue.heartbeat(job.id);
44
+ },
45
+ checkpoint: (payload) => {
46
+ void queue.checkpoint(job.id, payload);
47
+ },
48
+ });
49
+ await queue.complete(job.id, result);
50
+ }
51
+ catch (err) {
52
+ const message = err instanceof Error ? err.message : String(err);
53
+ await queue.fail(job.id, err);
54
+ errors.push(`${job.id}: ${message}`);
55
+ }
56
+ }
57
+ return { ran: claimed.length, errors };
58
+ }
59
+ export async function tick(queue, handlers, opts) {
60
+ const recovered = await queue.recoverStale();
61
+ const scheduled = await ensureCadences(queue, opts.db, opts.now);
62
+ const { ran, errors } = await runDue(queue, handlers, opts);
63
+ return { recovered, scheduled, ran, errors };
64
+ }
65
+ export function startLoop(queue, handlers, opts) {
66
+ let stopped = false;
67
+ let inflight = Promise.resolve();
68
+ const intervalMs = opts.intervalMs ?? 15_000;
69
+ const run = () => {
70
+ if (stopped)
71
+ return;
72
+ inflight = tick(queue, handlers, {
73
+ db: opts.db,
74
+ halted: opts.halted(),
75
+ now: opts.now?.() ?? new Date(),
76
+ })
77
+ .then((result) => opts.onTick?.(result))
78
+ .catch(() => {
79
+ /* the next interval retries; crash recovery requeues stale work */
80
+ });
81
+ };
82
+ run();
83
+ const handle = setInterval(run, intervalMs);
84
+ return async () => {
85
+ stopped = true;
86
+ clearInterval(handle);
87
+ await inflight;
88
+ };
89
+ }
90
+ //# sourceMappingURL=runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.js","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import { type SqliteDatabase } from "@agentsean/db";
2
+ import type { Clock, JobKind, JobQueue, JobStatus } from "./types.js";
3
+ export type SqliteQueueOptions = {
4
+ now?: Clock | undefined;
5
+ };
6
+ export declare function createSqliteQueue(db: SqliteDatabase, options?: SqliteQueueOptions): JobQueue;
7
+ export declare function parseJobKind(kind: string): JobKind;
8
+ export type { JobStatus };
9
+ //# sourceMappingURL=sqlite-queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite-queue.d.ts","sourceRoot":"","sources":["../src/sqlite-queue.ts"],"names":[],"mappings":"AAEA,OAAO,EAAQ,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAG1D,OAAO,KAAK,EACV,KAAK,EAGL,OAAO,EAEP,QAAQ,EACR,SAAS,EACV,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,kBAAkB,GAAG;IAC/B,GAAG,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC;CACzB,CAAC;AA+BF,wBAAgB,iBAAiB,CAC/B,EAAE,EAAE,cAAc,EAClB,OAAO,GAAE,kBAAuB,GAC/B,QAAQ,CA4LV;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGlD;AAED,YAAY,EAAE,SAAS,EAAE,CAAC"}
@@ -0,0 +1,226 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { and, eq, lte } from "drizzle-orm";
3
+ import { jobs } from "@agentsean/db";
4
+ import { backoffMs, MAX_ATTEMPTS, STALE_HEARTBEAT_MS } from "./backoff.js";
5
+ import { isJobKind } from "./cadence.js";
6
+ function parsePayload(raw) {
7
+ if (!raw)
8
+ return {};
9
+ try {
10
+ const v = JSON.parse(raw);
11
+ if (v && typeof v === "object" && !Array.isArray(v))
12
+ return v;
13
+ }
14
+ catch {
15
+ // ignore
16
+ }
17
+ return {};
18
+ }
19
+ function asJob(row) {
20
+ if (!isJobKind(row.kind))
21
+ return null;
22
+ return {
23
+ id: row.id,
24
+ siteId: row.siteId,
25
+ kind: row.kind,
26
+ status: row.status,
27
+ idempotencyKey: row.idempotencyKey,
28
+ payload: parsePayload(row.payload),
29
+ attempts: row.attempts,
30
+ runAt: row.runAt,
31
+ heartbeatAt: row.heartbeatAt,
32
+ error: row.error,
33
+ createdAt: row.createdAt,
34
+ completedAt: row.completedAt,
35
+ };
36
+ }
37
+ export function createSqliteQueue(db, options = {}) {
38
+ const now = () => options.now?.() ?? new Date();
39
+ const nowIso = () => now().toISOString();
40
+ const getSync = (id) => {
41
+ const row = db.select().from(jobs).where(eq(jobs.id, id)).get();
42
+ return row ? asJob(row) : null;
43
+ };
44
+ const queue = {
45
+ async enqueue(input) {
46
+ const existing = db
47
+ .select()
48
+ .from(jobs)
49
+ .where(eq(jobs.idempotencyKey, input.idempotencyKey))
50
+ .get();
51
+ if (existing) {
52
+ const job = asJob(existing);
53
+ if (job)
54
+ return job;
55
+ }
56
+ const id = randomUUID();
57
+ const createdAt = nowIso();
58
+ const runAt = input.runAt instanceof Date
59
+ ? input.runAt.toISOString()
60
+ : (input.runAt ?? createdAt);
61
+ try {
62
+ db.insert(jobs)
63
+ .values({
64
+ id,
65
+ siteId: input.siteId ?? null,
66
+ kind: input.kind,
67
+ status: "queued",
68
+ idempotencyKey: input.idempotencyKey,
69
+ payload: JSON.stringify(input.payload ?? {}),
70
+ attempts: 0,
71
+ runAt,
72
+ heartbeatAt: null,
73
+ error: null,
74
+ createdAt,
75
+ completedAt: null,
76
+ })
77
+ .run();
78
+ }
79
+ catch {
80
+ const raced = db
81
+ .select()
82
+ .from(jobs)
83
+ .where(eq(jobs.idempotencyKey, input.idempotencyKey))
84
+ .get();
85
+ if (raced) {
86
+ const job = asJob(raced);
87
+ if (job)
88
+ return job;
89
+ }
90
+ throw new Error("failed to enqueue job");
91
+ }
92
+ const created = getSync(id);
93
+ if (!created)
94
+ throw new Error("failed to load enqueued job");
95
+ return created;
96
+ },
97
+ async claimDue(limit) {
98
+ const cap = Math.min(Math.max(1, limit), 25);
99
+ const due = db
100
+ .select()
101
+ .from(jobs)
102
+ .where(and(eq(jobs.status, "queued"), lte(jobs.runAt, nowIso())))
103
+ .all()
104
+ .slice(0, cap);
105
+ const claimed = [];
106
+ const ts = nowIso();
107
+ for (const row of due) {
108
+ db.update(jobs)
109
+ .set({
110
+ status: "running",
111
+ attempts: row.attempts + 1,
112
+ heartbeatAt: ts,
113
+ })
114
+ .where(eq(jobs.id, row.id))
115
+ .run();
116
+ const job = getSync(row.id);
117
+ if (job)
118
+ claimed.push(job);
119
+ }
120
+ return claimed;
121
+ },
122
+ async heartbeat(id) {
123
+ db.update(jobs).set({ heartbeatAt: nowIso() }).where(eq(jobs.id, id)).run();
124
+ },
125
+ async checkpoint(id, payload) {
126
+ const row = db.select().from(jobs).where(eq(jobs.id, id)).get();
127
+ const prev = parsePayload(row?.payload ?? null);
128
+ db.update(jobs)
129
+ .set({
130
+ payload: JSON.stringify({ ...prev, ...payload }),
131
+ heartbeatAt: nowIso(),
132
+ })
133
+ .where(eq(jobs.id, id))
134
+ .run();
135
+ },
136
+ async complete(id, result) {
137
+ const row = db.select().from(jobs).where(eq(jobs.id, id)).get();
138
+ const prev = parsePayload(row?.payload ?? null);
139
+ const next = result === undefined ? prev : { ...prev, result };
140
+ db.update(jobs)
141
+ .set({
142
+ status: "completed",
143
+ completedAt: nowIso(),
144
+ heartbeatAt: nowIso(),
145
+ error: null,
146
+ payload: JSON.stringify(next),
147
+ })
148
+ .where(eq(jobs.id, id))
149
+ .run();
150
+ },
151
+ async fail(id, error) {
152
+ const row = db.select().from(jobs).where(eq(jobs.id, id)).get();
153
+ const attempts = row?.attempts ?? 1;
154
+ const message = error instanceof Error ? error.message : String(error);
155
+ const ts = nowIso();
156
+ if (attempts >= MAX_ATTEMPTS) {
157
+ db.update(jobs)
158
+ .set({
159
+ status: "failed",
160
+ error: message,
161
+ completedAt: ts,
162
+ heartbeatAt: ts,
163
+ })
164
+ .where(eq(jobs.id, id))
165
+ .run();
166
+ return;
167
+ }
168
+ const runAt = new Date(now().getTime() + backoffMs(attempts)).toISOString();
169
+ db.update(jobs)
170
+ .set({
171
+ status: "queued",
172
+ error: message,
173
+ runAt,
174
+ heartbeatAt: ts,
175
+ })
176
+ .where(eq(jobs.id, id))
177
+ .run();
178
+ },
179
+ async recoverStale() {
180
+ const cutoff = new Date(now().getTime() - STALE_HEARTBEAT_MS).toISOString();
181
+ const running = db.select().from(jobs).where(eq(jobs.status, "running")).all();
182
+ let n = 0;
183
+ for (const row of running) {
184
+ if (row.heartbeatAt && row.heartbeatAt >= cutoff)
185
+ continue;
186
+ db.update(jobs)
187
+ .set({
188
+ status: "queued",
189
+ error: "stale_heartbeat",
190
+ runAt: nowIso(),
191
+ })
192
+ .where(eq(jobs.id, row.id))
193
+ .run();
194
+ n++;
195
+ }
196
+ return n;
197
+ },
198
+ async cancel(id) {
199
+ db.update(jobs)
200
+ .set({ status: "cancelled", completedAt: nowIso() })
201
+ .where(eq(jobs.id, id))
202
+ .run();
203
+ },
204
+ async get(id) {
205
+ return getSync(id);
206
+ },
207
+ async list(filter) {
208
+ let rows = db.select().from(jobs).all();
209
+ if (filter?.siteId)
210
+ rows = rows.filter((r) => r.siteId === filter.siteId);
211
+ if (filter?.status)
212
+ rows = rows.filter((r) => r.status === filter.status);
213
+ return rows
214
+ .map(asJob)
215
+ .filter((j) => j !== null)
216
+ .toSorted((a, b) => (a.createdAt < b.createdAt ? 1 : -1));
217
+ },
218
+ };
219
+ return queue;
220
+ }
221
+ export function parseJobKind(kind) {
222
+ if (!isJobKind(kind))
223
+ throw new Error(`unknown job kind ${kind}`);
224
+ return kind;
225
+ }
226
+ //# sourceMappingURL=sqlite-queue.js.map