@terreno/jobs 57.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +56 -0
  3. package/biome.jsonc +20 -0
  4. package/bunfig.toml +5 -0
  5. package/dist/activeJobExecution.d.ts +3 -0
  6. package/dist/activeJobExecution.js +29 -0
  7. package/dist/claimLock.d.ts +16 -0
  8. package/dist/claimLock.js +34 -0
  9. package/dist/dispatchCompensation.d.ts +3 -0
  10. package/dist/dispatchCompensation.js +89 -0
  11. package/dist/dispatchError.d.ts +10 -0
  12. package/dist/dispatchError.js +33 -0
  13. package/dist/index.d.ts +9 -0
  14. package/dist/index.js +15 -0
  15. package/dist/jobExecutor.d.ts +34 -0
  16. package/dist/jobExecutor.js +328 -0
  17. package/dist/jobsApp.d.ts +61 -0
  18. package/dist/jobsApp.js +308 -0
  19. package/dist/jobsService.d.ts +38 -0
  20. package/dist/jobsService.js +505 -0
  21. package/dist/modelTypes.d.ts +52 -0
  22. package/dist/modelTypes.js +4 -0
  23. package/dist/models/job.d.ts +2 -0
  24. package/dist/models/job.js +124 -0
  25. package/dist/models/jobSchedule.d.ts +2 -0
  26. package/dist/models/jobSchedule.js +48 -0
  27. package/dist/retryBackoff.d.ts +17 -0
  28. package/dist/retryBackoff.js +22 -0
  29. package/dist/routes/jobsAdmin.d.ts +15 -0
  30. package/dist/routes/jobsAdmin.js +596 -0
  31. package/dist/routes/jobsExecute.d.ts +12 -0
  32. package/dist/routes/jobsExecute.js +119 -0
  33. package/dist/runners/gcpCloudTasks.d.ts +78 -0
  34. package/dist/runners/gcpCloudTasks.js +256 -0
  35. package/dist/runners/mongoRunner.d.ts +14 -0
  36. package/dist/runners/mongoRunner.js +204 -0
  37. package/dist/runners/vercelQueues.d.ts +111 -0
  38. package/dist/runners/vercelQueues.js +326 -0
  39. package/dist/scheduleCron.d.ts +15 -0
  40. package/dist/scheduleCron.js +61 -0
  41. package/dist/scheduler.d.ts +8 -0
  42. package/dist/scheduler.js +178 -0
  43. package/dist/types.d.ts +64 -0
  44. package/dist/types.js +2 -0
  45. package/package.json +82 -0
  46. package/src/activeJobExecution.ts +28 -0
  47. package/src/claimLock.ts +37 -0
  48. package/src/dispatchCompensation.ts +39 -0
  49. package/src/dispatchError.ts +14 -0
  50. package/src/index.ts +24 -0
  51. package/src/jobExecutor.ts +333 -0
  52. package/src/jobsApp.ts +255 -0
  53. package/src/jobsService.ts +395 -0
  54. package/src/modelTypes.ts +78 -0
  55. package/src/models/job.ts +129 -0
  56. package/src/models/jobSchedule.ts +57 -0
  57. package/src/retryBackoff.ts +34 -0
  58. package/src/routes/jobsAdmin.ts +562 -0
  59. package/src/routes/jobsExecute.ts +95 -0
  60. package/src/runners/gcpCloudTasks.ts +290 -0
  61. package/src/runners/mongoRunner.ts +135 -0
  62. package/src/runners/vercelQueues.ts +394 -0
  63. package/src/scheduleCron.ts +65 -0
  64. package/src/scheduler.ts +112 -0
  65. package/src/tests/bunSetup.ts +9 -0
  66. package/src/tests/gcpCloudTasksRunner.test.ts +349 -0
  67. package/src/tests/jobModel.test.ts +62 -0
  68. package/src/tests/jobScheduleModel.test.ts +44 -0
  69. package/src/tests/jobsAdmin.test.ts +728 -0
  70. package/src/tests/jobsApp.test.ts +113 -0
  71. package/src/tests/jobsCustomRunner.test.ts +367 -0
  72. package/src/tests/jobsDelayIdempotency.test.ts +255 -0
  73. package/src/tests/jobsExecute.test.ts +640 -0
  74. package/src/tests/jobsRetry.test.ts +420 -0
  75. package/src/tests/jobsSchedule.test.ts +505 -0
  76. package/src/tests/jobsTracer.test.ts +165 -0
  77. package/src/tests/jobsWorkerLifecycle.test.ts +509 -0
  78. package/src/tests/packageExports.test.ts +24 -0
  79. package/src/tests/retryBackoff.test.ts +76 -0
  80. package/src/tests/scheduleCron.test.ts +87 -0
  81. package/src/tests/vercelQueuesRunner.test.ts +524 -0
  82. package/src/types.ts +74 -0
  83. package/tsconfig.json +33 -0
@@ -0,0 +1,308 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __generator = (this && this.__generator) || function (thisArg, body) {
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
13
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
+ function verb(n) { return function (v) { return step([n, v]); }; }
15
+ function step(op) {
16
+ if (f) throw new TypeError("Generator is already executing.");
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19
+ if (y = 0, t) op = [op[0] & 2, t.value];
20
+ switch (op[0]) {
21
+ case 0: case 1: t = op; break;
22
+ case 4: _.label++; return { value: op[1], done: false };
23
+ case 5: _.label++; y = op[1]; op = [0]; continue;
24
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
+ default:
26
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
+ if (t[2]) _.ops.pop();
31
+ _.trys.pop(); continue;
32
+ }
33
+ op = body.call(thisArg, _);
34
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
+ }
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.JobsApp = void 0;
40
+ var api_1 = require("@terreno/api");
41
+ var jobExecutor_1 = require("./jobExecutor");
42
+ var jobsService_1 = require("./jobsService");
43
+ var job_1 = require("./models/job");
44
+ var jobSchedule_1 = require("./models/jobSchedule");
45
+ var jobsAdmin_1 = require("./routes/jobsAdmin");
46
+ var jobsExecute_1 = require("./routes/jobsExecute");
47
+ var mongoRunner_1 = require("./runners/mongoRunner");
48
+ /**
49
+ * TerrenoPlugin for durable background jobs.
50
+ *
51
+ * Registering `JobsApp` wires the plugin into the Express app but does **not**
52
+ * start a worker poll loop. Call {@link JobsApp.startWorker} explicitly from the
53
+ * API process or a dedicated worker entrypoint.
54
+ */
55
+ var JobsApp = /** @class */ (function () {
56
+ function JobsApp(options) {
57
+ var _a, _b;
58
+ this.workerActive = false;
59
+ this.options = options !== null && options !== void 0 ? options : {};
60
+ this.runner = (_a = this.options.runner) !== null && _a !== void 0 ? _a : new mongoRunner_1.MongoJobRunner();
61
+ this.service = new jobsService_1.JobsService({
62
+ defaultTimezone: (_b = this.options.timezone) !== null && _b !== void 0 ? _b : "UTC",
63
+ lockTtlMs: this.options.lockTtlMs,
64
+ runner: this.runner,
65
+ });
66
+ }
67
+ JobsApp.prototype.define = function (name, definition) {
68
+ this.service.define(name, definition);
69
+ if (this.workerActive && definition.schedule) {
70
+ this.service.markSchedulesDirty();
71
+ }
72
+ return this;
73
+ };
74
+ JobsApp.prototype.getBasePath = function () {
75
+ var _a;
76
+ return (_a = this.options.basePath) !== null && _a !== void 0 ? _a : "/jobs";
77
+ };
78
+ JobsApp.prototype.getDefinition = function (name) {
79
+ return this.service.getDefinition(name);
80
+ };
81
+ JobsApp.prototype.getDefaultTimezone = function () {
82
+ return this.service.getDefaultTimezone();
83
+ };
84
+ JobsApp.prototype.getLockTtlMs = function () {
85
+ return this.service.getLockTtlMs();
86
+ };
87
+ /** Execute a persisted job by id from an external dispatch surface (queue consumer, execute HTTP). */
88
+ JobsApp.prototype.executeQueuedJob = function (jobId_1) {
89
+ return __awaiter(this, arguments, void 0, function (jobId, signal) {
90
+ if (signal === void 0) { signal = AbortSignal.timeout(30 * 60 * 1000); }
91
+ return __generator(this, function (_a) {
92
+ return [2 /*return*/, (0, jobExecutor_1.executeJobById)({
93
+ host: this,
94
+ jobId: jobId,
95
+ signal: signal,
96
+ })];
97
+ });
98
+ });
99
+ };
100
+ JobsApp.prototype.getPollIntervalMs = function () {
101
+ var _a;
102
+ return (_a = this.options.pollIntervalMs) !== null && _a !== void 0 ? _a : 1000;
103
+ };
104
+ JobsApp.prototype.reconcileSchedules = function () {
105
+ return __awaiter(this, void 0, void 0, function () {
106
+ return __generator(this, function (_a) {
107
+ switch (_a.label) {
108
+ case 0: return [4 /*yield*/, this.service.reconcileSchedules()];
109
+ case 1:
110
+ _a.sent();
111
+ return [2 /*return*/];
112
+ }
113
+ });
114
+ });
115
+ };
116
+ JobsApp.prototype.reconcileSchedulesIfDirty = function () {
117
+ return __awaiter(this, void 0, void 0, function () {
118
+ return __generator(this, function (_a) {
119
+ switch (_a.label) {
120
+ case 0: return [4 /*yield*/, this.service.reconcileSchedulesIfDirty()];
121
+ case 1:
122
+ _a.sent();
123
+ return [2 /*return*/];
124
+ }
125
+ });
126
+ });
127
+ };
128
+ JobsApp.prototype.tickSchedules = function (now) {
129
+ return __awaiter(this, void 0, void 0, function () {
130
+ return __generator(this, function (_a) {
131
+ switch (_a.label) {
132
+ case 0: return [4 /*yield*/, this.service.tickSchedules(now)];
133
+ case 1:
134
+ _a.sent();
135
+ return [2 /*return*/];
136
+ }
137
+ });
138
+ });
139
+ };
140
+ JobsApp.prototype.register = function (app, openApi) {
141
+ (0, jobsService_1.registerJobsService)(this.service);
142
+ (0, jobsAdmin_1.registerJobsAdminRoutes)({
143
+ app: app,
144
+ options: {
145
+ accessControl: this.options.accessControl,
146
+ basePath: this.getBasePath(),
147
+ jobsService: this.service,
148
+ openApi: openApi,
149
+ redactPayload: this.options.redactPayload,
150
+ },
151
+ });
152
+ if (this.shouldMountExecuteRoute()) {
153
+ if (!this.options.executeAuth) {
154
+ throw new Error("JobsApp execute route is enabled but executeAuth is missing. Provide executeAuth when mountExecuteRoute is true or the runner requires the execute route.");
155
+ }
156
+ (0, jobsExecute_1.registerJobsExecuteRoute)({
157
+ app: app,
158
+ options: {
159
+ basePath: this.getBasePath(),
160
+ executeAuth: this.options.executeAuth,
161
+ host: this.service,
162
+ },
163
+ });
164
+ }
165
+ void job_1.Job.init().catch(function (error) {
166
+ api_1.logger.error("[jobs] Failed to build Job indexes: ".concat(String(error)));
167
+ });
168
+ void jobSchedule_1.JobSchedule.init().catch(function (error) {
169
+ api_1.logger.error("[jobs] Failed to build JobSchedule indexes: ".concat(String(error)));
170
+ });
171
+ };
172
+ JobsApp.prototype.adminContribution = function () {
173
+ return {
174
+ customScreens: [
175
+ {
176
+ displayName: "Jobs",
177
+ icon: "clock",
178
+ name: "jobs",
179
+ },
180
+ ],
181
+ homeWidgets: [{ displayName: "Jobs", icon: "clock", id: "jobs" }],
182
+ };
183
+ };
184
+ JobsApp.prototype.shouldMountExecuteRoute = function () {
185
+ if (this.options.mountExecuteRoute) {
186
+ return true;
187
+ }
188
+ return this.runner.requiresExecuteRoute === true;
189
+ };
190
+ JobsApp.prototype.isWorkerActive = function () {
191
+ return this.workerActive;
192
+ };
193
+ /**
194
+ * Starts the background worker poll loop without awaiting it.
195
+ *
196
+ * Fire-and-forget: resolves once the runner has been scheduled. Runner errors are
197
+ * logged and worker state is reset; they are not thrown from this method.
198
+ */
199
+ JobsApp.prototype.startWorker = function () {
200
+ return __awaiter(this, void 0, void 0, function () {
201
+ return __generator(this, function (_a) {
202
+ switch (_a.label) {
203
+ case 0:
204
+ if (this.workerActive) {
205
+ return [2 /*return*/];
206
+ }
207
+ if (!(this.workerStartPromise !== undefined)) return [3 /*break*/, 2];
208
+ return [4 /*yield*/, this.workerStartPromise];
209
+ case 1:
210
+ _a.sent();
211
+ return [2 /*return*/];
212
+ case 2:
213
+ this.workerStartPromise = this.beginWorkerStart();
214
+ _a.label = 3;
215
+ case 3:
216
+ _a.trys.push([3, , 5, 6]);
217
+ return [4 /*yield*/, this.workerStartPromise];
218
+ case 4:
219
+ _a.sent();
220
+ return [3 /*break*/, 6];
221
+ case 5:
222
+ this.workerStartPromise = undefined;
223
+ return [7 /*endfinally*/];
224
+ case 6: return [2 /*return*/];
225
+ }
226
+ });
227
+ });
228
+ };
229
+ JobsApp.prototype.stopWorker = function () {
230
+ return __awaiter(this, void 0, void 0, function () {
231
+ var stopPromise, _a;
232
+ var _b, _c, _d;
233
+ return __generator(this, function (_e) {
234
+ switch (_e.label) {
235
+ case 0:
236
+ if (!this.workerActive) {
237
+ return [2 /*return*/];
238
+ }
239
+ stopPromise = (_c = (_b = this.runner).stop) === null || _c === void 0 ? void 0 : _c.call(_b);
240
+ (_d = this.abortController) === null || _d === void 0 ? void 0 : _d.abort();
241
+ _e.label = 1;
242
+ case 1:
243
+ _e.trys.push([1, 3, , 4]);
244
+ return [4 /*yield*/, this.workerPromise];
245
+ case 2:
246
+ _e.sent();
247
+ return [3 /*break*/, 4];
248
+ case 3:
249
+ _a = _e.sent();
250
+ return [3 /*break*/, 4];
251
+ case 4: return [4 /*yield*/, stopPromise];
252
+ case 5:
253
+ _e.sent();
254
+ this.resetWorkerState();
255
+ return [2 /*return*/];
256
+ }
257
+ });
258
+ });
259
+ };
260
+ JobsApp.prototype.resetWorkerState = function () {
261
+ this.workerActive = false;
262
+ this.abortController = undefined;
263
+ this.workerPromise = undefined;
264
+ };
265
+ JobsApp.prototype.beginWorkerStart = function () {
266
+ return __awaiter(this, void 0, void 0, function () {
267
+ var error_1;
268
+ var _this = this;
269
+ return __generator(this, function (_a) {
270
+ switch (_a.label) {
271
+ case 0:
272
+ if (this.workerActive) {
273
+ return [2 /*return*/];
274
+ }
275
+ if (!this.runner.start) {
276
+ throw new Error("Job runner \"".concat(this.runner.id, "\" does not support start()"));
277
+ }
278
+ this.workerActive = true;
279
+ _a.label = 1;
280
+ case 1:
281
+ _a.trys.push([1, 3, , 4]);
282
+ return [4 /*yield*/, this.service.reconcileSchedules()];
283
+ case 2:
284
+ _a.sent();
285
+ this.abortController = new AbortController();
286
+ this.workerPromise = this.runner
287
+ .start({
288
+ jobs: this,
289
+ pollIntervalMs: this.getPollIntervalMs(),
290
+ signal: this.abortController.signal,
291
+ })
292
+ .catch(function (error) {
293
+ _this.resetWorkerState();
294
+ api_1.logger.error("[jobs] Worker stopped with error: ".concat(String(error)));
295
+ });
296
+ return [3 /*break*/, 4];
297
+ case 3:
298
+ error_1 = _a.sent();
299
+ this.resetWorkerState();
300
+ throw error_1;
301
+ case 4: return [2 /*return*/];
302
+ }
303
+ });
304
+ });
305
+ };
306
+ return JobsApp;
307
+ }());
308
+ exports.JobsApp = JobsApp;
@@ -0,0 +1,38 @@
1
+ import type { JobDocument, JobScheduleDocument } from "./modelTypes";
2
+ import type { EnqueueJobParams, JobDefinition, JobRunner } from "./types";
3
+ export declare class JobsService {
4
+ private readonly defaultTimezone;
5
+ private readonly definitions;
6
+ private readonly lockTtlMs;
7
+ private readonly runner;
8
+ private schedulesDirty;
9
+ constructor(options?: {
10
+ defaultTimezone?: string;
11
+ lockTtlMs?: number;
12
+ runner?: JobRunner;
13
+ });
14
+ define(name: string, definition: JobDefinition): void;
15
+ getDefinition(name: string): JobDefinition | undefined;
16
+ getDefaultTimezone(): string;
17
+ getLockTtlMs(): number;
18
+ isSchedulesDirty(): boolean;
19
+ markSchedulesDirty(): void;
20
+ reconcileSchedulesIfDirty(): Promise<void>;
21
+ reconcileSchedules(): Promise<void>;
22
+ tickSchedules(now?: Date): Promise<void>;
23
+ enqueue(params: EnqueueJobParams): Promise<JobDocument>;
24
+ adminRetryJob(jobId: string): Promise<{
25
+ original: JobDocument;
26
+ retry: JobDocument;
27
+ }>;
28
+ adminRequeueJob(jobId: string): Promise<JobDocument>;
29
+ adminCancelJob(jobId: string): Promise<JobDocument>;
30
+ pauseSchedule(name: string): Promise<JobScheduleDocument>;
31
+ resumeSchedule(name: string): Promise<JobScheduleDocument>;
32
+ }
33
+ export declare const getJobsService: () => JobsService;
34
+ /** Returns the registered jobs service when `JobsApp` is mounted; otherwise `undefined`. */
35
+ export declare const tryGetJobsService: () => JobsService | undefined;
36
+ export declare const registerJobsService: (service: JobsService) => void;
37
+ /** Clears the process-wide jobs service. Tests use this so AdminApp falls back to in-process runs. */
38
+ export declare const unregisterJobsService: () => void;