sidekiq-ts 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +686 -0
  2. package/dist/api.d.ts +172 -0
  3. package/dist/api.d.ts.map +1 -0
  4. package/dist/api.js +679 -0
  5. package/dist/backtrace.d.ts +3 -0
  6. package/dist/backtrace.d.ts.map +1 -0
  7. package/dist/backtrace.js +16 -0
  8. package/dist/cli-helpers.d.ts +22 -0
  9. package/dist/cli-helpers.d.ts.map +1 -0
  10. package/dist/cli-helpers.js +152 -0
  11. package/dist/cli.d.ts +3 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +143 -0
  14. package/dist/client.d.ts +25 -0
  15. package/dist/client.d.ts.map +1 -0
  16. package/dist/client.js +212 -0
  17. package/dist/config-loader.d.ts +16 -0
  18. package/dist/config-loader.d.ts.map +1 -0
  19. package/dist/config-loader.js +37 -0
  20. package/dist/config.d.ts +59 -0
  21. package/dist/config.d.ts.map +1 -0
  22. package/dist/config.js +155 -0
  23. package/dist/context.d.ts +10 -0
  24. package/dist/context.d.ts.map +1 -0
  25. package/dist/context.js +29 -0
  26. package/dist/cron.d.ts +44 -0
  27. package/dist/cron.d.ts.map +1 -0
  28. package/dist/cron.js +173 -0
  29. package/dist/index.d.ts +16 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +14 -0
  32. package/dist/interrupt-handler.d.ts +8 -0
  33. package/dist/interrupt-handler.d.ts.map +1 -0
  34. package/dist/interrupt-handler.js +24 -0
  35. package/dist/iterable-constants.d.ts +3 -0
  36. package/dist/iterable-constants.d.ts.map +1 -0
  37. package/dist/iterable-constants.js +2 -0
  38. package/dist/iterable-errors.d.ts +10 -0
  39. package/dist/iterable-errors.d.ts.map +1 -0
  40. package/dist/iterable-errors.js +18 -0
  41. package/dist/iterable.d.ts +44 -0
  42. package/dist/iterable.d.ts.map +1 -0
  43. package/dist/iterable.js +298 -0
  44. package/dist/job-logger.d.ts +12 -0
  45. package/dist/job-logger.d.ts.map +1 -0
  46. package/dist/job-logger.js +64 -0
  47. package/dist/job-util.d.ts +8 -0
  48. package/dist/job-util.d.ts.map +1 -0
  49. package/dist/job-util.js +158 -0
  50. package/dist/job.d.ts +73 -0
  51. package/dist/job.d.ts.map +1 -0
  52. package/dist/job.js +200 -0
  53. package/dist/json.d.ts +3 -0
  54. package/dist/json.d.ts.map +1 -0
  55. package/dist/json.js +2 -0
  56. package/dist/leader.d.ts +63 -0
  57. package/dist/leader.d.ts.map +1 -0
  58. package/dist/leader.js +193 -0
  59. package/dist/logger.d.ts +53 -0
  60. package/dist/logger.d.ts.map +1 -0
  61. package/dist/logger.js +143 -0
  62. package/dist/middleware.d.ts +23 -0
  63. package/dist/middleware.d.ts.map +1 -0
  64. package/dist/middleware.js +92 -0
  65. package/dist/periodic.d.ts +80 -0
  66. package/dist/periodic.d.ts.map +1 -0
  67. package/dist/periodic.js +205 -0
  68. package/dist/redis.d.ts +3 -0
  69. package/dist/redis.d.ts.map +1 -0
  70. package/dist/redis.js +1 -0
  71. package/dist/registry.d.ts +11 -0
  72. package/dist/registry.d.ts.map +1 -0
  73. package/dist/registry.js +8 -0
  74. package/dist/runner.d.ts +81 -0
  75. package/dist/runner.d.ts.map +1 -0
  76. package/dist/runner.js +791 -0
  77. package/dist/sidekiq.d.ts +43 -0
  78. package/dist/sidekiq.d.ts.map +1 -0
  79. package/dist/sidekiq.js +189 -0
  80. package/dist/testing.d.ts +32 -0
  81. package/dist/testing.d.ts.map +1 -0
  82. package/dist/testing.js +112 -0
  83. package/dist/types.d.ts +116 -0
  84. package/dist/types.d.ts.map +1 -0
  85. package/dist/types.js +1 -0
  86. package/package.json +42 -0
@@ -0,0 +1,298 @@
1
+ import { ITERATION_STATE_FLUSH_INTERVAL_SECONDS, ITERATION_STATE_TTL_SECONDS, } from "./iterable-constants.js";
2
+ import { IterableAbort, IterableInterrupted } from "./iterable-errors.js";
3
+ import { Job } from "./job.js";
4
+ import { dumpJson, loadJson } from "./json.js";
5
+ import { Sidekiq } from "./sidekiq.js";
6
+ const freezeValue = (value) => {
7
+ if (value && typeof value === "object") {
8
+ return Object.freeze(value);
9
+ }
10
+ return value;
11
+ };
12
+ export class IterableJob extends Job {
13
+ executions = 0;
14
+ cursorValue = null;
15
+ startTime = 0;
16
+ runtimeSeconds = 0;
17
+ argsValue = null;
18
+ cancelledValue = null;
19
+ currentObjectValue = null;
20
+ static abort() {
21
+ throw new IterableAbort();
22
+ }
23
+ get currentObject() {
24
+ return this.currentObjectValue;
25
+ }
26
+ arguments() {
27
+ return this.argsValue;
28
+ }
29
+ cursor() {
30
+ return freezeValue(this.cursorValue);
31
+ }
32
+ async cancel() {
33
+ if (await this.isCancelled()) {
34
+ return true;
35
+ }
36
+ if (!this.jid) {
37
+ return false;
38
+ }
39
+ const redis = await Sidekiq.defaultConfiguration.getRedisClient();
40
+ const key = this.iterationKey();
41
+ const now = Math.floor(Date.now() / 1000);
42
+ const pipeline = redis.multi();
43
+ pipeline.hSetNX(key, "cancelled", String(now));
44
+ pipeline.hGet(key, "cancelled");
45
+ pipeline.expire(key, ITERATION_STATE_TTL_SECONDS, "NX");
46
+ const result = await pipeline.exec();
47
+ const cancelled = result?.[1] ?? null;
48
+ this.cancelledValue = cancelled ? Number(cancelled) : null;
49
+ return Boolean(this.cancelledValue);
50
+ }
51
+ cancelled() {
52
+ return Boolean(this.cancelledValue);
53
+ }
54
+ // biome-ignore lint/suspicious/useAwait: lifecycle hook may be overridden with async implementation
55
+ async onStart() {
56
+ return undefined;
57
+ }
58
+ // biome-ignore lint/suspicious/useAwait: lifecycle hook may be overridden with async implementation
59
+ async onResume() {
60
+ return undefined;
61
+ }
62
+ // biome-ignore lint/suspicious/useAwait: lifecycle hook may be overridden with async implementation
63
+ async onStop() {
64
+ return undefined;
65
+ }
66
+ // biome-ignore lint/suspicious/useAwait: lifecycle hook may be overridden with async implementation
67
+ async onCancel() {
68
+ return undefined;
69
+ }
70
+ // biome-ignore lint/suspicious/useAwait: lifecycle hook may be overridden with async implementation
71
+ async onComplete() {
72
+ return undefined;
73
+ }
74
+ async aroundIteration(fn) {
75
+ await fn();
76
+ }
77
+ buildEnumerator(..._args) {
78
+ throw new Error(`${this.constructor.name} must implement a 'buildEnumerator' method`);
79
+ }
80
+ eachIteration(_item, ..._args) {
81
+ throw new Error(`${this.constructor.name} must implement an 'eachIteration' method`);
82
+ }
83
+ arrayEnumerator(array, cursor) {
84
+ if (!Array.isArray(array)) {
85
+ throw new Error("array must be an Array");
86
+ }
87
+ let index = cursor ?? 0;
88
+ const iterator = {
89
+ [Symbol.iterator]() {
90
+ return this;
91
+ },
92
+ next() {
93
+ if (index >= array.length) {
94
+ return { done: true, value: undefined };
95
+ }
96
+ const value = array[index];
97
+ const cursor = index;
98
+ index += 1;
99
+ return { done: false, value: [value, cursor] };
100
+ },
101
+ };
102
+ return iterator;
103
+ }
104
+ iterationKey() {
105
+ return `it-${this.jid ?? ""}`;
106
+ }
107
+ async perform(...args) {
108
+ this.argsValue = Object.freeze([...args]);
109
+ await this.fetchPreviousIterationState();
110
+ this.executions += 1;
111
+ this.startTime = this.monoNow();
112
+ const enumerator = this.buildEnumerator(...args, {
113
+ cursor: this.cursorValue,
114
+ });
115
+ if (!enumerator) {
116
+ this.logger().info(() => "'buildEnumerator' returned nil, skipping the job.");
117
+ return;
118
+ }
119
+ const iterator = this.assertEnumerator(enumerator);
120
+ if (this.executions === 1) {
121
+ await this.onStart();
122
+ }
123
+ else {
124
+ await this.onResume();
125
+ }
126
+ let completed = null;
127
+ try {
128
+ completed = await this.iterateWithEnumerator(iterator, args);
129
+ }
130
+ catch (error) {
131
+ if (error instanceof IterableAbort) {
132
+ completed = null;
133
+ }
134
+ else {
135
+ throw error;
136
+ }
137
+ }
138
+ finally {
139
+ await this.onStop();
140
+ }
141
+ const finished = this.handleCompleted(completed);
142
+ if (finished) {
143
+ await this.onComplete();
144
+ await this.cleanup();
145
+ }
146
+ else {
147
+ await this.reenqueueIterationJob();
148
+ }
149
+ }
150
+ async isCancelled() {
151
+ if (!this.jid) {
152
+ return false;
153
+ }
154
+ const redis = await Sidekiq.defaultConfiguration.getRedisClient();
155
+ const cancelled = await redis.hGet(this.iterationKey(), "cancelled");
156
+ this.cancelledValue = cancelled ? Number(cancelled) : null;
157
+ return Boolean(this.cancelledValue);
158
+ }
159
+ async fetchPreviousIterationState() {
160
+ if (!this.jid) {
161
+ return;
162
+ }
163
+ const redis = await Sidekiq.defaultConfiguration.getRedisClient();
164
+ const state = await redis.hGetAll(this.iterationKey());
165
+ if (Object.keys(state).length === 0) {
166
+ return;
167
+ }
168
+ this.executions = Number(state.ex ?? 0);
169
+ this.cursorValue = state.c ? loadJson(state.c) : null;
170
+ this.runtimeSeconds = Number(state.rt ?? 0);
171
+ }
172
+ async iterateWithEnumerator(iterator, args) {
173
+ if (await this.isCancelled()) {
174
+ await this.onCancel();
175
+ this.logger().info(() => "Job cancelled");
176
+ return true;
177
+ }
178
+ const timeLimit = Sidekiq.defaultConfiguration.timeout;
179
+ let foundRecord = false;
180
+ let stateFlushedAt = this.monoNow();
181
+ try {
182
+ for (let next = iterator.next(); !next.done; next = iterator.next()) {
183
+ const [object, cursor] = next.value;
184
+ foundRecord = true;
185
+ this.cursorValue = cursor;
186
+ this.currentObjectValue = object;
187
+ const interruptJob = this.interrupted() || this.shouldInterrupt();
188
+ if (this.monoNow() - stateFlushedAt >=
189
+ ITERATION_STATE_FLUSH_INTERVAL_SECONDS ||
190
+ interruptJob) {
191
+ const cancelled = await this.flushState();
192
+ stateFlushedAt = this.monoNow();
193
+ if (cancelled) {
194
+ this.cancelledValue = cancelled;
195
+ await this.onCancel();
196
+ this.logger().info(() => "Job cancelled");
197
+ return true;
198
+ }
199
+ }
200
+ if (interruptJob) {
201
+ return false;
202
+ }
203
+ await this.verifyIterationTime(timeLimit, async () => {
204
+ await this.aroundIteration(async () => {
205
+ try {
206
+ await this.eachIteration(object, ...args);
207
+ }
208
+ catch (error) {
209
+ await this.flushState();
210
+ throw error;
211
+ }
212
+ });
213
+ });
214
+ }
215
+ if (!foundRecord) {
216
+ this.logger().debug("Enumerator found nothing to iterate!");
217
+ }
218
+ return true;
219
+ }
220
+ finally {
221
+ this.runtimeSeconds += this.monoNow() - this.startTime;
222
+ }
223
+ }
224
+ async verifyIterationTime(timeLimitSeconds, fn) {
225
+ const start = this.monoNow();
226
+ await fn();
227
+ const total = this.monoNow() - start;
228
+ if (timeLimitSeconds > 0 && total > timeLimitSeconds) {
229
+ this.logger().warn(() => `Iteration took longer (${total.toFixed(2)}) than Sidekiq's shutdown timeout (${timeLimitSeconds}).`);
230
+ }
231
+ }
232
+ async reenqueueIterationJob() {
233
+ await this.flushState();
234
+ this.logger().debug(() => `Interrupting job (cursor=${String(this.cursorValue)})`);
235
+ throw new IterableInterrupted();
236
+ }
237
+ assertEnumerator(source) {
238
+ if (Array.isArray(source)) {
239
+ throw new Error(`buildEnumerator must return an Iterator, but returned ${source.constructor.name}.`);
240
+ }
241
+ if (typeof source.next ===
242
+ "function") {
243
+ return source;
244
+ }
245
+ const iterable = source;
246
+ if (iterable && typeof iterable[Symbol.iterator] === "function") {
247
+ return iterable[Symbol.iterator]();
248
+ }
249
+ const typeName = source && typeof source === "object"
250
+ ? (source.constructor?.name ??
251
+ "Object")
252
+ : typeof source;
253
+ throw new Error(`buildEnumerator must return an Iterator, but returned ${typeName}.`);
254
+ }
255
+ shouldInterrupt() {
256
+ const maxRuntime = Sidekiq.defaultConfiguration.maxIterationRuntime;
257
+ return Boolean(maxRuntime && this.monoNow() - this.startTime > maxRuntime);
258
+ }
259
+ async flushState() {
260
+ if (!this.jid) {
261
+ return null;
262
+ }
263
+ const redis = await Sidekiq.defaultConfiguration.getRedisClient();
264
+ const key = this.iterationKey();
265
+ const state = {
266
+ ex: String(this.executions),
267
+ c: dumpJson(this.cursorValue),
268
+ rt: String(this.runtimeSeconds),
269
+ };
270
+ const pipeline = redis.multi();
271
+ pipeline.hSet(key, state);
272
+ pipeline.expire(key, ITERATION_STATE_TTL_SECONDS, "NX");
273
+ pipeline.hGet(key, "cancelled");
274
+ const result = await pipeline.exec();
275
+ const cancelled = result?.[2] ?? null;
276
+ return cancelled ? Number(cancelled) : null;
277
+ }
278
+ async cleanup() {
279
+ this.logger().debug(() => `Completed iteration. executions=${this.executions} runtime=${this.runtimeSeconds.toFixed(3)}`);
280
+ if (!this.jid) {
281
+ return;
282
+ }
283
+ const redis = await Sidekiq.defaultConfiguration.getRedisClient();
284
+ await redis.unlink(this.iterationKey());
285
+ }
286
+ handleCompleted(completed) {
287
+ if (completed === null || completed === true) {
288
+ return true;
289
+ }
290
+ if (completed === false) {
291
+ return false;
292
+ }
293
+ throw new Error(`Unexpected completion value: ${String(completed)}`);
294
+ }
295
+ monoNow() {
296
+ return Number(process.hrtime.bigint()) / 1_000_000_000;
297
+ }
298
+ }
@@ -0,0 +1,12 @@
1
+ import type { Config } from "./config.js";
2
+ import type { JobLogger, JobPayload } from "./types.js";
3
+ export declare class DefaultJobLogger implements JobLogger {
4
+ private readonly config;
5
+ private readonly skip;
6
+ constructor(config: Config);
7
+ prepare<T>(payload: JobPayload, fn: () => Promise<T> | T): Promise<T>;
8
+ call<T>(payload: JobPayload, queue: string, fn: () => Promise<T> | T): Promise<T>;
9
+ private log;
10
+ private resolveLevel;
11
+ }
12
+ //# sourceMappingURL=job-logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job-logger.d.ts","sourceRoot":"","sources":["../src/job-logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExD,qBAAa,gBAAiB,YAAW,SAAS;IAChD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAU;gBAEnB,MAAM,EAAE,MAAM;IAKpB,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAiBrE,IAAI,CAAC,CAAC,EACV,OAAO,EAAE,UAAU,EACnB,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GACvB,OAAO,CAAC,CAAC,CAAC;IAcb,OAAO,CAAC,GAAG;IAqBX,OAAO,CAAC,YAAY;CAerB"}
@@ -0,0 +1,64 @@
1
+ import { Context } from "./context.js";
2
+ export class DefaultJobLogger {
3
+ config;
4
+ skip;
5
+ constructor(config) {
6
+ this.config = config;
7
+ this.skip = Boolean(this.config.skipDefaultJobLogging);
8
+ }
9
+ async prepare(payload, fn) {
10
+ const klass = payload.wrapped ?? payload.class;
11
+ const context = {
12
+ jid: payload.jid,
13
+ class: typeof klass === "string" ? klass : String(klass),
14
+ };
15
+ for (const attr of this.config.loggedJobAttributes) {
16
+ const value = payload[attr];
17
+ if (value !== undefined) {
18
+ context[attr] = value;
19
+ }
20
+ }
21
+ return await Context.with(context, fn);
22
+ }
23
+ async call(payload, queue, fn) {
24
+ const start = process.hrtime.bigint();
25
+ Context.add("queue", queue);
26
+ this.log("info", payload, queue, "start");
27
+ try {
28
+ const result = await fn();
29
+ this.log("info", payload, queue, "done", start);
30
+ return result;
31
+ }
32
+ catch (error) {
33
+ this.log("warn", payload, queue, "fail", start);
34
+ throw error;
35
+ }
36
+ }
37
+ log(fallbackLevel, payload, _queue, phase, start) {
38
+ if (this.skip) {
39
+ return;
40
+ }
41
+ const elapsed = start
42
+ ? Number((process.hrtime.bigint() - start) / 1000000n) / 1000
43
+ : null;
44
+ const level = this.resolveLevel(payload.log_level) ?? fallbackLevel;
45
+ const logger = this.config.logger;
46
+ if (elapsed !== null) {
47
+ Context.add("elapsed", Number(elapsed.toFixed(3)));
48
+ }
49
+ logger[level](() => phase);
50
+ }
51
+ resolveLevel(level) {
52
+ if (!level) {
53
+ return null;
54
+ }
55
+ const normalized = level.toLowerCase();
56
+ if (normalized === "debug" ||
57
+ normalized === "info" ||
58
+ normalized === "warn" ||
59
+ normalized === "error") {
60
+ return normalized;
61
+ }
62
+ return null;
63
+ }
64
+ }
@@ -0,0 +1,8 @@
1
+ import type { JobOptions, JobPayload, StrictArgsMode } from "./types.js";
2
+ export declare const TRANSIENT_ATTRIBUTES: string[];
3
+ export declare const nowInMillis: () => number;
4
+ export declare const generateJid: () => string;
5
+ export declare const validateItem: (item: JobPayload) => void;
6
+ export declare const normalizeItem: (item: JobPayload, defaultOptions: JobOptions) => JobPayload;
7
+ export declare const verifyJson: (args: unknown[], mode: StrictArgsMode) => void;
8
+ //# sourceMappingURL=job-util.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job-util.d.ts","sourceRoot":"","sources":["../src/job-util.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEV,UAAU,EACV,UAAU,EACV,cAAc,EACf,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,oBAAoB,EAAE,MAAM,EAAO,CAAC;AAIjD,eAAO,MAAM,WAAW,QAAO,MAAoB,CAAC;AAEpD,eAAO,MAAM,WAAW,QAAO,MAAyC,CAAC;AAiBzE,eAAO,MAAM,YAAY,GAAI,MAAM,UAAU,KAAG,IAmC/C,CAAC;AAeF,eAAO,MAAM,aAAa,GACxB,MAAM,UAAU,EAChB,gBAAgB,UAAU,KACzB,UA2CF,CAAC;AAyDF,eAAO,MAAM,UAAU,GAAI,MAAM,OAAO,EAAE,EAAE,MAAM,cAAc,KAAG,IAgBlE,CAAC"}
@@ -0,0 +1,158 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { registerJob } from "./registry.js";
3
+ export const TRANSIENT_ATTRIBUTES = [];
4
+ const MAX_RETRY_FOR_SECONDS = 1_000_000_000;
5
+ export const nowInMillis = () => Date.now();
6
+ export const generateJid = () => randomBytes(12).toString("hex");
7
+ const className = (klass) => {
8
+ if (typeof klass === "string") {
9
+ return klass;
10
+ }
11
+ const name = klass?.name;
12
+ if (!name) {
13
+ throw new Error("Job class must have a name");
14
+ }
15
+ return name;
16
+ };
17
+ const isJobClassLike = (value) => typeof value === "function" ||
18
+ (typeof value === "object" && value !== null && "name" in value);
19
+ export const validateItem = (item) => {
20
+ if (!item || typeof item !== "object") {
21
+ throw new Error(`Job must be an object with 'class' and 'args' keys: ${item}`);
22
+ }
23
+ if (!("class" in item && "args" in item)) {
24
+ throw new Error(`Job must include 'class' and 'args': ${JSON.stringify(item)}`);
25
+ }
26
+ if (!Array.isArray(item.args)) {
27
+ throw new Error(`Job args must be an Array: ${JSON.stringify(item)}`);
28
+ }
29
+ if (!isJobClassLike(item.class) && typeof item.class !== "string") {
30
+ throw new Error(`Job class must be a class or string name: ${JSON.stringify(item)}`);
31
+ }
32
+ if (item.at !== undefined && typeof item.at !== "number") {
33
+ throw new Error(`Job 'at' must be a number timestamp: ${JSON.stringify(item)}`);
34
+ }
35
+ if (item.tags && !Array.isArray(item.tags)) {
36
+ throw new Error(`Job tags must be an Array: ${JSON.stringify(item)}`);
37
+ }
38
+ if (typeof item.retry_for === "number" &&
39
+ item.retry_for > MAX_RETRY_FOR_SECONDS) {
40
+ throw new Error(`retry_for must be a relative amount of time: ${JSON.stringify(item)}`);
41
+ }
42
+ };
43
+ const normalizedHash = (itemClass, defaultOptions) => {
44
+ if (typeof itemClass === "function") {
45
+ const klass = itemClass;
46
+ if (klass.getSidekiqOptions) {
47
+ return klass.getSidekiqOptions();
48
+ }
49
+ }
50
+ return defaultOptions;
51
+ };
52
+ export const normalizeItem = (item, defaultOptions) => {
53
+ validateItem(item);
54
+ let defaults = normalizedHash(item.class, defaultOptions);
55
+ if (item.wrapped && typeof item.wrapped !== "string") {
56
+ const wrapped = item.wrapped;
57
+ if (wrapped.getSidekiqOptions) {
58
+ defaults = {
59
+ ...defaults,
60
+ ...wrapped.getSidekiqOptions(),
61
+ };
62
+ }
63
+ else {
64
+ // fall through without overriding defaults
65
+ }
66
+ }
67
+ const merged = {
68
+ ...defaults,
69
+ ...item,
70
+ };
71
+ if (!merged.queue || merged.queue === "") {
72
+ throw new Error("Job must include a valid queue name");
73
+ }
74
+ for (const key of TRANSIENT_ATTRIBUTES) {
75
+ merged[key] = undefined;
76
+ }
77
+ merged.jid = merged.jid ?? generateJid();
78
+ if (typeof merged.class === "function") {
79
+ registerJob(merged.class);
80
+ }
81
+ merged.class = className(merged.class);
82
+ if (merged.wrapped) {
83
+ merged.wrapped = className(merged.wrapped);
84
+ }
85
+ merged.queue = String(merged.queue);
86
+ if (typeof merged.retry_for === "number") {
87
+ merged.retry_for = Math.trunc(merged.retry_for);
88
+ }
89
+ merged.created_at = merged.created_at ?? nowInMillis();
90
+ return merged;
91
+ };
92
+ const isPlainObject = (value) => {
93
+ if (!value || typeof value !== "object") {
94
+ return false;
95
+ }
96
+ const proto = Object.getPrototypeOf(value);
97
+ return proto === Object.prototype || proto === null;
98
+ };
99
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: type checking for JSON requires many cases
100
+ const jsonUnsafe = (value) => {
101
+ if (value === null) {
102
+ return null;
103
+ }
104
+ const valueType = typeof value;
105
+ if (valueType === "string" || valueType === "boolean") {
106
+ return null;
107
+ }
108
+ if (valueType === "number") {
109
+ return Number.isFinite(value) ? null : value;
110
+ }
111
+ if (valueType === "undefined" ||
112
+ valueType === "bigint" ||
113
+ valueType === "symbol") {
114
+ return value;
115
+ }
116
+ if (valueType === "function") {
117
+ return value;
118
+ }
119
+ if (Array.isArray(value)) {
120
+ for (const entry of value) {
121
+ const unsafe = jsonUnsafe(entry);
122
+ if (unsafe !== null) {
123
+ return unsafe;
124
+ }
125
+ }
126
+ return null;
127
+ }
128
+ if (isPlainObject(value)) {
129
+ const keys = Reflect.ownKeys(value);
130
+ for (const key of keys) {
131
+ if (typeof key !== "string") {
132
+ return key;
133
+ }
134
+ const unsafe = jsonUnsafe(value[key]);
135
+ if (unsafe !== null) {
136
+ return unsafe;
137
+ }
138
+ }
139
+ return null;
140
+ }
141
+ return value;
142
+ };
143
+ export const verifyJson = (args, mode) => {
144
+ if (mode === "none") {
145
+ return;
146
+ }
147
+ const unsafe = jsonUnsafe(args);
148
+ if (unsafe === null) {
149
+ return;
150
+ }
151
+ const message = "Job arguments must be native JSON types, " +
152
+ `but ${String(unsafe)} is a ${typeof unsafe}. ` +
153
+ "See https://github.com/sidekiq/sidekiq/wiki/Best-Practices";
154
+ if (mode === "raise") {
155
+ throw new Error(message);
156
+ }
157
+ console.warn(message);
158
+ };
package/dist/job.d.ts ADDED
@@ -0,0 +1,73 @@
1
+ import type { BulkOptions, JobOptions, JobPayload, JobSetterOptions } from "./types.js";
2
+ export type JobConstructor<TArgs extends unknown[] = unknown[]> = {
3
+ new (): Job<TArgs>;
4
+ name: string;
5
+ sidekiqOptions?: JobOptions;
6
+ sidekiqRetryIn?: RetryInHandler;
7
+ sidekiqRetriesExhausted?: RetriesExhaustedHandler;
8
+ getSidekiqOptions(): JobOptions;
9
+ setSidekiqOptions(options: JobOptions): void;
10
+ queueAs(queue: string): void;
11
+ retryIn(handler: RetryInHandler): void;
12
+ retriesExhausted(handler: RetriesExhaustedHandler): void;
13
+ set(options: JobSetterOptions): JobSetter<TArgs>;
14
+ performAsync(...args: TArgs): Promise<string | null>;
15
+ performIn(interval: number, ...args: TArgs): Promise<string | null>;
16
+ performAt(timestamp: number, ...args: TArgs): Promise<string | null>;
17
+ performBulk(args: TArgs[], options?: BulkOptions): Promise<(string | null)[]>;
18
+ performInline(...args: TArgs): Promise<boolean | null>;
19
+ jobs(): JobPayload[];
20
+ clear(): void;
21
+ drain(): Promise<void>;
22
+ performOne(): Promise<void>;
23
+ processJob(payload: JobPayload): Promise<void>;
24
+ clientPush(item: JobPayload): Promise<string | null>;
25
+ };
26
+ type JobClass = abstract new () => Job<any>;
27
+ type JobArgsFromClass<T> = T extends {
28
+ new (): Job<infer TArgs>;
29
+ } ? TArgs : unknown[];
30
+ export declare abstract class Job<TArgs extends unknown[] = unknown[]> {
31
+ jid?: string;
32
+ _context?: {
33
+ stopping: () => boolean;
34
+ };
35
+ static getSidekiqOptions(this: JobConstructor): JobOptions;
36
+ static setSidekiqOptions(this: JobConstructor, options: JobOptions): void;
37
+ static queueAs(this: JobConstructor, queue: string): void;
38
+ static retryIn(this: JobConstructor, handler: RetryInHandler): void;
39
+ static retriesExhausted(this: JobConstructor, handler: RetriesExhaustedHandler): void;
40
+ static set<TClass extends JobClass>(this: TClass, options: JobSetterOptions): JobSetter<JobArgsFromClass<TClass>>;
41
+ static performAsync<TClass extends JobClass>(this: TClass, ...args: JobArgsFromClass<TClass>): Promise<string | null>;
42
+ static performIn<TClass extends JobClass>(this: TClass, interval: number, ...args: JobArgsFromClass<TClass>): Promise<string | null>;
43
+ static performAt<TClass extends JobClass>(this: TClass, timestamp: number, ...args: JobArgsFromClass<TClass>): Promise<string | null>;
44
+ static performBulk<TClass extends JobClass>(this: TClass, args: JobArgsFromClass<TClass>[], options?: BulkOptions): Promise<(string | null)[]>;
45
+ static performInline<TClass extends JobClass>(this: TClass, ...args: JobArgsFromClass<TClass>): Promise<boolean | null>;
46
+ static jobs(this: JobConstructor | typeof Job): JobPayload[];
47
+ static clear(this: JobConstructor): void;
48
+ static drain(this: JobConstructor): Promise<void>;
49
+ static performOne(this: JobConstructor): Promise<void>;
50
+ static processJob(this: JobConstructor, payload: JobPayload): Promise<void>;
51
+ static clearAll(): void;
52
+ static drainAll(): Promise<void>;
53
+ static clientPush(this: JobConstructor, item: JobPayload): Promise<string | null>;
54
+ logger(): import("./logger.js").Logger;
55
+ interrupted(): boolean;
56
+ abstract perform(...args: TArgs): Promise<void> | void;
57
+ }
58
+ export type RetryInHandler = (count: number, error: Error, payload: JobPayload) => number | "discard" | "kill" | "default";
59
+ export type RetriesExhaustedHandler = (payload: JobPayload, error: Error) => "discard" | undefined;
60
+ export declare class JobSetter<TArgs extends unknown[] = unknown[]> {
61
+ private readonly klass;
62
+ private options;
63
+ constructor(klass: JobConstructor<TArgs>, options: JobSetterOptions);
64
+ set(options: JobSetterOptions): JobSetter<TArgs>;
65
+ performAsync(...args: TArgs): Promise<string | null>;
66
+ performInline(...args: TArgs): Promise<boolean | null>;
67
+ performBulk(args: TArgs[], options?: BulkOptions): Promise<(string | null)[]>;
68
+ performIn(interval: number, ...args: TArgs): Promise<string | null>;
69
+ performAt(timestamp: number, ...args: TArgs): Promise<string | null>;
70
+ private at;
71
+ }
72
+ export {};
73
+ //# sourceMappingURL=job.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job.d.ts","sourceRoot":"","sources":["../src/job.ts"],"names":[],"mappings":"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"}