@contractspec/lib.jobs 1.57.0 → 1.59.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.
- package/dist/contracts/index.d.ts +494 -500
- package/dist/contracts/index.d.ts.map +1 -1
- package/dist/contracts/index.js +298 -461
- package/dist/entities/index.d.ts +117 -122
- package/dist/entities/index.d.ts.map +1 -1
- package/dist/entities/index.js +170 -193
- package/dist/events.d.ts +297 -303
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +199 -351
- package/dist/handlers/gmail-sync-handler.d.ts +5 -9
- package/dist/handlers/gmail-sync-handler.d.ts.map +1 -1
- package/dist/handlers/gmail-sync-handler.js +8 -8
- package/dist/handlers/index.d.ts +5 -9
- package/dist/handlers/index.d.ts.map +1 -1
- package/dist/handlers/index.js +53 -10
- package/dist/handlers/ping-job.d.ts +6 -10
- package/dist/handlers/ping-job.d.ts.map +1 -1
- package/dist/handlers/ping-job.js +13 -12
- package/dist/handlers/storage-document-handler.d.ts +7 -11
- package/dist/handlers/storage-document-handler.d.ts.map +1 -1
- package/dist/handlers/storage-document-handler.js +15 -13
- package/dist/index.d.ts +7 -24
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1366 -64
- package/dist/jobs.capability.d.ts +2 -7
- package/dist/jobs.capability.d.ts.map +1 -1
- package/dist/jobs.capability.js +29 -33
- package/dist/jobs.feature.d.ts +1 -6
- package/dist/jobs.feature.d.ts.map +1 -1
- package/dist/jobs.feature.js +45 -108
- package/dist/node/contracts/index.js +318 -0
- package/dist/node/entities/index.js +174 -0
- package/dist/node/events.js +200 -0
- package/dist/node/handlers/gmail-sync-handler.js +9 -0
- package/dist/node/handlers/index.js +55 -0
- package/dist/node/handlers/ping-job.js +14 -0
- package/dist/node/handlers/storage-document-handler.js +16 -0
- package/dist/node/index.js +1368 -0
- package/dist/node/jobs.capability.js +28 -0
- package/dist/node/jobs.feature.js +46 -0
- package/dist/node/queue/gcp-cloud-tasks.js +66 -0
- package/dist/node/queue/gcp-pubsub.js +54 -0
- package/dist/node/queue/index.js +478 -0
- package/dist/node/queue/memory-queue.js +160 -0
- package/dist/node/queue/register-defined-job.js +15 -0
- package/dist/node/queue/scaleway-sqs-queue.js +206 -0
- package/dist/node/queue/types.js +10 -0
- package/dist/node/scheduler/index.js +117 -0
- package/dist/queue/gcp-cloud-tasks.d.ts +33 -36
- package/dist/queue/gcp-cloud-tasks.d.ts.map +1 -1
- package/dist/queue/gcp-cloud-tasks.js +65 -59
- package/dist/queue/gcp-pubsub.d.ts +18 -21
- package/dist/queue/gcp-pubsub.d.ts.map +1 -1
- package/dist/queue/gcp-pubsub.js +53 -45
- package/dist/queue/index.d.ts +6 -15
- package/dist/queue/index.d.ts.map +1 -1
- package/dist/queue/index.js +476 -20
- package/dist/queue/memory-queue.d.ts +25 -29
- package/dist/queue/memory-queue.d.ts.map +1 -1
- package/dist/queue/memory-queue.js +159 -138
- package/dist/queue/register-defined-job.d.ts +3 -7
- package/dist/queue/register-defined-job.d.ts.map +1 -1
- package/dist/queue/register-defined-job.js +14 -14
- package/dist/queue/scaleway-sqs-queue.d.ts +31 -35
- package/dist/queue/scaleway-sqs-queue.d.ts.map +1 -1
- package/dist/queue/scaleway-sqs-queue.js +205 -173
- package/dist/queue/types.d.ts +2 -8
- package/dist/queue/types.d.ts.map +1 -1
- package/dist/queue/types.js +11 -12
- package/dist/scheduler/index.d.ts +68 -72
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +113 -141
- package/package.json +176 -50
- package/dist/_virtual/_rolldown/runtime.js +0 -36
- package/dist/contracts/index.js.map +0 -1
- package/dist/entities/index.js.map +0 -1
- package/dist/events.js.map +0 -1
- package/dist/handlers/gmail-sync-handler.js.map +0 -1
- package/dist/handlers/index.js.map +0 -1
- package/dist/handlers/ping-job.js.map +0 -1
- package/dist/handlers/storage-document-handler.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/jobs.capability.js.map +0 -1
- package/dist/jobs.feature.js.map +0 -1
- package/dist/queue/gcp-cloud-tasks.js.map +0 -1
- package/dist/queue/gcp-pubsub.js.map +0 -1
- package/dist/queue/index.js.map +0 -1
- package/dist/queue/memory-queue.js.map +0 -1
- package/dist/queue/register-defined-job.js.map +0 -1
- package/dist/queue/scaleway-sqs-queue.js.map +0 -1
- package/dist/queue/types.js.map +0 -1
- package/dist/scheduler/index.js.map +0 -1
|
@@ -0,0 +1,1368 @@
|
|
|
1
|
+
// src/contracts/index.ts
|
|
2
|
+
import { ScalarTypeEnum, defineSchemaModel } from "@contractspec/lib.schema";
|
|
3
|
+
import { defineCommand, defineQuery } from "@contractspec/lib.contracts";
|
|
4
|
+
var OWNERS = ["platform.jobs"];
|
|
5
|
+
var JobModel = defineSchemaModel({
|
|
6
|
+
name: "Job",
|
|
7
|
+
description: "Represents a background job",
|
|
8
|
+
fields: {
|
|
9
|
+
id: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
10
|
+
type: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
11
|
+
version: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
12
|
+
payload: { type: ScalarTypeEnum.JSON(), isOptional: false },
|
|
13
|
+
status: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
14
|
+
priority: { type: ScalarTypeEnum.Int_unsecure(), isOptional: false },
|
|
15
|
+
attempts: { type: ScalarTypeEnum.Int_unsecure(), isOptional: false },
|
|
16
|
+
maxRetries: { type: ScalarTypeEnum.Int_unsecure(), isOptional: false },
|
|
17
|
+
createdAt: { type: ScalarTypeEnum.DateTime(), isOptional: false },
|
|
18
|
+
updatedAt: { type: ScalarTypeEnum.DateTime(), isOptional: false },
|
|
19
|
+
scheduledAt: { type: ScalarTypeEnum.DateTime(), isOptional: true },
|
|
20
|
+
startedAt: { type: ScalarTypeEnum.DateTime(), isOptional: true },
|
|
21
|
+
completedAt: { type: ScalarTypeEnum.DateTime(), isOptional: true },
|
|
22
|
+
lastError: { type: ScalarTypeEnum.String_unsecure(), isOptional: true }
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
var ScheduledJobModel = defineSchemaModel({
|
|
26
|
+
name: "ScheduledJob",
|
|
27
|
+
description: "Represents a scheduled/recurring job",
|
|
28
|
+
fields: {
|
|
29
|
+
id: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
30
|
+
name: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
31
|
+
description: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
32
|
+
cronExpression: {
|
|
33
|
+
type: ScalarTypeEnum.String_unsecure(),
|
|
34
|
+
isOptional: false
|
|
35
|
+
},
|
|
36
|
+
timezone: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
37
|
+
jobType: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
38
|
+
enabled: { type: ScalarTypeEnum.Boolean(), isOptional: false },
|
|
39
|
+
lastRunAt: { type: ScalarTypeEnum.DateTime(), isOptional: true },
|
|
40
|
+
nextRunAt: { type: ScalarTypeEnum.DateTime(), isOptional: true },
|
|
41
|
+
createdAt: { type: ScalarTypeEnum.DateTime(), isOptional: false }
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
var QueueStatsModel = defineSchemaModel({
|
|
45
|
+
name: "QueueStats",
|
|
46
|
+
description: "Job queue statistics",
|
|
47
|
+
fields: {
|
|
48
|
+
pending: { type: ScalarTypeEnum.Int_unsecure(), isOptional: false },
|
|
49
|
+
running: { type: ScalarTypeEnum.Int_unsecure(), isOptional: false },
|
|
50
|
+
completed: { type: ScalarTypeEnum.Int_unsecure(), isOptional: false },
|
|
51
|
+
failed: { type: ScalarTypeEnum.Int_unsecure(), isOptional: false },
|
|
52
|
+
deadLetter: { type: ScalarTypeEnum.Int_unsecure(), isOptional: false }
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
var EnqueueJobInput = defineSchemaModel({
|
|
56
|
+
name: "EnqueueJobInput",
|
|
57
|
+
description: "Input for enqueuing a new job",
|
|
58
|
+
fields: {
|
|
59
|
+
type: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
60
|
+
payload: { type: ScalarTypeEnum.JSON(), isOptional: false },
|
|
61
|
+
delaySeconds: { type: ScalarTypeEnum.Int_unsecure(), isOptional: true },
|
|
62
|
+
dedupeKey: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
63
|
+
maxRetries: { type: ScalarTypeEnum.Int_unsecure(), isOptional: true },
|
|
64
|
+
priority: { type: ScalarTypeEnum.Int_unsecure(), isOptional: true },
|
|
65
|
+
timeoutMs: { type: ScalarTypeEnum.Int_unsecure(), isOptional: true }
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
var GetJobInput = defineSchemaModel({
|
|
69
|
+
name: "GetJobInput",
|
|
70
|
+
description: "Input for getting a job by ID",
|
|
71
|
+
fields: {
|
|
72
|
+
jobId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false }
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
var CancelJobInput = defineSchemaModel({
|
|
76
|
+
name: "CancelJobInput",
|
|
77
|
+
description: "Input for cancelling a job",
|
|
78
|
+
fields: {
|
|
79
|
+
jobId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false }
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
var CancelJobOutput = defineSchemaModel({
|
|
83
|
+
name: "CancelJobOutput",
|
|
84
|
+
description: "Output for cancel job operation",
|
|
85
|
+
fields: {
|
|
86
|
+
success: { type: ScalarTypeEnum.Boolean(), isOptional: false }
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
var CreateScheduledJobInput = defineSchemaModel({
|
|
90
|
+
name: "CreateScheduledJobInput",
|
|
91
|
+
description: "Input for creating a scheduled job",
|
|
92
|
+
fields: {
|
|
93
|
+
name: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
94
|
+
description: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
95
|
+
cronExpression: {
|
|
96
|
+
type: ScalarTypeEnum.String_unsecure(),
|
|
97
|
+
isOptional: false
|
|
98
|
+
},
|
|
99
|
+
timezone: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
100
|
+
jobType: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
101
|
+
payload: { type: ScalarTypeEnum.JSON(), isOptional: true },
|
|
102
|
+
maxRetries: { type: ScalarTypeEnum.Int_unsecure(), isOptional: true },
|
|
103
|
+
enabled: { type: ScalarTypeEnum.Boolean(), isOptional: true }
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
var ListScheduledJobsOutput = defineSchemaModel({
|
|
107
|
+
name: "ListScheduledJobsOutput",
|
|
108
|
+
description: "Output for listing scheduled jobs",
|
|
109
|
+
fields: {
|
|
110
|
+
schedules: { type: ScheduledJobModel, isArray: true, isOptional: false }
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
var ToggleScheduledJobInput = defineSchemaModel({
|
|
114
|
+
name: "ToggleScheduledJobInput",
|
|
115
|
+
description: "Input for toggling a scheduled job",
|
|
116
|
+
fields: {
|
|
117
|
+
name: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
118
|
+
enabled: { type: ScalarTypeEnum.Boolean(), isOptional: false }
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
var JobEnqueuedPayload = defineSchemaModel({
|
|
122
|
+
name: "JobEnqueuedPayload",
|
|
123
|
+
description: "Payload for job.enqueued event",
|
|
124
|
+
fields: {
|
|
125
|
+
jobId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
126
|
+
type: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
127
|
+
priority: { type: ScalarTypeEnum.Int_unsecure(), isOptional: false },
|
|
128
|
+
scheduledAt: { type: ScalarTypeEnum.DateTime(), isOptional: true },
|
|
129
|
+
tenantId: { type: ScalarTypeEnum.String_unsecure(), isOptional: true },
|
|
130
|
+
enqueuedAt: { type: ScalarTypeEnum.DateTime(), isOptional: false }
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
var JobCancelledPayload = defineSchemaModel({
|
|
134
|
+
name: "JobCancelledPayload",
|
|
135
|
+
description: "Payload for job.cancelled event",
|
|
136
|
+
fields: {
|
|
137
|
+
jobId: { type: ScalarTypeEnum.String_unsecure(), isOptional: false }
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
var EnqueueJobContract = defineCommand({
|
|
141
|
+
meta: {
|
|
142
|
+
key: "jobs.enqueue",
|
|
143
|
+
version: "1.0.0",
|
|
144
|
+
stability: "stable",
|
|
145
|
+
owners: [...OWNERS],
|
|
146
|
+
tags: ["jobs", "enqueue"],
|
|
147
|
+
description: "Enqueue a background job for async processing.",
|
|
148
|
+
goal: "Allow services to offload work to background processing.",
|
|
149
|
+
context: "Called by any service that needs async processing."
|
|
150
|
+
},
|
|
151
|
+
io: {
|
|
152
|
+
input: EnqueueJobInput,
|
|
153
|
+
output: JobModel
|
|
154
|
+
},
|
|
155
|
+
policy: {
|
|
156
|
+
auth: "user"
|
|
157
|
+
},
|
|
158
|
+
sideEffects: {
|
|
159
|
+
emits: [
|
|
160
|
+
{
|
|
161
|
+
key: "job.enqueued",
|
|
162
|
+
version: "1.0.0",
|
|
163
|
+
when: "Job is enqueued",
|
|
164
|
+
payload: JobEnqueuedPayload
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
var GetJobContract = defineQuery({
|
|
170
|
+
meta: {
|
|
171
|
+
key: "jobs.get",
|
|
172
|
+
version: "1.0.0",
|
|
173
|
+
stability: "stable",
|
|
174
|
+
owners: [...OWNERS],
|
|
175
|
+
tags: ["jobs", "get"],
|
|
176
|
+
description: "Get a job by ID.",
|
|
177
|
+
goal: "Check job status and result.",
|
|
178
|
+
context: "Called to poll job status or retrieve results."
|
|
179
|
+
},
|
|
180
|
+
io: {
|
|
181
|
+
input: GetJobInput,
|
|
182
|
+
output: JobModel
|
|
183
|
+
},
|
|
184
|
+
policy: {
|
|
185
|
+
auth: "user"
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
var CancelJobContract = defineCommand({
|
|
189
|
+
meta: {
|
|
190
|
+
key: "jobs.cancel",
|
|
191
|
+
version: "1.0.0",
|
|
192
|
+
stability: "stable",
|
|
193
|
+
owners: [...OWNERS],
|
|
194
|
+
tags: ["jobs", "cancel"],
|
|
195
|
+
description: "Cancel a pending job.",
|
|
196
|
+
goal: "Allow cancellation of jobs that are no longer needed.",
|
|
197
|
+
context: "Only pending jobs can be cancelled."
|
|
198
|
+
},
|
|
199
|
+
io: {
|
|
200
|
+
input: CancelJobInput,
|
|
201
|
+
output: CancelJobOutput,
|
|
202
|
+
errors: {
|
|
203
|
+
JOB_NOT_FOUND: {
|
|
204
|
+
description: "Job does not exist",
|
|
205
|
+
http: 404,
|
|
206
|
+
gqlCode: "JOB_NOT_FOUND",
|
|
207
|
+
when: "Job ID is invalid"
|
|
208
|
+
},
|
|
209
|
+
JOB_NOT_PENDING: {
|
|
210
|
+
description: "Job is not in pending state",
|
|
211
|
+
http: 409,
|
|
212
|
+
gqlCode: "JOB_NOT_PENDING",
|
|
213
|
+
when: "Job has already started or completed"
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
policy: {
|
|
218
|
+
auth: "user"
|
|
219
|
+
},
|
|
220
|
+
sideEffects: {
|
|
221
|
+
emits: [
|
|
222
|
+
{
|
|
223
|
+
key: "job.cancelled",
|
|
224
|
+
version: "1.0.0",
|
|
225
|
+
when: "Job is cancelled",
|
|
226
|
+
payload: JobCancelledPayload
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
var GetQueueStatsContract = defineQuery({
|
|
232
|
+
meta: {
|
|
233
|
+
key: "jobs.stats",
|
|
234
|
+
version: "1.0.0",
|
|
235
|
+
stability: "stable",
|
|
236
|
+
owners: [...OWNERS],
|
|
237
|
+
tags: ["jobs", "stats", "admin"],
|
|
238
|
+
description: "Get job queue statistics.",
|
|
239
|
+
goal: "Monitor queue health and backlog.",
|
|
240
|
+
context: "Admin dashboard monitoring."
|
|
241
|
+
},
|
|
242
|
+
io: {
|
|
243
|
+
input: null,
|
|
244
|
+
output: QueueStatsModel
|
|
245
|
+
},
|
|
246
|
+
policy: {
|
|
247
|
+
auth: "admin"
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
var CreateScheduledJobContract = defineCommand({
|
|
251
|
+
meta: {
|
|
252
|
+
key: "jobs.schedule.create",
|
|
253
|
+
version: "1.0.0",
|
|
254
|
+
stability: "stable",
|
|
255
|
+
owners: [...OWNERS],
|
|
256
|
+
tags: ["jobs", "schedule", "create"],
|
|
257
|
+
description: "Create a scheduled/recurring job.",
|
|
258
|
+
goal: "Set up recurring background tasks.",
|
|
259
|
+
context: "Admin configuration for periodic tasks."
|
|
260
|
+
},
|
|
261
|
+
io: {
|
|
262
|
+
input: CreateScheduledJobInput,
|
|
263
|
+
output: ScheduledJobModel
|
|
264
|
+
},
|
|
265
|
+
policy: {
|
|
266
|
+
auth: "admin"
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
var ListScheduledJobsContract = defineQuery({
|
|
270
|
+
meta: {
|
|
271
|
+
key: "jobs.schedule.list",
|
|
272
|
+
version: "1.0.0",
|
|
273
|
+
stability: "stable",
|
|
274
|
+
owners: [...OWNERS],
|
|
275
|
+
tags: ["jobs", "schedule", "list"],
|
|
276
|
+
description: "List all scheduled jobs.",
|
|
277
|
+
goal: "View configured recurring tasks.",
|
|
278
|
+
context: "Admin dashboard."
|
|
279
|
+
},
|
|
280
|
+
io: {
|
|
281
|
+
input: null,
|
|
282
|
+
output: ListScheduledJobsOutput
|
|
283
|
+
},
|
|
284
|
+
policy: {
|
|
285
|
+
auth: "admin"
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
var ToggleScheduledJobContract = defineCommand({
|
|
289
|
+
meta: {
|
|
290
|
+
key: "jobs.schedule.toggle",
|
|
291
|
+
version: "1.0.0",
|
|
292
|
+
stability: "stable",
|
|
293
|
+
owners: [...OWNERS],
|
|
294
|
+
tags: ["jobs", "schedule", "toggle"],
|
|
295
|
+
description: "Enable or disable a scheduled job.",
|
|
296
|
+
goal: "Control when recurring tasks run.",
|
|
297
|
+
context: "Admin control over scheduled tasks."
|
|
298
|
+
},
|
|
299
|
+
io: {
|
|
300
|
+
input: ToggleScheduledJobInput,
|
|
301
|
+
output: ScheduledJobModel
|
|
302
|
+
},
|
|
303
|
+
policy: {
|
|
304
|
+
auth: "admin"
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// src/entities/index.ts
|
|
309
|
+
import {
|
|
310
|
+
defineEntity,
|
|
311
|
+
defineEntityEnum,
|
|
312
|
+
field,
|
|
313
|
+
index
|
|
314
|
+
} from "@contractspec/lib.schema";
|
|
315
|
+
var JobStatusEnum = defineEntityEnum({
|
|
316
|
+
name: "JobStatus",
|
|
317
|
+
values: [
|
|
318
|
+
"PENDING",
|
|
319
|
+
"RUNNING",
|
|
320
|
+
"COMPLETED",
|
|
321
|
+
"FAILED",
|
|
322
|
+
"CANCELLED",
|
|
323
|
+
"DEAD_LETTER"
|
|
324
|
+
],
|
|
325
|
+
schema: "lssm_jobs",
|
|
326
|
+
description: "Status of a background job."
|
|
327
|
+
});
|
|
328
|
+
var JobEntity = defineEntity({
|
|
329
|
+
name: "Job",
|
|
330
|
+
description: "A background job for async processing.",
|
|
331
|
+
schema: "lssm_jobs",
|
|
332
|
+
map: "job",
|
|
333
|
+
fields: {
|
|
334
|
+
id: field.id({ description: "Unique job identifier" }),
|
|
335
|
+
type: field.string({ description: "Job type identifier" }),
|
|
336
|
+
version: field.int({ default: 1, description: "Job type version" }),
|
|
337
|
+
payload: field.json({ description: "Job payload data" }),
|
|
338
|
+
status: field.enum("JobStatus", { default: "PENDING" }),
|
|
339
|
+
priority: field.int({ default: 0, description: "Higher = more urgent" }),
|
|
340
|
+
attempts: field.int({
|
|
341
|
+
default: 0,
|
|
342
|
+
description: "Number of execution attempts"
|
|
343
|
+
}),
|
|
344
|
+
maxRetries: field.int({
|
|
345
|
+
default: 3,
|
|
346
|
+
description: "Maximum retry attempts"
|
|
347
|
+
}),
|
|
348
|
+
lastError: field.string({
|
|
349
|
+
isOptional: true,
|
|
350
|
+
description: "Last error message"
|
|
351
|
+
}),
|
|
352
|
+
lastErrorStack: field.string({
|
|
353
|
+
isOptional: true,
|
|
354
|
+
description: "Last error stack trace"
|
|
355
|
+
}),
|
|
356
|
+
scheduledAt: field.dateTime({
|
|
357
|
+
isOptional: true,
|
|
358
|
+
description: "When job should be processed"
|
|
359
|
+
}),
|
|
360
|
+
startedAt: field.dateTime({
|
|
361
|
+
isOptional: true,
|
|
362
|
+
description: "When processing started"
|
|
363
|
+
}),
|
|
364
|
+
completedAt: field.dateTime({
|
|
365
|
+
isOptional: true,
|
|
366
|
+
description: "When processing completed"
|
|
367
|
+
}),
|
|
368
|
+
timeoutAt: field.dateTime({
|
|
369
|
+
isOptional: true,
|
|
370
|
+
description: "Job timeout deadline"
|
|
371
|
+
}),
|
|
372
|
+
dedupeKey: field.string({
|
|
373
|
+
isOptional: true,
|
|
374
|
+
description: "Key for deduplication"
|
|
375
|
+
}),
|
|
376
|
+
tenantId: field.string({
|
|
377
|
+
isOptional: true,
|
|
378
|
+
description: "Tenant/org context"
|
|
379
|
+
}),
|
|
380
|
+
userId: field.string({
|
|
381
|
+
isOptional: true,
|
|
382
|
+
description: "User who enqueued"
|
|
383
|
+
}),
|
|
384
|
+
traceId: field.string({
|
|
385
|
+
isOptional: true,
|
|
386
|
+
description: "Distributed trace ID"
|
|
387
|
+
}),
|
|
388
|
+
metadata: field.json({
|
|
389
|
+
isOptional: true,
|
|
390
|
+
description: "Additional metadata"
|
|
391
|
+
}),
|
|
392
|
+
result: field.json({ isOptional: true, description: "Job result data" }),
|
|
393
|
+
createdAt: field.createdAt(),
|
|
394
|
+
updatedAt: field.updatedAt(),
|
|
395
|
+
scheduledJob: field.belongsTo("ScheduledJob", ["scheduledJobId"], ["id"]),
|
|
396
|
+
scheduledJobId: field.string({ isOptional: true }),
|
|
397
|
+
executions: field.hasMany("JobExecution")
|
|
398
|
+
},
|
|
399
|
+
indexes: [
|
|
400
|
+
index.on(["status", "scheduledAt"]),
|
|
401
|
+
index.on(["type", "status"]),
|
|
402
|
+
index.on(["tenantId", "status"]),
|
|
403
|
+
index.unique(["dedupeKey"], { name: "job_dedupe_key_unique" })
|
|
404
|
+
],
|
|
405
|
+
enums: [JobStatusEnum]
|
|
406
|
+
});
|
|
407
|
+
var ScheduledJobEntity = defineEntity({
|
|
408
|
+
name: "ScheduledJob",
|
|
409
|
+
description: "A scheduled/recurring job definition.",
|
|
410
|
+
schema: "lssm_jobs",
|
|
411
|
+
map: "scheduled_job",
|
|
412
|
+
fields: {
|
|
413
|
+
id: field.id(),
|
|
414
|
+
name: field.string({ isUnique: true, description: "Unique schedule name" }),
|
|
415
|
+
description: field.string({ isOptional: true }),
|
|
416
|
+
cronExpression: field.string({
|
|
417
|
+
description: "Cron expression for scheduling"
|
|
418
|
+
}),
|
|
419
|
+
timezone: field.string({
|
|
420
|
+
default: '"UTC"',
|
|
421
|
+
description: "Timezone for cron evaluation"
|
|
422
|
+
}),
|
|
423
|
+
jobType: field.string({ description: "Job type to create" }),
|
|
424
|
+
jobVersion: field.int({ default: 1 }),
|
|
425
|
+
payload: field.json({
|
|
426
|
+
isOptional: true,
|
|
427
|
+
description: "Default payload for created jobs"
|
|
428
|
+
}),
|
|
429
|
+
maxRetries: field.int({ default: 3 }),
|
|
430
|
+
timeoutMs: field.int({
|
|
431
|
+
isOptional: true,
|
|
432
|
+
description: "Job timeout in milliseconds"
|
|
433
|
+
}),
|
|
434
|
+
enabled: field.boolean({ default: true }),
|
|
435
|
+
lastRunAt: field.dateTime({ isOptional: true }),
|
|
436
|
+
nextRunAt: field.dateTime({ isOptional: true }),
|
|
437
|
+
tenantId: field.string({ isOptional: true }),
|
|
438
|
+
createdAt: field.createdAt(),
|
|
439
|
+
updatedAt: field.updatedAt(),
|
|
440
|
+
jobs: field.hasMany("Job")
|
|
441
|
+
},
|
|
442
|
+
indexes: [index.on(["enabled", "nextRunAt"])]
|
|
443
|
+
});
|
|
444
|
+
var JobExecutionEntity = defineEntity({
|
|
445
|
+
name: "JobExecution",
|
|
446
|
+
description: "A single execution attempt of a job.",
|
|
447
|
+
schema: "lssm_jobs",
|
|
448
|
+
map: "job_execution",
|
|
449
|
+
fields: {
|
|
450
|
+
id: field.id(),
|
|
451
|
+
jobId: field.foreignKey(),
|
|
452
|
+
attemptNumber: field.int({ description: "Which attempt this is" }),
|
|
453
|
+
startedAt: field.dateTime(),
|
|
454
|
+
completedAt: field.dateTime({ isOptional: true }),
|
|
455
|
+
durationMs: field.int({ isOptional: true }),
|
|
456
|
+
success: field.boolean({ isOptional: true }),
|
|
457
|
+
error: field.string({ isOptional: true }),
|
|
458
|
+
errorStack: field.string({ isOptional: true }),
|
|
459
|
+
result: field.json({ isOptional: true }),
|
|
460
|
+
workerId: field.string({
|
|
461
|
+
isOptional: true,
|
|
462
|
+
description: "ID of worker that processed"
|
|
463
|
+
}),
|
|
464
|
+
job: field.belongsTo("Job", ["jobId"], ["id"], { onDelete: "Cascade" })
|
|
465
|
+
},
|
|
466
|
+
indexes: [index.on(["jobId", "attemptNumber"])]
|
|
467
|
+
});
|
|
468
|
+
var jobEntities = [JobEntity, ScheduledJobEntity, JobExecutionEntity];
|
|
469
|
+
var jobsSchemaContribution = {
|
|
470
|
+
moduleId: "@contractspec/lib.jobs",
|
|
471
|
+
entities: jobEntities,
|
|
472
|
+
enums: [JobStatusEnum]
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
// src/events.ts
|
|
476
|
+
import { ScalarTypeEnum as ScalarTypeEnum2, defineSchemaModel as defineSchemaModel2 } from "@contractspec/lib.schema";
|
|
477
|
+
import { defineEvent, StabilityEnum } from "@contractspec/lib.contracts";
|
|
478
|
+
var JobEnqueuedPayload2 = defineSchemaModel2({
|
|
479
|
+
name: "JobEnqueuedEventPayload",
|
|
480
|
+
description: "Payload when a job is added to the queue",
|
|
481
|
+
fields: {
|
|
482
|
+
jobId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
|
|
483
|
+
type: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
|
|
484
|
+
priority: { type: ScalarTypeEnum2.Int_unsecure(), isOptional: false },
|
|
485
|
+
scheduledAt: { type: ScalarTypeEnum2.DateTime(), isOptional: true },
|
|
486
|
+
tenantId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: true },
|
|
487
|
+
enqueuedAt: { type: ScalarTypeEnum2.DateTime(), isOptional: false }
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
var JobStartedPayload = defineSchemaModel2({
|
|
491
|
+
name: "JobStartedEventPayload",
|
|
492
|
+
description: "Payload when a job starts processing",
|
|
493
|
+
fields: {
|
|
494
|
+
jobId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
|
|
495
|
+
type: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
|
|
496
|
+
attempt: { type: ScalarTypeEnum2.Int_unsecure(), isOptional: false },
|
|
497
|
+
startedAt: { type: ScalarTypeEnum2.DateTime(), isOptional: false }
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
var JobCompletedPayload = defineSchemaModel2({
|
|
501
|
+
name: "JobCompletedEventPayload",
|
|
502
|
+
description: "Payload when a job completes successfully",
|
|
503
|
+
fields: {
|
|
504
|
+
jobId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
|
|
505
|
+
type: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
|
|
506
|
+
attempt: { type: ScalarTypeEnum2.Int_unsecure(), isOptional: false },
|
|
507
|
+
durationMs: { type: ScalarTypeEnum2.Int_unsecure(), isOptional: false },
|
|
508
|
+
completedAt: { type: ScalarTypeEnum2.DateTime(), isOptional: false }
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
var JobFailedPayload = defineSchemaModel2({
|
|
512
|
+
name: "JobFailedEventPayload",
|
|
513
|
+
description: "Payload when a job attempt fails",
|
|
514
|
+
fields: {
|
|
515
|
+
jobId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
|
|
516
|
+
type: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
|
|
517
|
+
attempt: { type: ScalarTypeEnum2.Int_unsecure(), isOptional: false },
|
|
518
|
+
error: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
|
|
519
|
+
willRetry: { type: ScalarTypeEnum2.Boolean(), isOptional: false },
|
|
520
|
+
failedAt: { type: ScalarTypeEnum2.DateTime(), isOptional: false }
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
var JobRetryingPayload = defineSchemaModel2({
|
|
524
|
+
name: "JobRetryingEventPayload",
|
|
525
|
+
description: "Payload when a job is scheduled for retry",
|
|
526
|
+
fields: {
|
|
527
|
+
jobId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
|
|
528
|
+
type: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
|
|
529
|
+
attempt: { type: ScalarTypeEnum2.Int_unsecure(), isOptional: false },
|
|
530
|
+
nextAttemptAt: { type: ScalarTypeEnum2.DateTime(), isOptional: false },
|
|
531
|
+
backoffMs: { type: ScalarTypeEnum2.Int_unsecure(), isOptional: false }
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
var JobDeadLetteredPayload = defineSchemaModel2({
|
|
535
|
+
name: "JobDeadLetteredEventPayload",
|
|
536
|
+
description: "Payload when a job is moved to dead letter queue",
|
|
537
|
+
fields: {
|
|
538
|
+
jobId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
|
|
539
|
+
type: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
|
|
540
|
+
attempts: { type: ScalarTypeEnum2.Int_unsecure(), isOptional: false },
|
|
541
|
+
lastError: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
|
|
542
|
+
deadLetteredAt: { type: ScalarTypeEnum2.DateTime(), isOptional: false }
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
var JobCancelledPayload2 = defineSchemaModel2({
|
|
546
|
+
name: "JobCancelledEventPayload",
|
|
547
|
+
description: "Payload when a job is cancelled",
|
|
548
|
+
fields: {
|
|
549
|
+
jobId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
|
|
550
|
+
type: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
|
|
551
|
+
cancelledBy: { type: ScalarTypeEnum2.String_unsecure(), isOptional: true },
|
|
552
|
+
cancelledAt: { type: ScalarTypeEnum2.DateTime(), isOptional: false }
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
var ScheduledJobTriggeredPayload = defineSchemaModel2({
|
|
556
|
+
name: "ScheduledJobTriggeredEventPayload",
|
|
557
|
+
description: "Payload when a scheduled job is triggered",
|
|
558
|
+
fields: {
|
|
559
|
+
scheduleName: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
|
|
560
|
+
jobId: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
|
|
561
|
+
jobType: { type: ScalarTypeEnum2.String_unsecure(), isOptional: false },
|
|
562
|
+
triggeredAt: { type: ScalarTypeEnum2.DateTime(), isOptional: false },
|
|
563
|
+
nextRunAt: { type: ScalarTypeEnum2.DateTime(), isOptional: true }
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
var JobEnqueuedEvent = defineEvent({
|
|
567
|
+
meta: {
|
|
568
|
+
key: "job.enqueued",
|
|
569
|
+
version: "1.0.0",
|
|
570
|
+
description: "A job has been added to the queue.",
|
|
571
|
+
stability: StabilityEnum.Stable,
|
|
572
|
+
owners: ["@contractspec.libs.jobs"],
|
|
573
|
+
tags: ["job-queue", "lifecycle"]
|
|
574
|
+
},
|
|
575
|
+
payload: JobEnqueuedPayload2
|
|
576
|
+
});
|
|
577
|
+
var JobStartedEvent = defineEvent({
|
|
578
|
+
meta: {
|
|
579
|
+
key: "job.started",
|
|
580
|
+
version: "1.0.0",
|
|
581
|
+
description: "A job has started processing.",
|
|
582
|
+
stability: StabilityEnum.Stable,
|
|
583
|
+
owners: ["@contractspec.libs.jobs"],
|
|
584
|
+
tags: ["job-queue", "lifecycle"]
|
|
585
|
+
},
|
|
586
|
+
payload: JobStartedPayload
|
|
587
|
+
});
|
|
588
|
+
var JobCompletedEvent = defineEvent({
|
|
589
|
+
meta: {
|
|
590
|
+
key: "job.completed",
|
|
591
|
+
version: "1.0.0",
|
|
592
|
+
description: "A job has completed successfully.",
|
|
593
|
+
stability: StabilityEnum.Stable,
|
|
594
|
+
owners: ["@contractspec.libs.jobs"],
|
|
595
|
+
tags: ["job-queue", "lifecycle"]
|
|
596
|
+
},
|
|
597
|
+
payload: JobCompletedPayload
|
|
598
|
+
});
|
|
599
|
+
var JobFailedEvent = defineEvent({
|
|
600
|
+
meta: {
|
|
601
|
+
key: "job.failed",
|
|
602
|
+
version: "1.0.0",
|
|
603
|
+
description: "A job attempt has failed.",
|
|
604
|
+
stability: StabilityEnum.Stable,
|
|
605
|
+
owners: ["@contractspec.libs.jobs"],
|
|
606
|
+
tags: ["job-queue", "lifecycle", "error"]
|
|
607
|
+
},
|
|
608
|
+
payload: JobFailedPayload
|
|
609
|
+
});
|
|
610
|
+
var JobRetryingEvent = defineEvent({
|
|
611
|
+
meta: {
|
|
612
|
+
key: "job.retrying",
|
|
613
|
+
version: "1.0.0",
|
|
614
|
+
description: "A job is being scheduled for retry.",
|
|
615
|
+
stability: StabilityEnum.Stable,
|
|
616
|
+
owners: ["@contractspec.libs.jobs"],
|
|
617
|
+
tags: ["job-queue", "lifecycle", "retry"]
|
|
618
|
+
},
|
|
619
|
+
payload: JobRetryingPayload
|
|
620
|
+
});
|
|
621
|
+
var JobDeadLetteredEvent = defineEvent({
|
|
622
|
+
meta: {
|
|
623
|
+
key: "job.dead_lettered",
|
|
624
|
+
version: "1.0.0",
|
|
625
|
+
description: "A job has exhausted all retries and moved to dead letter queue.",
|
|
626
|
+
stability: StabilityEnum.Stable,
|
|
627
|
+
owners: ["@contractspec.libs.jobs"],
|
|
628
|
+
tags: ["job-queue", "lifecycle", "error"]
|
|
629
|
+
},
|
|
630
|
+
payload: JobDeadLetteredPayload
|
|
631
|
+
});
|
|
632
|
+
var JobCancelledEvent = defineEvent({
|
|
633
|
+
meta: {
|
|
634
|
+
key: "job.cancelled",
|
|
635
|
+
version: "1.0.0",
|
|
636
|
+
description: "A job has been cancelled.",
|
|
637
|
+
stability: StabilityEnum.Stable,
|
|
638
|
+
owners: ["@contractspec.libs.jobs"],
|
|
639
|
+
tags: ["job-queue", "lifecycle"]
|
|
640
|
+
},
|
|
641
|
+
payload: JobCancelledPayload2
|
|
642
|
+
});
|
|
643
|
+
var ScheduledJobTriggeredEvent = defineEvent({
|
|
644
|
+
meta: {
|
|
645
|
+
key: "scheduler.job_triggered",
|
|
646
|
+
version: "1.0.0",
|
|
647
|
+
description: "A scheduled job has been triggered.",
|
|
648
|
+
stability: StabilityEnum.Stable,
|
|
649
|
+
owners: ["@contractspec.libs.jobs"],
|
|
650
|
+
tags: ["job-queue", "scheduler"]
|
|
651
|
+
},
|
|
652
|
+
payload: ScheduledJobTriggeredPayload
|
|
653
|
+
});
|
|
654
|
+
var JobEvents = {
|
|
655
|
+
JobEnqueuedEvent,
|
|
656
|
+
JobStartedEvent,
|
|
657
|
+
JobCompletedEvent,
|
|
658
|
+
JobFailedEvent,
|
|
659
|
+
JobRetryingEvent,
|
|
660
|
+
JobDeadLetteredEvent,
|
|
661
|
+
JobCancelledEvent,
|
|
662
|
+
ScheduledJobTriggeredEvent
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
// src/handlers/gmail-sync-handler.ts
|
|
666
|
+
function createGmailSyncHandler(adapter) {
|
|
667
|
+
return async (job) => {
|
|
668
|
+
await adapter.syncThreads(job.payload);
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// src/handlers/ping-job.ts
|
|
673
|
+
import * as z from "zod";
|
|
674
|
+
var PING_JOB_TYPE = "core.ping";
|
|
675
|
+
var PingPayloadSchema = z.object({});
|
|
676
|
+
var pingJob = {
|
|
677
|
+
type: PING_JOB_TYPE,
|
|
678
|
+
schema: PingPayloadSchema,
|
|
679
|
+
handler: async (_payload, _job) => {}
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
// src/handlers/storage-document-handler.ts
|
|
683
|
+
function createStorageDocumentHandler(storage, adapter) {
|
|
684
|
+
return async (job) => {
|
|
685
|
+
const object2 = await storage.getObject({
|
|
686
|
+
bucket: job.payload.bucket,
|
|
687
|
+
key: job.payload.key
|
|
688
|
+
});
|
|
689
|
+
if (!object2) {
|
|
690
|
+
throw new Error(`Object ${job.payload.bucket}/${job.payload.key} not found`);
|
|
691
|
+
}
|
|
692
|
+
await adapter.ingestObject(object2);
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// src/queue/register-defined-job.ts
|
|
697
|
+
function registerDefinedJob(queue, def) {
|
|
698
|
+
const wrapped = async (job) => {
|
|
699
|
+
const payload = def.schema.parse(job.payload);
|
|
700
|
+
const typedJob = {
|
|
701
|
+
...job,
|
|
702
|
+
payload
|
|
703
|
+
};
|
|
704
|
+
await def.handler(payload, typedJob);
|
|
705
|
+
};
|
|
706
|
+
queue.register(def.type, wrapped);
|
|
707
|
+
}
|
|
708
|
+
// src/handlers/index.ts
|
|
709
|
+
function registerAllJobs(queue) {
|
|
710
|
+
registerDefinedJob(queue, pingJob);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// src/queue/types.ts
|
|
714
|
+
export * from "@contractspec/lib.contracts/jobs/queue";
|
|
715
|
+
import {
|
|
716
|
+
calculateBackoff,
|
|
717
|
+
DEFAULT_RETRY_POLICY
|
|
718
|
+
} from "@contractspec/lib.contracts/jobs/queue";
|
|
719
|
+
|
|
720
|
+
// src/queue/memory-queue.ts
|
|
721
|
+
import { randomUUID } from "node:crypto";
|
|
722
|
+
class MemoryJobQueue {
|
|
723
|
+
jobs = new Map;
|
|
724
|
+
handlers = new Map;
|
|
725
|
+
timer;
|
|
726
|
+
activeCount = 0;
|
|
727
|
+
pollIntervalMs;
|
|
728
|
+
concurrency;
|
|
729
|
+
retryPolicy;
|
|
730
|
+
constructor(options = {}) {
|
|
731
|
+
this.pollIntervalMs = options.pollIntervalMs ?? 200;
|
|
732
|
+
this.concurrency = options.concurrency ?? 5;
|
|
733
|
+
this.retryPolicy = options.retryPolicy ?? DEFAULT_RETRY_POLICY;
|
|
734
|
+
}
|
|
735
|
+
async enqueue(jobType, payload, options = {}) {
|
|
736
|
+
if (options.dedupeKey) {
|
|
737
|
+
const existing = Array.from(this.jobs.values()).find((j) => j.dedupeKey === options.dedupeKey && j.status === "pending");
|
|
738
|
+
if (existing) {
|
|
739
|
+
return existing;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
const now = new Date;
|
|
743
|
+
const scheduledAt = options.delaySeconds ? new Date(now.getTime() + options.delaySeconds * 1000) : now;
|
|
744
|
+
const job = {
|
|
745
|
+
id: randomUUID(),
|
|
746
|
+
type: jobType,
|
|
747
|
+
version: "1.0.0",
|
|
748
|
+
payload,
|
|
749
|
+
status: "pending",
|
|
750
|
+
priority: options.priority ?? 0,
|
|
751
|
+
attempts: 0,
|
|
752
|
+
maxRetries: options.maxRetries ?? this.retryPolicy.maxRetries,
|
|
753
|
+
createdAt: now,
|
|
754
|
+
updatedAt: now,
|
|
755
|
+
scheduledAt,
|
|
756
|
+
dedupeKey: options.dedupeKey,
|
|
757
|
+
tenantId: options.tenantId,
|
|
758
|
+
userId: options.userId,
|
|
759
|
+
traceId: options.traceId,
|
|
760
|
+
metadata: options.metadata
|
|
761
|
+
};
|
|
762
|
+
if (options.timeoutMs) {
|
|
763
|
+
job.timeoutAt = new Date(now.getTime() + options.timeoutMs);
|
|
764
|
+
}
|
|
765
|
+
this.jobs.set(job.id, job);
|
|
766
|
+
return job;
|
|
767
|
+
}
|
|
768
|
+
register(jobType, handler) {
|
|
769
|
+
this.handlers.set(jobType, handler);
|
|
770
|
+
}
|
|
771
|
+
start() {
|
|
772
|
+
if (this.timer)
|
|
773
|
+
return;
|
|
774
|
+
this.timer = setInterval(() => {
|
|
775
|
+
this.processNext();
|
|
776
|
+
}, this.pollIntervalMs);
|
|
777
|
+
}
|
|
778
|
+
async stop() {
|
|
779
|
+
if (this.timer) {
|
|
780
|
+
clearInterval(this.timer);
|
|
781
|
+
this.timer = undefined;
|
|
782
|
+
}
|
|
783
|
+
while (this.activeCount > 0) {
|
|
784
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
async getJob(jobId) {
|
|
788
|
+
return this.jobs.get(jobId) ?? null;
|
|
789
|
+
}
|
|
790
|
+
async cancelJob(jobId) {
|
|
791
|
+
const job = this.jobs.get(jobId);
|
|
792
|
+
if (!job || job.status !== "pending") {
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
795
|
+
job.status = "cancelled";
|
|
796
|
+
job.updatedAt = new Date;
|
|
797
|
+
return true;
|
|
798
|
+
}
|
|
799
|
+
async getStats() {
|
|
800
|
+
const stats = {
|
|
801
|
+
pending: 0,
|
|
802
|
+
running: 0,
|
|
803
|
+
completed: 0,
|
|
804
|
+
failed: 0,
|
|
805
|
+
deadLetter: 0
|
|
806
|
+
};
|
|
807
|
+
for (const job of this.jobs.values()) {
|
|
808
|
+
switch (job.status) {
|
|
809
|
+
case "pending":
|
|
810
|
+
stats.pending++;
|
|
811
|
+
break;
|
|
812
|
+
case "running":
|
|
813
|
+
stats.running++;
|
|
814
|
+
break;
|
|
815
|
+
case "completed":
|
|
816
|
+
stats.completed++;
|
|
817
|
+
break;
|
|
818
|
+
case "failed":
|
|
819
|
+
stats.failed++;
|
|
820
|
+
break;
|
|
821
|
+
case "dead_letter":
|
|
822
|
+
stats.deadLetter++;
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return stats;
|
|
827
|
+
}
|
|
828
|
+
async processNext() {
|
|
829
|
+
if (this.activeCount >= this.concurrency)
|
|
830
|
+
return;
|
|
831
|
+
const now = new Date;
|
|
832
|
+
const pendingJobs = Array.from(this.jobs.values()).filter((j) => j.status === "pending" && (!j.scheduledAt || j.scheduledAt <= now)).sort((a, b) => {
|
|
833
|
+
if (a.priority !== b.priority) {
|
|
834
|
+
return b.priority - a.priority;
|
|
835
|
+
}
|
|
836
|
+
return (a.scheduledAt?.getTime() ?? 0) - (b.scheduledAt?.getTime() ?? 0);
|
|
837
|
+
});
|
|
838
|
+
const job = pendingJobs[0];
|
|
839
|
+
if (!job)
|
|
840
|
+
return;
|
|
841
|
+
const handler = this.handlers.get(job.type);
|
|
842
|
+
if (!handler)
|
|
843
|
+
return;
|
|
844
|
+
this.activeCount++;
|
|
845
|
+
job.status = "running";
|
|
846
|
+
job.startedAt = new Date;
|
|
847
|
+
job.updatedAt = new Date;
|
|
848
|
+
job.attempts += 1;
|
|
849
|
+
try {
|
|
850
|
+
const result = await handler(job);
|
|
851
|
+
job.status = "completed";
|
|
852
|
+
job.completedAt = new Date;
|
|
853
|
+
job.result = result;
|
|
854
|
+
} catch (error) {
|
|
855
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
856
|
+
job.lastError = errorMessage;
|
|
857
|
+
if (job.attempts >= job.maxRetries) {
|
|
858
|
+
job.status = "dead_letter";
|
|
859
|
+
} else {
|
|
860
|
+
const backoff = calculateBackoff(job.attempts, this.retryPolicy);
|
|
861
|
+
job.status = "pending";
|
|
862
|
+
job.scheduledAt = new Date(Date.now() + backoff);
|
|
863
|
+
}
|
|
864
|
+
} finally {
|
|
865
|
+
job.updatedAt = new Date;
|
|
866
|
+
this.activeCount--;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// src/queue/scaleway-sqs-queue.ts
|
|
872
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
873
|
+
import {
|
|
874
|
+
DeleteMessageCommand,
|
|
875
|
+
ReceiveMessageCommand,
|
|
876
|
+
SendMessageCommand,
|
|
877
|
+
SQSClient
|
|
878
|
+
} from "@aws-sdk/client-sqs";
|
|
879
|
+
class ScalewaySqsJobQueue {
|
|
880
|
+
sqs;
|
|
881
|
+
queueUrl;
|
|
882
|
+
waitTimeSeconds;
|
|
883
|
+
maxNumberOfMessages;
|
|
884
|
+
visibilityTimeoutSeconds;
|
|
885
|
+
handlers = new Map;
|
|
886
|
+
logger;
|
|
887
|
+
running = false;
|
|
888
|
+
constructor(config) {
|
|
889
|
+
this.logger = config.logger;
|
|
890
|
+
const accessKeyId = config.credentials?.accessKeyId ?? process.env.SCALEWAY_ACCESS_KEY_QUEUE;
|
|
891
|
+
const secretAccessKey = config.credentials?.secretAccessKey ?? process.env.SCALEWAY_SECRET_KEY_QUEUE;
|
|
892
|
+
if (!accessKeyId || !secretAccessKey) {
|
|
893
|
+
throw new Error("Missing SCALEWAY_ACCESS_KEY_QUEUE / SCALEWAY_SECRET_KEY_QUEUE in env");
|
|
894
|
+
}
|
|
895
|
+
const region = config.region ?? process.env.SCALEWAY_REGION ?? "par";
|
|
896
|
+
const endpoint = config.endpoint ?? "https://sqs.mnq.fr-par.scaleway.com";
|
|
897
|
+
this.sqs = new SQSClient({
|
|
898
|
+
region,
|
|
899
|
+
endpoint,
|
|
900
|
+
credentials: {
|
|
901
|
+
accessKeyId,
|
|
902
|
+
secretAccessKey
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
this.queueUrl = config.queueUrl;
|
|
906
|
+
this.waitTimeSeconds = config.waitTimeSeconds ?? 20;
|
|
907
|
+
this.maxNumberOfMessages = config.maxNumberOfMessages ?? 5;
|
|
908
|
+
this.visibilityTimeoutSeconds = config.visibilityTimeoutSeconds ?? 60;
|
|
909
|
+
}
|
|
910
|
+
async enqueue(jobType, payload, options = {}) {
|
|
911
|
+
const id = randomUUID2();
|
|
912
|
+
const now = new Date;
|
|
913
|
+
const scheduledAt = options.delaySeconds ? new Date(now.getTime() + options.delaySeconds * 1000) : now;
|
|
914
|
+
const envelope = {
|
|
915
|
+
id,
|
|
916
|
+
type: jobType,
|
|
917
|
+
payload
|
|
918
|
+
};
|
|
919
|
+
await this.sqs.send(new SendMessageCommand({
|
|
920
|
+
QueueUrl: this.queueUrl,
|
|
921
|
+
MessageBody: JSON.stringify(envelope),
|
|
922
|
+
DelaySeconds: options.delaySeconds ?? 0
|
|
923
|
+
}));
|
|
924
|
+
return {
|
|
925
|
+
id,
|
|
926
|
+
type: jobType,
|
|
927
|
+
version: "1.0.0",
|
|
928
|
+
payload,
|
|
929
|
+
status: "pending",
|
|
930
|
+
priority: options.priority ?? 0,
|
|
931
|
+
attempts: 0,
|
|
932
|
+
maxRetries: options.maxRetries ?? DEFAULT_RETRY_POLICY.maxRetries,
|
|
933
|
+
createdAt: now,
|
|
934
|
+
updatedAt: now,
|
|
935
|
+
scheduledAt,
|
|
936
|
+
dedupeKey: options.dedupeKey,
|
|
937
|
+
tenantId: options.tenantId,
|
|
938
|
+
userId: options.userId,
|
|
939
|
+
traceId: options.traceId,
|
|
940
|
+
metadata: options.metadata
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
register(jobType, handler) {
|
|
944
|
+
if (this.handlers.has(jobType)) {
|
|
945
|
+
throw new Error(`Handler already registered for job type "${jobType}"`);
|
|
946
|
+
}
|
|
947
|
+
this.handlers.set(jobType, handler);
|
|
948
|
+
}
|
|
949
|
+
start() {
|
|
950
|
+
if (this.running)
|
|
951
|
+
return;
|
|
952
|
+
this.running = true;
|
|
953
|
+
this.pollLoop().catch((error) => {
|
|
954
|
+
this.logger?.error?.("jobs.queue.scaleway_sqs.poll_loop_fatal", {
|
|
955
|
+
error: error instanceof Error ? error.message : String(error)
|
|
956
|
+
});
|
|
957
|
+
this.running = false;
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
async stop() {
|
|
961
|
+
this.running = false;
|
|
962
|
+
}
|
|
963
|
+
async pollLoop() {
|
|
964
|
+
this.logger?.info?.("jobs.queue.scaleway_sqs.started", {
|
|
965
|
+
queueUrl: this.queueUrl
|
|
966
|
+
});
|
|
967
|
+
while (this.running) {
|
|
968
|
+
try {
|
|
969
|
+
const res = await this.sqs.send(new ReceiveMessageCommand({
|
|
970
|
+
QueueUrl: this.queueUrl,
|
|
971
|
+
MaxNumberOfMessages: this.maxNumberOfMessages,
|
|
972
|
+
WaitTimeSeconds: this.waitTimeSeconds,
|
|
973
|
+
VisibilityTimeout: this.visibilityTimeoutSeconds,
|
|
974
|
+
MessageSystemAttributeNames: ["ApproximateReceiveCount"]
|
|
975
|
+
}));
|
|
976
|
+
const messages = res.Messages ?? [];
|
|
977
|
+
if (messages.length === 0) {
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
for (const msg of messages) {
|
|
981
|
+
if (!msg.Body || !msg.ReceiptHandle) {
|
|
982
|
+
this.logger?.warn?.("jobs.queue.scaleway_sqs.invalid_message", {
|
|
983
|
+
messageId: msg.MessageId,
|
|
984
|
+
reason: "missing_body_or_receipt"
|
|
985
|
+
});
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
let envelope;
|
|
989
|
+
try {
|
|
990
|
+
envelope = JSON.parse(msg.Body);
|
|
991
|
+
} catch (err) {
|
|
992
|
+
this.logger?.warn?.("jobs.queue.scaleway_sqs.parse_failed", {
|
|
993
|
+
messageId: msg.MessageId,
|
|
994
|
+
error: err instanceof Error ? err.message : String(err)
|
|
995
|
+
});
|
|
996
|
+
await this.deleteMessage(msg.ReceiptHandle);
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
const handler = this.handlers.get(envelope.type);
|
|
1000
|
+
if (!handler) {
|
|
1001
|
+
this.logger?.warn?.("jobs.queue.scaleway_sqs.missing_handler", {
|
|
1002
|
+
jobType: envelope.type,
|
|
1003
|
+
messageId: msg.MessageId
|
|
1004
|
+
});
|
|
1005
|
+
await this.deleteMessage(msg.ReceiptHandle);
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
const now = new Date;
|
|
1009
|
+
const attempts = parseInt(msg.Attributes?.ApproximateReceiveCount ?? "1", 10);
|
|
1010
|
+
const job = {
|
|
1011
|
+
id: envelope.id,
|
|
1012
|
+
type: envelope.type,
|
|
1013
|
+
version: "1.0.0",
|
|
1014
|
+
payload: envelope.payload,
|
|
1015
|
+
status: "pending",
|
|
1016
|
+
priority: 0,
|
|
1017
|
+
attempts,
|
|
1018
|
+
maxRetries: DEFAULT_RETRY_POLICY.maxRetries,
|
|
1019
|
+
createdAt: now,
|
|
1020
|
+
updatedAt: now
|
|
1021
|
+
};
|
|
1022
|
+
job.status = "running";
|
|
1023
|
+
job.updatedAt = new Date;
|
|
1024
|
+
try {
|
|
1025
|
+
await handler(job);
|
|
1026
|
+
job.status = "completed";
|
|
1027
|
+
job.updatedAt = new Date;
|
|
1028
|
+
await this.deleteMessage(msg.ReceiptHandle);
|
|
1029
|
+
} catch (err) {
|
|
1030
|
+
job.status = "failed";
|
|
1031
|
+
job.lastError = err instanceof Error ? err.message : "Unknown job error";
|
|
1032
|
+
job.updatedAt = new Date;
|
|
1033
|
+
this.logger?.error?.("jobs.queue.scaleway_sqs.job_failed", {
|
|
1034
|
+
jobType: job.type,
|
|
1035
|
+
jobId: job.id,
|
|
1036
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
} catch (err) {
|
|
1041
|
+
this.logger?.error?.("jobs.queue.scaleway_sqs.poll_error", {
|
|
1042
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1043
|
+
});
|
|
1044
|
+
await this.sleep(5000);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
this.logger?.info?.("jobs.queue.scaleway_sqs.stopped", {
|
|
1048
|
+
queueUrl: this.queueUrl
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
async deleteMessage(receiptHandle) {
|
|
1052
|
+
try {
|
|
1053
|
+
await this.sqs.send(new DeleteMessageCommand({
|
|
1054
|
+
QueueUrl: this.queueUrl,
|
|
1055
|
+
ReceiptHandle: receiptHandle
|
|
1056
|
+
}));
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
this.logger?.warn?.("jobs.queue.scaleway_sqs.delete_failed", {
|
|
1059
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
async sleep(ms) {
|
|
1064
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// src/queue/gcp-cloud-tasks.ts
|
|
1069
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
1070
|
+
class GcpCloudTasksQueue {
|
|
1071
|
+
options;
|
|
1072
|
+
handlers = new Map;
|
|
1073
|
+
constructor(options) {
|
|
1074
|
+
this.options = options;
|
|
1075
|
+
}
|
|
1076
|
+
async enqueue(jobType, payload, options = {}) {
|
|
1077
|
+
const now = new Date;
|
|
1078
|
+
const enqueueTime = options.delaySeconds != null ? { seconds: Math.floor(Date.now() / 1000) + options.delaySeconds } : undefined;
|
|
1079
|
+
const body = Buffer.from(JSON.stringify({
|
|
1080
|
+
id: randomUUID3(),
|
|
1081
|
+
type: jobType,
|
|
1082
|
+
payload
|
|
1083
|
+
}), "utf-8");
|
|
1084
|
+
await this.options.client.createTask({
|
|
1085
|
+
parent: `projects/${this.options.projectId}/locations/${this.options.location}/queues/${this.options.queue}`,
|
|
1086
|
+
task: {
|
|
1087
|
+
httpRequest: {
|
|
1088
|
+
httpMethod: "POST",
|
|
1089
|
+
url: this.options.resolveUrl(jobType),
|
|
1090
|
+
body,
|
|
1091
|
+
headers: { "Content-Type": "application/json" },
|
|
1092
|
+
oidcToken: this.options.serviceAccountEmail ? { serviceAccountEmail: this.options.serviceAccountEmail } : undefined
|
|
1093
|
+
},
|
|
1094
|
+
scheduleTime: enqueueTime
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
return {
|
|
1098
|
+
id: randomUUID3(),
|
|
1099
|
+
type: jobType,
|
|
1100
|
+
version: "1.0.0",
|
|
1101
|
+
payload,
|
|
1102
|
+
status: "pending",
|
|
1103
|
+
priority: options.priority ?? 0,
|
|
1104
|
+
attempts: 0,
|
|
1105
|
+
maxRetries: options.maxRetries ?? DEFAULT_RETRY_POLICY.maxRetries,
|
|
1106
|
+
createdAt: now,
|
|
1107
|
+
updatedAt: now,
|
|
1108
|
+
scheduledAt: options.delaySeconds ? new Date(now.getTime() + options.delaySeconds * 1000) : now,
|
|
1109
|
+
dedupeKey: options.dedupeKey,
|
|
1110
|
+
tenantId: options.tenantId,
|
|
1111
|
+
userId: options.userId,
|
|
1112
|
+
traceId: options.traceId,
|
|
1113
|
+
metadata: options.metadata
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
register(jobType, handler) {
|
|
1117
|
+
this.handlers.set(jobType, handler);
|
|
1118
|
+
}
|
|
1119
|
+
start() {}
|
|
1120
|
+
async stop() {
|
|
1121
|
+
this.handlers.clear();
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// src/queue/gcp-pubsub.ts
|
|
1126
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
1127
|
+
class GcpPubSubQueue {
|
|
1128
|
+
options;
|
|
1129
|
+
handlers = new Map;
|
|
1130
|
+
constructor(options) {
|
|
1131
|
+
this.options = options;
|
|
1132
|
+
}
|
|
1133
|
+
async enqueue(jobType, payload, options = {}) {
|
|
1134
|
+
const now = new Date;
|
|
1135
|
+
await this.options.client.topic(this.options.topicName).publishMessage({
|
|
1136
|
+
data: Buffer.from(JSON.stringify({
|
|
1137
|
+
id: randomUUID4(),
|
|
1138
|
+
type: jobType,
|
|
1139
|
+
payload
|
|
1140
|
+
}), "utf-8")
|
|
1141
|
+
});
|
|
1142
|
+
return {
|
|
1143
|
+
id: randomUUID4(),
|
|
1144
|
+
type: jobType,
|
|
1145
|
+
version: "1.0.0",
|
|
1146
|
+
payload,
|
|
1147
|
+
status: "pending",
|
|
1148
|
+
priority: options.priority ?? 0,
|
|
1149
|
+
attempts: 0,
|
|
1150
|
+
maxRetries: options.maxRetries ?? DEFAULT_RETRY_POLICY.maxRetries,
|
|
1151
|
+
createdAt: now,
|
|
1152
|
+
updatedAt: now,
|
|
1153
|
+
scheduledAt: options.delaySeconds ? new Date(now.getTime() + options.delaySeconds * 1000) : now,
|
|
1154
|
+
dedupeKey: options.dedupeKey,
|
|
1155
|
+
tenantId: options.tenantId,
|
|
1156
|
+
userId: options.userId,
|
|
1157
|
+
traceId: options.traceId,
|
|
1158
|
+
metadata: options.metadata
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
register(jobType, handler) {
|
|
1162
|
+
this.handlers.set(jobType, handler);
|
|
1163
|
+
}
|
|
1164
|
+
start() {}
|
|
1165
|
+
async stop() {
|
|
1166
|
+
this.handlers.clear();
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
// src/scheduler/index.ts
|
|
1170
|
+
function getNextCronRun(cronExpression, after = new Date) {
|
|
1171
|
+
try {
|
|
1172
|
+
const parts = cronExpression.trim().split(/\s+/);
|
|
1173
|
+
if (parts.length !== 5) {
|
|
1174
|
+
console.warn(`Invalid cron expression: ${cronExpression}`);
|
|
1175
|
+
return null;
|
|
1176
|
+
}
|
|
1177
|
+
const minute = parts[0];
|
|
1178
|
+
const hour = parts[1];
|
|
1179
|
+
const dayOfMonth = parts[2];
|
|
1180
|
+
const month = parts[3];
|
|
1181
|
+
const next = new Date(after);
|
|
1182
|
+
next.setSeconds(0);
|
|
1183
|
+
next.setMilliseconds(0);
|
|
1184
|
+
if (minute && hour && minute !== "*" && hour !== "*" && dayOfMonth === "*" && month === "*") {
|
|
1185
|
+
const targetMinute = Number.parseInt(minute, 10);
|
|
1186
|
+
const targetHour = Number.parseInt(hour, 10);
|
|
1187
|
+
next.setHours(targetHour, targetMinute, 0, 0);
|
|
1188
|
+
if (next <= after) {
|
|
1189
|
+
next.setDate(next.getDate() + 1);
|
|
1190
|
+
}
|
|
1191
|
+
return next;
|
|
1192
|
+
}
|
|
1193
|
+
next.setMinutes(next.getMinutes() + 1);
|
|
1194
|
+
return next;
|
|
1195
|
+
} catch {
|
|
1196
|
+
return null;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
class JobScheduler {
|
|
1201
|
+
queue;
|
|
1202
|
+
schedules = new Map;
|
|
1203
|
+
timer;
|
|
1204
|
+
checkIntervalMs;
|
|
1205
|
+
constructor(queue2, options = {}) {
|
|
1206
|
+
this.queue = queue2;
|
|
1207
|
+
this.checkIntervalMs = options.checkIntervalMs ?? 60000;
|
|
1208
|
+
}
|
|
1209
|
+
schedule(config) {
|
|
1210
|
+
const nextRun = config.enabled !== false ? getNextCronRun(config.cronExpression) : null;
|
|
1211
|
+
this.schedules.set(config.name, {
|
|
1212
|
+
...config,
|
|
1213
|
+
enabled: config.enabled ?? true,
|
|
1214
|
+
nextRun,
|
|
1215
|
+
lastRun: null
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
unschedule(name) {
|
|
1219
|
+
return this.schedules.delete(name);
|
|
1220
|
+
}
|
|
1221
|
+
enable(name) {
|
|
1222
|
+
const schedule = this.schedules.get(name);
|
|
1223
|
+
if (!schedule)
|
|
1224
|
+
return false;
|
|
1225
|
+
schedule.enabled = true;
|
|
1226
|
+
schedule.nextRun = getNextCronRun(schedule.cronExpression);
|
|
1227
|
+
return true;
|
|
1228
|
+
}
|
|
1229
|
+
disable(name) {
|
|
1230
|
+
const schedule = this.schedules.get(name);
|
|
1231
|
+
if (!schedule)
|
|
1232
|
+
return false;
|
|
1233
|
+
schedule.enabled = false;
|
|
1234
|
+
schedule.nextRun = null;
|
|
1235
|
+
return true;
|
|
1236
|
+
}
|
|
1237
|
+
getSchedules() {
|
|
1238
|
+
return Array.from(this.schedules.values());
|
|
1239
|
+
}
|
|
1240
|
+
getSchedule(name) {
|
|
1241
|
+
return this.schedules.get(name);
|
|
1242
|
+
}
|
|
1243
|
+
start() {
|
|
1244
|
+
if (this.timer)
|
|
1245
|
+
return;
|
|
1246
|
+
this.checkSchedules();
|
|
1247
|
+
this.timer = setInterval(() => {
|
|
1248
|
+
this.checkSchedules();
|
|
1249
|
+
}, this.checkIntervalMs);
|
|
1250
|
+
}
|
|
1251
|
+
stop() {
|
|
1252
|
+
if (this.timer) {
|
|
1253
|
+
clearInterval(this.timer);
|
|
1254
|
+
this.timer = undefined;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
async checkSchedules() {
|
|
1258
|
+
const now = new Date;
|
|
1259
|
+
for (const schedule of this.schedules.values()) {
|
|
1260
|
+
if (!schedule.enabled || !schedule.nextRun)
|
|
1261
|
+
continue;
|
|
1262
|
+
if (schedule.nextRun <= now) {
|
|
1263
|
+
try {
|
|
1264
|
+
const payload = typeof schedule.payload === "function" ? await schedule.payload() : schedule.payload;
|
|
1265
|
+
await this.queue.enqueue(schedule.jobType, payload, schedule.options);
|
|
1266
|
+
schedule.lastRun = now;
|
|
1267
|
+
schedule.nextRun = getNextCronRun(schedule.cronExpression, now);
|
|
1268
|
+
} catch (error) {
|
|
1269
|
+
console.error(`Failed to enqueue scheduled job ${schedule.name}:`, error);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
function createScheduler(queue2, options) {
|
|
1276
|
+
return new JobScheduler(queue2, options);
|
|
1277
|
+
}
|
|
1278
|
+
function defineSchedule(config) {
|
|
1279
|
+
return config;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// src/jobs.feature.ts
|
|
1283
|
+
import { defineFeature } from "@contractspec/lib.contracts";
|
|
1284
|
+
var JobsFeature = defineFeature({
|
|
1285
|
+
meta: {
|
|
1286
|
+
key: "jobs",
|
|
1287
|
+
title: "Background Jobs",
|
|
1288
|
+
description: "Background job processing, scheduling, and queue management",
|
|
1289
|
+
domain: "platform",
|
|
1290
|
+
owners: ["@platform.jobs"],
|
|
1291
|
+
tags: ["jobs", "queue", "background", "scheduler"],
|
|
1292
|
+
stability: "stable",
|
|
1293
|
+
version: "1.0.0"
|
|
1294
|
+
},
|
|
1295
|
+
operations: [
|
|
1296
|
+
{ key: "jobs.enqueue", version: "1.0.0" },
|
|
1297
|
+
{ key: "jobs.cancel", version: "1.0.0" },
|
|
1298
|
+
{ key: "jobs.get", version: "1.0.0" },
|
|
1299
|
+
{ key: "jobs.stats", version: "1.0.0" },
|
|
1300
|
+
{ key: "jobs.schedule.create", version: "1.0.0" },
|
|
1301
|
+
{ key: "jobs.schedule.toggle", version: "1.0.0" },
|
|
1302
|
+
{ key: "jobs.schedule.list", version: "1.0.0" }
|
|
1303
|
+
],
|
|
1304
|
+
events: [
|
|
1305
|
+
{ key: "job.enqueued", version: "1.0.0" },
|
|
1306
|
+
{ key: "job.started", version: "1.0.0" },
|
|
1307
|
+
{ key: "job.completed", version: "1.0.0" },
|
|
1308
|
+
{ key: "job.failed", version: "1.0.0" },
|
|
1309
|
+
{ key: "job.retrying", version: "1.0.0" },
|
|
1310
|
+
{ key: "job.dead_lettered", version: "1.0.0" },
|
|
1311
|
+
{ key: "job.cancelled", version: "1.0.0" },
|
|
1312
|
+
{ key: "scheduler.job_triggered", version: "1.0.0" }
|
|
1313
|
+
],
|
|
1314
|
+
presentations: [],
|
|
1315
|
+
opToPresentation: [],
|
|
1316
|
+
presentationsTargets: [],
|
|
1317
|
+
capabilities: {
|
|
1318
|
+
provides: [
|
|
1319
|
+
{ key: "jobs", version: "1.0.0" },
|
|
1320
|
+
{ key: "scheduler", version: "1.0.0" }
|
|
1321
|
+
],
|
|
1322
|
+
requires: []
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
export {
|
|
1326
|
+
registerDefinedJob,
|
|
1327
|
+
registerAllJobs,
|
|
1328
|
+
pingJob,
|
|
1329
|
+
jobsSchemaContribution,
|
|
1330
|
+
jobEntities,
|
|
1331
|
+
defineSchedule,
|
|
1332
|
+
createStorageDocumentHandler,
|
|
1333
|
+
createScheduler,
|
|
1334
|
+
createGmailSyncHandler,
|
|
1335
|
+
calculateBackoff,
|
|
1336
|
+
ToggleScheduledJobContract,
|
|
1337
|
+
ScheduledJobTriggeredEvent,
|
|
1338
|
+
ScheduledJobModel,
|
|
1339
|
+
ScheduledJobEntity,
|
|
1340
|
+
ScalewaySqsJobQueue,
|
|
1341
|
+
QueueStatsModel,
|
|
1342
|
+
PingPayloadSchema,
|
|
1343
|
+
PING_JOB_TYPE,
|
|
1344
|
+
MemoryJobQueue,
|
|
1345
|
+
ListScheduledJobsContract,
|
|
1346
|
+
JobsFeature,
|
|
1347
|
+
JobStatusEnum,
|
|
1348
|
+
JobStartedEvent,
|
|
1349
|
+
JobScheduler,
|
|
1350
|
+
JobRetryingEvent,
|
|
1351
|
+
JobModel,
|
|
1352
|
+
JobFailedEvent,
|
|
1353
|
+
JobExecutionEntity,
|
|
1354
|
+
JobEvents,
|
|
1355
|
+
JobEntity,
|
|
1356
|
+
JobEnqueuedEvent,
|
|
1357
|
+
JobDeadLetteredEvent,
|
|
1358
|
+
JobCompletedEvent,
|
|
1359
|
+
JobCancelledEvent,
|
|
1360
|
+
GetQueueStatsContract,
|
|
1361
|
+
GetJobContract,
|
|
1362
|
+
GcpPubSubQueue,
|
|
1363
|
+
GcpCloudTasksQueue,
|
|
1364
|
+
EnqueueJobContract,
|
|
1365
|
+
DEFAULT_RETRY_POLICY,
|
|
1366
|
+
CreateScheduledJobContract,
|
|
1367
|
+
CancelJobContract
|
|
1368
|
+
};
|