@contractspec/lib.jobs 0.0.0-canary-20260113162409
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/LICENSE +21 -0
- package/README.md +126 -0
- package/dist/_virtual/rolldown_runtime.js +36 -0
- package/dist/contracts/index.d.ts +547 -0
- package/dist/contracts/index.d.ts.map +1 -0
- package/dist/contracts/index.js +482 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/entities/index.d.ts +145 -0
- package/dist/entities/index.d.ts.map +1 -0
- package/dist/entities/index.js +198 -0
- package/dist/entities/index.js.map +1 -0
- package/dist/events.d.ts +388 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +353 -0
- package/dist/events.js.map +1 -0
- package/dist/handlers/gmail-sync-handler.d.ts +10 -0
- package/dist/handlers/gmail-sync-handler.d.ts.map +1 -0
- package/dist/handlers/gmail-sync-handler.js +10 -0
- package/dist/handlers/gmail-sync-handler.js.map +1 -0
- package/dist/handlers/index.d.ts +10 -0
- package/dist/handlers/index.d.ts.map +1 -0
- package/dist/handlers/index.js +13 -0
- package/dist/handlers/index.js.map +1 -0
- package/dist/handlers/ping-job.d.ts +11 -0
- package/dist/handlers/ping-job.d.ts.map +1 -0
- package/dist/handlers/ping-job.js +14 -0
- package/dist/handlers/ping-job.js.map +1 -0
- package/dist/handlers/storage-document-handler.d.ts +13 -0
- package/dist/handlers/storage-document-handler.d.ts.map +1 -0
- package/dist/handlers/storage-document-handler.js +15 -0
- package/dist/handlers/storage-document-handler.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +67 -0
- package/dist/index.js.map +1 -0
- package/dist/jobs.capability.d.ts +8 -0
- package/dist/jobs.capability.d.ts.map +1 -0
- package/dist/jobs.capability.js +33 -0
- package/dist/jobs.capability.js.map +1 -0
- package/dist/jobs.feature.d.ts +12 -0
- package/dist/jobs.feature.d.ts.map +1 -0
- package/dist/jobs.feature.js +110 -0
- package/dist/jobs.feature.js.map +1 -0
- package/dist/queue/gcp-cloud-tasks.d.ts +42 -0
- package/dist/queue/gcp-cloud-tasks.d.ts.map +1 -0
- package/dist/queue/gcp-cloud-tasks.js +61 -0
- package/dist/queue/gcp-cloud-tasks.js.map +1 -0
- package/dist/queue/gcp-pubsub.d.ts +26 -0
- package/dist/queue/gcp-pubsub.d.ts.map +1 -0
- package/dist/queue/gcp-pubsub.js +47 -0
- package/dist/queue/gcp-pubsub.js.map +1 -0
- package/dist/queue/index.d.ts +16 -0
- package/dist/queue/index.d.ts.map +1 -0
- package/dist/queue/index.js +23 -0
- package/dist/queue/index.js.map +1 -0
- package/dist/queue/memory-queue.d.ts +35 -0
- package/dist/queue/memory-queue.d.ts.map +1 -0
- package/dist/queue/memory-queue.js +140 -0
- package/dist/queue/memory-queue.js.map +1 -0
- package/dist/queue/register-defined-job.d.ts +8 -0
- package/dist/queue/register-defined-job.d.ts.map +1 -0
- package/dist/queue/register-defined-job.js +16 -0
- package/dist/queue/register-defined-job.js.map +1 -0
- package/dist/queue/scaleway-sqs-queue.d.ts +39 -0
- package/dist/queue/scaleway-sqs-queue.d.ts.map +1 -0
- package/dist/queue/scaleway-sqs-queue.js +175 -0
- package/dist/queue/scaleway-sqs-queue.js.map +1 -0
- package/dist/queue/types.d.ts +8 -0
- package/dist/queue/types.d.ts.map +1 -0
- package/dist/queue/types.js +12 -0
- package/dist/queue/types.js.map +1 -0
- package/dist/scheduler/index.d.ts +93 -0
- package/dist/scheduler/index.d.ts.map +1 -0
- package/dist/scheduler/index.js +146 -0
- package/dist/scheduler/index.js.map +1 -0
- package/package.json +97 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/queue/register-defined-job.ts
|
|
2
|
+
function registerDefinedJob(queue, def) {
|
|
3
|
+
const wrapped = async (job) => {
|
|
4
|
+
const payload = def.schema.parse(job.payload);
|
|
5
|
+
const typedJob = {
|
|
6
|
+
...job,
|
|
7
|
+
payload
|
|
8
|
+
};
|
|
9
|
+
await def.handler(payload, typedJob);
|
|
10
|
+
};
|
|
11
|
+
queue.register(def.type, wrapped);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
export { registerDefinedJob };
|
|
16
|
+
//# sourceMappingURL=register-defined-job.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register-defined-job.js","names":[],"sources":["../../src/queue/register-defined-job.ts"],"sourcesContent":["import type { DefinedJob } from '@contractspec/lib.contracts/jobs/define-job';\nimport type {\n Job,\n JobHandler,\n JobQueue,\n} from '@contractspec/lib.contracts/jobs/queue';\n\nexport function registerDefinedJob<TPayload>(\n queue: JobQueue,\n def: DefinedJob<TPayload>\n): void {\n const wrapped: JobHandler<unknown> = async (job) => {\n const payload = def.schema.parse(job.payload);\n const typedJob: Job<TPayload> = {\n ...(job as Job<unknown>),\n payload,\n } as Job<TPayload>;\n\n await def.handler(payload, typedJob);\n };\n\n queue.register<TPayload>(def.type, wrapped as JobHandler<TPayload>);\n}\n"],"mappings":";AAOA,SAAgB,mBACd,OACA,KACM;CACN,MAAM,UAA+B,OAAO,QAAQ;EAClD,MAAM,UAAU,IAAI,OAAO,MAAM,IAAI,QAAQ;EAC7C,MAAM,WAA0B;GAC9B,GAAI;GACJ;GACD;AAED,QAAM,IAAI,QAAQ,SAAS,SAAS;;AAGtC,OAAM,SAAmB,IAAI,MAAM,QAAgC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { types_d_exports } from "./types.js";
|
|
2
|
+
import { Logger } from "@contractspec/lib.logger";
|
|
3
|
+
|
|
4
|
+
//#region src/queue/scaleway-sqs-queue.d.ts
|
|
5
|
+
interface ScalewaySqsQueueCredentials {
|
|
6
|
+
accessKeyId: string;
|
|
7
|
+
secretAccessKey: string;
|
|
8
|
+
}
|
|
9
|
+
interface ScalewaySqsQueueConfig {
|
|
10
|
+
queueUrl: string;
|
|
11
|
+
region?: string;
|
|
12
|
+
endpoint?: string;
|
|
13
|
+
waitTimeSeconds?: number;
|
|
14
|
+
maxNumberOfMessages?: number;
|
|
15
|
+
visibilityTimeoutSeconds?: number;
|
|
16
|
+
credentials?: ScalewaySqsQueueCredentials;
|
|
17
|
+
logger?: Logger;
|
|
18
|
+
}
|
|
19
|
+
declare class ScalewaySqsJobQueue implements types_d_exports.JobQueue {
|
|
20
|
+
private readonly sqs;
|
|
21
|
+
private readonly queueUrl;
|
|
22
|
+
private readonly waitTimeSeconds;
|
|
23
|
+
private readonly maxNumberOfMessages;
|
|
24
|
+
private readonly visibilityTimeoutSeconds;
|
|
25
|
+
private readonly handlers;
|
|
26
|
+
private readonly logger?;
|
|
27
|
+
private running;
|
|
28
|
+
constructor(config: ScalewaySqsQueueConfig);
|
|
29
|
+
enqueue<TPayload>(jobType: string, payload: TPayload, options?: types_d_exports.EnqueueOptions): Promise<types_d_exports.Job<TPayload>>;
|
|
30
|
+
register<TPayload, TResult = void>(jobType: string, handler: types_d_exports.JobHandler<TPayload, TResult>): void;
|
|
31
|
+
start(): void;
|
|
32
|
+
stop(): Promise<void>;
|
|
33
|
+
private pollLoop;
|
|
34
|
+
private deleteMessage;
|
|
35
|
+
private sleep;
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { ScalewaySqsJobQueue, ScalewaySqsQueueConfig, ScalewaySqsQueueCredentials };
|
|
39
|
+
//# sourceMappingURL=scaleway-sqs-queue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scaleway-sqs-queue.d.ts","names":[],"sources":["../../src/queue/scaleway-sqs-queue.ts"],"sourcesContent":[],"mappings":";;;;UAWiB,2BAAA;;EAAA,eAAA,EAAA,MAAA;AAKjB;AAiBa,UAjBI,sBAAA,CAiBgB;EAUX,QAAA,EAAA,MAAA;EAmCT,MAAA,CAAA,EAAA,MAAA;EACA,QAAA,CAAA,EAAA,MAAA;EACI,eAAA,CAAA,EAAA,MAAA;EAAJ,mBAAA,CAAA,EAAA,MAAA;EAAR,wBAAA,CAAA,EAAA,MAAA;EA4CmB,WAAA,CAAA,EArGR,2BAqGQ;EAAU,MAAA,CAAA,EApGvB,MAoGuB;;AAmBlB,cA9GH,mBAAA,YAA+B,eAAA,CAAA,QA8G5B,CAAA;EA9G4B,iBAAA,GAAA;EAAQ,iBAAA,QAAA;;;;;;;sBAU9B;8CAmCT,oBACA,eAAA,CAAA,iBACR,QAAQ,eAAA,CAAA,IAAI;+DA4CJ,eAAA,CAAA,WAAW,UAAU;;UAmBlB"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { types_exports } from "./types.js";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { DeleteMessageCommand, ReceiveMessageCommand, SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";
|
|
4
|
+
|
|
5
|
+
//#region src/queue/scaleway-sqs-queue.ts
|
|
6
|
+
var ScalewaySqsJobQueue = class {
|
|
7
|
+
sqs;
|
|
8
|
+
queueUrl;
|
|
9
|
+
waitTimeSeconds;
|
|
10
|
+
maxNumberOfMessages;
|
|
11
|
+
visibilityTimeoutSeconds;
|
|
12
|
+
handlers = /* @__PURE__ */ new Map();
|
|
13
|
+
logger;
|
|
14
|
+
running = false;
|
|
15
|
+
constructor(config) {
|
|
16
|
+
this.logger = config.logger;
|
|
17
|
+
const accessKeyId = config.credentials?.accessKeyId ?? process.env.SCALEWAY_ACCESS_KEY_QUEUE;
|
|
18
|
+
const secretAccessKey = config.credentials?.secretAccessKey ?? process.env.SCALEWAY_SECRET_KEY_QUEUE;
|
|
19
|
+
if (!accessKeyId || !secretAccessKey) throw new Error("Missing SCALEWAY_ACCESS_KEY_QUEUE / SCALEWAY_SECRET_KEY_QUEUE in env");
|
|
20
|
+
this.sqs = new SQSClient({
|
|
21
|
+
region: config.region ?? process.env.SCALEWAY_REGION ?? "par",
|
|
22
|
+
endpoint: config.endpoint ?? "https://sqs.mnq.fr-par.scaleway.com",
|
|
23
|
+
credentials: {
|
|
24
|
+
accessKeyId,
|
|
25
|
+
secretAccessKey
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
this.queueUrl = config.queueUrl;
|
|
29
|
+
this.waitTimeSeconds = config.waitTimeSeconds ?? 20;
|
|
30
|
+
this.maxNumberOfMessages = config.maxNumberOfMessages ?? 5;
|
|
31
|
+
this.visibilityTimeoutSeconds = config.visibilityTimeoutSeconds ?? 60;
|
|
32
|
+
}
|
|
33
|
+
async enqueue(jobType, payload, options = {}) {
|
|
34
|
+
const id = randomUUID();
|
|
35
|
+
const now = /* @__PURE__ */ new Date();
|
|
36
|
+
const scheduledAt = options.delaySeconds ? new Date(now.getTime() + options.delaySeconds * 1e3) : now;
|
|
37
|
+
const envelope = {
|
|
38
|
+
id,
|
|
39
|
+
type: jobType,
|
|
40
|
+
payload
|
|
41
|
+
};
|
|
42
|
+
await this.sqs.send(new SendMessageCommand({
|
|
43
|
+
QueueUrl: this.queueUrl,
|
|
44
|
+
MessageBody: JSON.stringify(envelope),
|
|
45
|
+
DelaySeconds: options.delaySeconds ?? 0
|
|
46
|
+
}));
|
|
47
|
+
return {
|
|
48
|
+
id,
|
|
49
|
+
type: jobType,
|
|
50
|
+
version: "1.0.0",
|
|
51
|
+
payload,
|
|
52
|
+
status: "pending",
|
|
53
|
+
priority: options.priority ?? 0,
|
|
54
|
+
attempts: 0,
|
|
55
|
+
maxRetries: options.maxRetries ?? types_exports.DEFAULT_RETRY_POLICY.maxRetries,
|
|
56
|
+
createdAt: now,
|
|
57
|
+
updatedAt: now,
|
|
58
|
+
scheduledAt,
|
|
59
|
+
dedupeKey: options.dedupeKey,
|
|
60
|
+
tenantId: options.tenantId,
|
|
61
|
+
userId: options.userId,
|
|
62
|
+
traceId: options.traceId,
|
|
63
|
+
metadata: options.metadata
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
register(jobType, handler) {
|
|
67
|
+
if (this.handlers.has(jobType)) throw new Error(`Handler already registered for job type "${jobType}"`);
|
|
68
|
+
this.handlers.set(jobType, handler);
|
|
69
|
+
}
|
|
70
|
+
start() {
|
|
71
|
+
if (this.running) return;
|
|
72
|
+
this.running = true;
|
|
73
|
+
this.pollLoop().catch((error) => {
|
|
74
|
+
this.logger?.error?.("jobs.queue.scaleway_sqs.poll_loop_fatal", { error: error instanceof Error ? error.message : String(error) });
|
|
75
|
+
this.running = false;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async stop() {
|
|
79
|
+
this.running = false;
|
|
80
|
+
}
|
|
81
|
+
async pollLoop() {
|
|
82
|
+
this.logger?.info?.("jobs.queue.scaleway_sqs.started", { queueUrl: this.queueUrl });
|
|
83
|
+
while (this.running) try {
|
|
84
|
+
const messages = (await this.sqs.send(new ReceiveMessageCommand({
|
|
85
|
+
QueueUrl: this.queueUrl,
|
|
86
|
+
MaxNumberOfMessages: this.maxNumberOfMessages,
|
|
87
|
+
WaitTimeSeconds: this.waitTimeSeconds,
|
|
88
|
+
VisibilityTimeout: this.visibilityTimeoutSeconds,
|
|
89
|
+
MessageSystemAttributeNames: ["ApproximateReceiveCount"]
|
|
90
|
+
}))).Messages ?? [];
|
|
91
|
+
if (messages.length === 0) continue;
|
|
92
|
+
for (const msg of messages) {
|
|
93
|
+
if (!msg.Body || !msg.ReceiptHandle) {
|
|
94
|
+
this.logger?.warn?.("jobs.queue.scaleway_sqs.invalid_message", {
|
|
95
|
+
messageId: msg.MessageId,
|
|
96
|
+
reason: "missing_body_or_receipt"
|
|
97
|
+
});
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
let envelope;
|
|
101
|
+
try {
|
|
102
|
+
envelope = JSON.parse(msg.Body);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
this.logger?.warn?.("jobs.queue.scaleway_sqs.parse_failed", {
|
|
105
|
+
messageId: msg.MessageId,
|
|
106
|
+
error: err instanceof Error ? err.message : String(err)
|
|
107
|
+
});
|
|
108
|
+
await this.deleteMessage(msg.ReceiptHandle);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const handler = this.handlers.get(envelope.type);
|
|
112
|
+
if (!handler) {
|
|
113
|
+
this.logger?.warn?.("jobs.queue.scaleway_sqs.missing_handler", {
|
|
114
|
+
jobType: envelope.type,
|
|
115
|
+
messageId: msg.MessageId
|
|
116
|
+
});
|
|
117
|
+
await this.deleteMessage(msg.ReceiptHandle);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const now = /* @__PURE__ */ new Date();
|
|
121
|
+
const attempts = parseInt(msg.Attributes?.ApproximateReceiveCount ?? "1", 10);
|
|
122
|
+
const job = {
|
|
123
|
+
id: envelope.id,
|
|
124
|
+
type: envelope.type,
|
|
125
|
+
version: "1.0.0",
|
|
126
|
+
payload: envelope.payload,
|
|
127
|
+
status: "pending",
|
|
128
|
+
priority: 0,
|
|
129
|
+
attempts,
|
|
130
|
+
maxRetries: types_exports.DEFAULT_RETRY_POLICY.maxRetries,
|
|
131
|
+
createdAt: now,
|
|
132
|
+
updatedAt: now
|
|
133
|
+
};
|
|
134
|
+
job.status = "running";
|
|
135
|
+
job.updatedAt = /* @__PURE__ */ new Date();
|
|
136
|
+
try {
|
|
137
|
+
await handler(job);
|
|
138
|
+
job.status = "completed";
|
|
139
|
+
job.updatedAt = /* @__PURE__ */ new Date();
|
|
140
|
+
await this.deleteMessage(msg.ReceiptHandle);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
job.status = "failed";
|
|
143
|
+
job.lastError = err instanceof Error ? err.message : "Unknown job error";
|
|
144
|
+
job.updatedAt = /* @__PURE__ */ new Date();
|
|
145
|
+
this.logger?.error?.("jobs.queue.scaleway_sqs.job_failed", {
|
|
146
|
+
jobType: job.type,
|
|
147
|
+
jobId: job.id,
|
|
148
|
+
error: err instanceof Error ? err.message : String(err)
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch (err) {
|
|
153
|
+
this.logger?.error?.("jobs.queue.scaleway_sqs.poll_error", { error: err instanceof Error ? err.message : String(err) });
|
|
154
|
+
await this.sleep(5e3);
|
|
155
|
+
}
|
|
156
|
+
this.logger?.info?.("jobs.queue.scaleway_sqs.stopped", { queueUrl: this.queueUrl });
|
|
157
|
+
}
|
|
158
|
+
async deleteMessage(receiptHandle) {
|
|
159
|
+
try {
|
|
160
|
+
await this.sqs.send(new DeleteMessageCommand({
|
|
161
|
+
QueueUrl: this.queueUrl,
|
|
162
|
+
ReceiptHandle: receiptHandle
|
|
163
|
+
}));
|
|
164
|
+
} catch (err) {
|
|
165
|
+
this.logger?.warn?.("jobs.queue.scaleway_sqs.delete_failed", { error: err instanceof Error ? err.message : String(err) });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async sleep(ms) {
|
|
169
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
//#endregion
|
|
174
|
+
export { ScalewaySqsJobQueue };
|
|
175
|
+
//# sourceMappingURL=scaleway-sqs-queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scaleway-sqs-queue.js","names":["DEFAULT_RETRY_POLICY"],"sources":["../../src/queue/scaleway-sqs-queue.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport {\n DeleteMessageCommand,\n ReceiveMessageCommand,\n SendMessageCommand,\n SQSClient,\n} from '@aws-sdk/client-sqs';\nimport type { Logger } from '@contractspec/lib.logger';\nimport { DEFAULT_RETRY_POLICY } from './types';\nimport type { EnqueueOptions, Job, JobHandler, JobQueue } from './types';\n\nexport interface ScalewaySqsQueueCredentials {\n accessKeyId: string;\n secretAccessKey: string;\n}\n\nexport interface ScalewaySqsQueueConfig {\n queueUrl: string;\n region?: string;\n endpoint?: string;\n waitTimeSeconds?: number;\n maxNumberOfMessages?: number;\n visibilityTimeoutSeconds?: number;\n credentials?: ScalewaySqsQueueCredentials;\n logger?: Logger;\n}\n\ninterface RawJobEnvelope<TPayload = unknown> {\n id: string;\n type: string;\n payload: TPayload;\n}\n\nexport class ScalewaySqsJobQueue implements JobQueue {\n private readonly sqs: SQSClient;\n private readonly queueUrl: string;\n private readonly waitTimeSeconds: number;\n private readonly maxNumberOfMessages: number;\n private readonly visibilityTimeoutSeconds: number;\n private readonly handlers = new Map<string, JobHandler>();\n private readonly logger?: Logger;\n private running = false;\n\n constructor(config: ScalewaySqsQueueConfig) {\n this.logger = config.logger;\n\n const accessKeyId =\n config.credentials?.accessKeyId ?? process.env.SCALEWAY_ACCESS_KEY_QUEUE;\n const secretAccessKey =\n config.credentials?.secretAccessKey ??\n process.env.SCALEWAY_SECRET_KEY_QUEUE;\n\n if (!accessKeyId || !secretAccessKey) {\n throw new Error(\n 'Missing SCALEWAY_ACCESS_KEY_QUEUE / SCALEWAY_SECRET_KEY_QUEUE in env'\n );\n }\n\n const region = config.region ?? process.env.SCALEWAY_REGION ?? 'par';\n const endpoint = config.endpoint ?? 'https://sqs.mnq.fr-par.scaleway.com';\n\n this.sqs = new SQSClient({\n region,\n endpoint,\n credentials: {\n accessKeyId,\n secretAccessKey,\n },\n });\n\n this.queueUrl = config.queueUrl;\n this.waitTimeSeconds = config.waitTimeSeconds ?? 20;\n this.maxNumberOfMessages = config.maxNumberOfMessages ?? 5;\n this.visibilityTimeoutSeconds = config.visibilityTimeoutSeconds ?? 60;\n }\n\n async enqueue<TPayload>(\n jobType: string,\n payload: TPayload,\n options: EnqueueOptions = {}\n ): Promise<Job<TPayload>> {\n const id = randomUUID();\n const now = new Date();\n const scheduledAt = options.delaySeconds\n ? new Date(now.getTime() + options.delaySeconds * 1000)\n : now;\n\n const envelope: RawJobEnvelope<TPayload> = {\n id,\n type: jobType,\n payload,\n };\n\n await this.sqs.send(\n new SendMessageCommand({\n QueueUrl: this.queueUrl,\n MessageBody: JSON.stringify(envelope),\n DelaySeconds: options.delaySeconds ?? 0,\n // If you use FIFO queues later, you'd set MessageGroupId / MessageDeduplicationId here.\n })\n );\n\n return {\n id,\n type: jobType,\n version: '1.0.0',\n payload,\n status: 'pending',\n priority: options.priority ?? 0,\n attempts: 0,\n maxRetries: options.maxRetries ?? DEFAULT_RETRY_POLICY.maxRetries,\n createdAt: now,\n updatedAt: now,\n scheduledAt,\n dedupeKey: options.dedupeKey,\n tenantId: options.tenantId,\n userId: options.userId,\n traceId: options.traceId,\n metadata: options.metadata,\n };\n }\n\n register<TPayload, TResult = void>(\n jobType: string,\n handler: JobHandler<TPayload, TResult>\n ): void {\n if (this.handlers.has(jobType)) {\n throw new Error(`Handler already registered for job type \"${jobType}\"`);\n }\n this.handlers.set(jobType, handler as JobHandler);\n }\n\n start(): void {\n if (this.running) return;\n this.running = true;\n void this.pollLoop().catch((error) => {\n this.logger?.error?.('jobs.queue.scaleway_sqs.poll_loop_fatal', {\n error: error instanceof Error ? error.message : String(error),\n });\n this.running = false;\n });\n }\n\n async stop(): Promise<void> {\n this.running = false;\n // Worst-case we wait for the current ReceiveMessage to finish (<= waitTimeSeconds)\n }\n\n private async pollLoop(): Promise<void> {\n this.logger?.info?.('jobs.queue.scaleway_sqs.started', {\n queueUrl: this.queueUrl,\n });\n\n while (this.running) {\n try {\n const res = await this.sqs.send(\n new ReceiveMessageCommand({\n QueueUrl: this.queueUrl,\n MaxNumberOfMessages: this.maxNumberOfMessages,\n WaitTimeSeconds: this.waitTimeSeconds,\n VisibilityTimeout: this.visibilityTimeoutSeconds,\n MessageSystemAttributeNames: ['ApproximateReceiveCount'],\n })\n );\n\n const messages = res.Messages ?? [];\n\n if (messages.length === 0) {\n continue;\n }\n\n for (const msg of messages) {\n if (!msg.Body || !msg.ReceiptHandle) {\n this.logger?.warn?.('jobs.queue.scaleway_sqs.invalid_message', {\n messageId: msg.MessageId,\n reason: 'missing_body_or_receipt',\n });\n continue;\n }\n\n let envelope: RawJobEnvelope;\n\n try {\n envelope = JSON.parse(msg.Body) as RawJobEnvelope;\n } catch (err) {\n this.logger?.warn?.('jobs.queue.scaleway_sqs.parse_failed', {\n messageId: msg.MessageId,\n error: err instanceof Error ? err.message : String(err),\n });\n await this.deleteMessage(msg.ReceiptHandle);\n continue;\n }\n\n const handler = this.handlers.get(envelope.type);\n if (!handler) {\n this.logger?.warn?.('jobs.queue.scaleway_sqs.missing_handler', {\n jobType: envelope.type,\n messageId: msg.MessageId,\n });\n await this.deleteMessage(msg.ReceiptHandle);\n continue;\n }\n\n const now = new Date();\n const attempts = parseInt(\n (msg.Attributes?.ApproximateReceiveCount as string | undefined) ??\n '1',\n 10\n );\n\n const job: Job = {\n id: envelope.id,\n type: envelope.type,\n version: '1.0.0',\n payload: envelope.payload,\n status: 'pending',\n priority: 0,\n attempts,\n maxRetries: DEFAULT_RETRY_POLICY.maxRetries,\n createdAt: now,\n updatedAt: now,\n };\n\n job.status = 'running';\n job.updatedAt = new Date();\n\n try {\n await handler(job);\n job.status = 'completed';\n job.updatedAt = new Date();\n await this.deleteMessage(msg.ReceiptHandle);\n } catch (err) {\n job.status = 'failed';\n job.lastError =\n err instanceof Error ? err.message : 'Unknown job error';\n job.updatedAt = new Date();\n\n this.logger?.error?.('jobs.queue.scaleway_sqs.job_failed', {\n jobType: job.type,\n jobId: job.id,\n error: err instanceof Error ? err.message : String(err),\n });\n // Do NOT delete message on failure:\n // - SQS/Scaleway will redeliver until MaxReceiveCount, then DLQ takes over.\n }\n }\n } catch (err) {\n this.logger?.error?.('jobs.queue.scaleway_sqs.poll_error', {\n error: err instanceof Error ? err.message : String(err),\n });\n await this.sleep(5000);\n }\n }\n\n this.logger?.info?.('jobs.queue.scaleway_sqs.stopped', {\n queueUrl: this.queueUrl,\n });\n }\n\n private async deleteMessage(receiptHandle: string): Promise<void> {\n try {\n await this.sqs.send(\n new DeleteMessageCommand({\n QueueUrl: this.queueUrl,\n ReceiptHandle: receiptHandle,\n })\n );\n } catch (err) {\n this.logger?.warn?.('jobs.queue.scaleway_sqs.delete_failed', {\n error: err instanceof Error ? err.message : String(err),\n });\n }\n }\n\n private async sleep(ms: number): Promise<void> {\n await new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "@contractspec/lib.contracts/jobs/queue";
|
|
2
|
+
|
|
3
|
+
//#region src/queue/types.d.ts
|
|
4
|
+
|
|
5
|
+
import * as import__contractspec_lib_contracts_jobs_queue from "@contractspec/lib.contracts/jobs/queue";
|
|
6
|
+
//#endregion
|
|
7
|
+
export { import__contractspec_lib_contracts_jobs_queue as types_d_exports };
|
|
8
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../../src/queue/types.ts"],"sourcesContent":[],"mappings":""}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { __reExport } from "../_virtual/rolldown_runtime.js";
|
|
2
|
+
|
|
3
|
+
export * from "@contractspec/lib.contracts/jobs/queue"
|
|
4
|
+
|
|
5
|
+
//#region src/queue/types.ts
|
|
6
|
+
var types_exports = {};
|
|
7
|
+
import * as import__contractspec_lib_contracts_jobs_queue from "@contractspec/lib.contracts/jobs/queue";
|
|
8
|
+
__reExport(types_exports, import__contractspec_lib_contracts_jobs_queue);
|
|
9
|
+
|
|
10
|
+
//#endregion
|
|
11
|
+
export { types_exports };
|
|
12
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","names":[],"sources":["../../src/queue/types.ts"],"sourcesContent":["export * from '@contractspec/lib.contracts/jobs/queue';\n"],"mappings":""}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { types_d_exports } from "../queue/types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/scheduler/index.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Scheduled job configuration.
|
|
7
|
+
*/
|
|
8
|
+
interface ScheduledJobConfig {
|
|
9
|
+
/** Unique name for the schedule */
|
|
10
|
+
name: string;
|
|
11
|
+
/** Cron expression (e.g., '0 0 * * *' for daily at midnight) */
|
|
12
|
+
cronExpression: string;
|
|
13
|
+
/** Job type to enqueue */
|
|
14
|
+
jobType: string;
|
|
15
|
+
/** Job payload (can be a function for dynamic payloads) */
|
|
16
|
+
payload?: unknown | (() => unknown | Promise<unknown>);
|
|
17
|
+
/** Enqueue options */
|
|
18
|
+
options?: types_d_exports.EnqueueOptions;
|
|
19
|
+
/** Timezone for cron evaluation (default: UTC) */
|
|
20
|
+
timezone?: string;
|
|
21
|
+
/** Whether the schedule is enabled */
|
|
22
|
+
enabled?: boolean;
|
|
23
|
+
/** Description */
|
|
24
|
+
description?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Active scheduled job with next run time.
|
|
28
|
+
*/
|
|
29
|
+
interface ActiveSchedule extends ScheduledJobConfig {
|
|
30
|
+
nextRun: Date | null;
|
|
31
|
+
lastRun: Date | null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Job scheduler for recurring jobs.
|
|
35
|
+
*/
|
|
36
|
+
declare class JobScheduler {
|
|
37
|
+
private readonly queue;
|
|
38
|
+
private readonly schedules;
|
|
39
|
+
private timer?;
|
|
40
|
+
private readonly checkIntervalMs;
|
|
41
|
+
constructor(queue: types_d_exports.JobQueue, options?: {
|
|
42
|
+
checkIntervalMs?: number;
|
|
43
|
+
});
|
|
44
|
+
/**
|
|
45
|
+
* Add a scheduled job.
|
|
46
|
+
*/
|
|
47
|
+
schedule(config: ScheduledJobConfig): void;
|
|
48
|
+
/**
|
|
49
|
+
* Remove a scheduled job.
|
|
50
|
+
*/
|
|
51
|
+
unschedule(name: string): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Enable a scheduled job.
|
|
54
|
+
*/
|
|
55
|
+
enable(name: string): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Disable a scheduled job.
|
|
58
|
+
*/
|
|
59
|
+
disable(name: string): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Get all schedules.
|
|
62
|
+
*/
|
|
63
|
+
getSchedules(): ActiveSchedule[];
|
|
64
|
+
/**
|
|
65
|
+
* Get a specific schedule.
|
|
66
|
+
*/
|
|
67
|
+
getSchedule(name: string): ActiveSchedule | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Start the scheduler.
|
|
70
|
+
*/
|
|
71
|
+
start(): void;
|
|
72
|
+
/**
|
|
73
|
+
* Stop the scheduler.
|
|
74
|
+
*/
|
|
75
|
+
stop(): void;
|
|
76
|
+
/**
|
|
77
|
+
* Check and enqueue due schedules.
|
|
78
|
+
*/
|
|
79
|
+
private checkSchedules;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Create a job scheduler instance.
|
|
83
|
+
*/
|
|
84
|
+
declare function createScheduler(queue: types_d_exports.JobQueue, options?: {
|
|
85
|
+
checkIntervalMs?: number;
|
|
86
|
+
}): JobScheduler;
|
|
87
|
+
/**
|
|
88
|
+
* Helper to define a scheduled job configuration.
|
|
89
|
+
*/
|
|
90
|
+
declare function defineSchedule(config: ScheduledJobConfig): ScheduledJobConfig;
|
|
91
|
+
//#endregion
|
|
92
|
+
export { ActiveSchedule, JobScheduler, ScheduledJobConfig, createScheduler, defineSchedule };
|
|
93
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/scheduler/index.ts"],"sourcesContent":[],"mappings":";;;;;;AAKA;AAsBiB,UAtBA,kBAAA,CAsBe;EACrB;EACA,IAAA,EAAA,MAAA;EAF6B;EAAkB,cAAA,EAAA,MAAA;EA8D7C;EAMe,OAAA,EAAA,MAAA;EAST;EA8CD,OAAA,CAAA,EAAA,OAAA,GAAA,CAAA,GAAA,GAAA,OAAA,GAzIqB,OAyIrB,CAAA,OAAA,CAAA,CAAA;EAOW;EAAc,OAAA,CAAA,EA9I/B,eAAA,CAAA,cA8I+B;EAkE3B;EAUA,QAAA,CAAA,EAAA,MAAc;;;;;;;;;UA9Mb,cAAA,SAAuB;WAC7B;WACA;;;;;cA4DE,YAAA;;;;;qBAMe,eAAA,CAAA;;;;;;mBAST;;;;;;;;;;;;;;;;kBA8CD;;;;6BAOW;;;;;;;;;;;;;;;;;iBAkEb,eAAA,QACP,eAAA,CAAA;;IAEN;;;;iBAOa,cAAA,SAAuB,qBAAqB"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
//#region src/scheduler/index.ts
|
|
2
|
+
/**
|
|
3
|
+
* Parse a cron expression to get the next run time.
|
|
4
|
+
* Simple implementation supporting: minute hour day month weekday
|
|
5
|
+
*/
|
|
6
|
+
function getNextCronRun(cronExpression, after = /* @__PURE__ */ new Date()) {
|
|
7
|
+
try {
|
|
8
|
+
const parts = cronExpression.trim().split(/\s+/);
|
|
9
|
+
if (parts.length !== 5) {
|
|
10
|
+
console.warn(`Invalid cron expression: ${cronExpression}`);
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const minute = parts[0];
|
|
14
|
+
const hour = parts[1];
|
|
15
|
+
const dayOfMonth = parts[2];
|
|
16
|
+
const month = parts[3];
|
|
17
|
+
const next = new Date(after);
|
|
18
|
+
next.setSeconds(0);
|
|
19
|
+
next.setMilliseconds(0);
|
|
20
|
+
if (minute && hour && minute !== "*" && hour !== "*" && dayOfMonth === "*" && month === "*") {
|
|
21
|
+
const targetMinute = Number.parseInt(minute, 10);
|
|
22
|
+
const targetHour = Number.parseInt(hour, 10);
|
|
23
|
+
next.setHours(targetHour, targetMinute, 0, 0);
|
|
24
|
+
if (next <= after) next.setDate(next.getDate() + 1);
|
|
25
|
+
return next;
|
|
26
|
+
}
|
|
27
|
+
next.setMinutes(next.getMinutes() + 1);
|
|
28
|
+
return next;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Job scheduler for recurring jobs.
|
|
35
|
+
*/
|
|
36
|
+
var JobScheduler = class {
|
|
37
|
+
schedules = /* @__PURE__ */ new Map();
|
|
38
|
+
timer;
|
|
39
|
+
checkIntervalMs;
|
|
40
|
+
constructor(queue, options = {}) {
|
|
41
|
+
this.queue = queue;
|
|
42
|
+
this.checkIntervalMs = options.checkIntervalMs ?? 6e4;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Add a scheduled job.
|
|
46
|
+
*/
|
|
47
|
+
schedule(config) {
|
|
48
|
+
const nextRun = config.enabled !== false ? getNextCronRun(config.cronExpression) : null;
|
|
49
|
+
this.schedules.set(config.name, {
|
|
50
|
+
...config,
|
|
51
|
+
enabled: config.enabled ?? true,
|
|
52
|
+
nextRun,
|
|
53
|
+
lastRun: null
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Remove a scheduled job.
|
|
58
|
+
*/
|
|
59
|
+
unschedule(name) {
|
|
60
|
+
return this.schedules.delete(name);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Enable a scheduled job.
|
|
64
|
+
*/
|
|
65
|
+
enable(name) {
|
|
66
|
+
const schedule = this.schedules.get(name);
|
|
67
|
+
if (!schedule) return false;
|
|
68
|
+
schedule.enabled = true;
|
|
69
|
+
schedule.nextRun = getNextCronRun(schedule.cronExpression);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Disable a scheduled job.
|
|
74
|
+
*/
|
|
75
|
+
disable(name) {
|
|
76
|
+
const schedule = this.schedules.get(name);
|
|
77
|
+
if (!schedule) return false;
|
|
78
|
+
schedule.enabled = false;
|
|
79
|
+
schedule.nextRun = null;
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get all schedules.
|
|
84
|
+
*/
|
|
85
|
+
getSchedules() {
|
|
86
|
+
return Array.from(this.schedules.values());
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get a specific schedule.
|
|
90
|
+
*/
|
|
91
|
+
getSchedule(name) {
|
|
92
|
+
return this.schedules.get(name);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Start the scheduler.
|
|
96
|
+
*/
|
|
97
|
+
start() {
|
|
98
|
+
if (this.timer) return;
|
|
99
|
+
this.checkSchedules();
|
|
100
|
+
this.timer = setInterval(() => {
|
|
101
|
+
this.checkSchedules();
|
|
102
|
+
}, this.checkIntervalMs);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Stop the scheduler.
|
|
106
|
+
*/
|
|
107
|
+
stop() {
|
|
108
|
+
if (this.timer) {
|
|
109
|
+
clearInterval(this.timer);
|
|
110
|
+
this.timer = void 0;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Check and enqueue due schedules.
|
|
115
|
+
*/
|
|
116
|
+
async checkSchedules() {
|
|
117
|
+
const now = /* @__PURE__ */ new Date();
|
|
118
|
+
for (const schedule of this.schedules.values()) {
|
|
119
|
+
if (!schedule.enabled || !schedule.nextRun) continue;
|
|
120
|
+
if (schedule.nextRun <= now) try {
|
|
121
|
+
const payload = typeof schedule.payload === "function" ? await schedule.payload() : schedule.payload;
|
|
122
|
+
await this.queue.enqueue(schedule.jobType, payload, schedule.options);
|
|
123
|
+
schedule.lastRun = now;
|
|
124
|
+
schedule.nextRun = getNextCronRun(schedule.cronExpression, now);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error(`Failed to enqueue scheduled job ${schedule.name}:`, error);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Create a job scheduler instance.
|
|
133
|
+
*/
|
|
134
|
+
function createScheduler(queue, options) {
|
|
135
|
+
return new JobScheduler(queue, options);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Helper to define a scheduled job configuration.
|
|
139
|
+
*/
|
|
140
|
+
function defineSchedule(config) {
|
|
141
|
+
return config;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
//#endregion
|
|
145
|
+
export { JobScheduler, createScheduler, defineSchedule };
|
|
146
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/scheduler/index.ts"],"sourcesContent":["import type { JobQueue, EnqueueOptions } from '../queue/types';\n\n/**\n * Scheduled job configuration.\n */\nexport interface ScheduledJobConfig {\n /** Unique name for the schedule */\n name: string;\n /** Cron expression (e.g., '0 0 * * *' for daily at midnight) */\n cronExpression: string;\n /** Job type to enqueue */\n jobType: string;\n /** Job payload (can be a function for dynamic payloads) */\n payload?: unknown | (() => unknown | Promise<unknown>);\n /** Enqueue options */\n options?: EnqueueOptions;\n /** Timezone for cron evaluation (default: UTC) */\n timezone?: string;\n /** Whether the schedule is enabled */\n enabled?: boolean;\n /** Description */\n description?: string;\n}\n\n/**\n * Active scheduled job with next run time.\n */\nexport interface ActiveSchedule extends ScheduledJobConfig {\n nextRun: Date | null;\n lastRun: Date | null;\n}\n\n/**\n * Parse a cron expression to get the next run time.\n * Simple implementation supporting: minute hour day month weekday\n */\nfunction getNextCronRun(\n cronExpression: string,\n after: Date = new Date()\n): Date | null {\n try {\n // Dynamically import cron-parser\n // This is a simplified fallback if cron-parser isn't available\n const parts = cronExpression.trim().split(/\\s+/);\n if (parts.length !== 5) {\n console.warn(`Invalid cron expression: ${cronExpression}`);\n return null;\n }\n\n // Simple parsing for common patterns\n const minute = parts[0];\n const hour = parts[1];\n const dayOfMonth = parts[2];\n const month = parts[3];\n const next = new Date(after);\n next.setSeconds(0);\n next.setMilliseconds(0);\n\n // Handle simple cases\n if (\n minute &&\n hour &&\n minute !== '*' &&\n hour !== '*' &&\n dayOfMonth === '*' &&\n month === '*'\n ) {\n // Daily at specific time\n const targetMinute = Number.parseInt(minute, 10);\n const targetHour = Number.parseInt(hour, 10);\n\n next.setHours(targetHour, targetMinute, 0, 0);\n if (next <= after) {\n next.setDate(next.getDate() + 1);\n }\n return next;\n }\n\n // For other patterns, add 1 minute as fallback\n next.setMinutes(next.getMinutes() + 1);\n return next;\n } catch {\n return null;\n }\n}\n\n/**\n * Job scheduler for recurring jobs.\n */\nexport class JobScheduler {\n private readonly schedules = new Map<string, ActiveSchedule>();\n private timer?: ReturnType<typeof setInterval>;\n private readonly checkIntervalMs: number;\n\n constructor(\n private readonly queue: JobQueue,\n options: { checkIntervalMs?: number } = {}\n ) {\n this.checkIntervalMs = options.checkIntervalMs ?? 60000; // 1 minute default\n }\n\n /**\n * Add a scheduled job.\n */\n schedule(config: ScheduledJobConfig): void {\n const nextRun =\n config.enabled !== false ? getNextCronRun(config.cronExpression) : null;\n\n this.schedules.set(config.name, {\n ...config,\n enabled: config.enabled ?? true,\n nextRun,\n lastRun: null,\n });\n }\n\n /**\n * Remove a scheduled job.\n */\n unschedule(name: string): boolean {\n return this.schedules.delete(name);\n }\n\n /**\n * Enable a scheduled job.\n */\n enable(name: string): boolean {\n const schedule = this.schedules.get(name);\n if (!schedule) return false;\n\n schedule.enabled = true;\n schedule.nextRun = getNextCronRun(schedule.cronExpression);\n return true;\n }\n\n /**\n * Disable a scheduled job.\n */\n disable(name: string): boolean {\n const schedule = this.schedules.get(name);\n if (!schedule) return false;\n\n schedule.enabled = false;\n schedule.nextRun = null;\n return true;\n }\n\n /**\n * Get all schedules.\n */\n getSchedules(): ActiveSchedule[] {\n return Array.from(this.schedules.values());\n }\n\n /**\n * Get a specific schedule.\n */\n getSchedule(name: string): ActiveSchedule | undefined {\n return this.schedules.get(name);\n }\n\n /**\n * Start the scheduler.\n */\n start(): void {\n if (this.timer) return;\n\n // Initial check\n void this.checkSchedules();\n\n // Periodic check\n this.timer = setInterval(() => {\n void this.checkSchedules();\n }, this.checkIntervalMs);\n }\n\n /**\n * Stop the scheduler.\n */\n stop(): void {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = undefined;\n }\n }\n\n /**\n * Check and enqueue due schedules.\n */\n private async checkSchedules(): Promise<void> {\n const now = new Date();\n\n for (const schedule of this.schedules.values()) {\n if (!schedule.enabled || !schedule.nextRun) continue;\n\n if (schedule.nextRun <= now) {\n try {\n // Resolve payload if it's a function\n const payload =\n typeof schedule.payload === 'function'\n ? await schedule.payload()\n : schedule.payload;\n\n // Enqueue the job\n await this.queue.enqueue(schedule.jobType, payload, schedule.options);\n\n // Update schedule\n schedule.lastRun = now;\n schedule.nextRun = getNextCronRun(schedule.cronExpression, now);\n } catch (error) {\n console.error(\n `Failed to enqueue scheduled job ${schedule.name}:`,\n error\n );\n }\n }\n }\n }\n}\n\n/**\n * Create a job scheduler instance.\n */\nexport function createScheduler(\n queue: JobQueue,\n options?: { checkIntervalMs?: number }\n): JobScheduler {\n return new JobScheduler(queue, options);\n}\n\n/**\n * Helper to define a scheduled job configuration.\n */\nexport function defineSchedule(config: ScheduledJobConfig): ScheduledJobConfig {\n return config;\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@contractspec/lib.jobs",
|
|
3
|
+
"version": "0.0.0-canary-20260113162409",
|
|
4
|
+
"description": "Background jobs and scheduler module for ContractSpec applications",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"contractspec",
|
|
7
|
+
"jobs",
|
|
8
|
+
"queue",
|
|
9
|
+
"scheduler",
|
|
10
|
+
"background",
|
|
11
|
+
"typescript"
|
|
12
|
+
],
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"scripts": {
|
|
16
|
+
"publish:pkg": "bun publish --tolerate-republish --ignore-scripts --verbose",
|
|
17
|
+
"publish:pkg:canary": "bun publish:pkg --tag canary",
|
|
18
|
+
"build": "bun build:types && bun build:bundle",
|
|
19
|
+
"build:bundle": "tsdown",
|
|
20
|
+
"build:types": "tsc --noEmit",
|
|
21
|
+
"dev": "bun build:bundle --watch",
|
|
22
|
+
"clean": "rimraf dist .turbo",
|
|
23
|
+
"lint": "bun lint:fix",
|
|
24
|
+
"lint:fix": "eslint src --fix",
|
|
25
|
+
"lint:check": "eslint src"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@contractspec/lib.schema": "0.0.0-canary-20260113162409",
|
|
29
|
+
"@contractspec/lib.contracts": "0.0.0-canary-20260113162409",
|
|
30
|
+
"@contractspec/lib.logger": "0.0.0-canary-20260113162409",
|
|
31
|
+
"@contractspec/lib.knowledge": "0.0.0-canary-20260113162409",
|
|
32
|
+
"@aws-sdk/client-sqs": "^3.966.0",
|
|
33
|
+
"zod": "^4.3.5",
|
|
34
|
+
"cron-parser": "^5.4.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@contractspec/tool.typescript": "0.0.0-canary-20260113162409",
|
|
38
|
+
"@contractspec/tool.tsdown": "0.0.0-canary-20260113162409",
|
|
39
|
+
"typescript": "^5.9.3"
|
|
40
|
+
},
|
|
41
|
+
"exports": {
|
|
42
|
+
".": "./dist/index.js",
|
|
43
|
+
"./contracts": "./dist/contracts/index.js",
|
|
44
|
+
"./entities": "./dist/entities/index.js",
|
|
45
|
+
"./events": "./dist/events.js",
|
|
46
|
+
"./handlers": "./dist/handlers/index.js",
|
|
47
|
+
"./handlers/gmail-sync-handler": "./dist/handlers/gmail-sync-handler.js",
|
|
48
|
+
"./handlers/ping-job": "./dist/handlers/ping-job.js",
|
|
49
|
+
"./handlers/storage-document-handler": "./dist/handlers/storage-document-handler.js",
|
|
50
|
+
"./jobs.capability": "./dist/jobs.capability.js",
|
|
51
|
+
"./jobs.feature": "./dist/jobs.feature.js",
|
|
52
|
+
"./queue": "./dist/queue/index.js",
|
|
53
|
+
"./queue/gcp-cloud-tasks": "./dist/queue/gcp-cloud-tasks.js",
|
|
54
|
+
"./queue/gcp-pubsub": "./dist/queue/gcp-pubsub.js",
|
|
55
|
+
"./queue/memory-queue": "./dist/queue/memory-queue.js",
|
|
56
|
+
"./queue/register-defined-job": "./dist/queue/register-defined-job.js",
|
|
57
|
+
"./queue/scaleway-sqs-queue": "./dist/queue/scaleway-sqs-queue.js",
|
|
58
|
+
"./queue/types": "./dist/queue/types.js",
|
|
59
|
+
"./scheduler": "./dist/scheduler/index.js",
|
|
60
|
+
"./*": "./*"
|
|
61
|
+
},
|
|
62
|
+
"files": [
|
|
63
|
+
"dist",
|
|
64
|
+
"README.md"
|
|
65
|
+
],
|
|
66
|
+
"publishConfig": {
|
|
67
|
+
"access": "public",
|
|
68
|
+
"exports": {
|
|
69
|
+
".": "./dist/index.js",
|
|
70
|
+
"./contracts": "./dist/contracts/index.js",
|
|
71
|
+
"./entities": "./dist/entities/index.js",
|
|
72
|
+
"./events": "./dist/events.js",
|
|
73
|
+
"./handlers": "./dist/handlers/index.js",
|
|
74
|
+
"./handlers/gmail-sync-handler": "./dist/handlers/gmail-sync-handler.js",
|
|
75
|
+
"./handlers/ping-job": "./dist/handlers/ping-job.js",
|
|
76
|
+
"./handlers/storage-document-handler": "./dist/handlers/storage-document-handler.js",
|
|
77
|
+
"./jobs.feature": "./dist/jobs.feature.js",
|
|
78
|
+
"./queue": "./dist/queue/index.js",
|
|
79
|
+
"./queue/gcp-cloud-tasks": "./dist/queue/gcp-cloud-tasks.js",
|
|
80
|
+
"./queue/gcp-pubsub": "./dist/queue/gcp-pubsub.js",
|
|
81
|
+
"./queue/memory-queue": "./dist/queue/memory-queue.js",
|
|
82
|
+
"./queue/register-defined-job": "./dist/queue/register-defined-job.js",
|
|
83
|
+
"./queue/scaleway-sqs-queue": "./dist/queue/scaleway-sqs-queue.js",
|
|
84
|
+
"./queue/types": "./dist/queue/types.js",
|
|
85
|
+
"./scheduler": "./dist/scheduler/index.js",
|
|
86
|
+
"./*": "./*"
|
|
87
|
+
},
|
|
88
|
+
"registry": "https://registry.npmjs.org/"
|
|
89
|
+
},
|
|
90
|
+
"license": "MIT",
|
|
91
|
+
"repository": {
|
|
92
|
+
"type": "git",
|
|
93
|
+
"url": "https://github.com/lssm-tech/contractspec.git",
|
|
94
|
+
"directory": "packages/libs/jobs"
|
|
95
|
+
},
|
|
96
|
+
"homepage": "https://contractspec.io"
|
|
97
|
+
}
|